13_透视变换.cpp
14_透视变换扭正.cpp
概念
仿射变换和透视变换更直观的叫法可以叫做「平面变换」和「空间变换」或者「二维坐标变换」和「三维坐标变换」。如果这么命名的话,其实很显然,这俩是一回事,只不过一个是二维坐标(x,y),一个是三维坐标(x,y,z)。
链接:图像处理的仿射变换与透视变换_知乎
仿射变换的方程组有6个未知数,所以要求解就需要找到3组映射点,三个点刚好确定一个平面。
透视变换的方程组有8个未知数,所以要求解就需要找到4组映射点,四个点就刚好确定了一个三维空间。
需求
给定一张图像,已知这张图像上的一块区域的四点(source points),需要将这块区域转换到目标区域(target points),使用 opencv 的透视变换 api 求解变换矩阵,进行透视变换。
流程
getPerspectiveTransform(srcPts, tarPts)
求解变换矩阵 p_matrixwarpPerspective()
进行仿射变换在这个案例里面,原始图像的4个点是手动给定的,在更复杂的案例中,这4个点可以通过特征识别计算出,比如人脸区域,特征图案区域等。
代码
//
// Created by jacob on 12/29/20.
// enable cv-logging::https://stackoverflow.com/questions/54828885/how-to-enable-logging-for-opencv
//
#include
#include
#include
using namespace std;
namespace cvlog = cv::utils::logging;
int main() {
// 0. set logging
cvlog::setLogLevel(cv::utils::logging::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "perspective transform")
// 1. read src image
string fileName = "../img/clock.jpg";
cv::Mat src = cv::imread(fileName, cv::IMREAD_COLOR);
// 2. do operation: perspective transformation
std::vector<cv::Point2f> srcPts = {
cv::Point2f(66, 61), cv::Point2f(70, 343), // u-l, b-l
cv::Point2f(379, 440), cv::Point2f(355, 165) // b-r, u-r
};
std::vector<cv::Point2f> tarPts = {
cv::Point2f(0, 0), cv::Point2f(0, 640),
cv::Point2f(480, 640), cv::Point2f(480, 0)
};
cv::Mat pM = cv::getPerspectiveTransform(srcPts, tarPts);
CV_LOG_INFO(NULL, "p_matrix:\n" << pM)
cv::Mat dst;
cv::warpPerspective(src, dst, pM, cv::Size(480, 640));
// display & wait key press
cv::imshow("src", src);
cv::imshow("dst", dst);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
运行结果
/home/jacob/CVWS/studyopencv002/cmake-build-debug/13_perspective_transform
[ INFO:0] perspective transform
[ INFO:0] p_matrix:
[1.588971099903391, -0.02253859716175018, -103.497238166757;
-0.8805138353460487, 2.446812484759694, -91.14164843750211;
-0.0002586959381707657, 0.0002645255256509643, 1]
代码
//
// Created by jacob on 12/29/20.
// enable cv-logging::https://stackoverflow.com/questions/54828885/how-to-enable-logging-for-opencv
// opencv 四边形拟合_谈谈OpenCV中的四边形:https://blog.csdn.net/weixin_39540271/article/details/111284004
//
#include
#include
#include
using namespace std;
namespace cvlog = cv::utils::logging;
int main() {
/*
* 0. set cv log
*/
cvlog::setLogLevel(cv::utils::logging::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "perspective transform")
/*
* 1. read src image
*/
string fileName = "../img/book.jpg";
cv::Mat src = cv::imread(fileName, cv::IMREAD_COLOR);
CV_LOG_INFO(NULL, "src size: " << src.size << ", src rows: " << src.rows << ", src cols: " << src.cols)
cv::resize(src, src, cv::Size(int(src.cols / 2), int(src.rows / 2)));
cv::imshow("src", src);
/*
* 2. do operation
*/
// 1. 彩色图像转化成灰色图像: dst_gray
cv::Mat dst_gray;
cv::cvtColor(src, dst_gray, cv::COLOR_BGR2GRAY);
cv::imshow("dst_gray", dst_gray);
// 2. 转成二值图: dst_bin
cv::Mat dst_bin;
cv::threshold(dst_gray, dst_bin, 0, 255, cv::THRESH_BINARY_INV | cv::THRESH_TRIANGLE);
cv::imshow("dst_bin", dst_bin);
// 3. 找出轮廓
cv::Mat dst_pre;
dst_pre = dst_bin.clone();
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::dilate(dst_bin, dst_pre, kernel, cv::Point(-1, -1), 1);
cv::imshow("dst_pre", dst_pre);
std::vector<std::vector<cv::Point>> contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours(dst_pre, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
CV_LOG_INFO(NULL, "contours: " << contours.data() << ", contours.size(): " << contours.size())
double maxArea = 0;
int maxIndex = 0;
for (int i = 0; i < contours.size(); ++i) {
std::vector<cv::Point> contour = contours[i];
double area = cv::contourArea(contour);
CV_LOG_INFO(NULL, "contour[" << i << "] area:: " << area)
if (maxArea <= area) {
maxArea = area;
maxIndex = i;
}
}
CV_LOG_INFO(NULL, "max area: " << maxArea)
CV_LOG_INFO(NULL, "max contours: " << contours[maxIndex] << ", len: " << contours[maxIndex].size())
cv::drawContours(src, contours, maxIndex, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
cv::Mat ployM = cv::Mat::zeros(src.size(), src.type());
std::vector<cv::Point> contourOut;
std::vector<std::vector<cv::Point>> contourOuts;
double contourLen = cv::arcLength(contours[maxIndex], true);
cv::approxPolyDP(contours[maxIndex], contourOut, 0.02 * contourLen, true);
CV_LOG_INFO(NULL, "contourOut.size(): " << contourOut.size() << ", contourOut: " << contourOut)
contourOuts.push_back(contourOut);
if (contourOuts[0].size() == 4) {
CV_LOG_INFO(NULL, "contourOuts[0].size: " << contourOuts[0].size())
cv::drawContours(ployM, contourOuts, -1, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
}
cv::imshow("ployM", ployM);
// 4. 找到边缘的交点
cv::Point2f s_u_r = contourOut[0];
cv::Point2f s_u_l = contourOut[1];
cv::Point2f s_b_l = contourOut[2];
cv::Point2f s_b_r = contourOut[3];
std::vector<cv::Point2f> sourcePts = {
s_u_r, s_u_l, s_b_l, s_b_r};
cv::Point2f t_u_r = cv::Point2f(src.cols, 0);
cv::Point2f t_u_l = cv::Point2f(0, 0);
cv::Point2f t_b_l = cv::Point2f(0, src.rows);
cv::Point2f t_b_r = cv::Point2f(src.cols, src.rows);
std::vector<cv::Point2f> targetPts = {
t_u_r, t_u_l, t_b_l, t_b_r};
// 5. 透视变换
const cv::Mat &pMatrix = cv::getPerspectiveTransform(sourcePts, targetPts);
CV_LOG_INFO(NULL, "pMatrix" << pMatrix)
cv::Mat pDst;
cv::warpPerspective(src, pDst, pMatrix, cv::Size(src.cols, src.rows));
cv::imshow("pDst", pDst);
/*
* display &wait key press
*/
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
代码说明
对图像进行预处理
cv::threshold()
或cv::adaptiveThreshold()
函数cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
cv::dilate(dst_bin, dst_pre, kernel, cv::Point(-1, -1), 1);
找出图像中的轮廓:cv::findContours()
cv::findContours(dst_pre, contours, hierarchy, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
在找到的轮廓中,挑选出最大轮廓,并返回最大轮廓索引:int maxIndex = getMaxContour(src, contours);
int getMaxContour(const cv::Mat &src, const vector<vector<cv::Point>> &contours) {
double maxArea = 0;
int maxIndex = 0;
for (int i = 0; i < contours.size(); ++i) {
vector<cv::Point> contour = contours[i];
double area = cv::contourArea(contour);
CV_LOG_INFO(NULL, "contour[" << i << "] area:: " << area)
if (maxArea <= area) {
maxArea = area;
maxIndex = i;
}
}
CV_LOG_INFO(NULL, "max area: " << maxArea)
CV_LOG_INFO(NULL, "max contours: " << contours[maxIndex] << ", len: " << contours[maxIndex].size())
cv::drawContours(src, contours, maxIndex, cv::Scalar(0, 0, 255), 2, cv::LINE_AA);
return maxIndex;
}
使用 cv::contourArea 计算轮廓向量中的每一个轮廓的面积,取最大面积,并返回最大面积的索引 maxIndex。绘制轮廓向量中的最大轮廓,使用最大索引 maxIndex。
对最大轮廓进行四边形拟合,得到四个拟合点:quadFitContours()
vector<cv::Point> quadFitContours(const cv::Mat &src, const vector<vector<cv::Point>> &contours, int maxIndex) {
cv::Mat ployM = cv::Mat::zeros(src.size(), src.type());
vector<cv::Point> contourOut;
vector<vector<cv::Point>> contourOuts;
double contourLen = cv::arcLength(contours[maxIndex], true);
cv::approxPolyDP(contours[maxIndex], contourOut, 0.02 * contourLen, true);
CV_LOG_INFO(NULL, "contourOut.size(): " << contourOut.size() << ", contourOut: " << contourOut)
contourOuts.push_back(contourOut);
if (contourOuts[0].size() == 4) {
CV_LOG_INFO(NULL, "contourOuts[0].size: " << contourOuts[0].size())
cv::drawContours(ployM, contourOuts, -1, cv::Scalar(255, 0, 0), 2, cv::LINE_AA);
}
cv::imshow("ployM", ployM);
return contourOut;
}
使用 arcLength 求出目标轮廓的周长,再使用 approxPolyDP 对轮廓进行多边形拟合(不一定是四边形,当前案例可以用四边形拟合,还需要思考如何手动进行四边形拟合),当拟合点为四个的时候,绘制拟合多边形(区别轮廓)。周长的用处是确定拟合精度。
进行透视变换操作
void doPerspectiveT(const cv::Mat &src, const vector<cv::Point> &contourOut) {
cv::Point2f s_u_r = contourOut[0];
cv::Point2f s_u_l = contourOut[1];
cv::Point2f s_b_l = contourOut[2];
cv::Point2f s_b_r = contourOut[3];
vector<cv::Point2f> sourcePts = {
s_u_r, s_u_l, s_b_l, s_b_r};
cv::Point2f t_u_r = cv::Point2f(src.cols, 0);
cv::Point2f t_u_l = cv::Point2f(0, 0);
cv::Point2f t_b_l = cv::Point2f(0, src.rows);
cv::Point2f t_b_r = cv::Point2f(src.cols, src.rows);
vector<cv::Point2f> targetPts = {
t_u_r, t_u_l, t_b_l, t_b_r};
// 5. 透视变换
const cv::Mat &pMatrix = cv::getPerspectiveTransform(sourcePts, targetPts);
CV_LOG_INFO(NULL, "pMatrix" << pMatrix)
cv::Mat pDst;
cv::warpPerspective(src, pDst, pMatrix, cv::Size(src.cols, src.rows));
cv::imshow("pDst", pDst);
}
通过拟合四点确定透视变换的原始点集,以及确定目标点集,使用 cv::getPerspectiveTransform 计算透视变换矩阵,然后通过 cv::warpPerspective 函数将原始图像四拟合点区域的图像透视变换到新的图像中。
需要弄清各个函数的参数返回值的数据结构,以及各个数据结构的操作函数。
意向
测试代码
//
// Created by jacob on 12/29/20.
//
#include
#include
#include
using namespace std;
namespace cvlog = cv::utils::logging;
void doHoughLines(const cv::Mat &imgM);
void doHoughLinesP(const cv::Mat &imgM);
int main(int argc, char **argv) {
// setting up cv log
cvlog::setLogLevel(cvlog::LOG_LEVEL_INFO);
CV_LOG_INFO(NULL, "perspective_T_hough_line_test.cpp")
// read img
string filename = "../img/imgM.jpg";
cv::Mat imgM = cv::imread(filename, cv::IMREAD_COLOR);
cv::imshow("imgM", imgM);
// do operation
doHoughLines(imgM);
cv::waitKey(0);
cv::destroyAllWindows();
return 0;
}
void doHoughLines(const cv::Mat &imgM) {
cv::Mat rst_s, rst_s_c3;
cv::cvtColor(imgM, rst_s, cv::COLOR_BGR2GRAY);
cv::cvtColor(rst_s, rst_s_c3, cv::COLOR_GRAY2BGR);
// use standard hough lines :: houghlines
vector<cv::Vec2f> lines; // will hold the results of the detection
HoughLines(rst_s, lines, 1, CV_PI / 180, 20, 0, 0); // runs the actual detection
int lineSize = lines.size();
int threshold = 0;
for (int i = 0; i < 500; ++i) {
HoughLines(rst_s, lines, 1, CV_PI / 180, i, 0, 0); // runs the actual detection
if (lineSize > lines.size()){
lineSize = lines.size();
threshold = i;
}
}
CV_LOG_INFO(NULL, "lines:: " << lines.size() << ", min_lines_size:: " << lineSize)
CV_LOG_INFO(NULL, "threshold:: " << threshold)
// Draw the lines
for (auto &line : lines) {
float rho = line[0], theta = line[1];
cv::Point pt1, pt2;
double a = cos(theta), b = sin(theta);
double x0 = a * rho, y0 = b * rho;
pt1.x = cvRound(x0 + 1000 * (-b));
pt1.y = cvRound(y0 + 1000 * (a));
pt2.x = cvRound(x0 - 1000 * (-b));
pt2.y = cvRound(y0 - 1000 * (a));
cv::line(rst_s_c3, pt1, pt2, cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
}
cv::imshow("rst_s_c3", rst_s_c3);
// calculate four intersection points
}
void doHoughLinesP(const cv::Mat &imgM) {
cv::Mat rst_p, rst_p_c3;
cv::cvtColor(imgM, rst_p, cv::COLOR_BGR2GRAY);
cv::cvtColor(rst_p, rst_p_c3, cv::COLOR_GRAY2BGR);
vector<cv::Vec4i> linesP; // will hold the results of the detection
HoughLinesP(rst_p, linesP, 1, CV_PI / 180, 250, 250, 2); // runs the actual detection
CV_LOG_INFO(NULL, "linesP:: " << linesP.size()); // (x1, y1, x2, y2) 线段的两个端点坐标
// Draw the lines
for (auto l : linesP) {
line(rst_p_c3, cv::Point(l[0], l[1]), cv::Point(l[2], l[3]), cv::Scalar(0, 0, 255), 1, cv::LINE_AA);
}
cv::imshow("rst_p_c3", rst_p_c3);
// calculate four intersection points
}
相较于 cv::approxPolyDP 函数的多边形拟合,霍夫直线在此案例中理论上可以拟合所需要的四边形,但是调参很麻烦。
未验证代码片段
calcPoint
Point2f calcPoint(Point2f kb1,Point2f kb2){
double k1 = kb1.x;
double b1 = kb1.y;
double k2 = kb2.x;
double b2 = kb2.y;
double x = (b1-b2)/(k2-k1);
double y = k1*x + b1;
return Point2f(x,y);
}
main.cpp
// [ 【Point】 ]
vector<Vec4i> lines;
HoughLinesP(maxGrayImg,lines,1,CV_PI/180,118,210,5);
vector<Point2f> kbs;
kbs.push_back(Point2f(0,0));
cout<<lines.size()<<endl;
for (int i = 1; i <= 4; ++i) {
Vec4i lineResult = lines[i];
Point pt1(lineResult[0],lineResult[1]);
Point pt2(lineResult[2],lineResult[3]);
//line(src,pt1,pt2,colors[i],3);
double k = (lineResult[3] -lineResult[1] )/(lineResult[2]-lineResult[0]);
double b = lineResult[1] - lineResult[0]*k;
Point2f kb(k,b);
kbs.push_back(kb);
}
// 左上角 3,4
Point2f top_left = calcPoint(kbs[3],kbs[4]);
// 右上角 3,2
Point2f top_right = calcPoint(kbs[3],kbs[2]);
// 左下角 1,4
Point2f bottom_left = calcPoint(kbs[1],kbs[4]);
// 右下角 1,2
Point2f bottom_right = calcPoint(kbs[1],kbs[2]);
理解
HoughLinesP 函数返回的直线类型 vect,其为:(x1, y1, x2, y2) ,是线段的两个端点坐标。
对于每一个线段(以两个端点表示(x1, y1),(x2, y2)),端点坐标已知,可以求的线段的斜率 k 以及偏移量 b:
double k = (lineResult[3] - lineResult[1] )/(lineResult[2]-lineResult[0]);
double b = lineResult[1] - lineResult[0]*k;
计算两条直线的交点,两直线分别以不同的斜率、偏移量坐标表示 cv::Point2f kb(k, b)
Point2f kb(k,b);
kbs.push_back(kb);
Point2f calcPoint(Point2f kb1,Point2f kb2);
假设求解直线 y = k1x + b1 与 y = k2x + b2 的交点(x,y)
可以先求出交点 x 坐标:x = (b2 - b1)/(k1 - k2)
然后再通过 x,k1,b1(或 k2,b2)求解出 y 坐标:y = k1*x + b1
返回(x,y)
链接:四边形拟合_谈谈OpenCV中的四边形