用opencv(基于霍夫变换或其他特征)编写鲁棒的(颜色和尺寸不变的)圆检测,

我写了下面非常简单的python代码来查找图像中的圆圈:

import cv import numpy as np WAITKEY_DELAY_MS = 10 STOP_KEY = 'q' cv.NamedWindow("image - press 'q' to quit", cv.CV_WINDOW_AUTOSIZE); cv.NamedWindow("post-process", cv.CV_WINDOW_AUTOSIZE); key_pressed = False while key_pressed != STOP_KEY: # grab image orig = cv.LoadImage('circles3.jpg') # create tmp images grey_scale = cv.CreateImage(cv.GetSize(orig), 8, 1) processed = cv.CreateImage(cv.GetSize(orig), 8, 1) cv.Smooth(orig, orig, cv.CV_GAUSSIAN, 3, 3) cv.CvtColor(orig, grey_scale, cv.CV_RGB2GRAY) # do some processing on the grey scale image cv.Erode(grey_scale, processed, None, 10) cv.Dilate(processed, processed, None, 10) cv.Canny(processed, processed, 5, 70, 3) cv.Smooth(processed, processed, cv.CV_GAUSSIAN, 15, 15) storage = cv.CreateMat(orig.width, 1, cv.CV_32FC3) # these parameters need to be adjusted for every single image HIGH = 50 LOW = 140 try: # extract circles cv.HoughCircles(processed, storage, cv.CV_HOUGH_GRADIENT, 2, 32.0, HIGH, LOW) for i in range(0, len(np.asarray(storage))): print "circle #%d" %i Radius = int(np.asarray(storage)[i][0][2]) x = int(np.asarray(storage)[i][0][0]) y = int(np.asarray(storage)[i][0][1]) center = (x, y) # green dot on center and red circle around cv.Circle(orig, center, 1, cv.CV_RGB(0, 255, 0), -1, 8, 0) cv.Circle(orig, center, Radius, cv.CV_RGB(255, 0, 0), 3, 8, 0) cv.Circle(processed, center, 1, cv.CV_RGB(0, 255, 0), -1, 8, 0) cv.Circle(processed, center, Radius, cv.CV_RGB(255, 0, 0), 3, 8, 0) except: print "nothing found" pass # show images cv.ShowImage("image - press 'q' to quit", orig) cv.ShowImage("post-process", processed) cv_key = cv.WaitKey(WAITKEY_DELAY_MS) key_pressed = chr(cv_key & 255) 

从以下两个例子中可以看出,“圈子发现质量”差异很大:

情况1:

输入1detection1后processed1

CASE2:

输入2检测功能2后processed2

Case1和Case2基本上是相同的图像,但仍然algorithm检测到不同的圆圈。 如果我用不同大小的圆圈呈现algorithm,则圆检测甚至可能完全失败。 这主要是由于每个新图像需要单独调整的HIGHLOW参数。

所以我的问题是:使这个algorithm更强大的各种可能性是什么? 它应该是尺寸和颜色不变的,以便检测不同颜色,不同大小的不同圆圈。 也许使用Hough变换不是做事的最佳方式? 有更好的方法吗?

以下是基于我作为视觉研究者的经验。 从你的问题来看,你似乎对可能的algorithm和方法感兴趣,而不仅仅是一段代码。 首先,我给你的示例图像提供一个快速和肮脏的Python脚本,并显示一些结果certificate它可能可以解决您的问题。 得到这些之后,我尝试回答有关健壮检测algorithm的问题。

快速结果

一些示例图像(除了你的所有图像都是从flickr.com下载的并且是CC许可的)与检测到的圆形(不更改/调整任何参数,下面的代码用于提取所有图像中的圆圈): 检测到样本图像1中的斑点检测到样本图像2中的斑点很多圈子在flickr图片1中的斑点

代码(基于MSER斑点检测器)

