文章目录
-
- 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
流程说明:
- 开始 :函数
JitterDetect
开始执行。 - 检查输入图像是否为空:如果输入图像为空,返回错误。
- 定义ROI区域:根据预设的偏移量和比例,定义感兴趣区域(ROI)。
- 裁剪图像到ROI:将输入图像和参考图像裁剪到指定的ROI区域。
- 检查图像是否需要缩放:如果图像宽度大于设定的最大宽度,进行缩放。
- 检测并计算关键点和描述子:使用SURF算法检测关键点并计算描述子。
- 检查关键点数量是否足够:如果关键点数量不足,返回错误。
- 匹配描述子:使用FLANN匹配器匹配两个图像的描述子。
- 检查匹配结果是否为空:如果没有找到匹配,返回错误。
- 排序匹配结果:将匹配结果按距离从小到大排序。
- 选择好的匹配点:选择距离最小的前N个匹配点。
- 提取匹配的关键点坐标:从好的匹配中提取对应的关键点坐标。
- 估计仿射变换矩阵:使用RANSAC算法估计图像间的仿射变换。
- 检查仿射变换矩阵是否有效:如果估计失败,返回错误。
- 提取平移量:从仿射变换矩阵中获取偏移量。
- 调整偏移量:根据图像缩放比例,调整偏移量。
- 返回成功状态:函数执行成功,返回0。