cv_resnet101_face-detection_cvpr22papermogface 硬件对接基于STM32F103C8T6最小系统板的人脸检测门禁原型1. 引言你有没有想过自己动手做一个能认人的智能门禁不是那种需要刷卡或者输密码的而是看一眼摄像头就能自动开门的那种。听起来像是科幻电影里的场景但其实用我们手边一些常见的硬件加上现在开源的AI模型完全可以自己搭建一个原型出来。这次我们要聊的就是用一块非常常见的STM32F103C8T6最小系统板作为核心让它配合摄像头抓拍图像然后把照片发给一台运行着人脸检测模型的电脑或者服务器电脑识别出人脸后再告诉STM32去控制一个继电器最终实现“刷脸开门”的效果。整个过程涉及硬件连接、图像采集、网络通信和AI推理是一个挺典型的软硬件协同项目。如果你对嵌入式开发有点兴趣又想看看AI模型怎么在实际的硬件上跑起来那这个项目应该能给你不少启发。它不要求你有特别高深的AI知识重点在于把几个模块顺畅地“搭”在一起让它们各司其职共同完成一个任务。2. 项目整体思路让硬件和AI“对话”在开始动手接线和写代码之前我们得先把这个项目的“骨架”理清楚。它主要分为三个部分就像一场需要三个角色配合的演出。第一个角色是“前台采集员”也就是我们的STM32F103C8T6最小系统板加上摄像头模块。STM32在这里的任务很明确初始化摄像头定时或者按按钮拍一张照片然后把这张照片的数据准备好发送出去。它自己并不负责认脸它的核心工作是“看见”并“传递”。第二个角色是“后台分析员”也就是我们运行着cv_resnet101_face-detection_cvpr22papermogface模型的服务器。这个模型是一个专门用于人脸检测的神经网络它的任务就是接收STM32发来的图片然后在图片里找出人脸的位置。如果找到了它就回复一个“识别成功”的信号如果没找到就回复“识别失败”。这个角色是项目的“大脑”负责最复杂的图像理解工作。第三个角色是“动作执行者”还是我们的STM32板子但这次它连接了一个继电器模块。当它收到后台“识别成功”的指令后就控制继电器吸合一下模拟开门动作比如点亮一个LED或者驱动一个电磁锁如果收到“识别失败”或者没收到指令就什么都不做。那么前台和后台之间怎么通信呢最常见的有两种方式串口和网络。如果服务器就是STM32旁边的电脑用串口线直接连起来最简单。如果想做得更灵活比如服务器在另一个房间那就可以给STM32配上Wi-Fi模块如ESP8266通过TCP协议把图片数据发送到服务器的指定端口。我们这个原型会以串口通信为例因为它最简单不需要额外的网络配置。理清了思路接下来我们就看看需要准备哪些“道具”。3. 硬件准备与连接要搭起这个舞台我们需要下面这些硬件。别担心它们都是很常见且价格不高的模块。核心控制器STM32F103C8T6最小系统板这块蓝色的小板子大家应该很熟悉了它基于ARM Cortex-M3内核有足够的性能来处理图像采集和通信而且IO口丰富是我们整个系统的指挥中心。眼睛OV7670摄像头模块带FIFO为什么强调要带FIFO因为OV7670输出的图像数据速度很快STM32如果直接去读可能会手忙脚乱。FIFO先入先出存储器就像一个临时仓库摄像头把一帧图像数据先存进去STM32可以慢慢从仓库里把数据搬出来这样就不容易丢数据了。我们选择QVGA320x240分辨率既能看清人脸数据量又不会太大。执行机构5V继电器模块这是一个低电平触发的继电器模块。当STM32给它的控制引脚一个低电平时继电器内部的开关就会吸合可以接通外部电路。我们就用这个来模拟门锁的开关。通信桥梁USB转TTL串口模块用来连接STM32和电脑既给STM32下载程序也建立它们之间的数据通信链路。其他杜邦线、面包板、电源等为了方便连接和测试面包板和一堆杜邦线是少不了的。STM32和继电器模块需要5V供电摄像头模块通常需要3.3V注意核对各自电压。下面是具体的连接方法接线的时候最好关闭电源模块引脚连接到STM32引脚说明OV7670摄像头SIOCPB10I2C时钟线用于配置摄像头参数SIODPB11I2C数据线VSYNCPA8垂直同步信号用于判断一帧开始HREFPC9行同步信号PCLKPC8像素时钟D0-D7PC0-PC78位数据总线传输像素数据XCLKPA9给摄像头提供主时钟RESET接3.3V硬件复位这里直接上拉PWDN接GND电源下降模式这里接地使其工作继电器模块INPB5控制信号输入VCC5V电源正极GNDGND电源地USB转TTLTXPA10 (USART1_RX)模块TX接STM32的RXRXPA9 (USART1_TX)模块RX接STM32的TXGNDGND共地连接要点电源共地所有模块的GND必须连接在一起这是通信的基础。时钟与同步摄像头的XCLK需要由STM32的定时器输出精确的时钟驱动VSYNC和HREF信号用于STM32准确捕获一帧图像。继电器控制继电器模块的IN引脚通过一个STM32的GPIO口控制。程序里让这个引脚输出低电平继电器就动作输出高电平继电器就断开。硬件连接好之后看起来可能会有点乱但功能区域是清晰的左边是摄像头中间是STM32右边是继电器和串口模块。确保连接牢固后我们就可以开始给STM32“灌输思想”了。4. 嵌入式端STM32的程序设计STM32端的程序我们使用HAL库来开发主要完成三个功能初始化所有外设、采集一帧图像、通过串口把图像数据发送出去。这里我们勾勒出核心的代码逻辑。4.1 外设初始化首先我们需要初始化用到的所有外设用于驱动摄像头的I2C和定时器用于捕获图像数据的GPIO和DMA以及用于通信的串口。// 系统时钟、GPIO等基础初始化略过... // 1. 初始化I2C1用于配置OV7670 hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 100000; // 100kHz hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; HAL_I2C_Init(hi2c1); // 2. 初始化TIM2产生XCLK时钟例如10MHz htim2.Instance TIM2; htim2.Init.Prescaler 0; htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 71; // 72MHz / (711) 1MHz 这里需要根据系统时钟计算目标是输出8-10MHz htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; HAL_TIM_OC_Init(htim2); // 配置通道为PWM输出模式并启动 // 3. 初始化USART1用于与电脑通信 huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1); // 4. 初始化用于图像数据捕获的GPIO (PC0-PC7为输入模式) 和 外部中断用于VSYNC, HREF // 5. 初始化一个定时器用于在收到开门指令后控制继电器吸合的时间比如2秒4.2 OV7670摄像头配置OV7670有一大堆寄存器需要配置才能输出我们想要的图像格式和分辨率。通常我们会找一个现成的配置数组。// OV7670 QVGA RGB565 配置寄存器序列示例 const uint8_t ov7670_qvga_rgb565[][2] { {0x12, 0x80}, // 复位所有寄存器 // ... 数十个寄存器配置包括格式、分辨率、曝光、增益等 {0x11, 0x00}, // 内部时钟分频 {0x0C, 0x00}, // 取消使能DCW {0x3E, 0x00}, // 取消使能PCLK分频 {0x70, 0x3A}, // 缩放控制 {0x71, 0x35}, // 缩放控制 {0x72, 0x11}, // 像素时钟分频 {0x73, 0xF0}, // 像素时钟分频 {0xA2, 0x02}, // 缩放偏移 {0x15, 0x20}, // 行时序控制设置QVGA // ... 更多配置 {0x00, 0x00} // 结束标记 }; void OV7670_Init(void) { HAL_Delay(100); // 上电延时 for(int i 0; ov7670_qvga_rgb565[i][0] ! 0xFF; i) { uint8_t reg_addr ov7670_qvga_rgb565[i][0]; uint8_t reg_val ov7670_qvga_rgb565[i][1]; HAL_I2C_Mem_Write(hi2c1, OV7670_ADDR, reg_addr, 1, reg_val, 1, 1000); HAL_Delay(2); } }4.3 图像采集与串口发送这是最核心的部分。我们利用VSYNC帧同步和HREF行同步信号配合DMA或查询方式将一帧图像数据读取到缓冲区。#define IMAGE_WIDTH 320 #define IMAGE_HEIGHT 240 #define IMAGE_SIZE (IMAGE_WIDTH * IMAGE_HEIGHT * 2) // RGB565每个像素2字节 uint8_t image_buffer[IMAGE_SIZE]; volatile uint32_t pixel_count 0; volatile uint8_t capture_done 0; // VSYNC中断服务函数下降沿表示一帧开始 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if(GPIO_Pin VSYNC_Pin) { if(HAL_GPIO_ReadPin(VSYNC_GPIO_Port, VSYNC_Pin) GPIO_PIN_RESET) { // 新的一帧开始重置计数器 pixel_count 0; capture_done 0; } } } // 在HREF为高电平期间于PCLK的下降沿读取数据这里简化处理实际可能用DMA // 假设我们在一个定时器中断或主循环中查询并读取 void Capture_One_Frame(void) { // 等待一帧开始VSYNC变低 while(HAL_GPIO_ReadPin(VSYNC_GPIO_Port, VSYNC_Pin) ! GPIO_PIN_RESET); // 等待VSYNC变高帧开始后的同步阶段 while(HAL_GPIO_ReadPin(VSYNC_GPIO_Port, VSYNC_Pin) ! GPIO_PIN_SET); for(int y 0; y IMAGE_HEIGHT; y) { // 等待HREF变高一行开始 while(HAL_GPIO_ReadPin(HREF_GPIO_Port, HREF_Pin) ! GPIO_PIN_SET); for(int x 0; x IMAGE_WIDTH; x) { // 等待PCLK下降沿数据稳定 while(HAL_GPIO_ReadPin(PCLK_GPIO_Port, PCLK_Pin) ! GPIO_PIN_RESET); // 读取8位数据RGB565需要两个周期读一个像素这里需要更精细的时序处理 uint8_t high_byte GPIO_Read_Data(); while(HAL_GPIO_ReadPin(PCLK_GPIO_Port, PCLK_Pin) ! GPIO_PIN_SET); // 等上升沿 while(HAL_GPIO_ReadPin(PCLK_GPIO_Port, PCLK_Pin) ! GPIO_PIN_RESET); // 等下个下降沿 uint8_t low_byte GPIO_Read_Data(); // 组合成一个像素存入缓冲区 image_buffer[pixel_count] high_byte; image_buffer[pixel_count] low_byte; } // 等待HREF变低一行结束 while(HAL_GPIO_ReadPin(HREF_GPIO_Port, HREF_Pin) ! GPIO_PIN_RESET); } capture_done 1; } // 主循环中当捕获完成通过串口发送图像数据 int main(void) { // 初始化... OV7670_Init(); char tx_buffer[64]; while (1) { // 1. 等待一个触发信号比如按键按下 if(Trigger_Capture()) { sprintf(tx_buffer, START_IMG,%d,%d\r\n, IMAGE_WIDTH, IMAGE_HEIGHT); HAL_UART_Transmit(huart1, (uint8_t*)tx_buffer, strlen(tx_buffer), 1000); // 2. 采集一帧图像 Capture_One_Frame(); // 3. 发送图像数据分包发送避免串口缓冲区溢出 for(uint32_t i 0; i IMAGE_SIZE; i 32) { // 每次发32字节 HAL_UART_Transmit(huart1, image_buffer[i], 32, 1000); HAL_Delay(1); // 小延时让接收方有时间处理 } sprintf(tx_buffer, END_IMG\r\n); HAL_UART_Transmit(huart1, (uint8_t*)tx_buffer, strlen(tx_buffer), 1000); // 4. 等待服务器返回结果 uint8_t rx_cmd 0; if(HAL_UART_Receive(huart1, rx_cmd, 1, 3000) HAL_OK) { if(rx_cmd O) { // 假设收到O代表开门 HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_RESET); // 继电器吸合 HAL_Delay(2000); // 保持2秒 HAL_GPIO_WritePin(RELAY_GPIO_Port, RELAY_Pin, GPIO_PIN_SET); // 继电器断开 } } } HAL_Delay(100); } }这段代码是一个高度简化的框架实际开发中图像采集部分为了效率和稳定性通常会使用DMA直接存储器访问来搬运数据并且要严格处理OV7670的时序。串口通信协议也需要设计得更健壮比如加入帧头帧尾、数据校验等。但核心逻辑就是采集-发送-等待指令-执行动作。5. 服务器端AI模型部署与推理现在图像数据已经通过串口发到电脑上了。电脑这边我们需要一个程序来接收数据并调用AI模型进行人脸检测。这里我们用Python来实现因为它有丰富的库支持。5.1 环境搭建与模型准备首先确保你的电脑上安装了Python然后安装必要的库pip install opencv-python pillow pyserial torch torchvisioncv_resnet101_face-detection_cvpr22papermogface这个模型通常可以在一些模型仓库如TorchHub找到或者作者会提供训练好的权重文件.pth。我们假设你已经下载好了模型文件mogface_resnet101.pth。5.2 串口数据接收与图像重建我们需要编写一个Python脚本监听串口按照STM32定制的协议接收并重组图像。import serial import cv2 import numpy as np import torch from PIL import Image import time # 配置串口端口号和波特率需要根据实际情况修改 ser serial.Serial(COM3, 115200, timeout1) # Windows # ser serial.Serial(/dev/ttyUSB0, 115200, timeout1) # Linux IMAGE_WIDTH 320 IMAGE_HEIGHT 240 IMAGE_SIZE IMAGE_WIDTH * IMAGE_HEIGHT * 2 # RGB565 def receive_image(): 从串口接收一帧图像数据 print(等待图像开始标记...) while True: line ser.readline().decode(ascii, errorsignore).strip() if line.startswith(START_IMG): # 解析图像尺寸这里我们使用固定的但协议里可以传递 # _, width, height line.split(,) print(开始接收图像数据...) break image_data bytearray() bytes_received 0 while bytes_received IMAGE_SIZE: # 协议中STM32是分块发送的比如每次32字节 chunk ser.read(32) if chunk: image_data.extend(chunk) bytes_received len(chunk) # 也可以增加超时判断 # 寻找结束标记 end_marker ser.readline().decode(ascii, errorsignore).strip() if end_marker ! END_IMG: print(图像接收未正确结束) return None print(f图像接收完成共 {len(image_data)} 字节) return image_data def convert_rgb565_to_bgr(image_data): 将RGB565字节数据转换为OpenCV可用的BGR格式图像 # 将字节数据转换为numpy数组并视为uint16 arr np.frombuffer(image_data, dtypenp.uint16).reshape((IMAGE_HEIGHT, IMAGE_WIDTH)) # 提取RGB分量 (RGB565: R[15:11], G[10:5], B[4:0]) r ((arr 0xF800) 11).astype(np.uint8) * 8 # 5位转8位 g ((arr 0x07E0) 5).astype(np.uint8) * 4 # 6位转8位 b (arr 0x001F).astype(np.uint8) * 8 # 5位转8位 # 合并并转换通道顺序为BGR bgr_img np.stack([b, g, r], axis2).astype(np.uint8) return bgr_img5.3 加载模型与人脸检测推理接下来是AI部分加载模型并对接收到的图像进行推理。# 加载人脸检测模型这里以PyTorch为例假设模型接口已知 def load_face_detector(model_path): # 根据模型的具体定义来加载 # 例如如果模型是通过torchvision定义的 # model torchvision.models.detection.fasterrcnn_resnet50_fpn(pretrainedFalse) # model.load_state_dict(torch.load(model_path)) # model.eval() # 由于cv_resnet101_face-detection_cvpr22papermogface是一个特定模型 # 这里我们用一个伪代码和OpenCV的DNN模块作为示例替代。 # 实际中你需要按照该模型作者提供的加载方式进行。 print(加载人脸检测模型...) # 示例使用OpenCV的DNN模块加载一个Caffe模型你需要替换成实际模型文件 # net cv2.dnn.readNetFromCaffe(deploy.prototxt, res10_300x300_ssd_iter_140000.caffemodel) # return net # 为演示我们这里返回None并假设有一个检测函数 return None def detect_faces_opencv(net, image): 使用OpenCV DNN进行人脸检测示例 (h, w) image.shape[:2] # 预处理图像构建blob blob cv2.dnn.blobFromImage(cv2.resize(image, (300, 300)), 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections net.forward() faces [] for i in range(0, detections.shape[2]): confidence detections[0, 0, i, 2] if confidence 0.5: # 置信度阈值 box detections[0, 0, i, 3:7] * np.array([w, h, w, h]) (startX, startY, endX, endY) box.astype(int) faces.append((startX, startY, endX, endY, confidence)) return faces def detect_faces_mogface(model, image): 使用MogFace模型进行检测此处需要根据模型实际接口实现 # 将OpenCV BGR图像转换为RGB PIL图像 rgb_image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) pil_image Image.fromarray(rgb_image) # 图像预处理根据模型要求进行缩放、归一化等 # transform transforms.Compose([...]) # input_tensor transform(pil_image).unsqueeze(0) # 推理 # with torch.no_grad(): # predictions model(input_tensor) # 解析预测结果返回人脸框列表 [(x1, y1, x2, y2, score), ...] # faces process_predictions(predictions, image.shape) # 为演示我们假设检测到一张人脸返回一个模拟框 h, w image.shape[:2] simulated_face [int(w*0.25), int(h*0.25), int(w*0.75), int(h*0.75), 0.99] return [simulated_face] def main_server_loop(): print(人脸检测门禁服务器启动...) # model load_face_detector(mogface_resnet101.pth) # 为了演示我们使用一个简单的OpenCV Haar级联分类器 face_cascade cv2.CascadeClassifier(cv2.data.haarcascades haarcascade_frontalface_default.xml) try: while True: # 1. 接收图像 img_data receive_image() if img_data is None: continue # 2. 转换为BGR图像 bgr_img convert_rgb565_to_bgr(img_data) if bgr_img is None: continue # 3. 进行人脸检测 # 使用MogFace模型 # faces detect_faces_mogface(model, bgr_img) # 使用OpenCV Haar级联分类器简单替代 gray cv2.cvtColor(bgr_img, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5, minSize(30, 30)) # 4. 根据检测结果发送指令 if len(faces) 0: print(f检测到 {len(faces)} 张人脸发送开门指令) ser.write(bO) # 发送字符O代表开门 # 在图像上画框并显示可选 for (x, y, w, h) in faces: cv2.rectangle(bgr_img, (x, y), (xw, yh), (0, 255, 0), 2) else: print(未检测到人脸) ser.write(bF) # 发送字符F代表失败 # 显示图像可选用于调试 cv2.imshow(Received Image, bgr_img) if cv2.waitKey(1) 0xFF ord(q): break except KeyboardInterrupt: print(服务器关闭) finally: ser.close() cv2.destroyAllWindows() if __name__ __main__: main_server_loop()这个Python脚本扮演了服务器的角色。它不断监听串口接收STM32发来的图像数据将其还原成图片然后调用人脸检测模型进行分析。一旦检测到人脸就通过串口往回发送一个简单的指令比如字母‘O’。STM32收到这个指令后就会执行开门动作。6. 联调测试与效果展示当硬件连接妥当两端代码都准备就绪后最激动人心的联调时刻就到了。这个过程可能不会一帆风顺但解决问题的每一步都很有收获。第一步分模块测试。STM32独立测试先不接摄像头和继电器只测试串口。写个简单程序让STM32定时通过串口发送“Hello”确保电脑端能正确收到。然后测试继电器控制写个程序让继电器每隔几秒吸合、断开一次听听那清脆的“咔嗒”声确认硬件控制没问题。摄像头测试单独测试OV7670。可以编写一个程序将采集到的图像数据通过某种方式比如用LCD屏显示或者通过串口发送小尺寸的灰度图输出确认摄像头工作正常图像没有严重的错位或色彩问题。服务器端测试先用一张本地图片测试你的人脸检测脚本确保模型能正确加载并输出检测框。第二步串联通信测试。让STM32开始采集并发送图像数据同时运行电脑上的Python接收脚本。你可能会在串口助手上看到一堆乱码这是正常的因为发送的是二进制图像数据。关键看Python脚本能不能正确解析出“START_IMG”和“END_IMG”标记并计算出接收到的数据量是否正确。如果数据量对不上可能是STM32发送太快电脑端处理不过来需要在发送循环里加一点延时。第三步全流程功能测试。当图像能够稳定地从STM32传到电脑并成功重建后就可以进行端到端测试了。让人站在摄像头前按下STM32的触发按键。你应该能在电脑端的OpenCV窗口里看到接收到的图像并且如果人脸检测成功图像上会画出绿色的框同时继电器会“咔嗒”一声吸合保持2秒后再断开。实际效果 当一切就绪这个原型系统运行起来会是这样按下按钮摄像头旁的指示灯闪烁一下表示在采集电脑屏幕快速显示刚刚拍下的画面并框出人脸同时你会听到继电器动作的声音。整个过程从按下按钮到继电器动作大概会有1到3秒的延迟主要耗时在图像传输和AI推理上。用cv_resnet101_face-detection这类较新的模型在光线良好、人脸正对摄像头的情况下检测准确率会很高。你可以尝试在不同距离、不同角度、不同光照条件下测试它的表现直观地感受AI模型的鲁棒性。你也可以在服务器端的代码里加入更多功能比如记录识别日志、或者与一个合法人脸数据库进行比对这就升级成人脸识别了。7. 总结回顾整个项目我们完成了一次从硬件到软件、从数据采集到AI推理的完整闭环。STM32F103C8T6作为前端可靠地完成了图像采集和指令执行的任务而运行在电脑上的cv_resnet101_face-detection模型则提供了强大的视觉感知能力。两者通过串口这个简单的桥梁协同工作实现了一个有趣的“刷脸开门”原型。这个项目的价值不在于它有多复杂或多高的精度而在于它清晰地展示了一种思路如何将前沿的AI模型与普及度极高的嵌入式硬件结合起来去解决一个具体的实际问题。它涉及的技能点很综合包括嵌入式编程、硬件接口、通信协议和AI应用部署对于学习者来说是一个很好的练手项目。在实际动手的过程中你可能会遇到比文中提到的更多的细节问题比如OV7670的稳定驱动、串口通信的数据纠错、服务器端模型的效率优化等等。每一个问题的解决都会让你对系统的理解更深一层。当你最终看到继电器随着你的脸出现在镜头前而动作时那种把代码和电路变成现实功能的成就感正是硬件开发的魅力所在。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。