AI-Powered Search and Rescue Drones
Every minute counts in search and rescue operations. AI-powered drones can scan vast areas, detect survivors through thermal imaging, and relay critical information to rescue teams—dramatically improving survival rates.
The SAR Drone System
Real-World Analogy
Think of SAR drones as "electronic bloodhounds." Just as a search dog uses its keen sense of smell to track a missing person, SAR drones use thermal cameras and AI to detect body heat and movement patterns from the air.
SVG: Search Patterns and Coverage Algorithm
Search Pattern Algorithms
Common Search Patterns
from typing import List, Tuple
from enum import Enum
class SearchPattern(Enum):
GRID = "grid"
SPIRAL = "spiral"
EXPANDING_SQUARE = "expanding_square"
SECTOR = "sector"
PARALLEL_TRACKS = "parallel_tracks"
class SearchPlanner:
def __init__(self, search_area: Tuple[float, float],
drone_speed: float = 10.0,
sensor_width: float = 50.0):
self.width, self.height = search_area
self.drone_speed = drone_speed
self.sensor_width = sensor_width
def generate_grid_pattern(self, overlap: float = 0.3) -> List[np.ndarray]:
"""Generate parallel track search pattern"""
track_spacing = self.sensor_width * (1 - overlap)
n_tracks = int(np.ceil(self.height / track_spacing))
waypoints = []
for i in range(n_tracks):
y = i * track_spacing + track_spacing / 2
if i % 2 == 0:
waypoints.append(np.array([0, y]))
waypoints.append(np.array([self.width, y]))
else:
waypoints.append(np.array([self.width, y]))
waypoints.append(np.array([0, y]))
return waypoints
def generate_spiral_pattern(self, center: np.ndarray,
max_radius: float) -> List[np.ndarray]:
"""Generate Archimedean spiral search pattern"""
waypoints = []
turns = max_radius / self.sensor_width * 2
points_per_turn = 36
for i in range(int(turns * points_per_turn)):
angle = i * 2 * np.pi / points_per_turn
radius = (i / points_per_turn) * self.sensor_width / 2
x = center[0] + radius * np.cos(angle)
y = center[1] + radius * np.sin(angle)
waypoints.append(np.array([x, y]))
return waypoints
def generate_expanding_square(self, center: np.ndarray,
last_known: np.ndarray) -> List[np.ndarray]:
"""Generate expanding square from last known position"""
waypoints = [last_known.copy()]
direction = 0 # 0=right, 1=up, 2=left, 3=down
segment_length = self.sensor_width
segments_per_side = 1
for side in range(8): # 8 sides
for segment in range(segments_per_side):
if direction == 0:
last_known[0] += segment_length
elif direction == 1:
last_known[1] += segment_length
elif direction == 2:
last_known[0] -= segment_length
else:
last_known[1] -= segment_length
waypoints.append(last_known.copy())
direction = (direction + 1) % 4
if side % 2 == 1:
segments_per_side += 1
return waypoints
def estimate_search_time(self, waypoints: List[np.ndarray]) -> float:
"""Estimate total search time in minutes"""
total_distance = sum(
np.linalg.norm(waypoints[i+1] - waypoints[i])
for i in range(len(waypoints)-1)
)
return total_distance / self.drone_speed / 60
def calculate_coverage(self, waypoints: List[np.ndarray],
time_elapsed: float) -> float:
"""Calculate percentage of area covered"""
total_distance = self.drone_speed * time_elapsed * 60
covered_area = total_distance * self.sensor_width
total_area = self.width * self.height
return min(100, covered_area / total_area * 100)
Victim Detection System
from typing import List, Dict, Optional
from dataclasses import dataclass
from enum import Enum
class ThreatLevel(Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
@dataclass
class Detection:
position: np.ndarray
confidence: float
detection_type: str
threat_level: ThreatLevel
timestamp: float
class VictimDetector:
def __init__(self, confidence_threshold: float = 0.7):
self.confidence_threshold = confidence_threshold
self.detections: List[Detection] = []
def analyze_thermal_frame(self, thermal_data: np.ndarray,
temperature_range: Tuple[float, float] = (30, 40)) -> List[Detection]:
"""Analyze thermal image for human heat signatures"""
detections = []
# Human body temperature range
min_temp, max_temp = temperature_range
# Find pixels in human temperature range
mask = (thermal_data >= min_temp) & (thermal_data <= max_temp)
# Connected component analysis
from scipy import ndimage
labeled, num_features = ndimage.label(mask)
for i in range(1, num_features + 1):
component = labeled == i
area = np.sum(component)
# Filter by size (humans have specific size range)
if 50 < area < 5000: # Adjust based on altitude
centroid = ndimage.center_of_mass(component)
max_temp_in_region = np.max(thermal_data[component])
# Calculate confidence based on temperature and size
temp_score = (max_temp_in_region - min_temp) / (max_temp - min_temp)
size_score = min(1.0, area / 500)
confidence = (temp_score * 0.6 + size_score * 0.4)
if confidence >= self.confidence_threshold:
detections.append(Detection(
position=np.array(centroid),
confidence=confidence,
detection_type="thermal_human",
threat_level=self._assess_threat(confidence),
timestamp=0
))
return detections
def analyze_visual_frame(self, rgb_frame: np.ndarray) -> List[Detection]:
"""Analyze RGB image for visual signs of victims"""
detections = []
# Color-based detection (bright clothing, etc.)
hsv = self._rgb_to_hsv(rgb_frame)
# Detect bright/saturated colors (survivor clothing)
bright_mask = (hsv[:,:,2] > 200) & (hsv[:,:,1] > 100)
# Detect movement if video available
# motion_mask = self._detect_motion(prev_frame, rgb_frame)
from scipy import ndimage
labeled, num_features = ndimage.label(bright_mask)
for i in range(1, num_features + 1):
component = labeled == i
area = np.sum(component)
if 100 < area < 10000:
centroid = ndimage.center_of_mass(component)
detections.append(Detection(
position=np.array(centroid),
confidence=0.6,
detection_type="visual_clothing",
threat_level=ThreatLevel.MEDIUM,
timestamp=0
))
return detections
def _assess_threat(self, confidence: float) -> ThreatLevel:
if confidence > 0.9:
return ThreatLevel.CRITICAL
elif confidence > 0.8:
return ThreatLevel.HIGH
elif confidence > 0.7:
return ThreatLevel.MEDIUM
return ThreatLevel.LOW
def _rgb_to_hsv(self, rgb: np.ndarray) -> np.ndarray:
"""Convert RGB to HSV color space"""
# Simplified HSV conversion
r, g, b = rgb[:,:,0]/255.0, rgb[:,:,1]/255.0, rgb[:,:,2]/255.0
max_c = np.maximum(np.maximum(r, g), b)
min_c = np.minimum(np.minimum(r, g), b)
v = max_c
s = np.zeros_like(v)
mask = max_c > 0
s[mask] = (max_c[mask] - min_c[mask]) / max_c[mask]
h = np.zeros_like(v)
mask = (max_c - min_c) > 0
h[mask & (max_c == r)] = 60 * ((g[mask & (max_c == r)] - b[mask & (max_c == r)]) / (max_c[mask & (max_c == r)] - min_c[mask & (max_c == r)]))
h[mask & (max_c == g)] = 120 + 60 * ((b[mask & (max_c == g)] - r[mask & (max_c == g)]) / (max_c[mask & (max_c == g)] - min_c[mask & (max_c == g)]))
h[mask & (max_c == b)] = 240 + 60 * ((r[mask & (max_c == b)] - g[mask & (max_c == b)]) / (max_c[mask & (max_c == b)] - min_c[mask & (max_c == b)]))
return np.stack([h, s * 255, v * 255], axis=2)
def merge_detections(self, detections: List[Detection],
max_distance: float = 30) -> List[Detection]:
"""Merge nearby detections"""
if not detections:
return []
merged = []
used = set()
for i, d1 in enumerate(detections):
if i in used:
continue
cluster = [d1]
for j, d2 in enumerate(detections[i+1:], i+1):
if j in used:
continue
if np.linalg.norm(d1.position - d2.position) < max_distance:
cluster.append(d2)
used.add(j)
# Merge cluster
avg_pos = np.mean([d.position for d in cluster], axis=0)
max_conf = max(d.confidence for d in cluster)
merged.append(Detection(
position=avg_pos,
confidence=max_conf,
detection_type="merged",
threat_level=max(cluster, key=lambda d: list(ThreatLevel).index(d.threat_level)).threat_level,
timestamp=d1.timestamp
))
used.add(i)
return merged
Coverage Algorithm
from typing import List, Set, Tuple
class CoveragePlanner:
def __init__(self, grid_size: Tuple[int, int] = (100, 100)):
self.rows, self.cols = grid_size
self.probability_map = np.ones(grid_size) / (grid_size[0] * grid_size[1])
def update_probability(self, sensor_pos: np.ndarray,
sensor_range: float,
detected: bool):
"""Update probability map based on sensor reading"""
for i in range(self.rows):
for j in range(self.cols):
cell_pos = np.array([i, j])
dist = np.linalg.norm(cell_pos - sensor_pos)
if dist < sensor_range:
if detected:
# Increase probability near sensor
self.probability_map[i, j] *= 1.5
else:
# Decrease probability in sensed area
self.probability_map[i, j] *= 0.1
# Normalize
self.probability_map /= self.probability_map.sum()
def get_next_target(self, current_pos: np.ndarray,
drone_range: float) -> np.ndarray:
"""Get next search target based on probability and distance"""
best_target = None
best_score = -float('inf')
for i in range(self.rows):
for j in range(self.cols):
cell_pos = np.array([i, j])
prob = self.probability_map[i, j]
# Score combines probability and distance cost
dist = np.linalg.norm(cell_pos - current_pos)
score = prob / (dist + 1) # +1 to avoid division by zero
if score > best_score:
best_score = score
best_target = cell_pos
return best_target
def calculate_expected_time(self, drone_speed: float) -> float:
"""Calculate expected time to find target"""
# Based on probability distribution
cumulative_prob = 0
time = 0
sorted_probs = np.sort(self.probability_map.flatten())[::-1]
for prob in sorted_probs:
cumulative_prob += prob
time += 1
if cumulative_prob > 0.9:
break
return time * 10 / drone_speed # Rough estimate
def visualize_probability(self):
"""Return probability map for visualization"""
return self.probability_map
Communication Relay System
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class RelayNode:
drone_id: str
position: np.ndarray
battery_level: float
signal_strength: float
class CommunicationRelay:
def __init__(self, base_station: np.ndarray, max_range: float = 500):
self.base_station = base_station
self.max_range = max_range
self.relay_nodes: List[RelayNode] = []
def add_relay(self, drone_id: str, position: np.ndarray,
battery: float = 100):
"""Add a relay node"""
self.relay_nodes.append(RelayNode(
drone_id=drone_id,
position=position,
battery_level=battery,
signal_strength=1.0
))
def find_path_to_base(self, start: np.ndarray) -> List[np.ndarray]:
"""Find multi-hop path from start to base station"""
if self._direct_connection(start, self.base_station):
return [start, self.base_station]
# Find relay chain
path = [start]
current = start
while not self._direct_connection(current, self.base_station):
next_relay = self._find_best_relay(current, path)
if next_relay is None:
return None # No path found
path.append(next_relay.position)
current = next_relay.position
path.append(self.base_station)
return path
def _direct_connection(self, pos1: np.ndarray,
pos2: np.ndarray) -> bool:
"""Check if direct connection is possible"""
return np.linalg.norm(pos1 - pos2) <= self.max_range
def _find_best_relay(self, current: np.ndarray,
exclude: List[np.ndarray]) -> RelayNode:
"""Find best relay node to extend path"""
best_relay = None
best_score = -float('inf')
for node in self.relay_nodes:
if any(np.array_equal(node.position, ex) for ex in exclude):
continue
dist_to_current = np.linalg.norm(node.position - current)
dist_to_base = np.linalg.norm(node.position - self.base_station)
if dist_to_current > self.max_range:
continue
# Score based on battery and progress toward base
score = (node.battery_level / 100) * 0.3 + \
(1 - dist_to_base / 1000) * 0.7
if score > best_score:
best_score = score
best_relay = node
return best_relay
def optimize_positions(self, search_area: Tuple[float, float]) -> List[np.ndarray]:
"""Optimize relay positions for maximum coverage"""
width, height = search_area
# Simple grid placement
n_relays = max(2, int(np.ceil(
max(width, height) / (self.max_range * 0.8)
)))
positions = []
for i in range(n_relays):
x = (i + 1) * width / (n_relays + 1)
y = height / 2
positions.append(np.array([x, y]))
return positions
def monitor_network(self) -> Dict:
"""Monitor relay network health"""
total_battery = sum(n.battery_level for n in self.relay_nodes)
avg_battery = total_battery / len(self.relay_nodes) if self.relay_nodes else 0
return {
'node_count': len(self.relay_nodes),
'average_battery': avg_battery,
'coverage_estimate': self._estimate_coverage(),
'weak_links': self._find_weak_links()
}
def _estimate_coverage(self) -> float:
"""Estimate percentage of area covered"""
if not self.relay_nodes:
return 0
covered = 0
total = 100 * 100 # Grid cells
for i in range(100):
for j in range(100):
pos = np.array([i, j])
for node in self.relay_nodes:
if np.linalg.norm(pos - node.position) < self.max_range:
covered += 1
break
return covered / total * 100
def _find_weak_links(self) -> List[str]:
"""Find nodes with low battery or poor signal"""
weak = []
for node in self.relay_nodes:
if node.battery_level < 30:
weak.append(f"{node.drone_id}: low battery ({node.battery_level}%)")
if node.signal_strength < 0.5:
weak.append(f"{node.drone_id}: weak signal ({node.signal_strength})")
return weak
Thermal Imaging Analysis
from typing import Tuple, List
class ThermalAnalyzer:
def __init__(self):
self = (34, 37) # Celsius
self = (10, 25)
def enhance_thermal(self, thermal_data: np.ndarray) -> np.ndarray:
"""Enhance thermal image for better analysis"""
# Histogram equalization
hist, bins = np.histogram(thermal_data.flatten(), 256)
cdf = hist.cumsum()
cdf = (cdf - cdf.min()) * 255 / (cdf.max() - cdf.min())
enhanced = np.interp(thermal_data.flatten(), bins[:-1], cdf)
return enhanced.reshape(thermal_data.shape)
def detect_hotspots(self, thermal_data: np.ndarray,
threshold: float = 35.0) -> List[Tuple[int, int]]:
"""Detect temperature hotspots"""
hotspots = []
mask = thermal_data > threshold
from scipy import ndimage
labeled, num_features = ndimage.label(mask)
for i in range(1, num_features + 1):
component = labeled == i
if np.sum(component) > 10: # Minimum size
centroid = ndimage.center_of_mass(component)
hotspots.append((int(centroid[0]), int(centroid[1])))
return hotspots
def classify_terrain(self, thermal_data: np.ndarray) -> np.ndarray:
"""Classify terrain types based on thermal signatures"""
classified = np.zeros_like(thermal_data, dtype=int)
# Water (cold, uniform)
classified[thermal_data < 15] = 1
# Vegetation (moderate)
classified[(thermal_data >= 15) & (thermal_data < 25)] = 2
# Bare ground (warm)
classified[(thermal_data >= 25) & (thermal_data < 35)] = 3
# Hot objects (potential victims or fires)
classified[thermal_data >= 35] = 4
return classified
def temporal_analysis(self, thermal_sequence: List[np.ndarray]) -> np.ndarray:
"""Analyze thermal changes over time"""
stack = np.stack(thermal_sequence, axis=0)
# Calculate statistics
mean_temp = np.mean(stack, axis=0)
std_temp = np.std(stack, axis=0)
max_temp = np.max(stack, axis=0)
# Detect changing signatures (potential movement)
movement = std_temp > 2.0
return {
'mean': mean_temp,
'std': std_temp,
'max': max_temp,
'movement_mask': movement
}
Hands-On Project: Multi-Drone SAR Coordination
# sar_coordinator.py
from typing import List, Dict
from enum import Enum
class SARState(Enum):
SEARCHING = "searching"
DETECTED = "detected"
CONFIRMING = "confirming"
RELAYING = "relaying"
class SARCoordinator:
def __init__(self, search_area: Tuple[float, float]):
self.area = search_area
self.drones: Dict[str, Dict] = {}
self.detections: List[Dict] = []
self.state = SARState.SEARCHING
self.coverage = np.zeros((50, 50))
def register_drone(self, drone_id: str, capabilities: List[str]):
"""Register a drone for SAR operation"""
self.drones[drone_id] = {
'capabilities': capabilities,
'position': np.array([0, 0]),
'battery': 100,
'status': 'ready',
'assigned_area': None
}
def plan_search(self) -> Dict[str, List[np.ndarray]]:
"""Assign search areas to drones"""
assignments = {}
n_drones = len(self.drones)
# Divide area into sectors
sector_width = self.area[0] / n_drones
for i, (drone_id, drone) in enumerate(self.drones.items()):
x_start = i * sector_width
x_end = (i + 1) * sector_width
# Generate waypoints for this sector
waypoints = []
for y in np.arange(0, self.area[1], 30):
waypoints.append(np.array([x_start, y]))
waypoints.append(np.array([x_end, y]))
assignments[drone_id] = waypoints
drone['assigned_area'] = (x_start, x_end)
return assignments
def process_detection(self, drone_id: str,
detection: Dict) -> Dict:
"""Process a new detection"""
drone = self.drones[drone_id]
self.detections.append({
'drone_id': drone_id,
'position': detection['position'],
'confidence': detection['confidence'],
'type': detection['type'],
'timestamp': detection['timestamp']
})
# Determine action based on confidence
if detection['confidence'] > 0.8:
return {
'action': 'dispatch_ground_team',
'position': detection['position'],
'priority': 'high'
}
elif detection['confidence'] > 0.6:
return {
'action': 'send_confirmation_drone',
'position': detection['position'],
'priority': 'medium'
}
else:
return {
'action': 'continue_search',
'position': detection['position'],
'priority': 'low'
}
def optimize_search_paths(self) -> Dict[str, List[np.ndarray]]:
"""Dynamically optimize search paths based on findings"""
# Update probability map based on detections
for det in self.detections:
pos = det['position']
# Increase probability around detections
for i in range(max(0, int(pos[0])-5), min(50, int(pos[0])+5)):
for j in range(max(0, int(pos[1])-5), min(50, int(pos[1])+5)):
self.coverage[i, j] += 0.1
# Find uncovered high-probability areas
optimized = {}
for drone_id, drone in self.drones.items():
# Find best uncovered area
best_area = self._find_best_uncovered_area()
if best_area is not None:
optimized[drone_id] = [best_area]
return optimized
def _find_best_uncovered_area(self) -> np.ndarray:
"""Find highest-priority uncovered area"""
# Simple implementation: find least-covered cell
min_coverage = np.min(self.coverage)
min_pos = np.unravel_index(
np.argmin(self.coverage), self.coverage.shape)
return np.array(min_pos) * 2 # Scale to actual coordinates
def generate_mission_report(self) -> str:
"""Generate comprehensive mission report"""
report = "=" * 60 + "\n"
report += "SEARCH AND RESCUE MISSION REPORT\n"
report += "=" * 60 + "\n\n"
report += f"Search Area: {self.area[0]}m x {self.area[1]}m\n"
report += f"Drones Deployed: {len(self.drones)}\n"
report += f"Detections Found: {len(self.detections)}\n"
report += f"Coverage: {np.mean(self.coverage > 0) * 100:.1f}%\n\n"
if self.detections:
report += "DETECTIONS:\n"
for i, det in enumerate(self.detections, 1):
report += f" {i}. Position: ({det['position'][0]:.0f}, {det['position'][1]:.0f})"
report += f" - Confidence: {det['confidence']:.1%}\n"
return report
# Usage example
coordinator = SARCoordinator(search_area=(500, 500))
coordinator.register_drone("SAR-01", ["thermal", "rgb"])
coordinator.register_drone("SAR-02", ["thermal", "relay"])
coordinator.register_drone("SAR-03", ["rgb", "speaker"])
# Simulate detection
detection = {
'position': np.array([150, 200]),
'confidence': 0.85,
'type': 'thermal_human',
'timestamp': 120
}
response = coordinator.process_detection("SAR-01", detection)
print(f"Recommended action: {response['action']}")
print(coordinator.generate_mission_report())
Exercise
- Implement A pathfinding* for obstacle avoidance in search patterns
- Create a time-series analyzer that tracks thermal changes over time
- Build a multi-drone task allocator that balances battery and coverage
- Design a ground team coordination system that guides rescuers to victims
⚠️
Always ensure compliance with local regulations for SAR drone operations. Many jurisdictions require special authorizations for emergency drone flights, especially over populated areas.