In [1]:
import os
import json
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from pathlib import Path
plt.style.use('dark_background')
# Dataset paths
DATASET_DIR = Path('../data/datasets/odm_data_aukerman-master')
IMAGES_DIR = DATASET_DIR / 'images'
# Inventory
images = sorted(IMAGES_DIR.glob('*.JPG'))
print(f"Dataset : Aukerman Agricultural Field")
print(f"Source : OpenDroneMap sample dataset")
print(f"Images : {len(images)} JPG files")
print(f"Dataset dir : {DATASET_DIR.resolve()}")
print()
# Read EXIF from first image to get camera info
import subprocess
result = subprocess.run(['mdls', str(images[0])], capture_output=True, text=True)
print(f"First image : {images[0].name}")
print(f"Last image : {images[-1].name}")
Dataset : Aukerman Agricultural Field Source : OpenDroneMap sample dataset Images : 77 JPG files Dataset dir : /Users/josemaciel/Desktop/uav-mission-intelligence/data/datasets/odm_data_aukerman-master First image : DSC00229.JPG Last image : DSC00311.JPG
In [7]:
from PIL import Image
from PIL.ExifTags import TAGS, GPSTAGS
def get_exif(filepath):
img = Image.open(filepath)
exif_raw = img._getexif()
if not exif_raw:
return {}
return {TAGS.get(k, k): v for k, v in exif_raw.items()}
def get_gps(exif):
gps_raw = exif.get('GPSInfo', {})
gps = {GPSTAGS.get(k, k): v for k, v in gps_raw.items()}
try:
def to_deg(val):
return float(val[0]) + float(val[1])/60 + float(val[2])/3600
lat = to_deg(gps['GPSLatitude'])
if gps['GPSLatitudeRef'] == 'S': lat = -lat
lon = to_deg(gps['GPSLongitude'])
if gps['GPSLongitudeRef'] == 'W': lon = -lon
alt = float(gps.get('GPSAltitude', 0))
return lat, lon, alt
except:
return None
coords = []
for img_path in images:
exif = get_exif(img_path)
gps = get_gps(exif)
if gps:
coords.append({'file': img_path.name, 'lat': gps[0], 'lon': gps[1], 'alt': gps[2]})
df = pd.DataFrame(coords)
print(f"Images with GPS : {len(df)} / {len(images)}")
print(f"Lat range : {df['lat'].min():.6f} → {df['lat'].max():.6f}")
print(f"Lon range : {df['lon'].min():.6f} → {df['lon'].max():.6f}")
print(f"Alt range : {df['alt'].min():.1f}m → {df['alt'].max():.1f}m")
fig, ax = plt.subplots(figsize=(10, 8))
fig.suptitle('Sprint 03 · Aukerman Dataset · Image Capture Positions',
color='white', fontsize=14, fontweight='bold')
sc = ax.scatter(df['lon'], df['lat'], c=df['alt'], cmap='plasma',
s=60, zorder=5, label='Image capture point')
ax.plot(df['lon'], df['lat'], color='#444444', linewidth=0.8, zorder=4)
ax.scatter(df['lon'].iloc[0], df['lat'].iloc[0], color='#FFD700', s=120, zorder=6, label='First image')
ax.scatter(df['lon'].iloc[-1], df['lat'].iloc[-1], color='#4ECDC4', s=120, zorder=6, label='Last image')
plt.colorbar(sc, ax=ax, label='Altitude (m)')
ax.set_xlabel('Longitude', color='#888888')
ax.set_ylabel('Latitude', color='#888888')
ax.tick_params(colors='#888888')
ax.legend(fontsize=9)
plt.tight_layout()
plt.savefig('../data/aukerman_capture_positions.png', dpi=150, bbox_inches='tight')
plt.show()
print("Capture positions plot saved ✅")
Images with GPS : 77 / 77 Lat range : 41.303408 → 41.304828 Lon range : -81.753956 → -81.750451 Alt range : 337.4m → 348.4m
Capture positions plot saved ✅
In [8]:
# Altitude profile across flight
fig, axes = plt.subplots(2, 1, figsize=(14, 8))
fig.suptitle('Sprint 03 · Aukerman Dataset · Flight Profile & Coverage',
color='white', fontsize=14, fontweight='bold')
# Altitude over time (image sequence)
ax1 = axes[0]
ax1.plot(range(len(df)), df['alt'], color='#00BFFF', linewidth=1.5)
ax1.fill_between(range(len(df)), df['alt'], alpha=0.2, color='#00BFFF')
ax1.set_xlabel('Image sequence', color='#888888')
ax1.set_ylabel('Altitude (m)', color='#888888')
ax1.set_title('Flight Altitude Profile', color='white')
ax1.tick_params(colors='#888888')
# Coverage area estimation
lat_range = df['lat'].max() - df['lat'].min()
lon_range = df['lon'].max() - df['lon'].min()
lat_m = lat_range * 111320
lon_m = lon_range * 111320 * abs(np.cos(np.radians(df['lat'].mean())))
area_ha = (lat_m * lon_m) / 10000
ax2 = axes[1]
ax2.scatter(df['lon'], df['lat'], c=range(len(df)), cmap='cool',
s=50, zorder=5)
ax2.set_xlabel('Longitude', color='#888888')
ax2.set_ylabel('Latitude', color='#888888')
ax2.set_title(f'Coverage Area ≈ {area_ha:.2f} ha ({lat_m:.0f}m × {lon_m:.0f}m)', color='white')
ax2.tick_params(colors='#888888')
plt.tight_layout()
plt.savefig('../data/aukerman_flight_profile.png', dpi=150, bbox_inches='tight')
plt.show()
print(f"Coverage area : {area_ha:.2f} ha")
print(f"North-South : {lat_m:.0f} m")
print(f"East-West : {lon_m:.0f} m")
print(f"Mean altitude : {df['alt'].mean():.1f} m")
print(f"Alt std dev : {df['alt'].std():.1f} m")
print("Flight profile saved ✅")
Coverage area : 4.64 ha North-South : 158 m East-West : 293 m Mean altitude : 342.1 m Alt std dev : 2.3 m Flight profile saved ✅
In [10]:
import rasterio
from rasterio.plot import show
import numpy as np
ORTHO_PATH = '../data/Valley-Parkway-Connector-6-29-2016-orthophoto.tif'
with rasterio.open(ORTHO_PATH) as src:
print(f"CRS : {src.crs}")
print(f"Resolution : {src.res[0]:.4f} x {src.res[1]:.4f} degrees/pixel")
print(f"Dimensions : {src.width} x {src.height} px")
print(f"Bands : {src.count}")
bounds = src.bounds
print(f"Bounds : {bounds}")
# Read RGB bands
r = src.read(1).astype(float)
g = src.read(2).astype(float)
b = src.read(3).astype(float)
def normalize(arr):
arr = np.clip(arr, np.percentile(arr[arr>0], 2), np.percentile(arr[arr>0], 98))
return (arr - arr.min()) / (arr.max() - arr.min())
rgb = np.dstack([normalize(r), normalize(g), normalize(b)])
fig, ax = plt.subplots(figsize=(12, 10))
fig.suptitle('Sprint 03 · Aukerman · Orthomosaic (WebODM)',
color='white', fontsize=14, fontweight='bold')
ax.imshow(rgb)
ax.set_title('Valley Parkway Connector — Georeferenced Orthophoto', color='white')
ax.axis('off')
plt.tight_layout()
plt.savefig('../data/aukerman_orthomosaic.png', dpi=150, bbox_inches='tight')
plt.show()
print("Orthomosaic saved ✅")
CRS : EPSG:32617 Resolution : 0.0804 x 0.0804 degrees/pixel Dimensions : 4457 x 3355 px Bands : 4 Bounds : BoundingBox(left=436854.66415070463, bottom=4572667.713463982, right=437212.99696562113, top=4572937.447947725)
Orthomosaic saved ✅
In [11]:
with rasterio.open(ORTHO_PATH) as src:
r = src.read(1).astype(float)
nir = src.read(4).astype(float) # Band 4 = NIR in ODM output
# Calculate NDVI
np.seterr(divide='ignore', invalid='ignore')
ndvi = np.where((nir + r) == 0, 0, (nir - r) / (nir + r))
# Mask nodata
ndvi_masked = np.ma.masked_where(ndvi == 0, ndvi)
fig, axes = plt.subplots(1, 2, figsize=(16, 7))
fig.suptitle('Sprint 03 · Aukerman · Plant Health Analysis (NDVI)',
color='white', fontsize=14, fontweight='bold')
# RGB reference
axes[0].imshow(rgb)
axes[0].set_title('RGB Orthomosaic', color='white')
axes[0].axis('off')
# NDVI
im = axes[1].imshow(ndvi_masked, cmap='RdYlGn', vmin=-0.2, vmax=0.8)
axes[1].set_title('NDVI — Vegetation Index', color='white')
axes[1].axis('off')
plt.colorbar(im, ax=axes[1], label='NDVI value', shrink=0.8)
plt.tight_layout()
plt.savefig('../data/aukerman_ndvi.png', dpi=150, bbox_inches='tight')
plt.show()
# Stats
valid = ndvi_masked.compressed()
print(f"NDVI mean : {valid.mean():.3f}")
print(f"NDVI median : {np.median(valid):.3f}")
print(f"NDVI max : {valid.max():.3f}")
print(f"Healthy veg : {(valid > 0.3).sum() / len(valid) * 100:.1f}% of pixels")
print("NDVI saved ✅")
NDVI mean : 0.382 NDVI median : 0.364 NDVI max : 1.000 Healthy veg : 73.4% of pixels NDVI saved ✅
In [ ]: