OpenCV实战(22)——单应性及其应用

news2024/11/23 18:26:55

OpenCV实战(22)——单应性及其应用

    • 0. 前言
    • 1. 单应性
      • 1.1 单应性基础
      • 1.2 计算两个图像之间的单应性
      • 1.3 完整代码
    • 2. 检测图像中的平面目标
      • 2.1 特征匹配
      • 2.2 完整代码
    • 小结
    • 系列链接

0. 前言

我们已经学习了如何从一组匹配项中计算图像对的基本矩阵。在射影几何中,还存在另一个非常有用的数学实体——单应性,可以利用多视图图像计算,它是一个具有特殊属性的矩阵,具有重要用途。

1. 单应性

1.1 单应性基础

我们已经学习了 3D 点与其在相机上的图像之间的投影关系,并且了解了投影方程使用相机的内在属性和相机位置(用旋转和平移分量指定)将 3D 点与其图像相关联。如果我们仔细检查投影方程,就会意识到存在两种有趣的特殊情况。第一种情况是场景的两个视图仅仅是由相机旋转得到的,则外部矩阵的第四列将全为 0 (即平移为空),例如,通过自身旋转拍摄建筑物或风景时,就会出现这种情况,由于相机离被摄主体足够远,因此移动部分可以忽略不计:
S [ x y 1 ] = [ f 0 0 0 f 0 0 0 1 ] [ r 1 r 2 r 3 0 r 4 r 5 r 6 0 r 7 r 8 r 9 0 ] [ X Y Z 1 ] S\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]=\left[ \begin{array}{ccc} f&0&0\\ 0&f&0\\ 0&0&1\\\end{array}\right]\left[ \begin{array}{ccc} r_1&r_2&r_3&0\\ r_4&r_5&r_6&0\\ r_7&r_8&r_9&0\\\end{array}\right]\left[ \begin{array}{ccc} X\\ Y\\ Z\\ 1\\\end{array}\right] S xy1 = f000f0001 r1r4r7r2r5r8r3r6r9000 XYZ1
这种特殊情况下的投影关系变成了 3x3 矩阵。当被拍摄到的物体是平面时,也会出现类似情况,在这种特定情况下,我们可以假设该平面上的点均位于 Z = 0 Z=0 Z=0 上,不失一般性,我们可以得到以下等式:
S [ x y 1 ] = [ f 0 0 0 f 0 0 0 1 ] [ r 1 r 2 r 3 t 1 r 4 r 5 r 6 t 2 r 7 r 8 r 9 t 3 ] [ X Y 0 1 ] S\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right]=\left[ \begin{array}{ccc} f&0&0\\ 0&f&0\\ 0&0&1\\\end{array}\right]\left[ \begin{array}{ccc} r_1&r_2&r_3&t_1\\ r_4&r_5&r_6&t_2\\ r_7&r_8&r_9&t_3\\\end{array}\right]\left[ \begin{array}{ccc} X\\ Y\\ 0\\ 1\\\end{array}\right] S xy1 = f000f0001 r1r4r7r2r5r8r3r6r9t1t2t3 XY01
场景点的零坐标将导致投影矩阵的第三列为 0,投影矩阵最终变为 3x3 矩阵。这个特殊的矩阵称为单应性,它意味着在特殊情况下(在本节中指纯旋转或平面对象),一个点与其图像通过以下形式的线性关系相关联:
S [ x ′ y ′ 1 ] = H [ x y 1 ] S\left[ \begin{array}{ccc} x'\\ y'\\ 1\\\end{array}\right]=H\left[ \begin{array}{ccc} x\\ y\\ 1\\\end{array}\right] S xy1 =H xy1
其中, H H H 是一个 3x3 矩阵,标量值 S S S 表示比例因子。得到矩阵 H H H 的估计值后,就可以使用这种关系将一个视图中的点转移到第二个视图。需要注意的是,在单应关系下基本矩阵将并不确定。

1.2 计算两个图像之间的单应性

假设我们有两张仅由旋转相机得到的图像。可以使用选择的特征和 cv::BFMatcher 函数来匹配这两个图像,然后应用随机抽样一致算法 (RANdom SAmple Consensus, RANSAC) 步骤,最后使用基于匹配集(包含大量异常值)的单应性估计。