这里是代码:

 import cv2 import math import numpy as np d_red = cv2.cv.RGB(150, 55, 65) l_red = cv2.cv.RGB(250, 200, 200) orig = cv2.imread("c.jpg") img = orig.copy() img2 = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) detector = cv2.FeatureDetector_create('MSER') fs = detector.detect(img2) fs.sort(key = lambda x: -x.size) def supress(x): for f in fs: distx = f.pt[0] - x.pt[0] disty = f.pt[1] - x.pt[1] dist = math.sqrt(distx*distx + disty*disty) if (f.size > x.size) and (dist<f.size/2): return True sfs = [x for x in fs if not supress(x)] for f in sfs: cv2.circle(img, (int(f.pt[0]), int(f.pt[1])), int(f.size/2), d_red, 2, cv2.CV_AA) cv2.circle(img, (int(f.pt[0]), int(f.pt[1])), int(f.size/2), l_red, 1, cv2.CV_AA) h, w = orig.shape[:2] vis = np.zeros((h, w*2+5), np.uint8) vis = cv2.cvtColor(vis, cv2.COLOR_GRAY2BGR) vis[:h, :w] = orig vis[:h, w+5:w*2+5] = img cv2.imshow("image", vis) cv2.imwrite("c_o.jpg", vis) cv2.waitKey() cv2.destroyAllWindows() 

正如你所看到的,它基于MSER斑点检测器。 除了简单的映射到灰度外,代码不会对图像进行预处理。 因此,预计将会丢失图像中的淡黄色斑点。

理论

简而言之,除了仅给出两个没有描述的样本图像之外,您不会告诉我们您对该问题的了解。 在这里,我解释了为什么我认为在问什么是有效的方法来解决这个问题之前,有关于这个问题的更多信息是非常重要的。

回到主要问题:这个问题最好的办法是什么? 让我们看看这是一个search问题。 为了简化讨论,假设我们正在寻找具有给定大小/半径的圆。 因此,问题归结为寻找中心。 每个像素是一个候选中心,因此,search空间包含所有的像素。

 P = {p1, ..., pn} P: search space p1...pn: pixels 

为了解决这个search问题,应该定义另外两个函数:

 E(P) : enumerates the search space V(p) : checks whether the item/pixel has the desirable properties, the items passing the check are added to the output list 

假设algorithm的复杂性并不重要,可以使用穷尽search或者蛮力search,其中E取每个像素并且传递给V.在实时应用中,减lesssearch空间并且优化V的计算效率是重要的。

我们正在接近主要问题。 我们如何定义V,更准确地说,候选人的属性应该是什么措施,应该如何解决分裂为理想和不理想的二分法问题。 最常见的方法是find一些属性,这些属性可以用来定义基于属性测量的简单决策规则。 这就是你正在做的反复试验。 你通过学习正面和反面的例子来编程一个分类器。 这是因为你使用的方法不知道你想要做什么。 您必须调整/调整决策规则的参数和/或对数据进行预处理,以减less方法所用的二分法问题(理想候选项)属性的变化。 您可以使用机器学习algorithm为给定的一组示例find最佳参数值。 从决策树到可以用于这个问题的遗传编程,都有大量的学习algorithm。 您也可以使用学习algorithm来找出几个圆检测algorithm的最佳参数值,并查看哪一个能够提供更好的精度。 这需要学习algorithm的主要负担,您只需要收集示例图像。

另一种经常被忽视的提高健壮性的方法是利用额外的可用信息。 如果您几乎不需要额外的努力即可知道圆的颜色,则可以显着提高检测器的准确性。 如果您知道飞机上的圆的位置,并且想要检测成像的圆,那么您应该记住,这两组位置之间的转换由2D单应性描述。 单应性可以用四点来估计。 那么你可以提高鲁棒性,以坚实的方法。 往往低估了特定领域知识的价值。 以这种方式来看,在第一种方法中,我们试图根据有限数量的样本近似一些决策规则。 在第二种方法中,我们知道决策规则,只需要find一种在algorithm中有效利用它们的方法。

概要

总而言之,有两种方法可以提高解决scheme的准确性和鲁棒性:

  1. 基于工具 :通过使用机器学习algorithm,find更简单易用的algorithm/更less的参数数量/调整algorithm/自动执行此过程
  2. 基于信息 :您是否使用所有现成​​的信息? 在这个问题中,你没有提到你对这个问题的了解。

