ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

基于FPGA的车牌识别系统设计与硬件流水线实现

基于FPGA的车牌识别系统设计与硬件流水线实现

那天晚上我把显示器的VGA线接到开发板的输出口,CMOS摄像头对着窗外,串口终端里开始一个字符一个字符地吐信号:先是“定位成功”,接着是分割出的字符序号,最后整行打印出识别出的车牌。那一瞬间,三个星期里写的所有Verilog、改过的几十版约束文件、烧录时看过的十几条时序报告,都变得具体了。

这篇文章记录的,就是“基于FPGA的车牌识别系统”从算法拆解到bit流烧录的完整实验过程。系统不依赖软核,不跑操作系统,纯粹用逻辑资源把灰度化、滤波、二值化、Sobel边缘检测、投影定位、字符分割、模板匹配这些常规图像处理算法串成一条硬件流水线,最终把结果通过UART送出来。内容适合正在学FPGA、想做图像处理方向课程设计或毕设的同学,也适合想了解“软件思路是怎么翻译成硬件逻辑”的自学者。我会把每个模块的设计理由、实际参数、上板调试中踩过的坑都写清楚,尽量让读者能照着复现。

1. 为什么我最终选择纯FPGA而不是直接跑OpenCV

1.1 CPU方案的瓶颈在哪里

车牌识别最直接的实现方式,是OpenCV加一台电脑或者树莓派:读视频帧、转灰度、高斯模糊、Canny找边缘、找轮廓、OCR。这条路成熟、资料多、调试方便,随便一搜都是现成代码。但如果是把摄像头直接接在FPGA开发板上做一套独立的硬件系统,情况就完全不一样。

CPU方案的核心瓶颈是“数据搬运”。每帧图像从摄像头进入内存需要拷贝,算法在CPU上逐级串行执行,一张1080p图像的光流、滤波、边缘检测动辄几十毫秒,算上识别可能要上百毫秒。这不代表CPU慢,而是通用处理器本质上是一条指令处理一份数据,面对每帧上百万像素的图像,只能一条一条来。

FPGA的方案则是反过来的:它不“读”数据,而是让数据流经过逻辑电路。像素时钟打一拍,灰度化单元处理一个像素;下一个时钟,同一个像素进入滤波窗口;再下一个时钟,它已经在Sobel卷积核里了。每个算法阶段都是一个独立的硬件模块,模块之间用行缓存和FIFO衔接,数据以流水线方式推进,延迟是微秒级的。

1.2 FPGA方案的真实收益和代价

我一开始也没打算纯逻辑实现,考虑过Zynq软硬协同:ARM核上跑Linux,OpenCV处理图像,FPGA部分只做采集加速。这个方案开发效率高,但它有一个问题:它仍然是“软件方案”,瓶颈还是在ARM核的串行执行上,而且工程复杂,还要配内核、配驱动。

纯FPGA方案的收益,主要有三块:

  • 延迟可控且极低。从摄像头输入到识别结果输出,整个链路是像素级流水线,不经过内存搬移。
  • 功耗低。纯逻辑满负荷跑,整个图像处理链路的功耗远低于开发板加电脑的组合。
  • 不依赖操作系统。上电配置bit流后立即工作,没有Linux启动、驱动加载这些环节,特别适合作为独立嵌入式设备。

代价也很明显:开发周期长,调试手段有限,算法复杂度天花板低。深度学习车牌识别模型在FPGA上也能跑,但那是另一个量级的工程,不是“常规图像处理算法”能做到的。

1.3 方案选择的判断标准

我当时的判断标准有四个:第一,场景是否固定(摄像头位置固定、车牌大小范围固定,这对后续定位算法极其重要);第二,是否要求实时性(流水线天然满足);第三,是否允许牺牲部分准确率(纯逻辑模板匹配的准确率不如OCR引擎,但能接受);第四,手头板子的资源是否够用。把这四项理清楚,答案就很明确:在这个固定场景、强调实时性的项目里,纯FPGA路线是最合适的。

那组方案对比做过之后,我基本确定,如果只是想做实验验证算法,CPU方案当然友好;但如果想真正理解图像处理算法在硬件上长什么样,纯FPGA是绕不开的必修课。

2. 系统整体框架:从CMOS像素到串口字符的完整链路

2.1 硬件平台与输入输出规划

