ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Hu矩与形状特征的车载识别系统:从车牌到车型的CPU实现

基于Hu矩与形状特征的车载识别系统:从车牌到车型的CPU实现 简介一份基于MFC实现的车型识别系统完整工程面向图像处理、计算机视觉及MFC界面开发的初学者与课程设计人群。项目主流程按载入图像、车辆提取、轮廓提取、车型识别四步组织依次执行图像做差、二值化、开运算、去噪、填充等操作帮助理解背景差分与目标分割的衔接。压缩包共26个文件以8个头文件和6个C源文件为核心配套BMP测试图像、图标及VS项目配置整体仅59KB结构简洁便于快速编译运行。已有258人学习浏览适合参考其模块划分与算法实现也可作为毕业设计或比赛方案的启动模板。1. 车型识别先看清楚你在做的到底是哪一类问题做车型识别系统的人十个里有七个是从车牌识别那边转过来的剩下的三个以为自己在做目标检测。CarShapeIdentify 这个方向真正的落点不是“看见一辆车”而是“认出这辆车是什么型号”。车牌识别能告诉你这是谁的车车型识别要回答的是这辆车是轿车、SUV、还是某品牌某代车型。它解决的是车牌看不清、没车牌、或者单纯想按车型做分类的场景——停车场闸口按车型计费、园区车辆统计、高速卡口车辆特征建档都靠这个。适合读这篇文章的人是你手里已经有一批车辆图片或者有一个摄像头点位想快速验证车型识别能不能跑通。不需要 GPU不依赖深度学习框架单机 CPU 就能把流程走完。下文我会按一套经典实现路径展开数据准备、特征提取、匹配检索、参数调优和排障。这套方案的最大优点是可复现最大缺点是特征表达能力有限读完你会知道边界在哪。先从车辆识别里最容易被忽略的形状特征说起。2. 车辆识别里的形状特征为什么不能只靠车牌2.1 车牌识别的局限与形状特征的兜底价值很多车辆识别项目最初只上牌识因为车牌是唯一标识看起来最直接。但真正做了现场部署的人都会遇到一类问题新能源车牌比蓝牌长一截、车尾车牌被泥糊住、夜间补光灯过曝导致牌面全白、外地牌照字体和标准字体有细微差别。这些情况下车牌识别准确率会掉得很厉害。更麻烦的是有些场景压根不允许拍车牌比如园区内部车辆统计只需要分成“员工车/访客车”两类或者需要识别品牌型号做精细化管理。这时“车型识别”就变成了独立需求。形状特征的兜底价值在于车辆轮廓是物理结构决定的不随车牌样式、贴纸、涂装变化。一辆五菱宏光和一辆丰田埃尔法即便把车标抠掉轮廓上的差异依然巨大。这给了我们一条不用训练深度模型就能跑通的路径——提取车辆轮廓的几何特征构建特征库然后做最近邻检索。处理一张图的耗时控制在几十毫秒内完全符合实时性要求。2.2 建立车型图库的最简目录约定识别系统的第一步永远是数据。对于 CarShapeIdentify 这类以形状为核心的项目数据收集的重点不是越多越好而是“同一个车型、多个角度、光照尽量均匀”。我一般建议按下述目录结构组织样本集。data/ ├── train/ │ ├── sedan_bora/ # 大众宝来 │ │ ├── 001_front.jpg │ │ ├── 002_front_left.jpg │ │ └── 003_front_right.jpg │ ├── suv_rav4/ # 丰田RAV4 │ │ ├── 001_front.jpg │ │ └── 002_side.jpg │ └── mpv_gl8/ # 别克GL8 │ ├── 001_front.jpg │ └── 002_front_left.jpg └── test/ ├── sedan_bora/ └── suv_rav4/目录名统一用“车辆类型_品牌型号”的格式train 和 test 严格分开。类名不要用中文避免后续脚本编码问题。每个类最少 20 张图角度尽量覆盖车头正视角、车头左前 30 度、车头右前 30 度。这里有一个实用经验宁可只有 30 个车型、每车型 50 张图也不要收 200 类、每类 3 张图特征库图片太少会让检索结果变成一场抽奖。数据标注环节不需要按照目标检测那种画框方式因为最终输入到识别模块的是整张车辆前脸图。你要做的是把图片统一裁剪到车辆主体居中、占比大于 60%然后换算到同一个长宽比下。这样既保留轮廓信息又避免背景干扰特征向量。别小看这一步我在实际项目中见过很多识别分数上不去的情况最后定位到的问题就是训练图里车占了不到三分之一轮廓特征全被背景吞掉了。3. 把车头照片变成特征向量灰度、尺寸归一与Hu矩3.1 图像预处理的三个关键操作及参数特征提取前必须做预处理。最常见的处理链是灰度化 → 统一尺寸 → 去噪 → 边缘提取。先看一段我常用的预处理代码。import cv2 import numpy as np def preprocess_vehicle(image, target_size(224, 224)): # 统一尺寸用letterbox而不是直接resize防止宽高比失真 h, w image.shape[:2] scale min(target_size[0] / h, target_size[1] / w) new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(image, (new_w, new_h), interpolationcv2.INTER_AREA) # 创建一个灰底画布把resize后的图贴到中间 canvas np.full((target_size[0], target_size[1], 3), 114, dtypenp.uint8) offset_x (target_size[1] - new_w) // 2 offset_y (target_size[0] - new_h) // 2 canvas[offset_y:offset_y new_h, offset_x:offset_x new_w] resized # 转为灰度并做中值滤波去噪 gray cv2.cvtColor(canvas, cv2.COLOR_BGR2GRAY) gray cv2.medianBlur(gray, 5) # Canny边缘提取低阈值50高阈值150 edges cv2.Canny(gray, 50, 150) return gray, edges这里最重要的参数是 target_size。224×224 是参考 ImageNet 的常规尺寸但做形状特征时其实可以更小比如 160×160特征差别不大但计算速度更快。Canny 的阈值才是真正的玄学50/150 在白天顺光条件下很稳定到了逆光或夜间场景就失灵了。遇到这类情况我会直接改成自适应阈值 c1, c2, edges cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)先用大津法找一个整体分割阈值再在这个基础上做边缘。还有一处细节medianBlur 的卷积核要是奇数常用 3、5、7我推荐 5去噪效果和边缘保留程度比较平衡。letterbox 预处理比直接 resize 多传递了一个信息车牌上的文字经过纵向压缩后轮廓特征会面目全非。尤其对于车灯和进气格栅这种横向结构等比例缩放才能保留它们之间的比例关系。这个细节在车辆识别里非常关键不少第一版系统翻车就翻在这里。3.2 Hu矩特征为什么七个不变矩能描述一辆车在边缘图拿到手之后我们要把它压缩成一个向量。最经典的做法是计算图像的 Hu 矩。Hu 矩是七个对平移、旋转、缩放都不敏感的区域描述子前六个对旋转敏感度低第七个对镜像形状敏感。车辆边缘图天然适合这套东西——轿车和 SUV 的整体拓扑在不变矩空间里的分布是分开的。def extract_hu_moments(edges): # 计算边缘图上所有轮廓的Hu矩取最大轮廓作为车辆主体 contours, _ cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None main_contour max(contours, keycv2.contourArea) moments cv2.moments(main_contour) hu cv2.HuMoments(moments).flatten() # 对Hu矩做log变换压缩动态范围 for i in range(len(hu)): if hu[i] ! 0: hu[i] -1 * np.sign(hu[i]) * np.log10(abs(hu[i]) 1e-6) return hu feature_vector extract_hu_moments(edges) print(feature_vector)这里有两个关键点。第一findContours 要找最大轮廓也就是把车辆轮廓主体挑出来。如果预处理阶段没有裁剪好车辆位置最大轮廓可能是地面阴影或者旁边的树。第二return 前做 log 变换是因为 Hu 矩的数值跨度极大第 7 阶矩可能只有 10 的负 8 次方不做 log 变换直接算相似度高阶矩几乎不起作用。加上 1e-6 是为了防止 log0 的问题公式里用了 np.sign 是为了保留矩的正负方向信息。特征是 7 维的但实际使用中我会再加 2 个维度车辆边缘轮廓的宽高比、轮廓面积占整个画布的比例。这两维能有效区分轿车和 SUV——SUV 车身更高、面积占比更大。最终特征向量是 9 维虽然不多但足够支撑一个几百辆车的检索库。如果你觉得 9 维太单薄可以再加 7 阶原始矩之外的统计量比如轮廓的周长、凸包面积比但每加一维都意味着对光照更敏感这部分是基于场景调优而不是越多越好的。4. 用特征库做车型匹配训练库构建与检索参数4.1 批量构建特征库并持久化到本地当图片库准备好之后第一步是把所有训练图片的 9 维特征全部算出来存成一个二进制文件。检索时直接加载这个特征库不需要每次重新过图。import pickle, os, glob import numpy as np import cv2 def build_feature_database(data_dir, output_path): # 遍历train目录下的所有子文件夹每个子文件夹名即车型类名 classes [d for d in os.listdir(data_dir) if os.path.isdir(os.path.join(data_dir, d))] all_features [] all_labels [] for cls_idx, cls_name in enumerate(classes): class_dir os.path.join(data_dir, cls_name) img_paths glob.glob(os.path.join(class_dir, *.jpg)) \ glob.glob(os.path.join(class_dir, *.png)) for img_path in img_paths: image cv2.imread(img_path) if image is None: continue gray, edges preprocess_vehicle(image) feature extract_hu_moments(edges) if feature is not None: all_features.append(feature) all_labels.append(cls_name) # 归一化所有特征向量为后续余弦相似度做准备 all_features np.array(all_features) norms np.linalg.norm(all_features, axis1, keepdimsTrue) all_features all_features / norms database { features: all_features, # shape: (N, 9) labels: np.array(all_labels), classes: np.array(classes) } with open(output_path, wb) as f: pickle.dump(database, f) print(fbuild done: {len(all_labels)} images, {len(classes)} classes)注意归一化用的是 L2 范数这一步做完后可以直接用点积代替余弦相似度的分母计算。pickle 是最近便的存储方案在特征库超过 5 万条、加载速度成为瓶颈时再换成 faiss 不迟。初次搭建阶段pickle 足够用而且便于排查问题。4.2 在线车型识别与 Top-K 阈值过滤特征库构建完成就到了车型识别系统最核心的查询环节。我采用的做法传入一张实时抓拍图提取特征和特征库里所有特征算相似度排名前 K 的结果投票得出最终车型。def identify_vehicle(query_image_path, database_path, top_k3): # 加载特征库 with open(database_path, rb) as f: db pickle.load(f) db_features db[features] db_labels db[labels] classes db[classes] # 提取查询图的特征向量 image cv2.imread(query_image_path) gray, edges preprocess_vehicle(image) query_feature extract_hu_moments(edges) if query_feature is None: return unknown, 0.0 # 归一化直接做矩阵点积求余弦相似度 query_feature query_feature / (np.linalg.norm(query_feature) 1e-8) similarities db_features query_feature # 取出Top K索引 top_indices np.argsort(similarities)[::-1][:top_k] top_scores similarities[top_indices] top_labels db_labels[top_indices] # 投票决定最终车型同时输出最高相似度分数 from collections import Counter vote_count Counter(top_labels.tolist()) best_label, _ vote_count.most_common(1)[0] if top_scores[0] 0.45: return unknown, float(top_scores[0]) return best_label, float(top_scores[0])这里有两个参数值得讲透。第一个是 top_k。K 值太小单张误匹配就能带偏结果K 值太大低分样本会拉低投票置信度。以 300 到 1000 辆车的小型特征库来说Top 3 到 Top 5 比较合适。第二个是阈值 0.45这是余弦相似度的经验下限。实测中同一车型不同角度的相似度一般在 0.6 以上不同车型则在 0.3 到 0.5 之间。阈值设 0.45 的意义是宁可不识别也不给错误答案——现场管理场景里误判一台超限车的代价比漏判大得多。这个阈值需要结合自己的特征库做统计后确定方法在第 6 章再展开。比对一张查询图与 1000 张特征库图纯 numpy 点积耗时在毫秒级整条链路在普通办公 CPU 上也能控制在 80ms 内。5. 车型识别系统踩坑排查五个让结果变玄学的现场问题做这个方案到现在我整理出了一份高频率踩坑清单。这些问题单独看都不起眼但叠加起来会让识别结果变得极其玄学半个小时内都查不出原因。以下五条是按踩中概率排序的。5.1 车牌才是最大的干扰源现象同一车型的不同车辆反复识别失败且置信度极低。 原因车牌区域有高对比度的字符和边框计算最大轮廓时车牌字符形成的内部轮廓会“污染”Hu矩分布。尤其是白色车牌、新能源绿色车牌其矩值贡献远大于车身轮廓本身。 解决在提取轮廓前先做一次粗略的车牌位置掩膜把车牌区域填充为与周围相同的亮度。常见做法是先按车牌颜色在 HSV 空间锁定区域用 cv2.inRange 找到掩膜后用 cv2.inpaint 或者直接 paint 成灰度背景。这一步等于把识别目标从“整张车图”拉回“车身轮廓”效果立竿见影。5.2 车头车尾两种视角混在一起轮廓特征完全错位现象报错率奇高车尾特征库识别车头图时相似度普遍低于 0.3。 原因轿车车头有进气格栅、大灯车尾只有尾灯和保险杠两种视角的边缘轮廓差异比不同车型之间的差异还大。 解决把特征库按视角拆成两套。训练和查询都只用车头前 30 度至正前方的图片。如果现场相机是单目且装在后端那就只采集车尾图。把这个约定写死在数据采集规范中要求前段同学不要混着传。CarShapeIdentify 这套形状特征的思路本身没有视角鲁棒性硬做是多视角只会把自己的准确率做到三成。5.3 夜间或逆光条件下 Canny 边缘大片断裂现象白天测试准确率很好傍晚开始识别结果稳定错乱。 原因Canny 固定阈值 50/150 是白天顺光调出来的暗光环境下边缘梯度非常低大量真实轮廓检测不出来。边缘图只剩一块一块的断裂碎片最大轮廓面积发生跳变。 解决改用边缘检测前先做 CLAHE 对比度增强限制对比度 clipLimit 设置为 2.0tileGridSize 设 8×8。这样低照度下的边缘能回来不少。如果仍不理想在检测流程里加一个前置判断当整张图平均灰度低于 80 时把 Canny 双阈值自动降低到 20/60。这个策略牺牲了一点背景噪音但保住了主要轮廓。5.4 特征库类别不均衡导致新车型永远不被选中现象新录入一个车型后查询结果总是偏向老车型连完全不应匹配的图上榜。 原因特征库中各类别图片数量差距大。某车型 300 张新车型只有 15 张Top K 检索时旧车型特征向量在近邻空间里占据了绝对密度优势点相似度被“多数票”裹挟。 解决先统计各类样本数量低于某阈值时做简单的数据增强——水平翻转、小幅旋转 5 度、亮度增益调整。把每类训练图数量拉平到 30 张以上。如果实在采集不到识别阶段就不能只投票还要加一条惩罚项类别出现频率超过全库均值 3 倍时对该类识别分数做衰减。5.5 resize 到 224×224 后侧面车看起来被压扁了现象录入侧面车型图时识别正常但实拍侧面图却识别成完全不同的类别。 原因直接 cv2.resize 把原图宽高比强行压缩到正方形车身高度没变但整车长度被压了接近一半比例特征直接失效。 解决改用我在第 3 章代码里的 letterbox 方案保证车辆纵横比不变剩余区域用灰色填充而不是拉伸。注意特征库里必须有一致的填充处理绝不能一部分图是 letterbox另一部分是直接 resize。这个坑排查起来非常耗费时间因为代码看起来没有报错特征维度没有变化只有一比对了才会发现整体比例特征出现了偏移。6. 识别效果的两个硬指标与一条升级路系统搭建完成后要证明它能用我习惯用混淆矩阵和类间相似度分布两个硬指标来验收。混淆矩阵不需要自己写用 sklearn 一行就能出重点看对角线以外的聚集区如果错误集中在某两个车形相似的类别先排查是不是视角混用问题。另一个指标更有诊断力把所有查询对的相似度分数和所有错误对的相似度分数画成两条曲线如果两者明显分离说明阈值设置合理如果重叠严重说明特征表达能力已经到边界了。关于升级路径绝大多数项目做一段时间后会撞到同一个天花板部分车型比如同代 A 级轿车轮廓非常接近9 维矩特征区分不开。我自己习惯的下一步是换成形状上下文特征在轮廓点上采样并计算点集间的匹配代价然后再叠加一个轻量分类器。这个方向保留了解释性也没有引入 GPU 依赖。如果业务场景允许一定时延再考虑走微调一个轻量 CNN 分类器的路线但那时这项目的地基已经不在 CarShapeIdentify 上了。这套方案对 30 到 100 类车型、单视角、中小样本量的场景非常匹配。做这类系统最怕的不是算法精度差而是现场光照和拍摄角度不稳定却硬要模型去背锅。先固定视角再统一预处理最后才调参数这是我在这类项目里总结出的血泪顺序。按今天这套流程做下来至少能帮你把不确定的那部分从 80% 压缩到 20%剩下 20% 是用数据量去补的。希望帮到你。本文还有配套的精品资源点击获取
返回列表