问题描述:将一个变形的图像还原至正常视角下的形状。
如图
思路:二值化处理+形态学操作+轮廓寻找+检测直线+寻找四个交点+透视变换
结果
直线检测和角点寻找的结果
代码实现
#include<iostream>
#include<opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
//加载图像
Mat src = imread("1.jpg");
if (src.empty())
{
cout << "no image!" << endl;
return -1;
}
imshow("src", src);
//二值化
Mat gray, binary;
cvtColor(src, gray, COLOR_BGR2GRAY);
threshold(gray, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);
imshow("binary", binary);
//形态学处理,消除微小颗粒
Mat closeImg;
Mat kern1 = getStructuringElement(MORPH_RECT, Size(5, 5), Point(-1, -1));
morphologyEx(binary, closeImg, MORPH_CLOSE, kern1, Point(-1, -1));
imshow("close", closeImg);
bitwise_not(closeImg, closeImg);
imshow("not", closeImg);
//寻找轮廓
int width = src.cols;
int height = src.rows;
vector<vector<Point>>contours;
vector<Vec4i>hie;
Mat mask = Mat::zeros(src.size(), CV_8UC3);
findContours(closeImg, contours, hie, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(-1, -1));
for (size_t i = 0; i < contours.size(); i++)
{
Rect rect = boundingRect(contours[i]);
if (rect.width > width/2 && rect.height > height/2 && rect.width < src.cols - 5)
{
drawContours(mask, contours, static_cast<int>(i), Scalar(0, 0, 255), 2, 8, hie, 0, Point());
}
}
imshow("mask", mask);
//检测直线
Mat gray_mask;
int accu = min(0.5*width, 0.5*height);
cvtColor(mask, gray_mask, COLOR_BGR2GRAY);
imshow("gray_mask", gray_mask);
vector<Vec4i>lines;
HoughLinesP(gray_mask, lines, 1, 3.1415 / 180.0, accu, accu, 0);
Mat mask_line = Mat::zeros(mask.size(), CV_8UC3);
for (size_t i = 0; i < lines.size(); i++)
{
line(mask_line, Point(lines[i][0], lines[i][1]), Point(lines[i][2], lines[i][3]), Scalar(0, 0, 255), 2, 8,0);
}
imshow("lines", mask_line);
cout << "直线数量:" << lines.size() << endl;
//寻找并定位上下左右四条直线
int deltaH = 0;//定义高度差
int deltaW = 0;//宽度差
Vec4i topLine, bottomLine, leftLine, rightLine;
for (int i = 0; i < lines.size(); i++)
{
Vec4i ln = lines[i];
deltaH = abs(ln[3] - ln[1]);
deltaW = abs(ln[2] - ln[0]);
//double slope = (ln[3] - ln[1]) / (ln[2] - ln[0] + 0.00001);
if (ln[1]<height / 2.0 && ln[3] < height / 2.0 && deltaH < accu - 1)
{
topLine = lines[i];
}
if (ln[1]>height / 2.0 && ln[3] > height / 2.0 && deltaH < accu - 1)
{
bottomLine = lines[i];
}
if (ln[0] < width / 2.0 && ln[2]<width / 2.0 && deltaW < accu - 1)
{
leftLine = lines[i];
}
if (ln[0] > width / 2.0 && ln[2]>width / 2.0 && deltaW < accu - 1)
{
rightLine = lines[i];
}
}
cout << "topLine:" << topLine[0] << "," << topLine[1] << ";" << topLine[2] << "," << topLine[3] << endl;
cout << "bottomLine:" << bottomLine[0] << "," << bottomLine[1] << ";" << bottomLine[2] << "," << bottomLine[3] << endl;
cout << "leftLine:" << leftLine[0] << "," << leftLine[1] << ";" << leftLine[2] << "," << leftLine[3] << endl;
cout << "rightLine:" << rightLine[0] << "," << rightLine[1] << ";" << rightLine[2] << "," << rightLine[3] << endl;
//拟合四条直线方程
//top
float k1 = float(topLine[3] - topLine[1]) / float(topLine[2] - topLine[0]);
float c1 = topLine[1] - k1 * topLine[0];
cout << "k1=" << k1 << ",c1=" << c1 << endl;
//bottom
float k2 = float(bottomLine[3] - bottomLine[1]) / float(bottomLine[2] - bottomLine[0]);
float c2 = bottomLine[1] - k2 * bottomLine[0];
cout << "k2=" << k2 << ",c2=" << c2 << endl;
//left
float k3 = float(leftLine[3] - leftLine[1]) /float (leftLine[2] - leftLine[0]);
float c3 = leftLine[1] - k3 * leftLine[0];
cout << "k3=" << k3 << ",c3=" << c3 << endl;
//right
float k4 = float(rightLine[3] - rightLine[1]) / float(rightLine[2] - rightLine[0]);
float c4 = rightLine[1] - k4 * rightLine[0];
cout << "k4=" << k4 << ",c4=" << c4 << endl;
//计算四角
Point pt1, pt2, pt3, pt4;
//左上
pt1.x = static_cast<int>((c1 - c3) / (k3 - k1));
pt1.y = static_cast<int>(k1*pt1.x + c1);
//右上
pt2.x = static_cast<int>((c4 - c1) / (k1 - k4));
pt2.y = static_cast<int>(k1*pt2.x + c1);
//左下
pt3.x = static_cast<int>((c2 - c3) / (k3 - k2));
pt3.y = static_cast<int>(k2*pt3.x + c2);
//右下
pt4.x = static_cast<int>((c4 - c2) / (k2 - k4));
pt4.y = static_cast<int>(k2*pt4.x + c2);
circle(mask_line, pt1, 2, Scalar(0, 255, 0), -1, 8);
circle(mask_line, pt2, 2, Scalar(0, 255, 0), -1, 8);
circle(mask_line, pt3, 2, Scalar(0, 255, 0), -1, 8);
circle(mask_line, pt4, 2, Scalar(0, 255, 0), -1, 8);
imshow("mask_line_point", mask_line);
cout << "pt1(x, y)=" << pt1.x << "," << pt1.y << endl;
cout << "pt2(x, y)=" << pt2.x << "," << pt2.y << endl;
cout << "pt3(x, y)=" << pt3.x << "," << pt3.y << endl;
cout << "pt4(x, y)=" << pt4.x << "," << pt4.y << endl;
//透视变换
//输入点
Point2f src_corners[4];
src_corners[0] = pt1;
src_corners[1] = pt2;
src_corners[2] = pt3;
src_corners[3] = pt4;
//输出点
Point2f dst_corners[4];
dst_corners[0] = Point2f(static_cast<float>(0), static_cast<float>(0));
dst_corners[1] = Point2f(static_cast<float>(width), static_cast<float>(0));
dst_corners[2] = Point2f(static_cast<float>(0), static_cast<float>(height));
dst_corners[3] = Point2f(static_cast<float>(width), static_cast<float>(height));
Mat resultImg = Mat::zeros(src.size(), CV_8UC3);
//Mat warpMat(3,3,CV_32F);
Mat warpMat = getPerspectiveTransform(src_corners, dst_corners);
cout << warpMat.type() << endl;
warpPerspective(src, resultImg, warpMat, resultImg.size());
//resultImg.convertTo(resultImg, CV_8U);
imshow("result", resultImg);
waitKey(0);
return 0;
}
代码解释:
(1)为什么进行二值化而不是canny边缘检测?
答:因为我们要提取的是卡片的整体边界,而进行canny边缘检测处理后,卡片中的文字边缘也会对后续的边界轮廓寻找产生干扰,因此不如使用二值化操作过滤文字干扰。
(2)为什么进行形态学闭操作?
答:因为经过二值化处理后,二值图像中会出现许多微小的颗粒,经过闭操作可以消除这些颗粒的影响。
(3)为什么要进行直线检测?
答:通过直线检测可以确定边缘轮廓的上下左右四条直线,再通过直线拟合,解二元一次方程组后便可计算出四个交点的座标。
(4)关于透视变换。
在opencv提供的函数里有getPerspectiveTransform,他需要提供原图像的四个角点座标,和输出图像的四个角点座标,这也是上面为什么要进行直线检测的原因。
注意:warpPerspective而不是warpAffine,真的不要太蠢,用成了warpAffine,找了半天bug才发现是自己用错了函数,简直不要太蠢!而给出的错误提示也很有意思,是这样的
Assertion failed ((M0.type() == 5 || M0.type() == 6) && M0.rows == 2 && M0.cols == 3) in warpAffine
这是说生成的变换矩阵有问题,到底什么问题呢?驴唇不对马嘴能没问题嘛。
所以说,遇到bug不要急着找解决方案,大骂什么太垃圾,百分之九十九的问题是你自己的代码有问题,还是仔细审核一下代码为上。