(1) 通过使用 cv::findHomography 函数进行单应性估计,该函数与 cv::findFundamentalMat 函数相似:

// 查找图像 1 和图像 2 间的单应性
std::vector<char> inliers;
cv::Mat homography= cv::findHomography(
        points1, points2,   // 对应点
        inliers,            // inliers 匹配 
        cv::RANSAC,         // RANSAC 方法
        1.);                // 投影点最大距离

(2) 本节使用的两张图像是通过旋转相机得到的,两张图像如下所示,我们还显示了由函数的 inliers 参数标识的正确关键点:

示例图像

(3) 使用循环在以上图像上绘制符合所发现单应性的最终正确值:

std::vector<cv::Point2f>::const_iterator itPts = points1.begin();
std::vector<cv::Point2f>::const_iterator itIn = inliers.begin();
while (itPtr!=points1.end()) {
    if (*itIn) cv::circle(image1, *itPtr, 3, cv::Scalar(255, 255, 255));
    ++itPts;
    ++itIn;
}

(4) 单应性是一个 3×3 的可逆矩阵,因此,一旦计算完成,就可以将图像点从一个图像转移到另一个图像。可以对图像的每个像素执行此操作,以完整转移图像。这个过程称为图像拼接,通常用于根据多个图像构建全景图,可以使用 cv::warpPerspective 函数执行此操作:

// 拼接图像 1 和图像 2
cv::Mat result;
cv::warpPerspective(image1,                     // 输入图像
        result,                                 // 结果
        homography,                             // 单应性
        cv::Size(2*image1.cols, image1.rows));  // 结果图像尺寸

(5) 由于现在这两个图像来自同一个角度,获得新图像后,可以将其附加到另一张图像以扩展视图:

cv::Mat half(result, cv::Rect(0, 0, image2.cols, image2.rows));
image2.copyTo(half);

结果如下图所示:

拼接结果

当两个视图通过单应性相关时,可以确定一个图像上的给定场景点在另一幅图像上的位置。由于第二个视图显示了在第一个图像中不可见的场景的一部分,因此可以使用单应性通过另一个图像中的值来扩展图像。这就是我们创建一个新图像的核心思想,它扩展了第一张图的可视角度,其在右侧增加了额外的场景像素值。
cv::findHomography 计算的单应性是将第一个图像中的点映射到第二个图像中的点。这个单应性最少可以由四个匹配计算,这里同样使用 RANSAC 算法,一旦找到具有最佳支持度的单应性,cv::findHomography 方法将使用所有识别的正确值对其进行细化。
为了将图像 1 中的点转移到图像 2 ,我们实际上需要的是逆单应性,可以通过调用cv::warpPerspective 函数完成,它使用作为输入提供的单应性的逆来获得输出图像每个点的颜色值。当一个输出像素被转移到输入图像之外时,分配给这个像素黑色值 (0)。需要注意的是,如果想在像素传输过程中直接使用单应性而非逆单应性,则可以使用 cv::WARP_INVERSE_MAP 标志作为第 5 个参数调用 cv::warpPerspective 函数。

1.3 完整代码

完整代码 estimateH.cpp 如下所示:

#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include <opencv2/stitching.hpp>

