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
No description has been provided for this image
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 ✅")
No description has been provided for this image
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)
No description has been provided for this image
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 ✅")
No description has been provided for this image
NDVI mean   : 0.382
NDVI median : 0.364
NDVI max    : 1.000
Healthy veg : 73.4% of pixels
NDVI saved ✅
In [ ]: