STM32F103+OV7670车牌识别:内存受限下的嵌入式图像处理实战
简介面向嵌入式方向毕业设计的一套车牌识别系统方案采用 STM32F103 微控制器与 OV7670 摄像头模块完整实现车辆图像采集、车牌定位与字符识别。系统覆盖图像预处理、字符分割、特征提取和模式识别等核心步骤并针对渝、辽、沪、浙、苏、粤六个省份的车牌样式进行过专项适配以提高真实场景下的识别准确率同时提供 AD 格式电路图展示如何将 OV7670 输出的模拟信号经 ADC 转换后送入主控处理对理解硬件接口设计很有帮助。压缩包为 7z 格式大小约 16.54MB文件总数暂无数据但结合项目描述可判断内含 STM32 工程源码、接口电路图以及设计说明文档能够支撑从硬件搭建、外设配置到算法调试的完整毕设流程。目前已有 2193 人学习下载适合电子、自动化、计算机等专业学生直接参考或在此基础上进行识别算法优化与功能扩展。 每年毕设季都会看到一批“基于STM32F103OV7670车牌识别系统”的题目冒出来。我去年拿到的正是一份这样的工程包压缩包解出来是几十个源文件加一堆寄存器配置表第一次编译就报了几十个错误调了两周才把图像稳定显示出来。做完之后回头看这个题的含金量并不在于“车牌识别”四个字而在于它把传感器驱动、外设时序、内存管理和嵌入式图像算法全部缝合在一个72MHz的Cortex-M3芯片上每一步都是在跟资源限制较劲。这篇文章我会按自己踩坑的顺序拆解整个系统先讲清楚F103到底适不适合做这件事再讲OV7670图像怎么进内存车牌定位分割识别的算法在MCU上怎么做最后是调试经验和答辩演示注意事项。适合刚拿到类似工程包找不到头绪的人参考也适合想自己从零搭一套的人做选型依据。1. 破题F103做车牌识别第一道坎不是算法而是内存1.1 一个容易被忽略的事实F103没有DCMI摄像头接口很多初学者拿到OV7670模块后的第一个坑就是误以为STM32F103自带摄像头接口。实际上F103系列压根没有DCMI外设这个数字摄像头接口是F4/F7系列才有的。没有DCMI意味着OV7670输出的PCLK像素时钟、VSYNC帧同步、HREF行同步信号都无法用硬件自动接收。有人会想那我用外部中断去跟PCLK一位一位读总行吧实测不行。OV7670在24MHz输入时钟下PCLK最高可以到24MHz中断服务程序根本跟不上这个速度一条GPIO读指令还没执行完下一拍像素就来了。所以市面上的F103OV7670方案基本都会选带FIFO的摄像头模块。模块上多一颗AL422B视频FIFO芯片OV7670负责把像素写入FIFOF103在VSYNC结束后像读外部SRAM一样把整帧数据读走完全绕开PCLK速度问题。这是整个硬件设计的前提理解这一点再看代码就不会懵。1.2 整帧图像放不进SRAM必须压缩任务F103的SRAM只有64KBZET6甚至48KBRCT6而一块QVGA分辨率的RGB565图像是320×240×2153600字节连最小号的F103都装不下一整帧。所以无论代码怎么优化都不能抱有“把整帧读进来再慢慢处理”的幻想。实际可行的思路是读FIFO的时候直接抽点降采样。假设算法只需要160×120的灰度图读进来的数据量就是19200字节后续处理也在同一块缓冲区上做SRAM就吃得下了。这个“读的同时顺便降采样”的思路一定要先想清楚后面的代码结构都建立在这个基础上。有同学问我能不能用DMA加FSMC去搬运DMA确实能搬但它在搬运过程中没法跳点搬完还得再做一次压缩内存压力反而更大。CPU循环读看似原始实际在72MHz下读160×120个像素并没有那么慢还能顺手完成灰度转换和特征提取是更实用的选择。2. 硬件链路搭建最小系统、摄像头模块与FSMC读FIFO2.1 从最小系统到开发板ZET6还是RCT6STM32F103ZET6和RCT6都能做这个项目但优先选ZET6。原因是ZET6带FSMC接口可以很方便地把AL422B映射成外部SRAM直接读RCT6没有FSMC读FIFO要靠GPIO模拟时序代码复杂速度也差不少。如果手里只有RCT6的板子也不是不能做但例程尽量找带FSMC的版本。供电问题直接影响稳定性。OV7670模块一般自带LDO对3.3V供电要求不苛刻但整个系统如果靠USB口供电插上摄像头和LCD之后电流可能到300mA以上USB口供电质量差就会出现图像纹波。我实测最稳的方式是用5V适配器从开发板DC座供电或者给OV7670模块单独用一路LDO供3.3V。ST-LINK下载器的供电只适合调试不适合长时间跑识别。这里顺带提醒一下GPIO电平STM32F103并不是所有引脚都耐受5V只有标注FT的引脚才能承受5V。OV7670模块的IO大多是3.3V正常接没问题别因为看到网上讨论“GPIO能不能承受5V”就给模块供5V逻辑电平超压大概率烧摄像头。2.2 带FIFO和不带FIFO的OV7670模块怎么选这块不用纠结直接买带AL422B的。两种模块的差别对比项带FIFO模块不带FIFO模块数据读取方式MCU像读内存一样读FIFO需要跟随PCLK逐像素采集F103适配难度低驱动简单高F103无DCMI几乎不可行典型代表带AL422B的OV7670模块裸摄像头小板价格略贵便宜带FIFO的模块通常引出SIO_C、SIO_D、VSYNC、RESET以及8位或16位数据线。建议接线方式SIO_C和SIO_D接GPIO模拟SCCBVSYNC接一个外部中断引脚数据线接FSMC的数据总线读地址通过FSMC片选映射到Bank1的NE1区域。这样F103在VSYNC到来时清FIFO写指针等VSYNC结束后从固定地址连续读取就能拿到整帧数据。3. OV7670寄存器配置与图像采集链路3.1 SCCB配置先把输出格式固定好OV7670用SCCB总线配置寄存器时序和I2C很像MCU侧用GPIO模拟即可。OV7670的写地址是0x42读地址是0x43。初始化代码本质上就是把一串寄存器配置表写进摄像头关键点有三个COM70x12要设置为RGB565输出同时开启QVGA或QQVGA分辨率。COM150x40要配置成RGB565格式RGB还是BGR的排列方式要根据后续算法确认。自动曝光和自动白平衡必须关掉。很多花屏和颜色漂移问题并不是硬件坏而是这两个自动功能在场景变亮变暗时自行调整导致后续二值化阈值失效、车牌区域被淹没。这块没有捷径寄存器值最好先照着一个能正常出图的例程来。自己一点点试寄存器表很容易陷入“图像全黑全绿”的泥潭我之前就因为一个曝光寄存器没设对折腾了整整一个晚上。3.2 AL422B的读时序与CPU读取方式AL422B的工作流程可以这样理解OV7670在VSYNC有效期间按PCLK把像素写入FIFOMCU在VSYNC结束后读取每读一次数据FIFO读指针自动加一。所以对MCU来说AL422B就是一个时钟驱动的串行内存。用FSMC读时把FIFO映射到外部SRAM地址连续读取同一个地址就会被FSMC当成连续读操作NE和RD信号不断翻转FIFO输出数据并自动递增。这个“同一个地址连续读”的写法是很多人卡住的地方注意不要给FIFO分配多个地址一个片选地址就够了。读取方式我建议用CPU循环读每次读16位像素按目标分辨率抽点存放同时完成灰度转换。例如目标160×120灰度图可以在水平和垂直方向每隔一个点取一次计算量完全能接受。DMA方式更适合整块搬运后再处理但DMA无法在搬运过程中跳点内存压力更大还要处理FIFO读指针和DMA完成中断的配合复杂度高很多。3.3 分辨率、帧率和内存预算推荐直接用QQVGA160×120作为算法输入。如果OV7670本身输出320×240读FIFO时配合抽点最终消费的数据量是固定的。帧率方面F103读一帧160×120并完成预处理实测大约需要几十毫秒到一百多毫秒加上后续定位识别整体在每秒1到2帧的水平做静态车辆识别足够动态抓拍基本不要指望。内存预算给一个参考160×120灰度图19200字节蓝色特征图用1bit存储约2400字节二值图直接复用灰度缓冲区再留几个临时数组总占用在30KB以内。RCT6的48KB SRAM也能跑但余量很小建议全程别开大优化否则一不留神就把栈撑爆。4. 车牌定位把蓝色块变成候选框4.1 蓝色特征提取比边缘检测更直接国内蓝底白字车牌最明显的特征是蓝色。与其费劲做Sobel边缘再加形态学不如直接在颜色上做文章。每读一个像素判断是否满足“B分量明显大于R和G且整体亮度适中”满足就标记为蓝色像素。因为输入已经是RGB565判断量极小。得到蓝色特征图后可以做连通域分析找出面积最大的蓝色连通域。这里有两种落地方式一种是标准的两遍扫描标记适合追求通用性一种是只做行列投影的简化版本效率更高。毕设演示场景下图像干净、车辆正对摄像头简化版就够用。4.2 行列投影简单高效的定位算法把蓝色特征图按行累加得到每行的蓝色像素数量。车牌区域会突出一块连续的高值区间其他蓝色物体也可能形成干扰但车牌通常更规整。取高值连续区间作为候选行范围然后在候选行范围内再按列累加找到列范围就能得到候选框。给候选框加约束条件能大幅提高可靠性宽高比在2.5到4.0之间面积不小于全图面积的某个比例不符合直接判定为定位失败。光照变化大时蓝色阈值可能要动态调整最简单的办法是按照蓝色分量的统计分布取一个百分位值。4.3 用边缘投影精修边界单纯靠蓝色框定位往往会把上下边框甚至车体的一部分也包括进来。这时候可以在候选框内部做灰度Sobel边缘检测车牌字符边缘密集、竖边缘和横边缘交替投影后集中在字符区域上下边缘就是字符行的上边界和下边界。这个精修步骤能在后续二值化、字符分割时省很多事。边缘投影时要注意去掉车牌的白色边框因为白边框和字符一样是高边缘区域。做法是先做水平投影找字符行范围再在字符行范围内做垂直投影把上下白边排除掉。5. 字符分割与识别模板匹配在MCU上的落地5.1 局部二值化大律法只用于车牌区域得到车牌候选框后需要把灰度图变成二值图。不建议全图固定阈值因为室内外光照差异大车牌区域的亮度和周围场景完全不同。正确做法是只在候选框内用大律法OTSU求阈值按这个阈值把像素分成黑白两类。白色像素对应字符和车牌边框蓝色底会变成黑色。OTSU在几十个灰度级上做统计计算量并不大。处理时用整数累加直方图避免浮点运算。每次识别前重新计算阈值能适应环境光的渐变。5.2 去边框、铆钉、间隔符二值化后第一件事是去掉四周边框。字符区域外有白色边框和螺丝这些如果不消除投影法会把边框当成一个大字符或者在边界处产生假投影峰值。处理办法对二值图做水平投影连续白色行集中在中间区域的就是字符行把上下杂散行裁掉再做垂直投影找到左右有效字符边界。第二件容易踩坑的是间隔符。国内车牌第二个和第三个字符之间有一个小圆点垂直投影时可能单独形成一个尖峰也可能和相邻字符黏在一起。按“宽度小于平均字符宽度三分之一的段直接丢弃”的原则处理基本能过滤掉。5.3 字符归一化与SAD模板匹配每个字符候选段裁出来后先按高度统一缩放到目标尺寸宽度等比例缩放尽量不要拉伸变形。缩放后的字符和模板库做SAD绝对差值和取最小差值对应的模板作为识别结果。模板库建议从标准车牌字体图中生成每个字符归一化到16×32或者24×48。31个省市简称汉字加24个字母加10个数字一共65个模板按24×48计算总大小约75KBFlash完全放得下。模板生成和匹配都用整数运算F103不擅长浮点SAD。5.4 识别结果的时间平滑单帧识别偶尔会跳字尤其汉字里“京”和“津”、“鲁”和“晋”这种轮廓接近的。我后来加了一个简单的时间平滑连续5帧识别结果中对每一位字符做多数投票只在投票结果明显占优时才更新显示。这个改进对答辩演示特别有用静止车辆停在那里识别结果不会频繁跳动观感好了很多。6. 跑通工程和调试点从花屏到识别率上不去6.1 拿到工程包后先做的三件事第一把工程在Keil里编译先把编译器版本和Include路径的报错清掉。第二确认接线和代码里的引脚定义一致很多工程包换了一块板子之后引脚就不对。第三把所有外设的初始化顺序理一遍先LCD、再摄像头、后算法任何一个顺序乱了就会出现“屏幕亮但没图像”或者“识别永远失败”。我个人的习惯是先用一个最简单的功能测试上电只初始化LCD和OV7670把摄像头图像直接显示在LCD上确认颜色正常、没有花屏再开始写算法。这能帮你把“图像采集链路”和“算法逻辑”两个问题域隔离开排查问题时不会两头猜。6.2 图像花屏、颜色不对的排查顺序花屏通常不是算法问题而是硬件链路问题。按这个顺序排查供电是否稳定USB供电时图像有没有水波纹有就换外部供电。OV7670寄存器配置是否成功读回COM7确认是RGB565模式。数据位顺序FSMC 16位读FIFO时高低字节是否反了RGB565和BGR565排列是否反了。帧同步时序VSYNC中断是否真的来了FIFO写指针有没有在帧开始时清零。颜色偏绿或偏紫九成是RGB通道顺序问题换个排列就好。图像看起来有拖影通常是对FIFO写指针清零的时机不对必须在帧开始前清零否则读到的是上一帧和下一帧混在一起的数据。6.3 识别率上不去的真实原因如果定位准了、分割也能切出7个框但识别结果不对问题多半在模板。车牌字符字体有行业标准但不同批次生产的车牌字体细节有差异模板最好从实拍的高清车牌中截取而不是从网图里随手扣。另一个常见原因是字符归一化时宽高比被破坏导致“京”和“津”这类字形混淆。光照是最大的不稳定因素。建议答辩演示时固定室内灯光或者用一个小型补光灯从侧上方打光。摄像头一定要正对车牌俯仰角太大时车牌在成像中有明显透视变形后面的定位和分割都会跟着崩。6.4 从毕设到可扩展的方向这套系统的架构其实可以往下延伸不少。想上实时操作系统的可以把采集、识别、显示拆成三个FreeRTOS任务F103跑FreeRTOS没有压力但要注意临界区保护FIFO读取和显示缓冲区。想做道闸联动的可以用TIM2的PWM输出驱动舵机占空比更新用DMAPA1和PA3正好是TIM2的通道。想记录历史数据的识别结果可以用内部Flash模拟EEPROM掉电保存最近几十组车牌。低功耗场景下识别完一帧后可以进入停机模式靠外部中断唤醒再采下一帧。这些方向都能作为毕设亮点写进论文。最后提醒一句毕设演示一定要准备一个固定脚本灯光、距离、车牌朝向全部固定。图像类项目最怕现场环境变化同一套代码在调试台上识别率90%到了答辩教室打光一变可能只剩50%。先把最稳定的主场景调试到完美再考虑适应不同环境这是我做完这个项目最深刻的体会。本文还有配套的精品资源点击获取