我用的是一块入门级FPGA开发板,主芯片是某厂商7系列的中低端型号,板上自带CMOS摄像头接口、VGA接口、UART转USB接口、按键和LED。这套硬件做车牌识别实验足够了,关键是整条链路都集成在同一块板上,不用飞线。

图像输入用的是一颗常见500万像素CMOS传感器模块,输出RGB565格式,分辨率我配置为640×480@60,这个分辨率对后面的定位算法足够,像素时钟大概在25MHz上下,逻辑资源压力也小。识别结果通过板载UART以115200波特率输出到PC终端,同时把处理后的二值图叠加在VGA输出上,方便直观观察每个处理阶段的中间结果。

在规划系统架构时,我给自己定了一个原则:每一级算法的输入输出都必须是标准视频流信号——数据、行有效、场有效这三个信号一起传递。这样每个模块都可以独立仿真、独立替换,调试时也能随时把任意中间级引出到VGA观察。

2.2 时钟域划分与数据流设计

整条链路涉及三个时钟域:CMOS传感器的像素时钟域、FPGA内部系统逻辑时钟域、VGA显示时钟域。像素时钟随分辨率变化,系统逻辑时钟由PLL产生,VGA时钟也是PLL单独输出。三个时钟域之间不能直接互连,否则亚稳态问题会在上板后爆发。

最核心的衔接点是CMOS像素时钟域与后续算法逻辑的桥接。CMOS输出的每个像素带有同步信号,进入FPGA后先经过异步FIFO做缓冲,把数据从像素时钟域平滑切换到系统时钟域。我用的FIFO位宽是16位RGB565,深度2048,足够缓冲一行数据。

整体数据流可以这样概括:CMOS输出的RGB565先做格式转换和灰度化,得到8位灰度值,再经过行缓存模块形成3×3窗口,进入中值滤波和Sobel边缘检测;边缘图经过二值化后,得到一张640×480的1位边缘图;定位模块在这张图里做水平和垂直方向的投影统计,圈出候选车牌区域;裁剪出的区域经过字符分割和归一化,变成16×8的字符点阵,送到模板匹配引擎;匹配结果由UART发送模块按ASCII码逐字符输出。

2.3 调试架构:中间结果可视化

纯逻辑设计最大的痛苦是看不见内部信号。我的解决办法是加了一条“调试旁路”:VGA显示模块可以从寄存器配置中选择显示源,包括原始灰度图、中值滤波后的图、Sobel边缘图、二值图、定位框叠加图。这样在调某个模块时,直接切换到对应画面,马上能看出算法参数是否合理。

同时,在关键模块内部预留了ILA调试核的观测点,用来抓取行缓存输出、投影积分数组、模板匹配的最小距离等信号。这套“可视化调试板上链路”帮我把很多问题收敛在半小时内,如果没有它,纯靠串口打印,排错效率会低很多。

3. 图像预处理链路的RTL实现细节:灰度、滤波、二值化、Sobel

3.1 灰度化:浮点系数转定点移位

灰度转换的教科书公式是Y = 0.299R + 0.587G + 0.114B。在FPGA里直接写小数乘法不是不行,但浪费DSP资源。常规做法是把系数放大成整数再移位截断:Y = (R×77 + G×150 + B×29) >> 8。这个公式的误差非常小,硬件上只需要两个加法器和三个乘法器,对DSP切片和LUT的占用都很低。

CMOS输出的RGB565不是每通道8位,而是R占高5位、G占6位、B占5位。我在做灰度转换前先把RGB565扩展成RGB888:R = {R5, R5, R5[2:0]},G = {G6, G6[5:4]},B = {B5, B5, B5[2:0]}。这样保证了后续灰度值的动态范围接近标准8位。

一个容易忽略的细节是无效像素的处理。CMOS模块输出数据在帧消隐期间是无意义的,如果灰度模块只是按像素时钟机械地计算,消隐期的数据会被当作有效像素,污染下游的行缓存。我要求每个模块都要随数据携带de(data enable)信号,计算完成后只在de有效的时钟沿输出结果。

3.2 3×3中值滤波的滑动窗口实现

中值滤波的作用是去除图像里的椒盐噪声,对车牌字符边缘的保持效果比均值滤波好得多。FPGA实现3×3窗口的标准套路是“两行缓存加九个寄存器”。

行缓存我用了FPGA内部的移位寄存器原语,每个原语可配置为1位×深度或M位×深度。这里每行需要缓存640个像素、8位灰度,我配置了两条深度640的8位移位链。数据流进来时,第一行缓存的输出进第二行缓存,同时三个行位置各抽出一组8位数据,与当前像素组合成3×3矩阵。