对于你共享的这两个图像,我会使用一个blob检测器而不是HT方法。 对于背景减法,我会build议尝试估计背景的颜色,因为在两个图像中不变,而圆的颜色是变化的。 而大部分地区是裸露的。

这是一个很好的build模问题。 我有以下build议/想法:

  1. 将图像分解为RGB然后处理。
  2. 预处理。
  3. dynamic参数search。
  4. 添加约束。
  5. 确定你想要检测的是什么。

更详细地说:

1:正如其他答案中指出的那样,直接转换为灰度丢弃了太多的信息 – 任何与背景亮度相似的圆都将丢失。 更好地考虑孤立或不同颜色空间中的颜色通道。 这里有两种方法:在每个预处理通道上HoughCircles执行HoughCircles ,然后合并结果,或者处理通道,然后合并它们,然后操作HoughCircles 。 在我的尝试下面,我已经尝试了第二种方法,分裂成RGB通道,处理,然后组合。 注意合成时要注意使图像饱和,我使用cv.And并避免这个问题(在这个阶段,我的圈子总是黑色的戒指/在白色背景上的光盘)。

2:预处理是非常棘手的,也是最好的处理方式。 我已经使用了AdaptiveThreshold ,它是一种非常强大的卷积方法,可以通过基于局部平均值(在哺乳动物视觉系统的早期path中也发生类似的过程)对像素进行阈值处理来增强图像中的边缘。 这也是有用的,因为它减less了一些噪音。 我只用一遍就用过dilate/erode 。 而且我保留了其他参数。 在HoughCirclesfind“实心圆圈”之前,似乎使用了Canny ,因此可能最好保留它。这个预处理过程非常繁重,可能会导致误报,但是在我们的情况下这可能是可取的?

3:正如你已经注意到HoughCircles参数param2 (你的参数LOW )需要调整每个图像,以获得最佳解决scheme,实际上从文档 :

它越小,可以检测到更多的假圆圈。

麻烦是每个图像的甜蜜点都会有所不同。 我认为这里最好的办法是设置一个条件,并通过不同的param2值进行search,直到满足这个条件。 你的图像显示不重叠的圆圈,当param2太低时,我们通常会得到大量重叠的圆圈。 所以我build议search:

最大数量的非重叠和不包含的圆圈

所以我们一直使用不同的param2值来调用HoughCircle,直到满足为止。 我在下面的例子中做了这个,只是递增参数2,直到达到阈值假设。 如果执行二进制search以查找何时遇到这种情况,则会更快(而且相当容易),但是您需要注意exception处理,因为opencv通常会为无辜的param2值(至less在我的安装)。 一个不同的条件,我们将非常有用的匹配将是圈子的数量。

4:我们可以添加更多限制吗? 更多的东西,我们可以告诉我们的模型一个简单的任务,我们可以使其检测到的圈子。 例如,我们知道:

  • 圈数。 – 即使是上限或下限也是有帮助的。
  • 圆圈,背景或“非圆”的可能颜色。
  • 他们的大小。
  • 他们在哪里可以在一个图像。

5:图像中的一些斑点只能松散地称为圆圈! 考虑到你的第二个图像中的两个“非圆形斑点”,我的代码找不到它们(好!),但是…如果我'photoshop'它们使得它们更加圆形,我的代码可以find它们…也许如果你想要发现不是圈子的东西,像Tim LukinsTim Lukins这种不同的方法可能会更好。

问题

通过做大量的预处理AdaptiveThresholding阈值处理和Canny,图像中的特征可能会出现很多失真,这可能会导致错误的圆圈检测或不正确的半径报告。 例如一个大的实心圆盘经过处理后会出现一个圆环,所以HughesCircles可能会find内环。 此外,即使是文件说明:

…通常这个函数很好地检测圆心,但是可能找不到正确的半径。

如果您需要更精确的半径检测,我build议采用以下方法(未实施):

  • 在原始图像上,从报告的圆心,在一个扩大的十字(4光线:上/下/左/右)
  • 在每个RGB通道中分别做这个
  • 以合理的方式将每条光线的每个通道的信息组合起来(如需要翻转,偏移,缩放等)
  • 取每条射线上前几个像素的平均值,用这个来检测射线发生明显偏差的位置。
  • 这4点是周围点的估计。
  • 使用这四个估计来确定更精确的半径和中心位置(!)。
  • 这可以通过使用扩展环而不是四条射线来推广。

