质量声明:原创文章,内容质量问题请评论吐槽。如对您产生干扰,可私信删除。
主要参考:https://blog.csdn.net/qq_24946843/article/details/82772654
文章目录
摘要: 先查找卡片轮廓, 再根据Hough线检测确定卡片的四个顶点坐标, 最后通过透视变换对畸变图像进行矫正和裁剪.
OpenCV 参数说明
寻找轮廓 findContours
void cv::findContours( InputArray image,
OutputArrayOfArrays contours,
int mode,
int method,
Point offset = Point()
)
参数:
- image 单通道二值图像
- contours 存放每条轮廓的坐标点, 可定义为 vector<vector<cv::Point>> contours
- mode 轮廓检索模式
- method 轮廓近似方法
轮廓检索模式 ENUM | 说明 |
---|---|
RETR_EXTERNAL | 仅检索极端的外部轮廓 |
RETR_LIST | 检索所有轮廓, 但不建立任何层次关系 |
RETR_CCOMP | 检索所有轮廓, 并建立两级嵌套层次 |
RETR_TREE | 检索所有轮廓,并建立完整嵌套层次 |
RETR_FLOODFILL |
轮廓近似算法 | 说明 |
---|---|
CHAIN_APPROX_NONE | 存储所有轮廓点 |
CHAIN_APPROX_SIMPLE | 压缩水平\竖直\对角轮廓线, 只保留直线型轮廓的端点 |
CHAIN_APPROX_TC89_L1 | Teh-Chin链近似算法 1 |
CHAIN_APPROX_TC89_KCOS | Teh-Chin链近似算法 2 |
线检测 HoughLinesP
void cv::HoughLinesP( InputArray image,
OutputArray lines,
double rho,
double theta,
int threshold,
double minLineLength = 0,
double maxLineGap = 0
)
参数:
- image 单通道二值图像
- lines 存放检测到的每条线段的两个端点坐标, 可定义为 vector<cv::Vec4i> lines
- rho 累加平面的距离分辨率(以像素为单位), 一般取 1
- theta 累加平面的角度分辨率(以弧度为单位), 一般取 CV_PI / 180
- threshold 累加平面的直线判定阈值, 可以等于 minLineLength
- minLineLength 检测线段的最小长度
- maxLineGap 共线线段间的最小间隔
注: 累加平面可看做由θ弧度,大小为ρ像素的单元组成的二维直方图, 详见 <学习OpenCV3> P312.
求取透视变换矩阵 getPerspectiveTransform
Mat cv::getPerspectiveTransform ( const Point2f src[],
const Point2f dst[],
int solveMethod = DECOMP_LU
)
参数:
- src[] 存放原始图像中四个基准点的坐标, 可定义为 cv::Point2f srcCorners[] = { pt1, pt2, pt3, pt4 };
- dst[] 存放变换后图像中对应基准点坐标
- solveMethod 计算方法, 默认为LU分解
透视变换 warpPerspective
void cv::warpPerspective( InputArray src,
OutputArray dst,
InputArray M,
Size dsize,
int flags = INTER_LINEAR,
int borderMode = BORDER_CONSTANT,
const Scalar & borderValue = Scalar()
)
参数:
- src 输入图像
- dst 输出图像, 与src同规格, 大小为 dsize
- M 3x3的变换矩阵, 可通过 getPerspectiveTransform() 求得
- dsize 输出图像的尺寸
- flags 插值算法与可选标志 WARP_INVERSE_MAP(设置M为逆变换阵) 的组合
- borderMode 附加边框/像素外推的方式, BORDER_CONSTANT 或 BORDER_REPLICATE
- borderValue BORDER_CONSTANT 对应的固定像素值
代码实现: 自动识别锚点
- 主函数
#include <iostream>
#include "opencv2/highgui/highgui.hpp" // 窗口显示
#include "opencv2/core/core.hpp" // 数***算
#include "opencv2/imgproc/imgproc.hpp" // 图像处理
using namespace std;
cv::Point2i getCrossPoint(cv::Vec4i lineA, cv::Vec4i lineB);
void correct(cv::Mat& img, cv::Mat& dst);
int main(int arc, char** argv)
{
cv::Mat src = cv::imread("card.jpg");
imshow("src", src);
cv::Mat dst;
correct(src, dst);
imshow("dst", dst);
cv::waitKey(0);
return 0;
}
- 计算直线交点坐标
cv::Point2i getCrossPoint(cv::Vec4i lineA, cv::Vec4i lineB)
{
cv::Point2i crossPt;
auto k1 = ((double)lineA[3] - lineA[1]) / ((double)lineA[2] - lineA[0] + 1e-4);
auto k2 = ((double)lineB[3] - lineB[1]) / ((double)lineB[2] - lineB[0] + 1e-4);
auto b1 = lineA[1] - k1 * lineA[0];
auto b2 = lineB[1] - k2 * lineB[0];
if(k1 > 1e6)
{
crossPt.x = lineA[0];
crossPt.y = cvRound(k2 * crossPt.x + b2);
}
else if(k2 > 1e6)
{
crossPt.x = lineB[0];
crossPt.y = cvRound(k1 * crossPt.x + b1);
}
else
{
crossPt.x = cvRound(-(b1 - b2) / (k1 - k2));
crossPt.y = cvRound(k1 * crossPt.x + b1);
}
return crossPt;
}
- 畸变矫正
void correct(cv::Mat& src, cv::Mat& dst)
{
cv::Mat temp;
// 灰度化
cv::Mat& imgray = temp;
cv::cvtColor(src, imgray, cv::COLOR_BGR2GRAY);
// 二值化
cv::Mat& imbin = temp;
cv::threshold(imgray, imbin, 0, 255, cv::THRESH_BINARY_INV + cv::THRESH_OTSU);
//cv::imshow("imbin.jpg", imbin);
// 闭操作
cv::Mat& immorph = temp;
cv::Mat SE = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(3, 3));
cv::morphologyEx(imbin, immorph, cv::MORPH_CLOSE, SE, cv::Point(-1, -1), 3);
//cv::imshow("immorph.jpg", immorph);
// 寻找轮廓
vector<vector<cv::Point>> contours;
cv::findContours(immorph, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 绘制轮廓
cv::Mat drawing = cv::Mat::zeros(src.size(), CV_8UC1);
for(int i = 1; i < contours.size(); i++)
{
cv::Rect rectangle = cv::boundingRect(contours[i]);
if(rectangle.width < src.cols / 2 && rectangle.height < src.rows / 2) continue;
cv::drawContours(drawing, contours, i, 255, 2);
}
cv::imshow("contours.jpg", drawing);
// 线检测
vector<cv::Vec4i> lines;
cv::HoughLinesP(drawing, lines, 1, CV_PI / 180, src.rows / 2, src.rows / 2);
// 绘制直线
drawing = cv::Mat::zeros(src.size(), CV_8UC1);
for(int i = 0; i < lines.size(); i++)
{
cv::Point pt1(lines[i][0], lines[i][1]);
cv::Point pt2(lines[i][2], lines[i][3]);
cv::line(drawing, pt1, pt2, 255, 1);
}
cv::imshow("lines.jpg", drawing);
// 定位直线
cv::Vec4i topLine, bottomLine, leftLine, rightLine;
for(int j = 0; j < lines.size(); j++)
{
cv::Vec4i ln = lines[j];
if(ln[1] < src.rows / 2 && ln[3] < src.rows / 2) topLine = ln;
if(ln[1] > src.rows / 2 && ln[3] > src.rows / 2) bottomLine = ln;
if(ln[0] < src.cols / 2 && ln[2] < src.cols / 2) leftLine = ln;
if(ln[0] > src.cols / 2 && ln[2] > src.cols / 2) rightLine = ln;
}
// 求解交点坐标
cv::Point pt1, pt2, pt3, pt4;
pt1 = getCrossPoint(topLine, leftLine); // 左上角
pt2 = getCrossPoint(topLine, rightLine); // 右上角
pt3 = getCrossPoint(bottomLine, rightLine); // 右下角
pt4 = getCrossPoint(bottomLine, leftLine); // 左下角
// 绘制交点
cv::circle(drawing, pt1, 2, 255);
cv::circle(drawing, pt2, 2, 255);
cv::circle(drawing, pt3, 2, 255);
cv::circle(drawing, pt4, 2, 255);
cv::imshow("lines.jpg", drawing);
// 求解透视变换矩阵
int n = cvFloor(min(src.cols / 85, src.rows / 54)); // 通用尺寸 85x54
int dstWidth = n * 85;
int dstHeight = n * 54;
cv::Point2f srcCorners[] = { pt1, pt2, pt3, pt4 };
cv::Point2f dstCorners[] = {
cv::Point(0, 0),
cv::Point(dstWidth - 1, 0),
cv::Point(dstWidth - 1, dstHeight - 1),
cv::Point(0, dstHeight - 1)
};
cv::Mat warpMatrix = cv::getPerspectiveTransform(srcCorners, dstCorners);
// 透视变换
cv::warpPerspective(src, dst, warpMatrix, cv::Size(dstWidth, dstHeight));
}
- 结果展示
代码实现: 鼠标点选锚点
#include <iostream>
#include "opencv2/highgui/highgui.hpp" // 窗口显示
#include "opencv2/core/core.hpp" // 数***算
#include "opencv2/imgproc/imgproc.hpp" // 图像处理
using namespace std;
vector<cv::Point> anchors;
void onMouse(int event, int x, int y, int flag, void* userdata);
int main()
{
bool runFlag = true;
cv::Mat dst, src = cv::imread("background.jpg");
while(runFlag)
{
// 手动标点
cv::imshow("drawing", src);
setMouseCallback("drawing", onMouse, &src); // 回调函数
cv::waitKey(0);
cv::destroyAllWindows();
// 求取透视变换矩阵
int roiWidth = 700, roiHeight = 550;
cv::Point2f dstCorners[] = {
cv::Point(0, 0),
cv::Point(roiWidth - 1, 0),
cv::Point(roiWidth - 1, roiHeight - 1),
cv::Point(0, roiHeight - 1)
};
cv::Point2f srcCorners[] = { anchors[0],anchors[1],anchors[2],anchors[3] };
cv::Mat warpMatrix = cv::getPerspectiveTransform(srcCorners, dstCorners);
// 透视变换
cv::Size roiSize = cv::Size(roiWidth, roiHeight);
cv::warpPerspective(src, dst, warpMatrix, roiSize);
imshow("dst", dst);
// 重新执行
while(true)
{
char key = cv::waitKey(10);
if(key == 32)
{
anchors.clear();
cv::destroyAllWindows();
break;
}
if(key == 27)
{
cv::destroyAllWindows();
runFlag = false;
break;
}
}
}
cv::imwrite("board.jpg", dst);
return 0;
}
void onMouse(int event, int x, int y, int flag, void* userdata)
{
cv::Mat& src = *(cv::Mat*) userdata; // 空指针变量 -> Mat型指针变量 -> Mat变量 -> 引用
cv::Mat drawing = src.clone();
cv::Point pt1, pt2;
switch(event)
{
case cv::EVENT_LBUTTONDOWN: // 按下左键选定
{
if(anchors.size() < 4)
{
anchors.push_back(cv::Point(x, y));
for(auto x : anchors)
cout << x << "\t";
cout << endl;
}
break;
}
case cv::EVENT_MOUSEMOVE: // 鼠标移动
{
if(anchors.size() > 0 and anchors.size() < 4)
{
drawing = src.clone();
pt1 = anchors.back();
pt2 = cv::Point(x, y);
line(drawing, pt1, pt2, cv::Scalar(50, 200, 50), 2);
}
break;
}
case cv::EVENT_RBUTTONDOWN: // 按下右键删除
{
if(anchors.size() > 0)
{
anchors.pop_back();
drawing = src.clone();
}
break;
}
}
// 绘制标记点
for(auto pt : anchors)
{
circle(drawing, pt, 3, cv::Scalar(50, 180, 50), -1);
}
cv::imshow("drawing", drawing);
}
代码实现: 视频矫正畸变并保存
#include <iostream>
#include "opencv2/highgui/highgui.hpp" // 窗口显示
#include "opencv2/core/core.hpp" // 数***算
#include "opencv2/imgproc/imgproc.hpp" // 图像处理
using namespace std;
string videoName = "../videos/2019-12-12 white droplet.avi";
string outputName = "../videos/white droplet.avi";
//string videoName = "../videos/2019-12-12 red droplet.avi";
//string outputName = "../videos/red droplet.avi";
vector<cv::Point> anchors;
string winname = "select anchors & perspective transform";
void onMouse(int event, int x, int y, int flag, void* userdata);
int main(int argc, char* argv[])
{
// 读入离线视频
cv::VideoCapture video;
video.open(videoName);
assert(video.isOpened());
// 读取第一帧
cv::Mat frame, image;
video >> frame;
cv::imshow(winname, frame);
// 标记锚点
setMouseCallback(winname, onMouse, &frame);
cv::waitKey(0); // 标记完成后, 按任意键继续
// 求取透视变换矩阵
int roiWidth = 700, roiHeight = 550;
cv::Point2f dstCorners[] = {
cv::Point(0, 0),
cv::Point(roiWidth - 1, 0),
cv::Point(roiWidth - 1, roiHeight - 1),
cv::Point(0, roiHeight - 1)
};
cv::Point2f srcCorners[] = { anchors[0],anchors[1],anchors[2],anchors[3] };
cv::Mat warpMatrix = cv::getPerspectiveTransform(srcCorners, dstCorners);
// 透视变换结果预览
cv::Size roiSize = cv::Size(roiWidth, roiHeight);
cv::warpPerspective(frame, image, warpMatrix, roiSize);
cv::putText(image, "[Space] Continue", cv::Point(20, 40), cv::FONT_HERSHEY_SIMPLEX, 1, 0);
cv::putText(image, "[ ESC ] Abort", cv::Point(20, 80), cv::FONT_HERSHEY_SIMPLEX, 1, 0);
imshow("preview", image);
// 是否执行透视变换
bool runFlag = true;
while(runFlag)
{
char key = (char)cv::waitKey(1);
switch(key)
{
case 27: // ESC
video.release();
cv::destroyAllWindows();
cout << "\nExit." << endl;
return 1;
case 32: // Space
runFlag = false;
cv::destroyAllWindows();
cout << "\nStart Perspective Tranformation... " << endl;
break;
default:
break;
}
}
// 逐帧执行透视变换
cv::VideoWriter recorder;
int fourcc = recorder.fourcc('M', 'J', 'P', 'G'); // avi格式编码
int fps = video.get(cv::CAP_PROP_FPS);
recorder.open(outputName, fourcc, fps, roiSize);
assert(recorder.isOpened());
video.set(cv::CAP_PROP_POS_FRAMES, 0);
int counter = 0;
while(true)
{
video >> frame;
if(frame.empty()) break;
cv::warpPerspective(frame, image, warpMatrix, roiSize);
recorder << image;
printf("\rFrame: %d", counter++);
}
video.release();
recorder.release();
cout << "\nDone." << endl;
return 0;
}
void onMouse(int event, int x, int y, int flag, void* userdata)
{
cv::Mat& src = *(cv::Mat*) userdata; // 空指针变量 -> Mat型指针变量 -> Mat变量 -> 引用
cv::Mat drawing = src.clone();
cv::Point pt1, pt2;
switch(event)
{
case cv::EVENT_LBUTTONDOWN: // 按下左键选定
{
if(anchors.size() < 4)
{
anchors.push_back(cv::Point(x, y));
for(auto x : anchors)
cout << x << "\t";
cout << endl;
}
break;
}
case cv::EVENT_MOUSEMOVE: // 鼠标移动
{
if(anchors.size() > 0 and anchors.size() < 4)
{
drawing = src.clone();
pt1 = anchors.back();
pt2 = cv::Point(x, y);
line(drawing, pt1, pt2, cv::Scalar(50, 200, 50), 2);
}
break;
}
case cv::EVENT_RBUTTONDOWN: // 按下右键删除
{
if(anchors.size() > 0)
{
anchors.pop_back();
drawing = src.clone();
}
break;
}
}
// 绘制标记点
for(auto pt : anchors)
{
circle(drawing, pt, 3, cv::Scalar(50, 180, 50), -1);
}
// 添加描述文字
cv::putText(drawing, "select | delete", cv::Point(20, 40), cv::FONT_HERSHEY_SIMPLEX, 1, 0);
if(anchors.size() == 4)
cv::putText(drawing, "Press any key to continue", cv::Point(20, 80), cv::FONT_HERSHEY_SIMPLEX, 1, 0);
cv::imshow(winname, drawing);
}