dji-thermal-cli 0.1.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. dji_thermal_cli/__init__.py +20 -0
  2. dji_thermal_cli/__main__.py +4 -0
  3. dji_thermal_cli/_version.py +8 -0
  4. dji_thermal_cli/adjust.py +133 -0
  5. dji_thermal_cli/api.py +592 -0
  6. dji_thermal_cli/checks.py +310 -0
  7. dji_thermal_cli/cli.py +520 -0
  8. dji_thermal_cli/core.py +343 -0
  9. dji_thermal_cli/dataset.py +154 -0
  10. dji_thermal_cli/djixmp.py +27 -0
  11. dji_thermal_cli/exrout.py +71 -0
  12. dji_thermal_cli/files.py +24 -0
  13. dji_thermal_cli/geo.py +61 -0
  14. dji_thermal_cli/listing.py +236 -0
  15. dji_thermal_cli/mosaic.py +685 -0
  16. dji_thermal_cli/outputs.py +121 -0
  17. dji_thermal_cli/privacy.py +100 -0
  18. dji_thermal_cli/py.typed +0 -0
  19. dji_thermal_cli/tiffout.py +294 -0
  20. dji_thermal_cli/tsdk/__init__.py +98 -0
  21. dji_thermal_cli/tsdk/_cnum.py +58 -0
  22. dji_thermal_cli/tsdk/base.py +184 -0
  23. dji_thermal_cli/tsdk/data/dji_tsdk_v18.npz +0 -0
  24. dji_thermal_cli/tsdk/dirp.py +518 -0
  25. dji_thermal_cli/tsdk/dirp_isp.py +222 -0
  26. dji_thermal_cli/tsdk/errors.py +54 -0
  27. dji_thermal_cli/tsdk/girp.py +231 -0
  28. dji_thermal_cli/tsdk/girp_isp.py +100 -0
  29. dji_thermal_cli/tsdk/hirp.py +425 -0
  30. dji_thermal_cli/tsdk/hirp_isp.py +1502 -0
  31. dji_thermal_cli/tsdk/hirp_mt.py +1140 -0
  32. dji_thermal_cli/tsdk/iirp.py +395 -0
  33. dji_thermal_cli/tsdk/iirp_isp.py +720 -0
  34. dji_thermal_cli/tsdk/jpeg.py +111 -0
  35. dji_thermal_cli/tsdk/powf32.py +110 -0
  36. dji_thermal_cli/tsdk/tables.py +137 -0
  37. dji_thermal_cli/units.py +58 -0
  38. dji_thermal_cli/webodm.py +203 -0
  39. dji_thermal_cli/xmpedit.py +69 -0
  40. dji_thermal_cli-0.1.0rc1.dist-info/METADATA +506 -0
  41. dji_thermal_cli-0.1.0rc1.dist-info/RECORD +45 -0
  42. dji_thermal_cli-0.1.0rc1.dist-info/WHEEL +4 -0
  43. dji_thermal_cli-0.1.0rc1.dist-info/entry_points.txt +3 -0
  44. dji_thermal_cli-0.1.0rc1.dist-info/licenses/LICENSE +661 -0
  45. dji_thermal_cli-0.1.0rc1.dist-info/licenses/LICENSES/optimized-routines.txt +249 -0
