ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

基于STM32与OpenMV的非接触式人脸识别测温系统设计与实现

基于STM32与OpenMV的非接触式人脸识别测温系统设计与实现 简介本资源是一套面向嵌入式AI初学者与课程设计者的完整无接触式健康监测系统实现方案聚焦公共卫生场景下的体温筛查、身份核验与防疫合规性判断三大刚需。系统以STM32F103为主控OpenMV模组为视觉处理单元集成GY-906DCI红外测温、Adaboost口罩检测、轻量级神经网络人脸识别及OLE跨平台数据对接能力适用于校园闸机、办公入口、社区哨点等部署场景。压缩包共198个文件含41个C语言驱动与算法源码.c/.h、30个编译中间文件.o/.d、29个Keil工程依赖项.crf以及Python训练脚本、TFLite模型、Keil工程uvprojx、Hex固件和详细文档docx/htm/md总大小8.03MB结构清晰模块解耦明确。目前已有60人学习下载提供可直接烧录运行的完整固件、多级调试日志支持、传感器标定说明及算法移植注释助读者快速掌握嵌入式端AI部署全流程。1. 项目概述与核心价值最近几年大家对公共卫生和个人健康监测的关注度越来越高尤其是在一些需要快速筛查的公共或半公共场合。传统的体温测量方式比如额温枪虽然方便但需要近距离接触存在交叉感染风险而且无法与身份信息自动关联数据记录和管理效率低下。我手头正好有STM32F103和OpenMV这两块开发板琢磨着能不能把它们结合起来做一个既能无接触测温又能识别人脸、判断是否佩戴口罩的“三合一”小系统。这个想法听起来有点复杂但拆解开来其实就是几个成熟模块和算法的组合应用。这个系统的核心目标很明确非接触、自动化、可记录。用户走到设备前系统通过摄像头捕捉人脸在识别身份的同时用红外测温模块测量额温并检查是否规范佩戴口罩。所有数据可以实时显示并上传到后台或本地存储形成一个简单的健康通行记录。这对于小型办公室、社区入口、实验室签到等场景是一个成本可控且实用性很强的解决方案。整个系统的硬件核心是STM32F103C8T6最小系统板作为主控负责逻辑调度、数据汇总和通信OpenMV Cam H7 Plus作为视觉处理单元负责人脸检测、识别和口罩判断测温则交给了GY-906 DCI非接触式红外测温模块。软件层面OpenMV上运行基于Haar Cascade或LBP Cascade的Adaboost算法进行人脸和口罩的快速初筛并结合轻量级神经网络模型进行更精确的人脸识别。STM32则通过串口与两者通信整合信息驱动显示和报警模块。2. 系统整体设计与硬件选型思路做一个项目第一步永远是整体设计。你不能拿到芯片和模块就开始焊线得先想清楚数据流和控制流怎么走。这个系统的设计思路是“视觉主导主控调度”。2.1 核心架构与数据流设计整个系统的工作流程可以概括为一个状态机等待 - 检测到人脸 - 进行人脸识别与口罩检测 - 启动测温 - 数据显示/上传。OpenMV作为“眼睛”和“初级大脑”它持续拍摄图像运行人脸检测算法。一旦在画面中锁定人脸区域它会立刻做两件事第一截取该区域图像送入预先训练好的人脸识别模型例如使用OpenMV自带的nn模块加载TensorFlow Lite转换后的模型进行特征比对识别出这是“张三”还是“李四”第二在同一区域运行口罩检测算法判断是否佩戴规范。这两个识别结果会通过串口UART实时发送给STM32。与此同时STM32在收到OpenMV的“发现目标”信号后会通过I2C总线向GY-906模块发送读取指令获取当前测量到的物体温度值。这里有一个关键点测温的时机和对象必须与视觉识别同步。你不能对着空气测温也不能在人脸识别完成前乱测。因此STM32需要协调好这个时序。通常我会让OpenMV在识别到人脸后发送一个包含人脸框坐标的指令给STM32STM32可以据此虽然GY-906是固定视场角但可以辅助判断或者简单地延时一小段时间后再读取温度以确保测的是人额头的温度。最后STM32将收到的“人员ID”、“口罩状态是/否”、“体温数值”这三类信息整合通过SPI接口驱动一块OLED显示屏进行本地显示同时也可以通过串口、蓝牙模块或Wi-Fi模块将数据打包上传到上位机或云平台。整个架构清晰模块各司其职STM32作为“总指挥”负责协调和通信。2.2 关键硬件选型与接口定义硬件是项目的骨架选型决定了系统的稳定性、成本和开发难度。主控制器STM32F103C8T6选择这款芯片的原因很简单性价比极高资源足够生态成熟。它基于ARM Cortex-M3内核72MHz主频对于处理串口数据、驱动OLED、管理I2C通信绰绰有余。64KB的Flash和20KB的RAM也足够存放程序和一些临时数据。它的外设资源正好匹配我们的需求多个USART用于与OpenMV、调试、无线模块通信、I2C连接GY-906和OLED、SPI驱动OLED如果选用SPI接口的屏以及足够的GPIO。市面上各种最小系统板价格低廉降低了入门门槛。视觉处理单元OpenMV Cam H7 Plus这是本项目的“智慧之眼”。为什么不直接用STM32跑OpenCV因为STM32F103的计算能力和内存根本无法胜任实时图像处理。OpenMV内置了强大的MicroPython解释器和专用的图像传感器处理器可以非常方便地调用写好的图像处理函数和机器学习模型。H7 Plus版本使用了更强大的STM32H7芯片处理速度更快能更流畅地运行人脸检测和识别算法。我们通过其主串口UART3与STM32通信发送识别结果和指令。红外测温模块GY-906 (MLX90614ESF-DCI)测温模块的选型是精度和成本平衡的结果。MLX90614是一款工业级红外测温芯片DCI版本测量距离约2-5cm视场角窄适合测量小目标如额头精度可达±0.5℃。它通过I2C接口通信使用非常简单只需发送设备地址和读取命令即可获取物体温度和 ambient温度。这里有个重要细节务必选择DCI远距离窄视场角版本而不是DAA宽视场角版本。DAA版本适合测大面积区域平均温度测人体额头会因为环境干扰导致误差很大。显示模块0.96寸OLED (SSD1306)选择I2C接口的OLED屏接线简单仅需4根线显示清晰功耗低。STM32有成熟的驱动库如OLED_ShowStringOLED_ShowNum可以方便地显示体温、姓名、口罩状态和简单的提示图标。电源与结构整个系统可以用一个5V/2A的USB电源供电通过降压模块如AMS1117-3.3为各模块提供稳定的3.3V。结构上需要将OpenMV的摄像头、GY-906传感器和OLED屏幕固定在一个朝向一致的面板上确保摄像头和测温探头对准同一个区域即人的面部区域。可以设计一个简单的3D打印外壳来整合它们。注意硬件连接时务必注意电平匹配。STM32F103和OpenMV的IO口大多是3.3V电平而GY-906和OLEDSSD1306通常也支持3.3V。直接连接即可。如果使用5V电平的模块需要加电平转换电路。3. 软件实现从算法到代码的细节拆解软件部分是项目的灵魂也是最体现功力的地方。我们将分STM32端和OpenMV端来讲解。3.1 OpenMV端人脸识别与口罩检测算法实现OpenMV的编程基于MicroPython代码非常简洁。核心任务有三个人脸检测、人脸识别、口罩检测。1. 人脸检测快速定位这是第一步我们使用经典的Haar Cascade或LBP Cascade分类器它们基于Adaboost算法训练在OpenMV上速度极快。import sensor, image, time from pyb import UART # 初始化摄像头 sensor.reset() sensor.set_pixformat(sensor.RGB565) sensor.set_framesize(sensor.QVGA) # 320x240 速度和精度的平衡 sensor.skip_frames(time 2000) # 加载人脸检测分类器文件需要提前放入OpenMV的存储中 face_cascade image.HaarCascade(frontalface, stages25) # 或使用LBP: image.LBPFrontalFaceCascade() uart UART(3, 115200) # 使用UART3与STM32通信 while(True): img sensor.snapshot() # 进行人脸检测 objects img.find_features(face_cascade, threshold0.75, scale_factor1.25) if objects: # 通常取最大的那个人脸框 max_face max(objects, keylambda r: r[2]*r[3]) (x, y, w, h) max_face # 在图像上框出人脸用于调试 img.draw_rectangle(max_face) # 截取人脸区域图像用于后续识别和口罩检测 face_img img.copy(roimax_face) # 接下来进行识别和口罩判断... # 将结果通过串口发送出去例如格式“ID, Mask_Flag, x, y” # uart.write(1,1,{},{}\n.format(x, y)) # 示例ID1 戴口罩1为什么选择Cascade而不是直接上神经网络做检测因为检测任务需要遍历图像不同位置和尺度计算量大。Cascade分类器速度极快能保证实时性先快速框出可能的人脸区域再对这个小区域进行更耗资源的识别和口罩判断这是经典的“检测识别”两级流水线效率最高。2. 人脸识别确认身份对于截取出来的face_img我们需要识别出他是谁。这里可以使用OpenMV的nn模块加载一个轻量级神经网络模型。这个模型需要预先在PC上训练好例如使用TensorFlow/Keras搭建一个简单的卷积神经网络在自制的人脸数据集上训练然后转换成TensorFlow Lite格式最后用OpenMV的tflite_converter工具转换成.tflite模型文件放入OpenMV中。# 假设已经转换好模型文件face_model.tflite和标签文件labels.txt import tf net tf.load(face_model.tflite, load_to_fbTrue) # 加载模型到帧缓存加速 labels [line.rstrip(\n) for line in open(labels.txt)] # 在循环中截取face_img后 face_img face_img.resize(96, 96) # 缩放到模型输入尺寸 face_img face_img.to_grayscale() # 如果模型是灰度输入 # 将图像数据转换为模型输入格式 tensor tf.convert_image(face_img, ...) # 根据模型要求进行转换 out net.predict(tensor) # 执行推理 idx out.index(max(out)) # 获取最大概率的索引 person_id labels[idx] # 得到对应人名或ID confidence max(out) # 置信度 if confidence 0.7: # 设置一个置信度阈值过低则视为“未知人员” person_id Unknown实操心得训练自己的人脸模型时数据集的质量至关重要。每个人脸需要多角度、不同光照条件的图片。建议使用face_recognition库在PC端先进行人脸对齐和裁剪确保输入模型的都是归一化的人脸区域这样能极大提升识别准确率。3. 口罩检测口罩检测可以作为一个二分类问题戴口罩/不戴口罩来处理。有两种实现思路思路A独立口罩检测模型。再训练一个专门的轻量级CNN模型输入是人脸区域图像输出是“戴口罩”或“不戴口罩”。这种方式准确率高但需要额外的模型加载和推理时间。思路B在人脸识别模型中扩展。更高效的方法是在训练人脸识别模型时就将“戴口罩的人脸”作为同一个人的另一类数据加入训练集。模型在识别出“张三”的同时其倒数第二层特征层的特征向量可以训练一个简单的线性分类器SVM或逻辑回归来判断是否戴口罩。在OpenMV上可以把这个线性分类器的权重集成进去在推理后多做一个计算。对于资源紧张的场景甚至可以直接用人脸检测框的宽高比和下巴区域像素统计做一个非常粗略的估计戴口罩时检测框的下半部分嘴部区域颜色和纹理相对均匀。在实际项目中我采用了思路A因为它的职责更单一效果更稳定。我训练了一个简单的4层卷积神经网络输入是48x48的灰度人脸图输出是2个节点。在OpenMV上运行速度很快。mask_net tf.load(mask_model.tflite, load_to_fbTrue) # ... 对同一个face_img进行预处理缩放到48x48... mask_tensor tf.convert_image(face_img, ...) mask_out mask_net.predict(mask_tensor) is_masked 1 if mask_out[0] mask_out[1] else 0 # 假设输出为[戴口罩概率 不戴概率]最后OpenMV将person_idis_masked以及人脸框中心坐标用于辅助STM32判断测温时机打包成一个字符串通过串口发送给STM32。格式可以自定义例如“ID:001,MSK:1,X:120,Y:100\n”。3.2 STM32端主控逻辑与多模块通信协调STM32端的程序使用Keil MDK或STM32CubeIDE开发主要逻辑集中在主循环中通过中断和状态标志来协调。1. 外设初始化这是标准步骤包括系统时钟、GPIO、USART用于与OpenMV和调试、I2C用于GY-906和OLED、SPI如果OLED用SPI等的初始化。使用STM32CubeMX工具可以图形化配置生成初始化代码非常方便。2. 与OpenMV的串口通信协议定义一个简单可靠的通信协议至关重要。例如OpenMV - STM32:“ID,MSK,X,Y\n”(例如:“2,1,150,110\n”)STM32 - OpenMV:“CMD:TEMP_START\n”(当STM32准备好测温时可以发送指令通知OpenMV OpenMV可以反馈一个特定姿势或亮灯提示用户保持静止)。在STM32中开启串口接收中断在中断服务函数中将字符存入缓冲区当检测到换行符\n时设置一个“数据包就绪”标志位。// 示例串口中断服务函数 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { char rc USART_ReceiveData(USART1); if(rc \n) { rx_buffer[rx_index] \0; // 字符串结束符 data_ready_flag 1; // 设置标志位 rx_index 0; } else { rx_buffer[rx_index] rc; if(rx_index RX_BUF_SIZE) rx_index 0; // 防止溢出 } } }在主循环中检测到这个标志位就解析缓冲区里的字符串提取出ID、口罩状态和坐标。3. 驱动GY-906读取温度GY-906的I2C地址通常是0x5A7位地址。读取物体温度TObj1的流程是先发送设备地址写位发送寄存器地址0x07然后发送重复起始条件发送设备地址读位连续读取两个字节的数据低字节在前组成一个16位整数。这个整数需要乘以0.02分辨率再减去273.15才能得到摄氏温度。float MLX90614_ReadTemp(void) { uint8_t data[2] {0}; uint16_t tempRaw 0; float tempC 0; // 1. 发送要读取的寄存器地址 0x07 (物体温度1) HAL_I2C_Mem_Read(hi2c1, 0x5A 1, 0x07, I2C_MEMADD_SIZE_8BIT, data, 2, 100); // 2. 计算原始值 tempRaw (data[1] 8) | data[0]; // 3. 转换为摄氏度 tempC (float)tempRaw * 0.02 - 273.15; return tempC; }注意事项GY-906的测量需要稳定时间。上电后最好等待几百毫秒再进行第一次读取。连续读取时间隔不宜太短建议大于100ms否则模块自身发热会影响精度。测温时务必确保被测对象额头在模块的视场角内并且距离在2-5厘米左右避免环境热源如灯光、窗户的干扰。4. 主状态机逻辑主循环的核心是一个状态机typedef enum { STATE_IDLE, STATE_FACE_DETECTED, STATE_MEASURING_TEMP, STATE_DISPLAY_RESULT, STATE_UPLOAD_DATA } SysState_t; SysState_t system_state STATE_IDLE; while(1) { switch(system_state) { case STATE_IDLE: if(data_ready_flag) { // 收到OpenMV数据包 ParseData(rx_buffer, id, mask_status, face_x, face_y); system_state STATE_FACE_DETECTED; data_ready_flag 0; } break; case STATE_FACE_DETECTED: // 可以在这里通过串口发送指令给OpenMV提示用户“请保持静止” UART_SendString(CMD:READY\n); HAL_Delay(300); // 等待用户稳定 system_state STATE_MEASURING_TEMP; break; case STATE_MEASURING_TEMP: current_temp MLX90614_ReadTemp(); // 简单的温度滤波比如连续读3次取平均 system_state STATE_DISPLAY_RESULT; break; case STATE_DISPLAY_RESULT: OLED_Clear(); OLED_ShowString(0, 0, ID:); OLED_ShowNum(24, 0, id, 3); OLED_ShowString(0, 16, Mask:); OLED_ShowString(48, 16, mask_status ? YES : NO!); OLED_ShowString(0, 32, Temp:); OLED_ShowFloatNum(48, 32, current_temp, 2); // 显示两位小数 if(current_temp 37.3 || mask_status 0) { // 阈值判断 OLED_ShowString(0, 48, ALERT!); Buzzer_On(); // 触发蜂鸣器报警 } else { OLED_ShowString(0, 48, PASS); Buzzer_Off(); } system_state STATE_UPLOAD_DATA; break; case STATE_UPLOAD_DATA: // 将 id, mask_status, current_temp 打包通过串口发送给Wi-Fi模块或PC sprintf(upload_buf, {\id\:%d,\mask\:%d,\temp\:%.2f}\n, id, mask_status, current_temp); UART_SendString(upload_buf); HAL_Delay(2000); // 结果显示和上传保持2秒 system_state STATE_IDLE; // 回到空闲状态等待下一个人 break; } }这个状态机清晰地描述了整个控制流程避免了逻辑混乱。HAL_Delay在实际应用中最好用定时器替代以实现非阻塞延时保证系统响应性。4. 系统集成、调试与性能优化要点当硬件焊接好两端代码都初步编写完成后就进入了最考验耐心的集成调试阶段。4.1 硬件联调与信号稳定性排查首先确保电源稳定。用万用表测量各模块的VCC电压是否在额定范围内尤其是3.3V。然后从最简单的模块开始调试OLED调试先写一个简单的测试程序让STM32在OLED上显示“Hello World”确保I2C/SPI通信和屏幕本身正常。GY-906调试编写一个单独读取温度的程序对着你的手或一个恒温物体比如一杯温水测量观察串口打印的值是否合理、稳定。常见问题I2C总线没应答。检查接线SCL SDA是否接反、接错检查上拉电阻通常模块已内置如果通信距离长可外加4.7k上拉用逻辑分析仪抓取I2C波形看时序是否正确。OpenMV与STM32串口调试这是通信的核心。先在OpenMV端写一个固定发送“TEST”字符串的程序在STM32端用中断接收并打印出来。确保波特率如115200、数据位、停止位、校验位双方设置完全一致。一个关键技巧在OpenMV发送数据和STM32接收解析时都加入\r\n作为帧结束符并在STM32解析前做好缓冲区溢出保护和字符串格式校验。4.2 视觉与测温的时空同步优化这是本项目的难点之一。摄像头“看到”人脸和红外传感器“感到”温度存在物理位置和时间的差异。空间对齐在机械结构上尽量让OpenMV摄像头和GY-906传感器的中心轴平行且靠近指向同一方向。可以通过标定建立一个粗略的映射关系当人脸出现在画面中央例如坐标(160,120)附近时认为其额头正对测温探头。时间同步在代码逻辑上采用“视觉触发延时测温”策略。当OpenMV稳定检测到人脸例如连续5帧都检测到且位置变化不大后才发送信号给STM32。STM32收到信号后延时200-500ms让人有时间稳定姿势再进行温度读取。可以在OLED上增加“请稍候测温”的提示提升用户体验。温度补偿GY-906读取的是物体表面温度。人体额头温度受环境温度影响。MLX90614本身会测量环境温度Ta芯片内部有补偿算法。我们读取的物体温度To已经是补偿后的。但为了更精确可以在设备安装位置放置一个参考黑体或使用已知温度物体进行一点软件上的偏移校准。4.3 模型优化与系统性能提升OpenMV上的神经网络模型是性能瓶颈。以下优化手段非常有效模型量化在PC端训练时使用TensorFlow的量化感知训练QAT或训练后动态范围量化DRQ将32位浮点权重转换为8位整数int8。这能大幅减少模型体积和提升推理速度而精度损失通常很小1%。输入尺寸优化人脸识别和口罩检测模型的输入图像尺寸不要盲目追求大。96x96或甚至48x48的灰度图像对于嵌入式设备已经足够。更小的尺寸意味着更少的计算量。帧率与分辨率平衡OpenMV的sensor.set_framesize()设置很重要。QVGA (320x240) 是一个很好的平衡点。分辨率太低如QQVGA会影响检测和识别精度太高如VGA则会严重拖慢帧率。可以实测不同分辨率下的算法帧率选择能满足实时性例如5fps的最高分辨率。STM32端的代码优化避免阻塞将所有HAL_Delay()替换为基于HAL_GetTick()的非阻塞状态机。例如测温等待时间可以用一个uint32_t start_tick HAL_GetTick();记录开始时间然后判断if(HAL_GetTick() - start_tick 300)。合理使用中断除了串口还可以将GY-906的DRDY数据就绪引脚连接到STM32的外部中断引脚如果模块支持实现异步读取而不是轮询。内存管理STM32F103内存小避免使用大的全局数组和递归。使用malloc/free要非常小心最好静态分配。5. 常见问题与故障排查实录在实际开发中我遇到了不少坑这里总结一下希望能帮你节省时间。问题现象可能原因排查步骤与解决方案OpenMV无法检测到人脸1. 光线太暗或过曝。2. 人脸距离摄像头太近或太远。3. Cascade分类器文件未正确加载或版本不匹配。4. 摄像头未正确对焦对于带自动对焦的型号。1. 调整环境光照或开启OpenMV的自动增益/白平衡。2. 调整距离使人脸框约占画面1/4到1/2。3. 检查.cascade文件路径和文件名尝试使用OpenMV IDE自带的示例文件测试。4. 运行sensor.set_auto_focus()或手动调焦。人脸识别准确率低1. 训练数据集质量差、数量少、多样性不足。2. 模型输入图像预处理不一致训练和推理时缩放、归一化方式不同。3. 现场光照与训练集差异大。1. 扩充数据集每人至少20-30张不同角度、表情、光照的图片。2. 确保推理代码中的图像预处理缩放、色彩空间转换、归一化与训练时完全一致。3. 考虑在OpenMV端增加简单的人脸对齐根据眼睛位置旋转或使用直方图均衡化增强对比度。GY-906测温数值跳动大或不准确1. 测量距离太远或不在视场中心。2. 被测物体发射率低如光亮的金属额头。3. 模块前方有透明遮挡物亚克力板。4. 环境有强热源干扰阳光、暖气。5. I2C通信受干扰。1. 确保额头距离探头2-5cm并正对。2. 在额头贴一小块黑色电工胶布发射率接近0.95作为测量点。3. 移除任何遮挡物或使用专门透红外线的材料如聚乙烯。4. 改变设备安装位置避开热源。5. 缩短I2C走线增加上拉电阻4.7kΩ并确保电源地线良好。STM32与OpenMV串口通信乱码或丢包1. 双方波特率等参数设置不一致。2. 串口线过长或受干扰。3. STM32接收中断处理不当未及时清除标志位或缓冲区溢出。4. 未处理帧尾数据粘包。1. 用示波器或逻辑分析仪测量波特率是否准确。2. 使用屏蔽线缩短距离。3. 检查中断优先级在中断服务函数中只做最必要的操作存数据、设标志主循环解析。确保RXNE标志被清除。4. 使用明确的帧头帧尾如$开头\n结尾并在主循环中解析完整帧。系统整体反应慢卡顿1. OpenMV图像处理算法耗时过长。2. STM32主循环中有长时间阻塞如长延时。3. 模型推理速度慢。1. 降低图像分辨率优化检测区域ROI跳帧处理如每2帧处理1帧。2. 将STM32所有延时改为非阻塞式。3. 对模型进行量化、剪枝优化或使用更轻量的模型如MobileNetV1的深度可分离卷积版。OLED显示异常花屏、不显示1. I2C/SPI通信失败。2. 初始化序列不正确或延时不足。3. 屏幕供电不足。1. 用逻辑分析仪检查I2C/SPI是否有起始信号和数据。2. 参考屏幕数据手册严格按初始化步骤和时序编写代码必要时增加HAL_Delay。3. 检查屏幕VCC电压确保在3.3V左右电流足够可并联一个100uF电容。最后分享一个调试心得一定要分模块调试逐层集成。不要试图一次性把所有的代码都写完再联调。先让OpenMV单独运行在它的IDE里看到人脸框和识别结果再让STM32单独运行能正确读温度和显示最后再把串口通信加上。每完成一步都用调试器或串口打印关键变量值来验证。遇到问题先用最简化的测试程序例如STM32只循环发送一个字符给OpenMV来排除硬件连接和基础配置问题。耐心和细致的调试是项目成功的关键。这个系统虽然涉及的技术点不少但每个部分都有成熟的方案和社区支持只要你愿意动手一定能把它搭建起来。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进