从零实现高精度手势检测:OpenCV+MediaPipe模型移植实战(1)(附c++完整代码)

引言

在AIoT时代,手势交互正在重塑人机交互方式。从智能家居的隔空操控到AR/VR的沉浸式体验,再到无障碍交互系统,高精度手掌检测是构建下一代自然交互的基础能力。
传统图像处理方法在复杂场景下捉襟见肘,而基于深度学习的方法在精度和鲁棒性上实现了质的飞跃。本文将手把手带你实现工业级手掌检测系统。

准备阶段

  • opencv 4.x
  • MediaPipe 数据模型
  • vs2022

处理效果

在这里插入图片描述

处理流程

和常规图像处理方式一样,分为预处理,预测,后处理。

  • 预处理
  • 预测
  • 后处理

一、模型输入要求:让AI"看懂"手掌的秘诀

1.1 输入规范

  • 尺寸:192×192像素
  • 颜色空间:RGB格式
  • 数值范围:[0,1]浮点数
  • 通道顺序:NHWC(批次数×高度×宽度×通道)

1.2 预处理代码

/**
 * @brief 图像预处理流程
 * @param image 原始输入图像
 * @return 预处理后的blob和填充偏移量
 */
std::pair<cv::Mat, cv::Point2i> PalmDetector::preprocess(const cv::Mat& image) {
    cv::Point2i pad_bias(0, 0);  // 记录填充偏移量
    // 计算保持长宽比的缩放比例
    float ratio = std::min(static_cast<float>(input_size.width) / image.cols,
        static_cast<float>(input_size.height) / image.rows);

    cv::Mat processed_image;
    if (image.rows != input_size.height || image.cols != input_size.width) {
        // 按比例缩放图像
        cv::Size ratio_size(static_cast<int>(image.cols * ratio),
            static_cast<int>(image.rows * ratio));
        cv::resize(image, processed_image, ratio_size);

        // 计算需要填充的像素数
        int pad_h = input_size.height - ratio_size.height;
        int pad_w = input_size.width - ratio_size.width;
        pad_bias.x = pad_w / 2;  // 水平方向填充偏移
        pad_bias.y = pad_h / 2;   // 垂直方向填充偏移

        // 执行对称填充
        cv::copyMakeBorder(processed_image, processed_image,
            pad_bias.y, pad_h - pad_bias.y,
            pad_bias.x, pad_w - pad_bias.x,
            cv::BORDER_CONSTANT, cv::Scalar(0, 0, 0));
    }
    else {
        processed_image = image.clone();
    }

    // 将图像转换为模型输入的blob格式
    cv::Mat blob;
    cv::dnn::Image2BlobParams params;
    params.datalayout = cv::dnn::DNN_LAYOUT_NHWC;    // 通道最后排列
    params.ddepth = CV_32F;                          // 浮点型数据
    params.mean = cv::Scalar::all(0);                 // 不进行均值减法
    params.scalefactor = cv::Scalar::all(1.0 / 255.0);// 归一化到[0,1]
    params.size = input_size;                         // 输入尺寸
    params.swapRB = true;                             // BGR转RGB
    params.paddingmode = cv::dnn::DNN_PMODE_NULL;     // 无特殊填充

    blob = cv::dnn::blobFromImageWithParams(processed_image, params);

    // 将填充偏移量转换回原始图像坐标系
    pad_bias.x = static_cast<int>(pad_bias.x / ratio);
    pad_bias.y = static_cast<int>(pad_bias.y / ratio);

    return { blob, pad_bias };
}

1.3 预处理关键技术

在这里插入图片描述

二.模型预测

通过opencv dnn模块调用模型推理。

2.1 预测代码

/**
 * @brief 执行手掌检测推理
 * @param image 输入图像(BGR格式)
 * @return 检测结果向量,每个元素包含[x1,y1,x2,y2,landmarks...,score]
 */
std::vector<std::vector<float>> PalmDetector::infer(const cv::Mat& image) {
    // 图像预处理:缩放、填充、转换为blob
    std::pair<cv::Mat, cv::Point2i> preprocess_result = preprocess(image);
    cv::Mat preprocessed_image = preprocess_result.first;
    cv::Point2i pad_bias = preprocess_result.second;

    // 设置模型输入并执行前向推理
    model.setInput(preprocessed_image);
    std::vector<cv::Mat> outputs;
    model.forward(outputs, model.getUnconnectedOutLayersNames());

    // 后处理:解码输出,应用NMS,生成最终检测结果
    return postprocess(outputs, image.size(), pad_bias);
}

三、模型输出解析:从数字到物理世界的映射

3.1 模型输出数据格式

boxes_output	[2016, 18]	 //手掌边框4 + 7个手掌关键点
scores_output	[2016, 1]	 2016 个 置信度

3.2 后处理模型代码

