从零打造高性能人体姿态检测系统:YOLOv8-Pose + ONNX Runtime 实战指南

whdahanh 发布于 2025-09-29 333 次阅读


在这个AI视觉技术快速发展的时代,人体姿态检测已经成为体感游戏、健身指导、安防监控等众多领域的核心技术。本文将带你深入了解如何使用YOLOv8-Pose、ONNX Runtime和OpenCV构建一个高性能的姿态检测系统,从原理到实现,从优化到部署,全方位解析这一前沿技术。

本项目已经上传到GitHub,复制https://github.com/Bayesianovich/yolov8-pose-ort到浏览器即可访问,欢迎star⭐⭐

🚀 为什么选择这套技术栈?

其实换成yolov11的onnx也能跑。

YOLOv8-Pose:姿态估计检测的经典模型

还记得早期的姿态检测需要先检测人体,再定位关键点的复杂流程吗?YOLOv8-Pose彻底改变了这一切。它将目标检测和关键点检测合并在单一网络中,不仅简化了流程,更在速度和精度上实现了质的飞跃。

关键优势:

  • 一网络双任务:同时完成人体检测和17个COCO标准关键点定位
  • 精度保证:在保持高速度的同时,检测精度达到业界领先水平

模型输出解析:

输出维度:[1, 56, 8400] 或 [1, 8400, 56]
├── 前4位:边界框 [center_x, center_y, width, height]
├── 第5位:置信度分数  
└── 后51位:17个关键点 [x1,y1,v1, x2,y2,v2, ..., x17,y17,v17]

ONNX Runtime:跨平台推理的完美选择

在选择推理引擎时,我们面临着性能、兼容性和部署便利性的三重考验。ONNX Runtime以其优异的表现成为了最佳选择:

  • 跨平台无忧:一次开发,Windows、Linux全覆盖
  • 硬件加速:CPU、GPU、NPU统一接口,轻松切换
  • 生产就绪:内置的图优化和内存管理,为生产环境量身定制

🛠️ ONNX Runtime部署姿态检测:实际项目实现详解

在我们的项目中,ONNX Runtime的部署采用了简洁高效的设计策略,专注于性能和易用性。让我们深入了解实际的部署实现。

项目部署架构

我们的部署架构强调简洁性和高效性:

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│  YOLOv8n-Pose   │    │  ONNX Runtime   │    │  检测器类       │
│   (.onnx)       │───▶│   CPU推理       │───▶│ (单一职责)      │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                              │
                       ┌─────────────────┐
                       │ 优化配置:       │
                       │ - 单线程推理     │
                       │ - 扩展图优化     │
                       │ - Arena内存     │
                       └─────────────────┘

1. 检测器类设计:专注核心功能

我们的YOLOv8PoseDetector类设计简洁而强大:

class YOLOv8PoseDetector {
private:
    // ONNX Runtime核心组件
    Ort::Env env;
    Ort::SessionOptions session_options;
    std::unique_ptr<Ort::Session> session;
    Ort::AllocatorWithDefaultOptions allocator;
    
    // 模型元数据
    std::vector<constchar*> input_names;
    std::vector<constchar*> output_names;
    std::vector<std::int64_t> input_dims;
    std::vector<std::int64_t> output_dims;
    
    // 可视化组件
    std::vector<std::string> class_names;
    std::vector<cv::Scalar> colors_table;
    
public:
    YOLOv8PoseDetector() : env(ORT_LOGGING_LEVEL_WARNING, "YOLOv8-Pose") {
        initializeColors();        // 初始化17色彩色表
        class_names = readClassNames();  // 加载类别名称
        initializeSession();       // 初始化ONNX Runtime会话
    }
    
    // 析构函数确保内存正确释放
    ~YOLOv8PoseDetector() {
        for (auto* name : input_names) {
            free(const_cast<char*>(name));
        }
        for (auto* name : output_names) {
            free(const_cast<char*>(name));
        }
    }
};

2. 会话初始化:优化的配置策略

我们采用了经过验证的最优配置:

void initializeSession() {
    try {
        // 核心配置:平衡性能和稳定性
        session_options.SetIntraOpNumThreads(1);  // 单线程避免上下文切换,只有图片和视频进行测试
        session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);
        
        // 模型加载:Windows路径处理
        std::wstring model_path_w(MODEL_PATH.begin(), MODEL_PATH.end());
        session = std::make_unique<Ort::Session>(env, model_path_w.c_str(), session_options);
        
        // 提取模型信息
        getModelInfo();
        
        std::cout << "YOLOv8 Pose model loaded successfully!" << std::endl;
    } catch (conststd::exception& e) {
        std::cerr << "Error initializing ONNX session: " << e.what() << std::endl;
        throw;
    }
}

