Environmental Monitoring with AI Drones
Environmental monitoring is critical for understanding and protecting our planet. AI-powered drones can survey vast areas, track wildlife, detect illegal activities, and measure pollution levels with unprecedented precision.
The Environmental Monitoring System
Real-World Analogy
Think of environmental drones as "flying scientists." Just as a field researcher collects data samples across a landscape, drones systematically gather environmental data—measuring air quality, tracking animal populations, and monitoring ecosystem health.
SVG: Environmental Sensor Suite and Monitoring Grid
Wildlife Tracking System
from typing import List, Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
class AnimalClass(Enum):
MAMMAL = "mammal"
BIRD = "bird"
REPTILE = "reptile"
AMPHIBIAN = "amphibian"
INSECT = "insect"
@dataclass
class AnimalDetection:
detection_id: str
species: str
animal_class: AnimalClass
position: np.ndarray
confidence: float
bounding_box: Tuple[int, int, int, int]
is_endangered: bool = False
class WildlifeTracker:
def __init__(self, survey_area: Tuple[float, float]):
self.area = survey_area
self.detections: List[AnimalDetection] = []
self.species_database = self._load_species_database()
self.individual_tracks: Dict[str, List[np.ndarray]] = {}
def _load_species_database(self) -> Dict:
"""Load species identification database"""
return {
'white_tailed_deer': {
'class': AnimalClass.MAMMAL,
'size_range': (100, 200), # pixels
'color_profile': [0.6, 0.4, 0.3], # Brown tones
'endangered': False
},
'red_tailed_hawk': {
'class': AnimalClass.BIRD,
'size_range': (30, 80),
'color_profile': [0.5, 0.3, 0.2],
'endangered': False
},
'gray_wolf': {
'class': AnimalClass.MAMMAL,
'size_range': (80, 150),
'color_profile': [0.5, 0.5, 0.5],
'endangered': True
},
'bald_eagle': {
'class': AnimalClass.BIRD,
'size_range': (40, 100),
'color_profile': [0.9, 0.9, 0.9],
'endangered': True
}
}
def process_frame(self, rgb_frame: np.ndarray,
thermal_frame: np.ndarray = None,
position: np.ndarray = None) -> List[AnimalDetection]:
"""Process camera frame for animal detection"""
detections = []
# Visual detection
visual_detections = self._detect_visual(rgb_frame, position)
detections.extend(visual_detections)
# Thermal detection (for mammals/reptiles)
if thermal_frame is not None:
thermal_detections = self._detect_thermal(thermal_frame, position)
detections.extend(thermal_detections)
# Merge overlapping detections
merged = self._merge_detections(detections)
# Update tracking
for det in merged:
self._update_track(det)
self.detections.extend(merged)
return merged
def _detect_visual(self, frame: np.ndarray,
position: np.ndarray) -> List[AnimalDetection]:
"""Detect animals using visual features"""
detections = []
# Color-based detection (simplified)
# In production, use trained ML model
hsv = self._rgb_to_hsv(frame)
# Detect brown/gray wildlife colors
brown_mask = (hsv[:,:,0] > 10) & (hsv[:,:,0] < 30) & \
(hsv[:,:,1] > 50) & (hsv[:,:,2] > 50)
from scipy import ndimage
labeled, num_features = ndimage.label(brown_mask)
for i in range(1, num_features + 1):
component = labeled == i
area = np.sum(component)
# Filter by expected animal size
if 100 < area < 5000:
centroid = ndimage.center_of_mass(component)
bbox = ndimage.find_objects(component == i)[0]
# Simple species classification
species = self._classify_species(area, frame[component])
if species:
det = AnimalDetection(
detection_id=f"DET-{len(self.detections)}",
species=species,
animal_class=self.species_database[species]['class'],
position=np.array([centroid[0], centroid[1], 0]) if position is None else position,
confidence=0.7,
bounding_box=(bbox[0].start, bbox[1].start,
bbox[0].stop, bbox[1].stop),
is_endangered=self.species_database[species]['endangered']
)
detections.append(det)
return detections
def _detect_thermal(self, thermal: np.ndarray,
position: np.ndarray) -> List[AnimalDetection]:
"""Detect warm-blooded animals using thermal"""
detections = []
# Animal body temperature range
body_temp_mask = (thermal > 30) & (thermal < 40)
from scipy import ndimage
labeled, num_features = ndimage.label(body_temp_mask)
for i in range(1, num_features + 1):
component = labeled == i
area = np.sum(component)
if 50 < area < 2000:
centroid = ndimage.center_of_mass(component)
max_temp = np.max(thermal[component])
detections.append(AnimalDetection(
detection_id=f"TH-{len(self.detections)}",
species="unknown_mammal",
animal_class=AnimalClass.MAMMAL,
position=np.array([centroid[0], centroid[1], 0]) if position is None else position,
confidence=0.8,
bounding_box=(0, 0, 0, 0),
is_endangered=False
))
return detections
def _classify_species(self, area: int, pixel_data: np.ndarray) -> Optional[str]:
"""Classify species based on size and appearance"""
mean_color = np.mean(pixel_data, axis=0)
for species, info in self.species_database.items():
if info['size_range'][0] <= area <= info['size_range'][1]:
color_diff = np.linalg.norm(mean_color - np.array(info['color_profile']))
if color_diff < 0.3:
return species
return None
def _merge_detections(self, detections: List[AnimalDetection],
max_distance: float = 30) -> List[AnimalDetection]:
"""Merge overlapping 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[:2] - d2.position[:2]) < max_distance:
cluster.append(d2)
used.add(j)
# Keep best detection from cluster
best = max(cluster, key=lambda d: d.confidence)
merged.append(best)
used.add(i)
return merged
def _update_track(self, detection: AnimalDetection):
"""Update individual animal track"""
species = detection.species
if species not in self.individual_tracks:
self.individual_tracks[species] = []
self.individual_tracks[species].append(detection.position.copy())
def _rgb_to_hsv(self, rgb: np.ndarray) -> np.ndarray:
"""Convert RGB to HSV"""
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)
return np.stack([h * 180, s * 255, v * 255], axis=2)
def generate_population_report(self) -> Dict:
"""Generate wildlife population report"""
species_counts = {}
endangered = []
for det in self.detections:
species = det.species
species_counts[species] = species_counts.get(species, 0) + 1
if det.is_endangered:
endangered.append(det)
return {
'total_detections': len(self.detections),
'species_counts': species_counts,
'unique_species': len(species_counts),
'endangered_detections': len(endangered),
'endangered_species': list(set(d.species for d in endangered)),
'coverage_area': self.area[0] * self.area[1] / 10000 # hectares
}
Deforestation Detection
from typing import List, Dict, Tuple
from dataclasses import dataclass
from datetime import datetime
@dataclass
class ForestChange:
change_id: str
location: np.ndarray
area_hectares: float
change_type: str # 'deforestation', 'degradation', 'regrowth'
confidence: float
timestamp: datetime
class DeforestationDetector:
def __init__(self):
self.baseline_ndvi = None
self.change_threshold = -0.2 # NDVI decrease indicating loss
def set_baseline(self, ndvi_map: np.ndarray):
"""Set baseline NDVI for comparison"""
self.baseline_ndvi = ndvi_map.copy()
def detect_changes(self, current_ndvi: np.ndarray,
resolution: float = 10.0) -> List[ForestChange]:
"""Detect forest changes by comparing to baseline"""
if self.baseline_ndvi is None:
raise ValueError("Baseline not set")
changes = []
ndvi_diff = current_ndvi - self.baseline_ndvi
# Find significant decreases (deforestation)
loss_mask = ndvi_diff < self.change_threshold
from scipy import ndimage
labeled, num_features = ndimage.label(loss_mask)
for i in range(1, num_features + 1):
component = labeled == i
area_pixels = np.sum(component)
area_hectares = area_pixels * (resolution ** 2) / 10000
if area_hectares > 0.5: # Minimum area threshold
centroid = ndimage.center_of_mass(component)
mean_change = np.mean(ndvi_diff[component])
change_type = self._classify_change(mean_change, area_hectares)
changes.append(ForestChange(
change_id=f"CHG-{len(changes)}",
location=np.array(centroid),
area_hectares=area_hectares,
change_type=change_type,
confidence=min(0.95, abs(mean_change) / 0.5),
timestamp=datetime.now()
))
return changes
def _classify_change(self, ndvi_change: float,
area: float) -> str:
"""Classify type of forest change"""
if ndvi_change < -0.3:
return 'deforestation'
elif ndvi_change < -0.15:
return 'degradation'
else:
return 'regrowth'
def calculate_carbon_impact(self, changes: List[ForestChange]) -> Dict:
"""Estimate carbon impact of deforestation"""
# Average carbon density: 150 tonnes/hectare for tropical forest
carbon_density = 150
total_loss = sum(c.area_hectares for c in changes
if c.change_type == 'deforestation')
total_degradation = sum(c.area_hectares for c in changes
if c.change_type == 'degradation')
carbon_lost = total_loss * carbon_density
carbon_degraded = total_degradation * carbon_density * 0.3
return {
'deforestation_hectares': total_loss,
'degradation_hectares': total_degradation,
'carbon_lost_tonnes': carbon_lost,
'carbon_degraded_tonnes': carbon_degraded,
'co2_equivalent_tonnes': (carbon_lost + carbon_degraded) * 3.67
}
Water Quality Analysis
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class WaterQualitySample:
location: np.ndarray
turbidity: float
chlorophyll: float
dissolved_organic: float
temperature: float
quality_index: float
class WaterQualityAnalyzer:
def __init__(self):
self.quality_thresholds = {
'excellent': 90,
'good': 70,
'moderate': 50,
'poor': 30
}
def analyze_water_body(self, multispectral_data: np.ndarray,
thermal_data: np.ndarray,
location: np.ndarray) -> WaterQualitySample:
"""Analyze water quality from drone sensors"""
# Calculate water quality indicators
turbidity = self._calculate_turbidity(multispectral_data)
chlorophyll = self._calculate_chlorophyll(multispectral_data)
doc = self._calculate_dissolved_organic(multispectral_data)
temp = np.mean(thermal_data)
# Calculate overall quality index
quality_index = self._calculate_quality_index(
turbidity, chlorophyll, doc, temp)
return WaterQualitySample(
location=location,
turbidity=turbidity,
chlorophyll=chlorophyll,
dissolved_organic=doc,
temperature=temp,
quality_index=quality_index
)
def _calculate_turbidity(self, multispectral: np.ndarray) -> float:
"""Estimate water turbidity"""
# Turbidity correlates with suspended particles
# Using red band reflectance
red_band = multispectral[:,:,3] if multispectral.shape[2] > 3 else multispectral[:,:,0]
nir_band = multispectral[:,:,4] if multispectral.shape[2] > 4 else multispectral[:,:,1]
# Turbidity index
turbidity = np.mean(red_band) / (np.mean(nir_band) + 1e-10)
return min(100, turbidity * 100)
def _calculate_chlorophyll(self, multispectral: np.ndarray) -> float:
"""Estimate chlorophyll concentration"""
# Chlorophyll peak in green and red-edge bands
if multispectral.shape[2] > 4:
green = multispectral[:,:,2]
red_edge = multispectral[:,:,4]
chlorophyll = np.mean(red_edge) / (np.mean(green) + 1e-10)
else:
chlorophyll = 1.0
return min(100, chlorophyll * 50)
def _calculate_dissolved_organic(self, multispectral: np.ndarray) -> float:
"""Estimate dissolved organic matter"""
# DOC absorbs in blue/UV
if multispectral.shape[2] > 1:
blue = multispectral[:,:,1]
doc = 1.0 / (np.mean(blue) + 1e-10)
else:
doc = 1.0
return min(100, doc * 10)
def _calculate_quality_index(self, turbidity: float,
chlorophyll: float,
doc: float,
temperature: float) -> float:
"""Calculate overall water quality index"""
# Weighted scoring
turb_score = max(0, 100 - turbidity)
chlor_score = max(0, 100 - abs(chlorophyll - 30) * 2)
doc_score = max(0, 100 - doc)
temp_score = max(0, 100 - abs(temperature - 20) * 2)
weights = [0.35, 0.3, 0.2, 0.15]
scores = [turb_score, chlor_score, doc_score, temp_score]
return sum(w * s for w, s in zip(weights, scores))
def classify_quality(self, index: float) -> str:
"""Classify water quality"""
for level, threshold in self.quality_thresholds.items():
if index >= threshold:
return level
return 'critical'
def detect_algal_bloom(self, chlorophyll_history: List[float]) -> Dict:
"""Detect potential algal bloom"""
if len(chlorophyll_history) < 3:
return {'alert': False, 'reason': 'Insufficient data'}
recent = chlorophyll_history[-3:]
trend = np.polyfit(range(3), recent, 1)[0]
alert = False
reasons = []
if recent[-1] > 50:
alert = True
reasons.append('High chlorophyll concentration')
if trend > 5:
alert = True
reasons.append('Rapidly increasing trend')
return {
'alert': alert,
'reasons': reasons,
'current_level': recent[-1],
'trend': trend
}
Air Pollution Mapping
from typing import Dict, List, Tuple
from dataclasses import dataclass
@dataclass
class AirQualityReading:
location: np.ndarray
pm25: float
pm10: float
no2: float
o3: float
aqi: float
class AirPollutionMapper:
def __init__(self):
# AQI breakpoints
self.aqi_breakpoints = [
(0, 50, 'good'),
(51, 100, 'moderate'),
(101, 150, 'unhealthy_sensitive'),
(151, 200, 'unhealthy'),
(201, 300, 'very_unhealthy'),
(301, 500, 'hazardous')
]
def estimate_aqi_from_visual(self, rgb_frame: np.ndarray) -> float:
"""Estimate AQI from visual haze (simplified)"""
# In production, use gas sensors
gray = np.mean(rgb_frame, axis=2)
brightness = np.mean(gray)
# Haze reduces contrast and brightness
haze_index = max(0, (200 - brightness) / 100)
estimated_pm25 = haze_index * 50
return self._pm25_to_aqi(estimated_pm25)
def create_pollution_map(self, readings: List[AirQualityReading],
grid_size: Tuple[int, int] = (50, 50)) -> np.ndarray:
"""Create interpolated pollution map"""
pollution_map = np.zeros(grid_size)
for reading in readings:
# Convert GPS to grid coordinates
x = int(reading.location[0] / 1000 * grid_size[0])
y = int(reading.location[1] / 1000 * grid_size[1])
# Gaussian spread
for i in range(max(0, x-5), min(grid_size[0], x+6)):
for j in range(max(0, y-5), min(grid_size[1], y+6)):
dist = np.sqrt((i-x)**2 + (j-y)**2)
contribution = reading.aqi * np.exp(-dist**2 / 8)
pollution_map[i, j] = max(pollution_map[i, j], contribution)
return pollution_map
def _pm25_to_aqi(self, pm25: float) -> float:
"""Convert PM2.5 concentration to AQI"""
# Simplified conversion
if pm25 <= 12:
return pm25 / 12 * 50
elif pm25 <= 35.4:
return 50 + (pm25 - 12) / 23.4 * 50
elif pm25 <= 55.4:
return 100 + (pm25 - 35.4) / 20 * 50
else:
return min(500, 150 + (pm25 - 55.4) / 40 * 150)
def identify_pollution_sources(self, pollution_map: np.ndarray,
wind_data: np.ndarray) -> List[Dict]:
"""Identify potential pollution sources"""
sources = []
# Find hotspots
threshold = np.percentile(pollution_map, 95)
hotspots = np.argwhere(pollution_map > threshold)
for hotspot in hotspots:
# Estimate source location (upwind)
wind_dir = np.arctan2(wind_data[1], wind_data[0])
source_estimate = hotspot - np.array([
np.cos(wind_dir) * 5,
np.sin(wind_dir) * 5
])
sources.append({
'estimated_source': source_estimate.tolist(),
'pollution_level': float(pollution_map[hotspot[0], hotspot[1]]),
'confidence': 0.7
})
return sources
Hands-On Project: Complete Environmental Monitoring System
# environmental_monitor.py
from typing import List, Dict
from dataclasses import dataclass
from enum import Enum
class MonitoringType(Enum):
WILDLIFE = "wildlife"
DEFORESTATION = "deforestation"
WATER_QUALITY = "water_quality"
AIR_QUALITY = "air_quality"
@dataclass
class MonitoringMission:
mission_id: str
mission_type: MonitoringType
area: tuple
altitude: float
duration: float
sensors_required: List[str]
class EnvironmentalMonitoringSystem:
def __init__(self):
self.wildlife_tracker = WildlifeTracker((1000, 1000))
self.deforestation_detector = DeforestationDetector()
self.water_analyzer = WaterQualityAnalyzer()
self.air_mapper = AirPollutionMapper()
def create_mission(self, mission_type: MonitoringType,
area: tuple) -> MonitoringMission:
"""Create monitoring mission"""
sensor_requirements = {
MonitoringType.WILDLIFE: ['rgb', 'thermal', 'multispectral'],
MonitoringType.DEFORESTATION: ['multispectral', 'lidar'],
MonitoringType.WATER_QUALITY: ['multispectral', 'thermal'],
MonitoringType.AIR_QUALITY: ['rgb', 'gas_sensors']
}
return MonitoringMission(
mission_id=f"MISSION-{np.random.randint(1000, 9999)}",
mission_type=mission_type,
area=area,
altitude=50,
duration=60,
sensors_required=sensor_requirements[mission_type]
)
def execute_mission(self, mission: MonitoringMission,
sensor_data: Dict) -> Dict:
"""Execute monitoring mission"""
results = {'mission_id': mission.mission_id}
if mission.mission_type == MonitoringType.WILDLIFE:
rgb = sensor_data.get('rgb', np.zeros((100, 100, 3)))
thermal = sensor_data.get('thermal', np.zeros((100, 100)))
detections = self.wildlife_tracker.process_frame(rgb, thermal)
results['wildlife'] = self.wildlife_tracker.generate_population_report()
elif mission.mission_type == MonitoringType.DEFORESTATION:
ndvi = sensor_data.get('ndvi', np.ones((100, 100)) * 0.6)
if self.deforestation_detector.baseline_ndvi is None:
self.deforestation_detector.set_baseline(ndvi)
else:
changes = self.deforestation_detector.detect_changes(ndvi)
results['deforestation'] = {
'changes_detected': len(changes),
'carbon_impact': self.deforestation_detector.calculate_carbon_impact(changes)
}
elif mission.mission_type == MonitoringType.WATER_QUALITY:
multispectral = sensor_data.get('multispectral', np.zeros((100, 100, 5)))
thermal = sensor_data.get('thermal', np.zeros((100, 100)))
sample = self.water_analyzer.analyze_water_body(
multispectral, thermal, np.array([500, 500, 0]))
results['water_quality'] = {
'quality_index': sample.quality_index,
'classification': self.water_analyzer.classify_quality(sample.quality_index)
}
elif mission.mission_type == MonitoringType.AIR_QUALITY:
rgb = sensor_data.get('rgb', np.ones((100, 100, 3)) * 180)
aqi = self.air_mapper.estimate_aqi_from_visual(rgb)
results['air_quality'] = {'aqi': aqi}
return results
def generate_environmental_report(self) -> str:
"""Generate comprehensive environmental report"""
report = "=" * 60 + "\n"
report += "ENVIRONMENTAL MONITORING REPORT\n"
report += "=" * 60 + "\n\n"
# Wildlife summary
wildlife = self.wildlife_tracker.generate_population_report()
report += "WILDLIFE SURVEY:\n"
report += f" Total Detections: {wildlife['total_detections']}\n"
report += f" Unique Species: {wildlife['unique_species']}\n"
report += f" Endangered Species: {wildlife['endangered_detections']}\n\n"
return report
# Usage
system = EnvironmentalMonitoringSystem()
# Create and execute wildlife mission
wildlife_mission = system.create_mission(
MonitoringType.WILDLIFE,
area=(1000, 1000)
)
# Simulate sensor data
sensor_data = {
'rgb': np.random.randint(50, 200, (100, 100, 3), dtype=np.uint8),
'thermal': np.random.uniform(15, 40, (100, 100))
}
results = system.execute_mission(wildlife_mission, sensor_data)
print(system.generate_environmental_report())
Exercise
- Implement animal behavior analysis that detects feeding, resting, and mating patterns
- Create a time-series deforestation predictor using historical data
- Build a water quality forecasting system based on weather patterns
- Design an integrated environmental dashboard combining all monitoring types
ℹ️
Environmental drones are used by organizations like WWF, National Geographic, and government agencies worldwide. They've helped discover new species, track endangered populations, and detect illegal logging operations.