OpenCV 笔记(26):图像的透视变换

1. 图像的透视变换

1.1 简介

图像的透视变换 (Perspective Transformation)是指将图像投影到一个新的视平面(Viewing Plane),也称作投影映射(Projective Mapping)。

透视变换是一种非线性变换,它可以将一个二维坐标系中的点映射到三维坐标系中的点,然后再将其投影到另一个二维坐标系中的点。透视变换可以改变图像中的形状,并可以模拟真实世界中的透视效果。

仿射变换可以看成是透视变换的特殊情况,下图是对几何变换的总结。
几何变换的总结.png

透视变换的应用:

  • 图像矫正 透视变换可以用于矫正图像的透视失真,例如由于拍摄角度或镜头畸变导致的图像倾斜或拉伸。

  • 图像配准 透视变换可以用于将两张或多张图像进行配准,使其具有相同的几何形状。这在医学图像处理、卫星图像处理等领域有着重要的应用。

  • 3D 建模 透视变换可以用于将二维图像投影到三维空间,从而生成三维模型。

  • 图像增强 透视变换可以用于调整图像的视角,使其看起来更具吸引力。

  • 图像合成 透视变换可以用于将不同的图像合成在一起,创建新的图像。

  • 特效 透视变换可以用于创建各种特效,例如虚拟场景、3D 动画等。

1.2 原理

透视变换的定义为将图像中的所有点按照一定的透视关系映射到新的图像中。
透视变换.png

透视关系可以由一个3x3的透视变换矩阵来表示,透视变换的矩阵如下:

其中,、、、 表示线性变换,、 表示平移变换,、 表示透视变换。

透视变换的过程为:

此时,得到的不是最后的坐标,还需要进一步转换:

最终的坐标为:

重新回顾一下整个透视变换的过程:

不难看出看出仿射变换是透视变换的一种特殊情况。

2. 透视变换的应用

2.1 商品图位置矫正

下面的代码,对图中的没有摆正的商品通过透视变换将其对齐,然后在原图中将商品放正。主要用到了 OpenCV 的 findHomography()warpPerspective()函数进行透视变换。findHomography()函数用于计算两个平面之间进行透视变换的矩阵,warpPerspective() 函数用于对图像进行透视变换。

go 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

bool ascendSort(vector<Point> a,vector<Point> b)
{
    return contourArea(a) > contourArea(b);
}

long pointSideLine(Point &lineP1, Point &lineP2, Point &point) {
    long x1 = lineP1.x;
    long y1 = lineP1.y;
    long x2 = lineP2.x;
    long y2 = lineP2.y;
    long x = point.x;
    long y = point.y;
    return (x - x1)*(y2 - y1) - (y - y1)*(x2 - x1);
}

vector<Point> sortPointByClockwise(vector<Point> points) {
    if (points.size() != 4) {
        return points;
    }
    Point unFoundPoint;
    vector<Point> result = {unFoundPoint, unFoundPoint, unFoundPoint, unFoundPoint};
    long minDistance = -1;
    for(auto point : points) {
        long distance = point.x * point.x + point.y * point.y;
        if(minDistance == -1 || distance < minDistance) {
            result[0] = point;
            minDistance = distance;
        }
    }

    if (result[0] != unFoundPoint) {
        Point &leftTop = result[0];
        points.erase(std::remove(points.begin(), points.end(), leftTop));
        if ((pointSideLine(leftTop, points[0], points[1]) * pointSideLine(leftTop, points[0], points[2])) < 0) {
            result[2] = points[0];
        } else if ((pointSideLine(leftTop, points[1], points[0]) * pointSideLine(leftTop, points[1], points[2])) < 0) {
            result[2] = points[1];
        } else if ((pointSideLine(leftTop, points[2], points[0]) * pointSideLine(leftTop, points[2], points[1])) < 0) { result[2] = points[2]; } } if (result[0] != unFoundPoint && result[2] != unFoundPoint) { Point &leftTop = result[0]; Point &rightBottom = result[2]; points.erase(std::remove(points.begin(), points.end(), rightBottom)); if (pointSideLine(leftTop, rightBottom, points[0]) > 0) {
            result[1] = points[0];
            result[3] = points[1];
        } else {
            result[1] = points[1];
            result[3] = points[0];
        }
    }

    if (result[0] != unFoundPoint && result[1] != unFoundPoint && result[2] != unFoundPoint && result[3] != unFoundPoint) {
        return result;
    }

    return points;
}