配置要点解析:

  • 单线程推理:避免线程切换开销
  • 扩展图优化:平衡编译时间和运行性能
  • 异常处理:确保初始化失败时程序安全退出

3. 模型信息提取:动态适配机制

自动提取并验证模型信息,支持不同YOLOv8变体:

void getModelInfo() {
    // 提取输入信息
    size_t num_input_nodes = session->GetInputCount();
    input_names.resize(num_input_nodes);
    
    for (size_t i = 0; i < num_input_nodes; i++) {
        auto input_name = session->GetInputNameAllocated(i, allocator);
        std::string name_str(input_name.get());
        input_names[i] = strdup(name_str.c_str());  // 创建持久化副本
        
        Ort::TypeInfo input_type_info = session->GetInputTypeInfo(i);
        auto input_tensor_info = input_type_info.GetTensorTypeAndShapeInfo();
        input_dims = input_tensor_info.GetShape();
    }
    
    // 提取输出信息
    size_t num_output_nodes = session->GetOutputCount();
    output_names.resize(num_output_nodes);
    
    for (size_t i = 0; i < num_output_nodes; i++) {
        auto output_name = session->GetOutputNameAllocated(i, allocator);
        std::string name_str(output_name.get());
        output_names[i] = strdup(name_str.c_str());
        
        Ort::TypeInfo output_type_info = session->GetOutputTypeInfo(i);
        auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();
        output_dims = output_tensor_info.GetShape();
    }
    
    std::cout << "Model info - Input: " << input_names[0] 
              << ", Output: " << output_names[0] << std::endl;
}

4. 推理执行:高效的数据流处理

核心推理流程专注于性能和准确性:

cv::Mat detectPose(const cv::Mat& frame) {
    cv::Mat result_frame = frame.clone();
    
    try {
        // 步骤1:预处理 - Letterbox算法
        cv::Mat blob;
        float x_factor, y_factor;
        preprocessImage(frame, blob, x_factor, y_factor);
        
        // 步骤2:创建输入张量 - 零拷贝优化
        std::vector<int64_t> input_shape = {1, 3, INPUT_SIZE, INPUT_SIZE};
        auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
        Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
            memory_info, 
            (float*)blob.data,      // 直接使用OpenCV Mat数据
            blob.total(), 
            input_shape.data(), 
            input_shape.size()
        );
        
        // 步骤3:执行推理
        auto output_tensors = session->Run(
            Ort::RunOptions{nullptr}, 
            input_names.data(), 
            &input_tensor, 
            input_names.size(), 
            output_names.data(), 
            output_names.size()
        );
        
        // 步骤4:后处理
        float* pdata = output_tensors[0].GetTensorMutableData<float>();
        int out_feat = static_cast<int>(output_dims[2]);  // 56特征
        int out_box = static_cast<int>(output_dims[1]);   // 8400检测框
        
        std::vector<cv::Rect> boxes;
        std::vector<float> confidences;
        std::vector<cv::Mat> keypoints_data;
        
        postprocessResults(pdata, out_feat, out_box, x_factor, y_factor, 
                         boxes, confidences, keypoints_data, frame.cols, frame.rows);
        
        // 步骤5:NMS去重
        std::vector<int> indices;
        cv::dnn::NMSBoxes(boxes, confidences, CONFIDENCE_THRESHOLD, NMS_THRESHOLD, indices);
        
        // 步骤6:可视化结果
        renderResults(result_frame, boxes, confidences, keypoints_data, indices, x_factor, y_factor);
        
    } catch (conststd::exception& e) {
        std::cerr << "Error during pose detection: " << e.what() << std::endl;
    }
    
    return result_frame;
}

5. 后处理实现

我们的后处理专门针对56特征输出优化:

