质量声明:原创文章,内容质量问题请评论吐槽。如对您产生干扰,可私信删除。
主要参考: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);
}