立创开源:基于STM32F411与MLX90640的双目手机热成像仪硬件设计与图像融合算法详解
大家好,我是Colourfate。最近在立创开源平台发布了一个挺有意思的项目——一个能插在手机上的双目热成像仪。很多朋友对热成像感兴趣,但觉得传感器太贵或者开发复杂。我这个项目就是想用相对低成本的方式,结合手机强大的算力,做出一个实用的热成像工具。今天,我就来手把手拆解这个项目的硬件设计和核心的图像融合算法,希望能给想做类似项目的朋友一些参考。
简单来说,这个设备就像一个给手机用的“热成像眼睛”。它包含一个普通的USB摄像头(可见光)和一个热成像传感器(MLX90640),通过一块STM32F411的小板子读取热成像数据,再和摄像头一起通过USB Hub连接到你的安卓手机。手机上的App会同时接收两路数据,然后用算法把它们融合在一起,最终在手机屏幕上显示出一张既有清晰画面、又能看到温度分布的热成像图。
下面,咱们就从硬件到软件,一步步来看它是怎么实现的。
1. 项目整体思路:为什么这么设计?
在开始讲具体电路和代码之前,咱们先搞清楚这个项目的核心思路。这能帮你理解后面每一个设计选择的用意。
热成像传感器,比如咱们用的MLX90640,它能感知物体的温度分布,但有个硬伤:分辨率太低,只有32x24像素。直接看就是一堆模糊的马赛克,细节全无。而普通的可见光摄像头,现在随便一个都是几十万、上百万像素,画面清晰,帧率也高。
所以,我的想法很直接:用可见光摄像头的高清画面,去“补偿”热成像传感器的低分辨率画面。让清晰可见的轮廓、纹理,和看不见的温度信息叠加在一起,最终得到一张既清晰又能看清热量分布的图像。
那为什么要把处理工作放到手机上呢?原因有两个:
- 降低硬件成本和复杂度:如果让单片机(MCU)来做图像融合算法,需要性能很强的MCU,成本高,开发也麻烦。而现在的手机处理器性能非常强大,完全能胜任。
- 灵活性和显示便捷:手机有现成的大屏幕、触摸交互和丰富的开发库(比如OpenCV),做App显示和算法调整非常方便。
因此,整个系统的架构就定下来了:传感器端只负责最基础的数据采集和转发,复杂的图像处理全部交给手机App。这个思路在很多边缘计算项目里都很常见,值得借鉴。
2. 硬件设计详解:从原理图到实物
硬件是整个项目的基础,设计得好不好直接关系到稳定性。我的硬件主要分为五大部分:主控MCU、USB Hub、热成像传感器、电源以及其他辅助电路。我会结合原理图和实际踩过的坑来讲解。
2.1 核心大脑:STM32F411CEU6最小系统
主控芯片我选择了意法半导体的STM32F411CEU6。选它主要看中三点:
- 带硬件浮点单元(FPU):MLX90640传感器输出的原始数据需要经过一系列公式计算才能得到温度值,这些计算涉及大量浮点运算。有FPU的MCU做这个速度飞快,没有的话软件模拟会慢很多。
- 集成USB FS(全速)接口:我们需要通过USB把处理好的热成像数据发送给手机,芯片自带USB外设省事很多。
- 性价比高:F4系列性能足够,价格也相对亲民。
最小系统电路就是保证芯片能跑起来的最简电路,包括:
- 电源:3.3V供电,用了多个去耦电容(图上那些C4, C5, C6, C7等)放在芯片电源引脚附近,这是稳定工作的关键,滤除高频噪声。
- 晶振:外部接了8MHz的晶振(Y1)作为主时钟源,芯片内部PLL可以倍频到更高的频率(比如100MHz)来运行。
- 复位电路:一个简单的RC复位(R1, C3)。
- 启动模式选择:通过BOOT0引脚(接地)选择从主Flash启动。
- 调试接口:留出了标准的SWD接口(J2),用于下载程序和调试。
注意:原理图中的U3磁珠(Ferrite Bead)是我最初预留做电源测试用的,实际焊接时不需要焊!直接短接或者空着就行。
2.2 数据中转站:SL2.1A USB Hub芯片
这是整个设计的“交通枢纽”。我们有两个设备要连到手机:STM32(虚拟串口)和USB摄像头。但手机通常只有一个USB口,所以需要一个Hub来扩展。
我选用的是SL2.1A这款Hub芯片。它支持USB 2.0高速(HS)模式,足以传输640x480分辨率的摄像头视频流。它的连接关系是:
- 上行端口:连接到一个USB Type-C公头(USB1),这就是插手机的那一头。
- 下行端口1:连接STM32F411的USB接口(USB FS)。
- 下行端口2:连接USB摄像头模块(CAM_DP/CAM_DM)。
这里有个大坑,我在第一版打样时就栽了:STM32到SL2.1A的USB数据线(D+和D-)接反了!原理图上画错了。这会导致通信失败。解决办法是用飞线交换D+和D-两根线。在开源工程的后续版本中我已经修正了这个问题,但如果你用的是最早的版本,请务必检查并修正。
提示:USB差分信号对布线有要求,特别是高速的摄像头信号。打样PCB时,一定要对CAM_DP/CAM_DM这两根线做阻抗控制(通常控制到90欧姆差分阻抗),否则视频信号可能会不稳定,出现画面卡顿、花屏。
2.3 温度之眼:MLX90640热成像传感器
这是项目的核心传感器,我把它做成了一个可插拔的子板形式。MLX90640是一个32x24像素(总共768个像素点)的红外热电堆阵列,通过I2C接口通信,最高速率可达1MHz。
在电路设计上要注意:
- 电源去耦:传感器旁边一定要放一个0.1uF的陶瓷电容,确保电源干净。
- I2C上拉电阻:SDA和SCL线上需要接上拉电阻(通常4.7kΩ),原理图中已经包含。
- 机械安装:MLX90640传感器本身有一定高度。而摄像头子板为了获得正对前方的视野,也需要一定高度。所以最好将摄像头子板用铜柱垫高,避免两者在垂直空间上冲突。
2.4 能量供给:电源电路
整个板子通过手机的USB Type-C口取电(5V)。我使用了一颗ME6211系列的LDO(低压差线性稳压器)将5V降压到3.3V,给STM32、MLX90640等芯片供电。
选择ME6211是因为它特别省心,输入端和输出端可以直接使用低ESR(等效串联电阻)的陶瓷电容,不需要像某些LDO那样必须额外加钽电容来保持稳定。这简化了设计,也降低了成本。
2.5 其他辅助电路
- 指示灯:电源指示灯(LED1)和一颗可编程控制的测试LED(LED2),方便调试。
- 调试串口:除了SWD,我还将STM32的UART2引出了测试点(TP1, TP2),可以在代码里打印调试信息,非常实用。
- 摄像头接口:摄像头我直接选用了一块现成的免驱UVC摄像头子板(分辨率640x480)。在PCB上留出了对应的焊盘,焊接时注意对准。连接器的线间距是0.8mm,购买时别选错。
3. 固件开发:STM32如何读取并发送数据
硬件搭好了,接下来让STM32“动”起来。它的任务很明确:不停地从MLX90640读取温度数据,打包,然后通过USB发送给手机。
3.1 开发环境与软件流程
我使用STM32CubeIDE进行开发,这是ST官方推出的免费集成开发环境,图形化配置工具非常方便。
整个固件的软件流程图可以概括为一个大循环:
初始化(I2C, USB等) -> 循环 { 1. 通过I2C读取MLX90640原始数据 2. 调用官方驱动库,将原始数据解算为每个像素点的温度值(浮点数) 3. 将浮点数温度(单位℃)转换为整数(例如25.12℃ -> 2512) 4. 按照自定义协议打包数据 5. 通过USB CDC(虚拟串口)发送数据包 }MLX90640的温度测量范围是-40°C到+300°C。保留两位小数后,数值范围是-4000到+30000,正好可以用一个16位有符号整数(int16_t)来表示,这样比传输浮点数节省了一半的带宽。
3.2 关键代码解析
1. I2C驱动适配:MLX90640的官方驱动(从Melexis官网下载)需要你实现底层的I2C_Read和I2C_Write函数。在STM32上,我们可以用HAL库快速实现:
// 示例:MLX90640驱动要求的I2C读取函数 int MLX90640_I2CRead(uint8_t slaveAddr, uint16_t startAddr, uint16_t nWords, uint16_t *data) { // slaveAddr: 传感器I2C地址 // startAddr: 要读取的寄存器起始地址 // nWords: 要读取的字数(16位) // data: 存放读取数据的数组 uint8_t cmd[2]; cmd[0] = startAddr >> 8; // 寄存器地址高字节 cmd[1] = startAddr & 0x00FF; // 寄存器地址低字节 // 使用HAL库的I2C操作 HAL_I2C_Master_Transmit(&hi2c1, slaveAddr << 1, cmd, 2, HAL_MAX_DELAY); HAL_I2C_Master_Receive(&hi2c1, slaveAddr << 1, (uint8_t*)data, nWords * 2, HAL_MAX_DELAY); return 0; }2. 数据打包协议:为了手机端能正确解析,我定义了一个简单的帧协议:
---------------------------------------------------------------------------- | header | type | length | data[0] | data[1] | ... | data[n] | ----------------------------------------------------------------------------- header:2字节帧头,固定为0x8000,用于标识一帧的开始。
- type:2字节数据类型,可以用来区分不同用途的数据包。
- length:2字节,表示后面
data数组的长度(字节数)。 - data:实际的数据负载,这里存放的就是768个像素点的温度值(每个点2字节,共1536字节)。
在STM32端,打包过程就是填充这个结构体,然后调用CDC_Transmit_FS()函数发送出去。
4. 手机App开发:双路数据接收与融合算法
这是项目的精华所在,也是最具挑战性的部分。手机App需要同时处理两路异步数据:USB摄像头的视频流和STM32发来的热成像数据流,并将它们实时融合显示。
4.1 软件框架与双缓冲队列
Android系统里,摄像头数据和串口数据都是通过回调(Callback)方式异步到达的,速度可能不一致。为了不让任何一路数据阻塞另一路,我采用了双缓冲队列机制。
以摄像头数据为例,流程如下:
- 初始化一个长度为2的队列(
mYUVQueue)。 - 当摄像头回调发生时,我新申请一块内存,把YUV图像数据拷贝进去,然后将这块内存的指针放入队列。
- 一个独立的融合线程不断尝试从队列里取数据。
- 流量控制:如果融合线程太慢,队列满了,新的回调就会丢弃队头最老的一帧,再放入新帧,保证数据是最新的。如果融合线程发现队列为空(摄像头帧率低),它就跳过本次融合,等待下一帧。
热成像数据(mThermalQueue)的处理方式完全相同。这样,两路数据的生产和消费就解耦了。
融合线程的工作逻辑很简单:
// 伪代码逻辑 while (融合线程运行) { // 等待摄像头数据(主等待源) YUVFrame cameraFrame = mYUVQueue.poll(); if (cameraFrame == null) continue; // 尝试获取最新的热成像数据,如果没有就用上一帧的 ThermalFrame thermalFrame = mThermalQueue.poll(); if (thermalFrame == null) { thermalFrame = mLastThermalFrame; // 使用上一帧 } else { mLastThermalFrame = thermalFrame; // 更新上一帧 } // 进行图像融合处理(见下一节) processFusion(cameraFrame, thermalFrame); }4.2 核心:基于OpenCV的图像融合算法
这是让画面变“神奇”的关键。我们的目标是把低分辨率(32x24)的热成像图,融合到高分辨率(640x480)的可见光图中。
第一步:提取可见光图像的高频信息。一张图像可以看作由低频信息(大块的颜色、缓慢变化的亮度)和高频信息(边缘、纹理、细节)组成。热成像图缺乏的就是高频细节。我们可以用高斯模糊来获取图像的低频部分,然后用原图减去低频部分,就得到了高频部分。
// 假设 im_cam 是640x480的可见光灰度图(Mat格式) Mat im_cam_l; // 低频图像 Mat im_cam_h; // 高频图像 // 使用高斯模糊获取低频信息。Size(11,11)是模糊核大小,数字越大越模糊 GaussianBlur(im_cam, im_cam_l, Size(11, 11), 5, 5); // 原图减去低频图,得到高频细节图 im_cam_h = im_cam - im_cam_l;第二步:处理热成像图像。将32x24的热成像温度数据(int16_t数组)转换成一个32x24的灰度图(Mat),然后使用插值算法(如cv::resize)将其放大到640x480,得到im_them_scale。
第三步:融合与伪彩色。将可见光的高频细节图im_cam_h和放大后的热成像图im_them_scale直接相加。因为im_cam_h包含细节纹理但整体亮度均值接近0,而im_them_scale包含温度分布信息,相加后,细节就被“添加”到了温度图上。
Mat im_fusion = im_cam_h + im_them_scale;最后,为了更直观地显示温度差异,我们给融合后的灰度图加上伪彩色。OpenCV提供了applyColorMap函数,我常用COLORMAP_JET(蓝-青-黄-红)。
Mat im_fusion_color; applyColorMap(im_fusion, im_fusion_color, COLORMAP_JET);这样,最终显示的im_fusion_color就是一张拥有可见光细节纹理的彩色热成像图了。
4.3 另一种简单方法:色彩映射
除了上述融合算法,App里还实现了另一种更简单的“色彩映射”模式。其思想是:
- 直接将低分辨率的热成像图放大并赋予伪彩色(得到一个RGB的热图)。
- 将这个RGB热图转换成YUV颜色空间。
- 将Y、U、V三个分量分别减去128(YUV的中间值),得到一组偏移量。
- 将这组偏移量叠加到可见光图像的YUV数据上。
这种方法相当于给可见光图像“染”上了一层热成像的颜色,实现起来更快,但融合效果不如高频提取算法自然。
5. 制作要点与效果展示
如果你也想动手做一个,这里有几点特别需要注意:
- PCB打样:板子厚度我用的1.2mm。最关键的是,给摄像头USB差分线(CAM_DP/DM)做阻抗控制,并严格按照差分线规则布线(等长、等距、少打过孔)。
- 焊接与组装:注意之前提到的STM32与SL2.1A的USB线序问题。焊接摄像头和MLX90640子板时注意对位。可以用尼龙柱将摄像头子板垫高,避免遮挡传感器。
- 外壳:我在项目文件中提供了3D打印外壳的文件(STEP和STL格式),可以直接打印使用,能让整个设备更美观、坚固。
最后,来看一下实际效果。通过融合算法,热成像图不再是模糊的马赛克,而是能清晰地看到电器元件的轮廓、人的面部特征,同时颜色代表了温度高低。你可以通过我的B站视频(链接在原始项目页)观看动态演示效果。
这个项目从硬件选型、电路设计、固件编写到安卓App和算法开发,涉及了嵌入式开发的全流程。希望这份详细的解析能帮助你理解如何将一个想法一步步实现为产品。开源的所有资料(原理图、PCB、固件代码、App代码、外壳模型)都可以在立创开源平台和GitHub上找到,欢迎一起交流改进!