中值计算用的是排序网络。9个像素排中值,最简单的做法是三个3输入排序器先分别排序,再取中间结果。我一开始写得比较粗糙,每个排序器串行比较了三次,结果一个像素要6个时钟才算完,导致流水线出了气泡。后来改成比较器树:第1级3个3输入排序器,第2级用逻辑选出跨组的中间值,这样中值滤波在一个时钟内就能出结果。

关于边界像素,我的选择是补零而不是复制边缘像素。图像边缘的噪声对车牌识别影响很小,补零逻辑更简单,一行开始和结束时窗口内的无效位置自动清零,不会多占用额外寄存器。

3.3 Sobel边缘检测与梯度幅值近似

Sobel算子对后续车牌定位非常关键。车牌的边缘特征明显:字符区域在水平和垂直方向都有密集的边缘响应,而普通车身表面相对平坦。

标准的Sobel有两个3×3卷积核:

Gx = [-1 0 1; -2 0 2; -1 0 1] Gy = [-1 -2 -1; 0 0 0; 1 2 1]

在Verilog里,我先把3×3窗口的每一行分别加权,再做列方向累加。这样卷积运算被拆成三级加法树,关键路径比直接写一个大表达式短很多。梯度幅值的严格计算是sqrt(Gx² + Gy²),在FPGA里开根号要用Cordic IP核,有点麻烦。实测用 |Gx| + |Gy| 近似,定位效果差别很小,我就采用了后者。

梯度计算完成后,还需要一个阈值来生成边缘二值图。阈值太大会把车牌字符的笔画也滤掉,太小又会让车身纹理和地面噪点全进来。调试阶段我把阈值做成一个可写寄存器,通过上位机串口命令在线修改,找到了一个对固定场景比较稳的数值。

3.4 二值化阈值:固定阈值还是动态阈值

项目一开始我用的是固定阈值,室内灯光下效果不错。后来把设备搬到走廊窗边,下午阳光斜射进镜头,同一套固定阈值就开始拉胯:强光下的白车牌反光区域被误判成背景,字符断裂。这时候才意识到需要动态阈值。

动态阈值最经典的是大津法(Otsu)。在FPGA里完整实现大津法的主要开销在灰度直方图统计:需要256个bin的累加器,每个bin至少16位,总计4Kbit的存储。我的做法是用一块BRAM做直方图存储,地址就是灰度值0~255,像素每个有效沿做一次“读旧值加1再写回”。这个操作必须在一个时钟内完成,所以BRAM要配置为读优先模式,否则会有一个周期的读写冲突。

一帧图像的直方图统计完成后,在帧消隐期间需要用状态机遍历256个bin,计算每个可能阈值的类间方差,选出最大灰度值作为下一帧的二值化阈值。我把这个计算结果锁存到寄存器,下一帧所有像素使用该阈值做比较。

实际测试中,动态阈值明显提升了室外场景的稳定性,代价是帧率会掉一帧(因为统计和计算占用了一个消隐期)。对于车牌识别这种场景,这个代价完全可以接受。

// 大津法简化思路的伪代码 // 在帧消隐期间执行 max_val = 0 best_th = 0 for t = 0 to 255 w0 = sum(hist[0..t-1]) w1 = total - w0 if w0 == 0 or w1 == 0: continue mu0 = sum(i*hist[i] for i in 0..t-1) / w0 mu1 = sum(i*hist[i] for i in t..255) / w1 sigma = w0 * w1 * (mu0 - mu1)^2 if sigma > max_val: max_val = sigma best_th = t

这个伪代码里的除法在硬件上比较费资源,我实际做的时候把均值计算换成了累加器的增量式维护,除法只在最后取整除一次,把实现成本降了很多。

4. 车牌定位、字符分割与模板匹配引擎

4.1 基于投影法的车牌定位

车牌定位是整个系统里最容易翻车的环节。我方向和垂直方向,统计每个方向上边缘像素的数量分布,然后找连续高密度区间。这个思路实现简单,对“固定位置、固定光照”的场景足够稳健。

