Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2199647
Added rudimentary support for T2S+ v2
diminDDL May 29, 2024
62808e1
Added comprehensive support for T2S+ v2
diminDDL May 29, 2024
c642d53
minor additions to calibration procedures and coefficients
diminDDL May 31, 2024
f72a7fd
wip termperature correction register search
diminDDL Jun 4, 2024
8fd9c56
updated readme, removed debug info, added some helpful comments
diminDDL Jun 7, 2024
b6c012b
added if statements to prevent crash when there's no dead pixels
Cuprum77 Jun 18, 2024
75ebbde
wip figuring out all the registers for the T2S+ v2
diminDDL Jun 21, 2024
4fc5425
added offset parameter and simplified reading frames from the camera …
diminDDL Sep 22, 2024
511673c
Merge pull request #1 from diminDDL/dev
diminDDL Sep 22, 2024
c3f051b
updated readme
diminDDL Sep 22, 2024
a723c33
proof of concept lock in thermometry implementation
diminDDL Sep 23, 2024
3050c7c
added lock in thermography to the UI and documentation
diminDDL Sep 28, 2024
56c8d07
Update README.md
diminDDL Sep 28, 2024
6ac4353
renamed the library
diminDDL Sep 28, 2024
227199d
updated readme
diminDDL Sep 28, 2024
7791e3e
Update README.md
diminDDL Sep 30, 2024
ed1364d
Update README.md
diminDDL Nov 10, 2024
bc89a91
Adds pyproject.toml in order to create a venv and fix pyplot.py to pr…
Nov 10, 2024
ff9f440
Auto-format using ruff, add ruff to project dependencies
Nov 10, 2024
0d0fb37
fix variable names in f-strings
Nov 11, 2024
760a266
fix ruff findings
Nov 11, 2024
206349b
fix OpenCV::inRange type error
Nov 11, 2024
eec01bf
Big non-functional refactoring: get rid of stray global variables + f…
Nov 19, 2024
732087a
Merge pull request #2 from frans-fuerst/master
diminDDL Feb 16, 2025
edf7d56
added inverted output mode to lock in, and improved bugs where cv2 so…
diminDDL Oct 9, 2025
30bc6ae
Add hot pixel detection and persistent calibration
tdamsma Mar 24, 2026
0d0d101
Fix OpenCV int cast errors in drawTemperature
tdamsma Mar 24, 2026
f3fa5b0
Replace skimage with cv2 for auto-exposure in opencv.py
tdamsma Mar 24, 2026
da0c35a
Migrate pyproject.toml from poetry to uv format
tdamsma Mar 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
__pycache__
__pycache__
.vscode
*.wpr
*.wpu
*.npy
*.csv
54 changes: 41 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# ht301_hacklib
Thermal camera opencv python lib.
# IRPyThermal
### Thermal camera python library

Supported thermal cameras:
- Hti HT-301
- Xtherm T3S, thanks to Angel-1024!
- Xtherm T2L, T2S+, thanks to Christoph Mair
- Xtherm T3S, thanks to Angel-1024
- Xtherm T2L, T2S+, thanks to Christoph Mair and DiminDDL

It's a very simple hacked together lib, might be useful for somebody,
uses `matplotlib` which is a little bit on the slow side,
It's a very simple hacked together lib, might be useful for somebody,
uses `matplotlib` which is a little bit on the slow side,
or pure `opencv` - much faster but without most features

Tested on ubuntu 20.04 and windows 11:
Upon startup the app automatically looks for supported cameras by matching the resolutions.

Running `python ./pyplot.py` will start the camera stream and show the controls below:
```
$ ./pyplot.py
keys:
'h' - help
'q' - quit
Expand All @@ -30,26 +30,52 @@ keys:
'a', 'z' - auto exposure on/off, auto exposure type
'k', 'l' - set the thermal range to normal/high (only for the TS2+)
'o' - change the camera orientation (rotate 90 degs CW). Only in OpenCV

left, right, up, down - set exposure limits

mouse:
left button - add Region Of Interest (ROI)
right button - add user temperature annotation
```
While the program is running you can use these controls in the camera view to control how it looks and behaves.