void postprocessResults(const float* pdata, int out_feat, int out_box, 
                       float x_factor, float y_factor,
                       std::vector<cv::Rect>& boxes,
                       std::vector<float>& confidences,
                       std::vector<cv::Mat>& keypoints) {
    
    cv::Mat detection_output = cv::Mat(out_box, out_feat, CV_32F, (float*)pdata);
    
    // 自动格式适配:处理[56,8400]和[8400,56]两种格式
    if (out_box == 56) {
        detection_output = detection_output.t();
        std::swap(out_box, out_feat);
    }
    
    // 遍历所有检测结果
    for(int i = 0; i < detection_output.rows; ++i) {
        float conf = detection_output.at<float>(i, 4);
        
        if(conf >= CONFIDENCE_THRESHOLD) {
            // 提取边界框(中心点格式转左上角格式)
            float cx = detection_output.at<float>(i, 0);
            float cy = detection_output.at<float>(i, 1);
            float bw = detection_output.at<float>(i, 2);
            float bh = detection_output.at<float>(i, 3);

            // 坐标还原到原图尺寸
            int left = static_cast<int>((cx - 0.5 * bw) * x_factor);
            int top = static_cast<int>((cy - 0.5 * bh) * y_factor);
            int width = static_cast<int>(bw * x_factor);
            int height = static_cast<int>(bh * y_factor);
            
            boxes.emplace_back(left, top, width, height);
            confidences.push_back(conf);

            // 提取关键点数据(固定51个值:17个关键点×3)
            cv::Mat keypoint_data = detection_output.row(i).colRange(5, 56).clone();
            keypoints.push_back(keypoint_data);
        }
    }
}

6. 可视化渲染:关键点和骨架绘制

实际的可视化实现:

void renderResults(cv::Mat& result_frame, 
                  const std::vector<cv::Rect>& boxes,
                  const std::vector<float>& confidences,
                  const std::vector<cv::Mat>& keypoints_data,
                  const std::vector<int>& indices,
                  float x_factor, float y_factor) {
    
    for (int i : indices) {
        // 绘制边界框
        cv::rectangle(result_frame, boxes[i], cv::Scalar(0, 255, 0), 2);
        
        // 绘制置信度
        std::string label = "Person: " + std::to_string(confidences[i]).substr(0, 4);
        cv::putText(result_frame, label, 
                   cv::Point(boxes[i].x, boxes[i].y - 10), 
                   cv::FONT_HERSHEY_SIMPLEX, 0.5, 
                   cv::Scalar(0, 255, 0), 2);
        
        // 处理关键点数据
        cv::Mat keypoints_raw = keypoints_data[i];
        cv::Mat keypoints = keypoints_raw.reshape(1, 17);  // 重塑为17x3
        cv::Mat scaled_keypoints(17, 3, CV_32F);
        
        // 关键点坐标变换到原图尺寸
        for (int j = 0; j < 17; ++j) {
            scaled_keypoints.at<float>(j, 0) = keypoints.at<float>(j, 0) * x_factor;
            scaled_keypoints.at<float>(j, 1) = keypoints.at<float>(j, 1) * y_factor;
            scaled_keypoints.at<float>(j, 2) = keypoints.at<float>(j, 2);
        }
        
        // 绘制姿态连接
        drawPoseConnections(result_frame, scaled_keypoints, colors_table);
    }
}

7. 实际配置参数

项目中使用的具体配置:

// 配置参数(在src/yolov8_ort_pose.cpp中定义)
const std::string MODEL_PATH = "G:\\yolov8-ort-pose\\yolov8n-pose.onnx";
const std::string IMAGE_PATH = "G:\\yolov8-ort-pose\\dance.png";
const float CONFIDENCE_THRESHOLD = 0.5f;  // 置信度阈值
const float NMS_THRESHOLD = 0.45f;        // NMS阈值
const int INPUT_SIZE = 640;               // 固定输入尺寸

8. 应用接口:图像和视频处理

项目提供了简洁的使用接口:

// 处理单张图像
void processImage(const std::string& input_path, const std::string& output_path);

// 处理视频文件
void processVideo(const std::string& input_path, const std::string& output_path);

// 主函数使用示例
int main(int argc, char* argv[]) {
    YOLOv8PoseDetector detector;
    
    if (argc == 1) {
        // 默认测试模式
        detector.processImage(IMAGE_PATH, "output/result.jpg");
    } elseif (argc == 3) {
        std::string mode = argv[1];
        std::string input_path = argv[2];
        
        if (mode == "-i") {
            detector.processImage(input_path, "output/result.jpg");
        } elseif (mode == "-v") {
            detector.processVideo(input_path, "output/result.mp4");
        }
    }
    
    return0;
}

