Why can't I read temperatures from the image pixels?
Because the visible image is a colour palette, not a measurement. A radiometric JPEG (“R-JPEG”) is an ordinary JPEG for viewing plus the sensor’s raw data stored inside the file. The colours depend on the palette and the display range chosen when the picture was rendered; the same pixel colour can mean different temperatures in two images. Temperatures come only from the embedded raw data and the camera’s calibration.
OpenDroneKit follows that rule strictly: its thermal code never infers a temperature from a palette colour india_thermal.py:1.
What does OpenDroneKit accept today?
On public main (1ef6351), the thermal loader accepts:
- A sidecar JSON file carrying the raw sensor counts (key "raw", a 2-D array) and the camera's calibration metadata, either under "metadata" or at the top level. This is what `exiftool -b -RawThermalImage` plus a metadata dump produces. thermal.py:244
- Calibration constants PlanckR1, PlanckR2, PlanckB, PlanckF and PlanckO (or planck_r1 ... planck_o). Every one is required. thermal.py:66
- Optional Emissivity (default 0.95), ReflectedApparentTemperature (default 20 C) and AtmosphericTransmission (default 1.0, treated as unity unless the file states otherwise, and that assumption is reported). thermal.py:92
- Raw counts and metadata passed in code: load_radiometric(raw_counts, metadata). thermal.py:232
Given a thermal JPEG directly, from FLIR or DJI, it refuses with this message:
Refusal · core/thermal.py:258
<file>: extracting the embedded radiometric payload from a thermal JPEG is not implemented. Export it first, for example with `exiftool -b -RawThermalImage`, and supply the counts with the camera's Planck constants. The rendered pixels are a palette, not temperatures.It also refuses when:
- Missing calibration constants: "Missing calibration constants: <names>. Without them raw counts cannot be converted to temperature, and guessing a default would change every value in the image." thermal.py:84
- Temperatures outside a physical range (1 K to 5000 K) are range-checked rather than returned. thermal.py:107
- A display range that would render nothing, or a frame with no finite temperatures: ScalingRefused ("The frame carries no finite temperatures to scale."). thermal_scaling.py:118
- Thermal maps for the Hub: "A thermal map requires a non-empty 2D temperature field." and "The thermal image contains no valid temperature measurements." india_thermal.py:257
- RGB/thermal registration refuses fewer than the required operator-supplied tie points, tie points outside either image, or a homography that fails its quality checks ("RGB/thermal registration quality failed: ..."). india_thermal.py:215
How do I get temperatures out of a DJI R-JPEG?
With DJI’s own Thermal SDK (TSDK). DJI does not publish the conversion from its raw data to temperature; the SDK performs it. The SDK ships a command-line tool, dji_irp, whose measure action writes a per-pixel temperature image (INT16 or FLOAT32). Download it, and check which cameras the current version supports, from DJI’s page: DJI Thermal SDK.
ExifTool can read the measurement parameters DJI stores in the file (object distance, humidity, emissivity, reflected temperature), but these are parameters, not the raw thermal array, and not the FLIR-style Planck constants OpenDroneKit’s loader needs (ExifTool DJI tags).
How do I bring a FLIR radiometric JPEG in?
FLIR files record the raw counts and the Planck calibration constants, which is exactly what the loader needs. ExifTool exposes both (ExifTool FLIR tags):
exiftool -b -RawThermalImage IR_0001.jpg > IR_0001_raw.png # raw sensor counts (16-bit)
exiftool -json -PlanckR1 -PlanckR2 -PlanckB -PlanckF -PlanckO -Emissivity -ReflectedApparentTemperature IR_0001.jpg
Put the counts (as a 2-D array under "raw") and the constants in one JSON sidecar, and OpenDroneKit converts it. All five Planck constants are required; a missing one is refused rather than guessed, because a default would change every value in the image thermal.py:84.
{
"raw": [[<counts row 1>], [<counts row 2>], ...],
"metadata": {
"PlanckR1": <from exiftool>, "PlanckR2": <from exiftool>, "PlanckB": <from exiftool>,
"PlanckF": <from exiftool>, "PlanckO": <from exiftool>,
"Emissivity": <optional>, "ReflectedApparentTemperature": <optional, deg C>
}
}
ExifTool writes the counts as an image (PNG or TIFF, depending on the camera); read it into a 2-D array, for example with Python, before writing the sidecar.
How are counts turned into temperature?
With the FLIR-convention Planck model thermal.py:14:
raw = R1 / (R2 · (exp(B / T) − F)) − O
T = B / ln(R1 / (R2 · (raw + O)) + F) T in kelvinA surface with emissivity below one also reflects its surroundings, so the reflected part is removed in count space first: surface = (measured − (1 − ε) · τ · reflected) / (ε · τ) thermal.py:142. Emissivity defaults to 0.95, reflected temperature to 20 °C, and atmospheric transmission is treated as 1 unless the file says otherwise, and that assumption is reported thermal.py:92. Temperatures outside 1 K to 5000 K are refused rather than returned.
What can I do with the temperatures?
- Display scaling. Manual, percentile auto, and anomaly scaling; every scaling reports how many pixels it clipped and the true extremes. Palettes: ironbow and greyscale. thermal_scaling.py:1
- Georeferenced output. A calibrated ThermalImage can be written as a georeferenced GeoTIFF (write_thermal_geotiff) and packaged for the local Hub; india_thermal.py never infers temperature from a colour palette. india_thermal.py:1
- Solar module classification. solar_thermal_anomaly_classifier expects one PV module per image (crops of 24x40 px upsampled to 96). It is a per-module classifier over polygons something else has already located; it cannot find modules in a survey frame. model_registry.json:165
The solar thermal model’s figures and weaknesses are on its model card: balanced accuracy 0.724 on validation, with soiling its weakest class.
Is direct DJI and FLIR decoding planned?
It is in development, not released. Uncommitted work in progress. The data/thermal branch currently points at the same commit as the integration branch (0ae10e4, 2026-09-29) and has no thermal commits of its own; the decoders exist only as uncommitted files in its working tree. Nothing here is released or on main.
core/thermal_dji.py: DJI R-JPEG decoding through the DJI Thermal SDK (TSDK), loaded at run time. The SDK is proprietary and would not ship with OpenDroneKit: the user downloads it under DJI's licence and points the app at it (ODK_DJI_TSDK_DIR or a dji_thermal_sdk_dir setting). Without it the decode is refused with instructions, never approximated.core/thermal_flir.py: A clean-room reader for the FLIR FFF records in APP1 (raw counts and Planck constants, emissivity, distance, atmosphere), written from ExifTool's documented tag tables, not its source.core/thermal_tlinear.py: FLIR TLinear radiometric TIFFs (Duo Pro R, Tau-core cameras) using the two gains documented in the FLIR Duo Pro R User Guide; any other gain is refused.core/thermal.py: Loader changes to route to the decoders above.
No release date is promised. Until it ships, use the DJI Thermal SDK or ExifTool as above, and treat any tool that reads temperatures from palette colours with suspicion.