You can specify the video device directly with `-d` to the startup command to skip the search routine.

Also for T2S+ V2 users (and potentially for others, where the camera sends raw data and doesn't do onboard processing resulting in the image looking like a pixelated mess) you can specify the `-r` option to tell the program to treat the camera stream as raw sensor data. This will perform a few calibration routines and present you with a usable image doing the sensor processing in python. There is also the `-o` parameter which adds an offset to the entire frames temperature values. This is useful for calibrating the camera since the raw mode sometimes ends up with a constant offset to the image, but this is specific to the camera and thus can be found once and then used for subsequent runs.

### Examples

Example that reads device `/dev/video14` in raw mode and adds an offset of -12.3 to the temperature values:

```
python ./pyplot.py -d /dev/video14 -r -o -12.3
```

![pyplot output](docs/pyplot-output1.png)
![pyplot output](docs/pyplot-output2.png)

View saved ('r' key) raw data file:
```
$ ./pyplot.py 2022-09-11_18-49-07.npy
python ./pyplot.py 2022-09-11_18-49-07.npy
```

Opencv version:
### Lock In Thermography

This feature allows you to use the camera in a lock-in mode. This allows for very precise measurements of hotspots on anything that can be modulated in some way. Examples include silicon dies, circuit boards, people (lock in based on heart rate) and potentially many others.

As mentioned above the device under test needs to be stimulated on command, the script sends commands into a provided serial port to do this.
A `1\n` indicates that we want to turn the supply/stimulant on, a `0\n` turns it off. The modulation frequency, serial port and integration time are all provided as command line arguments.

For example, the below command will use a modulation frequency of 1Hz, an integration time of 10s and send the commands to `/dev/ttyACM0`:

```
$ ./opencv.py
python ./pyplot.py -l 1 -p /dev/ttyACM0 -i 10
```
![opencv output](docs/opencv-output.png)

The modulation frequency should ideally be at least set 10x lower than the camera's frame rate, this is needed to get multiple samples as the sample heats up and cools down. The longer the integration time the more cycles will be "averaged" together, reducing noise, but the longer it takes to get a single frame. More info can be found in the paper linked below.

![lockin output](docs/lock-in.png)

<br>

Expand All @@ -59,8 +85,10 @@ $ ./opencv.py
- https://github.com/MCMH2000/OpenHD_HT301_Driver
- https://github.com/sumpster/ht301_viewer
- https://github.com/cmair/ht301_hacklib
- https://github.com/stawel/ht301_hacklib
- https://github.com/mcguire-steve/ht301_ircam

## Related materials
- https://www.mdpi.com/1424-8220/17/8/1718
- https://www-old.mpi-halle.mpg.de/mpi/publi/pdf/540_02.pdf

90 changes: 90 additions & 0 deletions display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env python3

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Button

# Load the numpy file
data = np.load("./2024-06-21_23-02-55.npy")


# Function to get the pixel number
def get_pixel_number(x, y, width, height, origin=(0, 0)):
zero_x, zero_y = origin
pixel_number = (y - zero_y) * width + (x - zero_x)
return pixel_number


# Click event handler
def onclick(event):
global arrow, rect
x, y = int(event.xdata), int(event.ydata)
height, width = data.shape

if x >= 0 and y >= 0 and x < width and y < height:
pixel_number = get_pixel_number(x, y, width, height, origin=(zero_x, zero_y))
pixel_value = data[y, x]
print(f"Pixel ({x}, {y}) has number: {pixel_number} and value: {pixel_value}")

# Clear the previous highlight
if arrow:
arrow.remove()
if rect:
rect.remove()

# Draw an arrow pointing to the clicked pixel with detailed information
arrow = ax.annotate(
f"({x}, {y})\nNumber: {pixel_number}\nValue: {pixel_value}",
xy=(x, y),
xytext=(x + 10, y + 10),
arrowprops=dict(facecolor="red", shrink=0.05),
bbox=dict(boxstyle="round,pad=0.3", edgecolor="red", facecolor="white"),
)

# Draw a rectangle around the clicked pixel
rect = plt.Rectangle(
(x - 0.5, y - 0.5), 1, 1, edgecolor="red", facecolor="none"
)
ax.add_patch(rect)

fig.canvas.draw()


# Function to highlight all non-zero pixels
def highlight_non_zero(event):
global scatter
if scatter:
scatter.remove()
scatter = None
else:
non_zero_pixels = np.argwhere(data != 0)
y, x = non_zero_pixels[:, 0], non_zero_pixels[:, 1]
scatter = ax.scatter(x, y, color="blue", s=10)

fig.canvas.draw()


# Load and display the image
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2) # Adjust to make space for the button
ax.imshow(data, cmap="gray")

