ARTICLE DETAIL

资讯详情

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

MFC与ONNX Runtime实现车型识别和车身颜色识别实战

MFC与ONNX Runtime实现车型识别和车身颜色识别实战 简介一款基于MFC框架的车型与车颜色识别系统源码资源面向计算机视觉、智能交通及C桌面应用开发者可解决车辆类型自动分类、车身颜色判别与界面交互相结合的工程实现问题。包内共117个文件以lib、dll运行库和cpp、h源码为主另含dat训练数据、训练模型及VC工程配置文件压缩包约24.86MB便于在Visual Studio中直接打开编译与二次拓展。代码中提供了svm分类器、目标提取、车辆跟踪和颜色识别等模块train.dat作为样本数据覆盖从特征训练到识别结果返回的完整流程适合学习传统图像处理与机器学习结合的落地方式。已有111人学习使用对想快速上手MFC视觉识别项目的开发者而言这套源码能提供可运行的工程骨架和调试验证思路。1. 一个 ZIP 名字背后的项目MFC_windows.zip 到底要解决什么问题如果你在 Windows 环境里做视觉项目大概率接过类似MFC_windows.zip这样的压缩包名字很直白打开后是一个基于 MFC 的桌面程序里面除了.exe还有模型文件、DLL 和一堆自以为精简的依赖。标题里的“车型识别”和“车颜色识别”点明了这个系统的两个核心任务先检测画面里的汽车是哪一款再判断这辆车的车身颜色。这类需求常见于停车场管理、无人值守岗亭、监控稽查等场景难点往往不在算法本身而在于把 Python 里的模型推理包装成一个双击就能跑的 Windows 程序。MFC 在这里不是最时髦的选择却是交付现场最稳的选择不依赖 Python 环境、不要求目标机器装 CUDA用 C 编译出来后直接塞进 zip 就能分发。下面按“工程集成 → 车型推理 → 颜色分类 → 打包发布”四个阶段讲清楚新手能照做老手可以重点关注参数边界和打包排错。2. MFC 工程里集成推理引擎先让识别代码跑起来2.1 选型OpenCV 和 ONNX Runtime 怎么分工车型识别系统的算法链路通常是“图像预处理 → 检测模型 → 后处理 → 颜色判断”。在 C 环境里我选择用 OpenCV 做图像读取、缩放、颜色空间转换和画框用 ONNX Runtime 加载导出的检测模型。理由很实际OpenCV 的 DNN 模块也能跑 ONNX但 ONNX Runtime 在 CPU 上的线程调度更可控还能在推理时挂上 profiling方便排查哪一帧超时。OpenCV - cv::Mat, cv::dnn::blobFromImage, cv::resize, cv::cvtColor ONNX RT - Ort::Session, Run() 输出 float 数组 MFC - Picture 控件显示结果按钮触发 Capture/Detect这套分工保证界面线程不被推理阻塞。如果检测耗时超过 100ms我一般会把推理放到工作线程里再用PostMessage把结果回传给主线程刷新控件。直接在主线程跑推理窗口拖动时会卡顿现场体验很差。顺带说一个容易被坑的点OpenCV 默认按 BGR 读取图像ONNX Runtime 模型训练时通常用 RGB。你必须在blobFromImage里设置swapRBtrue否则车型识别准率会断崖式下跌。这不是模型问题是通道顺序错了。2.2 让控制台程序支持 MFC工程配置与依赖准备基于 MFC 的 Windows 应用最省事的起点是 Visual Studio 的 MFC 应用程序向导选“基于对话框”即可。但很多团队手头已经有控制台程序想把识别逻辑直接塞进 MFC 工程。这时先让控制台程序支持 MFC比迁移代码更高效。常见做法是复制原main.cpp中的算法函数到 MFC 对话框类的按钮回调里然后做三步配置项目属性 → 常规 → “使用 MFC”选“在共享 DLL 中使用 MFC”。项目属性 → C/C → 预处理器定义里加_AFXDLL。链接器 → 输入 → 附加依赖项里添加opencv_world480.lib、onnxruntime.libDebug 版本对应opencv_world480d.lib。如果你不想装完整 Visual Studio想让 VSCode 编译 MFC也能做用 VSCode 打开工程后配置tasks.json调用vcvars64.bat里导出的cl.exe和nmake编译。但 MFC 向导生成的资源文件.rc在 VSCode 里没有可视化编辑器改对话框布局很痛苦所以我只在纯算法调试时用 VSCode界面调整还是回到 Visual Studio。下面是一个最小 MFC 对话框按钮回调把图片文件加载成cv::Mat并显示到控件里void CCarDlg::OnBnClickedButtonOpen() { CFileDialog dlg(TRUE, NULL, _T(*.jpg), OFN_FILEMUSTEXIST, _T(Image Files (*.jpg;*.png;*.bmp)|*.jpg;*.png;*.bmp|All Files (*.*)|*.*||), this); if (dlg.DoModal() ! IDOK) return; CString path dlg.GetPathName(); cv::Mat mat cv::imread(CT2A(path.GetString())); if (mat.empty()) { AfxMessageBox(_T(img empty)); return; } // 将 Mat 转成 CImage 再贴到 Picture 控件 CImage image; MatToCImage(mat, image); CRect rect; m_pictureCtrl.GetClientRect(rect); CDC* dc m_pictureCtrl.GetDC(); image.StretchBlt(dc-m_hDC, 0, 0, rect.Width(), rect.Height(), SRCCOPY); m_pictureCtrl.ReleaseDC(dc); }CT2A用于把宽字符CString转成 UTF-8 传给 OpenCV。MatToCImage的逻辑是把cv::Mat逐通道拷贝到BITMAPINFO里这里省略了实现但要注意 BGR 到 RGB 的转换否则车辆涂装颜色会偏蓝。2.3 CImage 与 Mat 的转换从 MFC 控件到模型输入MFC 里拿图像有三种来源文件对话框、摄像头、视频文件。文件方式见上面代码摄像头和视频文件统一推荐cv::VideoCapturecv::VideoCapture cap; if (cap.open(0)) { // 0 表示默认摄像头 cv::Mat frame; cap frame; }但 MFC 的Picture控件需要的是CImage所以帧数据每次都要转一次。我封装了一个MatToCImage核心思路是创建CImage后用SetPixelAddress直接接管cv::Mat的内存避免拷贝。注意cv::Mat的step可能大于宽度乘以通道数CImage 的 pitch 也有对齐直接按行拷贝最稳。提示不要在每一帧里频繁new CImage否则内存碎片会让程序跑十分钟后崩掉。建议在对话框初始化时创建好 CImage后续只更新像素数据。模型推理前还有一步预处理cv::dnn::blobFromImage。输入尺寸要根据模型决定常见 YOLO 系列用 640x640cv::Mat blob cv::dnn::blobFromImage(mat, 1.0 / 255.0, cv::Size(640, 640), cv::Scalar(0, 0, 0), true, false);参数含义1.0/255.0做归一化Size(640,640)是模型输入分辨率Scalar是均值swapRBtrue是因为 OpenCV BGR 转模型需要的 RGB。把blob塞进 ONNX Runtime 的Run()后拿到输出数组下一章处理检测框。3. 车型识别模型的输出与后处理从张量到检测框3.1 YOLO/ONNX 输出张量布局检测模型导出成 ONNX 后输出张量通常是一个三维数组[1, 84, 8400]或者[1, 25200, 85]取决于导出代码里有没有转置。前一个维度是 batch size中间维度是“4 个框坐标 类别数”最后一个维度是候选框数量。车型识别如果是 COCO 的 80 类那就是4 80 84如果是自定义的轿车、SUV、卡车等 5 类中间维度就是4 5 9。拿到 ONNX Runtime 输出后要判断数据布局C 是通道维度 4 num_classes N 是候选框数量 如果输出 shape 是 (1, C, N) 坐标按列访问输出名称通常为 output0 如果输出 shape 是 (1, N, C) 坐标按行访问输出名称可能带 transpose我的习惯是在 C 里打印session.GetOutputTypeInfo()的 shape确认后再写解析代码。直接硬编码(1, 25200, 85)换一个模型就崩。3.2 解码检测框、置信度过滤与 NMSONNX 输出的坐标一般是归一化的cx, cy, w, h需要映射回原图尺寸。下面是一段可编译的 C 后处理核心代码使用 ONNX Runtime 输出到std::vectorfloatvoid DecodeOutput(const float* data, int numCandidates, int numClasses, float confThreshold, float iouThreshold, float scaleX, float scaleY, std::vectorcv::Rect boxes, std::vectorfloat scores, std::vectorint classIds) { std::vectorcv::Rect tmpBoxes; std::vectorfloat tmpScores; std::vectorint tmpIds; for (int i 0; i numCandidates; i) { const float* ptr data i * (4 numClasses); float objConf ptr[4]; // 如果模型没有单独 objectness则这里可能是类别分数 if (objConf confThreshold) continue; float cx ptr[0]; float cy ptr[1]; float w ptr[2]; float h ptr[3]; // 归一化坐标转换为原图像素坐标 int x static_castint((cx - w / 2) * scaleX); int y static_castint((cy - h / 2) * scaleY); int bw static_castint(w * scaleX); int bh static_castint(h * scaleY); tmpBoxes.push_back(cv::Rect(x, y, bw, bh)); tmpScores.push_back(objConf); tmpIds.push_back(0); // 如果做多类别这里需要读取类别分数 argmax } // NMS 抑制重叠框 std::vectorint keep; cv::dnn::NMSBoxes(tmpBoxes, tmpScores, confThreshold, iouThreshold, keep); boxes.clear(); scores.clear(); classIds.clear(); for (int idx : keep) { boxes.push_back(tmpBoxes[idx]); scores.push_back(tmpScores[idx]); classIds.push_back(tmpIds[idx]); } }注意上面代码我简化成了单类别检测实际车型识别需要从ptr[4classIndex]里取最大类别分数。参数scaleX和scaleY是原图宽高除以输入尺寸640。如果模型是矩形推理比如1280x736必须分别计算宽高比不能用同一个缩放值。NMSBoxes最后一个参数keep是保留框的索引iouThreshold通常设 0.45~0.5车挨得近时设 0.4 更稳。3.3 三个要调的核心参数conf_thres、iou_thres、输入尺寸车型识别系统调到什么参数取决于现场机位和视角。下表是我从多个项目里总结出的默认区间后续按场景微调参数默认值范围调参逻辑conf_thres0.450.25~0.6摄像头俯拍、车小降到 0.3闸机正面近景0.5 以上iou_thres0.450.35~0.6车辆重叠多时降比如排队堵车场景降到 0.4输入尺寸640x640320~1280模型支持范围越大越准但延迟线性增加类别分数argmax无多类别时取分数最高的类不要超过 conf_thres 的类别也画框swapRBtruefalse/true训练时用 RGB 图像则必须 true一个很容易踩的坑是把conf_thres同时用于“整框置信度”和“类别置信度”。我见过有同事用 YOLOv5 导出的 ONNX输出头里没有 objectness直接拿ptr[4]当整框置信度结果阳光强烈的场景下误检暴增。正确做法是先看模型文档输出头是五维还是四维。为了保险我在代码里加了一个开关用于决定第 4 位是否参与过滤。提示现场调试时把检测框和分数直接画到 Picture 控件上不要只看日志。肉眼确认漏检和误检的速度比调阈值快得多。4. 车颜色识别用 HSV 统计而不是训练一个分类网4.1 车身区域怎么取检测框内避开玻璃和影子颜色识别通常在车型检测框基础上做而不是把整张图喂给分类网络。原因很简单车身颜色受车顶、侧面、阴影、高光影响整图统计会被天空和路面污染。常见做法是把检测框的上半部分沿水平方向切出“车身带”。我的经验是取检测框左上角(x w * 0.1, y h * 0.15)宽度w * 0.8高度h * 0.3。这个区域覆盖车顶和前机盖的一部分避开了挡风玻璃和保险杠。夜间场景下这个区域偏黑需要额外开补光灯或用多帧融合不能只靠颜色判断。切出 ROI 后先做高斯模糊再用cv::cvtColor转到 HSV 色彩空间。HSV 比 RGB 更适合光照变化大的室外因为 V 分量受亮度影响S 和 H 相对稳定。4.2 常见车色的 HSV 范围表车身颜色不可能用一个精确点表示必须给每个色系一个 HSV 范围。下面是我在白天场景下使用的经验表颜色H 范围S 范围V 范围黑色0~1800~800~60白色0~1800~3060~255灰色0~1800~6060~180红色0~10 / 156~180100~25570~255橙色11~25100~25570~255黄色26~3480~25580~255绿色35~8060~25560~255蓝色81~12580~25560~255紫色126~15560~25560~255注意 H 在 OpenCV 里是 0~180不是 HSL 的 360。红色横跨 0 附近需要两个区间合并统计。白色和灰色、黑色之间的边界很容易混关键差异在 V 和 S 上所以把 V 阈值拆细。4.3 在 MFC 工程里实现颜色投票颜色判断我用投票法把 ROI 的 HSV 像素逐点分类统计每个色系的像素数最大者胜出。下面是可放进 MFC 类的成员函数CString CCarDlg::GetCarColor(const cv::Mat frame, const cv::Rect carRect) { cv::Mat roi frame(carRect); cv::Mat hsv; cv::cvtColor(roi, hsv, cv::COLOR_BGR2HSV); cv::GaussianBlur(hsv, hsv, cv::Size(5, 5), 0); int colorCounter[9] { 0 }; for (int row 0; row hsv.rows; row) { const cv::Vec3b* p hsv.ptrcv::Vec3b(row); for (int col 0; col hsv.cols; col) { int H p[col][0]; int S p[col][1]; int V p[col][2]; colorCounter[0] (S 30 V 60) ? 1 : 0; // 黑 colorCounter[1] (S 30 V 60) ? 1 : 0; // 白 colorCounter[2] (S 60 V 60 V 180) ? 1 : 0; // 灰 colorCounter[3] ((H 0 H 10) || (H 156)) S 100 ? 1 : 0; // 红 colorCounter[4] (H 26 H 34 S 80) ? 1 : 0; // 黄 colorCounter[5] (H 35 H 80 S 60) ? 1 : 0; // 绿 colorCounter[6] (H 81 H 125 S 80) ? 1 : 0; // 蓝 colorCounter[7] (H 126 H 155 S 60) ? 1 : 0; // 紫 colorCounter[8] (H 11 H 25 S 100) ? 1 : 0; // 橙 } } int maxIdx static_castint( std::max_element(colorCounter, colorCounter 9) - colorCounter); static const CString names[] { _T(黑色), _T(白色), _T(灰色), _T(红色), _T(黄色), _T(绿色), _T(蓝色), _T(紫色), _T(橙色) }; return names[maxIdx]; }这段代码遍历 ROI 每个像素手写条件判断避免用cv::inRange循环 9 次效率更高。参数S 30 V 60是黑色白色则 V 必须大于 60灰色单独用 S 和 V 夹逼。实际使用时白色车在阴天 V 可能只有 100所以 V 上限放宽到 255 没问题。下一步把颜色文字用DrawText画到检测框上方加上置信度一起显示。提示夜间场景建议先判断整帧平均亮度如果低于某个阈值直接返回“未知”不要强行把深蓝色识别成黑色。很多现场验收不合格都是颜色模块在夜班乱报造成的。5. 发布阶段把 MFC_windows.zip 做对并排掉三个典型坑5.1 依赖打包减少 DLL 地狱交付现场最头疼的是 exe 双击后提示缺少 DLL。用 MFC 动态库编译时依赖至少包括mfc140u.dll、vcruntime140.dll、msvcp140.dll再加上 OpenCV 和 ONNX Runtime 的 DLL。我不建议把这些 DLL 直接扔到C:\Windows\System32正确做法是在 zip 包里建一个bin目录exe 和 DLL 全放一起用SetDllDirectory或者启动时切换当前目录。打包前检查依赖可以使用 Visual Studio 自带的dumpbin /dependents命令dumpbin /dependents 车型识别.exe把输出里的 DLL 名复制出来再逐个检查这些 DLL 是否已在目标系统存在。OpenCV 的opencv_world480.dll和 ONNX Runtime 的onnxruntime.dll必须随包发布。模型文件放在models/子目录程序启动时用PathAppend拼接绝对路径不要假设当前工作目录是 exe 所在目录因为 MFC 程序可能从快捷方式启动。5.2 “此项目需要 MFC 库”的两种解决方式目标机器运行时报“此项目需要 MFC 库”本质是缺少对应版本的 MFC 运行库。解决方式有两种第一改项目为“在静态库中使用 MFC”这会让 exe 体积增加 10~20MB但分发时不需要带 MFC DLL。缺点是 ONNX Runtime 和 OpenCV 如果同时动态链接静态 MFC 可能导致 CRT 重复初始化需要确认两者都使用/MT。第二保留动态 MFC在打包 zip 时附带vc_redist.x64.exe或者从开发机拷贝mfc140u.dll到应用程序目录。我通常选第二种因为 ONNX Runtime 官方二进制已经依赖动态 CRT硬改成静态容易出诡异崩溃。检查 mfc140u.dll 是否缺失可以在 zip 解压后在命令行里where mfc140u.dll如果没输出说明系统里没有必须在安装包附带运行库。5.3 报错 error read zip archive 和启动闪退怎么查用户反馈“zip 解压失败报 error read zip archive”多半不是程序问题而是压缩包在传输过程中损坏或者下载工具提前中止。我一般让用户先看文件大小是否和发布时的说明一致。用 7-Zip 打开 zip如果能看到文件列表但解压时中途报错说明 zip 中心目录完好但某个分卷数据损坏手动跳过损坏文件重新解压。MFC 程序本身启动闪退先用事件查看器看 Windows 日志里应用程序错误的异常代码是不是0xc000007b。这个异常码表示 DLL 位数不匹配最常见原因是把 32 位版本的 OpenCV DLL 和 64 位 exe 混在一起。另一个高频原因是模型文件路径中包含中文ONNX Runtime 的Session::Session接收std::wstring时可以处理中文路径但如果你用的是std::string且系统代码页不是 UTF-8就会读不到模型而闪退。最后还有一个我每次发布都会验证的步骤把 zip 拷贝到一台只装原版 Windows 的虚拟机里用“解压到桌面”后直接双击运行不设任何环境变量和系统路径。这一步能过滤掉大多数“在我机器上好好的”问题。真正在目标机器排查时先用%PATH%里的依赖诊断工具确认 exe 加载了哪些 DLL再决定是补 DLL 还是换静态编译。把 zip 里的models路径写成相对路径并基于 exe 目录解析基本不会再出现识别功能正常但模型加载失败的情况。本文还有配套的精品资源点击获取
返回列表