项目特点总结

我们的ONNX Runtime部署实现具有以下特点:

  1. 简洁高效:专注核心功能,去除冗余组件
  2. 内存安全:正确的资源管理和异常处理
  3. 零拷贝:直接使用OpenCV Mat数据,避免不必要的内存拷贝
  4. 自动适配:智能处理不同的模型输出格式
  5. 易于使用:简单的命令行接口,支持图像和视频处理

这种实现方式在保证性能的同时,维持了代码的可读性和维护性,是实际生产环境的理想选择。

🔧 核心技术实现深度解析

1. 图像预处理:Letterbox

传统的图像缩放会导致人体比例失调,影响检测精度。我们采用的Letterbox算法巧妙地解决了这个问题:

void preprocessImage(const cv::Mat& frame, cv::Mat& blob, 
                    float& x_factor, float& y_factor) {
    // 计算最优缩放比例,保持宽高比
    float scale = std::min(static_cast<float>(INPUT_SIZE) / frame.cols, 
                          static_cast<float>(INPUT_SIZE) / frame.rows);
    
    // 等比例缩放
    int new_width = static_cast<int>(frame.cols * scale);
    int new_height = static_cast<int>(frame.rows * scale);
    
    cv::Mat scaled_image;
    cv::resize(frame, scaled_image, cv::Size(new_width, new_height));
    
    // 创建640x640标准画布,左上角对齐
    cv::Mat resized_image = cv::Mat::zeros(cv::Size(INPUT_SIZE, INPUT_SIZE), CV_8UC3);
    cv::Rect roi(0, 0, new_width, new_height);
    scaled_image.copyTo(resized_image(roi));
    
    // 记录缩放因子用于后续坐标还原
    x_factor = 1.0f / scale;
    y_factor = 1.0f / scale;
    
    // 转换为模型输入格式
    blob = cv::dnn::blobFromImage(resized_image, 1.0/255.0, 
                                cv::Size(INPUT_SIZE, INPUT_SIZE), 
                                cv::Scalar(), true, false);
}

核心思想:

  • 保持原图宽高比,避免人体变形
  • 使用黑色填充,模拟真实场景边缘
  • 左上角对齐,简化坐标转换逻辑

2. ONNX Runtime高性能推理配置

推理引擎的配置直接影响系统性能。由于没有加载多张图片和多个视频,最优配置如下:

// 环境初始化
Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "YOLOv8-Pose");

// 性能调优配置
Ort::SessionOptions session_options;
session_options.SetIntraOpNumThreads(1);  // 根据CPU核数调整
session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);

// 创建推理会话
std::wstring model_path_w(MODEL_PATH.begin(), MODEL_PATH.end());
session = std::make_unique<Ort::Session>(env, model_path_w.c_str(), session_options);

性能优化要点:

  • 线程配置:单线程推理避免上下文切换开销
  • 图优化:EXTENDED级别平衡优化效果和编译时间
  • 内存管理:Arena分配器减少内存碎片

3. 图像后处理:从原始输出到精确结果

模型的原始输出需要经过精心设计的后处理流程才能得到可用的检测结果:

void postprocessResults(const float* pdata, int out_feat, int out_box, 
                       float x_factor, float y_factor,
                       std::vector<cv::Rect>& boxes,
                       std::vector<float>& confidences,
                       std::vector<cv::Mat>& keypoints) {
    
    cv::Mat detection_output = cv::Mat(out_box, out_feat, CV_32F, (float*)pdata);
    
    // 自动格式适配 - 处理不同的模型输出格式
    if (out_box == 56) {
        detection_output = detection_output.t();
        std::swap(out_box, out_feat);
    }
    
    // 遍历所有检测结果
    for(int i = 0; i < detection_output.rows; ++i) {
        float conf = detection_output.at<float>(i, 4);
        
        if(conf >= CONFIDENCE_THRESHOLD) {
            // 提取边界框(中心点格式转左上角格式)
            float cx = detection_output.at<float>(i, 0);
            float cy = detection_output.at<float>(i, 1);
            float bw = detection_output.at<float>(i, 2);
            float bh = detection_output.at<float>(i, 3);

            // 坐标还原到原图尺寸
            int left = static_cast<int>((cx - 0.5 * bw) * x_factor);
            int top = static_cast<int>((cy - 0.5 * bh) * y_factor);
            int width = static_cast<int>(bw * x_factor);
            int height = static_cast<int>(bh * y_factor);
            
            boxes.emplace_back(left, top, width, height);
            confidences.push_back(conf);

            // 提取关键点数据(51个值:17个关键点×3)
            cv::Mat keypoint_data = detection_output.row(i).colRange(5, 56).clone();
            keypoints.push_back(keypoint_data);
        }
    }
}

