1. 程式人生 > >OpenCV實踐之路——opencv玩數獨之一九宮格輪廓提取與透視變換

OpenCV實踐之路——opencv玩數獨之一九宮格輪廓提取與透視變換

本文由@星沉閣冰不語出品,轉載請註明作者和出處。

本文部分參考自如下連結:Sudoku-recognizer。前幾天發現了這個網頁,覺得挺好玩的,就想自己實現一下。本以為只是把程式碼從Python轉換到C++是一件很簡單的事情,經過這幾天的努力發現是自己想的太簡單了。到現在也沒有完全實現。前面的一小半可以說是參考了上述文章,所做的只是把opencv的程式碼轉換下語言風格,但是到了後面隨著Python語言用的越來越多,很多地方已經完全看不懂了,只能自己實現。而且隨著深入,自己的想法也越來越多。所以現在幾乎已經跳出上文的框架,開始自己去查資料查文件按照自己的思路實現了。但是考慮到完全實現耗時過長而且不確定什麼時候能除錯完成,加之如果等一切搞定再來記錄將會是一篇十分冗長的博文,於是就這樣一邊實現一邊記錄吧。先從最基礎的開始。

還是先說要實現什麼樣的效果吧。給一張帶有九宮格數獨的照片,檢測出其中的九宮格,然後取出來做透視變換。看圖的畫會更加一目瞭然。




主要思路:

一、高斯濾波去掉部分噪音,拉普拉斯銳化增強輪廓以便於檢測提取;自適應閾值化得到二值影象;

二、輪廓檢測,多邊形逼近,多邊形篩選;

三、根據篩選出的四邊形的四個頂點進行透視變換(頂點順序很重要);

這裡面需要注意的細節有以下幾點:

一、自適應閾值化的時候最後兩個引數的調整特別重要,這兩個引數決定著能不能快速檢測到需要的輪廓。如果找不到,就繼續調整這倆值。

二、findContours()查詢輪廓時,方法選取CV_CHAIN_APPROX_SIMPLE,這種方法只是儲存輪廓的頂點,後面透視變換時用到的頂點就是從這裡面提出來的。

三、輪廓篩選的時候可以用上述頂點數來選出四邊形,然後根據面積篩選出目標四邊形。

四、透視變換的時候頂點的對應順序要正確,最好按照左上,右上,左下,右下的順序。變換前後都是這個順序。

五、獲取透視變換矩陣的時候用到的點的格式應為Point2f,變換矩陣是一個3*3的矩陣,我得到的變換矩陣如下:

                                                                                                               

這一部分主要程式碼如下:

#include<opencv.hpp>
#include<iostream>

using namespace std;
using namespace cv;

int main()
{
	Mat src = imread("shudu.jpg");

	GaussianBlur(src, src, Size(3, 3), 0, 0);

	//拉普拉斯銳化
	Mat kernel(3, 3, CV_32F, Scalar(-1));
	kernel.at<float>(1, 1) = 8.9;
	filter2D(src, src, src.depth(), kernel);

	Mat gray, thresh;
	cvtColor(src, gray, CV_BGR2GRAY);

	//namedWindow("thresh",0);

	//輪廓提取
	adaptiveThreshold(gray, thresh, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY_INV,77,15);

	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3));
	erode(thresh, thresh, element);
	dilate(thresh, thresh, element);

	vector<vector<Point> > contours0;
	vector<Vec4i> hierarchy;
	findContours(thresh, contours0, hierarchy, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE, Point());


	//多邊形逼近
	vector<vector<Point> > contours;
	contours.resize(contours0.size());

	for (int i = 0; i < contours0.size(); i++)
	{
		approxPolyDP(contours0[i], contours[i], 55, true);//15是為了得到一個矩形,小於15的數回得到更多的點
	}

	//選出最大面積的多邊形
	double area = 0;
	int index=0;
	for (int i = 0; i < contours.size(); i++)
	{
		if (contourArea(contours[i])>area)
		{
			area = contourArea(contours[i]);
			index = i;
		}
	}
	
	//最外圍輪廓的顯示
	if (contourArea(contours[index])>50000)
	{
		Scalar color(0, 0, 255);
		drawContours(src, contours, index, color, 4, 8);
	}


	//cout << "contours[index]: " << endl << contours[index] << endl;

	//最外圍輪廓頂點的顯示
	//for (int i = 0; i < contours[index].size(); i++)
	//{
	//	circle(src, contours[index][i], 15, (0,0,255), 2, 8, 0);
	//}
	

	//透視變換,頂點的順序很重要!
	vector<Point2f> corner;//上面提取輪廓的頂點
	corner.push_back(Point(83, 80));
	corner.push_back(Point(652, 61));
	corner.push_back(Point(13, 548));	
	corner.push_back(Point(798, 495));

	vector<Point2f> PerspectiveTransform;//透視變換後的頂點
	RotatedRect box = minAreaRect(cv::Mat(contours[index]));
	PerspectiveTransform.push_back(Point(0, 0));
	PerspectiveTransform.push_back(Point(box.boundingRect().width - 1, 0));
	PerspectiveTransform.push_back(Point(0, box.boundingRect().height - 1));
	PerspectiveTransform.push_back(Point(box.boundingRect().width - 1, box.boundingRect().height - 1));
	 
	//cout << "corner: " << endl << corner << endl;

	//獲取變換矩陣
	Mat M = getPerspectiveTransform(corner, PerspectiveTransform);//Order of points matters!
	
	//cout << "PerspectiveTransform: " << endl << PerspectiveTransform << endl;

	Mat out;//提取出的數獨方框
	cv::Size size(box.boundingRect().width, box.boundingRect().height);
	warpPerspective(src, out, M,size, 1, 0, 0);

<span style="white-space:pre">	</span>imshow("src",src);
<span style="white-space:pre">	</span>imshow("out",out);
	while (uchar(waitKey()) == 'q') { }
	return 0;
}

未完待續......

最近開通了微信公眾號,感興趣的同學可以掃碼在微信上交流。