RGB-Thermal Align
This example demonstrates how to align thermal information from a thermal camera to an RGB camera. This setup is useful for applications requiring the overlay or comparison of thermal and color data. An OpenCV window is created to display the blended image of the RGB and aligned thermal data. Trackbars are provided to adjust the blending ratio.Demo
Setup
Please run the install script to download all required dependencies. Please note that this script must be ran from git context, so you have to download the depthai-python repository first and then run the scriptCommand Line
1git clone https://github.com/luxonis/depthai-python.git
2cd depthai-python/examples
3python3 install_requirements.py
Source code
Python
C++
Python
PythonGitHub
1#!/usr/bin/env python3
2
3import cv2
4import depthai as dai
5import numpy as np
6import time
7from datetime import timedelta
8
9FPS = 25.0
10
11RGB_SOCKET = dai.CameraBoardSocket.CAM_A
12COLOR_RESOLUTION = dai.ColorCameraProperties.SensorResolution.THE_1080_P
13
14class FPSCounter:
15 def __init__(self):
16 self.frameTimes = []
17
18 def tick(self):
19 now = time.time()
20 self.frameTimes.append(now)
21 self.frameTimes = self.frameTimes[-100:]
22
23 def getFps(self):
24 if len(self.frameTimes) <= 1:
25 return 0
26 # Calculate the FPS
27 return (len(self.frameTimes) - 1) / (self.frameTimes[-1] - self.frameTimes[0])
28
29device = dai.Device()
30
31thermalWidth, thermalHeight = -1, -1
32thermalFound = False
33for features in device.getConnectedCameraFeatures():
34 if dai.CameraSensorType.THERMAL in features.supportedTypes:
35 thermalFound = True
36 thermalSocket = features.socket
37 thermalWidth, thermalHeight = features.width, features.height
38 break
39if not thermalFound:
40 raise RuntimeError("No thermal camera found!")
41
42
43ISP_SCALE = 3
44
45calibrationHandler = device.readCalibration()
46rgbDistortion = calibrationHandler.getDistortionCoefficients(RGB_SOCKET)
47distortionModel = calibrationHandler.getDistortionModel(RGB_SOCKET)
48if distortionModel != dai.CameraModel.Perspective:
49 raise RuntimeError("Unsupported distortion model for RGB camera. This example supports only Perspective model.")
50
51pipeline = dai.Pipeline()
52
53# Define sources and outputs
54camRgb = pipeline.create(dai.node.ColorCamera)
55thermalCam = pipeline.create(dai.node.Camera)
56thermalCam.setBoardSocket(thermalSocket)
57thermalCam.setFps(FPS)
58
59sync = pipeline.create(dai.node.Sync)
60out = pipeline.create(dai.node.XLinkOut)
61align = pipeline.create(dai.node.ImageAlign)
62cfgIn = pipeline.create(dai.node.XLinkIn)
63
64
65camRgb.setBoardSocket(RGB_SOCKET)
66camRgb.setResolution(COLOR_RESOLUTION)
67camRgb.setFps(FPS)
68camRgb.setIspScale(1,ISP_SCALE)
69
70out.setStreamName("out")
71
72sync.setSyncThreshold(timedelta(seconds=0.5 / FPS))
73
74cfgIn.setStreamName("config")
75
76cfg = align.initialConfig.get()
77staticDepthPlane = cfg.staticDepthPlane
78
79# Linking
80align.outputAligned.link(sync.inputs["aligned"])
81camRgb.isp.link(sync.inputs["rgb"])
82camRgb.isp.link(align.inputAlignTo)
83thermalCam.raw.link(align.input)
84sync.out.link(out.input)
85cfgIn.out.link(align.inputConfig)
86
87
88rgbWeight = 0.4
89thermalWeight = 0.6
90
91
92def updateBlendWeights(percentRgb):
93 """
94 Update the rgb and depth weights used to blend depth/rgb image
95 @param[in] percent_rgb The rgb weight expressed as a percentage (0..100)
96 """
97 global thermalWeight
98 global rgbWeight
99 rgbWeight = float(percentRgb) / 100.0
100 thermalWeight = 1.0 - rgbWeight
101
102def updateDepthPlane(depth):
103 global staticDepthPlane
104 staticDepthPlane = depth
105
106# Connect to device and start pipeline
107with device:
108 device.startPipeline(pipeline)
109 queue = device.getOutputQueue("out", 8, False)
110 cfgQ = device.getInputQueue("config")
111
112 # Configure windows; trackbar adjusts blending ratio of rgb/depth
113 windowName = "rgb-thermal"
114
115 # Set the window to be resizable and the initial size
116 cv2.namedWindow(windowName, cv2.WINDOW_NORMAL)
117 cv2.resizeWindow(windowName, 1280, 720)
118 cv2.createTrackbar(
119 "RGB Weight %",
120 windowName,
121 int(rgbWeight * 100),
122 100,
123 updateBlendWeights,
124 )
125 cv2.createTrackbar(
126 "Static Depth Plane [mm]",
127 windowName,
128 0,
129 2000,
130 updateDepthPlane,
131 )
132 fpsCounter = FPSCounter()
133 while True:
134 messageGroup = queue.get()
135 assert isinstance(messageGroup, dai.MessageGroup)
136 frameRgb = messageGroup["rgb"]
137 assert isinstance(frameRgb, dai.ImgFrame)
138 thermalAligned = messageGroup["aligned"]
139 assert isinstance(thermalAligned, dai.ImgFrame)
140 frameRgbCv = frameRgb.getCvFrame()
141 fpsCounter.tick()
142
143 rgbIntrinsics = calibrationHandler.getCameraIntrinsics(RGB_SOCKET, int(frameRgbCv.shape[1]), int(frameRgbCv.shape[0]))
144
145 cvFrameUndistorted = cv2.undistort(
146 frameRgbCv,
147 np.array(rgbIntrinsics),
148 np.array(rgbDistortion),
149 )
150
151 # Colorize the aligned depth
152 thermalFrame = thermalAligned.getCvFrame().astype(np.float32)
153 # Create a mask for nan values
154 mask = np.isnan(thermalFrame)
155 # Replace nan values with a mean for visualization
156 thermalFrame[mask] = np.nanmean(thermalFrame)
157 thermalFrame = cv2.normalize(thermalFrame, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
158 colormappedFrame = cv2.applyColorMap(thermalFrame, cv2.COLORMAP_MAGMA)
159 # Apply the mask back with black pixels (0)
160 colormappedFrame[mask] = 0
161
162 blended = cv2.addWeighted(cvFrameUndistorted, rgbWeight, colormappedFrame, thermalWeight, 0)
163
164 cv2.putText(
165 blended,
166 f"FPS: {fpsCounter.getFps():.2f}",
167 (10, 30),
168 cv2.FONT_HERSHEY_SIMPLEX,
169 1,
170 (255, 255, 255),
171 2,
172 )
173
174 cv2.imshow(windowName, blended)
175
176 key = cv2.waitKey(1)
177 if key == ord("q"):
178 break
179
180 cfg.staticDepthPlane = staticDepthPlane
181 cfgQ.send(cfg)
Pipeline
Need assistance?
Head over to Discussion Forum for technical support or any other questions you might have.