C++基于opencv的视频质量检测--图像抖动检测

文章目录

    • 0.引言
    • [1. 原始代码分析](#1. 原始代码分析)
    • [2. 优化方案](#2. 优化方案)
    • [3. 优化后的代码](#3. 优化后的代码)
    • [4. 代码详细解读](#4. 代码详细解读)

0.引言

视频质量图像抖动检测已在C++基于opencv4的视频质量检测中有所介绍,本文将详细介绍其优化版本。

1. 原始代码分析

首先,我们来看图像抖动检测的原始代码:

cpp 复制代码
#include <algorithm>
#include <cmath>
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <vector>

namespace {
constexpr int kMinHessian = 400;
constexpr int kMinKeypoints = 20;
constexpr double kScaleWidth = 800.0;
constexpr double kInvalidReturn = -1.0;
constexpr double kDefaultMinDist = 100.0;
constexpr int kRoiYOffset = 5;
constexpr int kRoiHeightFactor = 3;
constexpr int kGoodMatchesCount = 50;
constexpr int kFilterThresholdFactor = 4;
constexpr int kFilterEndFactor = 3;
}  // namespace

/**
 * @brief 检测图像抖动的函数。
 * @param [in] srcImg 待检测的图像
 * @param [in] refImg 参考图像
 * @param [out] offsetX 待检测图像相对于参考图像在x轴上的偏移量
 * @param [out] offsetY 待检测图像相对于参考图像在y轴上的偏移量
 * @return 返回函数执行的状态
 * @retval 0 表示成功
 * @retval -1 表示失败
 */
int JitterDetect(const cv::Mat& srcImg, const cv::Mat& refImg, double& offsetX, double& offsetY) {
  if (srcImg.empty() || refImg.empty()) {
    return -1;
  }

  cv::Mat img = srcImg.clone();
  cv::Mat imgRef = refImg.clone();

  cv::Rect roi(0, img.rows / kRoiYOffset, img.cols, img.rows * kRoiHeightFactor / kRoiYOffset);
  img = img(roi);
  imgRef = imgRef(roi);

  double scale = 1.0;
  cv::Mat imgScaled, imgRefScaled;
  if (img.cols > kScaleWidth) {
    scale = kScaleWidth / static_cast<double>(img.cols);
    cv::resize(img, imgScaled, cv::Size(static_cast<int>(kScaleWidth), static_cast<int>(scale * img.rows)));
    cv::resize(imgRef, imgRefScaled, cv::Size(static_cast<int>(kScaleWidth), static_cast<int>(scale * img.rows)));
  } else {
    imgScaled = img;
    imgRefScaled = imgRef;
  }

  cv::Ptr<cv::xfeatures2d::SURF> detector = cv::xfeatures2d::SURF::create(kMinHessian);
  std::vector<cv::KeyPoint> keypoints1, keypoints2;

  detector->detect(imgScaled, keypoints1);
  detector->detect(imgRefScaled, keypoints2);
  if (keypoints1.size() < kMinKeypoints || keypoints2.size() < kMinKeypoints) {
    return -1;
  }

  cv::Ptr<cv::xfeatures2d::SURF> extractor = cv::xfeatures2d::SURF::create();
  cv::Mat descriptors1, descriptors2;
  extractor->compute(imgScaled, keypoints1, descriptors1);
  extractor->compute(imgRefScaled, keypoints2, descriptors2);

  cv::FlannBasedMatcher matcher;
  std::vector<cv::DMatch> matches;
  matcher.match(descriptors1, descriptors2, matches);

  double minDist = kDefaultMinDist;
  for (const auto& match : matches) {
    if (match.distance < minDist) {
      minDist = match.distance;
    }
  }

  std::vector<cv::DMatch> goodMatches;
  std::vector<float> distances(matches.size());
  for (size_t i = 0; i < matches.size(); ++i) {
    distances[i] = matches[i].distance;
  }
  std::sort(distances.begin(), distances.end());

  double distFlag =
      (matches.size() < kGoodMatchesCount) ? distances[matches.size() - 1] : distances[kGoodMatchesCount - 1];
  for (size_t i = 0, cntK = 0; i < matches.size() && cntK < kGoodMatchesCount; ++i) {
    if (matches[i].distance <= distFlag) {
      goodMatches.push_back(matches[i]);
      ++cntK;
    }
  }

  std::vector<float> moveX(goodMatches.size()), moveY(goodMatches.size());
  for (size_t i = 0; i < goodMatches.size(); ++i) {
    moveX[i] = std::abs(keypoints1[goodMatches[i].queryIdx].pt.x - keypoints2[goodMatches[i].trainIdx].pt.x);
    moveY[i] = std::abs(keypoints1[goodMatches[i].queryIdx].pt.y - keypoints2[goodMatches[i].trainIdx].pt.y);
  }

  std::sort(moveX.begin(), moveX.end());
  std::sort(moveY.begin(), moveY.end());

  for (size_t p = goodMatches.size() / kFilterThresholdFactor;
       p < goodMatches.size() * kFilterEndFactor / kFilterThresholdFactor; ++p) {
    offsetX += moveX[p];
    offsetY += moveY[p];
  }

  offsetX /= (goodMatches.size() / 2);
  offsetY /= (goodMatches.size() / 2);

  if (scale != 1.0) {
    offsetX *= scale;
    offsetY *= scale;
  }

  return 0;
}

以下是原始代码的核心步骤:

  • 图像预处理:对输入图像和参考图像进行ROI裁剪和缩放。
  • 特征检测与描述子计算:使用SURF算法检测关键点并计算描述子。
  • 特征匹配:使用FLANN匹配器匹配描述子。
  • 匹配点筛选:根据距离筛选好的匹配点。
  • 偏移量计算:手动计算匹配点之间的位移,得到图像的偏移量。

2. 优化方案

我们对代码进行如下优化:

  • 移除不必要的图像拷贝 :避免使用clone(),直接在ROI裁剪后的图像上进行操作,减少内存占用和拷贝时间。
  • 合并特征检测和描述子计算 :使用detectAndCompute方法,将特征检测和描述子计算合并,提高效率。
  • 简化特征匹配流程:通过排序匹配结果,直接选择距离最小的前N个匹配点,简化了匹配筛选过程。
  • 采用鲁棒的变换估计方法 :使用cv::estimateAffinePartial2D函数结合RANSAC算法,更准确地估计图像间的平移和旋转,增强鲁棒性。
  • 直接提取平移量:从估计的仿射变换矩阵中直接获取偏移量,避免手动计算的复杂性。

3. 优化后的代码

cpp 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/xfeatures2d.hpp>

namespace {
constexpr int kMinHessian = 400;
constexpr int kMinKeypoints = 20;
constexpr double kScaleWidth = 800.0;
constexpr int kRoiYOffset = 5;
constexpr int kRoiHeightFactor = 3;
}  // namespace

/**
 * @brief 检测图像抖动的函数。
 * @param [in] srcImg 待检测的图像
 * @param [in] refImg 参考图像
 * @param [out] offsetX 待检测图像相对于参考图像在x轴上的偏移量
 * @param [out] offsetY 待检测图像相对于参考图像在y轴上的偏移量
 * @return 返回函数执行的状态
 * @retval 0 表示成功
 * @retval -1 表示失败
 */
int JitterDetect(const cv::Mat& srcImg, const cv::Mat& refImg, double& offsetX, double& offsetY) {
    if (srcImg.empty() || refImg.empty()) {
        return -1;
    }

    // 定义ROI区域
    int roiY = srcImg.rows / kRoiYOffset;
    int roiHeight = srcImg.rows * kRoiHeightFactor / kRoiYOffset;
    cv::Rect roi(0, roiY, srcImg.cols, roiHeight);

    // 裁剪图像到ROI区域
    cv::Mat img = srcImg(roi);
    cv::Mat imgRef = refImg(roi);

    // 如果图像宽度大于设定值,则缩放图像
    double scale = 1.0;
    if (img.cols > kScaleWidth) {
        scale = kScaleWidth / static_cast<double>(img.cols);
        cv::resize(img, img, cv::Size(), scale, scale);
        cv::resize(imgRef, imgRef, cv::Size(), scale, scale);
    }

    // 初始化特征检测器和描述子提取器
    cv::Ptr<cv::xfeatures2d::SURF> detector = cv::xfeatures2d::SURF::create(kMinHessian);

    // 检测并计算关键点和描述子
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    cv::Mat descriptors1, descriptors2;
    detector->detectAndCompute(img, cv::noArray(), keypoints1, descriptors1);
    detector->detectAndCompute(imgRef, cv::noArray(), keypoints2, descriptors2);

    if (keypoints1.size() < kMinKeypoints || keypoints2.size() < kMinKeypoints) {
        return -1;
    }

    // 使用FLANN匹配器匹配描述子
    cv::FlannBasedMatcher matcher;
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1, descriptors2, matches);

    if (matches.empty()) {
        return -1;
    }

    // 按距离排序匹配结果
    std::sort(matches.begin(), matches.end(), [](const cv::DMatch& a, const cv::DMatch& b) {
        return a.distance < b.distance;
    });

    // 选择前N个好的匹配
    const int numGoodMatches = std::min(50, static_cast<int>(matches.size()));
    std::vector<cv::DMatch> goodMatches(matches.begin(), matches.begin() + numGoodMatches);

    // 提取匹配的关键点坐标
    std::vector<cv::Point2f> points1, points2;
    for (const auto& match : goodMatches) {
        points1.push_back(keypoints1[match.queryIdx].pt);
        points2.push_back(keypoints2[match.trainIdx].pt);
    }

    // 使用RANSAC估计仿射变换矩阵
    cv::Mat inliers;
    cv::Mat affine = cv::estimateAffinePartial2D(points1, points2, inliers, cv::RANSAC);

    if (affine.empty()) {
        return -1;
    }

    // 提取平移量
    offsetX = affine.at<double>(0, 2);
    offsetY = affine.at<double>(1, 2);

    // 根据缩放比例调整偏移量
    if (scale != 1.0) {
        offsetX /= scale;
        offsetY /= scale;
    }

    return 0;
}

4. 代码详细解读

是 否 是 否 否 是 是 否 是 否 开始 srcImg和refImg是否为空? 返回-1 定义ROI区域 裁剪图像到ROI 图像宽度是否大于设定值? 缩放图像 跳过缩放 检测并计算关键点和描述子 关键点数量是否足够? 返回-1 匹配描述子 匹配结果是否为空? 返回-1 按距离排序匹配结果 选择前N个好的匹配 提取匹配的关键点坐标 估计仿射变换矩阵 仿射变换矩阵是否为空? 返回-1 提取平移量 根据缩放比例调整偏移量 返回0

流程说明:

  1. 开始 :函数JitterDetect开始执行。
  2. 检查输入图像是否为空:如果输入图像为空,返回错误。
  3. 定义ROI区域:根据预设的偏移量和比例,定义感兴趣区域(ROI)。
  4. 裁剪图像到ROI:将输入图像和参考图像裁剪到指定的ROI区域。
  5. 检查图像是否需要缩放:如果图像宽度大于设定的最大宽度,进行缩放。
  6. 检测并计算关键点和描述子:使用SURF算法检测关键点并计算描述子。
  7. 检查关键点数量是否足够:如果关键点数量不足,返回错误。
  8. 匹配描述子:使用FLANN匹配器匹配两个图像的描述子。
  9. 检查匹配结果是否为空:如果没有找到匹配,返回错误。
  10. 排序匹配结果:将匹配结果按距离从小到大排序。
  11. 选择好的匹配点:选择距离最小的前N个匹配点。
  12. 提取匹配的关键点坐标:从好的匹配中提取对应的关键点坐标。
  13. 估计仿射变换矩阵:使用RANSAC算法估计图像间的仿射变换。
  14. 检查仿射变换矩阵是否有效:如果估计失败,返回错误。
  15. 提取平移量:从仿射变换矩阵中获取偏移量。
  16. 调整偏移量:根据图像缩放比例,调整偏移量。
  17. 返回成功状态:函数执行成功,返回0。
相关推荐
程序员-King.8 分钟前
2、桥接模式
c++·桥接模式
chnming198712 分钟前
STL关联式容器之map
开发语言·c++
程序伍六七25 分钟前
day16
开发语言·c++
小陈phd42 分钟前
Vscode LinuxC++环境配置
linux·c++·vscode
华清远见IT开放实验室44 分钟前
【每天学点AI】实战图像增强技术在人工智能图像处理中的应用
图像处理·人工智能·python·opencv·计算机视觉
火山口车神丶1 小时前
某车企ASW面试笔试题
c++·matlab
只怕自己不够好1 小时前
《OpenCV 图像缩放、翻转与变换全攻略:从基础操作到高级应用实战》
人工智能·opencv·计算机视觉
runing_an_min1 小时前
ffmpeg视频滤镜:替换部分帧-freezeframes
ffmpeg·音视频·freezeframes
是阿建吖!1 小时前
【优选算法】二分查找
c++·算法
Ajiang28247353043 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++