使用 OpenCV 移除圆形
我正在处理一个关于opencv的问题,想要找出哪些圆是填充的。不过,有时候圆的边缘会导致错误的判断。这让我想知道,是否可以通过把所有RGB中红色值高的像素变成白色来去掉这些圆。我打算先创建一个粉色像素的遮罩,然后从原始图像中减去这个遮罩,以去掉圆。目前我得到的是黑色的遮罩,看来我做错了什么。请给我一些指导。
rgb = cv2.imread(img, cv2.CV_LOAD_IMAGE_COLOR)
rgb_filtered = cv2.inRange(rgb, (200, 0, 90), (255, 110, 255))
cv2.imwrite('mask.png',rgb_filtered)
3 个回答
0
这是我完成这个任务的方法:
- 先把图片转换成灰度图,然后用高斯模糊来去掉噪点。
- 接着使用Otsu阈值处理,这个方法能很好地区分前景和背景,建议你了解一下。
- 然后使用霍夫圆变换来找出可能的圆形,遗憾的是这个步骤需要很多调试。也许使用分水岭分割会是个更好的选择。
- 从找到的圆形中提取感兴趣区域(ROI),并计算黑白像素的比例。
这是我的样本结果:

当我们把结果画到原始图片上时:

这是我的示例代码(抱歉用的是C++):
void findFilledCircles( Mat& img ){
Mat gray;
cvtColor( img, gray, CV_BGR2GRAY );
/* Apply some blurring to remove some noises */
GaussianBlur( gray, gray, Size(5, 5), 1, 1);
/* Otsu thresholding maximizes inter class variance, pretty good in separating background from foreground */
threshold( gray, gray, 0.0, 255.0, CV_THRESH_OTSU );
erode( gray, gray, Mat(), Point(-1, -1), 1 );
/* Sadly, this is tuning heavy, adjust the params for Hough Circles */
double dp = 1.0;
double min_dist = 15.0;
double param1 = 40.0;
double param2 = 10.0;
int min_radius = 15;
int max_radius = 22;
/* Use hough circles to find the circles, maybe we could use watershed for segmentation instead(?) */
vector<Vec3f> found_circles;
HoughCircles( gray, found_circles, CV_HOUGH_GRADIENT, dp, min_dist, param1, param2, min_radius, max_radius );
/* This is just to draw coloured circles on the 'originally' gray image */
vector<Mat> out = { gray, gray, gray };
Mat output;
merge( out, output );
float diameter = max_radius * 2;
float area = diameter * diameter;
Mat roi( max_radius, max_radius, CV_8UC3, Scalar(255, 255, 255) );
for( Vec3f circ: found_circles ) {
/* Basically we extract the region of the circles, and count the ratio of black pixels (0) and white pixels (255) */
Mat( gray, Rect( circ[0] - max_radius, circ[1] - max_radius, diameter, diameter ) ).copyTo( roi );
float filled_percentage = 1.0 - 1.0 * countNonZero( roi ) / area;
/* If more than half is filled, then maybe it's filled */
if( filled_percentage > 0.5 )
circle( output, Point2f( circ[0], circ[1] ), max_radius, Scalar( 0, 0, 255), 3 );
else
circle( output, Point2f( circ[0], circ[1] ), max_radius, Scalar( 255, 255, 0), 3 );
}
namedWindow("");
moveWindow("", 0, 0);
imshow("", output );
waitKey();
}
1
我尝试用Python来找出一个解决方案。基本的步骤如下:
- 先用高斯模糊来减少噪声。
- 然后使用Otsu的阈值方法。
- 接着找到没有父级的轮廓,这些轮廓应该是圆形。
- 最后检查每个轮廓内部白色像素和黑色像素的比例。
你可能需要调整白色像素的比例阈值,以适应你的应用。我用的是0.7,这个值看起来比较合理。
import cv2
import numpy
# Read image and apply gaussian blur
img = cv2.imread("circles.png", cv2.CV_LOAD_IMAGE_GRAYSCALE)
img = cv2.GaussianBlur(img, (5, 5), 0)
# Apply OTSU thresholding and reverse it so the circles are in the foreground (white)
_, otsu = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
otsu = cv2.bitwise_not(otsu).astype("uint8")
# Find contours that have no parent
contours, hierarchy = cv2.findContours(numpy.copy(otsu), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
parent_contours = [contours[idx] for idx, val in enumerate(hierarchy[0]) if val[3] == -1]
# Loop through all contours to check the ratio of white to black pixels inside each one
filled_circles_contours = list()
for contour in parent_contours:
contour_mask = numpy.zeros(img.shape).astype("uint8")
cv2.drawContours(contour_mask, [contour], -1, 1, thickness=-1)
white_len_mask = len(cv2.findNonZero(contour_mask))
white_len_thresholded = len(cv2.findNonZero(contour_mask * otsu))
white_ratio = float(white_len_thresholded) / white_len_mask
if white_ratio > 0.7:
filled_circles_contours.append(contour)
# Show image with detected circles
cv2.drawContours(img, filled_circles_contours, -1, (0, 0, 0), thickness=2)
cv2.namedWindow("Result")
cv2.imshow("Result", img)
cv2.waitKey(0)
这是我把上面的代码应用到你的图片后得到的结果:
5
这是我的解决方案。不幸的是,它也是用C++写的,下面是它的工作原理:
- 对图像进行阈值处理,以找出哪些部分是背景(白纸)。
- 通过提取轮廓来找到圆形。
- 现在每个轮廓都被认为是一个圆,所以计算包围这个轮廓的最小圆。如果输入数据没问题,就不需要调整参数(这意味着每个圆都是一个单独的轮廓,比如说圆可能不会因为绘制而连接在一起)。
检查每个圆形内部,看看里面是前景(绘画)像素多,还是背景(白纸)像素多(通过某个比例阈值)。
int main() { cv::Mat colorImage = cv::imread("countFilledCircles.png"); cv::Mat image = cv::imread("countFilledCircles.png", CV_LOAD_IMAGE_GRAYSCALE); // threshold the image! cv::Mat thresholded; cv::threshold(image,thresholded,0,255,CV_THRESH_BINARY_INV | CV_THRESH_OTSU); // save threshold image for demonstration: cv::imwrite("countFilledCircles_threshold.png", thresholded); // find outer-contours in the image these should be the circles! cv::Mat conts = thresholded.clone(); std::vector<std::vector<cv::Point> > contours; std::vector<cv::Vec4i> hierarchy; cv::findContours(conts,contours,hierarchy, CV_RETR_EXTERNAL, CV_C HAIN_APPROX_SIMPLE, cv::Point(0,0)); // colors in which marked/unmarked circle outlines will be drawn: cv::Scalar colorMarked(0,255,0); cv::Scalar colorUnMarked(0,0,255); // each outer contour is assumed to be a circle // TODO: you could first find the mean radius of all assumed circles and try to find outlier (dirt etc in the image) for(unsigned int i=0; i<contours.size(); ++i) { cv::Point2f center; float radius; // find minimum circle enclosing the contour cv::minEnclosingCircle(contours[i],center,radius); bool marked = false; cv::Rect circleROI(center.x-radius, center.y-radius, center.x+radius, center.y+radius); //circleROI = circleROI & cv::Rect(0,0,image.cols, image.rows); // count pixel inside the circle float sumCirclePixel = 0; float sumCirclePixelMarked = 0; for(int j=circleROI.y; j<circleROI.y+circleROI.height; ++j) for(int i=circleROI.x; i<circleROI.x+circleROI.width; ++i) { cv::Point2f current(i,j); // test if pixel really inside the circle: if(cv::norm(current-center) < radius) { // count total number of pixel in the circle sumCirclePixel = sumCirclePixel+1.0f; // and count all pixel in the circle which hold the segmentation threshold if(thresholded.at<unsigned char>(j,i)) sumCirclePixelMarked = sumCirclePixelMarked + 1.0f; } } const float ratioThreshold = 0.5f; if(sumCirclePixel) if(sumCirclePixelMarked/sumCirclePixel > ratioThreshold) marked = true; // draw the circle for demonstration if(marked) cv::circle(colorImage,center,radius,colorMarked,1); else cv::circle(colorImage,center,radius,colorUnMarked,1); } cv::imshow("thres", thresholded); cv::imshow("colorImage", colorImage); cv::imwrite("countFilledCircles_output.png", colorImage); cv::waitKey(-1); }
这样我得到了这些结果:
经过Otsu阈值处理后:
最终图像: