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.
- pythermalcamera-0.1.0/LICENSE +21 -0
- pythermalcamera-0.1.0/PKG-INFO +130 -0
- pythermalcamera-0.1.0/README.md +113 -0
- pythermalcamera-0.1.0/pyproject.toml +26 -0
- pythermalcamera-0.1.0/pythermalcamera/__init__.py +14 -0
- pythermalcamera-0.1.0/pythermalcamera/camera.py +506 -0
- pythermalcamera-0.1.0/pythermalcamera.egg-info/PKG-INFO +130 -0
- pythermalcamera-0.1.0/pythermalcamera.egg-info/SOURCES.txt +10 -0
- pythermalcamera-0.1.0/pythermalcamera.egg-info/dependency_links.txt +1 -0
- pythermalcamera-0.1.0/pythermalcamera.egg-info/requires.txt +2 -0
- pythermalcamera-0.1.0/pythermalcamera.egg-info/top_level.txt +1 -0
- pythermalcamera-0.1.0/setup.cfg +4 -0
|
@@ -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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pythermalcamera
|