结果

最后的代码在很多时候都非常好,这些例子是用代码完成的,如下所示:

检测您的第一张图片中的所有圈子: 在这里输入图像说明

如何应用Canny滤波器之前的预处理图像(不同的彩色圆圈是高度可见的): 在这里输入图像说明

在第二个图像中检测除两个(blob)以外的所有内容: 在这里输入图像说明

改变的第二个图像(斑点圆圈,大椭圆形更圆,从而改善检测),所有检测到: 在这里输入图像说明

在这幅康定斯基的绘画作品中,检测中心的performance相当不错(由于他的边界条件,我找不到同心圆环)。 在这里输入图像说明

码:

 import cv import numpy as np output = cv.LoadImage('case1.jpg') orig = cv.LoadImage('case1.jpg') # create tmp images rrr=cv.CreateImage((orig.width,orig.height), cv.IPL_DEPTH_8U, 1) ggg=cv.CreateImage((orig.width,orig.height), cv.IPL_DEPTH_8U, 1) bbb=cv.CreateImage((orig.width,orig.height), cv.IPL_DEPTH_8U, 1) processed = cv.CreateImage((orig.width,orig.height), cv.IPL_DEPTH_8U, 1) storage = cv.CreateMat(orig.width, 1, cv.CV_32FC3) def channel_processing(channel): pass cv.AdaptiveThreshold(channel, channel, 255, adaptive_method=cv.CV_ADAPTIVE_THRESH_MEAN_C, thresholdType=cv.CV_THRESH_BINARY, blockSize=55, param1=7) #mop up the dirt cv.Dilate(channel, channel, None, 1) cv.Erode(channel, channel, None, 1) def inter_centre_distance(x1,y1,x2,y2): return ((x1-x2)**2 + (y1-y2)**2)**0.5 def colliding_circles(circles): for index1, circle1 in enumerate(circles): for circle2 in circles[index1+1:]: x1, y1, Radius1 = circle1[0] x2, y2, Radius2 = circle2[0] #collision or containment: if inter_centre_distance(x1,y1,x2,y2) < Radius1 + Radius2: return True def find_circles(processed, storage, LOW): try: cv.HoughCircles(processed, storage, cv.CV_HOUGH_GRADIENT, 2, 32.0, 30, LOW)#, 0, 100) great to add circle constraint sizes. except: LOW += 1 print 'try' find_circles(processed, storage, LOW) circles = np.asarray(storage) print 'number of circles:', len(circles) if colliding_circles(circles): LOW += 1 storage = find_circles(processed, storage, LOW) print 'c', LOW return storage def draw_circles(storage, output): circles = np.asarray(storage) print len(circles), 'circles found' for circle in circles: Radius, x, y = int(circle[0][2]), int(circle[0][0]), int(circle[0][1]) cv.Circle(output, (x, y), 1, cv.CV_RGB(0, 255, 0), -1, 8, 0) cv.Circle(output, (x, y), Radius, cv.CV_RGB(255, 0, 0), 3, 8, 0) #split image into RGB components cv.Split(orig,rrr,ggg,bbb,None) #process each component channel_processing(rrr) channel_processing(ggg) channel_processing(bbb) #combine images using logical 'And' to avoid saturation cv.And(rrr, ggg, rrr) cv.And(rrr, bbb, processed) cv.ShowImage('before canny', processed) # cv.SaveImage('case3_processed.jpg',processed) #use canny, as HoughCircles seems to prefer ring like circles to filled ones. cv.Canny(processed, processed, 5, 70, 3) #smooth to reduce noise a bit more cv.Smooth(processed, processed, cv.CV_GAUSSIAN, 7, 7) cv.ShowImage('processed', processed) #find circles, with parameter search storage = find_circles(processed, storage, 100) draw_circles(storage, output) # show images cv.ShowImage("original with circles", output) cv.SaveImage('case1.jpg',output) cv.WaitKey(0) 

啊,是的…旧的颜色/尺寸不variables的圆问题(又名霍夫变换是太具体,不健壮)…