@@ -0,0 +1,20 @@
1
+ """dji-thermal-cli: DJI R-JPEG thermal images to temperatures, in pure Python.
2
+
3
+ A command line (`dji-thermal-cli`) and the same functionality as a Python API:
4
+
5
+ import dji_thermal_cli as dtc
6
+
7
+ dtc.check("flight/").findings
8
+ dtc.measure("flight/DJI_0001_T.JPG", unit="F").temperature
9
+ dtc.process("flight/", "out/", dtc.Settings(emissivity=0.95), outputs=["temp_c"])
10
+
11
+ See dji_thermal_cli.api for everything available; dji_thermal_cli.tsdk is the port of DJI's
12
+ Thermal SDK v1.8 underneath.
13
+ """
14
+
15
+ from ._version import __version__
16
+ from .api import * # noqa: F403 - the public API, listed in api.__all__
17
+ from .api import __all__ as _api_all
18
+ from .cli import main
19
+
20
+ __all__ = [*_api_all, "main", "__version__"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,8 @@
1
+ """The package version, read from the installed distribution (pyproject.toml is its one source)."""
2
+
3
+ from importlib import metadata
4
+
5
+ try:
6
+ __version__ = metadata.version("dji-thermal-cli")
7
+ except metadata.PackageNotFoundError: # a source tree that isn't installed
8
+ __version__ = "0+unknown"
@@ -0,0 +1,133 @@
1
+ """Dataset-wide adjustments applied on top of the SDK's measurement (the SDK is untouched).
2
+
3
+ - Gimbal override: one yaw, pitch and/or roll for every file, for when the recorded angle
4
+ is known to be wrong. It places frames in the mosaic and replaces the drone-dji
5
+ Gimbal*Degree values in the outputs' XMP (the originals are kept under dtc:).
6
+ - Out-of-range replacement, per pixel: every pixel above (or below) a threshold gets a given
7
+ value, or becomes no data. It applies to the CLI's temperature outputs (temp_c, temp_f,
8
+ temp_k, temp_ck, gray16) and the mosaic; temp_dc, raw and pseudo are the SDK's own and are
9
+ left as they are.
10
+ - Location-free outputs (strip_location): see privacy.
11
+ - record(): every output's XMP gets a dtc: record of what was done: the measurement
12
+ parameters actually used, the gimbal override and the replacement thresholds and counts.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import dataclasses
18
+ import numpy as np
19
+
20
+ from . import tiffout, xmpedit
21
+ from ._version import __version__
22
+
23
+ NO_DATA = tiffout.NO_DATA_C # a replacement value of "no data"
24
+ GIMBAL_AXES = ("yaw", "pitch", "roll")
25
+
26
+
27
+ @dataclasses.dataclass(frozen=True)
28
+ class Adjustments:
29
+ """None = not set. Temperatures are Celsius; a replacement value of NO_DATA marks the
30
+ pixels as no data."""
31
+
32
+ gimbal_yaw: float | None = None
33
+ gimbal_pitch: float | None = None
34
+ gimbal_roll: float | None = None
35
+ replace_above: float | None = None
36
+ replace_above_value: float | None = None
37
+ replace_below: float | None = None
38
+ replace_below_value: float | None = None
39
+ strip_location: bool = False # outputs without location or identifiers (privacy)
40
+
41
+ def gimbal(self) -> dict[str, float]:
42
+ """The overridden axes, {"pitch": -90.0, ...}."""
43
+ return {axis: v for axis in GIMBAL_AXES if (v := getattr(self, f"gimbal_{axis}")) is not None}
44
+
45
+ def replaces(self) -> bool:
46
+ return self.replace_above is not None or self.replace_below is not None
47
+
48
+
49
+ @dataclasses.dataclass
50
+ class Replaced:
51
+ above: int = 0
52
+ below: int = 0
53
+
54
+
55
+ def replace(temp_c: np.ndarray, adj: Adjustments) -> tuple[np.ndarray, Replaced]:
56
+ """A copy of the float32 Celsius frame with out-of-range pixels replaced, and the counts.
57
+ Pixels that are already no data (the SDK's floor) are left alone."""
58
+ out = np.array(temp_c, dtype=np.float32, copy=True)
59
+ counts = Replaced()
60
+ valid = out > np.float32(NO_DATA)
61
+ if adj.replace_above is not None:
62
+ hit = valid & (out > np.float32(adj.replace_above))
63
+ counts.above = int(hit.sum())
64
+ out[hit] = np.float32(adj.replace_above_value)
65
+ if adj.replace_below is not None:
66
+ hit = valid & (temp_c < np.float32(adj.replace_below)) # judged on the measured values
67
+ counts.below = int(hit.sum())
68
+ out[hit] = np.float32(adj.replace_below_value)
69
+ return out, counts
70
+
71
+
72
+ def remove_offset(temp_c: np.ndarray, offset_c: float) -> np.ndarray:
73
+ """The frame minus its normalization offset (computed in double, rounded once to float32);
74
+ no-data pixels stay no data."""
75
+ t = np.asarray(temp_c, dtype=np.float32)
76
+ out = (t.astype(np.float64) - offset_c).astype(np.float32)
77
+ return np.where(t > np.float32(NO_DATA), out, t)
78
+
79
+
80
+ def _num(v: float) -> str:
81
+ return f"{v:.4f}".rstrip("0").rstrip(".")
82
+
83
+
84
+ def _value(v: float) -> str:
85
+ return "nodata" if v == NO_DATA else _num(v)
86
+
87
+
88
+ def record(xmp: bytes | None, *, params: dict, distance_source: str | None, adj: Adjustments,
89
+ replaced: Replaced | None, original_gimbal: dict[str, float | None], offset_c: float = 0.0) -> bytes:
90
+ """The output XMP: the gimbal override applied to drone-dji, and the dtc: record."""
91
+ gimbal = adj.gimbal()
92
+ if gimbal:
93
+ xmp = xmpedit.set_properties(xmp, xmpedit.DRONE_DJI,
94
+ {f"Gimbal{a.capitalize()}Degree": f"{v:+.2f}" for a, v in gimbal.items()})
95
+ props = {"Software": f"dji-thermal-cli {__version__}"}
96
+ names = {"distance": "Distance", "humidity": "Humidity", "emissivity": "Emissivity",
97
+ "reflection": "ReflectedTemperatureC", "ambient_temp": "AmbientTemperatureC"}
98
+ for key, name in names.items():
99
+ if params.get(key) is not None:
100
+ props[name] = _num(float(params[key]))
101
+ if distance_source:
102
+ props["DistanceSource"] = distance_source
103
+ for axis, v in gimbal.items():
104
+ props[f"Gimbal{axis.capitalize()}Override"] = f"{v:+.2f}"
105
+ original = original_gimbal.get(axis)
106
+ props[f"OriginalGimbal{axis.capitalize()}Degree"] = "" if original is None else f"{original:+.2f}"
107
+ if offset_c:
108
+ props["NormalizationOffsetC"] = _num(offset_c)
109
+ if adj.strip_location:
110
+ props["LocationRemoved"] = "True"
111
+ if adj.replace_above is not None:
112
+ props["ReplaceAboveC"] = _num(adj.replace_above)
113
+ props["ReplaceAboveValueC"] = _value(adj.replace_above_value)
114
+ props["ReplacedAbovePixels"] = str(replaced.above if replaced else 0)
115
+ if adj.replace_below is not None:
116
+ props["ReplaceBelowC"] = _num(adj.replace_below)
117
+ props["ReplaceBelowValueC"] = _value(adj.replace_below_value)
118
+ props["ReplacedBelowPixels"] = str(replaced.below if replaced else 0)
119
+ return xmpedit.set_properties(xmp, xmpedit.DTC, props)
120
+
121
+
122
+ def describe(adj: Adjustments) -> dict:
123
+ """The adjustments as plain data, for reports."""
124
+ out = {f"gimbal_{a}": v for a, v in adj.gimbal().items()}
125
+ if adj.replace_above is not None:
126
+ out["replace_above_c"] = adj.replace_above
127
+ out["replace_above_value_c"] = _value(adj.replace_above_value)
128
+ if adj.replace_below is not None:
129
+ out["replace_below_c"] = adj.replace_below
130
+ out["replace_below_value_c"] = _value(adj.replace_below_value)
131
+ if adj.strip_location:
132
+ out["location_removed"] = True
133
+ return out