Initial commit

This commit is contained in:
2025-05-27 18:44:40 +02:00
commit deb475ff78
4 changed files with 366 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.idea/
View File
+363
View File
@@ -0,0 +1,363 @@
#!/usr/bin/env python3
'''
Les Wright 21 June 2023
https://youtube.com/leslaboratory
A Python program to read, parse and display thermal data from the Topdon TC001 Thermal camera!
'''
import cv2
import numpy as np
import argparse
import time
import io
def main():
# 256x192 General settings
width = 256 # Sensor width
height = 192 # sensor height
scale = 3 # scale multiplier
newWidth = width * scale
newHeight = height * scale
alpha = 1.0 # Contrast control (1.0-3.0)
colormap = -1
font = cv2.FONT_HERSHEY_SIMPLEX
dispFullscreen = False
cv2.namedWindow('Thermal', cv2.WINDOW_GUI_NORMAL)
cv2.resizeWindow('Thermal', newWidth, newHeight)
rad = 0 # blur radius
threshold = 2
hud = True
recording = False
elapsed = "00:00:00"
snaptime = "None"
# We need to know if we are running on the Pi, because openCV behaves a little oddly on all the builds!
# https://raspberrypi.stackexchange.com/questions/5100/detect-that-a-python-program-is-running-on-the-pi
def is_raspberrypi():
try:
with io.open('/sys/firmware/devicetree/base/model', 'r') as m:
if 'raspberry pi' in m.read().lower(): return True
except Exception:
pass
return False
isPi = is_raspberrypi()
parser = argparse.ArgumentParser()
parser.add_argument("--device", type=int, default=0, help="Video Device number e.g. 0, use v4l2-ctl --list-devices")
args = parser.parse_args()
if args.device:
dev = args.device
else:
dev = 0
# init video
cap = cv2.VideoCapture('/dev/video' + str(dev), cv2.CAP_V4L)
# cap = cv2.VideoCapture(0)
# pull in the video but do NOT automatically convert to RGB, else it breaks the temperature data!
# https://stackoverflow.com/questions/63108721/opencv-setting-videocap-property-to-cap-prop-convert-rgb-generates-weird-boolean
if isPi == True:
cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
else:
cap.set(cv2.CAP_PROP_CONVERT_RGB, 0.0)
def rec():
now = time.strftime("%Y%m%d--%H%M%S")
# do NOT use mp4 here, it is flakey!
videoOut = cv2.VideoWriter(now + 'output.avi', cv2.VideoWriter_fourcc(*'XVID'), 25, (newWidth, newHeight))
return (videoOut)
def snapshot(heatmap):
# I would put colons in here, but it Win throws a fit if you try and open them!
now = time.strftime("%Y%m%d-%H%M%S")
snaptime = time.strftime("%H:%M:%S")
cv2.imwrite("TC001" + now + ".png", heatmap)
return snaptime
P2Pro_resolution = (256, 384)
while (cap.isOpened()):
# Capture frame-by-frame
ret, frame = cap.read()
if ret == True:
frame = frame[0]
# split video frame (top is pseudo color, bottom is temperature data)
frame_mid_pos = int(len(frame) / 2)
picture_data = frame[0:frame_mid_pos]
thermal_data = frame[frame_mid_pos:]
# convert buffers to numpy arrays
yuv_picture = np.frombuffer(picture_data, dtype=np.uint8).reshape(
(height, width, 2))
rgb_picture = cv2.cvtColor(yuv_picture, cv2.COLOR_YUV2RGB_YUY2)
thermal_picture_16 = np.frombuffer(thermal_data, dtype=np.uint16).reshape(
(height, width))
thermal_picture_8 = np.frombuffer(thermal_data, dtype=np.uint8).reshape(
(height, width, 2))
imdata = yuv_picture
thdata = thermal_picture_16
# imdata, thdata = np.array_split(frame, 2)
# now parse the data from the bottom frame and convert to temp!
# https://www.eevblog.com/forum/thermal-imaging/infiray-and-their-p2-pro-discussion/200/
# Huge props to LeoDJ for figuring out how the data is stored and how to compute temp from it.
# grab data from the center pixel...
rawtemp = thermal_picture_16[96][128]
temp = (rawtemp / 64) - 273.15
temp = round(temp, 2)
# find the max temperature in the frame
maxtemp = thdata[...].max()
maxtemp = (maxtemp / 64) - 273.15
maxtemp = round(maxtemp, 2)
posmax = thdata[...].argmax()
#since argmax returns a linear index, convert back to row and col
mcol, mrow = divmod(posmax, width)
# find the lowest temperature in the frame
mintemp = thdata[...].min()
mintemp = (mintemp / 64) - 273.15
mintemp = round(mintemp, 2)
posmin = thdata[...].argmin()
lcol, lrow = divmod(posmin, width)
# find the average temperature in the frame
avgtemp = thdata[...].mean()
avgtemp = (avgtemp / 64) - 273.15
avgtemp = round(avgtemp, 2)
# Convert the real image to RGB
bgr = cv2.cvtColor(imdata, cv2.COLOR_YUV2BGR_YUYV)
# Contrast
bgr = cv2.convertScaleAbs(bgr, alpha=alpha) # Contrast
# bicubic interpolate, upscale and blur
bgr = cv2.resize(bgr, (newWidth, newHeight), interpolation=cv2.INTER_CUBIC) # Scale up!
if rad > 0:
bgr = cv2.blur(bgr, (rad, rad))
# apply colormap
if colormap == -1:
heatmap = bgr
cmapText = 'None'
if colormap == 0:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_JET)
cmapText = 'Jet'
if colormap == 1:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_HOT)
cmapText = 'Hot'
if colormap == 2:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_MAGMA)
cmapText = 'Magma'
if colormap == 3:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_INFERNO)
cmapText = 'Inferno'
if colormap == 4:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_PLASMA)
cmapText = 'Plasma'
if colormap == 5:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_BONE)
cmapText = 'Bone'
if colormap == 6:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_SPRING)
cmapText = 'Spring'
if colormap == 7:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_AUTUMN)
cmapText = 'Autumn'
if colormap == 8:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_VIRIDIS)
cmapText = 'Viridis'
if colormap == 9:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_PARULA)
cmapText = 'Parula'
if colormap == 10:
heatmap = cv2.applyColorMap(bgr, cv2.COLORMAP_RAINBOW)
heatmap = cv2.cvtColor(heatmap, cv2.COLOR_BGR2RGB)
cmapText = 'Inv Rainbow'
# print(heatmap.shape)
# draw crosshairs
cv2.line(heatmap, (int(newWidth / 2), int(newHeight / 2) + 20), \
(int(newWidth / 2), int(newHeight / 2) - 20), (255, 255, 255), 2) # vline
cv2.line(heatmap, (int(newWidth / 2) + 20, int(newHeight / 2)), \
(int(newWidth / 2) - 20, int(newHeight / 2)), (255, 255, 255), 2) # hline
cv2.line(heatmap, (int(newWidth / 2), int(newHeight / 2) + 20), \
(int(newWidth / 2), int(newHeight / 2) - 20), (0, 0, 0), 1) # vline
cv2.line(heatmap, (int(newWidth / 2) + 20, int(newHeight / 2)), \
(int(newWidth / 2) - 20, int(newHeight / 2)), (0, 0, 0), 1) # hline
# show temp
cv2.putText(heatmap, str(temp) + ' C', (int(newWidth / 2) + 10, int(newHeight / 2) - 10), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 0), 2, cv2.LINE_AA)
cv2.putText(heatmap, str(temp) + ' C', (int(newWidth / 2) + 10, int(newHeight / 2) - 10), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 255, 255), 1, cv2.LINE_AA)
offset = 0
if hud == True:
# display black box for our data
cv2.rectangle(heatmap, (0, 0), (160, 120), (0, 0, 0), -1)
# put text in the box
cv2.putText(heatmap, 'Avg Temp: ' + str(avgtemp) + ' C', (10, 14+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Label Threshold: ' + str(threshold) + ' C', (10, 28+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Colormap: ' + cmapText, (10, 42+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Blur: ' + str(rad) + ' ', (10, 56+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Scaling: ' + str(scale) + ' ', (10, 70+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Contrast: ' + str(alpha) + ' ', (10, 84+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
cv2.putText(heatmap, 'Snapshot: ' + snaptime + ' ', (10, 98+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 255), 1, cv2.LINE_AA)
if recording == False:
cv2.putText(heatmap, 'Recording: ' + elapsed, (10, 112+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (200, 200, 200), 1, cv2.LINE_AA)
if recording == True:
cv2.putText(heatmap, 'Recording: ' + elapsed, (10, 112+offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.4, (40, 40, 255), 1, cv2.LINE_AA)
# Yeah, this looks like we can probably do this next bit more efficiently!
# display floating max temp
if maxtemp > avgtemp + threshold:
cv2.circle(heatmap, (mrow * scale, mcol * scale), 5, (0, 0, 0), 2)
cv2.circle(heatmap, (mrow * scale, mcol * scale), 5, (0, 0, 255), -1)
cv2.putText(heatmap, str(maxtemp) + ' C', ((mrow * scale) + 10, (mcol * scale) + 5 + offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 0), 2, cv2.LINE_AA)
cv2.putText(heatmap, str(maxtemp) + ' C', ((mrow * scale) + 10, (mcol * scale) + 5 + offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 255, 255), 1, cv2.LINE_AA)
# display floating min temp
if mintemp < avgtemp - threshold:
cv2.circle(heatmap, (lrow * scale, lcol * scale), 5, (0, 0, 0), 2)
cv2.circle(heatmap, (lrow * scale, lcol * scale), 5, (255, 0, 0), -1)
cv2.putText(heatmap, str(mintemp) + ' C', ((lrow * scale) + 10, (lcol * scale) + 5 + offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 0, 0), 2, cv2.LINE_AA)
cv2.putText(heatmap, str(mintemp) + ' C', ((lrow * scale) + 10, (lcol * scale) + 5 + offset), \
cv2.FONT_HERSHEY_SIMPLEX, 0.45, (0, 255, 255), 1, cv2.LINE_AA)
# display image
cv2.imshow('Thermal', heatmap)
if recording == True:
elapsed = (time.time() - start)
elapsed = time.strftime("%H:%M:%S", time.gmtime(elapsed))
# print(elapsed)
videoOut.write(heatmap)
keyPress = cv2.waitKey(1)
if keyPress == ord('a'): # Increase blur radius
rad += 1
if keyPress == ord('z'): # Decrease blur radius
rad -= 1
if rad <= 0:
rad = 0
if keyPress == ord('s'): # Increase threshold
threshold += 1
if keyPress == ord('x'): # Decrease threashold
threshold -= 1
if threshold <= 0:
threshold = 0
if keyPress == ord('d'): # Increase scale
scale += 1
if scale >= 5:
scale = 5
newWidth = width * scale
newHeight = height * scale
if dispFullscreen == False and isPi == False:
cv2.resizeWindow('Thermal', newWidth, newHeight)
if keyPress == ord('c'): # Decrease scale
scale -= 1
if scale <= 1:
scale = 1
newWidth = width * scale
newHeight = height * scale
if dispFullscreen == False and isPi == False:
cv2.resizeWindow('Thermal', newWidth, newHeight)
if keyPress == ord('q'): # enable fullscreen
dispFullscreen = True
cv2.namedWindow('Thermal', cv2.WND_PROP_FULLSCREEN)
cv2.setWindowProperty('Thermal', cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
if keyPress == ord('w'): # disable fullscreen
dispFullscreen = False
cv2.namedWindow('Thermal', cv2.WINDOW_GUI_NORMAL)
cv2.setWindowProperty('Thermal', cv2.WND_PROP_AUTOSIZE, cv2.WINDOW_GUI_NORMAL)
cv2.resizeWindow('Thermal', newWidth, newHeight)
if keyPress == ord('f'): # contrast+
alpha += 0.1
alpha = round(alpha, 1) # fix round error
if alpha >= 3.0:
alpha = 3.0
if keyPress == ord('v'): # contrast-
alpha -= 0.1
alpha = round(alpha, 1) # fix round error
if alpha <= 0:
alpha = 0.0
if keyPress == ord('h'):
if hud == True:
hud = False
elif hud == False:
hud = True
if keyPress == ord('m'): # m to cycle through color maps
colormap += 1
if colormap == 11:
colormap = -1
if keyPress == ord('r') and recording == False: # r to start reording
videoOut = rec()
recording = True
start = time.time()
if keyPress == ord('t'): # f to finish reording
recording = False
elapsed = "00:00:00"
if keyPress == ord('p'): # f to finish reording
snaptime = snapshot(heatmap)
if keyPress == ord('q'):
break
capture.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
print('Les Wright 21 June 2023')
print('https://youtube.com/leslaboratory')
print('A Python program to read, parse and display thermal data from the Topdon TC001 Thermal camera!')
print('')
print('Tested on Debian all features are working correctly')
print('This will work on the Pi However a number of workarounds are implemented!')
print('Seemingly there are bugs in the compiled version of cv2 that ships with the Pi!')
print('')
print('Key Bindings:')
print('')
print('a z: Increase/Decrease Blur')
print('s x: Floating High and Low Temp Label Threshold')
print('d c: Change Interpolated scale Note: This will not change the window size on the Pi')
print('f v: Contrast')
print('q w: Fullscreen Windowed (note going back to windowed does not seem to work on the Pi!)')
print('r t: Record and Stop')
print('p : Snapshot')
print('m : Cycle through ColorMaps')
print('h : Toggle HUD')
main()
+2
View File
@@ -0,0 +1,2 @@
numpy
opencv-python