int main() {
    // 读取输入图像
    cv::Mat image1= cv::imread("4.png",0);
    cv::Mat image2= cv::imread("5.png",0);
    if (!image1.data || !image2.data)
        return 0; 
    cv::namedWindow("Image 1");
    cv::imshow("Image 1",image1);
    cv::namedWindow("Image 2");
    cv::imshow("Image 2",image2);
    // 关键点和描述符向量
    std::vector<cv::KeyPoint> keypoints1;
    std::vector<cv::KeyPoint> keypoints2;
    cv::Mat descriptors1, descriptors2;
    // 1. 构建 SIFT 特征描述符 
    cv::Ptr<cv::Feature2D> ptrFeature2D = cv::xfeatures2d::SIFT::create(74);
    // 2. 探测 SIFT 特征及其描述符
    ptrFeature2D->detectAndCompute(image1, cv::noArray(), keypoints1, descriptors1);
    ptrFeature2D->detectAndCompute(image2, cv::noArray(), keypoints2, descriptors2);
    std::cout << "Number of feature points (1): " << keypoints1.size() << std::endl;
    std::cout << "Number of feature points (2): " << keypoints2.size() << std::endl;
    // 3. 匹配两种图像描述符 
    cv::BFMatcher matcher(cv::NORM_L2, true);                            
    std::vector<cv::DMatch> matches;
    matcher.match(descriptors1,descriptors2,matches);
    // 绘制匹配
    cv::Mat imageMatches;
    cv::drawMatches(image1,keypoints1,  // 第一张图像及其关键点
                    image2,keypoints2,  // 第二张图像及其关键点
                    matches,			// 匹配
                    imageMatches,		// 结果图像
                    cv::Scalar(255,255,255),  // 线条颜色
                    cv::Scalar(255,255,255)/*,// 关键点颜色
                    std::vector<char>(),
                    2*/); 
    cv::namedWindow("Matches (pure rotation case)");
    cv::imshow("Matches (pure rotation case)",imageMatches);
    // 将关键点转换为 Point2f
    std::vector<cv::Point2f> points1, points2;
    for (std::vector<cv::DMatch>::const_iterator it= matches.begin();
            it!= matches.end(); ++it) {
                // 获取左侧图像关键点
                float x= keypoints1[it->queryIdx].pt.x;
                float y= keypoints1[it->queryIdx].pt.y;
                points1.push_back(cv::Point2f(x,y));
                // 获取右侧图像关键点
                x= keypoints2[it->trainIdx].pt.x;
                y= keypoints2[it->trainIdx].pt.y;
                points2.push_back(cv::Point2f(x,y));
    }
    std::cout << points1.size() << " " << points2.size() << std::endl;
    // 查找图像 1 和图像 2 间的单应性
    std::vector<char> inliers;
    cv::Mat homography= cv::findHomography(
            points1, points2,   // 对应点
            inliers,            // inliers 匹配 
            cv::RANSAC,         // RANSAC 方法
            1.);                // 投影点最大距离
    // 绘制 inliers 点
    cv::drawMatches(image1, keypoints1, // 第一张图及其关键点
            image2, keypoints2,         // 第二张图像及其关键点
            matches,                    // 匹配
            imageMatches,               // 结果图像
            cv::Scalar(255, 255, 255),  // 线条颜色
            cv::Scalar(255, 255, 255)/*,// 关键点颜色
            inliers,
            2*/);
    cv::namedWindow("Homography inlier points");
    cv::imshow("Homography inlier points", imageMatches);
    // 拼接图像 1 和图像 2
    cv::Mat result;
    cv::warpPerspective(image1,                     // 输入图像
            result,                                 // 结果
            homography,                             // 单应性
            cv::Size(2*image1.cols, image1.rows));  // 结果图像尺寸
    cv::Mat half(result, cv::Rect(0, 0, image2.cols, image2.rows));
    image2.copyTo(half);
    cv::namedWindow("Image mosaic");
    cv::imshow("Image mosaic",result);
    // 读取输入图像
    std::vector<cv::Mat> images;
    images.push_back(cv::imread("4.png"));
    images.push_back(cv::imread("5.png"));
    cv::Mat panorama;   // 输出全景图相
    // 创建拼接
    cv::Ptr<cv::Stitcher> stitcher = cv::Stitcher::create();
    // 拼接图像
    cv::Stitcher::Status status = stitcher->stitch(images, panorama);
    if (status == cv::Stitcher::OK){
        cv::namedWindow("Panorama");
        cv::imshow("Panorama", panorama);
    }
    cv::waitKey();
    return 0;
}

2. 检测图像中的平面目标

一个平面的两个图像之间也存在单应性,我们可以利用它来识别图像中的平面物体。在上一小节中,我们介绍了如何使用单应性将通过相机旋转拍摄的图像拼接在一起以创建全景图,我们也还了解到平面的不同图像也会产生单应性,接下来,我们将学习如何利用这一原理识别图像中的平面对象。
假设我们要检测图像中的平面对象,例如海报、绘画、标牌、书籍封面等。为了检测平面对象,需要检测该对象上的特征点并尝试将它们与图像中的特征点进行匹配。然后将使用类似于特征匹配中使用的匹配方案来验证这些匹配,不同的是,本节的匹配方案基于单应性。

