OpenCV 筆記(26):圖像的透視變換

1. 圖像的透視變換

1.1 簡介

圖像的透視變換(Perspective Transformation)是指將圖像投影到一個新的視平面(Viewing Plane),也稱作投影映射(Projective Mapping)。

透視變換是一種非線性變換,它可以將一個二維坐標(biāo)系中的點映射到三維坐標(biāo)系中的點,然后再將其投影到另一個二維坐標(biāo)系中的點。透視變換可以改變圖像中的形狀,并可以模擬真實世界中的透視效果。

仿射變換可以看成是透視變換的特殊情況,下圖是對幾何變換的總結(jié)。

幾何變換的總結(jié).png

透視變換的應(yīng)用:

  • 圖像矯正
    透視變換可以用于矯正圖像的透視失真,例如由于拍攝角度或鏡頭畸變導(dǎo)致的圖像傾斜或拉伸。
  • 圖像配準(zhǔn)
    透視變換可以用于將兩張或多張圖像進(jìn)行配準(zhǔn),使其具有相同的幾何形狀。這在醫(yī)學(xué)圖像處理、衛(wèi)星圖像處理等領(lǐng)域有著重要的應(yīng)用。
  • 3D 建模
    透視變換可以用于將二維圖像投影到三維空間,從而生成三維模型。
  • 圖像增強(qiáng)
    透視變換可以用于調(diào)整圖像的視角,使其看起來更具吸引力。
  • 圖像合成
    透視變換可以用于將不同的圖像合成在一起,創(chuàng)建新的圖像。
  • 特效
    透視變換可以用于創(chuàng)建各種特效,例如虛擬場景、3D 動畫等。

1.2 原理

透視變換的定義為將圖像中的所有點按照一定的透視關(guān)系映射到新的圖像中。

透視變換.png

透視關(guān)系可以由一個3x3的透視變換矩陣來表示,透視變換的矩陣如下:

\begin{vmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{vmatrix}

其中,a_{11}a_{12}、a_{21}a_{22} 表示線性變換,a_{31}、a_{32} 表示平移變換,a_{13}、a_{23} 表示透視變換。

透視變換的過程為:

\begin{vmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{vmatrix} \begin{vmatrix} x \\ y \\ 1 \end{vmatrix} = \begin{vmatrix} x' \\ y' \\ z' \end{vmatrix}

此時,得到的\begin{vmatrix} x' \\ y' \\ z' \end{vmatrix}不是最后的坐標(biāo),還需要進(jìn)一步轉(zhuǎn)換:\begin{vmatrix} x' \\ y' \\ z' \end{vmatrix} = z' \begin{vmatrix} \frac{x'}{z'} \\ \frac{y'}{z'} \\ 1 \end{vmatrix}

最終的坐標(biāo)為:\begin{vmatrix} x'' \\ y'' \\ 1 \end{vmatrix} = \begin{vmatrix} \frac{x'}{z'} \\ \frac{y'}{z'} \\ 1 \end{vmatrix}

重新回顧一下整個透視變換的過程:

\begin{vmatrix} x'' \\ y'' \\ 1 \end{vmatrix} = \begin{vmatrix} \frac{x'}{z'} \\ \frac{y'}{z'} \\ 1 \end{vmatrix} = \frac{1}{z'} \begin{vmatrix} x' \\ y' \\ z' \end{vmatrix} = \frac{1}{z'} \begin{vmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{vmatrix} \begin{vmatrix} x \\ y \\ 1 \end{vmatrix} = \frac{1}{a_{31}x+a_{32}y+a_{33}} \begin{vmatrix} a_{11} & a_{12} & a_{13}\\ a_{21} & a_{22} & a_{23}\\ a_{31} & a_{32} & a_{33} \end{vmatrix} \begin{vmatrix} x \\ y \\ 1 \end{vmatrix}

不難看出看出仿射變換是透視變換的一種特殊情況。

2. 透視變換的應(yīng)用

2.1 商品圖位置矯正

下面的代碼,對圖中的沒有擺正的商品通過透視變換將其對齊,然后在原圖中將商品放正。主要用到了 OpenCV 的 findHomography()warpPerspective()函數(shù)進(jìn)行透視變換。findHomography()函數(shù)用于計算兩個平面之間進(jìn)行透視變換的矩陣,warpPerspective() 函數(shù)用于對圖像進(jìn)行透視變換。

#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); // 將頂點按照順時針方向進(jìn)行排序

    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
對商品圖進(jìn)行透視變換的結(jié)果.png

簡單介紹一下 warpPerspective() 函數(shù):

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

第一個參數(shù) src: 輸入圖像。

第二個參數(shù) dst: 輸出圖像,與 src 具有相同的類型和大小。

第三個參數(shù) M: 3x3 的透視變換矩陣。

第四個參數(shù) dsize: 輸出圖像的大小。

上述代碼,還需要注意調(diào)用 findHomography() 函數(shù)時,輸入點的集合和輸出點的集合順序要一致。

2.2 廣告牌內(nèi)容替換

透視變換還有一個比較經(jīng)典的例子,就是替換一張圖像中廣告牌的內(nèi)容,下面的代碼展示了這個例子:

#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 轉(zhuǎn)換到 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 函數(shù)實現(xiàn)二值化
    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); // 將頂點按照順時針方向進(jìn)行排序

    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. 總結(jié)

透視變換是一種重要的圖像處理技術(shù),它具有廣泛的應(yīng)用價值。它可以改變圖像的視角,從而使圖像更加符合人眼的視覺感受,或滿足特定的應(yīng)用需求。它可以用于圖像矯正、圖像配準(zhǔn)、3D 建模、增強(qiáng)現(xiàn)實等領(lǐng)域。

透視變換是一種非線性變換,因此它可能會導(dǎo)致圖像變形。例如,如果透視變換矩陣不合適,可能會使圖像中的物體看起來拉伸或壓縮。此外,透視變換也可能會導(dǎo)致圖像中的物體出現(xiàn)重疊或遮擋。在使用透視變換時,需要考慮這些局限性,并選擇合適的參數(shù)來獲得最佳效果。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容