我知道你可能不会接受这个答案,因为它是用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, 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 );
好的,这是起点。这是我得到的边缘蒙版:
接近你的结果。现在,我将应用 dilation。在这里,操作的迭代次数很重要,因为我想要漂亮、厚的边缘。关闭打开的轮廓也是需要的,所以,我想要一个温和的扩张。我使用矩形结构元素设置iterations = 5 的数量。
//Prepare a rectangular, 3x3 structuring element:
cv::Mat SE = cv::getStructuringElement( cv::MORPH_RECT, cv::Size(3, 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 有一个缺点。如果它不是闭合的,它也可以填充轮廓的内部。我尝试使用“边框蒙版”一次性清理垃圾并关闭轮廓,这只是扩张蒙版侧面的白线。
我将此蒙版实现为 四条超粗线,它们与扩张蒙版接壤。要应用线条,我需要 starting 和 ending 点,它们对应于图像角。这些在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,0), 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,binDilation.rows), 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, cv::Point( fillOffsetX, targetRows - fillOffsetY ), fillColor, (cv::Rect*)0, fillTolerance, fillTolerance);
cv::floodFill( binDilation, cv::Point( targetCols - fillOffsetX, fillOffsetY ), fillColor, (cv::Rect*)0, fillTolerance, fillTolerance);
cv::floodFill( binDilation, cv::Point( targetCols - fillOffsetX, targetRows - fillOffsetY ), fillColor, (cv::Rect*)0, fillTolerance, fillTolerance);
这也可以实现为循环,就像“边框掩码”一样。完成此操作后,我得到了这个掩码:
接近了,对吧?现在,根据您的图像,一些垃圾可以在所有这些“清理”操作中幸存下来。我建议申请area filter。区域过滤器将删除阈值区域以下的每个像素块。这很有用,因为立方体的斑点是蒙版上最大的斑点,它们肯定会在区域过滤器中幸存下来。
不管怎样,我只是对立方体的轮廓感兴趣;我不需要立方体内的那些线。我将膨胀(倒置的)blob,然后腐蚀回到原始尺寸以消除立方体内的线条:
//Get the inverted image:
cv::Mat cubeMask = 255 - binDilation;
//Set some really high iterations here:
int closeIterations = 50;
//Dilate
cv::morphologyEx( cubeMask, cubeMask, cv::MORPH_DILATE, SE, cv::Point(-1,-1), closeIterations );
//Erode:
cv::morphologyEx( cubeMask, cubeMask, cv::MORPH_ERODE, SE, cv::Point(-1,-1), 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, cv::Point(0, 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,0,255), 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, cv::Scalar(255,0,0), 3, cv::LINE_8, false );
gridCounter++;
}
}
在这里,对于每个网格,我正在绘制它的矩形并在其中心绘制一个漂亮的数字。绘制矩形函数需要定义一个矩形:左上角起始坐标和矩形宽高,使用cv::Rect类型的gridBox变量定义。
这是一个很酷的动画,展示了立方体如何被分成 9 个网格:
这是最终的图片!
一些建议:
- 您的源图像太大,请尝试将其调整为较小的尺寸,操作
并缩小结果。
- 实施区域过滤器。摆脱小东西非常方便
像素块。
- 取决于您的图片(我刚刚测试过您在您的
问题)和相机引入的透视失真,a
简单的
contour 到 boundingRect 可能还不够。在这种情况下,
另一种方法是获取立方体轮廓的四个点
通过 霍夫线检测。