具体实现分两步:

  • 第一步,水平投影找候选行区间。把二值边缘图逐行累加边缘像素计数,若连续N行的计数都超过阈值,就把这些行标记为候选带。车牌字符本身有密集的横向边缘,这个特征不会被车身光滑面干扰。阈值N我设为5行,阈值计数取图像宽度的1/8。
  • 第二步,在候选带内做垂直投影。统计每列边缘像素数,找到连续的高密度列区间,再用宽高比(常规车牌宽高比约3.14:1)过滤明显不合理的区域。

这两个步骤在FPGA里都是逐像素流式处理,核心资源是两组长度为640的整数累加数组。一个细节是,边缘图是逐行串行输入的,水平投影可以直接在线累加;但垂直投影要等整行统计完,所以垂直方向需要一块行缓冲。我用了双缓冲(Ping-Pong)结构:一个缓冲在统计当前行,另一个缓冲在上一行的数据中找峰值区间,两个角色每帧交替。

定位框的最终坐标被锁存到一组寄存器里,供字符分割模块使用。这里有个实用技巧:如果当前帧定位失败,不要清空旧坐标,继续沿用上一帧的定位框,下一帧再刷新。这样画面抖动不至于导致识别结果频繁丢失,我实测这个“保持策略”对稳定性提升很明显。

4.2 字符分割与归一化

车牌定位框拿到之后,下一步是把车牌上的字符分割成单个字符。常规车牌通常是1个汉字、1个字母、5个字母数字的组合,字符之间有比较规律的间隙。

字符分割的实现基础是垂直投影。对定位框内的二值图做逐列累加,字符笔画所在的列累加值高,字符间隙的列累加值趋近于零。根据这个投影曲线,状态机找出每一段“从非零跳到零再跳回非零”的区间,就是候选字符。

分割过程中有两个常见坑:

  • 车牌的边框和上下边缘会有横线,垂直投影时容易把边框误判成一个高密度字符。我的对策是对每列投影做形态学开运算:先腐蚀掉单列的窄峰,再做膨胀恢复宽度。这样孤立边框线的窄响应被消除,真正字符的宽响应会保留。
  • 汉字内部结构和部分笔画断开,可能导致一个汉字被切分成多块。解决办法是限制最小字符宽度,如果某个区间宽度小于正常字宽的0.4倍,就判定为断裂,直接合并到相邻区间。

分割出的每个字符区域,我归一化成16×8的点阵:水平方向均匀抽取8列,垂直方向均匀抽取16行。这一步的作用是消除字符大小差异,让模板匹配的输入数据维度固定。归一化后的点阵存入一个16×8的寄存器数组,同时生成valid信号,表示一个完整的字符数据已经准备好。

4.3 模板匹配识别引擎

模板匹配的目标是把归一化后的字符点阵与预存的字符模板做逐像素比较,找到差异最小的一项作为识别结果。模板库我用了一个上位机脚本把标准车牌字体渲染成16×8点阵,再转成COE文件,由FPGA的ROM初始化。库里包含数字0~9、大写字母(除去与数字容易混淆的I和O)、以及常见汉字,一共几十个模板。

匹配核心单元是“异或归约”:把待识别字符的16×8点阵与模板ROM输出的点阵逐位异或,为1的位表示该位置不一致,累加后得到差异数。所有模板在硬件上是并行匹配的,每个模板对应一个累加器,一个字符的匹配结果在一行时间内就能全部算完。

选出最小差异数之后,我不能直接把差异最小的模板当作结果,而是加了一个拒绝门限:如果最小差异数仍然超过总点阵面积的1/4,说明当前字符辨识度很低,大概率是噪声或分割错误,这时输出一个“?”而不是强行给结果。这个门限很关键,因为分割模块偶尔会把背景噪声当成字符送进来,没有拒绝门限的话,系统会把垃圾也识别成一个莫名其妙的字符。