在过去我更多地依赖于OpenCV的结构和形状分析function。 你可以从“samples”文件夹中得到一个很好的主意,特别是fitellipse.pysquares.py

为了您的澄清,我提出了这些例子的混合版本,并根据您的原始来源。 检测到的轮廓为绿色,椭圆形为红色。

在这里输入图像说明

目前尚不完全:

  • 预处理步骤需要稍微调整以检测更微弱的圆圈。
  • 你可以进一步testing轮廓,以确定它是否是一个圆圈。

祝你好运!

 import cv import numpy as np # grab image orig = cv.LoadImage('circles3.jpg') # create tmp images grey_scale = cv.CreateImage(cv.GetSize(orig), 8, 1) processed = cv.CreateImage(cv.GetSize(orig), 8, 1) cv.Smooth(orig, orig, cv.CV_GAUSSIAN, 3, 3) cv.CvtColor(orig, grey_scale, cv.CV_RGB2GRAY) # do some processing on the grey scale image cv.Erode(grey_scale, processed, None, 10) cv.Dilate(processed, processed, None, 10) cv.Canny(processed, processed, 5, 70, 3) cv.Smooth(processed, processed, cv.CV_GAUSSIAN, 15, 15) #storage = cv.CreateMat(orig.width, 1, cv.CV_32FC3) storage = cv.CreateMemStorage(0) contours = cv.FindContours(processed, storage, cv.CV_RETR_EXTERNAL) # NB 'processed' image is modified by this! #contours = cv.ApproxPoly (contours, storage, cv.CV_POLY_APPROX_DP, 3, 1) # If you wanted to reduce the number of points... cv.DrawContours (orig, contours, cv.RGB(0,255,0), cv.RGB(255,0,0), 2, 3, cv.CV_AA, (0, 0)) def contour_iterator(contour): while contour: yield contour contour = contour.h_next() for c in contour_iterator(contours): # Number of points must be more than or equal to 6 for cv.FitEllipse2 if len(c) >= 6: # Copy the contour into an array of (x,y)s PointArray2D32f = cv.CreateMat(1, len(c), cv.CV_32FC2) for (i, (x, y)) in enumerate(c): PointArray2D32f[0, i] = (x, y) # Fits ellipse to current contour. (center, size, angle) = cv.FitEllipse2(PointArray2D32f) # Convert ellipse data from float to integer representation. center = (cv.Round(center[0]), cv.Round(center[1])) size = (cv.Round(size[0] * 0.5), cv.Round(size[1] * 0.5)) # Draw ellipse cv.Ellipse(orig, center, size, angle, 0, 360, cv.RGB(255,0,0), 2,cv.CV_AA, 0) # show images cv.ShowImage("image - press 'q' to quit", orig) #cv.ShowImage("post-process", processed) cv.WaitKey(-1) 

编辑:

只是一个更新,说我相信所有这些答案的一个主要主题是,有许多进一步的假设和约束可以应用于你想要承认的通告 。 我自己的回答对此没有任何说法 – 无论是低级预处理还是高级几何拟合。 事实上,由于图像的绘制方式或图像的非仿射/投影变换,以及其渲染/捕捉的其他属性(颜色,噪声,光照,边缘厚度) – 只在一个图像中产生任意数量的候选圆圈。

有更复杂的技术。 但他们会花费你。 我个人喜欢@fraxel使用addaptive阈值的想法。 这是快速,可靠和合理的。 然后,您可以进一步testing最后的轮廓(例如,使用胡矩),或者使用简单的椭圆轴比率testing – 例如((min(size)/ max(size))> 0.7)。

与“计算机视觉”一样,实用主义,原则和解构主义之间存在紧张关系。 我喜欢告诉那些认为简历很简单的人,这不是 – 它实际上是一个人工智能完整的问题。 在这之外你能经常希望的最好的事情是在大多数时候工作。

