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:
parent
4f1ae7f7c4
commit
90a9059544
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -1,11 +1,12 @@
|
||||||
# FireMapper — Session Post-Processing
|
# FireMapper — Session Post-Processing
|
||||||
|
|
||||||
Tools that turn raw FireMapper capture sessions into mapping-ready imagery:
|
Tools that convert raw FireMapper capture sessions into mapping-ready imagery:
|
||||||
embed GPS/orientation metadata into the photos, stretch the radiometric thermal
|
they embed GPS/orientation metadata into the images, stretch the radiometric
|
||||||
frames for viewing, and plot trigger points + image footprints on a map.
|
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,
|
This repository contains **only the Python tools** — the capture data (session
|
||||||
`*_exif/`, `*_stretched/`) is large (tens of GB) and is git-ignored.
|
folders, `*_exif/`, `*_stretched/`) is large (tens of GB) and is git-ignored.
|
||||||
|
|
||||||
## Hardware / data
|
## Hardware / data
|
||||||
|
|
||||||
|
|
@ -92,7 +93,7 @@ Functions:
|
||||||
`XMP-GPano:Pose{Heading,Pitch,Roll}Degrees` (the `XMP-Camera:Pose*` namespace is **not**
|
`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.
|
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
|
- **exiftool** is required by the embed step. Resolved from PATH, a local
|
||||||
`tools/exiftool.exe`, the OliverBetz user install (`%LOCALAPPDATA%\Programs\ExifTool`),
|
`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.
|
(robust to outliers); configurable. Values are radiometric signal, not °C.
|
||||||
- **Footprints** assume **flat ground** at a user-set elevation (default 110 m, the Speyer
|
- **Footprints** assume **flat ground** at a user-set elevation (default 110 m, the Speyer
|
||||||
area). There is no terrain model.
|
area). There is no terrain model.
|
||||||
- **Sweep side:** if footprints/headings come out mirrored vs a known flight, flip
|
- **Parallelism:** both steps fan out across a configurable worker pool
|
||||||
`SCAN_SIGN` at the top of `session_map.py`.
|
(`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
|
- **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
|
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.
|
created with the app as master inside `__init__`, and all vars are parented to it.
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
#!/usr/bin/env python3
|
#!/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,
|
1. Embed GPS & metadata - writes each frame's JSON sidecar (GPS, orientation,
|
||||||
time, lens, ...) into EXIF/XMP of tagged image copies, ready for mapping
|
capture time, lens) into the EXIF/XMP of tagged image copies for use in
|
||||||
software. (wraps embed_metadata.py)
|
mapping software. (wraps embed_metadata.py)
|
||||||
|
|
||||||
2. Thermal stretch - rescales the whole session's 16-bit radiometric
|
2. Thermal stretch - rescales the session's 16-bit radiometric thermal frames
|
||||||
thermal frames into one shared brightness window and saves viewable
|
to a single shared brightness window and saves 8-bit images in the chosen
|
||||||
8-bit images in a heat-map palette. (wraps stretch_thermal.py)
|
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
|
Launch with: python firemapper_gui.py
|
||||||
explanation, and click the button. Long jobs run in the background with a
|
|
||||||
progress bar; your original files are never modified.
|
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
|
from __future__ import annotations
|
||||||
|
|
@ -106,9 +109,9 @@ class FireMapperGUI(tk.Tk):
|
||||||
ttk.Label(head, text="FireMapper - Session Post-Processing",
|
ttk.Label(head, text="FireMapper - Session Post-Processing",
|
||||||
font=("Segoe UI Semibold", 15)).pack(anchor="w")
|
font=("Segoe UI Semibold", 15)).pack(anchor="w")
|
||||||
ttk.Label(head, foreground="#555", wraplength=900, justify="left",
|
ttk.Label(head, foreground="#555", wraplength=900, justify="left",
|
||||||
text="Turn a raw capture session into mapping-ready imagery. "
|
text="Convert a raw capture session into mapping-ready imagery. Select a "
|
||||||
"Pick a session folder, then run either step. Originals are "
|
"session folder and run any step. Original files are never modified; "
|
||||||
"never changed - results are written to new folders.").pack(anchor="w")
|
"all results are written to separate output folders.").pack(anchor="w")
|
||||||
ttk.Separator(self).pack(fill="x", padx=PAD, pady=(6, 8))
|
ttk.Separator(self).pack(fill="x", padx=PAD, pady=(6, 8))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -142,7 +145,8 @@ class FireMapperGUI(tk.Tk):
|
||||||
ttk.Spinbox(row, from_=1, to=(os.cpu_count() or 32), increment=1, width=6,
|
ttk.Spinbox(row, from_=1, to=(os.cpu_count() or 32), increment=1, width=6,
|
||||||
textvariable=self.workers).pack(side="left")
|
textvariable=self.workers).pack(side="left")
|
||||||
ttk.Label(row, foreground="#777",
|
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)
|
).pack(side="left", padx=6)
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
|
@ -165,15 +169,15 @@ class FireMapperGUI(tk.Tk):
|
||||||
tab = ttk.Frame(nb, padding=PAD)
|
tab = ttk.Frame(nb, padding=PAD)
|
||||||
nb.add(tab, text=" 1. Embed GPS & Metadata ")
|
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,
|
self._explain(tab,
|
||||||
"Copies every image in the session and writes its matching JSON data into "
|
"Creates a tagged copy of every image in the session and writes the "
|
||||||
"the copy's EXIF/XMP tags: GPS position, camera heading / pitch / roll, UTC "
|
"corresponding JSON values into its EXIF/XMP metadata: GPS position, camera "
|
||||||
"capture time, lens & exposure, plus the complete JSON in the comment field. "
|
"heading, pitch and roll, UTC capture time, lens and exposure, and the complete "
|
||||||
"The tagged copies drop straight into mapping / photogrammetry tools "
|
"JSON record in the comment field. The tagged copies are compatible with "
|
||||||
"(Pix4D, Metashape, QGIS, ...), which read the embedded GPS to place each "
|
"photogrammetry and GIS applications (Pix4D, Metashape, QGIS), which read the "
|
||||||
"photo on the map. Requires exiftool - it is found automatically, and "
|
"embedded GPS to position each image. Requires exiftool, which is located "
|
||||||
"installed for you the first time if missing."
|
"automatically and installed on first use if it is not already present."
|
||||||
).pack(fill="x", pady=(4, 10))
|
).pack(fill="x", pady=(4, 10))
|
||||||
|
|
||||||
self.embed_session = tk.StringVar(self, value=autodetect_session())
|
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, "Session folder", self.embed_session).pack(fill="x", pady=3)
|
||||||
self._folder_row(tab, "Output folder", self.embed_out).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))
|
foreground="#777").pack(anchor="w", padx=(16, 0))
|
||||||
|
|
||||||
gps = ttk.LabelFrame(tab, text="GPS source for camera frames", padding=8)
|
gps = ttk.LabelFrame(tab, text="GPS source for camera frames", padding=8)
|
||||||
gps.pack(fill="x", pady=(10, 4))
|
gps.pack(fill="x", pady=(10, 4))
|
||||||
ttk.Radiobutton(gps, variable=self.embed_gps, value="position",
|
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",
|
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",
|
ttk.Label(gps, foreground="#777",
|
||||||
text="Thermal frames have no INS solution and always use raw GNSS.").pack(anchor="w")
|
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))
|
adv.pack(fill="x", pady=(8, 2))
|
||||||
ttk.Label(adv, text="exiftool path", width=16).pack(side="left")
|
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.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))
|
self._workers_row(tab).pack(fill="x", pady=(8, 2))
|
||||||
|
|
||||||
|
|
@ -214,15 +218,16 @@ class FireMapperGUI(tk.Tk):
|
||||||
tab = ttk.Frame(nb, padding=PAD)
|
tab = ttk.Frame(nb, padding=PAD)
|
||||||
nb.add(tab, text=" 2. Thermal Stretch ")
|
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,
|
self._explain(tab,
|
||||||
"The thermal camera records 16-bit radiometric frames whose values fill only "
|
"The thermal camera records 16-bit radiometric frames that occupy only a small "
|
||||||
"a tiny part of the range, so raw files look almost black and flicker between "
|
"portion of the available range; consequently the raw files appear nearly black "
|
||||||
"frames. This finds ONE brightness window shared by the whole session and "
|
"and vary in brightness from frame to frame. This step determines a single "
|
||||||
"stretches every frame into it, saving easy-to-view 8-bit images. Because all "
|
"brightness window for the entire session and rescales every frame into it, "
|
||||||
"frames share the window, hot and cold areas stay consistent across the flight. "
|
"producing viewable 8-bit images. Because all frames share one window, warm and "
|
||||||
"Values are radiometric signal (proportional to temperature), not calibrated "
|
"cool areas remain consistent across the flight. Pixel values represent "
|
||||||
"degrees. Tip: click 'Preview palettes' to compare the look before processing."
|
"radiometric signal (proportional to temperature), not calibrated degrees. Use "
|
||||||
|
"'Preview palettes' to compare the available palettes before processing."
|
||||||
).pack(fill="x", pady=(4, 10))
|
).pack(fill="x", pady=(4, 10))
|
||||||
|
|
||||||
self.th_session = tk.StringVar(self, value=autodetect_session())
|
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, "Session folder", self.th_session).pack(fill="x", pady=3)
|
||||||
self._folder_row(tab, "Output folder", self.th_out).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))
|
foreground="#777").pack(anchor="w", padx=(16, 0))
|
||||||
|
|
||||||
opts = ttk.Frame(tab)
|
opts = ttk.Frame(tab)
|
||||||
|
|
@ -252,8 +257,8 @@ class FireMapperGUI(tk.Tk):
|
||||||
|
|
||||||
self.th_embed = tk.BooleanVar(self, value=True)
|
self.th_embed = tk.BooleanVar(self, value=True)
|
||||||
ttk.Checkbutton(tab, variable=self.th_embed,
|
ttk.Checkbutton(tab, variable=self.th_embed,
|
||||||
text="Embed GPS / orientation metadata into the stretched PNGs "
|
text="Embed GPS and orientation metadata into the stretched PNGs "
|
||||||
"(needs exiftool)").pack(anchor="w", pady=(6, 0))
|
"(requires exiftool)").pack(anchor="w", pady=(6, 0))
|
||||||
|
|
||||||
self._workers_row(tab).pack(fill="x", pady=(6, 2))
|
self._workers_row(tab).pack(fill="x", pady=(6, 2))
|
||||||
|
|
||||||
|
|
@ -279,21 +284,21 @@ class FireMapperGUI(tk.Tk):
|
||||||
|
|
||||||
if TkinterMapView is None:
|
if TkinterMapView is None:
|
||||||
ttk.Label(tab, foreground="#a00000", wraplength=600, justify="left",
|
ttk.Label(tab, foreground="#a00000", wraplength=600, justify="left",
|
||||||
text="The map needs the 'tkintermapview' package.\n\n"
|
text="The map requires the 'tkintermapview' package.\n\n"
|
||||||
"Install it from a terminal with:\n"
|
"Install it from a terminal:\n"
|
||||||
" pip install tkintermapview\n\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
|
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")
|
font=HEADING).pack(anchor="w")
|
||||||
self._explain(tab,
|
self._explain(tab,
|
||||||
"Tick the sessions to plot, then click 'Show on map'. Each image becomes a "
|
"Select the sessions to display and click 'Show on map'. Each image is shown as "
|
||||||
"trigger point (its GPS position) and, optionally, an oblique footprint - the "
|
"a trigger point (its GPS position) and, optionally, as an oblique footprint: "
|
||||||
"ground patch the photo covers, projected from the camera off-nadir angle "
|
"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 "
|
"(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 "
|
"attitude and the height above ground. Footprints assume flat terrain at the "
|
||||||
"elevation set below. Needs an internet connection for the map tiles."
|
"elevation specified below. An internet connection is required for the map tiles."
|
||||||
).pack(fill="x", pady=(4, 8))
|
).pack(fill="x", pady=(4, 8))
|
||||||
|
|
||||||
body = ttk.Frame(tab)
|
body = ttk.Frame(tab)
|
||||||
|
|
@ -341,7 +346,7 @@ class FireMapperGUI(tk.Tk):
|
||||||
field("Thermal off-nadir", self.map_toff)
|
field("Thermal off-nadir", self.map_toff)
|
||||||
field("Plot every Nth", self.map_step)
|
field("Plot every Nth", self.map_step)
|
||||||
ttk.Label(opt, foreground="#777",
|
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="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="Footprints", variable=self.map_foot).pack(anchor="w")
|
||||||
ttk.Checkbutton(opt, text="Cameras (blue)", variable=self.map_cam).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
|
return
|
||||||
sel = [s for s, v in self.session_vars.items() if v.get()]
|
sel = [s for s, v in self.session_vars.items() if v.get()]
|
||||||
if not sel:
|
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
|
return
|
||||||
try:
|
try:
|
||||||
ground = float(self.map_ground.get())
|
ground = float(self.map_ground.get())
|
||||||
|
|
|
||||||
158
manual.html
158
manual.html
|
|
@ -47,20 +47,20 @@
|
||||||
<body>
|
<body>
|
||||||
<header><div class="wrap">
|
<header><div class="wrap">
|
||||||
<h1>FireMapper — Session Post-Processing</h1>
|
<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>
|
</div></header>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<p>FireMapper records, for every photo, a small <code>.json</code> file next to it
|
<p>For every image it captures, FireMapper writes a small <code>.json</code> file alongside
|
||||||
containing GPS position, platform/IMU attitude, lens and timing. These tools read those
|
it containing the GPS position, platform/IMU attitude, lens parameters and timing. These
|
||||||
files and:</p>
|
tools read those files and:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Embed</b> the data into the photos' EXIF/XMP so mapping software can place each
|
<li><b>Embed</b> the data into each image's EXIF/XMP so that mapping software can position
|
||||||
image on the map;</li>
|
it geographically;</li>
|
||||||
<li><b>Stretch</b> the dark 16-bit thermal frames into easy-to-view colour images;</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>
|
<li><b>Map</b> the trigger points and image footprints on OpenStreetMap.</li>
|
||||||
</ul>
|
</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>
|
new folder.</p>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
|
|
@ -76,95 +76,104 @@ new folder.</p>
|
||||||
<section id="start">
|
<section id="start">
|
||||||
<h2>Getting started</h2>
|
<h2>Getting started</h2>
|
||||||
<ol>
|
<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>
|
<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>,
|
<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 one folder.
|
<code>stretch_thermal.py</code>, <code>session_map.py</code>) in a single folder.
|
||||||
Placing them next to your session folders (or next to a parent folder that groups
|
Locating them alongside your session folders (or alongside a parent folder that groups
|
||||||
several flight strips) lets the program find your data automatically.</li>
|
several flight strips) allows the program to detect your data automatically.</li>
|
||||||
<li>Start the program:
|
<li>Start the application:
|
||||||
<pre><code>python firemapper_gui.py</code></pre>
|
<pre><code>python firemapper_gui.py</code></pre>
|
||||||
A window opens with three tabs. Pick a tab, read the on-screen description, fill in
|
A window opens with three tabs. Select a tab, review the on-screen description, set the
|
||||||
the options, and click the button.</li>
|
options, and start the operation.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<p class="muted">The program finds session folders automatically — any folder that
|
<p class="muted">The program detects session folders automatically: any folder containing a
|
||||||
contains a <code>thermal/</code>, a <code>cam…/</code> folder or a
|
<code>thermal/</code> directory, a <code>cam…/</code> directory, or a
|
||||||
<code>manifest.json</code> counts as a session, and it also looks one level inside
|
<code>manifest.json</code> file is treated as a session. Grouping folders (for example
|
||||||
grouping folders (e.g. <code>Streifen/</code>).</p>
|
<code>Streifen/</code>) are also scanned one level deep.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="embed">
|
<section id="embed">
|
||||||
<h2><span class="pill blue">Tab 1</span> Embed GPS & Metadata</h2>
|
<h2><span class="pill blue">Tab 1</span> Embed GPS & Metadata</h2>
|
||||||
<p>Writes each photo's JSON data into a tagged <b>copy</b> of the image: GPS position,
|
<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 & exposure, and the full
|
true camera pointing direction, capture time (UTC), lens and exposure, and the complete
|
||||||
JSON in the comment field. The copies drop straight into Pix4D, Metashape, QGIS, etc.</p>
|
JSON record in the comment field. The copies are compatible with Pix4D, Metashape, QGIS
|
||||||
|
and similar applications.</p>
|
||||||
<h3>How to use</h3>
|
<h3>How to use</h3>
|
||||||
<ol>
|
<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>
|
<kbd>Browse…</kbd>.</li>
|
||||||
<li><b>Output folder</b> — leave blank to write to <code><session>_exif</code>
|
<li><b>Output folder</b> — if left blank, results are written to
|
||||||
next to the session.</li>
|
<code><session>_exif</code> beside the session.</li>
|
||||||
<li><b>GPS source</b> — <i>Fused INS (position)</i> is the most accurate (recommended);
|
<li><b>GPS source</b> — <i>Fused INS (position)</i> provides the highest accuracy
|
||||||
<i>Raw GNSS (gps)</i> uses the bare satellite fix. Thermal frames always use raw GNSS.</li>
|
(recommended); <i>Raw GNSS (gps)</i> uses the uncorrected satellite fix. Thermal
|
||||||
<li>Click <b>Embed metadata</b>. Progress and a log appear; when done you can open the
|
frames always use raw GNSS.</li>
|
||||||
output folder.</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>
|
</ol>
|
||||||
<p class="muted">Requires <b>exiftool</b>. It is found automatically, and installed for
|
<p class="muted">Requires <b>exiftool</b>, which is located automatically and installed on
|
||||||
you the first time if missing (Windows). Leave the “exiftool path” field blank.</p>
|
first use if it is not already present (Windows). Leave the “exiftool path” field blank.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="thermal">
|
<section id="thermal">
|
||||||
<h2><span class="pill orange">Tab 2</span> Thermal Stretch</h2>
|
<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
|
<p>The thermal camera records 16-bit radiometric frames that occupy only a small portion
|
||||||
part of the range, so the raw files look almost black and flicker between frames. This
|
of the available range; consequently the raw files appear nearly black and vary in
|
||||||
finds <b>one brightness window shared by the whole session</b> and stretches every frame
|
brightness from frame to frame. This step determines <b>a single brightness window for the
|
||||||
into it, saving easy-to-view 8-bit images. Because all frames use the same window, hot
|
entire session</b> and rescales every frame into it, producing viewable 8-bit images.
|
||||||
and cold areas stay consistent across the flight.</p>
|
Because all frames share one window, warm and cool areas remain consistent across the
|
||||||
<div class="note">Values are radiometric <b>signal</b> (proportional to temperature),
|
flight.</p>
|
||||||
not calibrated degrees.</div>
|
<div class="note">Pixel values represent radiometric <b>signal</b> (proportional to
|
||||||
|
temperature), not calibrated degrees.</div>
|
||||||
<h3>Options</h3>
|
<h3>Options</h3>
|
||||||
<table>
|
<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>
|
bright = hot); <b>gray</b> — plain grayscale.</td></tr>
|
||||||
<tr><th>Window low % / high %</th><td>The brightness window, as percentiles pooled over
|
<tr><th>Window low % / high %</th><td>The brightness window, expressed as percentiles
|
||||||
the whole session (default 1–99 %). Lower the high % / raise the low % for more
|
pooled across the whole session (default 1–99 %). Decrease the high % or increase the
|
||||||
contrast; tick <b>Absolute min/max</b> to use the true extremes.</td></tr>
|
low % for greater contrast; enable <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
|
<tr><th>Embed metadata</th><td>When enabled (default), the stretched PNGs also receive
|
||||||
and orientation EXIF written in, so they are self-contained.</td></tr>
|
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>
|
</table>
|
||||||
<h3>How to use</h3>
|
<h3>How to use</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li>Pick the <b>session folder</b> and <b>palette</b>.</li>
|
<li>Select the <b>session folder</b> and <b>palette</b>.</li>
|
||||||
<li>Click <b>Preview palettes</b> to compare gray / inferno / ironbow on a sample
|
<li>Click <b>Preview palettes</b> to compare gray, inferno and ironbow on a sample
|
||||||
frame before committing.</li>
|
frame before processing.</li>
|
||||||
<li>Click <b>Stretch all frames</b>. Output goes to
|
<li>Click <b>Stretch all frames</b>. Output is written to
|
||||||
<code><session>/thermal_stretched</code> unless you set an output folder.</li>
|
<code><session>/thermal_stretched</code> unless an output folder is specified.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="map">
|
<section id="map">
|
||||||
<h2><span class="pill blue">Tab 3</span> Map</h2>
|
<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
|
<p>Plots each image as a <b>trigger point</b> (its GPS position) and, optionally, as an
|
||||||
<b>oblique footprint</b> — the patch of ground the photo covers — on OpenStreetMap.
|
<b>oblique footprint</b> — the area of ground the image covers — on OpenStreetMap.
|
||||||
Cameras are <span class="pill blue">blue</span>, thermal is
|
Cameras are shown in <span class="pill blue">blue</span>, thermal in
|
||||||
<span class="pill orange">orange</span>.</p>
|
<span class="pill orange">orange</span>.</p>
|
||||||
<h3>How to use</h3>
|
<h3>How to use</h3>
|
||||||
<ol>
|
<ol>
|
||||||
<li><b>Tick the sessions</b> to plot (use <kbd>All</kbd>/<kbd>None</kbd>; <kbd>Refresh</kbd>
|
<li><b>Select the sessions</b> to display (use <kbd>All</kbd>/<kbd>None</kbd>;
|
||||||
rescans the folder). Several flight strips can be shown together.</li>
|
<kbd>Refresh</kbd> rescans the folder). Several flight strips may be shown together.</li>
|
||||||
<li>Set the <b>options</b> (see below), then click <b>Show on map</b>. The map zooms to
|
<li>Set the <b>options</b> (described below), then click <b>Show on map</b>. The map
|
||||||
fit the data.</li>
|
zooms to fit the data.</li>
|
||||||
</ol>
|
</ol>
|
||||||
<h3>Options</h3>
|
<h3>Options</h3>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>Ground elev (m)</th><td>Terrain height used to project the footprints (flat-ground
|
<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>
|
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, needed for its
|
<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>
|
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>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
|
<tr><th>Plot every Nth</th><td><i>auto</i> reduces very large surveys so the map remains
|
||||||
responsive; set a number to override.</td></tr>
|
responsive; enter a number to override.</td></tr>
|
||||||
<tr><th>Trigger points / Footprints / Cameras / Thermal</th><td>Toggle what is drawn.</td></tr>
|
<tr><th>Trigger points / Footprints / Cameras / Thermal</th><td>Control which layers are drawn.</td></tr>
|
||||||
</table>
|
</table>
|
||||||
<div class="note"><b>Needs an internet connection</b> for the map tiles.</div>
|
<div class="note"><b>Needs an internet connection</b> for the map tiles.</div>
|
||||||
<h3>How footprints are computed</h3>
|
<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
|
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
|
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>
|
(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
|
<div class="warn">The footprints assume flat ground at the elevation you specify. If the
|
||||||
swath appears on the <b>wrong side</b> of the flight line compared to reality, ask your
|
entire swath appears on the <b>wrong side</b> of the flight line relative to reality, flip
|
||||||
developer to flip the <code>SCAN_SIGN</code> setting in <code>session_map.py</code>.</div>
|
the <code>SCAN_SIGN</code> setting at the top of <code>session_map.py</code>.</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="cli">
|
<section id="cli">
|
||||||
<h2>Command line (optional)</h2>
|
<h2>Command line (optional)</h2>
|
||||||
<p>The embed and stretch steps also run without the GUI:</p>
|
<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] [--dry-run]
|
<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]
|
python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray]
|
||||||
[--lo-pct 1 --hi-pct 99 | --absolute | --lo N --hi N]
|
[--lo-pct 1 --hi-pct 99 | --absolute | --lo N --hi N]
|
||||||
[--out DIR] [--no-embed-exif] [--sample]</code></pre>
|
[--out DIR] [--no-embed-exif] [--workers N] [--sample]</code></pre>
|
||||||
<p class="muted">With no <code>SESSION</code> they use the newest session found nearby.</p>
|
<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>
|
||||||
|
|
||||||
<section id="trouble">
|
<section id="trouble">
|
||||||
|
|
@ -194,7 +206,11 @@ python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray]
|
||||||
<table>
|
<table>
|
||||||
<tr><th>“exiftool could not be found”</th>
|
<tr><th>“exiftool could not be found”</th>
|
||||||
<td>Install it once: <code>winget install OliverBetz.ExifTool</code> (or
|
<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>
|
<tr><th>Map is blank / tiles don't load</th>
|
||||||
<td>The map needs internet access for OpenStreetMap tiles.</td></tr>
|
<td>The map needs internet access for OpenStreetMap tiles.</td></tr>
|
||||||
<tr><th>No sessions in the Map list</th>
|
<tr><th>No sessions in the Map list</th>
|
||||||
|
|
@ -208,7 +224,7 @@ python stretch_thermal.py [SESSION] [--colormap inferno|ironbow|gray]
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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>
|
all results are written to new folders.</footer>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue