问题描述
我有一张可爱的魔方的照片:
我想将其拆分为正方形并标识每个正方形的颜色。我可以先对它进行Guassian模糊处理,然后再进行“ Canny”处理,然后再执行“ Dilate”处理,以获取以下信息:
这看起来不错,但我无法将其变成正方形。我尝试的任何类型的“ findContours”都只会显示一个或两个正方形。我的目标是九点。人们对此有什么想法吗?
当前最佳解决方案:
下面的代码需要numpy + opencv2。它需要一个名为“ ./sides/rubiks-side-F.png”的文件,并将多个文件输出到“ steps”文件夹。
import numpy as np
import cv2 as cv
def save_image(name,file):
return cv.imwrite('./steps/' + name + '.png',file)
def angle_cos(p0,p1,p2):
d1,d2 = (p0-p1).astype('float'),(p2-p1).astype('float')
return abs(np.dot(d1,d2) / np.sqrt(np.dot(d1,d1)*np.dot(d2,d2)))
def find_squares(img):
img = cv.GaussianBlur(img,(5,5),0)
squares = []
for gray in cv.split(img):
bin = cv.Canny(gray,500,700,apertureSize=5)
save_image('post_canny',bin)
bin = cv.dilate(bin,None)
save_image('post_dilation',bin)
for thrs in range(0,255,26):
if thrs != 0:
_retval,bin = cv.threshold(gray,thrs,cv.THRESH_BINARY)
save_image('threshold',bin)
contours,_hierarchy = cv.findContours(
bin,cv.RETR_LIST,cv.CHAIN_APPROX_SIMPLE)
for cnt in contours:
cnt_len = cv.arcLength(cnt,True)
cnt = cv.approxpolyDP(cnt,0.02*cnt_len,True)
if len(cnt) == 4 and cv.contourArea(cnt) > 1000 and cv.isContourConvex(cnt):
cnt = cnt.reshape(-1,2)
max_cos = np.max(
[angle_cos(cnt[i],cnt[(i+1) % 4],cnt[(i+2) % 4]) for i in range(4)])
if max_cos < 0.2:
squares.append(cnt)
return squares
img = cv.imread("./sides/rubiks-side-F.png")
squares = find_squares(img)
cv.drawContours(img,squares,-1,(0,0),3)
save_image('squares',img)
您可以找到其他两面here
解决方法
我知道您可能不会接受此答案,因为它是用C++
编写的。没关系;我只想向您展示一种可能方法来检测正方形。如果您希望将此代码移植到Python
,我将尝试包括尽可能多的细节。
目标是尽可能准确地检测所有9
平方。这些是步骤:
- 获取一个边缘遮罩,其中整个立方体的轮廓是 清晰可见。
- 过滤这些边缘以获得二进制立方体(分段)蒙版。
- 使用立方体蒙版获取立方体的边界框/矩形。
- 使用边界矩形获取尺寸和位置 每个正方形(所有正方形具有恒定的尺寸)。
首先,我将尝试使用您描述的步骤来获得边缘遮罩。我只想确保我到达与您当前所在位置相似的起点。
管道是这样的:read the image > grayscale conversion > Gaussian Blur > Canny Edge detector
:
//read the input image:
std::string imageName = "C://opencvImages//cube.png";
cv::Mat testImage = cv::imread( imageName );
//Convert BGR to Gray:
cv::Mat grayImage;
cv::cvtColor( testImage,grayImage,cv::COLOR_RGB2GRAY );
//Apply Gaussian blur with a X-Y Sigma of 50:
cv::GaussianBlur( grayImage,cv::Size(3,3),50,50 );
//Prepare edges matrix:
cv::Mat testEdges;
//Setup lower and upper thresholds for edge detection:
float lowerThreshold = 20;
float upperThreshold = 3 * lowerThreshold;
//Get Edges via Canny:
cv::Canny( grayImage,testEdges,lowerThreshold,upperThreshold );
好的,这是起点。这是我得到的边缘蒙版:
接近您的结果。现在,我将应用膨胀。在这里,操作的迭代次数很重要,因为我想要漂亮的浓密边缘。还需要关闭打开的轮廓,因此,我希望进行轻微的扩张。我使用矩形结构元素设置iterations = 5
的数量。
//Prepare a rectangular,3x3 structuring element:
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT,3) );
//OP iterations:
int dilateIterations = 5;
//Prepare the dilation matrix:
cv::Mat binDilation;
//Perform the morph operation:
cv::morphologyEx( testEdges,binDilation,cv::MORPH_DILATE,SE,cv::Point(-1,-1),dilateIterations );
我明白了:
到目前为止,这是具有良好且非常定义的边缘的输出。最重要的是清楚定义多维数据集,因为稍后我将依靠其轮廓来计算bounding rectangle
。
接下来是我尝试尽可能准确地从所有其他物体中清洁立方体的边缘。如您所见,有很多垃圾和像素不属于多维数据集。我对泛滥背景颜色(白色)与立方体(黑色)不同的背景特别感兴趣,以便获得很好的分割效果。
但是, Flood-filling
有一个缺点。如果未闭合,它也可以填充轮廓的内部。我尝试用“边界蒙版”一次性清理垃圾并封闭轮廓,边界蒙版只是膨胀蒙版侧面的白线。
我将此遮罩实现为与扩张遮罩接壤的 4条超粗线。要应用这些线,我需要开始和结束点,它们对应于图像角。这些定义在vector
中:
std::vector< std::vector<cv::Point> > imageCorners;
imageCorners.push_back( { cv::Point(0,0),cv::Point(binDilation.cols,0) } );
imageCorners.push_back( { cv::Point(binDilation.cols,binDilation.rows) } );
imageCorners.push_back( { cv::Point(binDilation.cols,binDilation.rows),cv::Point(0,binDilation.rows) } );
imageCorners.push_back( { cv::Point(0,0) } );
四个条目的向量中的四个开始/结束坐标。我应用“边界蒙版”遍历这些坐标并绘制粗线:
//Define the SUPER THICKNESS:
int lineThicness = 200;
//Loop through my line coordinates and draw four lines at the borders:
for ( int c = 0 ; c < 4 ; c++ ){
//Get current vector of points:
std::vector<cv::Point> currentVect = imageCorners[c];
//Get the starting/ending points:
cv::Point startPoint = currentVect[0];
cv::Point endPoint = currentVect[1];
//Draw the line:
cv::line( binDilation,startPoint,endPoint,cv::Scalar(255,255,255),lineThicness );
}
很酷。这使我得到以下输出:
现在,让我们应用floodFill
算法。此操作将用“替代”颜色填充相同颜色像素的封闭区域。它需要一个种子点和替代颜色(在这种情况下为白色)。让我们在刚创建的白色蒙版内的四个角处填充水。
//Set the offset of the image corners. Ensure the area to be filled is black:
int fillOffsetX = 200;
int fillOffsetY = 200;
cv::Scalar fillTolerance = 0; //No tolerance
int fillColor = 255; //Fill color is white
//Get the dimensions of the image:
int targetCols = binDilation.cols;
int targetRows = binDilation.rows;
//Flood-fill at the four corners of the image:
cv::floodFill( binDilation,cv::Point( fillOffsetX,fillOffsetY ),fillColor,(cv::Rect*)0,fillTolerance,fillTolerance);
cv::floodFill( binDilation,targetRows - fillOffsetY ),cv::Point( targetCols - fillOffsetX,fillTolerance);
这也可以实现为循环,就像“边框蒙版”一样。完成此操作后,我得到了这个面具:
靠近吧?现在,根据您的图像,所有这些“清理”操作都会使一些垃圾幸存。我建议应用area filter。区域过滤器将删除阈值区域以下的每个像素斑点。这很有用,因为多维数据集的斑点是蒙版上最大的斑点,这些斑点肯定会在区域过滤器中幸存下来。
无论如何,我只是对立方体的轮廓感兴趣;我不需要多维数据集中的那些行。我要扩大(倒置)斑点的地狱,然后侵蚀回到原始尺寸,以消除立方体中的线条:
//Get the inverted image:
cv::Mat cubeMask = 255 - binDilation;
//Set some really high iterations here:
int closeIterations = 50;
//Dilate
cv::morphologyEx( cubeMask,cubeMask,closeIterations );
//Erode:
cv::morphologyEx( cubeMask,cv::MORPH_ERODE,closeIterations );
这是关闭操作。而且很残酷,这是应用它的结果。记得我以前把图像倒过来了:
那不是很好吗?还是什么?签出立方体遮罩,此处已覆盖到原始RBG图像中:
好极了,现在让我们获取该Blob的边界框。方法如下:
Get blob contour > Convert contour to bounding box
这非常容易实现,等效的Python
应该与此类似。首先,通过findContours
获取轮廓。如您所见,应该只一个轮廓:立方体轮廓。接下来,使用boundingRect
将轮廓转换为边界矩形。在C++
中,代码如下:
//Lets get the blob contour:
std::vector< std::vector<cv::Point> > contours;
std::vector<cv::Vec4i> hierarchy;
cv::findContours( cubeMask,contours,hierarchy,CV_RETR_TREE,CV_CHAIN_APPROX_SIMPLE,0) );
//There should be only one contour,the item number 0:
cv::Rect boundigRect = cv::boundingRect( contours[0] );
这些是找到的轮廓(仅一个):
一旦将该轮廓转换为边界矩形,就可以得到以下漂亮的图像:
啊,我们到这里很近。由于所有正方形都具有相同的尺寸,并且您的图像似乎没有很大的透视失真,因此我们可以使用边界矩形来估算正方形尺寸。所有正方形的宽度和高度都相同,每个立方体宽度有3个正方形,每个立方体高度有3个正方形。
将边界矩形划分为9个相等的子正方形(或者,我称之为“网格”),并从边界框的坐标开始获取其尺寸和位置,如下所示:
//Number of squares or "grids"
int verticalGrids = 3;
int horizontalGrids = 3;
//Grid dimensions:
float gridWidth = (float)boundigRect.width / 3.0;
float gridHeight = (float)boundigRect.height / 3.0;
//Grid counter:
int gridCounter = 1;
//Loop thru vertical dimension:
for ( int j = 0; j < verticalGrids; ++j ) {
//Grid starting Y:
int yo = j * gridHeight;
//Loop thru horizontal dimension:
for ( int i = 0; i < horizontalGrids; ++i ) {
//Grid starting X:
int xo = i * gridWidth;
//Grid dimensions:
cv::Rect gridBox;
gridBox.x = boundigRect.x + xo;
gridBox.y = boundigRect.y + yo;
gridBox.width = gridWidth;
gridBox.height = gridHeight;
//Draw a rectangle using the grid dimensions:
cv::rectangle( testImage,gridBox,cv::Scalar(0,5 );
//Int to string:
std::string gridCounterString = std::to_string( gridCounter );
//String position:
cv::Point textPosition;
textPosition.x = gridBox.x + 0.5 * gridBox.width;
textPosition.y = gridBox.y + 0.5 * gridBox.height;
//Draw string:
cv::putText( testImage,gridCounterString,textPosition,cv::FONT_HERSHEY_SIMPLEX,1,3,cv::LINE_8,false );
gridCounter++;
}
}
在这里,对于每个网格,我都在绘制其矩形,并在其中心绘制一个漂亮的数字。绘制矩形函数需要一个已定义的矩形:使用gridBox
类型的cv::Rect
变量定义的左上起始坐标以及矩形的宽度和高度。
这是一个很酷的动画,说明如何将多维数据集分为9个网格:
这是最后一张图片!
一些建议:
- 您的源图像太大,请尝试将其调整为较小的尺寸,然后进行操作
并按比例缩小结果。 - 实施区域过滤器。摆脱小事非常方便 像素斑点。
- 取决于您的图片(我刚刚对您发布到
问题)以及摄像机引入的透视失真,
简单的
contour
至boundingRect
可能还不够。在这种情况下, 另一种方法是获取立方体轮廓的四个点 通过强线检测。