通过你的代码看,我注意到了以下几点:

  • 灰度转换。 我明白你为什么这样做,但是要意识到你在那里扔掉信息。 正如您在“后期处理”图片中看到的,您的黄色圆圈与背景强度相同,只是颜色不同。

  • 噪音消除后的边缘检测(消除/扩张)。 这不应该是必要的; Canny应该照顾这个。

  • Canny边缘检测。 你的“开放”的圈子有两个边缘,一个内部和外部的边缘。 由于它们非常接近,Canny高斯滤波器可能会将它们加在一起。 如果没有,你会有两个边缘靠近在一起。 也就是说,在Canny之前,你有开放的圈子。 之后,你分别有0/2和1个边。 由于Hough再次调用Canny,在第一种情况下,两个边缘可能会平滑(取决于初始宽度),这就是为什么核心Houghalgorithm可以将开放和实心圆圈视为相同的原因。

所以,我的第一个build议是改变灰度映射。 不要使用强度,而要使用色调/饱和度/值。 另外,使用差分方法 – 您正在寻找边缘。 所以,计算一个HSV变换,平滑一个副本,然后取出原始和平滑副本之间的差异。 这将为您获得每个点的dH, dS, dV值(色调,饱和度,值的局部变化)。 正方形并添加以获得一维图像,在所有边缘(内部和外部)附近具有峰值。

我的第二个build议是当地的正常化,但我不确定这是否是必要的。 这个想法是,你并不关心你得到的边缘信号的确切值,无论如何它应该是二元的(边或不)。 因此,可以通过除以局部平均值(其中局部在您的边缘大小的数量级上)来标准化每个值。

您可能知道,霍夫变换使用“模型”来查找(通常)边缘检测图像中的某些特征。 在HoughCircles的情况下,模型是一个完美的圆。 这意味着可能不存在一个参数的组合,这将使它能够检测图片中更多的不规则和椭圆形的圆圈,而不会增加误报的次数。 另一方面,由于基本的投票机制,一个非闭合的完美圆或者一个带有“凹痕”的完美圆可能一直出现。 所以根据您的预期输出,您可能会或可能不想使用此方法。

也就是说,我看到有几件事可以帮助你在这个函数的途中:

  1. HoughCircles内部呼叫Canny ,所以我想你可以离开这个呼叫。
  2. param1 (您称为HIGH )通常初始化值为200 。 它用作内部调用Canny的参数: cv.Canny(processed, cannied, HIGH, HIGH/2) 。 这可能有助于像这样自己运行Canny ,看看如何通过Hough变换设置HIGH影响正在处理的图像。
  3. param2 (你称之为LOW )通常初始化为100左右。 这是Hough变换累加器的投票阈值。 将其设置得更高意味着更多的假阴性,降低更多的假阳性。 我相信这是你想开始摆弄的第一个。

参考: http : //docs.opencv.org/3.0-beta/modules/imgproc/doc/feature_detection.html#houghcircles

更新re:实心圆 :在find具有Hough变换的圆形后,可以通过对边界颜色进行采样并将其与假定圆内的一个或多个点进行比较来testing它们是否填充。 或者,您可以将假定的圆圈内的一个或多个点与给定的背景颜色进行比较。 如果前面的比较成功,则填充该圆圈,或者如果失败,则replace比较。

确定看图像。 我build议使用**Active Contours**

  • 活动轮廓活动轮廓的好处在于,它们几乎完全适合任何给定的形状。 无论是正方形还是三angular形,在你的情况下,他们是最好的候选人。
  • 如果你能够提取圈子的中心,那太棒了。 活动的轮廓总是需要一个开始,他们可以增长或缩小以适应。 没有必要中心总是与中心alignment。 稍微偏移量仍然可以。
  • 在你的情况下,如果你让轮廓从中心向外扩展,它们应该放在一个圆的边界上。
  • 请注意,生长或缩小的活动轮廓使用气球能量 ,这意味着您可以设置轮廓的方向,向内或向外。
  • 您可能需要使用灰度梯度图像。 但是你仍然可以尝试颜色。 如果它的工作!
  • 如果您不提供中心,请投入大量活动轮廓,然后进行增长/缩小。 定居下来的轮廓被保留下来,不稳定的轮廓被扔掉。 这是一个powershell的方法。 将CPU密集。 但需要更仔细的工作,以确保你留下正确的轮廓,并抛出不良的。

我希望这样你可以解决这个问题。