module match_core( input wire clk, input wire rst_n, input wire [127:0] char_data, // 16x8点阵 input wire tmpl_valid, output wire [7:0] min_dist, output wire [4:0] match_index ); // 每个模板一个比较单元,并行计算异或累加 // 选择最小距离的模板索引作为识别结果

这段代码只是一个示意。实际工程里我把它拆成了ROM读端口、异或累加阵列、最小值选择器三个子模块,这样综合后的关键路径更好收敛。模板匹配的完整周期在一个水平扫描行内完成,换算下来延迟大约几十微秒,对整体性能没有任何压力。

5. bit流烧录实验记录:综合、约束与第一次上板

5.1 从RTL仿真到bit流生成的固定流程

工程做到这一步,RTL代码已经全部写完,我以为剩下的事情就是把bit流烧进去就行。事实证明,从仿真通过到上板正常工作,中间隔着一整个“物理问题”的坑。

先说一下我固定的流程。每个功能模块先在仿真环境里验证:灰度化模块给一组RGB数据,检查输出Y值;Sobel模块给已知渐变图,检查梯度输出;定位模块用一段模拟的边缘图序列,检查候选框坐标。都通过之后,才是集成仿真:把串行像素流接起来,跑一整帧数据,在仿真波形里看最终识别结果。

仿真通过后,进入综合与实现阶段。在综合前,我把所有顶层端口都锁定到物理引脚:CMOS的像素时钟、行场同步、数据线,VGA的RGB和同步信号,UART的TX/RX,还有系统时钟和复位按键。引脚约束我直接照开发板的原理图填写,这里最需要注意的是IO电平标准,CMOS摄像头模块用的是3.3V或2.5V接口,VGA通常要求3.3V,写错电平标准轻则信号不正常,重则可能让IO处于过压状态。

实现完成后,我第一眼看的是资源报告,第二眼一定是时序报告。资源报告过多会提醒我优化逻辑,时序报告如果出现setup违例,就必须回头改代码而不能直接烧录。时序报告里最关心的参数是最差路径的WNS(Worst Negative Slack),负数就意味着上电后可能会不定期出现信号采样错误。

5.2 约束文件里最容易埋雷的位置

约束文件(XDC)是上板前最容易出错的环节。我总结了自己遇到的几个典型问题:

  • 时钟约束必须写在综合前还是实现前?我的经验是越早越好,最好在综合前就把主时钟、像素时钟、VGA时钟全部定义好,否则综合器不会为不同时钟域做优化。
  • PLL的locked信号务必连到复位逻辑。第一次我没接,导致像素FIFO的复位信号在PLL还没锁定时就被释放,上板后第一帧图像是一片乱码。
  • 多个时钟域之间的路径要么加set_false_path要么用异步FIFO,不能裸奔。我的CMOS输入是外部时钟,和内部100MHz系统时钟完全不同源,如果不声明异步,时序分析器会在这条路径上报出大量违例,干扰真正重要的问题定位。

5.3 第一次烧录失败:从一片黑屏到定位成功

第一次烧录bit流后的现象是:VGA输出有画面但全黑,串口没有任何输出,CMOS摄像头的LED亮了说明供电正常。我先把系统分成三个独立故障域:图像采集链路、图像处理链路、串口输出链路,然后逐个排查。

第一步查PLL锁定信号。板上的锁定LED常亮,说明时钟没问题。第二步查CMOS是否有数据。用ILA抓像素时钟和行场信号,发现行场信号确实在翻转,但像素数据一直是0。这说明CMOS在输出“空数据”,大概率是寄存器配置没生效。于是我用ILA抓SCCB(类I2C)接口波形,逐位分析配置帧,发现起始条件的时序不符合传感器文档要求,SCCB总线的起始和停止条件有一个时序门槛,我的FPGA状态机写得太快,导致传感器没能识别。把起始条件的时钟等待从2个周期延长到5个周期后,CMOS开始输出有效像素。

第二步的坑在二值化模块。VGA画面从全黑变成了正常的灰度图,但二值图彻底白屏,边缘图也是一团黑。这个问题只花了半小时定位:Sobel模块的输入数据有效信号没对齐,我用了行缓存输出打拍后的灰度数据,但有效信号还是原始de,结果卷积核在一行前已经切换,边缘位置全错了。修复方法很简单,让de经过和像素数据完全相同的延迟链,保证对齐。

5.4 时序违例的定位与收敛

图像链路跑通后,我把系统时钟从50MHz提回100MHz,时序报告立刻出现了setup违例,最差WNS约-1.1ns。查看最差路径详情,起点在Sobel模块的加法树,终点在二值化比较器,中间经过了一条很长的组合逻辑链。这是因为我把Gx和Gy的卷积运算写成了一个超长表达式,综合器虽然尽量优化,但三级乘法累加仍然在100MHz下超了一点点。

我没有直接降频,而是做了两处优化:

  • 将Sobel卷积表达式拆成两级流水:第1级算每行加权和,第2级竖向累加,中间插入触发器。
  • 把梯度幅值计算中的乘法器改用移位和加法实现,释放了部分DSP资源,也缩短了路径。

优化后WNS变成+0.4ns,留了一点余量。这个余量在上板测试中证明了它的价值:连续运行两个小时,随机抓取多帧都没有出现像素错位。如果当时只是降频草草收尾,问题并不会真正消失,高温或外部干扰下仍然会偶发故障。

6. 实测数据、资源占用与后续优化空间

6.1 不同场景下的识别效果

整个系统在固定场景下取得了让我满意的效果。我把摄像头放在一个模拟停车场的环境下,对同一批测试车牌做了几轮统计,结果如下表所示。

测试场景定位成功率字符识别成功率单帧处理延迟
室内均匀光照96%93%约5ms
室外白天顺光91%89%约5ms
室外逆光/强反光72%63%约5ms
夜间无补光58%41%约5ms

单帧处理延迟是流水线从像素输入到匹配结果输出的纯逻辑延迟,不包含显示刷新等待。这个延迟比CPU方案低了一个数量级以上,也是FPGA方案最直观的优势。

准确率数据说明两个问题:一是在可控光照场景下,固定模板匹配方案完全够用;二是一旦光照模型超出预期,常规图像处理算法的天花板就出现了。逆光场景的定位失败主要是因为车牌区域整体过曝,边缘梯度被高光淹没;夜间失败则是因为字符对比度太低,二值化阈值计算失效。

6.2 资源与时钟频率汇总

整个工程在7系列FPGA上综合实现后的资源占用情况如下:

资源类型使用量占比
LUT约18K32%
Flip Flop约11K19%
Block RAM18个26%
DSP Slice8个10%

最高可运行时钟频率约132MHz,我实际工作时钟为100MHz,留了充足余量。这个资源占用说明,即使是一颗中小型FPGA芯片也足够容纳整条车牌识别流水线,不需要依赖高端器件。整个设计没有使用软核,也没有外挂DDR内存,关键是合理利用了Block RAM做行缓存和直方图存储。

6.3 如果再让我做一次,这些地方一定会调整

第一,摄像头分辨率我会选到800×600或1024×768,而不是640×480。更高分辨率意味着更高像素时钟,但投影定位的分辨率越好,对远处小车牌识别的容忍度越高。现在的640×480在摄像头距离车牌10米以上时,字符宽度经常不足8像素,匹配准确率明显下降。

第二,模板匹配的字符库必须覆盖不同字体和常见磨损状态。第一版库里只有标准样式字体,实际测试时遇到笔画粘连或轻微形变的车牌,拒识率很高。后来我增加了两个变体字体模板,错检率下降了不少。

第三,分割模块可以做得更聪明。当前的分割逻辑是基于垂直投影的,对轻度倾斜的车牌还能硬扛,但倾斜角度超过10度后,字符投影重叠,分割错误很难通过参数调整解决。更稳的方案是先对定位框做一次透视矫正,把四边形矫正为矩形,再做投影分割,这部分逻辑在当前工程里没有做。

这个内容还能往哪个方向扩展

如果继续改进这套系统,最直接的路线是增加颜色辅助定位。经典车牌颜色中,蓝底白字占绝大多数,而车身颜色多集中在灰、黑、白、红、银等,在HSV色域下蓝色像素的分布特征非常明显。FPGA里做HSV转换比RGB复杂一些,但也不是不能做。定位模块先检测蓝色像素密集区域来锁定候选框,再用边缘投影进一步验证,这个组合能显著降低复杂背景下误定位的概率。

另一个方向是字符识别的升级。模板匹配在工程上简单,但对形变、遮挡、光照变化鲁棒性差。可以考虑用更简单的MLP(多层感知机)来替代模板库:训练好的权重量化后存成ROM,像素点阵作为输入,逐层做乘加和激活。FPGA上的DSP资源够用,延迟也不会增加太多。这条路比直接上卷积网络要可控得多,识别准确率却会比纯模板匹配上一个台阶。

我个人在实际操作中的体会是:这个项目最大的价值不是“识别出车牌”这件事本身,而是强迫我完成了一次“软件思维到硬件思维”的转换。每一个看似简单的算法步骤,在RTL里都要考虑数据有效信号、时钟域、资源占用、时序收敛。如果你也想做类似的方向,我建议从VGA显示中间结果开始,先把一条数据通路完全打通,再逐步在通路上添加算法模块。这样每次新增逻辑都能立刻从画面上验证效果,而不是写完整个系统才后悔某个初级环节出了问题。

RELATED READING

延伸阅读

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