☰
基于Hu矩与形状特征的车载识别系统:从车牌到车型的CPU实现
2026/10/5 9:26:00 网站建设 项目流程

简介:一份基于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), interpolation=cv2.INTER_AREA) # 创建一个灰底画布,把resize后的图贴到中间 canvas = np.full((target_size[0], target_size[1], 3), 114, dtype=np.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, key=cv2.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, axis=1, keepdims=True) 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(f"build 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_k=3): # 加载特征库 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.0,tileGridSize 设 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% 是用数据量去补的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询