13 行 41 列的灰度车牌,投影法切出 5 段、每段宽 5 列,16 个点阵模板逐个比相关, σ 到 0.15 时 5 / 5 全对。 噪声加到 0.20 仍是 5 段但只对 3 个,到 0.30 段数掉成 4,0 / 5。
上一节把一堆文字框还原成表格,这一节处理图像——同一个「先定位再识别」的套路, 但车牌对切分的要求高得多。
一、三步链条:定位、切字、识别
车牌识别是三步串联:先从一张大图里框出车牌,再把车牌切成单个字符,最后逐个认。 Demo 只做后两步。定位那一步要检测模型(YOLO 之类),沙箱里装不了,我也不硬编一段假装跑过。 串联的结构有个共同弱点:前面错一步,后面全错。框偏一点,切字就切歪;切歪一段,识别再准也没用。 所以工业上的做法早就改了:整行当序列识别,不切字。这件事放到后面讲,先看清楚「切」为什么会出事。
二、投影法切字:拿空白列当下刀处
二值化之后,每一列有几行是黑的,就是一个数。把所有列的这个数按横坐标排开,叫垂直投影。 字符内部的列投影高,字符之间的空隙投影低到 0。所以投影等于 0 的列,就是天然的刀口。 Demo 的二值化阈值是 0.45,41 列切出 5 段,每段宽 5 列,x 范围分别是 4~9、11~16、 18~23、25~30、32~37。 这个方法快得离谱:一遍扫描就够,不需要模型,也不吃显存。代价是它假设「字符之间一定有空白的列」。 这个假设在真实场景里经常不成立——连笔、污损、透视变形、反光,任何一条都能把空隙填掉。
三、模板匹配:最像的就是答案
模板一共 16 个:10 个数字加 A 到 F。每个都是 5×7 的点阵。 把切出来的字符段缩放到 5×7,和每个模板算相关系数,取最大的那个当答案。 σ 到 0.15 这一档,五个字符的相关值都是 1.000。 这不是巧合——图本来就是我按模板画出来的,噪声又小,形状一点没走样。 所以这 5/5 不能当「方法真好」的证据。干净的点阵图对点阵模板,本来就该满分。 真实车牌有反光、污渍、倾斜,靠的是卷积网络。 相关系数是模板匹配里最常用的度量。 它比欧氏距离好在不受整体亮度影响:图整体偏暗一点,相关值几乎不变。
四、σ 从 0.05 加到 0.30:崩的顺序说明了什么
0.05、0.10、0.15 三档全对。 0.20 时还是 5 段,但结果变成 B4806,逐字 3 / 5——「8」认成 4、「D」认成 0,错的是形状接近的。 0.25 时错成 2B806,2 / 5。到 0.30,段数直接掉成 4,结果 1854,0 / 5。 注意这个顺序:先崩认字,最后才崩切字。 认字崩是渐变。噪声一点点盖住笔画,相关值一点点掉,错得还挺「像」——8 和 4 本来就只差一笔。 切字崩是突变。两个字的空隙被噪声填满,两段粘成一段,段数少一个,后面全乱。 而且一崩就没救:模板匹配拿到的不是字符,是两个字的混合体,怎么比都错。 这正是工业上不切字的理由。CRNN 把整行当序列,一个输出对一位,绕开了「一切错全错」这条链。
五、动手 Demo:车牌识别:投影法切字 + 模板匹配,噪声一大就散架
用 5×7 点阵字模把「B38D6」画成一张 13 行 41 列的灰度图,加高斯噪声、按 0.45 二值化; 算出每列的黑点数做垂直投影,投影为 0 的列当刀口切成字符段; 每段缩到 5×7 和 16 个模板算相关系数取最大;最后把噪声从 0.05 扫到 0.30,看结果怎么一步步垮掉。
col = (g > 0.45).sum(axis=0) # 每列的黑点数
cuts = [i for i in range(len(col)) if col[i] == 0]
seg = img[np.ix_(rows, cols)] # 缩到 5×7
r = float(np.corrcoef(seg.ravel(), tpl.ravel())[0, 1])
实跑输出:
--------------------------------------------------------------------
车牌识别:投影法切字 + 模板匹配,噪声一大就散架
--------------------------------------------------------------------
车牌内容(真值) B38D6
图像尺寸 13 行 × 41 列 灰度
字符模版 16 个(10 数字 + A~F)
二值化阈值 0.45
先看 σ = 0.15 这一档的细节:
切出字符段 5 段
段 x 范围 宽度 识别 相关
--------------------------------------
第 1 段 4 ~ 9 5 B 1.000
第 2 段 11 ~ 16 5 3 1.000
第 3 段 18 ~ 23 5 8 1.000
第 4 段 25 ~ 30 5 D 1.000
第 5 段 32 ~ 37 5 6 1.000
识别结果 B38D6
逐字正确 5 / 5
把噪声从 0.05 加到 0.30,同一套代码不认识任何东西了:
噪声 σ 切出段数 识别结果 逐字正确
--------------------------------------
0.05 5 B38D6 5 / 5
0.10 5 B38D6 5 / 5
0.15 5 B38D6 5 / 5
0.20 5 B4806 3 / 5
0.25 5 2B806 2 / 5
0.30 4 1854 0 / 5
注意崩的顺序:σ 到 0.20 时还是 5 段,但个别字认错(B 认成 4);
0.25 时错成 2B806;到 0.30,段数直接掉成 4,全错。
也就是说,先崩的是「认字」,最后才崩「切字」。
而切字一崩就彻底没救——投影法假设字符之间有空白列,
噪声填满空白列,两个字就粘成一段,后面怎么认都是错的。
真实车牌识别是三步:定位(从大图里框出车牌)→ 切字 → 识别。
工业上的做法根本不切字,直接用 CRNN 把整行当序列识别,
就是为了绕开这个「一切错全错」的链条。
车牌图是我用 5×7 点阵字模画出来的,噪声是我加的。
真实车牌还有反光、污渍、透视变形,比这更难。
第一档切出 5 段,宽度全是 5 列,间隔均匀——因为这张图是我按固定间距画的,不是真实车片。 相关值 5 个都是 1.000。 自己画的图对自己的模板,满分是必然的,只能说明「链路通了」,不能说明「方法强」。 σ=0.20 那行是关键证据:段数还是 5,说明切分没坏;结果变成 B4806,说明坏在认字。 这两件事分得开,才看得出崩的顺序。 σ=0.30 时只剩 4 段,结果 1854。段数一少,后面无论怎么认都是错的——这就是串联系统的放大效应。
30 秒实验
- 把二值化阈值从 0.45 改成 0.30 和 0.60,看切出的段数怎么变。
- 把模板库里的 A~F 换成三个汉字,看识别结果和相关系数有什么变化。
- 想一想:为什么噪声先让「认字」错,最后才让「切字」错?
常见误区
- 「投影法切字很稳」——它假设字符之间有空白列,噪声一填就粘成一段。
- 「5/5 说明识别率高」——点阵图对点阵模板,干净时本来就该满分。
- 「切错一个字符只错一个字」——切错一段,后面整行全错。
自测三问(点开对答案)
1. 投影法切字靠的是什么信号? —— 垂直投影为 0 的列,也就是字符之间那几列空白。 2. 为什么工业上的车牌识别不切字? —— 切字一错整行全错;把整行当序列识别能绕开这条链。 3. 这个 Demo 的 5/5 能说明什么、不能说明什么? —— 只能说明切分和匹配这条链在干净图上通了,不能说明真实车片也这么好。图像这条线走完了。下一个实例换到文本:把一整个网页压成几句话。
- ☐ (本节完,下一节 5.2.7 实例7:使用AI总结网页内容)