
简介这是一套基于深度学习的车辆特征分析系统源码与配套资源面向Python开发者、计算机视觉学习者及智能交通项目实践者。系统基于Python与深度学习方法构建车辆信息库通过上传图片即可自动识别车辆品牌、类型与颜色并不断积累品牌百科数据整体覆盖数据、模型、接口与前端展示等环节兼顾车牌识别等应用场景。压缩包共1826个文件约925MB包含大量jpg数据集、py源码及pyc编译文件以及html/css/js构成的Web界面、pth模型权重、sql数据库脚本等可满足模型训练、系统部署与二次开发需求。目前已有27人学习浏览适合需要完整项目参考的初学者与进阶开发者。利用该资源可深入理解车辆识别从数据组织、特征提取到前端交互的工程流程并能基于现有代码优化识别效果。1. 为什么一套Python车辆特征分析系统值得你亲手搭一遍停车场的闸机又抬不起来了监控里那辆白色SUV的车牌反光道闸系统误判成无牌车——这种场景下只做“车辆检测”远远不够你需要的是“车辆特征分析”车型、颜色、车标、年款甚至车身上的一张贴纸。python基于深度学习的车俩特征分析系统就是用Python生态把检测、分类、特征提取串成一条自动化流水线输入一段监控视频或者一张卡口抓拍图输出“这是一辆2020款白色本田雅阁置信度0.91”这样的结构化信息。这套东西不只是停车场能用收费站车型识别、园区重点车辆管控、二手车估价辅助都在用同一套技术栈。适合谁搞过图像分类但没碰过完整项目的Python开发者或者已经有业务数据但不知道从哪下手的算法工程师。深度学习在这里的核心价值不是“识别精度高”这种漂亮话而是它能扛住白天黑夜、逆光、雨天这种真实环境的抖动传统视觉在光照变化面前基本是纸糊的。2. 数据准备是第一个分水岭抓拍图怎么变成可训练的数据集2.1 别急着标注先按业务把“特征”拆成三类车辆特征分析系统的数据和普通目标检测数据集最大的区别在于“标签结构”。做通用检测一个框加一个类别就够了但车辆特征分析至少包含三层信息首先是“车”本身的存在性即目标检测的bounding box其次是“车的身份”包括车型轿车/SUV/MPV/卡车、品牌、具体年款最后是“外观属性”比如颜色、有无天窗、行李架、贴膜深浅。这三层信息如果混在一套标签体系里模型训练时会出现严重的梯度冲突——一个分支既想学“这是SUV”又想学“这是红色”特征空间会互相打架。我一般会在项目开始时先把标注规范定成JSON结构每辆车一个对象里面嵌套三个字段detection、type、attribute。检测框用xmin、ymin、xmax、ymax的绝对坐标type和attribute用枚举值。为什么不用COCO那种单层标签因为后续你要做车辆检索或者去重时按“白色 本田 雅阁 2018款”这种组合条件过滤数据嵌套结构比扁平标签好写检索逻辑而且能直接喂给多任务模型。{ image_id: camera_01_20240612_183502.jpg, vehicles: [ { bbox: [126, 214, 486, 732], type: sedan, brand: honda, model: accord, year: 2018, color: white, attributes: { roof_rack: false, sunroof: true } } ] }这段JSON是单张图片里一辆车的完整标注。注意bbox坐标必须是整数很多新手用float坐标训练时发现数据增强模块报错其实大部分增强库对坐标做了线性变换float本身没问题但转成yolo格式时除以图片宽高后要保留6位小数否则框会抖动。color字段我没有用“白/灰/黑”这种中文而是映射成英文枚举因为后续做one-hot编码时需要固定类别表中文容易在字典排序时出乱子。2.2 用公开数据集做底座用现场抓拍做迁移修正这里必须说句实话车辆特征分析想从零采集数据再人工标注一个中小团队三个月都攒不够能用的量。常见做法是拿公开数据集打底——比如CompCars、VeRi、Stanford Cars这类的车辆细粒度数据集加上UA-DETRAC这种检测数据集。但这不代表你的模型能直接上线因为公开数据集大多是欧美或者特定城市的车辆分布到了国内三四线城市老款面包车、三轮农用车、改装车这些东西它没见过。我一般会拿公开数据集先训练一个base model然后部署到现场做“半自动标注”让模型跑一遍抓拍图只保留置信度高于0.95的检测框人工只修正框和标签这样把人工标注量降到原来的十分之一。这个方法在真实项目里比直接标注新数据快得多而且能让你在两周内完成第一版模型的闭环验证。import os import json from pathlib import Path def convert_compcars_to_project_format(compcars_root, output_path): 把CompCars的层级目录结构转成上面定义的统一JSON格式。 CompCars原始目录是 brand/model/year/xxx.jpg 这种嵌套结构。 dataset [] for brand_dir in Path(compcars_root).iterdir(): if not brand_dir.is_dir(): continue for model_dir in brand_dir.iterdir(): for year_dir in model_dir.iterdir(): for img_path in year_dir.glob(*.jpg): image_id str(img_path.relative_to(compcars_root)) # CompCars没有bbox先给整图作为一个宽松框后续用检测模型精修 h, w 720, 1280 # 占位真实项目中用PIL读取实际尺寸 dataset.append({ image_id: image_id, vehicles: [{ bbox: [0, 0, w, h], type: unknown, brand: brand_dir.name, model: model_dir.name, year: int(year_dir.name), color: unknown, attributes: {} }] }) with open(output_path, w, encodingutf-8) as f: json.dump(dataset, f, ensure_asciiFalse, indent2) convert_compcars_to_project_format(./compcars, ./compcars_initial.json)这段脚本的关键点是利用CompCars的目录名直接把brand和model标签带出来省掉了最痛苦的品牌标注环节。color和bbox先填unknown这是刻意的设计——与其编造一个错误值污染数据集不如留空等检测模型和颜色分类模型去补齐。这里有个经验参数CompCars的图片分辨率普遍在1600x1200以上做训练前统一缩放到640x640会损失车灯、车标这类细粒度信息建议用letterbox而不是resize保持长宽比。2.3 数据增强的顺序错了模型就学歪了车辆特征分析的数据增强不能直接套用ImageNet那套随机裁剪。车辆有强烈的结构先验你把车顶裁掉模型大概率学出一个废特征。我常用的增强组合是HSV色域扰动模拟早晚光照色温变化、轻度透视变换模拟不同卡口角度、随机遮挡模拟灯杆或树影、mosaic拼接增加小目标样本数量。排列顺序很重要先做几何变换再做光学变换因为HSV扰动作用在几何变换后的图像上才符合真实物理过程——光线变化是发生在物体姿态确定之后的。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.LongestMaxSize(max_size640), A.PadIfNeeded(min_height640, min_width640, border_mode0, value0), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.6), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit25, val_shift_limit25, p0.5), A.Perspective(scale(0.04, 0.08), p0.3), A.CoarseDropout(max_holes4, max_height40, max_width40, fill_value0, p0.3), A.Mosaic(p0.5), # 注意Mosaic要在最后做因为它内部会重新组合四个图 ToTensorV2() ])这个增强管线的设计逻辑是LongestMaxSize加PadIfNeeded保证送进网络的图都是640x640且不拉伸变形Perspective的scale控制在0.08以内是因为超过这个值车身的形变就不像真实世界了。CoarseDropout的fill_value设置为0黑色模拟的是夜间灯杆挡住车身的情况但注意别把关键的车灯区域挡太多所以max_holes设小一点。Mosaic放在最后是因为它内部会自动对四张图各自做一遍基础的尺度扰动如果你在前面已经做过Perspective叠加之后会出现双重形变。提示增强参数不是固定的先按我上面这套跑一个epoch看训练loss曲线的波动幅度。如果loss在下降但验证集mAP不涨通常就是增强太狠了把CoarseDropout的p从0.3降到0.15再试。3. 模型选型与训练从YOLO到多任务头的演进路线3.1 为什么第一版不要直接上最新的检测模型车辆特征分析系统的检测主干我现在90%的项目会选YOLOv8或者YOLO11系列的medium版本。原因不是精度最高而是Python生态的配套最完整——Ultralytics的库直接支持自定义多任务头、导出ONNX、量化部署这些在纯研究型模型上往往要自己写一堆胶水代码。你搜“深度学习cnn”能看到很多论文复现项目用ResNet或EfficientNet做特征提取但那是做学术实验的思路工程落地时你更需要在“检测框质量”和“分类准确率”之间找一个平衡点。单阶段检测器在车辆这种刚性物体上有天然优势车不会像人那样有夸张的姿态变化anchor-free的回归头能很好拟合bbox。真正决定成败的是输入分辨率。我用640x640做baseline但如果你的卡口距离远、车辆像素占比小要上960甚至1280。代价是推理时间几乎翻倍GPU显存从4GB涨到8GB这在消费级显卡上很紧张。from ultralytics import YOLO model YOLO(yolov8m.pt) model.train( data./vehicle_dataset.yaml, epochs120, imgsz640, batch16, lr00.01, lrf0.01, momentum0.937, weight_decay0.0005, optimizerSGD, device0, patience20, augmentFalse, cacheTrue )这里有几个参数值得单独说。imgsz设为640是因为yolov8m的默认预训练权重就是在640下训练的你直接拉到960会丢掉预训练的大部分迁移收益正确做法是先640训练30轮再用finetune方式切到960。optimizer选SGD而不是AdamW因为检测任务的loss surface相对平滑SGD配合余弦退火的泛化性更好AdamW在小数据集上容易过拟合训练集的噪点。cacheTrue一次性把数据读进内存第一次跑会慢几分钟但之后每个epoch能省下大量IO时间。3.2 多任务头让一个模型同时输出车型、颜色和关键点很多人会偷懒用“一个检测模型 三个分类模型”分别做车型、颜色、车标。这样做的问题是三个模型独立推理计算量翻三倍而且每个模型都要重新做预处理管线复杂度爆炸。更麻烦的是检测框稍有偏移三个分类器的输入就会不一致最后的结果很难对齐。常见做法是只改YOLO的head把原来的分类分支换成多个并行的分类头每个头负责一个属性。Ultralytics的架构里detect head的输出维度是4 num_classes你需要扩成4 num_types num_colors num_brands。这个改动不涉及主干网络所以预训练权重可以直接加载。import torch import torch.nn as nn class VehicleMultiTaskHead(nn.Module): 把YOLO检测头替换成多任务头输出检测框类型颜色品牌 def __init__(self, num_types8, num_colors10, num_brands20): super().__init__() # 每个格子预测的维度4个框坐标 1个objectness 3组分类 self.num_types num_types self.num_colors num_colors self.num_brands num_brands self.total_classes num_types num_colors num_brands self.conv nn.Conv2d(256, self.total_classes, kernel_size1) def forward(self, x): # x形状: [batch, 256, h, w] raw self.conv(x) # [batch, total_classes, h, w] # 把分类维度拆开 bs, _, h, w raw.shape raw raw.view(bs, 4 1 self.total_classes, h, w) return raw这个多任务头的关键点在于如何设计loss权重。如果让三个分类任务的loss直接相加颜色分类的loss量级通常比品牌分类大因为颜色更容易学模型会把梯度集中到颜色上品牌准确率就废了。我常用的做法是给每个任务设可学习权重或者手动固定权重type_loss_weight1.0color_loss_weight0.8brand_loss_weight1.2。品牌难学所以权重给大一点颜色简单所以压一点这能让三个头收敛速度接近。3.3 训练中的两个关键监控指标分类精度和recall的平衡车辆特征分析系统的验收指标和纯检测项目不一样不能只看mAP。你的业务方大概率问的是“这辆车是不是那辆黑名单车”——这是一个recall优先的问题漏报比误报严重得多。所以我训练时会同时盯三个数detection的mAP50-95attribute的top-1 accuracy以及“整车识别准确率”即检测框正确且type和color同时正确的比例。第三个指标才是业务真正的KPI。def compute_vehicle_level_accuracy(pred_boxes, pred_types, pred_colors, gt_boxes, gt_types, gt_colors, iou_thresh0.5): 整车级评估框IoU达标且类型和颜色同时正确才算对 assert len(pred_boxes) len(pred_types) len(pred_colors) correct 0 for pred_box, pred_type, pred_color in zip(pred_boxes, pred_types, pred_colors): for gt_box, gt_type, gt_color in zip(gt_boxes, gt_types, gt_colors): iou compute_iou(pred_box, gt_box) if iou iou_thresh and pred_type gt_type and pred_color gt_color: correct 1 break return correct / max(len(pred_boxes), 1)这个评估函数揭示了一个残酷事实即使检测mAP做到了0.85如果type精度是0.8color精度是0.9那整车准确率只有0.850.80.90.612。所以模型选型时我宁愿选择检测精度稍低但分类头更稳的方案。这也是为什么我强烈建议用多任务头而不是独立分类器——独立分类器你根本没法在训练时感知“框与分类的耦合错误”。4. 特征分析与后处理从检测框到“可查询的结构化车辆档案”4.1 颜色识别的坑把RGB转到HSV空间再分类别用原始像素车辆颜色识别看起来是图像分类里最简单的问题实际却最容易翻车。银灰色和香槟金在RGB空间里几乎分不开夜间白色车灯照射下红色车会变成橙色。直接把RGB三通道喂给分类网络模型学到的是光照特征而不是颜色特征。我的做法是转HSV空间并把色调H作为主导特征。具体来说把RGB转为HSV后H通道单独作为一个输入分支S和V作为辅助分支。这样做的好处是色调对光照强度的变化不敏感而饱和度S和明度V其实承载了“这个颜色是亮红还是暗红”的信息三个通道分开处理更符合人的颜色感知机制。import cv2 import numpy as np def extract_color_feature(crop_img): 输入检测模型裁出的车辆区域输出HSV颜色特征向量 # 去掉底部保险杠区域那里容易被泥水污染颜色 h, w crop_img.shape[:2] body_region crop_img[int(h*0.15):int(h*0.85), int(w*0.1):int(w*0.9)] hsv cv2.cvtColor(body_region, cv2.COLOR_BGR2HSV) # 计算H通道的直方图32个bin忽略低饱和度像素 mask (hsv[:, :, 1] 60) (hsv[:, :, 2] 40) hist_h cv2.calcHist([hsv], [0], mask.astype(np.uint8), [32], [0, 180]) hist_h cv2.normalize(hist_h, hist_h).flatten() # S和V的均值作为辅助特征 s_mean np.mean(hsv[:, :, 1]) v_mean np.mean(hsv[:, :, 2]) return np.concatenate([hist_h, [s_mean, v_mean]])这段代码的核心思路是“先验区域裁切”加“低饱和度像素过滤”。车身区域我只取高度15%到85%这个带子因为这个范围的像素基本是车门和引擎盖的颜色。饱和度阈值60和亮度阈值40用于排除阴影和白色反光像素否则黑色的车在阳光直射下会有一大片白色区域混进来。这个特征提取配合传统分类器比如随机森林在颜色识别上能做到90%以上准确率而且推理时间几乎为零不需要跑神经网络。注意HSV不是万能的。荧光色的车、贴了改色膜的车、重度做旧的老车都会让颜色分类失效这是物理层面的限制不是调参能解决的。碰到这类车颜色置信度高不了业务层面应该允许“未知颜色”这个类别存在而不是强行硬报一个颜色。4.2 车型细粒度识别用检测框的中心区域别用整张图车型分类比颜色分类更难因为需要从形状轮廓判断。很多人在这一步犯的错误是直接把检测框里的图resize到224x224丢给ResNet结果分类器学到的是“车头还是车尾”而不是“SUV还是轿车”。原因很简单检测框往往把车头和车尾都框进去方向不同轮廓完全不同。我通常会在车型分类前先做方向判别用车灯位置或车窗长宽比判断车头朝向然后把图像旋转到统一方向再做分类。更省事的方案是利用检测框的中心区域约占整个框的60%这个区域受车头车尾方向影响最小包含的信息主要是A柱角度、车顶弧度、C柱倾斜度而这些是区分轿车和SUV的最关键特征。def extract_body_center(crop_img, bbox, center_ratio0.6): 从检测框中提取车身中心区域用于车型分类 xmin, ymin, xmax, ymax bbox w xmax - xmin h ymax - ymin cx xmin w / 2 cy ymin h / 2 cw w * center_ratio ch h * center_ratio cx_min max(0, int(cx - cw / 2)) cx_max min(crop_img.shape[1], int(cx cw / 2)) cy_min max(0, int(cy - ch / 2)) cy_max min(crop_img.shape[0], int(cy ch / 2)) center_region crop_img[cy_min:cy_max, cx_min:cx_max] return center_regioncenter_ratio这个参数在0.55到0.65之间有良好表现。太大会引入车头或车尾的边缘特征太小则只看到车顶丢失了A柱和C柱的信息。这里的尺寸几何关系很重要实际车辆的长宽比大约2:1车长对车宽而检测框的宽高比取决于卡口角度正面抓拍时框偏方侧面抓拍时框偏长。所以在取中心区域前先判断框的宽高比如果大于1.5就切成上下两段分别对上半和下半做中心提取——这是处理侧面车辆的必要步骤。4.3 结构化输出与时间序列关联单帧的车辆特征分析做完系统还没闭环你需要的是“特征档案”而不是一帧结果。常见的做法是引入一个轻量级的跟踪器ByteTrack或DeepSORT把视频序列中同一辆车的多帧结果做投票融合然后用一个ES数据库或者简单的JSON文件存储车辆档案带时间戳和摄像头ID。class VehicleTracker: 基于IoU的轻量级车辆跟踪器不依赖ReID模型 def __init__(self, iou_threshold0.3, max_age10): self.tracks {} self.track_id 0 self.iou_threshold iou_threshold self.max_age max_age def update(self, boxes, features): boxes: 当前帧检测框列表 [[xmin, ymin, xmax, ymax], ...] features: 当前帧对应车辆的颜色车型特征 matched_pairs [] unmatched_tracks set(self.tracks.keys()) for i, box in enumerate(boxes): best_track None best_iou 0 for track_id, track_info in self.tracks.items(): last_box track_info[box] iou self._compute_iou(box, last_box) if iou self.iou_threshold and iou best_iou: best_iou iou best_track track_id if best_track is not None: self.tracks[best_track][box] box self.tracks[best_track][frames] 1 # 特征平滑更新颜色与车型的投票计数 for attr_name, attr_value in features[i].items(): attr_votes self.tracks[best_track][votes].setdefault(attr_name, {}) attr_votes[attr_value] attr_votes.get(attr_value, 0) 1 matched_pairs.append(i) else: self.track_id 1 self.tracks[self.track_id] { box: box, frames: 1, votes: {k: {v: 1} for k, v in features[i].items()} } return matched_pairs这个跟踪器的设计哲学是“够用就好”。车辆在卡口或停车场场景下运动是连续平滑的IoU匹配就足够稳定不需要复杂的运动模型和外观ReID。max_age10表示如果一帧没匹配上就保留10帧如果车辆被遮挡超过10帧就会丢失ID这个参数在停车场场景够用在高速卡口因为车速快可以适当调小到5。特征投票机制是关键每辆车的颜色和车型是多个帧的众数而不是某个单帧的结果能有效消除单帧误判。5. 部署与推理避坑从PyTorch到ONNX的5个常见问题5.1 模型导出后的shape问题导致推理崩溃PyTorch模型在训练时是动态shape但ONNX导出后通常固定输入尺寸。如果你在训练时用了640x640导出ONNX也必须是640x640但实际部署时如果使用整图推理会遇到小图被强行拉伸的问题。常见做法是动态轴导出把height和width设为dynamic但这样会增加推理延迟因为TensorRT需要重新优化。我一般建议如果摄像头固定直接按摄像头的分辨率裁剪后resize到640x640固定shape推理如果摄像头分辨率不固定导出动态轴onnx并用onnxruntime的torch.cuda.sync保证时序。5.2 颜色识别与车型识别的预处理不一致这是多任务模型最容易踩的坑。你检测部分的预处理是标准化到[0,1]但颜色识别分支需要HSV特征车型分支需要中心区域裁剪。如果共用一套预处理总有一个分支的输入是错位的。我见过新手在pipeline里把整张图标准化了然后颜色分支直接吃标准化后的RGB提取HSV特征时全部失效。解决方法是流程分叉检测模型跑完每个检测框单独做crop然后颜色分支和车型分支各走各的预处理链路。这个看似简单的问题在工程里会因为代码写得耦合度高而很难排查。5.3 GPU与CPU推理的batch大小选择推理阶段如果单路视频流batch1就够了如果接多路摄像头batch8或16能压满GPU。但要注意ONNX Runtime的CUDA EP在batch过大时内存占用会爆炸TensorRT则相反batch固定后性能极佳。我常用的配置是TensorRT用固定batch4输入分辨率固定这样推理延迟在2ms左右RTX 3060实测。5.4 模型量化后精度下跌的补救措施int8量化在车辆特征分析上精度下降通常在1-3个点但有时会在特定颜色上崩盘——比如深蓝色和黑色在int8量化后会变得几乎不可区分。这是因为深色区域的像素值集中在低数值区间量化步长过大导致信息丢失。补救措施给量化校准集里多放深色车辆样本或者对颜色分支保留fp16精度只量化检测主干。5.5 现场数据与训练数据分布不一致的排查方法模型在现场跑得不准第一个要怀疑的不是模型而是摄像头的位置和角度。我养成的习惯是部署后先截500张现场抓拍图统计检测框的宽高比分布和颜色分布跟训练集对比。如果现场的车大多是侧面角度而训练集以正面为主那再好的模型也会翻车。这种问题靠调参是没用的必须补充对应角度的数据做finetune。具体做法是写一个脚本用训练好的模型在新场景数据上做伪标注人工抽检后合并到训练集然后增量训练20-30轮。这个循环跑两次现场准确率一般能提升到可上线水平。6. 进阶技巧用特征向量做车辆去重与检索当你的系统稳定跑通后下一步通常是从“识别单辆车”升级到“在历史库里找同一辆车”。车辆特征分析产生的结构化属性颜色车型品牌年款其实远远不够做精确检索因为系统里可能有一千辆白色本田雅阁。要区分这些车需要提取一个可比较的特征向量。我常用的方案是在多任务模型的主干网络后面接一个全局特征池化层输出512维embedding。这个embedding经过了车型和颜色分类头的监督训练所以它在语义空间上是“按车型和颜色排列的”而不是按像素排列的这正是我们要的检索空间。训练时用triplet loss辅助优化让同款车比如白色雅阁的特征向量距离接近不同款车距离拉远。import torch import torch.nn as nn class VehicleEmbeddingModel(nn.Module): 在检测模型基础上增加embedding分支 def __init__(self, backbone, embed_dim512): super().__init__() self.backbone backbone self.embed_head nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(1280, embed_dim), # 1280是backbone输出的通道数 nn.L2Norm() # 归一化到单位球面方便用余弦相似度 ) def forward(self, x): features self.backbone(x) embedding self.embed_head(features) return embedding这里用AdaptiveAvgPool2d把空间特征压成向量再L2归一化目的是把特征向量映射到单位球面上这样两个向量的点积就是余弦相似度可以直接用内积做快速检索。向量索引我用的是hnswlib或者faiss百万级别的车辆数据库单次检索延迟可以控制在10ms以内。embedding在检索场景的价值你可以用一张现场抓拍图到历史库里找到同一辆车过去三天每次出现的记录从而画出它的行驶轨迹和停留位置。这个技巧的核心是区分“分类”和“检索”。分类模型只告诉你这车是什么检索模型告诉你是哪一辆。做重点车辆管控的业务检索的价值远大于分类。踩过的坑是embedding训练时如果只用triplet loss而没有分类头的约束模型会快速收敛到把颜色作为主导特征同款不同色的车反而会被拉远所以embedding分支和分类分支必须同时训练互相约束才能学到稳定的车辆身份特征。做这套系统的最后一段血泪经验是别把第一个版本的验证准确率当成上线后的效果——现场的光照、摄像头畸变、车辆运动模糊会让所有指标往下掉5到10个点这是物理规律不是你的模型不行。最好的策略是快速把第一版跑通然后基于现场的失败案例做数据补充和增量训练迭代两轮后系统才会真正变稳。希望帮到你。本文还有配套的精品资源点击获取