Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 138 additions & 0 deletions examples/ext_dev/sensors/thermal160/thermal160.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import numpy as np
import time
from maix import app, image, display
from maix.peripheral import uart
from maix.sys import device_name

PMOD_W = 160
PMOD_H = 120
FRAME_SIZE = PMOD_W * PMOD_H
SKIP_COUNT = 10

CMAP = True # 渲染管线分支路由开关:True为热成像伪彩映射,False为原生灰度零拷贝

class HardwareHAL:
_PORT_REGISTRY = {
"MaixCAM2": "/dev/ttyS2",
# 以下设备将在后续逐步提供支持
"MaixCAM": None,
"MaixCAM-Pro": None,
}
_device = ""

@classmethod
def serial_port(cls):
dn = device_name()
if not isinstance(dn, str) or not dn.strip():
raise TypeError(f"Invalid device identifier received: '{dn}'")
port = cls._PORT_REGISTRY.get(dn)
if port is None:
raise RuntimeError(f"Platform mismatch: Device '{dn}' is not supported")
cls._device = dn
return port

def main():
disp = display.Display()
disp.set_hmirror(True)
disp.set_vflip(False)

devices = uart.list_devices()
if not devices:
print("Error: No available UART devices found! Hardware HAL execution aborted.")
return

hw = HardwareHAL()
port_name = hw.serial_port()
serial = uart.UART(port=port_name, baudrate=2000000)

try:
if HardwareHAL._device == "MaixCAM2":
serial.write(b'\x44')
serial.close()
time.sleep(0.1)
serial = uart.UART(port=port_name, baudrate=4000000)

lut = np.array(image.cmap_colors_rgb(image.CMap.THERMAL_IRONBOW), dtype=np.uint8)
color_buf = np.zeros((PMOD_H, PMOD_W, 3), dtype=np.uint8)
buffer = bytearray()
skip = 0
frame_count = 0

fps_window_size = 30
frame_timestamps = []
fps_ema = 0.0
ema_alpha = 0.2

print("System: Data pump and rendering pipeline successfully initialized.")

while not app.need_exit():
chunk = serial.read(4096, timeout=10)
if not chunk:
continue
buffer.extend(chunk)

while True:
idx = buffer.find(b'\xFF')
if idx == -1:
buffer.clear()
break

if len(buffer) - (idx + 1) >= FRAME_SIZE:
frame_data = buffer[idx + 1 : idx + 1 + FRAME_SIZE]

err_idx = frame_data.find(b'\xFF')
if err_idx != -1:
print(f"Warning: Protocol violation! Unexpected 0xFF found in payload at offset {err_idx}. Resyncing...")
del buffer[:idx + 1 + err_idx]
continue

if skip <= SKIP_COUNT:
skip += 1
else:
try:
img = image.from_bytes(PMOD_W, PMOD_H, image.Format.FMT_GRAYSCALE, frame_data)
except TypeError:
img = image.from_bytes(PMOD_W, PMOD_H, image.Format.FMT_GRAYSCALE, bytes(frame_data))

if CMAP:
img.gaussian(1)
gray_np = image.image2cv(img, ensure_bgr=False, copy=False).squeeze()
np.take(lut, gray_np, axis=0, out=color_buf)
img_disp = image.cv2image(color_buf, bgr=False, copy=False)
else:
img_disp = img
disp.show(img_disp)

current_time = time.time()
frame_timestamps.append(current_time)
if len(frame_timestamps) > fps_window_size:
frame_timestamps.pop(0)
if len(frame_timestamps) > 1:
window_duration = frame_timestamps[-1] - frame_timestamps[0]
if window_duration > 0:
window_fps = (len(frame_timestamps) - 1) / window_duration
if fps_ema == 0.0:
fps_ema = window_fps
else:
fps_ema = (ema_alpha * window_fps) + ((1.0 - ema_alpha) * fps_ema)
if frame_count % 10 == 0:
osd_text = f"FPS: {fps_ema:.2f}"
print(osd_text)