设计亮点:

  • 自动格式适配:智能处理不同模型的输出格式
  • 早期筛选:置信度预筛选减少无效计算
  • 坐标精确还原:考虑预处理缩放因子的精确坐标转换

4. 解剖学准确的可视化渲染

最后一步是将检测结果以直观的方式展现出来。我们基于人体解剖学结构设计了科学的可视化方案:

void drawPoseConnections(cv::Mat& frame, const cv::Mat& keypoints,
                        const std::vector<cv::Scalar>& colors_tables) {
    
    // 基于人体解剖学的骨架连接定义
    std::vector<std::pair<int, int>> connections_pairs = {
        // 面部连接
        {0, 1}, {0, 2}, {1, 3}, {2, 4},
        // 躯干连接
        {5, 6}, {5, 11}, {6, 12}, {11, 12},
        // 左臂连接
        {5, 7}, {7, 9},
        // 右臂连接  
        {6, 8}, {8, 10},
        // 左腿连接
        {11, 13}, {13, 15},
        // 右腿连接
        {12, 14}, {14, 16}
    };
    
    constfloat visibility_threshold = 0.3f;
    
    // 先绘制骨架连接,再绘制关键点
    for (constauto& connection : connections_pairs) {
        int pt1_idx = connection.first;
        int pt2_idx = connection.second;
        
        float vis1 = keypoints.at<float>(pt1_idx, 2);
        float vis2 = keypoints.at<float>(pt2_idx, 2);
        
        if (vis1 > visibility_threshold && vis2 > visibility_threshold) {
            cv::Point pt1(keypoints.at<float>(pt1_idx, 0), keypoints.at<float>(pt1_idx, 1));
            cv::Point pt2(keypoints.at<float>(pt2_idx, 0), keypoints.at<float>(pt2_idx, 1));
            
            cv::line(frame, pt1, pt2, colors_tables[connection.first % colors_tables.size()], 
                    2, cv::LINE_AA);
        }
    }
}

🎨 Pose前后处理技术深度解析

姿态检测的精髓在于前后处理的精细化设计。让我们深入解析每个环节的技术细节和设计思路。

📐 前处理:从原始图像到模型输入

1. 图像预处理管道设计

我们的预处理流程专门针对人体姿态检测优化:

void preprocessImage(const cv::Mat& frame, cv::Mat& blob, float& x_factor, float& y_factor) {
    int original_width = frame.cols;
    int original_height = frame.rows;
    
    // 步骤1:计算保持宽高比的最佳缩放因子
    float scale = std::min(static_cast<float>(INPUT_SIZE) / original_width, 
                          static_cast<float>(INPUT_SIZE) / original_height);
    
    // 步骤2:计算缩放后的实际尺寸
    int new_width = static_cast<int>(original_width * scale);
    int new_height = static_cast<int>(original_height * scale);
    
    // 步骤3:高质量图像缩放
    cv::Mat scaled_image;
    cv::resize(frame, scaled_image, cv::Size(new_width, new_height), 0, 0, cv::INTER_LINEAR);
    
    // 步骤4:创建标准化画布(左上角对齐策略)
    cv::Mat resized_image = cv::Mat::zeros(cv::Size(INPUT_SIZE, INPUT_SIZE), CV_8UC3);
    cv::Rect roi(0, 0, new_width, new_height);
    scaled_image.copyTo(resized_image(roi));
    
    // 步骤5:记录坐标还原参数
    x_factor = 1.0f / scale;
    y_factor = 1.0f / scale;
    
    // 步骤6:转换为神经网络输入格式
    blob = cv::dnn::blobFromImage(
        resized_image,              // 输入图像
        1.0 / 255.0,               // 像素归一化:[0,255] -> [0,1]
        cv::Size(INPUT_SIZE, INPUT_SIZE), // 目标尺寸
        cv::Scalar(),              // 不进行均值减法
        true,                      // swapRB: BGR->RGB
        false                      // 不进行中心裁剪
    );
}

2. Letterbox算法的设计哲学

