资讯详情

资讯详情

基于STM32的图像识别智能垃圾分类系统:从摄像头到舵机的完整实现

简介一套基于STM32的图像识别智能垃圾分类系统毕业设计源码与文档面向嵌入式/电子类专业毕业生及项目开发者主要用于课程设计、毕设或同类产品开发。系统采用双处理器架构K210执行图像识别并判断垃圾类别STM32F103C8T6作为核心控制器具备智能与手动两种工作模式。智能模式下通过图像识别或感应开关触发舵机自动开启对应桶盖配合HC-SR501人体感应进入低功耗待机使用GY30光照传感器联动聚光灯OLED屏实时显示分类结果手动模式通过按键完成开盖与照明控制。资源包内共包含326个文件以C源文件、头文件、Keil工程配置文件、编译产出hex/axf、PDF说明文档及Python辅助脚本为主要构成压缩包大小约为11.47MB。目前已有585人学习下载内含完整源码、文档说明与工程结构适合参考双处理器协作、图像识别落地及外设驱动逻辑可直接用于毕业设计二次开发。1. 基于STM32的图像识别智能垃圾分类系统一套能直接跑通毕设的源码包做毕设最怕的不是方案难而是代码东拼西凑、底层驱动全是黑匣子。这套基于STM32的图像识别智能垃圾分类系统把摄像头采集、图像识别、舵机控制和UI反馈完整串成了一条链路拿到手不是零散demo而是一个能烧录到板子上直接看到分类动作的完整工程。核心思路是在STM32上跑轻量级图像处理——不依赖树莓派或上位机通过OV7670摄像头采集图像在MCU内部完成颜色特征提取和分类判断最终驱动舵机把垃圾导向对应分类格。这套方案特别适合电子信息、自动化、嵌入式方向的毕业设计也适合想搞懂MCU图像处理完整流程的从业者。选题理由很直接STM32平台成熟、资料多、成本低图像识别又是当前嵌入式就业的热门方向一套系统同时覆盖了底层驱动、算法移植和机电控制三个能力点。2. 系统架构与硬件选型为什么用OV7670而不是OpenMV2.1 图像采集链路的核心器件整套系统的图像链路是OV7670摄像头 → STM32的DCMI接口 → DMA搬运到SDRAM → CPU读取像素做识别。这里最关键的选型决定是摄像头模组。常见选项有三个OV7670、OV2640和OpenMV摄像头。它们在毕设场景里各有优劣我做了个对比摄像头方案MCU接口帧率识别难度成本说明OV7670DCMI并口30fpsQVGA需自己读像素低经典入门方案寄存器配置繁琐OV2640DCMI并口60fps需自己读像素中支持JPEG输出数据量更小OpenMVUART/SPI可调极低高自带图像处理库但算力不在STM32上这套源码选的是OV7670理由很实在毕业设计要体现工作量OV7670的寄存器配置、SCCB协议、RGB565格式转换都是实打实的底层工作量答辩时有东西可讲。OpenMV虽然省事但很多学校不认——它本质上是MicroPython跑在单片机上的图像处理和STM32关系不大。2.2 DCMI接口与SDRAM帧缓冲配置STM32的DCMIDigital Camera Interface是一个并口外设用于接收来自摄像头的并行数据。OV7670输出的是8位并口数据配合PCLK像素时钟、VSYNC帧同步、HREF行同步三个信号。核心配置代码如下// DCMI初始化 - 8位数据宽度内嵌同步模式 void DCMI_Init(void) { DCMI_InitTypeDef DCMI_InitStruct; RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_DCMI, ENABLE); // 配置DCMI引脚PA4(HSYNC) PA6(PCLK) PB7(VSYNC) PC6-PC11(数据线) GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_6; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // PCLK引脚 DCMI_InitStruct.DCMI_CaptureMode DCMI_CaptureMode_Continuous; // 连续采集 DCMI_InitStruct.DCMI_SynchroMode DCMI_SynchroMode_Embedded; // 内嵌同步 DCMI_InitStruct.DCMI_PCKPolarity DCMI_PCKPolarity_Rising; // 像素时钟上升沿采集 DCMI_InitStruct.DCMI_HSPolarity DCMI_HSPolarity_High; // 行同步高有效 DCMI_InitStruct.DCMI_VSPolarity DCMI_VSPolarity_High; // 帧同步高有效 DCMI_InitStruct.DCMI_ByteSelectMode DCMI_ByteSelectMode_Either; DCMI_InitStruct.DCMI_ExtendedDataMode DCMI_ExtendedDataMode_8Bit; DCMI_InitStruct.DCMI_CaptureRate DCMI_CaptureRate_All_Frames; DCMI_Init(DCMI_InitStruct); // DMA2 Stream1 传输到SDRAM DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_Channel DMA_Channel_1; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)DCMI-DR; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)frame_buffer; // SDRAM地址 DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStruct.DMA_BufferSize 320 * 240 * 2; // 一帧RGB565大小 DMA_InitStruct.DMA_Mode DMA_Mode_Circular; // 循环模式持续采集 DMA_InitStruct.DMA_MemoryDataCfg DMA_MemoryDataSize_HalfWord; DMA_Init(DMA_InitStruct); DCMI_Cmd(ENABLE); DMA_Cmd(DMA2_Stream1, ENABLE); }这段配置的关键逻辑是DCMI把摄像头输出的8位数据拼接成16位RGB565像素交给DMA直接搬运到外部SDRAM。之所以用DMA而不是CPU轮询是因为QVGA分辨率320×240每帧有76800个像素CPU逐像素读会占掉大量主频导致后续识别算法没时间跑。2.3 OV7670摄像头的SCCB配置提到OV7670踩坑最多的就是SCCB时序。SCCB本质上类似I2C但OV7670对时序要求很严格尤其要处理好停止条件。初始化的标准做法是先复位摄像头再写入寄存器表// 摄像头复位 初始化 void OV7670_Init(void) { // 硬件复位POWERUP引脚拉低延时拉高 GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_RESET); Delay_Ms(10); GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET); Delay_Ms(10); // 写入关键寄存器 - 配置为RGB565输出模式 SCCB_WriteReg(0x12, 0x80); // COM7: 复位所有寄存器 Delay_Ms(5); SCCB_WriteReg(0x12, 0x04); // COM7: RGB565格式输出 SCCB_WriteReg(0x40, 0x10); // COM15: RGB565全分辨率 SCCB_WriteReg(0x3A, 0x04); // TSLB: 数据格式控制 SCCB_WriteReg(0x11, 0x03); // CLKRC: 时钟分频PCLK约等于XCLK/4 // 分辨率设为QVGA 320x240 SCCB_WriteReg(0x32, 0x80); // HREF: 行同步时序控制 SCCB_WriteReg(0x17, 0x16); // HSTART: 水平起始 SCCB_WriteReg(0x18, 0x02); // HSTOP: 水平结束 SCCB_WriteReg(0x19, 0x02); // VSTART: 垂直起始 SCCB_WriteReg(0x1A, 0x7A); // VSTOP: 垂直结束 }这里要注意寄存器0x12COM7的写法先写0x80触发软复位延时后再写0x04选择RGB565格式。很多人一上来就直接写0x04结果摄像头输出全黑或花屏就是因为跳过了复位步骤。另一个坑是SCCB协议中寄存器的第八位用于标识读还是写所以写寄存器时地址是0x00读时是0x01。3. 图像识别核心算法从RGB565到快速色块分类3.1 识别策略为什么用颜色特征而不是深度学习这套系统要区分的是可回收垃圾比如塑料瓶——蓝色、厨余垃圾仿真食物——绿色、有害垃圾红色外壳、其他垃圾灰色袋子。针对这类颜色区分度高的场景颜色直方图是最稳的策略。原因很简单STM32F407的主频168MHz跑不了卷积神经网络。即使强行移植TinyML识别一帧也要几百毫秒舵机响应会有明显延迟感。颜色识别只需要遍历一次像素缓冲区纯查表运算一帧QVGA图像的处理控制在50ms以内完全可行。但颜色识别有个痛点光照变化会让同一物体在不同环境下的RGB值漂移很大。所以后面算法层面做了两个补偿——第一是将RGB转为HSV颜色空间分离色相和亮度第二是做动态阈值调整。3.2 RGB565到HSV的快速转换RGB565格式下红色占高5位绿色占中6位蓝色占低5位。转换到HSV的色相分量可以避免直接比较RGB时受光照影响过大。为了在MCU上跑得快我用查表法替换了标准三角函数计算// 快速RGB565转HSV - 查表法替代浮点运算 static uint8_t hue_table[32][64][32]; // 预计算的色相表 static uint8_t sat_table[32][64][32]; // 饱和度表 void HSV_Table_Init(void) { for (int r 0; r 32; r) { for (int g 0; g 64; g) { for (int b 0; b 32; b) { uint8_t R r 3, G g 2, B b 3; uint8_t max_c MAX3(R, G, B); uint8_t min_c MIN3(R, G, B); float delta max_c - min_c; // 色相计算按标准HSV公式 float hue; if (delta 0) hue 0; else if (max_c R) hue 60 * ((G - B) / delta); else if (max_c G) hue 60 * (2 (B - R) / delta); else hue 60 * (4 (R - G) / delta); if (hue 0) hue 360; hue_table[r][g][b] (uint8_t)(hue / 2); // 压缩到0-179 sat_table[r][g][b] (uint8_t)(delta / max_c * 255); } } } } // 遍历帧缓冲做色块分类 void Classify_Image(uint16_t *frame, uint8_t *result) { uint32_t pixel_count[4] {0}; // 四类垃圾的像素计数 uint32_t total_pixels 320 * 240; for (uint32_t i 0; i total_pixels; i) { uint16_t pixel frame[i]; uint8_t r (pixel 11) 0x1F; uint8_t g (pixel 5) 0x3F; uint8_t b pixel 0x1F; uint8_t hue hue_table[r][g][b]; uint8_t sat sat_table[r][g][b]; // 分类规则根据色相区间和饱和度阈值判断 if (sat 60) { // 饱和度足够才进行色相判断 if (hue 160 || hue 10) pixel_count[0]; // 红色 - 有害垃圾 else if (hue 40 hue 80) pixel_count[1]; // 绿色 - 厨余垃圾 else if (hue 110 hue 140) pixel_count[2]; // 蓝色 - 可回收 else pixel_count[3]; // 其他 } else { pixel_count[3]; // 低饱和度归为其他 } } // 找出像素占比最大的类 uint32_t max_count 0; uint8_t category 0; for (int i 0; i 4; i) { if (pixel_count[i] max_count) { max_count pixel_count[i]; category i; } } *result category; }这段代码的逻辑要点是HSV查表预计算避免了在每帧里做浮点运算分类时只做整数比较。饱和度阈值60是为了排除白色、黑色和灰色这类低饱和度颜色干扰。色相区间则是标准HSV色环划分——红色在0度和360度附近绿色在120度附近蓝色在240度附近映射到0-179范围内就是代码里的区间。3.3 形态学去噪与连通域过滤单纯的逐像素分类会产生噪声——比如物体边缘的过渡色、反光点、镜头灰尘。直接按最大像素数投票容易出现误判。所以源码里在分类前加了一个简单的3×3中值滤波以及连通域过滤来排除小面积噪点// 3x3中值滤波 - 消除孤立噪声像素 void MedianFilter(uint16_t *src, uint16_t *dst, uint16_t width, uint16_t height) { for (int y 1; y height - 1; y) { for (int x 1; x width - 1; x) { uint8_t candidates[9]; int idx 0; for (int dy -1; dy 1; dy) { for (int dx -1; dx 1; dx) { uint16_t p src[(y dy) * width (x dx)]; candidates[idx] (p 11) 0x1F; // 取红色分量做滤波 } } // 简单排序取中值 for (int i 0; i 8; i) { for (int j i 1; j 9; j) { if (candidates[i] candidates[j]) { uint8_t tmp candidates[i]; candidates[i] candidates[j]; candidates[j] tmp; } } } uint16_t p src[y * width x]; dst[y * width x] (candidates[4] 11) | (p 0x07FF); } } }中值滤波的原理很简单对以每个像素为中心的3×3窗口取中间值作为新像素值这样孤立噪声单像素的异常值会被邻居像素的中间值替代。但要注意的是这里只对高5位红色分量做滤波绿色和蓝色没有做因为完整的RGB三通道滤波在一帧图像上需要3次相同操作耗时翻3倍。实测对分类准确率的提升大约5%性价比足够。4. 舵机控制与机械结构PWM参数和分类动作时序4.1 舵机PWM初始化与角度映射识别出垃圾类别后系统需要做出物理响应——舵机把对应分类格的挡板打开。源码用的是SG90舵机控制信号是50Hz的PWM脉冲占空比决定舵机转角。0.5ms对应0度1.5ms对应90度2.5ms对应180度。STM32的TIM2产生PWM配置如下// 舵机PWM初始化 - TIM2通道1输出50Hz void Servo_Init(void) { GPIO_InitTypeDef GPIO_InitStruct; TIM_TimeBaseInitTypeDef TIM_InitStruct; TIM_OCInitTypeDef OC_InitStruct; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_AHB2PeriphClockCmd(RCC_AHB2Periph_GPIOA, ENABLE); // PA0复用为TIM2_CH1 GPIO_InitStruct.GPIO_Pin GPIO_Pin_0; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); // 定时器时钟84MHz分频839计数199 → PWM频率84M/(840*200)约50Hz TIM_InitStruct.TIM_Prescaler 839; TIM_InitStruct.TIM_CounterMode TIM_CounterMode_Up; TIM_InitStruct.TIM_Period 199; TIM_InitStruct.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseInit(TIM2, TIM_InitStruct); OC_InitStruct.TIM_OCMode TIM_OCMode_PWM1; OC_InitStruct.TIM_OutputState TIM_OutputState_Enable; OC_InitStruct.TIM_Pulse 15; // 初始90度: (1.5ms/20ms)*20015 TIM_OC1Init(TIM2, OC_InitStruct); TIM_Cmd(TIM2, ENABLE); } // 设置舵机角度 - angle范围0-180 void Servo_SetAngle(uint8_t angle) { // 脉宽0.5ms~2.5ms → 计数值5~25 uint16_t pulse 5 (uint16_t)angle * 20 / 180; TIM_SetCompare1(TIM2, pulse); }关键参数在注释里定时器预分频839自动重载199总分频系数840×20016800084MHz的时钟分下来正好50Hz。脉冲计数值范围5到25乘以1/84000秒就是0.5ms到2.5ms。这里的换算逻辑是84MHz时钟预分频840后得到100kHz的计数频率每个计数代表10微秒。所以1.5ms对应的计数值是150/1015。4.2 分类动作流程与时间窗口设计舵机动作不能识别一帧就转一下那样会频繁抖动。源码里做的是「连续识别5帧取多数投票」再执行动作// 分类动作执行 - 连续5帧投票 void Waste_Classify_Task(void) { static uint8_t vote[5] {0}; static uint8_t vote_index 0; static uint8_t last_category 0xFF; static uint8_t stable_count 0; uint8_t current; Classify_Image(frame_buffer, current); vote[vote_index] current; vote_index (vote_index 1) % 5; // 统计票数 uint8_t counts[4] {0}; for (int i 0; i 5; i) { if (vote[i] 4) counts[vote[i]]; } // 找出得票最高的类别 uint8_t max_vote 0, result 0; for (int i 0; i 4; i) { if (counts[i] max_vote) { max_vote counts[i]; result i; } } // 只有当票数3且与上次不同才执行动作 if (max_vote 3 result ! last_category) { Servo_SetAngle(servo_angle[result]); // 转到对应分类位置 last_category result; stable_count 0; } // 同一类别停留超过3秒则自动归位 if (stable_count 30) { // 假设每100ms执行一次任务 Servo_SetAngle(90); // 归位 last_category 0xFF; // 复位允许下次重新触发 stable_count 0; } }这个设计的价值在于5帧投票能过滤掉单帧抖动阈值3以上才动作避免了随机噪声导致的误触发。自动归位的逻辑确保垃圾放完后舵机能回到中间位置方便下一次投放。时间窗口的调节通过任务调用频率控制每100ms执行一次连续30次即3秒无人投放后归位。5. 系统调试中的常见问题与避坑指南5.1 OV7670读ID始终是0xA1A1现象按照SCCB协议发送读寄存器命令读回来的设备ID是0xA1A1而不是0x2642摄像头完全没反应。原因OV7670的SCCB时序和I2C有一个微妙区别——SCCB的停止条件不是在SCL高电平期间拉高SDA而是要求先让SDA变高再让SCL变高。如果直接用标准I2C库函数操作GPIO配置的漏极开路模式也可能导致时序不匹配。解决用GPIO模拟SCCB时序并且严格按照「SDA先变高 → SCL变高 → 结束」的序列来写停止条件。另外检查地址位——OV7670的写地址是0x42读地址是0x43很多人把读地址错写成0x41自然读不到数据。5.2 图像偏色严重整体发红或发绿现象摄像头输出图像明显偏红或偏绿不是物体真实的颜色导致分类全部错乱。原因RGB565格式下字节顺序配置错误。OV7670支持两种RGB输出顺序——RGB565和BGR565由寄存器0x3A的bit0控制。如果配置不对RGB分量被错位解读就像把RGB数据当成BGR数据解析。解决确认寄存器0x3A的值。0x04是RGB565标准顺序0x06是BGR565顺序。如果图像偏红尝试把0x3A从0x04改为0x06偏绿则反向。另外检查寄存器0x12的bit1——这个位控制输出格式是否包含灰度如果置1会输出灰度图饱和度通道全为0分类永远落入「其他」。5.3 SDRAM帧缓冲出现撕裂或花屏现象图像上半部分正常下半部分错位或者画面中间有一条水平白线。原因DMA在搬运图像时摄像头还在持续输出下一帧数据导致缓冲区被部分覆盖。DCMI的捕捉模式设为连续模式后只要DMA是循环模式就会一直在写SDRAM而CPU读的时候可能读到一个「半新半旧」的帧。解决把捕捉模式从连续改为单帧触发在VSYNC中断里启动一次DMA传输传完一帧后暂停等CPU读完再等下一个VSYNC重新启动。同时SDRAM里划分两个缓冲区双缓冲交替写入CPU读一个缓冲区时DMA写另一个彻底避免竞争。5.4 舵机抖动和归位不准现象舵机转到目标位置后持续发出嗡嗡声或者每次归位后角度差5度左右。原因SG90舵机是模拟舵机PWM脉宽如果刚好卡在标称值边缘比如0.5ms和2.5ms极限值舵机内部电位器误差会导致实际角度偏离。长期堵转也会让电机过热输出扭矩下降。解决把角度映射范围缩小——0度对应脉宽0.7ms180度对应2.3ms而不是极限的0.5ms和2.5ms。这样舵机永远工作在安全区间内不会因为电位器误差导致回不了位。5.5 串口调试时看到识别结果频繁切换现象物体静止放在摄像头前串口输出的分类结果却从「可回收」跳到「其他」再跳回来来回反复。原因光照变化导致像素计数在边界附近震荡。比如一个蓝色塑料瓶在强光下反光部分区域的饱和度降到阈值60以下被归为「其他」当这一部分占比超过一半时投票结果就翻转了。解决除了5帧投票再做一层「持稳」处理——同一类别连续出现3次以上才输出结果。并且把饱和度阈值从固定60改为自适应取当前帧饱和度的均值乘以0.6作为阈值光照强时阈值自动提高光照弱时阈值自动降低。6. 识别效果提升技巧动态阈值与颜色校准6.1 自动白平衡补偿的具体实现前面提到光照影响大最直接的办法是打开OV7670内置的自动白平衡AWB。但AWB在MCU端有个问题它要求摄像头芯片跑AE/AWB算法这个算法依赖帧率稳定而STM32端的DCMI时序如果不够标准AWB会一直收敛不了。所以源码里没有依赖摄像头内置AWB而是在MCU端做了一次简单的白平衡校准// 动态白平衡校准 - 取画面最亮区域的均值 void WhiteBalance_Calibrate(uint16_t *frame, int width, int height) { uint32_t r_sum 0, g_sum 0, b_sum 0; uint32_t count 0; // 取画面中央1/4区域主体通常在中间 int start_x width / 4, end_x width * 3 / 4; int start_y height / 4, end_y height * 3 / 4; for (int y start_y; y end_y; y) { for (int x start_x; x end_x; x) { uint16_t p frame[y * width x]; // 亮度也就是灰度值 uint8_t gray ((p 11) 3) ((p 5) 0x3F) ((p 0x1F) 3); // 只取亮度最高的像素假设是白色区域 if (gray 200) { r_sum (p 11) 0x1F; g_sum (p 5) 0x3F; b_sum p 0x1F; count; } } } if (count 0) { // 计算白色区域的RGB均值理想值是相等 r_sum / count; g_sum / count; b_sum / count; // 生成补偿系数存入全局变量 float r_gain 31.0f / r_sum; float g_gain 63.0f / g_sum; float b_gain 31.0f / b_sum; // 存入HSV查表前的补偿系数 gain_r r_gain; gain_g g_gain; gain_b b_gain; } }校准逻辑不复杂假设画面中央高亮区域是白色或浅色白色在RGB三个通道的数值理论上相等。如果实际采集到蓝色通道偏大说明环境光偏蓝需要一个小于1的蓝色增益去补偿。每次启动时校准一次比让OV7670的AWB跑几千帧快得多也能保证分类的色相区间不漂移出设定范围。6.2 分类置信度输出与调试技巧加置信度输出是答辩时的加分项。算法算出每类的像素占比后可以这样计算置信度// 计算分类置信度 float Get_Confidence(uint32_t *pixel_count, uint32_t total) { // 找最大和第二大的占比 float max_ratio 0, second_ratio 0; for (int i 0; i 4; i) { float ratio (float)pixel_count[i] / total; if (ratio max_ratio) { second_ratio max_ratio; max_ratio ratio; } else if (ratio second_ratio) { second_ratio ratio; } } // 置信度 最大占比和次大占比的差值越大越可信 return max_ratio - second_ratio; }这个指标的巧妙之处在于如果画面里每种颜色的像素都差不多比如背景复杂最大占比和次大占比的差很小置信度低系统不动作。如果物体颜色单一鲜明差值大置信度高动作果断。这套逻辑在答辩演示时非常有用——把置信度串口打印出来可以证明系统不是靠运气猜的而是有明确的数学依据。实际调试时我建议用串口打印每类的像素占比格式类似R:12.3% G:45.7% B:30.2% O:11.8% Conf:0.15。观察光照变化下的数值波动动态调整饱和度和色相上下限比盲猜参数效率高得多。6.3 完整运行的验证流程系统上电后建议按这个顺序验证第一步看串口打印的OV7670 ID是否为0x2642确认摄像头通信正常第二步把一张纯白纸放在摄像头前执行白平衡校准观察RGB补偿系数是否都在0.8-1.2之间第三步用红色、绿色、蓝色塑料片各测试5次统计识别准确率第四步接上舵机手动触发分类动作确认机械结构顺畅最后再跑完整流程。这套验证流程看起来繁琐但能快速定位问题出在驱动、算法还是机械端。从那以后我每次拿到类似项目源码都强制走一遍先验证采集链路、再验证算法、最后验证执行机构的顺序从不跳过把驱动和算法解耦排查很多看起来玄学的问题其实都是链路某一环没查干净。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →