Rewrite user-facing text (GUI, manual, CLAUDE.md) in a professional register

Revised the GUI explanations, on-screen labels and dialogs, the HTML user
manual, and CLAUDE.md to a formal, consistent tone: removed casual phrasing
and contractions, standardised terminology, and documented the new parallel
"--workers" option (CLI) and "Parallel workers" control (GUI). Also added a
manual troubleshooting entry for full-disk GPS loss and a parallelism note to
CLAUDE.md. No functional changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
NoahC 2026-06-26 11:54:12 +02:00
parent 4f1ae7f7c4
commit 90a9059544
3 changed files with 158 additions and 130 deletions

View File

@ -1,11 +1,12 @@
# FireMapper — Session Post-Processing
Tools that turn raw FireMapper capture sessions into mapping-ready imagery:
embed GPS/orientation metadata into the photos, stretch the radiometric thermal
frames for viewing, and plot trigger points + image footprints on a map.
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 repo contains **only the Python tools** — the capture data (session folders,
`*_exif/`, `*_stretched/`) is large (tens of GB) and is git-ignored.
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
@ -92,7 +93,7 @@ Functions:
`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 & gotchas
## Conventions & caveats
- **exiftool** is required by the embed step. Resolved from PATH, a local
`tools/exiftool.exe`, the OliverBetz user install (`%LOCALAPPDATA%\Programs\ExifTool`),
@ -108,8 +109,14 @@ writable in stock exiftool). It uses this geometry — **not** the raw IMU.
(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.
- **Sweep side:** if footprints/headings come out mirrored vs a known flight, flip
`SCAN_SIGN` at the top of `session_map.py`.
- **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.

View File

@ -1,23 +1,26 @@
#!/usr/bin/env python3
"""
FireMapper - Session Post-Processing (GUI)
==========================================
FireMapper - Session Post-Processing (graphical interface)
==========================================================
A friendly front-end for the two FireMapper post-processing steps:
Graphical front-end for the FireMapper post-processing workflow:
1. Embed GPS & metadata - writes each frame's JSON sidecar (GPS, heading,
time, lens, ...) into EXIF/XMP of tagged image copies, ready for mapping
software. (wraps embed_metadata.py)
1. Embed GPS & metadata - writes each frame's JSON sidecar (GPS, orientation,
capture time, lens) into the EXIF/XMP of tagged image copies for use in
mapping software. (wraps embed_metadata.py)
2. Thermal stretch - rescales the whole session's 16-bit radiometric
thermal frames into one shared brightness window and saves viewable
8-bit images in a heat-map palette. (wraps stretch_thermal.py)
2. Thermal stretch - rescales the session's 16-bit radiometric thermal frames
to a single shared brightness window and saves 8-bit images in the chosen
palette. (wraps stretch_thermal.py)
Just run: python firemapper_gui.py
3. Map - plots trigger points and image footprints on OpenStreetMap.
(wraps session_map.py)
No command line needed - pick a session folder, read the on-screen
explanation, and click the button. Long jobs run in the background with a
progress bar; your original files are never modified.
Launch with: python firemapper_gui.py
Select a session folder, review the description on each tab, set the options,
and start the operation. Long-running tasks execute on a background thread with
a progress indicator. Original files are never modified.
"""
from __future__ import annotations
@ -106,9 +109,9 @@ class FireMapperGUI(tk.Tk):
ttk.Label(head, text="FireMapper - Session Post-Processing",
font=("Segoe UI Semibold", 15)).pack(anchor="w")
ttk.Label(head, foreground="#555", wraplength=900, justify="left",
text="Turn a raw capture session into mapping-ready imagery. "
"Pick a session folder, then run either step. Originals are "
"never changed - results are written to new folders.").pack(anchor="w")
text="Convert a raw capture session into mapping-ready imagery. Select a "
"session folder and run any step. Original files are never modified; "
"all results are written to separate output folders.").pack(anchor="w")
ttk.Separator(self).pack(fill="x", padx=PAD, pady=(6, 8))
@staticmethod
@ -142,7 +145,8 @@ class FireMapperGUI(tk.Tk):
ttk.Spinbox(row, from_=1, to=(os.cpu_count() or 32), increment=1, width=6,
textvariable=self.workers).pack(side="left")
ttk.Label(row, foreground="#777",
text=f" threads / exiftool processes (you have {os.cpu_count()} cores)"
text=f" concurrent workers for copying, rendering and exiftool "
f"({os.cpu_count()} logical cores available)"
).pack(side="left", padx=6)
return row
@ -165,15 +169,15 @@ class FireMapperGUI(tk.Tk):
tab = ttk.Frame(nb, padding=PAD)
nb.add(tab, text=" 1. Embed GPS & Metadata ")
ttk.Label(tab, text="Embed GPS & metadata into images", font=HEADING).pack(anchor="w")
ttk.Label(tab, text="Embed GPS & Metadata", font=HEADING).pack(anchor="w")
self._explain(tab,
"Copies every image in the session and writes its matching JSON data into "
"the copy's EXIF/XMP tags: GPS position, camera heading / pitch / roll, UTC "
"capture time, lens & exposure, plus the complete JSON in the comment field. "
"The tagged copies drop straight into mapping / photogrammetry tools "
"(Pix4D, Metashape, QGIS, ...), which read the embedded GPS to place each "
"photo on the map. Requires exiftool - it is found automatically, and "
"installed for you the first time if missing."
"Creates a tagged copy of every image in the session and writes the "
"corresponding JSON values into its EXIF/XMP metadata: GPS position, camera "
"heading, pitch and roll, UTC capture time, lens and exposure, and the complete "
"JSON record in the comment field. The tagged copies are compatible with "
"photogrammetry and GIS applications (Pix4D, Metashape, QGIS), which read the "
"embedded GPS to position each image. Requires exiftool, which is located "
"automatically and installed on first use if it is not already present."
).pack(fill="x", pady=(4, 10))
self.embed_session = tk.StringVar(self, value=autodetect_session())
@ -183,15 +187,15 @@ class FireMapperGUI(tk.Tk):
self._folder_row(tab, "Session folder", self.embed_session).pack(fill="x", pady=3)
self._folder_row(tab, "Output folder", self.embed_out).pack(fill="x", pady=3)
ttk.Label(tab, text="Leave output blank to use <session>_exif next to the session.",
ttk.Label(tab, text="If left blank, output is written to <session>_exif beside the session.",
foreground="#777").pack(anchor="w", padx=(16, 0))
gps = ttk.LabelFrame(tab, text="GPS source for camera frames", padding=8)
gps.pack(fill="x", pady=(10, 4))
ttk.Radiobutton(gps, variable=self.embed_gps, value="position",
text="Fused INS (position) - most accurate [recommended]").pack(anchor="w")
text="Fused INS (position) - highest accuracy (recommended)").pack(anchor="w")
ttk.Radiobutton(gps, variable=self.embed_gps, value="gps",
text="Raw GNSS (gps) - bare satellite fix").pack(anchor="w")
text="Raw GNSS (gps) - uncorrected satellite fix").pack(anchor="w")
ttk.Label(gps, foreground="#777",
text="Thermal frames have no INS solution and always use raw GNSS.").pack(anchor="w")
@ -199,7 +203,7 @@ class FireMapperGUI(tk.Tk):
adv.pack(fill="x", pady=(8, 2))
ttk.Label(adv, text="exiftool path", width=16).pack(side="left")
ttk.Entry(adv, textvariable=self.embed_exif).pack(side="left", fill="x", expand=True)
ttk.Label(adv, text="(optional - auto)", foreground="#777").pack(side="left", padx=6)
ttk.Label(adv, text="(optional; auto-detected)", foreground="#777").pack(side="left", padx=6)
self._workers_row(tab).pack(fill="x", pady=(8, 2))
@ -214,15 +218,16 @@ class FireMapperGUI(tk.Tk):
tab = ttk.Frame(nb, padding=PAD)
nb.add(tab, text=" 2. Thermal Stretch ")
ttk.Label(tab, text="Stretch thermal frames for viewing", font=HEADING).pack(anchor="w")
ttk.Label(tab, text="Stretch Thermal Frames for Visualisation", font=HEADING).pack(anchor="w")
self._explain(tab,
"The thermal camera records 16-bit radiometric frames whose values fill only "
"a tiny part of the range, so raw files look almost black and flicker between "
"frames. This finds ONE brightness window shared by the whole session and "
"stretches every frame into it, saving easy-to-view 8-bit images. Because all "
"frames share the window, hot and cold areas stay consistent across the flight. "
"Values are radiometric signal (proportional to temperature), not calibrated "
"degrees. Tip: click 'Preview palettes' to compare the look before processing."
"The thermal camera records 16-bit radiometric frames that occupy only a small "
"portion of the available range; consequently the raw files appear nearly black "
"and vary in brightness from frame to frame. This step determines a single "
"brightness window for the entire session and rescales every frame into it, "
"producing viewable 8-bit images. Because all frames share one window, warm and "
"cool areas remain consistent across the flight. Pixel values represent "
"radiometric signal (proportional to temperature), not calibrated degrees. Use "
"'Preview palettes' to compare the available palettes before processing."
).pack(fill="x", pady=(4, 10))
self.th_session = tk.StringVar(self, value=autodetect_session())
@ -234,7 +239,7 @@ class FireMapperGUI(tk.Tk):
self._folder_row(tab, "Session folder", self.th_session).pack(fill="x", pady=3)
self._folder_row(tab, "Output folder", self.th_out).pack(fill="x", pady=3)
ttk.Label(tab, text="Leave output blank to use <session>/thermal_stretched.",
ttk.Label(tab, text="If left blank, output is written to <session>/thermal_stretched.",
foreground="#777").pack(anchor="w", padx=(16, 0))
opts = ttk.Frame(tab)
@ -252,8 +257,8 @@ class FireMapperGUI(tk.Tk):
self.th_embed = tk.BooleanVar(self, value=True)
ttk.Checkbutton(tab, variable=self.th_embed,
text="Embed GPS / orientation metadata into the stretched PNGs "
"(needs exiftool)").pack(anchor="w", pady=(6, 0))
text="Embed GPS and orientation metadata into the stretched PNGs "
"(requires exiftool)").pack(anchor="w", pady=(6, 0))
self._workers_row(tab).pack(fill="x", pady=(6, 2))
@ -279,21 +284,21 @@ class FireMapperGUI(tk.Tk):
if TkinterMapView is None:
ttk.Label(tab, foreground="#a00000", wraplength=600, justify="left",
text="The map needs the 'tkintermapview' package.\n\n"
"Install it from a terminal with:\n"
text="The map requires the 'tkintermapview' package.\n\n"
"Install it from a terminal:\n"
" pip install tkintermapview\n\n"
"then reopen this program.").pack(anchor="w", pady=20)
"then restart the application.").pack(anchor="w", pady=20)
return
ttk.Label(tab, text="Trigger points & image footprints on OpenStreetMap",
ttk.Label(tab, text="Trigger Points & Image Footprints on OpenStreetMap",
font=HEADING).pack(anchor="w")
self._explain(tab,
"Tick the sessions to plot, then click 'Show on map'. Each image becomes a "
"trigger point (its GPS position) and, optionally, an oblique footprint - the "
"ground patch the photo covers, projected from the camera off-nadir angle "
"Select the sessions to display and click 'Show on map'. Each image is shown as "
"a trigger point (its GPS position) and, optionally, as an oblique footprint: "
"the ground area the image covers, projected from the camera off-nadir angle "
"(cam25 = 25 deg, cam45 = 45 deg), the cross-track scan angle, the aircraft "
"attitude and the height above ground. Footprints assume flat ground at the "
"elevation set below. Needs an internet connection for the map tiles."
"attitude and the height above ground. Footprints assume flat terrain at the "
"elevation specified below. An internet connection is required for the map tiles."
).pack(fill="x", pady=(4, 8))
body = ttk.Frame(tab)
@ -341,7 +346,7 @@ class FireMapperGUI(tk.Tk):
field("Thermal off-nadir", self.map_toff)
field("Plot every Nth", self.map_step)
ttk.Label(opt, foreground="#777",
text="Thermal footprints need the FOV above.").pack(anchor="w", pady=(2, 4))
text="Thermal footprints require the field-of-view values above.").pack(anchor="w", pady=(2, 4))
ttk.Checkbutton(opt, text="Trigger points", variable=self.map_markers).pack(anchor="w")
ttk.Checkbutton(opt, text="Footprints", variable=self.map_foot).pack(anchor="w")
ttk.Checkbutton(opt, text="Cameras (blue)", variable=self.map_cam).pack(anchor="w")
@ -389,7 +394,7 @@ class FireMapperGUI(tk.Tk):
return
sel = [s for s, v in self.session_vars.items() if v.get()]
if not sel:
messagebox.showwarning("No sessions", "Tick at least one session to plot.")
messagebox.showwarning("No sessions", "Select at least one session to display.")
return
try:
ground = float(self.map_ground.get())

View File

@ -47,20 +47,20 @@
<body>
<header><div class="wrap">
<h1>FireMapper — Session Post-Processing</h1>
<p>User manual for turning a raw capture session into mapping-ready imagery.</p>
<p>User manual for converting a raw capture session into mapping-ready imagery.</p>
</div></header>
<main>
<p>FireMapper records, for every photo, a small <code>.json</code> file next to it
containing GPS position, platform/IMU attitude, lens and timing. These tools read those
files and:</p>
<p>For every image it captures, FireMapper writes a small <code>.json</code> file alongside
it containing the GPS position, platform/IMU attitude, lens parameters and timing. These
tools read those files and:</p>
<ul>
<li><b>Embed</b> the data into the photos' EXIF/XMP so mapping software can place each
image on the map;</li>
<li><b>Stretch</b> the dark 16-bit thermal frames into easy-to-view colour images;</li>
<li><b>Embed</b> the data into each image's EXIF/XMP so that mapping software can position
it geographically;</li>
<li><b>Stretch</b> the low-contrast 16-bit thermal frames into clear colour images;</li>
<li><b>Map</b> the trigger points and image footprints on OpenStreetMap.</li>
</ul>
<p class="note"><b>Your originals are never changed.</b> Every step writes results into a
<p class="note"><b>Original files are never modified.</b> Every step writes its results to a
new folder.</p>
<nav>
@ -76,95 +76,104 @@ new folder.</p>
<section id="start">
<h2>Getting started</h2>
<ol>
<li>Make sure Python 3 is installed, plus the packages:
<li>Ensure Python 3 is installed, together with the required packages:
<pre><code>pip install Pillow numpy tkintermapview requests</code></pre></li>
<li>Put the four scripts (<code>firemapper_gui.py</code>, <code>embed_metadata.py</code>,
<code>stretch_thermal.py</code>, <code>session_map.py</code>) in one folder.
Placing them next to your session folders (or next to a parent folder that groups
several flight strips) lets the program find your data automatically.</li>
<li>Start the program:
<li>Place the four scripts (<code>firemapper_gui.py</code>, <code>embed_metadata.py</code>,
<code>stretch_thermal.py</code>, <code>session_map.py</code>) in a single folder.
Locating them alongside your session folders (or alongside a parent folder that groups
several flight strips) allows the program to detect your data automatically.</li>
<li>Start the application:
<pre><code>python firemapper_gui.py</code></pre>
A window opens with three tabs. Pick a tab, read the on-screen description, fill in
the options, and click the button.</li>
A window opens with three tabs. Select a tab, review the on-screen description, set the
options, and start the operation.</li>
</ol>
<p class="muted">The program finds session folders automatically — any folder that
contains a <code>thermal/</code>, a <code>cam…/</code> folder or a
<code>manifest.json</code> counts as a session, and it also looks one level inside
grouping folders (e.g. <code>Streifen/</code>).</p>
<p class="muted">The program detects session folders automatically: any folder containing a
<code>thermal/</code> directory, a <code>cam…/</code> directory, or a
<code>manifest.json</code> file is treated as a session. Grouping folders (for example
<code>Streifen/</code>) are also scanned one level deep.</p>
</section>
<section id="embed">
<h2><span class="pill blue">Tab 1</span> Embed GPS &amp; Metadata</h2>
<p>Writes each photo's JSON data into a tagged <b>copy</b> of the image: GPS position,
true camera pointing direction, capture time (UTC), lens &amp; exposure, and the full
JSON in the comment field. The copies drop straight into Pix4D, Metashape, QGIS, etc.</p>
<p>Writes each image's JSON data into a tagged <b>copy</b> of the file: GPS position,
true camera pointing direction, capture time (UTC), lens and exposure, and the complete
JSON record in the comment field. The copies are compatible with Pix4D, Metashape, QGIS
and similar applications.</p>
<h3>How to use</h3>
<ol>
<li><b>Session folder</b>auto-filled with the newest session; or click
<li><b>Session folder</b>pre-filled with the most recent session, or selected via
<kbd>Browse…</kbd>.</li>
<li><b>Output folder</b> — leave blank to write to <code>&lt;session&gt;_exif</code>
next to the session.</li>
<li><b>GPS source</b><i>Fused INS (position)</i> is the most accurate (recommended);
<i>Raw GNSS (gps)</i> uses the bare satellite fix. Thermal frames always use raw GNSS.</li>
<li>Click <b>Embed metadata</b>. Progress and a log appear; when done you can open the
output folder.</li>
<li><b>Output folder</b> — if left blank, results are written to
<code>&lt;session&gt;_exif</code> beside the session.</li>
<li><b>GPS source</b><i>Fused INS (position)</i> provides the highest accuracy
(recommended); <i>Raw GNSS (gps)</i> uses the uncorrected satellite fix. Thermal
frames always use raw GNSS.</li>
<li><b>Parallel workers</b> — the number of images copied and tagged concurrently
(default: derived from the available CPU cores). Higher values are faster on
SSD/NVMe storage.</li>
<li>Click <b>Embed metadata</b>. A progress bar and log are displayed; on completion the
output folder can be opened.</li>
</ol>
<p class="muted">Requires <b>exiftool</b>. It is found automatically, and installed for
you the first time if missing (Windows). Leave the “exiftool path” field blank.</p>
<p class="muted">Requires <b>exiftool</b>, which is located automatically and installed on
first use if it is not already present (Windows). Leave the “exiftool path” field blank.</p>
</section>
<section id="thermal">
<h2><span class="pill orange">Tab 2</span> Thermal Stretch</h2>
<p>The thermal camera records 16-bit radiometric frames whose values fill only a tiny
part of the range, so the raw files look almost black and flicker between frames. This
finds <b>one brightness window shared by the whole session</b> and stretches every frame
into it, saving easy-to-view 8-bit images. Because all frames use the same window, hot
and cold areas stay consistent across the flight.</p>
<div class="note">Values are radiometric <b>signal</b> (proportional to temperature),
not calibrated degrees.</div>
<p>The thermal camera records 16-bit radiometric frames that occupy only a small portion
of the available range; consequently the raw files appear nearly black and vary in
brightness from frame to frame. This step determines <b>a single brightness window for the
entire session</b> and rescales every frame into it, producing viewable 8-bit images.
Because all frames share one window, warm and cool areas remain consistent across the
flight.</p>
<div class="note">Pixel values represent radiometric <b>signal</b> (proportional to
temperature), not calibrated degrees.</div>
<h3>Options</h3>
<table>
<tr><th>Palette</th><td><b>inferno</b> / <b>ironbow</b>heat colour maps (dark = cool,
<tr><th>Palette</th><td><b>inferno</b> / <b>ironbow</b>thermal colour maps (dark = cool,
bright = hot); <b>gray</b> — plain grayscale.</td></tr>
<tr><th>Window low % / high %</th><td>The brightness window, as percentiles pooled over
the whole session (default 199 %). Lower the high % / raise the low % for more
contrast; tick <b>Absolute min/max</b> to use the true extremes.</td></tr>
<tr><th>Embed metadata</th><td>When ticked (default), the stretched PNGs also get GPS
and orientation EXIF written in, so they are self-contained.</td></tr>
<tr><th>Window low % / high %</th><td>The brightness window, expressed as percentiles
pooled across the whole session (default 199 %). Decrease the high % or increase the
low % for greater contrast; enable <b>Absolute min/max</b> to use the true extremes.</td></tr>
<tr><th>Embed metadata</th><td>When enabled (default), the stretched PNGs also receive
GPS and orientation EXIF, making them self-contained.</td></tr>
<tr><th>Parallel workers</th><td>The number of frames rendered and tagged concurrently
(default: derived from the available CPU cores). Higher values are faster on SSD/NVMe
storage.</td></tr>
</table>
<h3>How to use</h3>
<ol>
<li>Pick the <b>session folder</b> and <b>palette</b>.</li>
<li>Click <b>Preview palettes</b> to compare gray / inferno / ironbow on a sample
frame before committing.</li>
<li>Click <b>Stretch all frames</b>. Output goes to
<code>&lt;session&gt;/thermal_stretched</code> unless you set an output folder.</li>
<li>Select the <b>session folder</b> and <b>palette</b>.</li>
<li>Click <b>Preview palettes</b> to compare gray, inferno and ironbow on a sample
frame before processing.</li>
<li>Click <b>Stretch all frames</b>. Output is written to
<code>&lt;session&gt;/thermal_stretched</code> unless an output folder is specified.</li>
</ol>
</section>
<section id="map">
<h2><span class="pill blue">Tab 3</span> Map</h2>
<p>Plots each photo as a <b>trigger point</b> (its GPS position) and, optionally, an
<b>oblique footprint</b> — the patch of ground the photo covers — on OpenStreetMap.
Cameras are <span class="pill blue">blue</span>, thermal is
<p>Plots each image as a <b>trigger point</b> (its GPS position) and, optionally, as an
<b>oblique footprint</b> — the area of ground the image covers — on OpenStreetMap.
Cameras are shown in <span class="pill blue">blue</span>, thermal in
<span class="pill orange">orange</span>.</p>
<h3>How to use</h3>
<ol>
<li><b>Tick the sessions</b> to plot (use <kbd>All</kbd>/<kbd>None</kbd>; <kbd>Refresh</kbd>
rescans the folder). Several flight strips can be shown together.</li>
<li>Set the <b>options</b> (see below), then click <b>Show on map</b>. The map zooms to
fit the data.</li>
<li><b>Select the sessions</b> to display (use <kbd>All</kbd>/<kbd>None</kbd>;
<kbd>Refresh</kbd> rescans the folder). Several flight strips may be shown together.</li>
<li>Set the <b>options</b> (described below), then click <b>Show on map</b>. The map
zooms to fit the data.</li>
</ol>
<h3>Options</h3>
<table>
<tr><th>Ground elev (m)</th><td>Terrain height used to project the footprints (flat-ground
assumption). Default 110 m — set this to your site's elevation.</td></tr>
<tr><th>Thermal FOV H/V</th><td>Field of view of the thermal camera, needed for its
assumption). Default 110 m; set this to the elevation of your site.</td></tr>
<tr><th>Thermal FOV H/V</th><td>Field of view of the thermal camera, required for its
footprints. Pre-filled for the FLIR A65 25° lens (25° × 20°).</td></tr>
<tr><th>Thermal off-nadir</th><td>Mounting tilt of the thermal camera (0 = straight down).</td></tr>
<tr><th>Plot every Nth</th><td><i>auto</i> thins very large surveys so the map stays
responsive; set a number to override.</td></tr>
<tr><th>Trigger points / Footprints / Cameras / Thermal</th><td>Toggle what is drawn.</td></tr>
<tr><th>Plot every Nth</th><td><i>auto</i> reduces very large surveys so the map remains
responsive; enter a number to override.</td></tr>
<tr><th>Trigger points / Footprints / Cameras / Thermal</th><td>Control which layers are drawn.</td></tr>
</table>
<div class="note"><b>Needs an internet connection</b> for the map tiles.</div>
<h3>How footprints are computed</h3>
@ -173,20 +182,23 @@ new folder.</p>
each camera's fixed mounting — thermal looks straight down (mounted vertical to the
flight line); the RGB cameras look forward-and-down by their off-nadir angle
(cam25 = 25°, cam45 = 45°) and are mounted landscape across the flight line.</p>
<div class="warn">The footprints assume flat ground at the elevation you set. If the whole
swath appears on the <b>wrong side</b> of the flight line compared to reality, ask your
developer to flip the <code>SCAN_SIGN</code> setting in <code>session_map.py</code>.</div>
<div class="warn">The footprints assume flat ground at the elevation you specify. If the
entire swath appears on the <b>wrong side</b> of the flight line relative to reality, flip
the <code>SCAN_SIGN</code> setting at the top of <code>session_map.py</code>.</div>
</section>
<section id="cli">
<h2>Command line (optional)</h2>
<p>The embed and stretch steps also run without the GUI:</p>
<pre><code>python embed_metadata.py [SESSION] [--out DIR] [--gps-source position|gps] [--dry-run]
<p>The embed and stretch steps may also be run without the graphical interface:</p>
<pre><code>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 1 --hi-pct 99 | --absolute | --lo N --hi N]
[--out DIR] [--no-embed-exif] [--sample]</code></pre>
<p class="muted">With no <code>SESSION</code> they use the newest session found nearby.</p>
[--out DIR] [--no-embed-exif] [--workers N] [--sample]</code></pre>
<p class="muted">If <code>SESSION</code> is omitted, the most recent session found nearby is
used. <code>--workers</code> sets the number of parallel workers (default: derived from the
CPU core count).</p>
</section>
<section id="trouble">
@ -194,7 +206,11 @@ python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray]
<table>
<tr><th>“exiftool could not be found”</th>
<td>Install it once: <code>winget install OliverBetz.ExifTool</code> (or
<code>choco install exiftool</code>), then reopen the program.</td></tr>
<code>choco install exiftool</code>), then restart the application.</td></tr>
<tr><th>Stretched or embedded images have no GPS</th>
<td>exiftool writes metadata via a temporary copy, so a full (or nearly full) disk can
drop the GPS tag. Free up disk space and run the step again. The tools verify that GPS
was written and report a clear error rather than producing files without it.</td></tr>
<tr><th>Map is blank / tiles don't load</th>
<td>The map needs internet access for OpenStreetMap tiles.</td></tr>
<tr><th>No sessions in the Map list</th>
@ -208,7 +224,7 @@ python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray]
</section>
</main>
<footer>FireMapper — Session Post-Processing · GGS Speyer. Originals are never modified;
<footer>FireMapper — Session Post-Processing · GGS Speyer. Original files are never modified;
all results are written to new folders.</footer>
</body>
</html>