/**
 * @brief 后处理模型输出
 * @param output_blobs 模型输出层数据
 * @param original_size 原始图像尺寸
 * @param pad_bias 预处理时的填充偏移量
 * @return 格式化后的检测结果
 */
std::vector<std::vector<float>> PalmDetector::postprocess(
    const std::vector<cv::Mat>& output_blobs,
    const cv::Size& original_size,
    const cv::Point2i& pad_bias) {

    // 解析模型输出:得分矩阵和边界框/关键点矩阵
    cv::Mat scores = output_blobs[1].reshape(1, output_blobs[1].total() / 1);
    cv::Mat boxes = output_blobs[0].reshape(1, output_blobs[0].total() / 18);

    std::vector<float> score_vec;                     // 有效检测得分
    std::vector<cv::Rect2f> boxes_vec;                // 检测框坐标
    std::vector<std::vector<cv::Point2f>> landmarks_vec; // 手掌关键点

    // 计算原始图像的缩放比例(预处理时保持长宽比)
    float scale = std::max(original_size.height, original_size.width);

    // 遍历所有候选检测结果
    for (int i = 0; i < scores.rows; i++) {
        // 计算sigmoid得分
        float score = 1.0f / (1.0f + std::exp(-scores.at<float>(i, 0)));

        // 提取当前检测的框偏移量和关键点偏移量
        cv::Mat box_delta = boxes.row(i).colRange(0, 4);
        cv::Mat landmark_delta = boxes.row(i).colRange(4, 18);
        cv::Point2f anchor = anchors[i];  // 当前锚点坐标

        // 解码边界框参数
        cv::Point2f cxy_delta(box_delta.at<float>(0) / input_size.width,
            box_delta.at<float>(1) / input_size.height);
        cv::Point2f wh_delta(box_delta.at<float>(2) / input_size.width,
            box_delta.at<float>(3) / input_size.height);

        // 计算实际图像坐标(考虑填充偏移)
        cv::Point2f xy1(
            (cxy_delta.x - wh_delta.x / 2 + anchor.x) * scale - pad_bias.x,
            (cxy_delta.y - wh_delta.y / 2 + anchor.y) * scale - pad_bias.y
        );
        cv::Point2f xy2(
            (cxy_delta.x + wh_delta.x / 2 + anchor.x) * scale - pad_bias.x,
            (cxy_delta.y + wh_delta.y / 2 + anchor.y) * scale - pad_bias.y
        );

        // 过滤低置信度检测
        if (score > score_threshold) {
            score_vec.push_back(score);
            boxes_vec.emplace_back(xy1.x, xy1.y, xy2.x - xy1.x, xy2.y - xy1.y);

            // 解码关键点坐标
            std::vector<cv::Point2f> landmarks;
            for (int j = 0; j < 7; j++) {  // 7个关键点
                float dx = landmark_delta.at<float>(j * 2) / input_size.width;
                float dy = landmark_delta.at<float>(j * 2 + 1) / input_size.height;
                dx += anchor.x;
                dy += anchor.y;
                // 转换到原始图像坐标系
                dx = dx * scale - pad_bias.x;
                dy = dy * scale - pad_bias.y;
                landmarks.emplace_back(dx, dy);
            }
            landmarks_vec.push_back(landmarks);
        }
    }

    // 应用非极大值抑制(NMS)
    std::vector<int> indices;
    std::vector<cv::Rect> boxes_int;  // NMSBoxes需要整数坐标
    for (const auto& box : boxes_vec) {
        boxes_int.emplace_back(static_cast<int>(box.x),
            static_cast<int>(box.y),
            static_cast<int>(box.width),
            static_cast<int>(box.height));
    }
    cv::dnn::NMSBoxes(boxes_int, score_vec, score_threshold,
        nms_threshold, indices);

    // 格式化输出结果:框坐标+关键点+得分
    std::vector<std::vector<float>> results;
    for (int idx : indices) {
        std::vector<float> result;
        const auto& box = boxes_vec[idx];
        // 添加框坐标(x1,y1,x2,y2)
        result.push_back(box.x);
        result.push_back(box.y);
        result.push_back(box.x + box.width);
        result.push_back(box.y + box.height);

        // 添加关键点坐标(x,y)*7
        for (const auto& point : landmarks_vec[idx]) {
            result.push_back(point.x);
            result.push_back(point.y);
        }

        // 添加置信度得分
        result.push_back(score_vec[idx]);
        results.push_back(result);
    }

    return results;
}

3.3 后处理输出格式

  • std::vector<std::vector>
    可拆分为std::vector<18个浮点数>
    其中18个浮点数含义:
    在这里插入图片描述

手掌示意图:
在这里插入图片描述

3.4 关键点解码流程:

  • 遍历7个关键点(每个点x,y坐标)
  • 应用与边界框相同的坐标转换逻辑
  • 使用Sigmoid函数处理置信度得分

四.参考以及源码

opencv zoom
源码地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_37160641

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值