2.1 特征匹配

(1) 定义一个与 RobustMatcher 类相似的 TargetMatcher 类:

class TargetMatcher {
    private:
        // 指向特征点检测器对象的指针
        cv::Ptr<cv::FeatureDetector> detector;
        // 指向特征描述符提取器对象的指针
        cv::Ptr<cv::DescriptorExtractor> descriptor;
        cv::Mat target;     // 目标图像
        int normType;
        double distance;    // 最小重投影误差
    public:
        TargetMatcher(const cv::Ptr<cv::FeatureDetector> &detector,
                    const cv::Ptr<cv::DescriptorExtractor> &descriptor = cv::Ptr<cv::DescriptorExtractor>()) : 
            detector(detector), descriptor(descriptor), 
            normType(cv::NORM_L2), distance(1.0) {
            if (!this->descriptor) {
                this->descriptor = this->detector;
            }
        }

(2) 添加 target 属性,表示要匹配的平面物体的参考图像。匹配方法与 RobustMatcher 类的方法相同,只是在 ransacTest 方法中使用 cv::findHomography 而非 cv::findFundamentalMat。我们还添加了一个用于目标匹配并找到目标位置的方法:

// 检测图像中的平面目标
cv::Mat detectTarget(const cv::Mat& image,
                std::vector<cv::Point2f>& detectedCorners,
                std::vector<cv::DMatch>& matches,
                std::vector<cv::KeyPoint>& keypoints1,
                std::vector<cv::KeyPoint>& keypoints2) {
    // 1. 检测图像关键点
    cv::Mat homography = match(target, image, matches, keypoints1, keypoints2);
    std::vector<cv::Point2f> corners;
    corners.push_back(cv::Point2f(0,0));
    corners.push_back(cv::Point2f(target.cols-1,0));
    corners.push_back(cv::Point2f(target.cols-1,target.rows-1));
    corners.push_back(cv::Point2f(0,target.rows-1));
    cv::perspectiveTransform(corners, detectedCorners, homography);
    return homography;
}

(3) 通过 match 方法得到单应性后,定义目标的四个角(即其参考图像的四个角)。然后使用 cv::perspectiveTransform 函数将它们传递到其他图像,该函数将输入向量中的每个点乘以单应性矩阵可以得到在其他图像中这些点的坐标。然后执行目标匹配:

