问题背景

当我们将Python训练的YOLO模型部署到C++环境时,常遇到部分目标漏检问题。这通常源于预处理/后处理差异数据类型隐式转换模型转换误差。本文通过完整案例解析核心问题并提供可落地的解决方案。


一、常见原因分析
  1. 预处理不一致

    • Python常用OpenCV(BGR通道,归一化 [ 0 , 1 ] [0,1] [0,1]
    • C++可能误用其他库(如RGB通道,归一化 [ − 1 , 1 ] [-1,1] [1,1]
      差异值 = ∣ Python输出 C++输出 − 1 ∣ \text{差异值} = \left| \frac{\text{Python输出}}{\text{C++输出}} -1 \right| 差异值= C++输出Python输出1
  2. 后处理阈值偏差

    • Python端conf_thres=0.25,C++端因数据类型转换实际变为0.2499
    • IOU阈值计算中的浮点精度丢失
  3. 模型转换陷阱

    转换方式精度丢失风险
    ONNX导出
    TensorRT引擎
    直接权重迁移极高

二、关键解决方案
1. 强制预处理对齐(C++示例)
// 使用OpenCV确保与Python一致
cv::Mat preprocess(cv::Mat& img) {cv::Mat resized;cv::resize(img, resized, cv::Size(640, 640));  // YOLO输入尺寸resized.convertTo(resized, CV_32F, 1.0/255.0); // 归一化[0,1]// 通道顺序转换 BGR -> RGBcv::cvtColor(resized, resized, cv::COLOR_BGR2RGB);return resized;
}
2. 后处理精确控制
  • 阈值比较使用相对容差
    bool is_valid = (confidence > 0.25f - std::numeric_limits<float>::epsilon());
    
  • IOU计算改用双精度
    IOU = area intersect area union \text{IOU} = \frac{\text{area}_{\text{intersect}}}{\text{area}_{\text{union}}} IOU=areaunionareaintersect
    double calculate_iou(const Box& a, const Box& b) {// 使用double避免浮点累积误差
    }
    
3. 模型转换验证工具链
导出
导出
验证
转换
精度测试
Python模型
ONNX
.Bin .Xml
Netron可视化
TensorRT引擎
COCO API验证

三、调试技巧
  1. 逐层输出对比

    • 在Python/C++中分别输出第一个卷积层结果
    • 计算L1误差:
      Error = 1 n ∑ i = 1 n ∣ y py − y cpp ∣ \text{Error} = \frac{1}{n} \sum_{i=1}^{n} |y_{\text{py}} - y_{\text{cpp}}| Error=n1i=1nypyycpp
  2. 测试用例固化

    # Python保存测试数据
    np.save("test_input.npy", image_tensor)
    np.save("test_output.npy", model_output)
    

    C++加载相同数据进行对比测试


四、完整代码示例

C++后处理核心逻辑


#include "openvino_yolov5n.h"
#include <filesystem>
#include <fstream> OpenvinoModel::OpenvinoModel()
{core = ov::Core();//core.register_plugin("C:/openvino_windows_2025/runtime/bin/intel64/Releas/openvino_intel_gpu_plugin.dll", "GPU");
}
ov::InferRequest OpenvinoModel::init_model(const std::string& model_path, const std::string& weights_path)
{try {std::cout << "从: " << model_path << " 加载模型" << std::endl;// 加载模型model = core.read_model(model_path);// 保存第一个输出张量的名称main_output_name = model->outputs()[0].get_any_name();// 设置预处理ov::preprocess::PrePostProcessor ppp(model);// 输入设置 - 修改这部分auto& input = ppp.input();// 设置输入张量属性input.tensor().set_element_type(ov::element::f32).set_layout("NCHW")  // 直接使用 NCHW 布局.set_spatial_static_shape(640, 640);  // 设置固定的空间维度// 设置模型输入期望的布局input.model().set_layout("NCHW");// 构建预处理model = ppp.build();// 编译模型complied_model = core.compile_model(model, "CPU");std::cout << "模型编译成功。" << std::endl;// 创建推理请求infer_request = complied_model.create_infer_request();return infer_request;}catch (const ov::Exception& e) {std::cerr << "OpenVINO 错误: " << e.what() << std::endl;throw;}catch (const std::exception& e) {std::cerr << "错误: " << e.what() << std::endl;throw;}
}
void OpenvinoModel::infer(const ov::Tensor& data)
{infer_request.set_input_tensor(0, data);infer_request.infer();
}
std::vector<std::map<std::string, float>> nms_box(float* detectionResults, size_t detectionCount)
{const int NUM_CLASSES = 2;  // 明确指定类别数量const int DATA_PER_DETECTION = 5 + NUM_CLASSES;  // 7 = 4坐标 + 1置信度 + 2类别分数//std::vector<cv::Rect> boxes;//std::vector<int> classIds;  // 存储原始类别ID//std::vector<float> confidences;//const float min_width = 10.0f;//const float min_height = 10.0f;//const float max_ratio = 5.0f;//for (size_t i = 0; i < detectionCount; ++i)//{//    float* det = detectionResults + i * DATA_PER_DETECTION;//    float confidence = det[4];//    if (confidence >= CONFIDENCE_THRESHOLD)//    {//        // 关键修正:使用正确的类别数量//        cv::Mat classesScores(1, NUM_CLASSES, CV_32F, det + 5);//        cv::Point minLoc, maxLoc;//        double minVal, maxVal;//        cv::minMaxLoc(classesScores, &minVal, &maxVal, &minLoc, &maxLoc);//        int modelClass = maxLoc.x;//        float classScore = static_cast<float>(maxVal);//        // 使用最大分数进行阈值判断//        if (classScore > SCORE_THRESHOLD)//        {//            float x = det[0];//            float y = det[1];//            float w = det[2];//            float h = det[3];//            if (w < min_width || h < min_height) continue;//            float aspect_ratio = w / h;//            if (aspect_ratio > max_ratio || aspect_ratio < 1.0f / max_ratio) continue;//            if (x < 0.02f * 640 || y < 0.02f * 640) continue;//            float xmin = x - (w / 2);//            float ymin = y - (h / 2);//            boxes.emplace_back(xmin, ymin, w, h);//            confidences.push_back(confidence);//            classIds.push_back(modelClass);  // 保存原始类别ID//        }//    }//}std::vector<cv::Rect> boxes;std::vector<int> classIds;std::vector<float> confidences;  // 现在存储综合分数for (size_t i = 0; i < detectionCount; ++i) {float* det = detectionResults + i * DATA_PER_DETECTION;float confidence = det[4];cv::Mat classesScores(1, NUM_CLASSES, CV_32F, det + 5);cv::Point maxLoc;double maxVal;cv::minMaxLoc(classesScores, nullptr, &maxVal, nullptr, &maxLoc);float classScore = static_cast<float>(maxVal);float final_score = confidence * classScore;  // 综合分数//std::cout << final_score<< std::endl;if (final_score >= SCORE_THRESHOLD) {float x = det[0];float y = det[1];float w = det[2];float h = det[3];// 调试时暂时禁用额外过滤float xmin = x - w / 2;float ymin = y - h / 2;boxes.emplace_back(xmin, ymin, w, h);confidences.push_back(final_score);classIds.push_back(maxLoc.x);// 调试输出/*std::cout << "Kept: score=" << final_score << " class=" << maxLoc.x<< " xywh=[" << x << "," << y << "," << w << "," << h << "]\n";*/}}// 自定义标签映射std::vector<std::string> custom_labels = { "mark", "pool" };std::vector<int> indexes;cv::dnn::NMSBoxes(boxes, confidences, SCORE_THRESHOLD, NMS_THRESHOLD, indexes);std::vector<std::map<std::string, float>> ans;for (int index : indexes){int original_class_id = classIds[index];// 动态映射到自定义标签int mappedClass = (original_class_id < custom_labels.size()) ? original_class_id : 0;  // 越界时默认第一个类别std::map<std::string, float> detection;detection["class_index"] = static_cast<float>(mappedClass);detection["confidence"] = confidences[index];detection["box_xmin"] = static_cast<float>(boxes[index].x);detection["box_ymin"] = static_cast<float>(boxes[index].y);detection["box_w"] = static_cast<float>(boxes[index].width);detection["box_h"] = static_cast<float>(boxes[index].height);// 添加原始类别ID用于调试(可选)detection["original_class_id"] = static_cast<float>(original_class_id);ans.push_back(detection);}return ans;
}// 在nms_box函数后添加这个函数
std::vector<std::map<std::string, float>> transform_boxes(const std::vector<std::map<std::string, float>>& detections,int delta_w,int delta_h, float ratio, int orig_width, int orig_height)
{std::vector<std::map<std::string, float>> transformed;for (const auto& det : detections) {// 计算原始图像上的坐标(去除填充)float xmin = det.at("box_xmin");float ymin = det.at("box_ymin");float width = det.at("box_w");float height = det.at("box_h");// 去除填充xmin = std::max(0.0f, xmin);ymin = std::max(0.0f, ymin);width = std::min(width, static_cast<float>(640 - delta_w) - xmin);height = std::min(height, static_cast<float>(640 - delta_h) - ymin);// 缩放回原始尺寸xmin = xmin / ratio;ymin = ymin / ratio;width = width / ratio;height = height / ratio;// 确保不超出原始图像边界xmin = std::clamp(xmin, 0.0f, static_cast<float>(orig_width));ymin = std::clamp(ymin, 0.0f, static_cast<float>(orig_height));width = std::clamp(width, 0.0f, static_cast<float>(orig_width) - xmin);height = std::clamp(height, 0.0f, static_cast<float>(orig_height) - ymin);// 创建新的检测结果std::map<std::string, float> new_det = det;new_det["box_xmin"] = xmin;new_det["box_ymin"] = ymin;new_det["box_w"] = width;new_det["box_h"] = height;transformed.push_back(new_det);}return transformed;
}//std::tuple<cv::Mat, int, int> resize_and_pad(const cv::Mat& image, const cv::Size& new_shape)
//{
//    cv::Size old_size = image.size();
//    float ratio = static_cast<float>(new_shape.width) / std::max(old_size.width, old_size.height);
//    cv::Size new_size(static_cast<int>(old_size.width * ratio), static_cast<int>(old_size.height * ratio));
//    cv::Mat resized_image;
//    cv::resize(image, resized_image, new_size);
//    int delta_w = new_shape.width - new_size.width;
//    int delta_h = new_shape.height - new_size.height;
//    cv::Scalar color(100, 100, 100);
//    cv::Mat padded_image;
//    cv::copyMakeBorder(resized_image, padded_image, 0, delta_h, 0, delta_w, cv::BORDER_CONSTANT, color);
//    return std::make_tuple(padded_image, delta_w, delta_h);
//}
std::tuple<cv::Mat, int, int, float> resize_and_pad(const cv::Mat& image, const cv::Size& new_shape)
{cv::Size old_size = image.size();float ratio = static_cast<float>(new_shape.width) / std::max(old_size.width, old_size.height);cv::Size new_size(static_cast<int>(old_size.width * ratio), static_cast<int>(old_size.height * ratio));cv::Mat resized_image;cv::resize(image, resized_image, new_size);int delta_w = new_shape.width - new_size.width;int delta_h = new_shape.height - new_size.height;cv::Scalar color(100, 100, 100);cv::Mat padded_image;cv::copyMakeBorder(resized_image, padded_image, 0, delta_h, 0, delta_w, cv::BORDER_CONSTANT, color);return std::make_tuple(padded_image, delta_w, delta_h, ratio);  // 添加ratio到返回值
}

五、总结

通过以下关键步骤可解决90%的漏检问题:

  1. ✅ 预处理使用相同库和参数
  2. ✅ 后处理进行双精度计算
  3. ✅ 模型转换后逐层验证输出
  4. ✅ 建立跨语言测试数据基准

经验提示:当出现漏检时,优先检查小目标(面积<32×32像素)的处理,其对数值误差最敏感。

部署完成后,建议使用COCO mAP指标验证,确保精度损失<0.5%。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/86153.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/86153.shtml
英文地址,请注明出处:http://en.pswp.cn/bicheng/86153.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【2025CCF中国开源大会】开放注册与会议通知(第二轮)

点击蓝字 关注我们 CCF Opensource Development Committee 2025 CCF中国开源大会 由中国计算机学会主办的 2025 CCF中国开源大会&#xff08;CCF ChinaOSC&#xff09;拟于 2025年8月2日-3日 在上海召开。本届大会以“蓄势引领、众行致远”为主题&#xff0c;由上海交通大学校长…

本地聊天室

测试版还没测试过&#xff0c;后面更新不会继续开源&#xff0c;有问题自行修复 开发环境: PHP版本7.2 Swoole扩展 本地服务器环境&#xff08;如XAMPP、MAMP&#xff09; 功能说明: 注册/登录系统&#xff0c;支持本地用户数据存储 ​ 发送文本、图片和语音消息 ​ 实…

golang学习随便记x-调试与杂类(待续)

编译与调试 调试时从终端键盘输入 调试带有需要用户键盘输入的程序时&#xff0c;VSCode报错&#xff1a;Unable to process evaluate: debuggee is running&#xff0c;因为调试器不知道具体是哪个终端输入。需要配置启动文件 .vscode/launch.json 类似如下&#xff08;注意…

MultipartFile、File 和 Mat

1. MultipartFile (来自 Spring Web) 用途&#xff1a; 代表通过 multipart 形式提交&#xff08;通常是 HTTP POST 请求&#xff09;接收到的文件。 它是 Spring Web 中用于处理 Web 客户端文件上传的核心接口。 关键特性&#xff1a; 抽象&#xff1a; 这是一个接口&#xf…

.NET 9.0 SignalR 支持修剪和原生 AOT

什么是 SignalR&#xff1f; SignalR 是一个库&#xff0c;可用于向应用程序添加实时 Web 功能。它提供了一个简单的 API&#xff0c;用于创建可从服务器和客户端调用的服务器到客户端远程过程调用 (RPC)。现在&#xff0c;SignalR 在 .NET 8.0 和 .NET 9.0 中支持修剪和原生 …

下载资源管理

本文章仅用于作者管理自己的站内资源&#xff0c;方便日后查找&#xff0c;后续更新资源该文章持续更新。 1、环境安装 python3.11.11环境 python3.7.9 ARM.CMSIS.5.6.0(这个在站内重复上传了) Nordic8.32.1 java8 2、工具类软件安装包 2.1、蓝牙类 SI Connect 蓝牙OT…

​​FFmpeg命令全解析:三步完成视频合并、精准裁剪​​、英伟达显卡加速

一、裁剪 常规裁剪 根据时长裁剪&#xff0c;常规的裁剪 -c copy 表示直接复制流&#xff08;不重新编码&#xff09;&#xff0c;速度极快&#xff0c;但要求切割时间必须是关键帧。否则裁剪下来的画面开头/结尾 会模糊花屏 ffmpeg -i input.mp4 -ss 00:00:30 -to 00:01:00 …

HTML5 更新的功能

文章目录 前言**一、语义化标签&#xff08;Semantic Elements&#xff09;****二、多媒体支持&#xff08;Audio & Video&#xff09;****三、图形与绘图&#xff08;Canvas & SVG&#xff09;****1. <canvas>****2. SVG 内联支持** **四、表单增强&#xff08;…

React 全面入门与进阶实战教程

文章目录 一、认识 React1.1 核心特点 二、快速搭建 React 项目2.1 使用 Create React App2.2 使用 Vite 创建更轻量的 React 项目2.3 项目结构概览 三、React 核心语法基础3.1 JSX&#xff1a;React 的模板语法3.2 函数组件与 Props3.3 useState&#xff1a;定义响应式状态3.4…

牛津大学开源视频中的开放世界目标计数!

视频中的开放世界目标计数 GitHub PaPer Niki Amini-Naieni nikianrobots.ox.ac.uk Andrew Zisserman azrobots.ox.ac.uk 视觉几何组&#xff08;VGG&#xff09;&#xff0c;牛津大学&#xff0c;英国 ​ 图 1&#xff1a;视频中的目标计数&#xff1a;给定顶行的视频&#…

什么是Sentinel?以及优缺点

Sentinel 是阿里巴巴开源的分布式系统流量控制组件&#xff0c;主要用于服务限流、熔断降级、系统负载保护等场景&#xff0c;帮助提高微服务系统的稳定性和可靠性。它以流量为切入点&#xff0c;通过对流量的监控与控制&#xff0c;保障服务在高并发或异常情况下的可用性。 S…

2025 MWC 上海盛大开幕,聚焦AI、5G-Advanced及开放API

全球商业领袖与政策制定者齐聚一堂,共同探讨中国在API创新中的引领地位与产业发展势头 2025年6月18日,上海——GSMA 2025 MWC 上海今日在上海浦东嘉里大酒店举行开幕式,正式拉开帷幕。本届为期三天的盛会在上海新国际博览中心(SNIEC)举行,汇聚约400位演讲嘉宾与思想领袖,带来主…

使用Python脚本进行日常管理

在IT行业&#xff0c;特别是在系统运维领域&#xff0c;效率和准确性是至关重要的。随着技术的发展&#xff0c;手动处理大量的服务器和网络设备变得越来越不可行。因此&#xff0c;自动化运维成为了解决这一问题的有效手段。Python&#xff0c;作为一种广泛使用的编程语言&…

HCIA-数据通信基础

前言&#xff1a;本博客仅作记录学习使用&#xff0c;部分图片出自网络&#xff0c;如有侵犯您的权益&#xff0c;请联系删除 本篇笔记是根据B站上的视频教程整理而成&#xff0c;感谢UP主的精彩讲解&#xff01;如果需要了解更多细节&#xff0c;可以参考以下视频&#xff1a;…

安全版V4.5密码加密算法由SM3改为MD5

文章目录 环境文档用途详细信息 环境 系统平台&#xff1a;Linux x86-64 Red Hat Enterprise Linux 7 版本&#xff1a;4.5 文档用途 本文档用于指导瀚高数据库安全版V4.5的密码加密算法由SM3改为MD5 详细信息 1、用默认三权用户和普通用户登录数据库&#xff0c;修改密码…

MyBatis中#{}和${}的深度解析:SQL注入与动态拼接的终极抉择

MyBatis中#{}和${}的深度解析&#xff1a;SQL注入与动态拼接的终极抉择 摘要&#xff1a;在MyBatis的Mapper.xml文件中&#xff0c;#{}和${}这两个看似简单的符号&#xff0c;却隐藏着SQL安全与性能的核心秘密。本文将深入剖析它们的底层差异&#xff0c;并通过真实场景演示如何…

AWS多项目架构完全指南:基于App Runner的安全中转服务设计

引言:云原生架构的演进之路 在数字化转型浪潮中,企业常常面临这样的挑战:如何在保证安全隔离的前提下,快速为多个项目部署服务,并实现与现有系统的无缝集成?本文将以真实案例为基础,详细介绍如何利用AWS App Runner、Transit Gateway和VPC连接器等现代化服务,构建高可…

Selenium操作指南

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 大家好&#xff0c;今天带大家一起系统的学习下模拟浏览器运行库Selenium&#xff0c;它是一个用于Web自动化测试及爬虫应用的重要工具。 Selenium测试直接运行在…

基于Qt开发的ModbusTcp主站软件开发教程​——从协议原理到工业级实现

目录 第一章 环境配置与库集成1. 安装Qt与Modbus模块2. 第三方库兼容性(备选方案)第二章 Modbus TCP协议与Qt类解析1. 协议核心要点2. Qt关键类说明第三章 主站连接管理与通信初始化1. 连接建立与断线重连2. 请求超时与响应机制第四章 数据读写操作实战1. 读取保持寄存器(功…

什么是缺口箱线图?如何绘制?

大家好&#xff0c;我是带我去滑雪&#xff01; 箱线图是一种用于展示数据分布特征的统计图表&#xff0c;又称为盒状图或盒须图。它主要通过一个“箱子”和延伸出的“须”来展示一组数据的中位数、上下四分位数、最大值、最小值以及异常值。箱子的中线表示中位数&#xff0c;上…