1. 为什么透视形变是车牌识别的“硬骨头”
1.1 从一张“拍歪了”的车牌说起
先还原一个真实场景。你在停车场出口装了一台相机,角度不可能永远正对车牌。车子斜着开过来,车牌在画面里就是一个被压扁的平行四边形;再碰上广角镜头,边缘还会带一点桶形畸变。这时候你把图丢给一个只会在“正脸”车牌上工作的识别流程,字符分割直接崩掉,识别率能从95%掉到40%以下。
这就是透视形变(perspective distortion)的杀伤力。它和普通的旋转、缩放不是一回事。旋转是刚体变换,字符之间的相对比例不变;缩放是等比的,字符宽高比不变。而透视形变是非线性的投影变换,近大远小,车牌四条边不再平行,字符被“挤”成梯形,同一个字符的左边和右边高度都不一样。
我一开始做车牌识别的时候,天真地以为把图转正就行了。结果发现,用最小外接矩形去框车牌,框出来的矩形和真实车牌边界差得离谱,字符切出来全是斜的。后来才明白,透视形变必须用投影变换来矫正,而不是简单的仿射变换。仿射只能处理平行四边形,投影才能处理任意四边形。
1.2 透视形变矫正的核心思路
一句话概括:找到车牌在图像中的四个角点,然后把这四个角点映射到一个标准矩形上,用投影变换矩阵把整块车牌“拉平”。
这个思路听起来简单,但魔鬼在细节里。四个角点怎么找?找不准怎么办?映射的目标矩形尺寸怎么定?变换之后字符会不会被拉伸变形?这些问题每一个都会直接影响最终识别率。
我踩过的坑是:一开始用轮廓的最小外接矩形取四个顶点,结果车牌有圆角或者被遮挡时,顶点位置偏了好几个像素,矫正出来的车牌字符间距忽大忽小,分割阈值完全没法统一。后来改用轮廓逼近+四边形筛选的方式,才把角点精度稳定下来。
1.3 这篇笔记适合谁看
如果你正在做车牌识别、文档扫描矫正、或者任何需要把倾斜平面“拉正”的项目,这篇内容应该能帮你省下不少试错时间。我会从角点定位、投影变换矩阵计算、目标尺寸推导、插值方式选择这几个环节逐一拆解,每个环节都给出我实际跑通的参数和代码逻辑。不需要你有深度学习背景,但需要你了解Halcon的基本算子用法,比如threshold、connection、select_shape这些。
2. 角点定位:整个流程里最容易翻车的一步
2.1 为什么不能直接用最小外接矩形
Halcon里有个算子叫smallest_rectangle2,它能返回轮廓的最小外接矩形,包括中心、角度、半宽半高。很多人图省事,直接拿这个矩形的四个顶点当车牌角点。我试过,在车牌倾斜角度小于10度的时候勉强能用,一旦超过15度,误差就肉眼可见了。
原因在于:最小外接矩形是“面积最小”的矩形,不是“贴合车牌边界”的矩形。当车牌是梯形时,最小外接矩形会偏向短边那一侧,导致长边方向的角点向内缩,短边方向的角点向外扩。矫正之后,车牌一边被拉长,一边被压缩,字符宽度不一致,后续分割必挂。
正确的做法是:先提取车牌区域的轮廓,然后用多边形逼近得到近似四边形,再从四边形顶点里筛选出四个角点。
2.2 轮廓提取与四边形逼近的实操参数
假设你已经通过颜色分割或者边缘检测拿到了车牌区域的Region。接下来:
* 假设 PlateRegion 是已经提取到的车牌区域 connection(PlateRegion, ConnectedRegions) select_shape(ConnectedRegions, SelectedRegions, 'area', 'and', 5000, 999999) * 对区域做边界提取 gen_contour_region_xld(SelectedRegions, Contours, 'border') * 多边形逼近,关键参数是 Tolerance gen_polygons_xld(Contours, Polygons, 'ramer', 3.0)这里gen_polygons_xld的Tolerance参数我试过从1.0到10.0。3.0是一个比较稳的值:太小了会把车牌边缘的锯齿也当成顶点,逼近出来七八个点;太大了会把真正的角点合并掉,变成三角形。你可以根据图像分辨率微调,高分辨率图像可以适当放大到4.0或5.0。
逼近之后,用get_contour_xld拿到多边形的顶点坐标,然后筛选出恰好四个顶点的多边形。如果顶点数不是4,说明逼近参数不对,或者车牌区域本身不完整,需要回到分割步骤调整。
2.3 四个角点的排序逻辑
拿到四个顶点之后,顺序是乱的。你需要把它们排成左上、右上、右下、左下的顺序,才能和标准矩形的四个角一一对应。
我的排序逻辑是这样的:
- 先按y坐标排序,最小的两个是上边,最大的两个是下边。
- 在上边两个点里,x小的为左上,x大的为右上。
- 在下边两个点里,x小的为左下,x大的为右下。
这个逻辑在车牌旋转不超过45度时完全够用。如果旋转角度更大,比如车牌竖着出现在画面里,就需要先用smallest_rectangle2拿到角度,把图像旋转到水平附近再做排序。我一般会在预处理阶段加一个角度判断,超过30度就先粗旋转一次。
注意:角点排序错了,投影变换会把车牌镜像翻转,字符顺序全反,识别结果就是乱码。这个错误很隐蔽,因为矫正后的图像看起来“挺正常”,只是左右反了。
2.4 角点精度对最终识别率的影响实测
我做过一组对比实验,在同一批200张透视形变车牌图上:
| 角点定位方式 | 字符分割成功率 | 最终识别率 |
|---|---|---|
| 最小外接矩形顶点 | 72% | 58% |
| 多边形逼近+排序 | 94% | 89% |
| 手动标注角点(上限) | 98% | 95% |
可以看到,角点定位方式直接决定了识别率的上限。多边形逼近已经非常接近手动标注的效果,而最小外接矩形差了30个百分点。这30个点就是透视形变矫正的价值所在。
3. 投影变换矩阵:把梯形“拉”成矩形
3.1 投影变换和仿射变换的本质区别
Halcon里有两个算子容易混淆:vector_to_rigid和vector_to_proj_hom_mat2d。前者是刚体变换,只能旋转和平移;后者是投影变换,能处理任意四边形到四边形的映射。
仿射变换有6个自由度,投影变换有8个自由度。多出来的两个自由度就是用来描述“近大远小”的。你可以这样理解:仿射变换保持平行线仍然平行,投影变换不保持。车牌在透视形变下,原来平行的上下边在图像里不再平行,所以必须用投影变换。
计算投影变换矩阵需要四对对应点。我们已经有了车牌在图像中的四个角点,还需要定义目标矩形的四个角点。目标矩形的宽高怎么定,下一节详细说。
3.2 目标矩形尺寸的推导方法
目标矩形的宽高不能随便设。设小了,字符被压缩,细节丢失;设大了,插值引入模糊,还浪费计算量。
我的做法是:用图像中车牌四边形的对边长度平均值作为目标宽高。
具体来说:
- 上边长 = 左上到右上的距离
- 下边长 = 左下到右下的距离
- 目标宽度 = (上边长 + 下边长) / 2
- 左边长 = 左上到左下的距离
- 右边长 = 右上到右下的距离
- 目标高度 = (左边长 + 右边长) / 2
这样算出来的尺寸,既不会过度拉伸,也不会过度压缩。我试过直接用长边或者短边,效果都不如取平均。因为透视形变下,长边被拉长、短边被压缩,取平均相当于做了一个“折中还原”。
另外,如果你知道车牌的标准物理尺寸(比如440mm×140mm),也可以按比例设定目标尺寸,比如宽440像素、高140像素。但实际项目中,相机分辨率不同,固定像素值不一定合适,还是按图像中的实际边长来算更稳。
3.3 投影变换矩阵的计算与图像矫正
有了四对对应点,计算矩阵就一行代码:
* ImagePoints 是图像中车牌四个角点,按左上、右上、右下、左下排列 * TargetPoints 是目标矩形四个角点,同样顺序 vector_to_proj_hom_mat2d(ImagePoints, TargetPoints, HomMat2D) * 对图像做投影变换 projective_trans_image(Image, TransImage, HomMat2D, 'bilinear', 'false', 'false')这里projective_trans_image的插值方式我选的是'bilinear'。也试过'nearest_neighbor',速度快但字符边缘锯齿严重,分割时容易断笔。'bicubic'效果更好但速度慢一倍,对识别率提升不到1个百分点,性价比不高。双线性插值是速度和质量的平衡点。
还有一个参数是AdaptImageSize,我一般设为'false',保持原图尺寸,避免变换后图像被裁剪或者补边。如果你需要固定输出尺寸,可以设为'true',但要注意目标矩形的宽高比要和原图一致,否则会拉伸。
3.4 变换后字符宽高比的验证
矫正完之后,一定要验证一下字符宽高比是否正常。中国车牌的标准字符宽高比大约是1:2(宽:高),矫正后如果字符变得又扁又宽,说明目标矩形宽度设大了;如果变得又瘦又高,说明目标矩形高度设大了。
我一般会在矫正后的车牌区域里,用threshold+connection粗略分割一下字符,统计字符区域的平均宽高比。如果偏离1:2超过20%,就回头调整目标矩形尺寸。这个验证步骤花不了几秒钟,但能避免后面整条流程白跑。
4. 插值与边缘处理:那些文档里不会写的细节
4.1 双线性插值的边界问题
投影变换之后,图像四个角可能会出现黑色区域,因为原图中没有对应的像素。Halcon默认用0填充,也就是黑色。这些黑色区域如果落在车牌内部,会干扰后续的阈值分割。
我的处理方式是:在投影变换之前,先把车牌区域向外扩展几个像素。用dilation_circle对车牌Region做一次膨胀,半径3到5像素,然后再做变换。这样变换后的车牌边界会稍微超出实际字符区域,黑色填充落在字符外面,不影响分割。
4.2 灰度归一化提升对比度
透视形变矫正之后,车牌区域的灰度分布可能会发生变化。原来在暗处的字符,矫正后可能变得更暗;原来在亮处的,可能变得更亮。这会导致全局阈值分割失效。
我通常会在矫正后加一步scale_image或者equ_histo_image,把灰度拉伸到0-255全范围。实测下来,直方图均衡化对低对比度车牌的提升最明显,识别率能提高5到8个百分点。但要注意,均衡化会放大噪声,如果图像本身噪点多,先用median_image做一次中值滤波,半径2到3。
4.3 字符分割的阈值选择
矫正后的车牌,字符和背景的对比度通常比较明显。我一般用threshold配合dyn_threshold。全局阈值适合光照均匀的场景,局部动态阈值适合光照不均的场景。
具体参数:全局阈值我一般从100开始试,根据直方图双峰位置调整。动态阈值用dyn_threshold,Offset设10到15,LightDark设'dark'(假设字符比背景暗)。如果字符是白底黑字,就设'light'。
分割完之后,用select_shape按面积和宽高比筛选字符区域。面积范围根据目标矩形尺寸推算,宽高比限制在0.3到0.8之间(宽/高)。这样能把大部分噪声和边框干扰滤掉。
5. 完整流程串讲与参数速查
5.1 从原图到识别结果的七步流程
把前面所有环节串起来,一个完整的透视形变车牌识别流程是这样的:
- 车牌区域提取:颜色分割或边缘检测,拿到车牌Region。
- 轮廓提取与多边形逼近:
gen_contour_region_xld+gen_polygons_xld,Tolerance=3.0。 - 四角点筛选与排序:筛选四边形,按左上、右上、右下、左下排序。
- 目标矩形尺寸计算:对边长度取平均,得到目标宽高。
- 投影变换矩阵计算:
vector_to_proj_hom_mat2d。 - 图像矫正:
projective_trans_image,双线性插值。 - 字符分割与识别:阈值分割 + 形态学处理 + 分类器识别。
每一步的参数我都整理成了速查表,方便你直接抄作业。
5.2 关键参数速查表
| 环节 | 算子/参数 | 推荐值 | 说明 |
|---|---|---|---|
| 多边形逼近 | gen_polygons_xldTolerance | 3.0 | 高分辨率可调到4.0-5.0 |
| 区域膨胀 | dilation_circleRadius | 3-5像素 | 防止变换后边界黑边干扰 |
| 投影变换 | projective_trans_imageInterpolation | 'bilinear' | 速度与质量平衡 |
| 灰度增强 | equ_histo_image | 默认参数 | 低对比度场景必加 |
| 中值滤波 | median_imageRadius | 2-3 | 噪声多时先用 |
| 全局阈值 | thresholdMinGray | 100起调 | 根据直方图双峰调整 |
| 动态阈值 | dyn_thresholdOffset | 10-15 | 光照不均时使用 |
| 字符筛选 | select_shapewidth/height | 0.3-0.8 | 宽高比范围 |
5.3 我踩过的三个典型坑
第一个坑:角点顺序搞反。有一次测试,矫正后的车牌看起来完全正常,但识别结果全是乱码。查了半天才发现,右上和左下的点搞反了,投影变换把车牌做了镜像。后来我在排序逻辑里加了一个判断:如果左上点的x坐标大于右上点,就交换它们。这个检查现在是我代码里的标配。
第二个坑:目标矩形宽度设得太大。一开始我想着“宽一点总比窄一点好,信息不会丢”,结果字符被拉得太宽,宽高比变成1:1.5,分类器直接不认。后来老老实实按对边平均来算,问题消失。
第三个坑:忘了做灰度归一化。在地下停车场拍的图,车牌区域整体偏暗,阈值分割只能切出一半字符。加了直方图均衡化之后,字符完整分割,识别率从60%跳到85%。这个步骤现在是我流程里的固定环节,不管图像看起来亮不亮,都跑一遍。
6. 扩展思考:这套方法还能用在哪
6.1 文档扫描矫正
同样的思路可以直接迁移到文档扫描。拍纸质文档时,手机不可能完全平行于纸面,拍出来总是梯形。用轮廓逼近找到纸张四个角,投影变换拉平,再走OCR,识别率比直接OCR高出一大截。区别在于文档的角点通常是直角,筛选四边形时可以用角度约束,把不是近似直角的四边形滤掉。
6.2 工业读码与标签识别
产线上的二维码或者条形码标签,贴在曲面上或者倾斜安装时,也会有透视形变。用同样的角点定位+投影变换流程,先把标签区域拉平,再解码,成功率明显提升。工业场景的特点是背景比较干净,分割比车牌容易,但精度要求更高,角点误差要控制在1个像素以内。
6.3 参数迁移的注意事项
迁移到新场景时,多边形逼近的Tolerance和目标矩形尺寸计算方式需要重新调。文档的边界更直,Tolerance可以设小一点,2.0左右;工业标签通常比较小,目标矩形尺寸要按实际标签比例来,不能再用对边平均,否则小标签会被过度放大。我的经验是:每换一个场景,先拿20张图做参数扫描,找到使字符分割成功率最高的那组参数,再固化到流程里。
这套透视形变矫正的思路,核心就是“找角点、算矩阵、拉平”。听起来简单,但角点定位的精度、目标尺寸的合理性、插值方式的选择,每一个细节都会影响最终结果。我在实际项目里反复调了三四轮,才把识别率从60%推到90%以上。如果你也在做类似的事情,建议先把角点定位这一环做扎实,后面的流程会顺很多。