STM32 OV7725摄像头模块颜色识别实战(1)--HSL阈值分割与腐蚀中心定位,实现串口坐标输出
1. OV7725摄像头模块与STM32的硬件连接OV7725是一款30万像素的CMOS图像传感器常用于嵌入式视觉项目。它通过SCCBSerial Camera Control Bus接口进行配置通过并行数据接口输出图像数据。与STM32连接时需要注意以下几个关键点电源连接OV7725需要3.3V供电注意AVDD和DVDD都要连接时钟信号外部需要提供24MHz时钟可以使用STM32的MCO输出数据接口8位并行数据总线(D0-D7)连接到STM32的GPIO控制信号VSYNC(垂直同步)HREF(行同步)PCLK(像素时钟)SCCB接口类似于I2C使用SIO_C(时钟)和SIO_D(数据)两根线实际接线时我推荐使用杜邦线先测试确认各信号正常后再设计PCB。常见问题是PCLK信号质量不好导致图像错位这时可以尝试降低时钟频率或在信号线上加小电容滤波。2. 图像采集与显示到PCOV7725输出的默认格式是RGB565每个像素用16位表示(5位红6位绿5位蓝)。要将图像显示在PC上需要配置OV7725寄存器通过SCCB设置分辨率、输出格式等STM32采集图像数据使用GPIO读取数据总线配合VSYNC和HREF信号通过串口发送到PC使用高速串口(如256000bps)发送数据这里有个实际项目中的坑OV7725默认是大端模式而很多PC端软件预期小端模式。如果图像颜色异常可以尝试修改字节序。发送图像的协议可以这样设计[帧头0x01][帧头0xFE][图像数据...][帧尾0xFE][帧尾0x01]PC端可以使用山外调试助手等工具显示图像。如果图像出现错位检查VSYNC和HREF信号的解析是否正确。3. RGB到HSL色彩空间转换HSL色彩空间更适合颜色识别因为它将颜色信息(H)、饱和度(S)和亮度(L)分开表示。转换过程如下RGB565转RGB888先扩展颜色深度void RGB565_To_RGB888(uint16_t rgb, COLOR_RGB *color_rgb) { color_rgb-Red (rgb 0xF800) 8; color_rgb-Green (rgb 0x07E0) 3; color_rgb-Blue (rgb 0x001F) 3; }RGB转HSL使用以下公式计算void RGB888_TO_HSL(COLOR_RGB* color_rgb, COLOR_HLS* color_hls) { uint8_t r color_rgb-Red; uint8_t g color_rgb-Green; uint8_t b color_rgb-Blue; uint8_t max maxOf3Values(r, g, b); uint8_t min minOf3Values(r, g, b); uint8_t dif max - min; // 计算亮度L color_hls-Lightness (max min) * 240 / 255 / 2; // 计算饱和度S if(max min) { color_hls-Saturation 0; } else { if(color_hls-Lightness 120) { color_hls-Saturation dif * 240 / (max min); } else { color_hls-Saturation dif * 240 / (480 - (max min)); } } // 计算色相H if(max min) { color_hls-Hue 0; } else if(max r) { if(g b) { color_hls-Hue 40 * (g - b) / dif; } else { color_hls-Hue 40 * (g - b) / dif 240; } } else if(max g) { color_hls-Hue 40 * (b - r) / dif 80; } else if(max b) { color_hls-Hue 40 * (r - g) / dif 160; } }在实际项目中我发现HSL的H分量对光照变化比较鲁棒适合用来识别特定颜色的物体。4. 颜色阈值分割与二值化得到HSL值后可以通过阈值分割提取特定颜色的区域定义目标颜色范围TARGET_CONDITION red_condition { 0, // H_MIN 20, // H_MAX (红色在HSL色环的0-20度) 50, // S_MIN 240, // S_MAX 30, // L_MIN 220, // L_MAX 10, // WIDTH_MIN 10, // HEIGHT_MIN 240, // WIDTH_MAX 320 // HEIGHT_MAX };颜色匹配函数int ColorMatch(const COLOR_HLS* color_hls, const TARGET_CONDITION* condition) { // 检查亮度和饱和度 if(color_hls-Lightness condition-L_MIN color_hls-Lightness condition-L_MAX color_hls-Saturation condition-S_MIN color_hls-Saturation condition-S_MAX) { // 检查色相 if(condition-H_MAX condition-H_MIN) { if(color_hls-Hue condition-H_MIN color_hls-Hue condition-H_MAX) { return 1; } } else { // 处理色相环绕情况(如H_MIN350, H_MAX10) if(color_hls-Hue condition-H_MAX || color_hls-Hue condition-H_MIN) { return 1; } } } return 0; }生成二值化图像for(int x0; x240; x) { for(int y0; y320; y) { uint16_t rgb565 GetPixel(x, y); COLOR_RGB rgb888; COLOR_HLS hls; RGB565_To_RGB888(rgb565, rgb888); RGB888_TO_HSL(rgb888, hls); if(ColorMatch(hls, red_condition)) { SetBinaryPixel(x, y, 1); // 目标颜色设为白色 } else { SetBinaryPixel(x, y, 0); // 其他设为黑色 } } }在实际测试中我发现室内的光照条件会显著影响颜色识别效果。建议在实际使用前在不同光照条件下采集样本图像调整HSL阈值参数。5. 腐蚀中心定位算法腐蚀中心算法用于定位目标物体的中心位置主要步骤如下寻找初始腐蚀中心int SearchCenter(unsigned short* x, unsigned short* y, const TARGET_CONDITION* condition, SEARCH_AREA* area) { // 以目标最小尺寸的1/3为搜索步长 unsigned short SpaceX condition-WIDTH_MIN / 3; unsigned short SpaceY condition-HEIGHT_MIN / 3; for(int iarea-Y_Start; iarea-Y_End; iSpaceY) { for(int jarea-X_Start; jarea-X_End; jSpaceX) { int failCount 0; // 检查搜索区域中心的十字区域 for(int k0; kSpaceXSpaceY; k) { if(k SpaceX) { // 水平线 if(!ReadColor(jk, iSpaceY/2)) failCount; } else { // 垂直线 if(!ReadColor(jSpaceX/2, ik-SpaceX)) failCount; } if(failCount (SpaceXSpaceY)/10) break; // 容错率10% } if(failCount (SpaceXSpaceY)/10) { *x j SpaceX / 2; *y i SpaceY / 2; return 1; // 找到腐蚀中心 } } } return 0; // 未找到 }从腐蚀中心向外扩展int Corrode(unsigned short oldX, unsigned short oldY, const TARGET_CONDITION* condition, RESULT* result) { unsigned short Xmin, Xmax, Ymin, Ymax; unsigned short i; unsigned short failCount 0; // 向左搜索边界 for(ioldX; i0; i--) { if(!ReadColor(i, oldY)) failCount; if(failCount condition-WIDTH_MIN/10 || i0) break; } Xmin i; // 向右搜索边界 (类似代码省略) // 向上搜索边界 (类似代码省略) // 向下搜索边界 (类似代码省略) // 计算中心点和宽高 result-x (Xmin Xmax) / 2; result-y (Ymin Ymax) / 2; result-w Xmax - Xmin; result-h Ymax - Ymin; // 检查目标尺寸是否在合理范围内 if(result-w condition-WIDTH_MIN result-w condition-WIDTH_MAX result-h condition-HEIGHT_MIN result-h condition-HEIGHT_MAX) { return 1; } return 0; }迭代优化中心位置int Trace(const TARGET_CONDITION* condition, RESULT* result_final) { static unsigned short x0 0, y0 0; static unsigned char flag 0; static SEARCH_AREA area {0, 240, 0, 320}; // 全图搜索 RESULT result; if(flag 0) { // 初始搜索或上次失败 if(!SearchCenter(x0, y0, condition, area)) { flag 0; return 0; } flag 1; } // 使用上次的中心点进行腐蚀 result.x x0; result.y y0; // 迭代腐蚀8次 for(int i0; i8; i) { Corrode(result.x, result.y, condition, result); } if(Corrode(result.x, result.y, condition, result)) { // 更新中心点和搜索区域 x0 result.x; y0 result.y; area.X_Start result.x - result.w/2; area.X_End result.x result.w/2; area.Y_Start result.y - result.h/2; area.Y_End result.y result.h/2; *result_final result; return 1; } else { flag 0; return 0; } }在实际测试中腐蚀中心算法对孤立的目标物体效果很好但对于多个相邻的同类物体可能会识别为一个整体。这时可以考虑使用连通域分析等更复杂的算法。6. 串口输出坐标数据定位到目标中心后可以通过串口将坐标发送给上位机RESULT target; if(Trace(red_condition, target)) { // 发送坐标数据协议: [0xAA][0x55][X高][X低][Y高][Y低][0x55][0xAA] uint8_t buf[8] {0xAA, 0x55, (target.x 8) 0xFF, target.x 0xFF, (target.y 8) 0xFF, target.y 0xFF, 0x55, 0xAA}; HAL_UART_Transmit(huart1, buf, 8, 100); // 在图像上标记中心点 GUI_DrawLine(target.x-10, target.y, target.x10, target.y); GUI_DrawLine(target.x, target.y-10, target.x, target.y10); }PC端可以用串口助手接收数据或者自己编写上位机程序解析坐标。如果需要更高的传输速率可以考虑使用USB CDC或者网络接口。7. 实际项目中的优化建议内存优化STM32F1内存有限可以使用压缩格式存储二值化图像将二值化图像按位存储320x240的图像只需9600字节#define BINARY_WIDTH 40 // 320/8 uint8_t binaryImage[240][BINARY_WIDTH]; void SetBinaryPixel(int x, int y, int value) { if(value) { binaryImage[x][y/8] | (0x80 (y%8)); } else { binaryImage[x][y/8] ~(0x80 (y%8)); } }性能优化使用DMA传输图像数据对HSL转换进行查表优化限制搜索区域减少计算量鲁棒性改进添加目标丢失检测机制使用移动平均滤波平滑坐标输出在不同光照条件下测试并调整阈值扩展功能添加多目标识别实现简单的目标跟踪算法增加形状识别功能在最近的一个颜色分拣机器人项目中这套方案成功实现了对红色工件的识别和定位定位精度达到±2mm完全满足项目需求。实际测试中发现环境光的变化会影响颜色识别效果后来我们增加了自动白平衡和动态阈值调整功能显著提高了系统的鲁棒性。