diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 219d6ea..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,130 +0,0 @@ -# FireMapper — Session Post-Processing - -Tools that convert raw FireMapper capture sessions into mapping-ready imagery: -they embed GPS/orientation metadata into the images, stretch the radiometric -thermal frames for visualisation, and plot trigger points and image footprints -on a map. - -This repository contains **only the Python tools** — the capture data (session -folders, `*_exif/`, `*_stretched/`) is large (tens of GB) and is git-ignored. - -## Hardware / data - -FireMapper (GGS Speyer) is an aerial rig: -- **RGB cameras** — Allied Vision Alvium *1800 U-1620c*, 5328×3040, saved as TIFF in - `cam25/`, `cam45/` (the number is the **off-nadir mounting angle** in degrees). -- **Thermal camera** — FLIR **A65**, 640×512, 16-bit **radiometric** PNG in `thermal/` - (values are signal counts, *not* calibrated °C). 25° lens → ~25°×20° FOV. -- **INS** — VN-200 (fused attitude + position). - -A **capture session** is a folder (typically `session_YYYYMMDD_HHMMSS/`, but any name -works) containing: - -``` -/ - cam25/ _step_NN.tiff + _step_NN.json (same-basename JSON sidecar each) - cam45/ … - thermal/ _step_NN.png + _step_NN.json - steps/ step_NN.json (per-step summaries, NOT per-image — ignored by the tools) - manifest.json (camera ids / models / serials) - imu_log.jsonl -``` - -Sessions may be grouped under a **parent folder** (e.g. `Streifen/` = flight strips). -Session discovery is content-based (a dir that holds `thermal/`, a `cam*/`, or a -`manifest.json`) and looks one level into parent folders — so the tools find sessions -whether you run from the session's parent or from a grouping folder. - -## The tools - -| File | What it does | Output | -|------|--------------|--------| -| `embed_metadata.py` | Copies each image and writes its JSON sidecar into the copy's EXIF/XMP (GPS, true camera orientation, UTC time, lens/exposure, full JSON in `UserComment`). | `_exif/` (mirrors layout; originals untouched) | -| `stretch_thermal.py` | Rescales all thermal frames to one session-wide brightness window → 8-bit, with a palette (inferno/ironbow/gray). Carries metadata into each output: from the JSON sidecar if present, otherwise copied from the source image's own EXIF/XMP (so stretching already-embedded PNGs keeps GPS/orientation). | `/thermal_stretched/` | -| `session_map.py` | Shared geometry: trigger points + oblique footprints; also the camera-orientation math used by `embed_metadata`. Not run directly. | — | -| `firemapper_gui.py` | Tkinter GUI wrapping all of the above. Tabs: **1. Embed**, **2. Thermal Stretch**, **3. Map**. | — | - -`embed_metadata.py` and `stretch_thermal.py` are also usable as CLIs (see below). -`stretch_thermal` imports `embed_metadata` (to reuse exiftool + tag-building); -`embed_metadata` and `stretch_thermal` import `session_map` (geometry/discovery). - -## Running - -GUI (recommended for end users): -``` -python firemapper_gui.py -``` - -CLI: -``` -python embed_metadata.py [SESSION] [--out DIR] [--gps-source position|gps] - [--workers N] [--dry-run] -python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray] - [--lo-pct P --hi-pct P | --absolute | --lo N --hi N] - [--out DIR] [--no-embed-exif] [--workers N] [--sample] -``` -With no `SESSION` argument they auto-pick the newest session near the script. - -## Camera geometry (important — keep map and EXIF consistent) - -All orientation/footprint math lives in `session_map.py` and is shared by the map and -the EXIF writer so they never diverge. The pointing chain is: - -``` -aircraft attitude (yaw/pitch/roll, body→NED) - @ platform roll about the FORWARD axis by platform_angle_deg (cross-track sweep) - @ camera mounting offset -``` - -Mounting (operator-confirmed): -- **Thermal** — optical axis straight **down** (nadir). Mounted **vertical/portrait**: - sensor *width* runs **along** the flight track. -- **RGB** — optical axis tilted toward the **forward** direction by the `cam` - off-nadir angle (cam25 = 25° from straight-down, cam45 = 45°). Mounted **landscape**: - sensor *width* runs **across** the flight track. - -Functions: -- `camera_orientation(frame)` → `(heading, pitch, roll)` of the true optical axis for - EXIF. Roll uses a *canonical landscape* frame, so it is the camera **bank** (≈0 in - level flight), not the 90° sensor portrait/landscape rotation. -- `camera_axes_ned(frame, sensor=True)` → the sensor-accurate image axes for footprints. - -`embed_metadata.build_tags()` writes the orientation as `GPSImgDirection` + -`XMP-GPano:Pose{Heading,Pitch,Roll}Degrees` (the `XMP-Camera:Pose*` namespace is **not** -writable in stock exiftool). It uses this geometry — **not** the raw IMU. - -## Conventions & caveats - -- **exiftool** is required by the embed step. Resolved from PATH, a local - `tools/exiftool.exe`, the OliverBetz user install (`%LOCALAPPDATA%\Programs\ExifTool`), - else auto-installed via `winget install OliverBetz.ExifTool`. (SourceForge auto-download - is unreliable — GDPR/consent wall.) -- **Disk space:** exiftool writes metadata via a temp copy, so a (near-)full disk causes - partial/failed writes — symptom is an image with most tags but **no GPS**. The embed and - stretch steps now check exiftool's exit code, verify GPS landed, retry once, and raise a - clear "full disk" error instead of silently producing GPS-less files. -- **GPS source:** cam frames use the fused INS `position` block (falls back to raw `gps`); - thermal has only `gps`. UTC time = GPS week/tow − 18 leap seconds. -- **Thermal stretch window:** default = pooled 1–99th percentile across the whole session - (robust to outliers); configurable. Values are radiometric signal, not °C. -- **Footprints** assume **flat ground** at a user-set elevation (default 110 m, the Speyer - area). There is no terrain model. -- **Parallelism:** both steps fan out across a configurable worker pool - (`embed.default_workers()` = `min(cpu, 16)`; CLI `--workers N`, GUI "Parallel workers"). - `embed.run_exiftool_parallel()` splits the per-file argument segments round-robin across - several concurrent exiftool processes and is shared by both tools; copy/render run on a - `ThreadPoolExecutor`. Tk variables are read on the main thread and captured before the - worker thread is spawned. Designed for SSD/NVMe storage, where concurrent I/O helps. -- **Sweep side:** if footprints or headings appear mirrored relative to a known flight, - flip `SCAN_SIGN` at the top of `session_map.py`. -- **Tkinter:** never create `ttk.Style()` before the root window — it spawns a stray - default root and `StringVar`s bind to the wrong one (entries render blank). The style is - created with the app as master inside `__init__`, and all vars are parented to it. - -## Dependencies - -Python 3.13, `Pillow`, `numpy`, `tkintermapview`, `requests` (map tiles need internet), -and `exiftool` (auto-installed). Tkinter ships with the standard Windows Python. -``` -pip install Pillow numpy tkintermapview requests -```