Skip to content

Commit 4ca3326

Browse files
authored
Merge pull request #179 from MK16kawai/dev
add th160 example
2 parents 4ca5228 + 6b5a4bc commit 4ca3326

8 files changed

Lines changed: 539 additions & 4 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import numpy as np
2+
import time
3+
from maix import app, image, display
4+
from maix.peripheral import uart
5+
from maix.sys import device_name
6+
7+
PMOD_W = 160
8+
PMOD_H = 120
9+
FRAME_SIZE = PMOD_W * PMOD_H
10+
SKIP_COUNT = 10
11+
12+
CMAP = True # 渲染管线分支路由开关:True为热成像伪彩映射,False为原生灰度零拷贝
13+
14+
class HardwareHAL:
15+
_PORT_REGISTRY = {
16+
"MaixCAM2": "/dev/ttyS2",
17+
# 以下设备将在后续逐步提供支持
18+
"MaixCAM": None,
19+
"MaixCAM-Pro": None,
20+
}
21+
_device = ""
22+
23+
@classmethod
24+
def serial_port(cls):
25+
dn = device_name()
26+
if not isinstance(dn, str) or not dn.strip():
27+
raise TypeError(f"Invalid device identifier received: '{dn}'")
28+
port = cls._PORT_REGISTRY.get(dn)
29+
if port is None:
30+
raise RuntimeError(f"Platform mismatch: Device '{dn}' is not supported")
31+
cls._device = dn
32+
return port
33+
34+
def main():
35+
disp = display.Display()
36+
disp.set_hmirror(True)
37+
disp.set_vflip(False)
38+
39+
devices = uart.list_devices()
40+
if not devices:
41+
print("Error: No available UART devices found! Hardware HAL execution aborted.")
42+
return
43+
44+
hw = HardwareHAL()
45+
port_name = hw.serial_port()
46+
serial = uart.UART(port=port_name, baudrate=2000000)
47+
48+
try:
49+
if HardwareHAL._device == "MaixCAM2":
50+
serial.write(b'\x44')
51+
serial.close()
52+
time.sleep(0.1)
53+
serial = uart.UART(port=port_name, baudrate=4000000)
54+
55+
lut = np.array(image.cmap_colors_rgb(image.CMap.THERMAL_IRONBOW), dtype=np.uint8)
56+
color_buf = np.zeros((PMOD_H, PMOD_W, 3), dtype=np.uint8)
57+
buffer = bytearray()
58+
skip = 0
59+
frame_count = 0
60+
61+
fps_window_size = 30
62+
frame_timestamps = []
63+
fps_ema = 0.0
64+
ema_alpha = 0.2
65+
66+
print("System: Data pump and rendering pipeline successfully initialized.")
67+
68+
while not app.need_exit():
69+
chunk = serial.read(4096, timeout=10)
70+
if not chunk:
71+
continue
72+
buffer.extend(chunk)
73+
74+
while True:
75+
idx = buffer.find(b'\xFF')
76+
if idx == -1:
77+
buffer.clear()
78+
break
79+
80+
if len(buffer) - (idx + 1) >= FRAME_SIZE:
81+
frame_data = buffer[idx + 1 : idx + 1 + FRAME_SIZE]
82+
83+
err_idx = frame_data.find(b'\xFF')
84+
if err_idx != -1:
85+
print(f"Warning: Protocol violation! Unexpected 0xFF found in payload at offset {err_idx}. Resyncing...")
86+
del buffer[:idx + 1 + err_idx]
87+
continue
88+
89+
if skip <= SKIP_COUNT:
90+
skip += 1
91+
else:
92+
try:
93+
img = image.from_bytes(PMOD_W, PMOD_H, image.Format.FMT_GRAYSCALE, frame_data)
94+
except TypeError:
95+
img = image.from_bytes(PMOD_W, PMOD_H, image.Format.FMT_GRAYSCALE, bytes(frame_data))
96+
97+
if CMAP:
98+
img.gaussian(1)
99+
gray_np = image.image2cv(img, ensure_bgr=False, copy=False).squeeze()
100+
np.take(lut, gray_np, axis=0, out=color_buf)
101+
img_disp = image.cv2image(color_buf, bgr=False, copy=False)
102+
else:
103+
img_disp = img
104+
disp.show(img_disp)
105+
106+
current_time = time.time()
107+
frame_timestamps.append(current_time)
108+
if len(frame_timestamps) > fps_window_size:
109+
frame_timestamps.pop(0)
110+
if len(frame_timestamps) > 1:
111+
window_duration = frame_timestamps[-1] - frame_timestamps[0]
112+
if window_duration > 0:
113+
window_fps = (len(frame_timestamps) - 1) / window_duration
114+
if fps_ema == 0.0:
115+
fps_ema = window_fps
116+
else:
117+
fps_ema = (ema_alpha * window_fps) + ((1.0 - ema_alpha) * fps_ema)
118+
if frame_count % 10 == 0:
119+
osd_text = f"FPS: {fps_ema:.2f}"
120+
print(osd_text)
121+
122+
frame_count += 1
123+
buffer = buffer[idx + 1 + FRAME_SIZE:]
124+
else:
125+
if idx > 0:
126+
buffer = buffer[idx:]
127+
break
128+
129+
except Exception as e:
130+
print(f"Fatal: Unhandled pipeline exception: {str(e)}")
131+
raise
132+
finally:
133+
if 'serial' in locals() and serial:
134+
serial.close()
135+
print("System: UART resource securely released via interrupt vector.")
136+
137+
if __name__ == "__main__":
138+
main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.claude/
2+
.claudeignore
3+
.ruff_cache/
4+
bak/
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# MaixCAM Thermal 160 实时热成像监控
2+
3+
这是一个基于 MaixPy v4 框架开发的实时热成像监控程序,专门为 MaixCAM2(及后续支持型号)设计。该程序通过 UART 接收 160x120
4+
分辨率的热成像原始数据,并实时渲染为具有 Ironbow(铁红) 伪彩映射的视频流。
5+
6+
## 硬件要求
7+
8+
* 设备:MaixCAM2, 可参考该APP 代码移植到其他能够使用串口外设的平台
9+
* 传感器:支持 PMOD 接口或 UART 输出的 160x120 像素热成像模组。
10+
* 连接方式:
11+
* MaixCAM2 默认使用 /dev/ttyS2。
12+
* 波特率:初始 2,000,000,握手后最高可跳变至 4,000,000。
13+
14+
## 配置说明
15+
16+
在代码顶层可以根据需要调整以下常量:
17+
18+
* CMAP = True:设为 True 显示彩色热成像,False 则显示原始灰度图(零拷贝,性能更高)。
19+
* SKIP_COUNT = 10:启动时跳过的初始帧数,用于稳定传感器数据。
20+
21+
## 协议简介
22+
23+
程序期望的 UART 数据格式为:
24+
25+
* 帧头:0xFF
26+
* 负载:19,200 字节 (160 * 120) 的单字节灰度数据。
27+
* 校验:负载中不包含 0xFF。
28+
总计单包大小为 19201 bytes
29+
30+
## 注意事项
31+
32+
* MaixCAM2 兼容性:程序包含针对 MaixCAM2 的波特率切换指令 (0x44),使用其他串口设备时请根据实际通讯协议修改 HardwareHAL 类。
33+
* 资源释放:程序通过 finally 块确保在退出时安全释放 UART 资源。
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# MaixCAM Thermal 160 Real-time Thermal Imaging Monitoring
2+
3+
This is a real-time thermal imaging monitoring application developed based on the MaixPy v4 framework, specifically designed for MaixCAM2 (and future supported models). The application receives raw thermal imaging data at 160x120 resolution via UART and renders it as a real-time video stream with Ironbow pseudo-color mapping.
4+
5+
## Hardware Requirements
6+
7+
* Device: MaixCAM2, code can be ported to other platforms that support UART peripherals
8+
* Sensor: Thermal imaging module with PMOD interface or UART output supporting 160x120 pixels.
9+
* Connection method:
10+
* MaixCAM2 defaults to /dev/ttyS2.
11+
* Baud rate: Initial 2,000,000, can jump to up to 4,000,000 after handshake.
12+
13+
## Configuration
14+
15+
Adjust the following constants at the top of the code as needed:
16+
17+
* CMAP = True: Set to True to display color thermal imaging, False to display original grayscale (zero-copy, higher performance).
18+
* SKIP_COUNT = 10: Number of initial frames to skip on startup, used to stabilize sensor data.
19+
20+
## Protocol Overview
21+
22+
The expected UART data format is:
23+
24+
* Header: 0xFF
25+
* Payload: 19,200 bytes (160 * 120) of single-byte grayscale data.
26+
* Checksum: Payload does not contain 0xFF.
27+
* Total packet size: 19201 bytes
28+
29+
## Notes
30+
31+
* MaixCAM2 Compatibility: The program includes baud rate switching commands (0x44) specifically for MaixCAM2. When using other UART devices, please modify the HardwareHAL class according to the actual communication protocol.
32+
* Resource Release: The program ensures safe UART resource release on exit through the finally block.
33+
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
id: thermal160_camera
2+
name: Thermal160 Camera
3+
name[zh]: 热成像仪160
4+
version: 1.0.0
5+
icon: assets/thermal.json
6+
author: Sipeed Ltd
7+
desc: Thermal Camera
8+
desc[zh]: 热成像仪
9+
include:
10+
- assets/thermal.json
11+
- app.yaml
12+
- main.py
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"v":"4.8.0","meta":{"g":"LottieFiles AE 1.0.0","a":"Bas Milius","k":"Meteocons, Weather icons, Icon set","d":"Thermometer - Meteocons.com","tc":""},"fr":60,"ip":0,"op":360,"w":512,"h":512,"nm":"thermometer","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"thermometer","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[256,256,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-24],[32,-24]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-88],[32,-88]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[4,-56],[32,-56]],"c":false},"ix":2},"nm":"Path 3","mn":"ADBE Vector Shape - Group","hd":false},{"ind":3,"ty":"sh","ix":4,"ks":{"a":0,"k":{"i":[[0,-19.305],[30.928,0],[0,31.389],[-14.496,10.272],[0,0],[-17.673,0],[0,-17.937],[0,0]],"o":[[0,31.389],[-30.928,0],[0,-19.305],[0,0],[0,-17.937],[17.673,0],[0,0],[14.496,10.272]],"v":[[56,79.164],[0,136],[-56,79.164],[-32,32.559],[-32,-103.522],[0,-136],[32,-103.522],[32,32.559]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.796078443527,0.835294127464,0.882352948189,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":6,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[256,256],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"thermometer-glass","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":120,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":150,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":210,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":240,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":270,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":300,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":330,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.985,236],[255.985,336]],"c":false}]},{"t":359,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[255.798,213],[255.798,336]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.937254905701,0.266666680574,0.266666680574,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":24,"ix":5},"lc":2,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ind":2,"ty":"sh","ix":3,"ks":{"a":0,"k":{"i":[[0,19.882],[19.882,0],[0,-19.882],[-19.882,0]],"o":[[0,-19.882],[-19.882,0],[0,19.882],[19.882,0]],"v":[[292,336],[256,300],[220,336],[256,372]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.937254905701,0.266666680574,0.266666680574,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[256,336],"ix":2},"a":{"a":0,"k":[256,336],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"thermometer-mercury","np":4,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360,"st":0,"bm":0}],"markers":[]}

0 commit comments

Comments
 (0)