int main(int argc,char *argv[])
{
    Mat src = imread(".../product.jpg");
    imshow("src", src);

    Mat gray, binary;
    cvtColor(src, gray, COLOR_BGR2GRAY);
    threshold(gray, binary, 0, 255, THRESH_BINARY | THRESH_OTSU);
    imshow("binary", binary);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(binary, contours, hierarchy, RETR_TREE, CHAIN_APPROX_SIMPLE);

    sort(contours.begin(), contours.end(), ascendSort);//ascending sort

    RotatedRect rrt = minAreaRect(contours[0]);
    Rect bbox = rrt.boundingRect();

    if (bbox.height > 2000) {
        rrt = minAreaRect(contours[1]);
        bbox = rrt.boundingRect();
    }

    Mat roi;
    try {
        roi = src(bbox);
    }
    catch (...) {
    }
    imshow("roi", roi);

    int width = static_cast<int>(rrt.size.width);
    int height = static_cast<int>(rrt.size.height);
    float angle = rrt.angle;

    printf("height %d, width :%d, angle:%f\n", height, width, angle);

    Point2f vertices[4];
    rrt.points(vertices);
    vector<Point> src_pts;

    for (int i = 0; i < 4; i++) {
        printf("x=%.2f, y=%.2f\n", vertices[i].x, vertices[i].y);
        src_pts.push_back(vertices[i]);
    }

    src_pts = sortPointByClockwise(src_pts); // 将顶点按照顺时针方向进行排序

    vector<Point> dst_pts;
    dst_pts.push_back(Point(0, 0));
    dst_pts.push_back(Point(width, 0));
    dst_pts.push_back(Point(width, height));
    dst_pts.push_back(Point(0, height));

    Mat M = findHomography(src_pts, dst_pts);
    Mat result = Mat::zeros(Size(width, height), CV_8UC3);
    warpPerspective(src, result, M, result.size());

    imshow("result", result);

    resize(result,result,roi.size());

    result.copyTo(roi);

    imshow("final", src);
    waitKey(0);
    return 0;
}

商品图和二值图像.png 对商品图进行透视变换的结果.png

简单介绍一下 warpPerspective() 函数:

go 复制代码
void warpPerspective( InputArray src, OutputArray dst,
                                   InputArray M, Size dsize,
                                   int flags = INTER_LINEAR,
                                   int borderMode = BORDER_CONSTANT,
                                   const Scalar& borderValue = Scalar());

第一个参数 src: 输入图像。

第二个参数 dst: 输出图像,与 src 具有相同的类型和大小。

第三个参数 M: 3x3 的透视变换矩阵。

第四个参数 dsize: 输出图像的大小。

上述代码,还需要注意调用 findHomography() 函数时,输入点的集合和输出点的集合顺序要一致。

2.2 广告牌内容替换

透视变换还有一个比较经典的例子,就是替换一张图像中广告牌的内容,下面的代码展示了这个例子:

go 复制代码
#include <opencv2/opencv.hpp>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>

using namespace std;
using namespace cv;

bool ascendSort(vector<Point> a,vector<Point> b)
{
    return contourArea(a) > contourArea(b);
}

long pointSideLine(Point &lineP1, Point &lineP2, Point &point) {
    long x1 = lineP1.x;
    long y1 = lineP1.y;
    long x2 = lineP2.x;
    long y2 = lineP2.y;
    long x = point.x;
    long y = point.y;
    return (x - x1)*(y2 - y1) - (y - y1)*(x2 - x1);
}

vector<Point> sortPointByClockwise(vector<Point> points) {
    if (points.size() != 4) {
        return points;
    }
    Point unFoundPoint;
    vector<Point> result = {unFoundPoint, unFoundPoint, unFoundPoint, unFoundPoint};
    long minDistance = -1;
    for(auto point : points) {
        long distance = point.x * point.x + point.y * point.y;
        if(minDistance == -1 || distance < minDistance) {
            result[0] = point;
            minDistance = distance;
        }
    }

    if (result[0] != unFoundPoint) {
        Point &leftTop = result[0];
        points.erase(std::remove(points.begin(), points.end(), leftTop));
        if ((pointSideLine(leftTop, points[0], points[1]) * pointSideLine(leftTop, points[0], points[2])) < 0) {
            result[2] = points[0];
        } else if ((pointSideLine(leftTop, points[1], points[0]) * pointSideLine(leftTop, points[1], points[2])) < 0) {
            result[2] = points[1];
        } else if ((pointSideLine(leftTop, points[2], points[0]) * pointSideLine(leftTop, points[2], points[1])) < 0) { result[2] = points[2]; } } if (result[0] != unFoundPoint && result[2] != unFoundPoint) { Point &leftTop = result[0]; Point &rightBottom = result[2]; points.erase(std::remove(points.begin(), points.end(), rightBottom)); if (pointSideLine(leftTop, rightBottom, points[0]) > 0) {
            result[1] = points[0];
            result[3] = points[1];
        } else {
            result[1] = points[1];
            result[3] = points[0];
        }
    }

    if (result[0] != unFoundPoint && result[1] != unFoundPoint && result[2] != unFoundPoint && result[3] != unFoundPoint) {
        return result;
    }

    return points;
}