# Define origin
zero_x, zero_y = 0, 192 # Adjust these values to set a different origin

# Initialize annotation and rectangle variables
arrow = None
rect = None
scatter = None

# Connect the click event
cid = fig.canvas.mpl_connect("button_press_event", onclick)

# Add a button to highlight non-zero pixels
ax_button = plt.axes([0.8, 0.05, 0.1, 0.075])
btn = Button(ax_button, "Toggle Non-Zero")
btn.on_clicked(highlight_non_zero)

# plt.tight_layout()

plt.show()
Binary file added docs/lock-in.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 41 additions & 19 deletions example_simple.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
#!/usr/bin/python3
import time
import numpy as np
import ht301_hacklib
#!/usr/bin/env python3

import time
import irpythermal

# create camera object and do a initial calibration
cap = ht301_hacklib.HT301()
cap = irpythermal.HT301()

# read frame (raw ADC data)
ret, frame = cap.read()
Expand All @@ -14,26 +13,49 @@
info, temperature_lookup_table = cap.info()

point = (10, 20)
print('temperature at point:', point, 'is:', temperature_lookup_table[frame[point]], '°C', '[raw ADC value:', frame[point], ']')
#print('additional info:', info)

time.sleep(5) # not needed
print('click, click')
print(
"temperature at point:",
point,
"is:",
temperature_lookup_table[frame[point]],
"°C",
"[raw ADC value:",
frame[point],
"]",
)
# print('additional info:', info)

time.sleep(5) # not needed
print("click, click")
cap.calibrate()
time.sleep(5) # not needed
time.sleep(5) # not needed

# read next frame
ret, frame = cap.read()
info, temperature_lookup_table = cap.info()

print('temperature at point:', point, 'is:', temperature_lookup_table[frame[point]], '°C', '[raw ADC value:', frame[point], ']')
#print('additional info:', info)

print('temperature at any point:', temperature_lookup_table[frame])
print('frame shape:', frame.shape, 'data type:', frame.dtype)
#result: frame shape: (288, 384) data type: uint16
print('T lut shape:', temperature_lookup_table.shape, 'data type:', temperature_lookup_table.dtype)
#result: T lut shape: (16384,) data type: float64
print(
"temperature at point:",
point,
"is:",
temperature_lookup_table[frame[point]],
"°C",
"[raw ADC value:",
frame[point],
"]",
)
# print('additional info:', info)

print("temperature at any point:", temperature_lookup_table[frame])
print("frame shape:", frame.shape, "data type:", frame.dtype)
# result: frame shape: (288, 384) data type: uint16
print(
"T lut shape:",
temperature_lookup_table.shape,
"data type:",
temperature_lookup_table.dtype,
)
# result: T lut shape: (16384,) data type: float64

# release camera object
cap.release()
Loading