pythermalcamera 0.1.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matthew Wood
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythermalcamera
3
+ Version: 0.1.0
4
+ Summary: A Python library for interacting with the Topdon TC001 thermal camera.
5
+ Author-email: Matthew Wood <matt.wood@corintech.com>
6
+ Project-URL: Homepage, https://github.com/matt-wood-ct/PyThermalCamera
7
+ Project-URL: Bug Tracker, https://github.com/matt-wood-ct/PyThermalCamera/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: opencv-python
15
+ Requires-Dist: numpy
16
+ Dynamic: license-file
17
+
18
+ # pythermalcamera - Topdon TC001 Thermal Camera Library
19
+
20
+ A Python library for interacting with the **Topdon TC001** thermal camera. This library simplifies device discovery, provides a live interactive preview with temperature analysis, and supports capturing full-resolution thermal images with associated JSON metadata.
21
+
22
+ ## Features
23
+
24
+ - **Auto-Detection**: Automatically finds the correct video device ID for the TC001 by scanning `/dev/video*`.
25
+ - **Live Preview**: Interactive window showing the thermal heatmap with a center crosshair, HUD, and real-time statistics (Min/Max/Avg).
26
+ - **Area of Interest (ROI)**: Interactively select a region in the preview to focus temperature analysis (statistics will only reflect the chosen box).
27
+ - **High-Quality Captures**: Save colorized heatmaps as PNG and full temperature metadata as JSON.
28
+ - **Marker Overlays**: Toggleable hotspot and coldspot markers on both the preview and captured images.
29
+ - **Background Threading**: Optionally run the preview in a non-blocking background thread while performing other tasks in the main script.
30
+ - **Extensive Metadata**: Captures include timestamps, raw temperature stats, ROI coordinates, and all rendering settings (colormap, alpha, blur, etc.).
31
+
32
+ ## Prerequisites
33
+
34
+ - **Hardware**: Topdon TC001 Thermal Camera.
35
+ - **Operating System**: Linux (developed and tested on Linux with V4L2).
36
+ - **Dependencies**:
37
+ - `opencv-python`
38
+ - `numpy`
39
+
40
+ ## Installation
41
+
42
+ Ensure you have the dependencies installed:
43
+
44
+ ```bash
45
+ pip install opencv-python numpy
46
+ ```
47
+
48
+ Then, include the `pythermalcamera` package in your project.
49
+
50
+ ## Usage
51
+
52
+ ### Quick Start (Demo Script)
53
+
54
+ Run the included demo to see the library in action:
55
+
56
+ ```bash
57
+ # Auto-detect camera and start interactive preview
58
+ python3 demo_library.py
59
+
60
+ # Run preview in background and take a manual capture after 5 seconds
61
+ python3 demo_library.py --preview
62
+
63
+ # Enable markers on the manual capture
64
+ python3 demo_library.py --markers
65
+ ```
66
+
67
+ ### Basic Library API
68
+
69
+ ```python
70
+ from pythermalcamera import ThermalCamera
71
+ import cv2
72
+
73
+ # Initialize with auto-detection and non-blocking preview
74
+ with ThermalCamera(include_preview=True) as cam:
75
+ # Do something else while the preview runs...
76
+ import time
77
+ time.sleep(5)
78
+
79
+ # Take a manual snapshot with specific settings
80
+ result = cam.capture(
81
+ filename_prefix="Snapshot",
82
+ colormap=cv2.COLORMAP_MAGMA,
83
+ include_markers=True
84
+ )
85
+
86
+ if result:
87
+ print(f"Captured {result['image']}")
88
+ print(f"Max Temp: {result['metadata']['max_temp']}°C")
89
+ ```
90
+
91
+ ## Interactive Controls (Preview Window)
92
+
93
+ When the preview window is active, use the following keyboard shortcuts:
94
+
95
+ | Key | Action |
96
+ |-----|--------|
97
+ | **q** | Quit preview |
98
+ | **p** | Take snapshot (saves PNG + JSON) |
99
+ | **r** | Reset/Clear ROI (Area of Interest) |
100
+ | **m** | Cycle through available colormaps |
101
+ | **h** | Toggle HUD (On-screen statistics) |
102
+ | **k** | Toggle markers (hotspot/coldspot) for snapshots |
103
+ | **a/z** | Increase / Decrease Blur |
104
+ | **s/x** | Increase / Decrease Marker Threshold |
105
+ | **d/c** | Increase / Decrease Display Scale |
106
+ | **f/v** | Increase / Decrease Contrast (Alpha) |
107
+
108
+ **Mouse Controls:**
109
+ - **Left-Click & Drag**: Select an Region of Interest (ROI) box on the preview.
110
+
111
+ ## Metadata Format
112
+
113
+ Snapshots generate a `.json` file containing:
114
+ - `timestamp`: Unix timestamp of the capture.
115
+ - `center_temp`, `max_temp`, `min_temp`, `avg_temp`: Temperature readings in Celsius.
116
+ - `max_pos`, `min_pos`: Pixel coordinates of the hotspot and coldspot.
117
+ - `roi`: Coordinates of the active ROI at the time of capture.
118
+ - `settings`: All rendering parameters used to generate the PNG (colormap, scale, blur, etc.).
119
+
120
+ ## Credits and Attribution
121
+
122
+ This library is inspired by and based on the work of:
123
+ - **Les Wright's PyThermalCamera**: [https://github.com/leswright1977/PyThermalCamera](https://github.com/leswright1977/PyThermalCamera)
124
+ - **Researcher LeoDJ**: For their significant contributions and enhancements to the thermal camera research. Specifically, huge kudos to LeoDJ for reverse engineering the thermal image format to extract raw temperature data.
125
+ - [EEVBlog forum discussion](https://www.eevblog.com/forum/thermal-imaging/infiray-and-their-p2-pro-discussion/200/)
126
+ - [LeoDJ's P2Pro-Viewer GitHub](https://github.com/LeoDJ/P2Pro-Viewer/tree/main)
127
+
128
+ ## License
129
+
130
+ This project is licensed under the MIT License - see the LICENSE file for details (if provided).
@@ -0,0 +1,113 @@
1
+ # pythermalcamera - Topdon TC001 Thermal Camera Library
2
+
3
+ A Python library for interacting with the **Topdon TC001** thermal camera. This library simplifies device discovery, provides a live interactive preview with temperature analysis, and supports capturing full-resolution thermal images with associated JSON metadata.
4
+
5
+ ## Features
6
+
7
+ - **Auto-Detection**: Automatically finds the correct video device ID for the TC001 by scanning `/dev/video*`.
8
+ - **Live Preview**: Interactive window showing the thermal heatmap with a center crosshair, HUD, and real-time statistics (Min/Max/Avg).
9
+ - **Area of Interest (ROI)**: Interactively select a region in the preview to focus temperature analysis (statistics will only reflect the chosen box).
10
+ - **High-Quality Captures**: Save colorized heatmaps as PNG and full temperature metadata as JSON.
11
+ - **Marker Overlays**: Toggleable hotspot and coldspot markers on both the preview and captured images.
12
+ - **Background Threading**: Optionally run the preview in a non-blocking background thread while performing other tasks in the main script.
13
+ - **Extensive Metadata**: Captures include timestamps, raw temperature stats, ROI coordinates, and all rendering settings (colormap, alpha, blur, etc.).
14
+
15
+ ## Prerequisites
16
+
17
+ - **Hardware**: Topdon TC001 Thermal Camera.
18
+ - **Operating System**: Linux (developed and tested on Linux with V4L2).
19
+ - **Dependencies**:
20
+ - `opencv-python`
21
+ - `numpy`
22
+
23
+ ## Installation
24
+
25
+ Ensure you have the dependencies installed:
26
+
27
+ ```bash
28
+ pip install opencv-python numpy
29
+ ```
30
+
31
+ Then, include the `pythermalcamera` package in your project.
32
+
33
+ ## Usage
34
+
35
+ ### Quick Start (Demo Script)
36
+
37
+ Run the included demo to see the library in action:
38
+
39
+ ```bash
40
+ # Auto-detect camera and start interactive preview
41
+ python3 demo_library.py
42
+
43
+ # Run preview in background and take a manual capture after 5 seconds
44
+ python3 demo_library.py --preview
45
+
46
+ # Enable markers on the manual capture
47
+ python3 demo_library.py --markers
48
+ ```
49
+
50
+ ### Basic Library API
51
+
52
+ ```python
53
+ from pythermalcamera import ThermalCamera
54
+ import cv2
55
+
56
+ # Initialize with auto-detection and non-blocking preview
57
+ with ThermalCamera(include_preview=True) as cam:
58
+ # Do something else while the preview runs...
59
+ import time
60
+ time.sleep(5)
61
+
62
+ # Take a manual snapshot with specific settings
63
+ result = cam.capture(
64
+ filename_prefix="Snapshot",
65
+ colormap=cv2.COLORMAP_MAGMA,
66
+ include_markers=True
67
+ )
68
+
69
+ if result:
70
+ print(f"Captured {result['image']}")
71
+ print(f"Max Temp: {result['metadata']['max_temp']}°C")
72
+ ```
73
+
74
+ ## Interactive Controls (Preview Window)
75
+
76
+ When the preview window is active, use the following keyboard shortcuts:
77
+
78
+ | Key | Action |
79
+ |-----|--------|
80
+ | **q** | Quit preview |
81
+ | **p** | Take snapshot (saves PNG + JSON) |
82
+ | **r** | Reset/Clear ROI (Area of Interest) |
83
+ | **m** | Cycle through available colormaps |
84
+ | **h** | Toggle HUD (On-screen statistics) |
85
+ | **k** | Toggle markers (hotspot/coldspot) for snapshots |
86
+ | **a/z** | Increase / Decrease Blur |
87
+ | **s/x** | Increase / Decrease Marker Threshold |
88
+ | **d/c** | Increase / Decrease Display Scale |
89
+ | **f/v** | Increase / Decrease Contrast (Alpha) |
90
+
91
+ **Mouse Controls:**
92
+ - **Left-Click & Drag**: Select an Region of Interest (ROI) box on the preview.
93
+
94
+ ## Metadata Format
95
+
96
+ Snapshots generate a `.json` file containing:
97
+ - `timestamp`: Unix timestamp of the capture.
98
+ - `center_temp`, `max_temp`, `min_temp`, `avg_temp`: Temperature readings in Celsius.
99
+ - `max_pos`, `min_pos`: Pixel coordinates of the hotspot and coldspot.
100
+ - `roi`: Coordinates of the active ROI at the time of capture.
101
+ - `settings`: All rendering parameters used to generate the PNG (colormap, scale, blur, etc.).
102
+
103
+ ## Credits and Attribution
104
+
105
+ This library is inspired by and based on the work of:
106
+ - **Les Wright's PyThermalCamera**: [https://github.com/leswright1977/PyThermalCamera](https://github.com/leswright1977/PyThermalCamera)
107
+ - **Researcher LeoDJ**: For their significant contributions and enhancements to the thermal camera research. Specifically, huge kudos to LeoDJ for reverse engineering the thermal image format to extract raw temperature data.
108
+ - [EEVBlog forum discussion](https://www.eevblog.com/forum/thermal-imaging/infiray-and-their-p2-pro-discussion/200/)
109
+ - [LeoDJ's P2Pro-Viewer GitHub](https://github.com/LeoDJ/P2Pro-Viewer/tree/main)
110
+
111
+ ## License
112
+
113
+ This project is licensed under the MIT License - see the LICENSE file for details (if provided).
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pythermalcamera"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name="Matthew Wood", email="matt.wood@corintech.com" },
10
+ ]
11
+ description = "A Python library for interacting with the Topdon TC001 thermal camera."
12
+ readme = "README.md"
13
+ requires-python = ">=3.7"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+ dependencies = [
20
+ "opencv-python",
21
+ "numpy",
22
+ ]
23
+
24
+ [project.urls]
25
+ "Homepage" = "https://github.com/matt-wood-ct/PyThermalCamera"
26
+ "Bug Tracker" = "https://github.com/matt-wood-ct/PyThermalCamera/issues"
@@ -0,0 +1,14 @@
1
+ """
2
+ Topdon TC001 Thermal Camera Library.
3
+
4
+ This library provides a high-level interface for the Topdon TC001 thermal camera,
5
+ including auto-detection, live preview, and temperature analysis.
6
+
7
+ Based on work by Les Wright (https://github.com/leswright1977/PyThermalCamera)
8
+ and downstream researcher LeoDJ, who reverse engineered the thermal image
9
+ format to extract raw temperature data.
10
+ See LeoDJ's work here: https://github.com/LeoDJ/P2Pro-Viewer
11
+ """
12
+ __version__ = "0.1.0"
13
+
14
+ from .camera import ThermalCamera, ThermalFrame
@@ -0,0 +1,506 @@
1
+ import cv2
2
+ import numpy as np
3
+ import time
4
+ import io
5
+ import os
6
+ import json
7
+ import threading
8
+
9
+ # Based on work by Les Wright (https://github.com/leswright1977/PyThermalCamera)
10
+ # and downstream researcher LeoDJ, who reverse engineered the thermal image
11
+ # format to extract raw temperature data.
12
+ # See LeoDJ's work here: https://github.com/LeoDJ/P2Pro-Viewer
13
+
14
+
15
+ class ThermalFrame:
16
+ """Class representing a single frame from the thermal camera."""
17
+
18
+ def __init__(self, raw_frame, roi=None):
19
+ """
20
+ Initialize a ThermalFrame.
21
+
22
+ :param raw_frame: Raw frame data from the video device.
23
+ :param roi: Optional (x, y, w, h) tuple defining the Area of Interest for statistics.
24
+ """
25
+ self.raw_frame = raw_frame
26
+ self.roi = roi # (x, y, w, h)
27
+ # The frame is 256x384. Top half (256x192) is image data,
28
+ # bottom half (256x192) is thermal data.
29
+ self.imdata, self.thdata = np.array_split(raw_frame, 2)
30
+ self.height, self.width = self.imdata.shape[:2]
31
+ self._process_thermal()
32
+
33
+ def _process_thermal(self):
34
+ """Extract temperature statistics from thermal data."""
35
+ # Convert thermal data to raw temperatures
36
+ # thdata[..., 0] is hi, thdata[..., 1] is lo in the original code's naming,
37
+ # but lo*256 suggests thdata[..., 1] is the MSB.
38
+ thdata_int = self.thdata.astype(np.int32)
39
+ self.raw_temps = thdata_int[..., 0] + thdata_int[..., 1] * 256
40
+
41
+ # Determine analysis region
42
+ if self.roi:
43
+ x, y, w, h = self.roi
44
+ # Ensure ROI is within bounds
45
+ x1 = max(0, min(x, self.width - 1))
46
+ y1 = max(0, min(y, self.height - 1))
47
+ x2 = max(0, min(x + w, self.width))
48
+ y2 = max(0, min(y + h, self.height))
49
+
50
+ roi_temps = self.raw_temps[y1:y2, x1:x2]
51
+
52
+ # Center temperature (relative to ROI center)
53
+ cy, cx = (y1 + y2) // 2, (x1 + x2) // 2
54
+ self.center_temp = self._raw_to_celsius(self.raw_temps[cy, cx])
55
+
56
+ if roi_temps.size > 0:
57
+ # Max temperature in ROI
58
+ max_idx = np.argmax(roi_temps)
59
+ m_roi_col, m_roi_row = divmod(max_idx, roi_temps.shape[1])
60
+ m_col, m_row = y1 + m_roi_col, x1 + m_roi_row
61
+ self.max_temp = self._raw_to_celsius(self.raw_temps[m_col, m_row])
62
+ self.max_pos = (m_row, m_col)
63
+
64
+ # Min temperature in ROI
65
+ min_idx = np.argmin(roi_temps)
66
+ l_roi_col, l_roi_row = divmod(min_idx, roi_temps.shape[1])
67
+ l_col, l_row = y1 + l_roi_col, x1 + l_roi_row
68
+ self.min_temp = self._raw_to_celsius(self.raw_temps[l_col, l_row])
69
+ self.min_pos = (l_row, l_col)
70
+
71
+ # Average temperature in ROI
72
+ self.avg_temp = self._raw_to_celsius(np.mean(roi_temps))
73
+ else:
74
+ # Fallback if ROI is invalid
75
+ self.center_temp = 0
76
+ self.max_temp = 0
77
+ self.max_pos = (0, 0)
78
+ self.min_temp = 0
79
+ self.min_pos = (0, 0)
80
+ self.avg_temp = 0
81
+ else:
82
+ # Full frame stats
83
+ # Center temperature
84
+ cy, cx = self.height // 2, self.width // 2
85
+ self.center_temp = self._raw_to_celsius(self.raw_temps[cy, cx])
86
+
87
+ # Max temperature
88
+ max_idx = np.argmax(self.raw_temps)
89
+ m_col, m_row = divmod(max_idx, self.width)
90
+ self.max_temp = self._raw_to_celsius(self.raw_temps[m_col, m_row])
91
+ self.max_pos = (m_row, m_col)
92
+
93
+ # Min temperature
94
+ min_idx = np.argmin(self.raw_temps)
95
+ l_col, l_row = divmod(min_idx, self.width)
96
+ self.min_temp = self._raw_to_celsius(self.raw_temps[l_col, l_row])
97
+ self.min_pos = (l_row, l_col)
98
+
99
+ # Average temperature
100
+ self.avg_temp = self._raw_to_celsius(np.mean(self.raw_temps))
101
+
102
+ def _raw_to_celsius(self, raw):
103
+ return round((raw / 64) - 273.15, 2)
104
+
105
+ def get_heatmap(self, colormap=cv2.COLORMAP_JET, alpha=1.0, scale=3, blur=0):
106
+ """
107
+ Generate a colorized heatmap from the image data.
108
+
109
+ :param colormap: OpenCV colormap to apply.
110
+ :param alpha: Contrast/Brightness adjustment (default 1.0).
111
+ :param scale: Zoom factor for the display (default 3x).
112
+ :param blur: Gaussian blur factor (default 0).
113
+ :return: Colorized BGR image.
114
+ """
115
+ # Convert YUYV to BGR (OpenCV format)
116
+ bgr = cv2.cvtColor(self.imdata, cv2.COLOR_YUV2BGR_YUYV)
117
+
118
+ if alpha != 1.0:
119
+ bgr = cv2.convertScaleAbs(bgr, alpha=alpha)
120
+
121
+ new_width = self.width * scale
122
+ new_height = self.height * scale
123
+ bgr = cv2.resize(bgr, (new_width, new_height), interpolation=cv2.INTER_CUBIC)
124
+
125
+ if blur > 0:
126
+ bgr = cv2.blur(bgr, (blur, blur))
127
+
128
+ if colormap == "inv_rainbow":
129
+ heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_RAINBOW)
130
+ heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
131
+ else:
132
+ heatmap = cv2.applyColorMap(bgr, colormap)
133
+
134
+ return heatmap
135
+
136
+ class ThermalCamera:
137
+ """Library for interacting with the Topdon TC001 Thermal Camera."""
138
+
139
+ @staticmethod
140
+ def detect_devices():
141
+ """
142
+ Scan /dev/video* devices to find a potential TC001 camera.
143
+ Returns a list of matching device IDs.
144
+ """
145
+ matches = []
146
+ for i in range(16):
147
+ dev_path = f'/dev/video{i}'
148
+ if not os.path.exists(dev_path):
149
+ continue
150
+
151
+ cap = cv2.VideoCapture(dev_path, cv2.CAP_V4L)
152
+ if not cap.isOpened():
153
+ continue
154
+
155
+ # Set to raw mode to check dimensions
156
+ cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
157
+
158
+ # Try a couple of frames to be sure
159
+ for _ in range(5):
160
+ ret, frame = cap.read()
161
+ if not ret or frame is None:
162
+ continue
163
+
164
+ h, w = frame.shape[:2]
165
+ # TC001 is 256x384 (combined image and thermal)
166
+ if (h == 384 and w == 256) or (h == 256 and w == 384):
167
+ matches.append(i)
168
+ break
169
+
170
+ cap.release()
171
+ return matches
172
+
173
+ def __init__(self, device_id=None, include_preview=False):
174
+ """
175
+ Initialize the Thermal Camera.
176
+
177
+ :param device_id: Video device index. If None, the library will attempt auto-detection.
178
+ :param include_preview: If True, starts an interactive live preview in a background thread.
179
+ """
180
+ if device_id is None:
181
+ print("No device ID provided. Attempting to auto-detect Thermal Camera...")
182
+ matches = self.detect_devices()
183
+ if matches:
184
+ device_id = matches[0]
185
+ print(f"Auto-detected Thermal Camera on device {device_id}")
186
+ else:
187
+ device_id = 0
188
+ print("Could not auto-detect. Falling back to device 0.")
189
+
190
+ self.device_id = device_id
191
+ # In Linux, VideoCapture can take a path or device ID
192
+ # The original code uses '/dev/video' + str(dev)
193
+ dev_path = f'/dev/video{device_id}'
194
+ if os.path.exists(dev_path):
195
+ self.cap = cv2.VideoCapture(dev_path, cv2.CAP_V4L)
196
+ else:
197
+ self.cap = cv2.VideoCapture(device_id)
198
+
199
+ if not self.cap.isOpened():
200
+ print(f"Warning: Could not open video device {device_id}")
201
+
202
+ # Crucial: Pull in video but do NOT automatically convert to RGB
203
+ self.cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
204
+
205
+ # Default display settings
206
+ self.colormap = cv2.COLORMAP_JET
207
+ self.alpha = 1.0
208
+ self.scale = 3
209
+ self.blur = 0
210
+ self.threshold = 2
211
+ self.hud = True
212
+ self.include_markers = False
213
+ self.roi = None
214
+
215
+ self._preview_thread = None
216
+ self._stop_preview = threading.Event()
217
+
218
+ if include_preview:
219
+ self._preview_thread = threading.Thread(target=self.live_preview, daemon=True)
220
+ self._preview_thread.start()
221
+
222
+ def get_frame(self, roi=None):
223
+ """
224
+ Capture a single frame and return a ThermalFrame object.
225
+
226
+ :param roi: Optional (x, y, w, h) tuple to override the default ROI for this frame.
227
+ :return: ThermalFrame object or None if capture fails.
228
+ """
229
+ if not self.cap.isOpened():
230
+ return None
231
+ ret, frame = self.cap.read()
232
+ if not ret:
233
+ return None
234
+ # Use provided ROI or fallback to instance ROI
235
+ target_roi = roi if roi is not None else self.roi
236
+ return ThermalFrame(frame, roi=target_roi)
237
+
238
+ def capture(self, filename_prefix="TC001", folder=".", colormap=None,
239
+ alpha=None, scale=None, blur=None, include_markers=None, frame=None):
240
+ """
241
+ Take a snapshot and store both the image and temperature metadata.
242
+
243
+ :param filename_prefix: Prefix for the generated filenames.
244
+ :param folder: Directory where files should be saved.
245
+ :param colormap: Colormap to use (overrides instance default).
246
+ :param alpha: Contrast factor (overrides instance default).
247
+ :param scale: Image scale (overrides instance default).
248
+ :param blur: Blur factor (overrides instance default).
249
+ :param include_markers: Whether to overlay hotspots/coldspots on the saved image.
250
+ :param frame: Optional ThermalFrame to use instead of capturing a new one.
251
+ :return: Dictionary containing 'image', 'metadata_file', and 'metadata' dict, or None.
252
+ """
253
+ if frame is None:
254
+ frame = self.get_frame()
255
+
256
+ if frame is None:
257
+ return None
258
+
259
+ # Use instance variables if parameters are not provided
260
+ colormap = colormap if colormap is not None else self.colormap
261
+ alpha = alpha if alpha is not None else self.alpha
262
+ scale = scale if scale is not None else self.scale
263
+ blur = blur if blur is not None else self.blur
264
+ include_markers = include_markers if include_markers is not None else self.include_markers
265
+
266
+ now = time.strftime("%Y%m%d-%H%M%S")
267
+ img_filename = os.path.join(folder, f"{filename_prefix}_{now}.png")
268
+ meta_filename = os.path.join(folder, f"{filename_prefix}_{now}.json")
269
+
270
+ heatmap = frame.get_heatmap(colormap=colormap, alpha=alpha, scale=scale, blur=blur)
271
+
272
+ if include_markers:
273
+ self._draw_markers(heatmap, frame, scale)
274
+
275
+ cv2.imwrite(img_filename, heatmap)
276
+
277
+ metadata = {
278
+ "timestamp": time.time(),
279
+ "time_str": time.strftime("%Y-%m-%d %H:%M:%S"),
280
+ "center_temp": float(frame.center_temp),
281
+ "max_temp": float(frame.max_temp),
282
+ "min_temp": float(frame.min_temp),
283
+ "avg_temp": float(frame.avg_temp),
284
+ "max_pos": [int(x) for x in frame.max_pos],
285
+ "min_pos": [int(x) for x in frame.min_pos],
286
+ "roi": frame.roi,
287
+ "settings": {
288
+ "colormap": str(colormap),
289
+ "alpha": alpha,
290
+ "scale": scale,
291
+ "blur": blur,
292
+ "include_markers": include_markers,
293
+ "threshold": self.threshold
294
+ }
295
+ }
296
+
297
+ with open(meta_filename, "w") as f:
298
+ json.dump(metadata, f, indent=4)
299
+
300
+ return {
301
+ "image": img_filename,
302
+ "metadata_file": meta_filename,
303
+ "metadata": metadata
304
+ }
305
+
306
+ def live_preview(self, colormap=None, alpha=None, scale=None, blur=None,
307
+ threshold=None, hud=None):
308
+ """
309
+ Start a live preview window with interactive controls.
310
+
311
+ Note: If `include_preview=True` was passed to `__init__`, this is already running
312
+ in a background thread.
313
+
314
+ :param colormap: Initial colormap.
315
+ :param alpha: Initial contrast factor.
316
+ :param scale: Initial display scale.
317
+ :param blur: Initial blur factor.
318
+ :param threshold: Initial marker sensitivity threshold.
319
+ :param hud: Boolean to toggle the on-screen display (HUD).
320
+ """
321
+ cv2.namedWindow('Thermal', cv2.WINDOW_GUI_NORMAL)
322
+
323
+ # Mouse callback for ROI selection
324
+ selection = {"start": None, "current": None, "selecting": False}
325
+
326
+ def mouse_callback(event, x, y, flags, param):
327
+ if event == cv2.EVENT_LBUTTONDOWN:
328
+ selection["start"] = (x, y)
329
+ selection["current"] = (x, y)
330
+ selection["selecting"] = True
331
+ elif event == cv2.EVENT_MOUSEMOVE:
332
+ if selection["selecting"]:
333
+ selection["current"] = (x, y)
334
+ elif event == cv2.EVENT_LBUTTONUP:
335
+ if selection["selecting"]:
336
+ x1, y1 = selection["start"]
337
+ x2, y2 = x, y
338
+ # Normalize coordinates and scale back to thermal frame size
339
+ ix1, ix2 = min(x1, x2) // self.scale, max(x1, x2) // self.scale
340
+ iy1, iy2 = min(y1, y2) // self.scale, max(y1, y2) // self.scale
341
+
342
+ if ix2 - ix1 > 2 and iy2 - iy1 > 2:
343
+ self.roi = (ix1, iy1, ix2 - ix1, iy2 - iy1)
344
+ selection["selecting"] = False
345
+ selection["start"] = None
346
+
347
+ cv2.setMouseCallback('Thermal', mouse_callback)
348
+
349
+ # Override instance variables with parameters if provided
350
+ if colormap is not None: self.colormap = colormap
351
+ if alpha is not None: self.alpha = alpha
352
+ if scale is not None: self.scale = scale
353
+ if blur is not None: self.blur = blur
354
+ if threshold is not None: self.threshold = threshold
355
+ if hud is not None: self.hud = hud
356
+
357
+ colormaps = [
358
+ (cv2.COLORMAP_JET, "Jet"),
359
+ (cv2.COLORMAP_HOT, "Hot"),
360
+ (cv2.COLORMAP_MAGMA, "Magma"),
361
+ (cv2.COLORMAP_INFERNO, "Inferno"),
362
+ (cv2.COLORMAP_PLASMA, "Plasma"),
363
+ (cv2.COLORMAP_BONE, "Bone"),
364
+ (cv2.COLORMAP_SPRING, "Spring"),
365
+ (cv2.COLORMAP_AUTUMN, "Autumn"),
366
+ (cv2.COLORMAP_VIRIDIS, "Viridis"),
367
+ (cv2.COLORMAP_PARULA, "Parula"),
368
+ ("inv_rainbow", "Inv Rainbow")
369
+ ]
370
+
371
+ # Find initial colormap index
372
+ cmap_idx = 0
373
+ for i, (cmap, name) in enumerate(colormaps):
374
+ if cmap == self.colormap:
375
+ cmap_idx = i
376
+ break
377
+
378
+ print("\nInteractive Controls:")
379
+ print(" a/z : Increase/Decrease Blur")
380
+ print(" s/x : Increase/Decrease Floating Label Threshold")
381
+ print(" d/c : Increase/Decrease Scale")
382
+ print(" f/v : Increase/Decrease Contrast (Alpha)")
383
+ print(" m : Cycle ColorMaps")
384
+ print(" h : Toggle HUD")
385
+ print(" k : Toggle Markers in Snapshot")
386
+ print(" r : Clear ROI")
387
+ print(" p : Take Snapshot")
388
+ print(" q : Quit")
389
+
390
+ while not self._stop_preview.is_set():
391
+ frame = self.get_frame()
392
+ if frame is None:
393
+ break
394
+
395
+ current_cmap, cmap_name = colormaps[cmap_idx]
396
+ self.colormap = current_cmap # Sync with instance variable for cycle colormap
397
+
398
+ heatmap = frame.get_heatmap(colormap=self.colormap, alpha=self.alpha, scale=self.scale, blur=self.blur)
399
+
400
+ # Draw Crosshair
401
+ h, w = heatmap.shape[:2]
402
+ cv2.line(heatmap, (w//2, h//2-20), (w//2, h//2+20), (255,255,255), 2)
403
+ cv2.line(heatmap, (w//2-20, h//2), (w//2+20, h//2), (255,255,255), 2)
404
+ cv2.putText(heatmap, f"{frame.center_temp} C", (w//2+10, h//2-10),
405
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0,0,0), 2, cv2.LINE_AA)
406
+ cv2.putText(heatmap, f"{frame.center_temp} C", (w//2+10, h//2-10),
407
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0,255,255), 1, cv2.LINE_AA)
408
+
409
+ if self.hud:
410
+ # Stats box
411
+ cv2.rectangle(heatmap, (0, 0), (180, 120), (0,0,0), -1)
412
+ cv2.putText(heatmap, f"Avg: {frame.avg_temp} C", (10, 15),
413
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
414
+ cv2.putText(heatmap, f"Max: {frame.max_temp} C", (10, 30),
415
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
416
+ cv2.putText(heatmap, f"Min: {frame.min_temp} C", (10, 45),
417
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
418
+ cv2.putText(heatmap, f"Colormap: {cmap_name}", (10, 60),
419
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
420
+ cv2.putText(heatmap, f"Blur: {self.blur}", (10, 75),
421
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
422
+ cv2.putText(heatmap, f"Scale: {self.scale}", (10, 90),
423
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
424
+ cv2.putText(heatmap, f"Contrast: {self.alpha:.1f}", (10, 105),
425
+ cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,255,255), 1, cv2.LINE_AA)
426
+
427
+ # Floating markers
428
+ self._draw_markers(heatmap, frame, self.scale)
429
+
430
+ # Draw active ROI box
431
+ if self.roi:
432
+ rx, ry, rw, rh = self.roi
433
+ cv2.rectangle(heatmap, (rx*self.scale, ry*self.scale),
434
+ ((rx+rw)*self.scale, (ry+rh)*self.scale), (255, 255, 255), 2)
435
+
436
+ # Draw ongoing selection
437
+ if selection["selecting"] and selection["start"] and selection["current"]:
438
+ cv2.rectangle(heatmap, selection["start"], selection["current"], (0, 255, 0), 1)
439
+
440
+ cv2.imshow('Thermal', heatmap)
441
+ key = cv2.waitKey(1) & 0xFF
442
+ if key == ord('q'):
443
+ break
444
+ elif key == ord('p'):
445
+ res = self.capture(frame=frame, filename_prefix="Preview")
446
+ if res:
447
+ print(f"Captured: {res['image']}")
448
+ elif key == ord('a'):
449
+ self.blur += 1
450
+ elif key == ord('z'):
451
+ self.blur = max(0, self.blur - 1)
452
+ elif key == ord('s'):
453
+ self.threshold += 1
454
+ elif key == ord('x'):
455
+ self.threshold = max(0, self.threshold - 1)
456
+ elif key == ord('d'):
457
+ self.scale = min(5, self.scale + 1)
458
+ elif key == ord('c'):
459
+ self.scale = max(1, self.scale - 1)
460
+ elif key == ord('f'):
461
+ self.alpha = min(3.0, self.alpha + 0.1)
462
+ elif key == ord('v'):
463
+ self.alpha = max(0.1, self.alpha - 0.1)
464
+ elif key == ord('m'):
465
+ cmap_idx = (cmap_idx + 1) % len(colormaps)
466
+ self.colormap, _ = colormaps[cmap_idx] # Update instance variable
467
+ elif key == ord('h'):
468
+ self.hud = not self.hud
469
+ elif key == ord('r'):
470
+ self.roi = None
471
+ elif key == ord('k'):
472
+ self.include_markers = not self.include_markers
473
+ print(f"Markers in captures: {'Enabled' if self.include_markers else 'Disabled'}")
474
+
475
+ cv2.destroyAllWindows()
476
+ self._stop_preview.set()
477
+
478
+ def _draw_markers(self, heatmap, frame, scale):
479
+ """Draw hotspot and coldspot markers on the given heatmap."""
480
+ if frame.max_temp > frame.avg_temp + self.threshold:
481
+ mx, my = frame.max_pos
482
+ cv2.circle(heatmap, (mx*scale, my*scale), 5, (0,0,255), -1)
483
+ cv2.putText(heatmap, f"{frame.max_temp} C", (mx*scale+10, my*scale+5),
484
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0,255,255), 1, cv2.LINE_AA)
485
+
486
+ if frame.min_temp < frame.avg_temp - self.threshold:
487
+ lx, ly = frame.min_pos
488
+ cv2.circle(heatmap, (lx*scale, ly*scale), 5, (255,0,0), -1)
489
+ cv2.putText(heatmap, f"{frame.min_temp} C", (lx*scale+10, ly*scale+5),
490
+ cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0,255,255), 1, cv2.LINE_AA)
491
+
492
+ def close(self):
493
+ """Release the camera resources."""
494
+ self._stop_preview.set()
495
+ if self._preview_thread and self._preview_thread.is_alive():
496
+ self._preview_thread.join(timeout=1.0)
497
+
498
+ if self.cap:
499
+ self.cap.release()
500
+ self.cap = None
501
+
502
+ def __enter__(self):
503
+ return self
504
+
505
+ def __exit__(self, exc_type, exc_val, exc_tb):
506
+ self.close()
@@ -0,0 +1,130 @@
1
+ Metadata-Version: 2.4
2
+ Name: pythermalcamera
3
+ Version: 0.1.0
4
+ Summary: A Python library for interacting with the Topdon TC001 thermal camera.
5
+ Author-email: Matthew Wood <matt.wood@corintech.com>
6
+ Project-URL: Homepage, https://github.com/matt-wood-ct/PyThermalCamera
7
+ Project-URL: Bug Tracker, https://github.com/matt-wood-ct/PyThermalCamera/issues
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.7
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: opencv-python
15
+ Requires-Dist: numpy
16
+ Dynamic: license-file
17
+
18
+ # pythermalcamera - Topdon TC001 Thermal Camera Library
19
+
20
+ A Python library for interacting with the **Topdon TC001** thermal camera. This library simplifies device discovery, provides a live interactive preview with temperature analysis, and supports capturing full-resolution thermal images with associated JSON metadata.
21
+
22
+ ## Features
23
+
24
+ - **Auto-Detection**: Automatically finds the correct video device ID for the TC001 by scanning `/dev/video*`.
25
+ - **Live Preview**: Interactive window showing the thermal heatmap with a center crosshair, HUD, and real-time statistics (Min/Max/Avg).
26
+ - **Area of Interest (ROI)**: Interactively select a region in the preview to focus temperature analysis (statistics will only reflect the chosen box).
27
+ - **High-Quality Captures**: Save colorized heatmaps as PNG and full temperature metadata as JSON.
28
+ - **Marker Overlays**: Toggleable hotspot and coldspot markers on both the preview and captured images.
29
+ - **Background Threading**: Optionally run the preview in a non-blocking background thread while performing other tasks in the main script.
30
+ - **Extensive Metadata**: Captures include timestamps, raw temperature stats, ROI coordinates, and all rendering settings (colormap, alpha, blur, etc.).
31
+
32
+ ## Prerequisites
33
+
34
+ - **Hardware**: Topdon TC001 Thermal Camera.
35
+ - **Operating System**: Linux (developed and tested on Linux with V4L2).
36
+ - **Dependencies**:
37
+ - `opencv-python`
38
+ - `numpy`
39
+
40
+ ## Installation
41
+
42
+ Ensure you have the dependencies installed:
43
+
44
+ ```bash
45
+ pip install opencv-python numpy
46
+ ```
47
+
48
+ Then, include the `pythermalcamera` package in your project.
49
+
50
+ ## Usage
51
+
52
+ ### Quick Start (Demo Script)
53
+
54
+ Run the included demo to see the library in action:
55
+
56
+ ```bash
57
+ # Auto-detect camera and start interactive preview
58
+ python3 demo_library.py
59
+
60
+ # Run preview in background and take a manual capture after 5 seconds
61
+ python3 demo_library.py --preview
62
+
63
+ # Enable markers on the manual capture
64
+ python3 demo_library.py --markers
65
+ ```
66
+
67
+ ### Basic Library API
68
+
69
+ ```python
70
+ from pythermalcamera import ThermalCamera
71
+ import cv2
72
+
73
+ # Initialize with auto-detection and non-blocking preview
74
+ with ThermalCamera(include_preview=True) as cam:
75
+ # Do something else while the preview runs...
76
+ import time
77
+ time.sleep(5)
78
+
79
+ # Take a manual snapshot with specific settings
80
+ result = cam.capture(
81
+ filename_prefix="Snapshot",
82
+ colormap=cv2.COLORMAP_MAGMA,
83
+ include_markers=True
84
+ )
85
+
86
+ if result:
87
+ print(f"Captured {result['image']}")
88
+ print(f"Max Temp: {result['metadata']['max_temp']}°C")
89
+ ```
90
+
91
+ ## Interactive Controls (Preview Window)
92
+
93
+ When the preview window is active, use the following keyboard shortcuts:
94
+
95
+ | Key | Action |
96
+ |-----|--------|
97
+ | **q** | Quit preview |
98
+ | **p** | Take snapshot (saves PNG + JSON) |
99
+ | **r** | Reset/Clear ROI (Area of Interest) |
100
+ | **m** | Cycle through available colormaps |
101
+ | **h** | Toggle HUD (On-screen statistics) |
102
+ | **k** | Toggle markers (hotspot/coldspot) for snapshots |
103
+ | **a/z** | Increase / Decrease Blur |
104
+ | **s/x** | Increase / Decrease Marker Threshold |
105
+ | **d/c** | Increase / Decrease Display Scale |
106
+ | **f/v** | Increase / Decrease Contrast (Alpha) |
107
+
108
+ **Mouse Controls:**
109
+ - **Left-Click & Drag**: Select an Region of Interest (ROI) box on the preview.
110
+
111
+ ## Metadata Format
112
+
113
+ Snapshots generate a `.json` file containing:
114
+ - `timestamp`: Unix timestamp of the capture.
115
+ - `center_temp`, `max_temp`, `min_temp`, `avg_temp`: Temperature readings in Celsius.
116
+ - `max_pos`, `min_pos`: Pixel coordinates of the hotspot and coldspot.
117
+ - `roi`: Coordinates of the active ROI at the time of capture.
118
+ - `settings`: All rendering parameters used to generate the PNG (colormap, scale, blur, etc.).
119
+
120
+ ## Credits and Attribution
121
+
122
+ This library is inspired by and based on the work of:
123
+ - **Les Wright's PyThermalCamera**: [https://github.com/leswright1977/PyThermalCamera](https://github.com/leswright1977/PyThermalCamera)
124
+ - **Researcher LeoDJ**: For their significant contributions and enhancements to the thermal camera research. Specifically, huge kudos to LeoDJ for reverse engineering the thermal image format to extract raw temperature data.
125
+ - [EEVBlog forum discussion](https://www.eevblog.com/forum/thermal-imaging/infiray-and-their-p2-pro-discussion/200/)
126
+ - [LeoDJ's P2Pro-Viewer GitHub](https://github.com/LeoDJ/P2Pro-Viewer/tree/main)
127
+
128
+ ## License
129
+
130
+ This project is licensed under the MIT License - see the LICENSE file for details (if provided).
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pythermalcamera/__init__.py
5
+ pythermalcamera/camera.py
6
+ pythermalcamera.egg-info/PKG-INFO
7
+ pythermalcamera.egg-info/SOURCES.txt
8
+ pythermalcamera.egg-info/dependency_links.txt
9
+ pythermalcamera.egg-info/requires.txt
10
+ pythermalcamera.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ opencv-python
2
+ numpy
@@ -0,0 +1 @@
1
+ pythermalcamera
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+