    // 准备匹配器
    TargetMatcher tmatcher(cv::FastFeatureDetector::create(10),cv::BRISK::create());
    tmatcher.setNormType(cv::NORM_HAMMING);
    // 定义输出数据
    std::vector<cv::DMatch> matches;
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    std::vector<cv::Point2f> corners;
    // 设置目标图像
    tmatcher.setTarget(target);
    // 使用目标匹配图像
    tmatcher.detectTarget(image, corners, matches, keypoints1, keypoints2);
    // 绘制目标角点
    cv::Point pt= cv::Point(corners[0]);
    cv::line(image,cv::Point(corners[0]), cv::Point(corners[1]), cv::Scalar(255,255,255),3);
    cv::line(image,cv::Point(corners[1]), cv::Point(corners[2]), cv::Scalar(255,255,255),3);
    cv::line(image,cv::Point(corners[2]), cv::Point(corners[3]), cv::Scalar(255,255,255),3);
    cv::line(image,cv::Point(corners[3]), cv::Point(corners[0]), cv::Scalar(255,255,255),3);

使用 cv::drawMatches 函数,结果如下所示:

匹配结果
我们还可以利用单应性来修改平面对象的透视效果。例如,如果我们有几张从建筑物不同角度拍摄的图片,可以计算这些图像之间的单应性,并通过将图像围在一起组合拼接成立面图像。计算单应性至少需要两个视图之间的四个匹配点,cv::getPerspectiveTransform 函数可以根据四个对应点计算变换公式。

2.2 完整代码

头文件 (targetMatcher.h) 完整代码如下所示:

#if !defined TMATCHER
#define TMATCHER

#define VERBOSE 1

#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>

class TargetMatcher {
    private:
        // 指向特征点检测器对象的指针
        cv::Ptr<cv::FeatureDetector> detector;
        // 指向特征描述符提取器对象的指针
        cv::Ptr<cv::DescriptorExtractor> descriptor;
        cv::Mat target;     // 目标图像
        int normType;
        double distance;    // 最小重投影误差
        int numberOfLevels; // 金字塔尺寸
        double scaleFactor; // 缩放因子
        // 目标图像金字塔及其关键点
        std::vector<cv::Mat> pyramid;
        std::vector<std::vector<cv::KeyPoint> > pyrKeypoints;
        std::vector<cv::Mat> pyrDescriptors;
        // 创建目标图像金字塔
        void createPyramid() {
            pyramid.clear();
            cv::Mat layer(target);
            for (int i=0; i<numberOfLevels; i++) {
                pyramid.push_back(target.clone());
                resize(target, target, cv::Size(), scaleFactor, scaleFactor);
            }
            pyrKeypoints.clear();
            pyrDescriptors.clear();
            // 图像金字塔关键点检测和描述符
            for (int i=0; i<numberOfLevels; i++) {
                // 在第 i 层检测目标关键点
                pyrKeypoints.push_back(std::vector<cv::KeyPoint>());
                detector->detect(pyramid[i], pyrKeypoints[i]);
                if (VERBOSE) {
                    std::cout << "Interest points: target=" << pyrKeypoints[i].size() << std::endl;
                }
                // 在第 i 层计算描述符
                pyrDescriptors.push_back(cv::Mat());
                descriptor->compute(pyramid[i], pyrKeypoints[i], pyrDescriptors[i]);
            }
        }
    public:
        TargetMatcher(const cv::Ptr<cv::FeatureDetector> &detector,
                    const cv::Ptr<cv::DescriptorExtractor> &descriptor = cv::Ptr<cv::DescriptorExtractor>(),
                    int numberOfLevels=8, double scaleFactor=0.9) : 
            detector(detector), descriptor(descriptor), 
            normType(cv::NORM_L2), distance(1.0),
            numberOfLevels(numberOfLevels), scaleFactor(scaleFactor) {
            if (!this->descriptor) {
                this->descriptor = this->detector;
            }
        }
        // 设置匹配时使用的归一化方法
        void setNormType(int norm) {
            normType = norm;
        }
        // 设置最小重投影距离
        void setReprojectionDistance(double d) {
            distance = d;
        }
        // 设置目标图像
        void setTarget(const cv::Mat t) {
            if (VERBOSE) cv::imshow("Target", t);
            target = t;
            createPyramid();
        }
        // 使用 RANSAC 标示较好匹配
        cv::Mat ransacTest(const std::vector<cv::DMatch>& matches,
                    std::vector<cv::KeyPoint>& keypoints1, 
                    std::vector<cv::KeyPoint>& keypoints2,
                    std::vector<cv::DMatch>& outMatches) {
            // 将关键点转换为 Point2f
            std::vector<cv::Point2f> points1, points2;	
            outMatches.clear();
            for (std::vector<cv::DMatch>::const_iterator it= matches.begin();
                        it!= matches.end(); ++it) {
                points1.push_back(keypoints1[it->queryIdx].pt);
                points2.push_back(keypoints2[it->trainIdx].pt);
            }
            // 查找图像 1 和 图像 2 间的单应性
            std::vector<uchar> inliers(points1.size(),0);
            cv::Mat homography= cv::findHomography(
                        points1,points2, // 对应点
                        inliers,         // 匹配状态
                        cv::RHO,	     // RHO 方法
                        distance);       // 到重投影点的最大距离
            std::vector<uchar>::const_iterator itIn= inliers.begin();
            std::vector<cv::DMatch>::const_iterator itM= matches.begin();
            for ( ;itIn!= inliers.end(); ++itIn, ++itM) {
                if (*itIn) {    // 有效匹配
                    outMatches.push_back(*itM);
                }
            }
            return homography;
        }
        // 检测图像中的平面目标
        cv::Mat detectTarget(const cv::Mat& image,
                        std::vector<cv::Point2f>& detectedCorners) {
            // 1. 检测图像关键点
            std::vector<cv::KeyPoint> keypoints;
            detector->detect(image, keypoints);
            if (VERBOSE)
                std::cout << "Interest points: image=" << keypoints.size() << std::endl;
            // 计算描述符
            cv::Mat descriptors;
            descriptor->compute(image, keypoints, descriptors);
            std::vector<cv::DMatch> matches;
            cv::Mat bestHomography;
            cv::Size bestSize;
            int maxInliers = 0;
            cv::Mat homography;
            // 构建匹配器
            cv::BFMatcher matcher(normType);
            // 2. 鲁棒地检测到的每一金字塔层的单应性
            for (int i = 0; i < numberOfLevels; i++) {
                // 查找目标和图像之间的 RANSAC 单应性 
                matches.clear();
                // 匹配描述符
                matcher.match(pyrDescriptors[i], descriptors, matches);
                if (VERBOSE)
                    std::cout << "Number of matches (level " << i << ")=" << matches.size() << std::endl;
                // 使用 RANSAC 验证匹配
                std::vector<cv::DMatch> inliers;
                homography = ransacTest(matches, pyrKeypoints[i], keypoints, inliers);
                if (VERBOSE)
                    std::cout << "Number of inliers=" << inliers.size() << std::endl;
                if (inliers.size() > maxInliers) {
                    maxInliers = inliers.size();
                    bestHomography = homography;
                    bestSize = pyramid[i].size();
                }
                if (VERBOSE) {
                    cv::Mat imageMatches;
                    cv::drawMatches(target, pyrKeypoints[i],    // 第一张图向及其关键点
                        image, keypoints,                       // 第二张图像及其关键点
                        inliers,                                // 匹配
                        imageMatches,                           // 结果图像
                        cv::Scalar(255, 255, 255),              // 线条颜色
                        cv::Scalar(255, 255, 255),              // 关键点颜色
                        std::vector<char>(),
                        cv::DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
                    cv::imshow("Target matches", imageMatches);
                    cv::waitKey();
                }
            }
            // 3. 使用最佳单应性找到图像上的角点位置 
            if (maxInliers > 8) {   // 有效评估
                // 最佳尺寸的目标角点
                std::vector<cv::Point2f> corners;
                corners.push_back(cv::Point2f(0, 0));
                corners.push_back(cv::Point2f(bestSize.width - 1, 0));
                corners.push_back(cv::Point2f(bestSize.width - 1, bestSize.height - 1));
                corners.push_back(cv::Point2f(0, bestSize.height - 1));
                // 重新投射目标角点
                cv::perspectiveTransform(corners, detectedCorners, bestHomography);
            }
            if (VERBOSE)
                std::cout << "Best number of inliers=" << maxInliers << std::endl;
            return bestHomography;
        }
};

#endif

主函数文件 (fastCorners.cpp) 完整代码如下所示:

#include <iostream>
#include <vector>
#include <opencv2/core/core.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/features2d/features2d.hpp>
#include <opencv2/calib3d/calib3d.hpp>
#include <opencv2/xfeatures2d.hpp>
#include "targetMatcher.h"

int main() {
    // 读取输入图像
    cv::Mat target= cv::imread("target.png",0);
    cv::Mat image= cv::imread("book.png",0);
    if (!target.data || !image.data)
        return 0; 
    cv::namedWindow("Target");
    cv::imshow("Target",target);
    cv::namedWindow("Image");
    cv::imshow("Image", image);
    // 准备匹配器
    TargetMatcher tmatcher(cv::FastFeatureDetector::create(10),cv::BRISK::create());
    tmatcher.setNormType(cv::NORM_HAMMING);
    // 定义输出数据
    std::vector<cv::DMatch> matches;
    std::vector<cv::KeyPoint> keypoints1, keypoints2;
    std::vector<cv::Point2f> corners;
    // 设置目标图像
    tmatcher.setTarget(target);
    // 使用目标匹配图像
    tmatcher.detectTarget(image, corners);
    // 绘制目标角点
    if (corners.size()==4) {
        cv::line(image, cv::Point(corners[0]), cv::Point(corners[1]), cv::Scalar(255, 255, 255), 3);
        cv::line(image, cv::Point(corners[1]), cv::Point(corners[2]), cv::Scalar(255, 255, 255), 3);
        cv::line(image, cv::Point(corners[2]), cv::Point(corners[3]), cv::Scalar(255, 255, 255), 3);
        cv::line(image, cv::Point(corners[3]), cv::Point(corners[0]), cv::Scalar(255, 255, 255), 3);
    }
    cv::namedWindow("Target detection");
    cv::imshow("Target detection",image);
    cv::waitKey();
    return 0;
}

小结

在计算机视觉中,平面的单应性被定义为从一个平面到另一个平面的投影映射,在多视图计算中,单应性具有重要用途,本节学习了使用 cv::findHomography 函数进行单应性估计,并利用单应性执行图像匹配任务。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/494792.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

读论文《大气压等离子体电离波沿介质管传输特性研究》

文章目录 一、研究背景和意义二、研究目的与内容三、电离波概述3.1 电离波与传统的流注放电3.2 电离波传输速度的计算方法 四、放电参数对电离波传输特性的影响4.1 施加电压与电压波形对电离波传输的影响4.1.1 交流高压对电离波的影响4.1.2 脉冲高压对电离波的影响![在这里插入…

《编程思维与实践》1047.Base64编码

《编程思维与实践》1047.Base64编码 题目 思路 直接模拟:将每个Base64编码值都分为两部分:前半部分由上一个字符求得,后半部分由下一个字符求得. 特别地,如果字符为第一个或最后一个,则直接可以求得Base64编码. 如下图: 其中,% 2 n 2^n 2n表示取出后n位的二进制位, 这是因…

专业游戏录屏软件Camtasia 2023强悍来袭,Camtasia Studio 2023的新增功能!

Camtasia Studio 2023是一款专门录制屏幕动作的工具&#xff0c;它能在任何颜色模式下轻松地记录 屏幕动作&#xff0c;包括影像、音效、鼠标移动轨迹、解说声音等等&#xff0c;另外&#xff0c;它还具有即时播放和编 辑压缩的功能&#xff0c;可对视频片段进行剪接、添加转场…

又一起数据泄露事件五个月内的第二次

据报道&#xff0c;T-Mobile 在发现攻击者从 2023 年 2 月下旬开始的一个多月内访问了数百名客户的个人信息后&#xff0c;披露了 2023 年的第二次数据泄露事件。 与之前报告的数据泄露事件&#xff08;最近一次影响了 3700 万人&#xff09;相比&#xff0c;此次事件仅影响了…

Linux一学就会——编写自己的shell

编写自己的shell 进程程序替换 替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数 以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动 例程开始执行…

Node.js 是什么?

简介 Node.js入门指南&#xff0c;服务器端JavaScript运行时环境。Node.js是在Google Chrome V8 JavaScript引擎的基础上构建的&#xff0c;它主要用于创建web服务器&#xff0c;但并不局限于此。 实际上Node.js 是把运行在浏览器中的js引擎抽离处理&#xff0c;进行再次封装…

MagicaCloth2安装教程

您可访问官网查看详情&#xff1b; MagicaSoft Unity Assets – Magica Soft 也可通过我的资源文件获得此插件的详细教程&#xff1a; (19条消息) UnityMagicaCloth2插件中文文档&#xff08;机翻/部分&#xff09;-Unity3D文档类资源-CSDN文库 MagicaCloth2是基于ECS开发的…

水质信息监测与管理系统

1.1 系统总体设计 1.1.1 系统组成 水质信息监测与管理系统由水质监测站网管理、水质监测数据管理、水质分析评价、水质监测资料整汇编、水质信息查询、水质信息发布等组成。 水质监测站网管理主要实现对各类监测站网&#xff08;固定监测站网、自动监测站网、动态监测站网&a…

Party Again!转录组+LC代谢组=899/组!

转录组代谢组&#xff0c;是基于代谢组和转录组数据&#xff0c;开展表达基因&#xff08;mRNA&#xff09;与代谢物的相关性分析。可实现差异代谢物与时序表达的差异基因的共表达分析&#xff0c;构建核心调控网络机制&#xff0c;找出其中的关键候选基因&#xff0c;揭示表型…

前端001_初始化数据库管控管理系统

数据库管控管理系统采用 Vue.js ElementUI 来搭建系统的前端。 1、技术栈 技术名说明vue.js前端vuex状态管理器mockjs模拟后台apiaxios拦截器echart图标element-ui组件库vue-element-admin脚手架&#xff0c;原始参照的项目模版mavon-editormarkdown编辑器 2、ElementUI 简…

用DG备库做的rman备份恢复一个数据库

环境描述&#xff1a; 1.因为主库存储空间不足&#xff0c;于是将备份放在dg备库上做。 2.主库因为磁盘空间问题&#xff0c;数据文件有两个目录。 3.dg备库因为主库两个数据文件目录里面有两个同名数据文件&#xff0c;所有dg备库也有两个数据文件目录。 4.主库与备库与测…

Grafana 系列-统一展示-1-开篇

系列文章 Grafana 系列文章 Grafana 简介 Grafana 是 Grafana Labs 的第一款也是最重要的产品。它的定位是可视化, 用于监控展示 和 可观察性. 是当前最为完善、流行的云原生、公有云和企业监控可视化平台。 Dashboard anything. Observe everything 无论你的数据存储在哪…

从 PC 解锁 Android 手机的 6 种有效方法

在这个数字时代&#xff0c;手机已成为我们生活的重要组成部分。我们将它们用于各种用途&#xff0c;从跟踪我们的工作和社交日程到与亲人交流。 然而&#xff0c;有时我们的手机会成为令人沮丧的源头&#xff0c;尤其是当我们不小心将自己拒之门外时。但是您知道可以使用计算…

基于SpringBoot+Vue实现的酒店管理系统

【简介】 基于springbootvue实现的酒店管理系统&#xff0c;用于酒店客房业务管理与酒店内部管理。 【功能结构】 【技术架构】 后端&#xff1a;springbootmybatis 前端&#xff1a;vue element-ui 环境&#xff1a;mysqlmaven node 【代码结构与数据库】 【功能详述】…

LeetCode232. 用栈实现队列

232. 用栈实现队列 描述示例解题思路以及代码 描述 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾 int pop()…

UICollectionView 实现整页翻动(每页3个cell)

提示&#xff1a;页面架构是通过UICollectionView做的分页&#xff0c;分页点PageControl使用的是<SDCycleScrollView/TAPageControl.h> &#xff0c;布局架构使用的是Masonry 前言 为了实现UICollectionView无限翻动&#xff0c;连续滑动&#xff0c;主要是利用pagingE…

海豚1.3单节点,多集群设置

最近出差忙项目&#xff0c;一直没更新&#xff0c;现在项目结尾了。回来继续搞集群 公司因为CDH升级为CDP&#xff0c;两套环境数据和任务慢慢迁移&#xff0c;但是调度任务需要同时跑批。 而我们的海豚调度是单节点的&#xff0c;master和worker等服务都在一台节点上。 之前…

基于VBA实现成绩排序的最佳方法-解放老师的双手

作为一名老师&#xff0c;每到期末就要面对一件让人头疼的事情——成绩表统计。 首先&#xff0c;要收集每个学生的考试成绩。这需要花费大量的时间和精力&#xff0c;因为每个学生都有多门科目的成绩需要统计。 其次&#xff0c;要将每个学生的成绩录入到电子表格中。这看起来…

【今天聊聊生产力】提升研发生产力的神器推荐

1、Free Mybatis Tool、MybatisX 用于DAO层和Mapper层之间跳转 Mapper和DAO层跳转&#xff0c;可以用的插件比较多&#xff0c;比较推荐如下两款&#xff0c;功能基本一致&#xff0c;只是样式小有差别。 Free Mybatis Tool&#xff0c;样式为一个绿色箭头&#xff0c;简洁明了…

docker 部署JAVA应用OOM的排障经历——筑梦之路

故障现象&#xff1a; 使用docker部署JAVA的应用&#xff0c;tomcat作为中间件容器&#xff0c;启动应用时总是报错无法创建Java虚拟机&#xff0c;然后就是OOM 报错信息&#xff1a; 不管是从docker容器的日志还是系统日志均未发现有用的信息&#xff0c;也尝试更换过镜像tom…