The Electromagnetic Spectrum and Thermal Imaging
Everything above absolute zero (-273.15°C) emits infrared radiation. Thermal cameras detect this invisible heat radiation and convert it into visible images. Unlike regular cameras that see reflected light, thermal cameras see emitted heat — they work in total darkness.
How Thermal Cameras Work
A thermal camera contains a microbolometer — an array of tiny temperature-sensitive resistors. Each pixel measures incoming IR radiation and converts it to a temperature reading.
Thermal Camera Specifications
| Spec | Entry Level | Mid-Range | Professional |
|---|---|---|---|
| Resolution | 80×60 | 160×120 | 640×480 |
| NETD | <150mK | <60mK | <30mK |
| Frame Rate | 9Hz | 30Hz | 60Hz |
| Lens | 25° FOV | 25–50° FOV | Interchangeable |
| Examples | FLIR Lepton 3.5 | DJI Zenmuse H20T | FLIR A65 |
class ThermalAnalyzer:
"""Basic thermal image analysis."""
def __init__(self, calibration_offset=0.0):
self.offset = calibration_offset
def raw_to_temperature(self, raw_value, emissivity=0.95):
"""Convert raw sensor value to temperature (simplified)."""
# Simplified conversion (real cameras use complex calibration)
temp_kelvin = (raw_value / 1000.0) + 273.15 + self.offset
temp_celsius = temp_kelvin - 273.15
return temp_celsius
def analyze_heat_map(self, thermal_image):
"""Extract temperature statistics from thermal image."""
return {
"min_temp": np.min(thermal_image),
"max_temp": np.max(thermal_image),
"mean_temp": np.mean(thermal_image),
"std_temp": np.std(thermal_image),
"hot_spots": np.sum(thermal_image > np.mean(thermal_image) + 2 * np.std(thermal_image)),
"cold_spots": np.sum(thermal_image < np.mean(thermal_image) - 2 * np.std(thermal_image))
}
def detect_anomalies(self, thermal_image, threshold=5.0):
"""Detect temperature anomalies (potential issues)."""
mean_temp = np.mean(thermal_image)
anomalies = np.where(np.abs(thermal_image - mean_temp) > threshold)
return list(zip(anomalies[1], anomalies[0])) # (x, y) positions
# Demo: Simulated thermal image of a building facade
np.random.seed(42)
thermal_image = np.random.normal(25, 2, (120, 160)) # Base temperature
# Add a heat leak
thermal_image[40:60, 70:90] += 15 # Window heat leak
# Add cold spot (insulation gap)
thermal_image[80:100, 100:120] -= 8
analyzer = ThermalAnalyzer()
stats = analyzer.analyze_heat_map(thermal_image)
anomalies = analyzer.detect_anomalies(thermal_image, threshold=8.0)
print(f"Temperature range: {stats['min_temp']:.1f}°C to {stats['max_temp']:.1f}°C")
print(f"Mean: {stats['mean_temp']:.1f}°C ± {stats['std_temp']:.1f}°C")
print(f"Hot spots detected: {stats['hot_spots']}")
print(f"Anomalies found: {len(anomalies)}")
Hands-On Project: Thermal anomaly detector
Build a system to automatically detect hot spots in thermal images from drone surveys:
from collections import deque
class ThermalSurveyAnalyzer:
"""Analyze thermal survey data for anomalies."""
def __init__(self, flight_altitude=30, gsd=0.1):
self.flight_altitude = flight_altitude # meters
self.gsd = gsd # ground sampling distance (m/pixel)
def stitch_thermal_frames(self, frames, positions):
"""Simple thermal frame stitching (naive average)."""
if not frames:
return None
# Simple averaging for overlapping regions
canvas = np.zeros_like(frames[0], dtype=float)
count = np.zeros_like(frames[0], dtype=float)
for frame, pos in zip(frames, positions):
canvas += frame
count += 1
return canvas / np.maximum(count, 1)
def generate_report(self, thermal_data):
"""Generate a survey report."""
report = {
"total_pixels": thermal_data.size,
"temperature_range": {
"min": float(np.min(thermal_data)),
"max": float(np.max(thermal_data)),
"mean": float(np.mean(thermal_data)),
},
"anomalies": [],
}
# Define anomaly thresholds
mean = np.mean(thermal_data)
std = np.std(thermal_data)
# Hot anomalies (>2σ above mean)
hot_mask = thermal_data > mean + 2 * std
hot_coords = np.where(hot_mask)
if len(hot_coords[0]) > 0:
report["anomalies"].append({
"type": "hot",
"count": int(np.sum(hot_mask)),
"max_temp": float(np.max(thermal_data[hot_mask])),
"locations": list(zip(hot_coords[1].tolist()[:10], hot_coords[0].tolist()[:10]))
})
# Cold anomalies (>2σ below mean)
cold_mask = thermal_data < mean - 2 * std
cold_coords = np.where(cold_mask)
if len(cold_coords[0]) > 0:
report["anomalies"].append({
"type": "cold",
"count": int(np.sum(cold_mask)),
"min_temp": float(np.min(thermal_data[cold_mask])),
"locations": list(zip(cold_coords[1].tolist()[:10], cold_coords[0].tolist()[:10]))
})
return report
# Demo
analyzer = ThermalSurveyAnalyzer(flight_altitude=30, gsd=0.05)
# Simulated thermal survey data
np.random.seed(42)
survey_data = np.random.normal(22, 3, (480, 640)) # Building facade
# Add anomalies
survey_data[100:150, 200:250] += 20 # Heat leak
survey_data[300:350, 400:450] -= 12 # Cold insulation gap
report = analyzer.generate_report(survey_data)
print(f"Survey area: {640 * 0.05:.1f}m × {480 * 0.05:.1f}m")
print(f"Temperature: {report['temperature_range']['min']:.1f}°C to {report['temperature_range']['max']:.1f}°C")
print(f"Anomalies detected: {len(report['anomalies'])}")
for a in report['anomalies']:
print(f" - {a['type'].upper()}: {a['count']} pixels")
Key Takeaways
- Thermal cameras detect infrared radiation (heat), not visible light
- LWIR (8-14μm) is most common for drone applications (uncooled sensors)
- Emissivity affects accuracy — different materials radiate heat differently
- Common palettes: Ironbow (color), White Hot/Black Hot (grayscale)
- Applications: building inspection, search & rescue, agriculture, industrial monitoring
- NETD (Noise Equivalent Temperature Difference) indicates sensitivity