为什么选择左上角对齐?

原图(1920x1080)          Letterbox处理后(640x640)
┌─────────────────┐      ┌──────────┬─────────┐
│                 │      │ 缩放图像  │  黑色   │
│    人体图像      │ ──▶  │ 640x360  │  填充   │
│                 │      │          │  区域   │
└─────────────────┘      └──────────┴─────────┘
                        左上角对齐   简化坐标转换

技术优势分析:

  • 坐标转换简化:无需计算填充偏移量
  • 内存布局优化:连续的内存访问模式
  • 计算效率提升:减少坐标变换的复杂度
  • 边界处理友好:人体通常出现在图像上方

3. 数据格式转换详解

// OpenCV Mat格式转换为ONNX Runtime Tensor
std::vector<int64_t> input_shape = {1, 3, INPUT_SIZE, INPUT_SIZE};
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);

Ort::Value input_tensor = Ort::Value::CreateTensor<float>(
    memory_info,
    (float*)blob.data,          // 零拷贝数据传输
    blob.total(),               // 1*3*640*640 = 1,228,800个float
    input_shape.data(),
    input_shape.size()
);

内存布局转换:

OpenCV Mat (HWC): [Height][Width][Channels]
                 ↓ cv::dnn::blobFromImage
ONNX Tensor (NCHW): [Batch][Channels][Height][Width]

🔄 后处理:从模型输出到可视化结果

1. YOLOv8-Pose输出解析

模型输出格式深度分析:

// YOLOv8-Pose输出张量结构
输出形状: [1, 56, 8400] 或 [1, 8400, 56]

├── 维度0: Batch Size (固定为1)
├── 维度1/2: 特征维度(56) 和 检测框数量(8400)

特征56维详细结构:
├── [0-3]: 边界框坐标 [center_x, center_y, width, height]
├── [4]:   人体检测置信度 [0-1]
└── [5-55]: 17个COCO关键点 [x1,y1,v1, x2,y2,v2, ..., x17,y17,v17]
            其中 vi 为可见性分数 [0-1]

2. 输出格式适配

void postprocessResults(const float* pdata, int out_feat, int out_box, 
                       float x_factor, float y_factor,
                       std::vector<cv::Rect>& boxes,
                       std::vector<float>& confidences,
                       std::vector<cv::Mat>& keypoints,
                       int original_width, int original_height) {
    
    // 创建输出矩阵包装器
    cv::Mat detection_output = cv::Mat(out_box, out_feat, CV_32F, (float*)pdata);
    
    // 自动格式检测与转换
    if (out_box == 56) {
        // 检测到转置格式 [56, 8400] -> [8400, 56]
        detection_output = detection_output.t();
        std::swap(out_box, out_feat);
    }
    
    // 批量处理检测结果
    processDetections(detection_output, x_factor, y_factor, 
                     boxes, confidences, keypoints, 
                     original_width, original_height);
}

3. 检测结果处理

void processDetections(const cv::Mat& detection_output,
                      float x_factor, float y_factor,
                      std::vector<cv::Rect>& boxes,
                      std::vector<float>& confidences,
                      std::vector<cv::Mat>& keypoints,
                      int original_width, int original_height) {
    
    // 预分配容器容量,提升性能
    boxes.reserve(100);
    confidences.reserve(100);
    keypoints.reserve(100);
    
    for(int i = 0; i < detection_output.rows; ++i) {
        float confidence = detection_output.at<float>(i, 4);
        
        // 早期置信度筛选
        if(confidence < CONFIDENCE_THRESHOLD) continue;
        
        // 边界框坐标提取与转换
        float cx = detection_output.at<float>(i, 0);
        float cy = detection_output.at<float>(i, 1);
        float bw = detection_output.at<float>(i, 2);
        float bh = detection_output.at<float>(i, 3);
        
        // 中心点格式 -> 左上角格式 + 坐标还原
        int left = static_cast<int>((cx - 0.5f * bw) * x_factor);
        int top = static_cast<int>((cy - 0.5f * bh) * y_factor);
        int width = static_cast<int>(bw * x_factor);
        int height = static_cast<int>(bh * y_factor);
        
        // 边界检查与修正
        left = std::max(0, std::min(left, original_width - 1));
        top = std::max(0, std::min(top, original_height - 1));
        width = std::min(width, original_width - left);
        height = std::min(height, original_height - top);
        
        boxes.emplace_back(left, top, width, height);
        confidences.push_back(confidence);
        
        // 关键点数据提取
        extractKeypoints(detection_output, i, keypoints);
    }
}