frame_count += 1
buffer = buffer[idx + 1 + FRAME_SIZE:]
else:
if idx > 0:
buffer = buffer[idx:]
break

except Exception as e:
print(f"Fatal: Unhandled pipeline exception: {str(e)}")
raise
finally:
if 'serial' in locals() and serial:
serial.close()
print("System: UART resource securely released via interrupt vector.")

if __name__ == "__main__":
main()
4 changes: 4 additions & 0 deletions projects/app_thermal160_camera/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.claude/
.claudeignore
.ruff_cache/
bak/
33 changes: 33 additions & 0 deletions projects/app_thermal160_camera/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# MaixCAM Thermal 160 实时热成像监控

这是一个基于 MaixPy v4 框架开发的实时热成像监控程序,专门为 MaixCAM2(及后续支持型号)设计。该程序通过 UART 接收 160x120
分辨率的热成像原始数据,并实时渲染为具有 Ironbow(铁红) 伪彩映射的视频流。

## 硬件要求

* 设备:MaixCAM2, 可参考该APP 代码移植到其他能够使用串口外设的平台
* 传感器:支持 PMOD 接口或 UART 输出的 160x120 像素热成像模组。
* 连接方式:
* MaixCAM2 默认使用 /dev/ttyS2。
* 波特率:初始 2,000,000,握手后最高可跳变至 4,000,000。

## 配置说明

在代码顶层可以根据需要调整以下常量:

* CMAP = True:设为 True 显示彩色热成像,False 则显示原始灰度图(零拷贝,性能更高)。
* SKIP_COUNT = 10:启动时跳过的初始帧数,用于稳定传感器数据。

## 协议简介

程序期望的 UART 数据格式为:

* 帧头:0xFF
* 负载:19,200 字节 (160 * 120) 的单字节灰度数据。
* 校验:负载中不包含 0xFF。
总计单包大小为 19201 bytes

## 注意事项

* MaixCAM2 兼容性:程序包含针对 MaixCAM2 的波特率切换指令 (0x44),使用其他串口设备时请根据实际通讯协议修改 HardwareHAL 类。
* 资源释放:程序通过 finally 块确保在退出时安全释放 UART 资源。
33 changes: 33 additions & 0 deletions projects/app_thermal160_camera/README_EN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# MaixCAM Thermal 160 Real-time Thermal Imaging Monitoring

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.

## Hardware Requirements

* Device: MaixCAM2, code can be ported to other platforms that support UART peripherals
* Sensor: Thermal imaging module with PMOD interface or UART output supporting 160x120 pixels.
* Connection method:
* MaixCAM2 defaults to /dev/ttyS2.
* Baud rate: Initial 2,000,000, can jump to up to 4,000,000 after handshake.

## Configuration

Adjust the following constants at the top of the code as needed:

* CMAP = True: Set to True to display color thermal imaging, False to display original grayscale (zero-copy, higher performance).
* SKIP_COUNT = 10: Number of initial frames to skip on startup, used to stabilize sensor data.

## Protocol Overview

The expected UART data format is:

* Header: 0xFF
* Payload: 19,200 bytes (160 * 120) of single-byte grayscale data.
* Checksum: Payload does not contain 0xFF.
* Total packet size: 19201 bytes

## Notes

* 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.
* Resource Release: The program ensures safe UART resource release on exit through the finally block.

12 changes: 12 additions & 0 deletions projects/app_thermal160_camera/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
id: thermal160_camera
name: Thermal160 Camera
name[zh]: 热成像仪160
version: 1.0.0
icon: assets/thermal.json
author: Sipeed Ltd
desc: Thermal Camera
desc[zh]: 热成像仪
include:
- assets/thermal.json
- app.yaml
- main.py
1 change: 1 addition & 0 deletions projects/app_thermal160_camera/assets/thermal.json
Original file line number Diff line number Diff line change
@@ -0,0 +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":[]}
Loading
Loading