当我们将Python训练的YOLO模型部署到C++环境时,常遇到部分目标漏检问题。这通常源于预处理/后处理差异、数据类型隐式转换或模型转换误差。本文通过完整案例解析核心问题并提供可落地的解决方案。
预处理不一致
后处理阈值偏差
conf_thres=0.25
,C++端因数据类型转换实际变为0.2499
模型转换陷阱
转换方式 | 精度丢失风险 |
---|---|
ONNX导出 | 中 |
TensorRT引擎 | 高 |
直接权重迁移 | 极高 |
// 使用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 -> RGB
cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB);
return resized;
}
bool is_valid = (confidence > 0.25f - std::numeric_limits<float>::epsilon());
double calculate_iou(const Box& a, const Box& b) {
// 使用double避免浮点累积误差
}
逐层输出对比
测试用例固化
# Python保存测试数据
np.save("test_input.npy", image_tensor)
np.save("test_output.npy", model_output)
C++加载相同数据进行对比测试
C++后处理核心逻辑
#include "openvino_yolov5n.h"
#include
#include
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 boxes;
//std::vector classIds; // 存储原始类别ID
//std::vector 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(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 resize_and_pad(const cv::Mat& image, const cv::Size& new_shape)
//{
// cv::Size old_size = image.size();
// float ratio = static_cast(new_shape.width) / std::max(old_size.width, old_size.height);
// cv::Size new_size(static_cast(old_size.width * ratio), static_cast(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%的漏检问题:
经验提示:当出现漏检时,优先检查小目标(面积<32×32像素)的处理,其对数值误差最敏感。
部署完成后,建议使用COCO mAP指标验证,确保精度损失<0.5%。