4. 关键点数据处理与验证

void extractKeypoints(const cv::Mat& detection_output, int detection_idx, 
                     std::vector<cv::Mat>& keypoints) {
    // 提取51个关键点值 (17个关键点 × 3个属性)
    cv::Mat keypoint_data = detection_output.row(detection_idx).colRange(5, 56).clone();
    
    // 可选:关键点质量验证
    validateKeypointQuality(keypoint_data);
    
    keypoints.push_back(keypoint_data);
}

void validateKeypointQuality(cv::Mat& keypoint_data) {
    // 重塑为便于处理的格式 [17, 3]
    cv::Mat reshaped = keypoint_data.reshape(1, 17);
    
    for(int i = 0; i < 17; ++i) {
        float x = reshaped.at<float>(i, 0);
        float y = reshaped.at<float>(i, 1);
        float visibility = reshaped.at<float>(i, 2);
        
        // 坐标合理性检查
        if(x < 0 || x > INPUT_SIZE || y < 0 || y > INPUT_SIZE) {
            reshaped.at<float>(i, 2) = 0.0f;  // 标记为不可见
        }
        
        // 可见性值规范化
        visibility = std::max(0.0f, std::min(1.0f, visibility));
        reshaped.at<float>(i, 2) = visibility;
    }
}

5. NMS非极大值抑制

// 高效NMS处理
std::vector<int> performNMS(const std::vector<cv::Rect>& boxes,
                           const std::vector<float>& confidences) {
    std::vector<int> indices;
    
    // OpenCV优化的NMS实现
    cv::dnn::NMSBoxes(
        boxes,                    // 边界框列表
        confidences,             // 置信度列表
        CONFIDENCE_THRESHOLD,    // 置信度阈值 (0.5)
        NMS_THRESHOLD,          // IoU阈值 (0.45)
        indices,                // 输出:保留的索引
        1.0f,                   // eta参数(自适应阈值)
        0                       // top_k(0=不限制)
    );
    
    return indices;
}

6. 坐标变换与可视化

void renderPoseResults(cv::Mat& result_frame,
                      const std::vector<cv::Rect>& boxes,
                      const std::vector<float>& confidences,
                      const std::vector<cv::Mat>& keypoints_data,
                      const std::vector<int>& indices,
                      float x_factor, float y_factor) {
    
    for(int idx : indices) {
        // 绘制检测框
        drawBoundingBox(result_frame, boxes[idx], confidences[idx]);
        
        // 处理关键点可视化
        cv::Mat keypoints_raw = keypoints_data[idx];
        cv::Mat keypoints = keypoints_raw.reshape(1, 17);  // [17, 3]
        
        // 坐标变换
        cv::Mat scaled_keypoints = transformKeypoints(keypoints, x_factor, y_factor);
        
        // 绘制姿态骨架
        drawPoseSkeleton(result_frame, scaled_keypoints);
        
        // 绘制关键点
        drawKeypoints(result_frame, scaled_keypoints);
    }
}

cv::Mat transformKeypoints(const cv::Mat& keypoints, float x_factor, float y_factor) {
    cv::Mat scaled_keypoints(17, 3, CV_32F);
    
    for(int j = 0; j < 17; ++j) {
        // 坐标变换:模型空间 -> 原图空间
        scaled_keypoints.at<float>(j, 0) = keypoints.at<float>(j, 0) * x_factor;
        scaled_keypoints.at<float>(j, 1) = keypoints.at<float>(j, 1) * y_factor;
        scaled_keypoints.at<float>(j, 2) = keypoints.at<float>(j, 2);  // 可见性不变
    }
    
    return scaled_keypoints;
}

🎯 COCO-17关键点标准与可视化策略

1. 关键点语义定义