int main() {
    Mat billboard = imread(".../billboard.jpg");
    imshow("billboard", billboard);

    Mat hsv;
    cvtColor(billboard, hsv, cv::COLOR_BGR2HSV); // BGR 转换到 HSV 色彩空间
    imshow("hsv", hsv);

    cv::Scalar lower_white(0, 0, 0);
    cv::Scalar upper_white(180, 30, 255);

    Mat mask;
    inRange(hsv, lower_white, upper_white, mask); // 通过 inRange 函数实现二值化
    imshow("mask", mask);

    Mat structureElement = getStructuringElement(MORPH_RECT, Size(105, 105), Point(-1, -1));
    morphologyEx(mask, mask, MORPH_OPEN, structureElement, Point(-1, -1), 1);

    imshow("mask2", mask);

    vector<vector<Point>> contours;
    vector<Vec4i> hierarchy;
    findContours(mask, contours, hierarchy, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
    sort(contours.begin(), contours.end(), ascendSort);//ascending sort

    RotatedRect rrt = minAreaRect(contours[0]);// 获取最大轮廓的最小外接矩形
    Rect bbox = rrt.boundingRect();
    int width = static_cast<int>(rrt.size.width);
    int height = static_cast<int>(rrt.size.height);

    printf("width %d, height :%d\n", width, height);

    Point2f pt[4];
    rrt.points(pt);

    Mat roi;
    try {
        roi = billboard(bbox);
    }
    catch (...) {
    }
    imshow("roi", roi);

    Mat girl = imread(".../girl.jpg");
    imshow("girl", girl);

    int width_girl = girl.cols;
    int height_girl = girl.rows;

    vector<Point> src_pts;
    src_pts.push_back(Point(0, 0));
    src_pts.push_back(Point(width_girl, 0));
    src_pts.push_back(Point(width_girl, height_girl));
    src_pts.push_back(Point(0, height_girl));

    vector<Point> dst_pts;
    for (int i = 0; i < 4; i++) {
        printf("x=%.2f, y=%.2f\n", pt[i].x, pt[i].y);
        dst_pts.push_back(pt[i]);
    }

    dst_pts = sortPointByClockwise(dst_pts); // 将顶点按照顺时针方向进行排序

    Mat M = findHomography(src_pts,dst_pts);
    Mat result;
    warpPerspective(girl, result, M, billboard.size());
    imshow("result", result);

    result.copyTo(billboard,mask);
    imshow("final", billboard);

    waitKey(0);
    return 0;
}

广告牌替换的过程1.png 广告牌替换的过程2.png

3. 总结

透视变换是一种重要的图像处理技术,它具有广泛的应用价值。它可以改变图像的视角,从而使图像更加符合人眼的视觉感受,或满足特定的应用需求。它可以用于图像矫正、图像配准、3D 建模、增强现实等领域。

透视变换是一种非线性变换,因此它可能会导致图像变形。例如,如果透视变换矩阵不合适,可能会使图像中的物体看起来拉伸或压缩。此外,透视变换也可能会导致图像中的物体出现重叠或遮挡。在使用透视变换时,需要考虑这些局限性,并选择合适的参数来获得最佳效果。

相关推荐
小于小于大橙子3 小时前
视觉SLAM数学基础
人工智能·数码相机·自动化·自动驾驶·几何学
2401_858286113 小时前
L7.【LeetCode笔记】相交链表
笔记·leetcode·链表
埃菲尔铁塔_CV算法4 小时前
图像算法之 OCR 识别算法:原理与应用场景
图像处理·python·计算机视觉
封步宇AIGC4 小时前
量化交易系统开发-实时行情自动化交易-3.4.2.Okex行情交易数据
人工智能·python·机器学习·数据挖掘
封步宇AIGC4 小时前
量化交易系统开发-实时行情自动化交易-2.技术栈
人工智能·python·机器学习·数据挖掘
龙中舞王5 小时前
Unity学习笔记(2):场景绘制
笔记·学习·unity
陌上阳光5 小时前
动手学深度学习68 Transformer
人工智能·深度学习·transformer
OpenI启智社区5 小时前
共筑开源技术新篇章 | 2024 CCF中国开源大会盛大开幕
人工智能·开源·ccf中国开源大会·大湾区
AI服务老曹5 小时前
建立更及时、更有效的安全生产优化提升策略的智慧油站开源了
大数据·人工智能·物联网·开源·音视频
YRr YRr5 小时前
PyTorch:torchvision中的dataset的使用
人工智能