AI-Powered Aerial Photography
Drone photography has evolved from manual capture to AI-assisted cinematography. Intelligent systems can automatically frame shots, track subjects, and execute complex cinematic movements that would be impossible for human pilots.
The AI Photography System
Real-World Analogy
Think of an AI drone camera as a "virtual cinematographer." Just as a Hollywood director uses dolly shots, crane movements, and tracking shots to tell a story, AI can plan and execute cinematic flight paths that create professional-quality footage.
SVG: Cinematic Flight Paths and Framing Rules
Auto-Framing System
from typing import Tuple, List, Dict
from dataclasses import dataclass
from enum import Enum
class FramingRule(Enum):
RULE_OF_THIRDS = "rule_of_thirds"
CENTER = "center"
LEADING_LINES = "leading_lines"
SYMMETRY = "symmetry"
GOLDEN_RATIO = "golden_ratio"
@dataclass
class Frame:
width: int
height: int
subject_position: Tuple[float, float] # Normalized 0-1
rule: FramingRule
class AutoFramer:
def __init__(self, camera_fov: Tuple[float, float] = (60, 45)):
self.h_fov, self.v_fov = camera_fov
self.frame_rules = {
FramingRule.RULE_OF_THIRDS: self._rule_of_thirds,
FramingRule.CENTER: self._center_frame,
FramingRule.GOLDEN_RATIO: self._golden_ratio
}
def compute_framing(self, subject_position: np.ndarray,
drone_position: np.ndarray,
rule: FramingRule = FramingRule.RULE_OF_THIRDS) -> Dict:
"""Compute camera framing to place subject according to rule"""
# Calculate subject position in frame
subject_frame_pos = self._project_to_frame(
subject_position, drone_position)
# Get target position based on rule
target_pos = self.frame_rules[rule]()
# Calculate required drone movement
offset = target_pos - subject_frame_pos
# Convert frame offset to world coordinates
world_offset = self._frame_to_world_offset(offset)
return {
'current_subject_pos': subject_frame_pos.tolist(),
'target_subject_pos': target_pos.tolist(),
'required_drone_offset': world_offset.tolist(),
'rule_applied': rule.value
}
def _project_to_frame(self, subject: np.ndarray,
drone_pos: np.ndarray) -> np.ndarray:
"""Project world position to normalized frame coordinates"""
# Vector from drone to subject
to_subject = subject - drone_pos
# Simple projection (would use camera intrinsics in production)
# Normalize by field of view
x_norm = np.arctan2(to_subject[0], to_subject[2]) / np.radians(self.h_fov) + 0.5
y_norm = np.arctan2(to_subject[1], to_subject[2]) / np.radians(self.v_fov) + 0.5
return np.array([x_norm, y_norm])
def _rule_of_thirds(self) -> np.ndarray:
"""Return optimal position for rule of thirds"""
# Intersection point (upper right third)
return np.array([2/3, 1/3])
def _center_frame(self) -> np.ndarray:
return np.array([0.5, 0.5])
def _golden_ratio(self) -> np.ndarray:
phi = 1.618
return np.array([1/phi, 1/phi])
def _frame_to_world_offset(self, frame_offset: np.ndarray) -> np.ndarray:
"""Convert frame offset to world coordinate offset"""
# Simplified - would use proper camera model in production
world_x = frame_offset[0] * 10 # Scale factor
world_y = frame_offset[1] * 10
return np.array([world_x, world_y, 0])
def analyze_composition(self, image: np.ndarray,
subject_bbox: Tuple[int, int, int, int]) -> Dict:
"""Analyze current composition quality"""
h, w = image.shape[:2]
# Subject position
sx = (subject_bbox[0] + subject_bbox[2]) / 2 / w
sy = (subject_bbox[1] + subject_bbox[3]) / 2 / h
# Distance from rule of thirds points
thirds_points = [(1/3, 1/3), (2/3, 1/3),
(1/3, 2/3), (2/3, 2/3)]
min_dist = min(
np.sqrt((sx - px)**2 + (sy - py)**2)
for px, py in thirds_points
)
# Composition score (lower is better)
composition_score = 1 - min(min_dist * 2, 1)
return {
'subject_position': (sx, sy),
'composition_score': composition_score,
'recommended_rule': 'rule_of_thirds' if composition_score < 0.7 else 'center',
'suggestions': self._get_suggestions(sx, sy)
}
def _get_suggestions(self, sx: float, sy: float) -> List[str]:
suggestions = []
if sx < 0.3:
suggestions.append("Move subject to the right")
elif sx > 0.7:
suggestions.append("Move subject to the left")
if sy < 0.3:
suggestions.append("Move subject down")
elif sy > 0.7:
suggestions.append("Move subject up")
return suggestions
Cinematic Shot Planning
from typing import List, Dict, Tuple
from dataclasses import dataclass
from enum import Enum
class ShotType(Enum):
ORBIT = "orbit"
DOLLY_ZOOM = "dolly_zoom"
REVEAL = "reveal"
FLYBY = "flyby"
PULLBACK = "pullback"
HOVER_PAN = "hover_pan"
@dataclass
class CinematicShot:
shot_type: ShotType
subject_position: np.ndarray
duration: float # seconds
altitude: float
parameters: Dict
class CinematicPlanner:
def __init__(self, drone_speed: float = 10.0):
self.max_speed = drone_speed
self.shot_generators = {
ShotType.ORBIT: self._generate_orbit,
ShotType.DOLLY_ZOOM: self._generate_dolly_zoom,
ShotType.REVEAL: self._generate_reveal,
ShotType.FLYBY: self._generate_flyby,
ShotType.PULLBACK: self._generate_pullback,
}
def plan_shot(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate waypoints for cinematic shot"""
return self.shot_generators[shot.shot_type](shot)
def _generate_orbit(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate orbit shot waypoints"""
waypoints = []
n_points = int(shot.duration * 10) # 10 points per second
radius = shot.parameters.get('radius', 20)
altitude = shot.altitude
for i in range(n_points):
angle = (i / n_points) * 2 * np.pi
x = shot.subject_position[0] + radius * np.cos(angle)
y = shot.subject_position[1] + radius * np.sin(angle)
waypoints.append(np.array([x, y, altitude]))
return waypoints
def _generate_dolly_zoom(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate dolly zoom (vertigo effect) waypoints"""
waypoints = []
n_points = int(shot.duration * 5)
start_distance = shot.parameters.get('start_distance', 50)
end_distance = shot.parameters.get('end_distance', 10)
direction = shot.parameters.get('direction', np.array([0, 1, 0]))
direction = direction / np.linalg.norm(direction)
for i in range(n_points):
t = i / (n_points - 1)
distance = start_distance + t * (end_distance - start_distance)
position = shot.subject_position + direction * distance
position[2] = shot.altitude
waypoints.append(position)
return waypoints
def _generate_reveal(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate reveal shot (subject appears from behind obstacle)"""
waypoints = []
obstacle_offset = shot.parameters.get('obstacle_offset', np.array([-30, 0, 0]))
# Start behind obstacle
start_pos = shot.subject_position + obstacle_offset
start_pos[2] = shot.altitude
waypoints.append(start_pos)
# Rise to reveal
reveal_pos = shot.subject_position.copy()
reveal_pos[2] = shot.altitude + 10
waypoints.append(reveal_pos)
# Move to final framing position
final_offset = shot.parameters.get('final_offset', np.array([0, -20, 0]))
final_pos = shot.subject_position + final_offset
final_pos[2] = shot.altitude
waypoints.append(final_pos)
return waypoints
def _generate_flyby(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate flyby shot"""
waypoints = []
n_points = int(shot.duration * 5)
start_offset = shot.parameters.get('start_offset', np.array([-40, 0, 0]))
end_offset = shot.parameters.get('end_offset', np.array([40, 0, 0]))
for i in range(n_points):
t = i / (n_points - 1)
offset = start_offset + t * (end_offset - start_offset)
position = shot.subject_position + offset
position[2] = shot.altitude
waypoints.append(position)
return waypoints
def _generate_pullback(self, shot: CinematicShot) -> List[np.ndarray]:
"""Generate pullback shot (start close, pull away)"""
waypoints = []
n_points = int(shot.duration * 5)
start_distance = shot.parameters.get('start_distance', 5)
end_distance = shot.parameters.get('end_distance', 60)
direction = shot.parameters.get('direction', np.array([0, -1, 0]))
direction = direction / np.linalg.norm(direction)
for i in range(n_points):
t = i / (n_points - 1)
distance = start_distance + t * (end_distance - start_distance)
altitude = shot.altitude + t * 20 # Rise as we pull back
position = shot.subject_position + direction * distance
position[2] = altitude
waypoints.append(position)
return waypoints
def plan_sequence(self, shots: List[CinematicShot]) -> List[np.ndarray]:
"""Plan a complete shot sequence with transitions"""
all_waypoints = []
for i, shot in enumerate(shots):
shot_waypoints = self.plan_shot(shot)
# Add transition from previous shot
if all_waypoints and i > 0:
transition = self._plan_transition(
all_waypoints[-1], shot_waypoints[0])
all_waypoints.extend(transition)
all_waypoints.extend(shot_waypoints)
return all_waypoints
def _plan_transition(self, start: np.ndarray,
end: np.ndarray) -> List[np.ndarray]:
"""Plan smooth transition between shots"""
n_points = 20
transition = []
for i in range(n_points):
t = i / (n_points - 1)
# Smooth easing (ease-in-out)
t_ease = 3 * t**2 - 2 * t**3
point = start + t_ease * (end - start)
transition.append(point)
return transition
Subject Tracking System
from typing import Tuple, List, Optional, Dict
from dataclasses import dataclass
from enum import Enum
class TrackingMode(Enum):
FOLLOW = "follow"
LEAD = "lead"
ORBIT = "orbit"
PARALLEL = "parallel"
STATIC_FRAMING = "static_framing"
@dataclass
class TrackingState:
subject_id: str
subject_position: np.ndarray
subject_velocity: np.ndarray
confidence: float
frame_position: Tuple[float, float]
class SubjectTracker:
def __init__(self, camera_fov: Tuple[float, float] = (60, 45)):
self.h_fov, self.v_fov = camera_fov
self.tracked_subjects: Dict[str, TrackingState] = {}
self.tracking_history: Dict[str, List[np.ndarray]] = {}
def initialize_tracking(self, subject_id: str,
initial_position: np.ndarray,
mode: TrackingMode = TrackingMode.FOLLOW):
"""Initialize tracking for a subject"""
self.tracked_subjects[subject_id] = TrackingState(
subject_id=subject_id,
subject_position=initial_position,
subject_velocity=np.zeros(3),
confidence=1.0,
frame_position=(0.5, 0.5)
)
self.tracking_history[subject_id] = [initial_position.copy()]
def update_tracking(self, subject_id: str,
detected_position: np.ndarray,
dt: float = 0.1) -> Dict:
"""Update tracking with new detection"""
if subject_id not in self.tracked_subjects:
self.initialize_tracking(subject_id, detected_position)
return self._get_tracking_command(subject_id)
state = self.tracked_subjects[subject_id]
# Update velocity (exponential moving average)
new_velocity = (detected_position - state.subject_position) / dt
state.subject_velocity = 0.7 * state.subject_velocity + 0.3 * new_velocity
# Update position
state.subject_position = detected_position
state.confidence = 0.9 # Would be from detector
# Store history
self.tracking_history[subject_id].append(detected_position.copy())
if len(self.tracking_history[subject_id]) > 100:
self.tracking_history[subject_id].pop(0)
return self._get_tracking_command(subject_id)
def _get_tracking_command(self, subject_id: str) -> Dict:
"""Get camera/drone command for tracking"""
state = self.tracked_subjects[subject_id]
return {
'subject_id': subject_id,
'subject_position': state.subject_position.tolist(),
'subject_velocity': state.subject_velocity.tolist(),
'confidence': state.confidence,
'track_id': subject_id
}
def predict_subject_position(self, subject_id: str,
time_ahead: float) -> np.ndarray:
"""Predict future subject position"""
if subject_id not in self.tracked_subjects:
return np.zeros(3)
state = self.tracked_subjects[subject_id]
# Simple linear prediction
future_pos = state.subject_position + state.subject_velocity * time_ahead
# Add acceleration if we have enough history
history = self.tracked_history[subject_id]
if len(history) >= 3:
# Estimate acceleration
v1 = (history[-2] - history[-3]) / 0.1
v2 = (history[-1] - history[-2]) / 0.1
acceleration = (v2 - v1) / 0.1
future_pos += 0.5 * acceleration * time_ahead**2
return future_pos
def get_tracking_quality(self, subject_id: str) -> Dict:
"""Assess tracking quality"""
if subject_id not in self.tracked_subjects:
return {'quality': 0, 'issues': ['Subject not tracked']}
state = self.tracked_subjects[subject_id]
history = self.tracking_history[subject_id]
issues = []
# Check confidence
if state.confidence < 0.5:
issues.append("Low detection confidence")
# Check if subject is near frame edge
fx, fy = state.frame_position
if fx < 0.1 or fx > 0.9 or fy < 0.1 or fy > 0.9:
issues.append("Subject near frame edge")
# Check for erratic movement
if len(history) >= 5:
recent = np.array(history[-5:])
velocity_changes = np.diff(recent, axis=0)
if np.std(velocity_changes) > 5:
issues.append("Erratic subject movement")
quality = max(0, 1 - len(issues) * 0.2)
return {
'quality': quality,
'issues': issues,
'confidence': state.confidence,
'position_in_frame': state.frame_position
}
Scene Composition Analysis
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class CompositionElement:
element_type: str
position: Tuple[int, int]
size: Tuple[int, int]
visual_weight: float
color_dominance: float
class CompositionAnalyzer:
def __init__(self):
self.rule_of_thirds_points = [
(1/3, 1/3), (2/3, 1/3),
(1/3, 2/3), (2/3, 2/3)
]
def analyze_image(self, image: np.ndarray) -> Dict:
"""Complete composition analysis"""
h, w = image.shape[:2]
return {
'brightness_analysis': self._analyze_brightness(image),
'color_analysis': self._analyze_colors(image),
'balance_score': self._calculate_balance(image),
'depth_estimation': self._estimate_depth(image),
'focal_points': self._find_focal_points(image),
'rule_of_thirds_score': self._score_rule_of_thirds(image)
}
def _analyze_brightness(self, image: np.ndarray) -> Dict:
"""Analyze brightness distribution"""
gray = np.mean(image, axis=2)
return {
'mean': float(np.mean(gray)),
'std': float(np.std(gray)),
'histogram': np.histogram(gray, bins=10)[0].tolist()
}
def _analyze_colors(self, image: np.ndarray) -> Dict:
"""Analyze color distribution"""
# Average color in regions
h, w = image.shape[:2]
regions = {
'top_left': np.mean(image[:h//2, :w//2], axis=(0,1)),
'top_right': np.mean(image[:h//2, w//2:], axis=(0,1)),
'bottom_left': np.mean(image[h//2:, :w//2], axis=(0,1)),
'bottom_right': np.mean(image[h//2:, w//2:], axis=(0,1))
}
# Overall color temperature
avg_color = np.mean(image.reshape(-1, 3), axis=0)
warmth = avg_color[0] - avg_color[2] # R - B
return {
'regions': {k: v.tolist() for k, v in regions.items()},
'color_temperature': 'warm' if warmth > 10 else 'cool',
'dominant_hue': float(np.argmax(avg_color))
}
def _calculate_balance(self, image: np.ndarray) -> float:
"""Calculate visual balance score"""
gray = np.mean(image, axis=2)
h, w = gray.shape
# Weight distribution
left_weight = np.mean(gray[:, :w//2])
right_weight = np.mean(gray[:, w//2:])
top_weight = np.mean(gray[:h//2, :])
bottom_weight = np.mean(gray[h//2:, :])
horizontal_balance = 1 - abs(left_weight - right_weight) / 255
vertical_balance = 1 - abs(top_weight - bottom_weight) / 255
return (horizontal_balance + vertical_balance) / 2
def _estimate_depth(self, image: np.ndarray) -> np.ndarray:
"""Simple depth estimation from single image"""
# Simplified - would use ML model in production
gray = np.mean(image, axis=2)
h, w = gray.shape
# Gradient-based depth approximation
y_gradient = np.gradient(gray, axis=0)
depth_approx = np.cumsum(np.abs(y_gradient), axis=0)
# Normalize
depth_approx = depth_approx / np.max(depth_approx)
return depth_approx
def _find_focal_points(self, image: np.ndarray) -> List[Tuple[int, int]]:
"""Find potential focal points"""
gray = np.mean(image, axis=2)
h, w = gray.shape
# Simple edge-based focal point detection
from scipy import ndimage
edges = ndimage.sobel(gray)
# Find high-contrast regions
focal_points = []
block_size = 50
for y in range(0, h - block_size, block_size // 2):
for x in range(0, w - block_size, block_size // 2):
block = edges[y:y+block_size, x:x+block_size]
contrast = np.std(block)
if contrast > 30:
focal_points.append((x + block_size // 2, y + block_size // 2))
# Non-maximum suppression
return self._non_max_suppression(focal_points, 30)
def _non_max_suppression(self, points: List[Tuple[int, int]],
min_distance: int) -> List[Tuple[int, int]]:
"""Simple non-maximum suppression"""
if not points:
return []
selected = [points[0]]
for point in points[1:]:
if all(np.sqrt((point[0]-s[0])**2 + (point[1]-s[1])**2) > min_distance
for s in selected):
selected.append(point)
return selected
def _score_rule_of_thirds(self, image: np.ndarray) -> float:
"""Score how well image follows rule of thirds"""
gray = np.mean(image, axis=2)
h, w = gray.shape
# Find strongest edges
from scipy import ndimage
edges = ndimage.sobel(gray)
# Check alignment with rule of thirds lines
thirds_x = [w//3, 2*w//3]
thirds_y = [h//3, 2*h//3]
score = 0
for x in thirds_x:
column = edges[:, max(0, x-5):min(w, x+5)]
score += np.mean(column)
for y in thirds_y:
row = edges[max(0, y-5):min(h, y+5), :]
score += np.mean(row)
return min(1.0, score / 200)
Hands-On Project: Complete Aerial Photography System
# aerial_photography_system.py
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum
class ShootMode(Enum):
AUTOMATIC = "automatic"
SEMI_AUTO = "semi_auto"
MANUAL = "manual"
@dataclass
class ShootPlan:
location: str
subject: str
shots: List[CinematicShot]
mode: ShootMode
duration_estimate: float
class AerialPhotographySystem:
def __init__(self):
self.framer = AutoFramer()
self.planner = CinematicPlanner(drone_speed=8.0)
self.tracker = SubjectTracker()
self.analyzer = CompositionAnalyzer()
def create_shoot_plan(self, location_data: Dict,
subject_data: Dict) -> ShootPlan:
"""Create comprehensive shoot plan"""
subject_pos = np.array(subject_data['position'])
altitude = subject_data.get('altitude', 30)
# Define shot sequence
shots = [
CinematicShot(
shot_type=ShotType.REVEAL,
subject_position=subject_pos,
duration=8.0,
altitude=altitude,
parameters={
'obstacle_offset': np.array([-40, 0, 0]),
'final_offset': np.array([0, -25, 0])
}
),
CinematicShot(
shot_type=ShotType.ORBIT,
subject_position=subject_pos,
duration=12.0,
altitude=altitude,
parameters={'radius': 25}
),
CinematicShot(
shot_type=ShotType.PULLBACK,
subject_position=subject_pos,
duration=10.0,
altitude=altitude,
parameters={
'start_distance': 10,
'end_distance': 50
}
)
]
# Estimate duration
total_duration = sum(s.duration for s in shots) + len(shots) * 5
return ShootPlan(
location=location_data.get('name', 'Unknown'),
subject=subject_data.get('name', 'Subject'),
shots=shots,
mode=ShootMode.AUTOMATIC,
duration_estimate=total_duration
def execute_shoot_plan(self, plan: ShootPlan) -> Dict:
"""Execute the shoot plan"""
print(f"Executing shoot plan: {plan.location}")
print(f"Subject: {plan.subject}")
print(f"Planned shots: {len(plan.shots)}")
# Generate all waypoints
all_waypoints = self.planner.plan_sequence(plan.shots)
print(f"Total waypoints: {len(all_waypoints)}")
print(f"Estimated duration: {plan.duration_estimate:.0f} seconds")
return {
'waypoints': [wp.tolist() for wp in all_waypoints],
'total_waypoints': len(all_waypoints),
'estimated_time': plan.duration_estimate,
'shots_planned': len(plan.shots)
}
def analyze_shot(self, image: np.ndarray,
subject_bbox: Optional[Tuple] = None) -> Dict:
"""Analyze a captured shot"""
composition = self.analyzer.analyze_image(image)
analysis = {
'composition': composition,
'quality_score': self._calculate_quality_score(composition),
'suggestions': []
}
if subject_bbox:
framing = self.framer.analyze_composition(image, subject_bbox)
analysis['framing'] = framing
analysis['suggestions'].extend(framing.get('suggestions', []))
return analysis
def _calculate_quality_score(self, composition: Dict) -> float:
"""Calculate overall quality score"""
scores = [
composition.get('balance_score', 0.5),
composition.get('rule_of_thirds_score', 0.5),
]
return np.mean(scores)
def get_shoot_recommendations(self, location: str,
time_of_day: str) -> Dict:
"""Get recommendations for optimal shooting"""
recommendations = {
'best_times': ['golden_hour', 'blue_hour'],
'suggested_altitudes': [30, 50, 80],
'recommended_shots': [
ShotType.REVEAL,
ShotType.ORBIT,
ShotType.PULLBACK
],
'weather_considerations': [
'Wind speed < 10 mph',
'No precipitation',
'Good visibility'
]
}
if time_of_day == 'midday':
recommendations['warnings'] = [
'Harsh shadows',
'High contrast',
'Consider ND filters'
]
return recommendations
# Usage
system = AerialPhotographySystem()
# Create shoot plan
location = {
'name': 'Downtown Skyline',
'coordinates': (40.7128, -74.0060)
}
subject = {
'name': 'Main Building',
'position': [100, 50, 0],
'altitude': 40
}
plan = system.create_shoot_plan(location, subject)
result = system.execute_shoot_plan(plan)
# Analyze a captured frame
sample_image = np.random.randint(0, 255, (1080, 1920, 3), dtype=np.uint8)
analysis = system.analyze_shot(sample_image, subject_bbox=(800, 400, 1100, 700))
print(f"\nShot Analysis:")
print(f"Quality Score: {analysis['quality_score']:.2f}")
print(f"Suggestions: {analysis['suggestions']}")
Exercise
- Implement dynamic framing that adjusts composition based on subject movement
- Create a shot list generator from a storyboard description
- Build a multi-camera system that switches between angles automatically
- Design a color grading assistant that recommends post-processing settings
âšī¸
AI-powered drone cinematography is used in major film productions, real estate marketing, and sports broadcasting. DJI's ActiveTrack and Skydio's autonomous tracking are industry leaders in this space.