enum COCOKeypoints {
    NOSE = 0,           // 鼻子 - 面部中心参考点
    LEFT_EYE = 1,       // 左眼 - 视线方向判断
    RIGHT_EYE = 2,      // 右眼 - 视线方向判断
    LEFT_EAR = 3,       // 左耳 - 头部姿态
    RIGHT_EAR = 4,      // 右耳 - 头部姿态
    LEFT_SHOULDER = 5,  // 左肩 - 上肢起点
    RIGHT_SHOULDER = 6, // 右肩 - 上肢起点
    LEFT_ELBOW = 7,     // 左肘 - 关节点
    RIGHT_ELBOW = 8,    // 右肘 - 关节点
    LEFT_WRIST = 9,     // 左腕 - 手部起点
    RIGHT_WRIST = 10,   // 右腕 - 手部起点
    LEFT_HIP = 11,      // 左髋 - 下肢起点
    RIGHT_HIP = 12,     // 右髋 - 下肢起点
    LEFT_KNEE = 13,     // 左膝 - 关节点
    RIGHT_KNEE = 14,    // 右膝 - 关节点
    LEFT_ANKLE = 15,    // 左踝 - 脚部起点
    RIGHT_ANKLE = 16    // 右踝 - 脚部起点
};

2. 解剖学骨架连接拓扑

void drawPoseSkeleton(cv::Mat& frame, const cv::Mat& keypoints) {
    // 分组连接:按身体部位组织
    struct BodyPart {
        std::vector<std::pair<int, int>> connections;
        cv::Scalar color;
        int thickness;
    };
    
    std::vector<BodyPart> body_parts = {
        // 头部区域
        {{{NOSE, LEFT_EYE}, {NOSE, RIGHT_EYE}, {LEFT_EYE, LEFT_EAR}, {RIGHT_EYE, RIGHT_EAR}},
         cv::Scalar(255, 0, 0), 2},
        
        // 躯干核心
        {{{LEFT_SHOULDER, RIGHT_SHOULDER}, {LEFT_SHOULDER, LEFT_HIP}, 
          {RIGHT_SHOULDER, RIGHT_HIP}, {LEFT_HIP, RIGHT_HIP}},
         cv::Scalar(0, 255, 0), 3},
        
        // 左侧肢体
        {{{LEFT_SHOULDER, LEFT_ELBOW}, {LEFT_ELBOW, LEFT_WRIST},
          {LEFT_HIP, LEFT_KNEE}, {LEFT_KNEE, LEFT_ANKLE}},
         cv::Scalar(0, 0, 255), 2},
        
        // 右侧肢体
        {{{RIGHT_SHOULDER, RIGHT_ELBOW}, {RIGHT_ELBOW, RIGHT_WRIST},
          {RIGHT_HIP, RIGHT_KNEE}, {RIGHT_KNEE, RIGHT_ANKLE}},
         cv::Scalar(255, 255, 0), 2}
    };
    
    constfloat visibility_threshold = 0.3f;
    
    // 按部位分组绘制
    for(constauto& part : body_parts) {
        for(constauto& connection : part.connections) {
            drawConnection(frame, keypoints, connection.first, connection.second,
                         part.color, part.thickness, visibility_threshold);
        }
    }
}

3. 自适应可视化优化

void drawConnection(cv::Mat& frame, const cv::Mat& keypoints,
                   int pt1_idx, int pt2_idx, 
                   cv::Scalar color, int thickness,
                   float visibility_threshold) {
    
    float vis1 = keypoints.at<float>(pt1_idx, 2);
    float vis2 = keypoints.at<float>(pt2_idx, 2);
    
    if(vis1 > visibility_threshold && vis2 > visibility_threshold) {
        cv::Point pt1(static_cast<int>(keypoints.at<float>(pt1_idx, 0)),
                     static_cast<int>(keypoints.at<float>(pt1_idx, 1)));
        cv::Point pt2(static_cast<int>(keypoints.at<float>(pt2_idx, 0)),
                     static_cast<int>(keypoints.at<float>(pt2_idx, 1)));
        
        // 基于可见性调整颜色强度
        float avg_visibility = (vis1 + vis2) / 2.0f;
        cv::Scalar adjusted_color = color * avg_visibility;
        
        cv::line(frame, pt1, pt2, adjusted_color, thickness, cv::LINE_AA);
    }
}

通过这套精心设计的前后处理系统,我们实现了从原始图像到精确姿态可视化的完整流程,每个环节都经过性能和准确性的双重优化。

如果你觉得这篇文章对你有帮助,欢迎分享给更多的朋友。有任何问题或建议,也欢迎在评论区交流讨论!

相关链接:

[YOLOv8官方文档](https://docs.ultralytics.com/)

[ONNX Runtime文档](https://onnxruntime.ai/)



微信扫描下方的二维码阅读本文

此作者没有提供个人介绍
最后更新于 2025-09-29