Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.idea/
|
||||
+363
@@ -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()
|
||||
@@ -0,0 +1,2 @@
|
||||
numpy
|
||||
opencv-python
|
||||
Reference in New Issue
Block a user