当第一次面对堆积如山的车牌图片时,我完全没料到这个项目会让我在数据标注环节就踩遍所有可能的坑。从文件命名混乱导致的标注错位,到稀缺省份样本不足引发的模型偏见,再到多标签输出时的梯度冲突——这个看似标准的计算机视觉项目,几乎在每个环节都藏着意想不到的陷阱。
在启动Labelme之前,我花了整整两天重构文件目录结构。早期随意存放的图片导致后续标注进度难以追踪,这个教训让我意识到数据工程必须从磁盘层级就开始规划。最终采用的目录树如下:
code复制plate_dataset/
├── raw_images/ # 原始图片
│ ├── batch_1/ # 按采集批次分组
│ └── batch_2/
├── resized_512/ # 预处理后的512x512图片
├── labelme_json/ # 标注生成的JSON文件
└── converted_data/ # 转换后的二值化标注
关键经验:使用YYYYMMDD_sequence格式命名图片文件(如20230615_0001.jpg),这种时序编码既能避免重复,又能通过文件名直接判断标注进度。曾因使用car1.jpg这类模糊命名导致2000张图片需要重新核对,这个惨痛教训值得所有CV项目引以为戒。
在标注界面中,这几个设置项显著提升了效率:
python复制# 标注时自动保存的配置示例
{
"flags": {
"auto_save": true,
"keep_prev": false
},
"labels": ["plate"], # 固定标签避免拼写错误
"default_dir": "/path/to/resized_512" # 指定默认打开目录
}
特别注意:务必在标注前统一完成图片resize操作。早期尝试先标注后resize,导致二值化标注出现中间灰度值(如127),迫使3000张标注推倒重来。这是因为OpenCV的resize插值会污染纯黑白标注图。
标注过程中定期执行质量检查脚本可及时发现问题:
bash复制python check_annotation.py --json_dir labelme_json --img_dir resized_512
针对"藏"、"青"等稀缺车牌,常规的旋转/翻转增强效果有限。我们开发了结合车牌物理特性的增强管道:
| 增强类型 | 参数范围 | 适用省份 | 效果提升 |
|---|---|---|---|
| 透视变换 | 最大倾斜角15度 | 所有省份 | +12% |
| 光照模拟 | HSV空间V通道±30% | 欠曝光样本 | +8% |
| 字符间距扰动 | 水平位移±5像素 | 长车牌 | +5% |
| 背景纹理融合 | 添加柏油/水泥纹理 | 高原省份 | +15% |
藏牌增强示例:
python复制def augment_tibetan_plate(img):
# 添加高原强光照效果
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hsv[:,:,2] = np.clip(hsv[:,:,2]*1.3, 0, 255)
# 模拟风沙磨损效果
kernel = np.ones((2,2), np.uint8)
erosion = cv2.erode(img, kernel, iterations=1)
# 添加背景纹理
texture = cv2.imread('textures/stone.jpg')
return blend_with_texture(erosion, texture, alpha=0.2)
为解决模型对模糊车牌的识别瓶颈,我们引入对抗生成思想:
这种方法使"贵"字车牌在测试集的识别率从83%提升到91%。需要注意的是,增强后的样本需要保持合理的类别分布,避免引入新的偏差。
传统多输出模型常忽略不同字符间的关联性,我们设计的分层共享架构在底层共享特征提取,高层按字符位置分化:
python复制def build_multi_output_cnn():
input_layer = Input(shape=(80, 240, 3))
# 共享特征提取层
x = Conv2D(32, (3,3), activation='relu')(input_layer)
x = MaxPooling2D()(x)
x = Conv2D(64, (3,3), activation='relu')(x)
x = MaxPooling2D()(x)
# 按字符位置分支
branches = []
for _ in range(7):
branch = Flatten()(x)
branch = Dense(128, activation='relu')(branch)
branch = Dropout(0.5)(branch)
branches.append(Dense(65, activation='softmax')(branch))
return Model(inputs=input_layer, outputs=branches)
初期直接求和7个交叉熵损失导致模型收敛不稳定,通过以下改进实现平稳训练:
动态加权损失:根据各字符的识别难度调整权重
python复制losses = {
'c1': 'sparse_categorical_crossentropy', # 省份字符权重更高
'c2': 'sparse_categorical_crossentropy',
...
}
loss_weights = {'c1': 1.5, 'c2': 1.2, ..., 'c7': 1.0}
渐进式训练:先单独训练省份字符分类器,再微调完整模型
梯度裁剪:设置clipnorm=1.0防止梯度爆炸
采用余弦退火配合热重启的策略,在batch size为32时的典型配置:
python复制lr_schedule = tf.keras.optimizers.schedules.CosineDecayRestarts(
initial_learning_rate=1e-3,
first_decay_steps=1000,
t_mul=2.0,
m_mul=0.9
)
配合早停机制(patience=8),模型在测试集上的综合准确率达到97.3%,其中最难识别的省份字符(藏)准确率从最初的68%提升到89%。
将训练好的模型转换为TFLite格式时,这些参数影响显著:
| 优化方法 | 推理速度(ms) | 准确率变化 | 模型大小 |
|---|---|---|---|
| FP32原始模型 | 45 | 基准 | 86MB |
| FP16量化 | 28 | -0.2% | 43MB |
| INT8量化 | 19 | -1.5% | 22MB |
| 剪枝+INT8量化 | 15 | -2.1% | 11MB |
实际部署建议:对ARM设备使用FP16量化,x86平台选用INT8量化。曾因过度剪枝(稀疏度>80%)导致"鄂"字识别率骤降15%,需谨慎平衡压缩率与精度。
为应对新式车牌不断出现的情况,设计了一套增量更新机制:
最近一次更新中,该方案成功吸收了新能源车牌特性,未出现灾难性遗忘问题。关键代码片段:
python复制# 知识蒸馏损失计算
def distil_loss(student_logits, teacher_logits, true_labels):
ce_loss = tf.keras.losses.SparseCategoricalCrossentropy()(true_labels, student_logits)
kl_loss = tf.keras.losses.KLDivergence()(
tf.nn.softmax(teacher_logits/temp),
tf.nn.softmax(student_logits/temp)
)
return alpha*ce_loss + (1-alpha)*kl_loss
整个项目最意外的收获是发现:良好的数据工程实践比模型调参更能提升最终效果。当标注流程规范化和数据增强策略完善后,即使简单的CNN结构也能达到商业级识别精度。那些深夜手动检查标注的枯燥工作,最终都转化为了模型指标上实实在在的提升。