我编写了以下非常简单的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)
从以下两个例子可以看出,“圈子发现质量”差异很大:
CASE1:
CASE2:
Case1和Case2基本上是相同的图像,但算法仍会检测到不同的圆圈。如果我向算法呈现具有不同大小圆圈的图像,则圆圈检测甚至可能完全失败。这主要是由于HIGH
和LOW
参数需要针对每张新图片单独调整。
因此我的问题:使这种算法更加健壮的各种可能性是什么?它应该是大小和颜色不变的,以便检测具有不同颜色和不同尺寸的不同圆。也许使用霍夫变换不是最好的做事方式?有更好的方法吗?
答案 0 :(得分:35)
以下是基于我作为视觉研究员的经验。从您的问题中,您似乎对可能的算法和方法感兴趣,而不仅仅是一段代码。首先,我为您的示例图像提供了一个快速而肮脏的Python脚本,并显示了一些结果,以证明它可以解决您的问题。在完成这些工作之后,我尝试回答有关可靠检测算法的问题。
使用检测到的圆圈(不更改/调整任何参数,以下代码用于提取所有图像中的圆圈) ):
以下是代码:
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 blob探测器。除了简单映射到灰度之外,代码不会对图像进行预处理。因此,期望在图像中遗漏那些微弱的黄色斑点。
简而言之:除了只提供两张没有描述它们的样本图像之外,你不会告诉我们你对这个问题的了解。在这里,我解释了为什么我认为在询问解决问题的有效方法之前获得有关问题的更多信息非常重要。
回到主要问题:这个问题的最佳方法是什么? 让我们将此视为搜索问题。为简化讨论,假设我们正在寻找具有给定大小/半径的圆。因此,问题归结为寻找中心。每个像素都是候选中心,因此,搜索空间包含所有像素。
P = {p1, ..., pn}
P: search space
p1...pn: pixels
要解决此搜索问题,应定义另外两个函数:
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
假设算法的复杂性并不重要,可以使用穷举或强力搜索,其中E占据每个像素并传递给V.在实时应用中,减少它很重要搜索空间和优化计算效率。
我们越来越接近主要问题了。我们如何定义V,更准确地说候选人应该测量哪些属性,以及如何解决将它们分成期望和不合需要的二分法问题。最常见的方法是找到一些属性,这些属性可用于根据属性的度量定义简单的决策规则。这就是你通过反复试验做的事情。您通过学习积极和消极的例子来编写分类器。这是因为您使用的方法不知道您想要做什么。您必须调整/调整决策规则的参数和/或预处理数据,以便减少方法用于二分问题的属性(期望候选者)的变化。您可以使用机器学习算法来查找给定示例集的最佳参数值。从决策树到遗传编程,您可以使用大量学习算法来解决这个问题。您还可以使用学习算法查找多个圆检测算法的最佳参数值,并查看哪个算法可以提供更好的精度。这会给您只需要收集样本图像的学习算法带来主要负担。
另一种经常被忽视的提高稳健性的方法是利用额外的现成信息。如果你知道圆的颜色几乎没有额外的努力,你可以显着提高探测器的准确性。如果你知道圆圈在平面上的位置,并且想要检测成像圆圈,你应该记住这两组位置之间的转换由2D单应法描述。并且可以仅使用四个点来估计单应性。然后你可以提高坚固性以获得坚如磐石的方法。特定领域知识的价值往往被低估。以这种方式看待它,在第一种方法中,我们尝试基于有限数量的样本来估计一些决策规则。在第二种方法中,我们知道决策规则,只需要找到一种在算法中有效利用它们的方法。
总而言之,有两种方法可以提高解决方案的准确性/稳健性:
对于你共享的这两个图像我会使用blob检测器而不是HT方法。对于背景减法,我建议尝试估计背景的颜色,因为在两个图像中它不变,而圆的颜色变化。而且大部分区域都是裸露的。
答案 1 :(得分:27)
这是一个很好的建模问题。我有以下建议/想法:
更详细:
1:如其他答案中所述,直接转换为灰度会丢弃太多信息 - 任何与背景亮度相似的圆圈都将丢失。将色彩通道隔离或在不同的色彩空间中考虑要好得多。这里有两种方法:在每个预先处理的通道上单独执行HoughCircles
,然后组合结果,或处理通道,然后将它们组合,然后运行HoughCircles
。在下面的尝试中,我尝试了第二种方法,分裂为RGB通道,处理,然后组合。在组合时要小心过度饱和图像,我使用cv.And
来避免这个问题(在这个阶段,我的圆圈总是白色背景上的黑色圆环/光盘)。
2:预处理非常棘手,而且通常最好的处理方式。我已经使用AdaptiveThreshold
这是一种非常强大的卷积方法,它可以通过基于局部平均值对像素进行阈值处理来增强图像中的边缘(类似的过程也发生在哺乳动物视觉系统的早期路径中) 。这也很有用,因为它可以减少一些噪音。我只使用过一次dilate/erode
。而且我还保留了其他参数。似乎在使用Canny
之前使用HoughCircles
对查找“填充的圆圈”有很大帮助,所以最好保留它。这种预处理非常繁重,可能会导致错误有些更多&#blobby圆圈的积极因素,但在我们的情况下,这可能是可取的?
3:正如您已经注意到HoughCircles参数param2
(您的参数LOW
)需要针对每张图片进行调整才能获得最佳解决方案,事实上来自{{3 }}:
它越小,可以检测到更多的假圆圈。
麻烦的是,每个图像的最佳位置都会有所不同。我认为这里最好的方法是设置条件并搜索不同的param2
值,直到满足此条件。您的图片显示不重叠的圆圈,当param2
太低时,我们通常会遇到重叠的圆圈。所以我建议搜索:
非重叠且不包含的圆圈的最大数量
所以我们一直用不同的param2
值调用HoughCircles,直到满足为止。我在下面的示例中执行此操作,只需递增param2
直到达到阈值假设。如果您执行二进制搜索以查找何时满足,那么它会更快(并且相当容易),但是您需要小心处理异常处理,因为opencv经常会因为param2
无辜的值而抛出错误(至少在我的安装上)。我们非常有用的不同条件是圈数。
4:我们可以在模型中添加更多约束吗?我们可以告诉我们的模型越多,我们可以轻松地检查圆圈。例如,我们知道吗:
5:你图像中的一些斑点只能被称为圆圈!考虑两个非圆形斑点&#39;在你的第二张图片中,我的代码无法找到它们(好!),但是......如果我是“photoshop&#39;它们更圆形,我的代码可以找到它们......也许如果你想检测不是圆形的东西,可以使用Tim Lukins
这样的不同方法。
<强>问题强>
通过繁重的预处理AdaptiveThresholding
和'Canny&#39;图像中的特征可能会有很多失真,这可能导致虚假圆检测或半径报告不正确。例如,处理后的大型实心圆盘可能会出现一个环,因此HughesCircles可能会找到内环。甚至文档也指出:
...通常该功能可以很好地检测圆圈的中心,但是可能无法找到正确的半径。
如果您需要更准确的半径检测,我建议采用以下方法(未实施):
<强>结果
最后的代码在很多时候都做得很好,这些例子是用如下所示的代码完成的:
检测第一张图片中的所有圈子:
在应用canny过滤器之前,预处理图像的外观如何(不同颜色的圆圈高度可见):
检测第二张图片中除了两个(blob)之外的所有图片:
改变了第二张图像(blob是圆形的,大椭圆形更圆形,从而改善了检测),所有检测到:
在这张康定斯基画中检测中心的确很不错(由于边界条件,我找不到同心环)。
代码:
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)
答案 2 :(得分:11)
啊,是的...圆形问题的旧颜色/大小不变量(AKA Hough变换太具体而且不健壮)......
过去,我更多地依赖于OpenCV的structural and shape analysis功能。您可以从“samples”文件夹中了解可能的内容 - 尤其是fitellipse.py
和squares.py
。
为了您的解释,我提供了这些示例的混合版本并基于您的原始来源。检测到的轮廓为绿色,拟合的椭圆为红色。
它还没有完全出现:
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)
# N.B. '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阈值的想法。这是快速,可靠和相当稳健的。然后,您可以使用椭圆轴的简单比率测试进一步测试最终轮廓(例如使用Hu矩)或配件 - 例如if((min(size)/ max(size))&gt; 0.7)。
与计算机视觉一样,实用主义,原则和唯物主义之间存在紧张关系。因为我喜欢告诉那些认为简历容易的人,但事实并非如此 - 这实际上是一个AI complete问题。在大多数情况下,你可以经常希望在这之外的最好的东西。
答案 3 :(得分:8)
查看代码,我注意到以下内容:
灰度转换。我理解你为什么这样做,但意识到你在扔 那边的信息。正如您在“后处理”图像中看到的那样,您的黄色圆圈是 与背景相同的强度,只是用不同的颜色。
去除噪音后的边缘检测(擦除/扩张)。这不是必要的; Canny应该照顾好这个。
Canny边缘检测。你的“开放”圆圈有两条边,一条内边和外边。由于它们相当接近,Canny高斯滤波器可能会将它们加在一起。如果没有,你将有两个边缘靠近在一起。即在Canny之前,你有开放和充实的圈子。之后,你分别有0/2和1个边缘。由于Hough再次调用Canny,在第一种情况下可以将两条边平滑在一起(取决于初始宽度),这就是为什么核心Hough算法可以对开放和实心圆进行相同的处理。
所以,我的第一个建议是改变灰度映射。不要使用强度,而是使用色调/饱和度/值。此外,使用差分方法 - 您正在寻找边缘。因此,计算HSV
变换,平滑副本,然后获取原始和平滑副本之间的差异。这将为每个点获取dH, dS, dV
值(色调,饱和度,值的局部变化)。平方并添加以获得一维图像,所有边缘(内部和外部)附近都有峰值。
我的第二个建议是本地标准化,但我不确定是否有必要。你的想法是你并不特别关心你得到的边缘信号的确切值,无论如何它应该是二进制的(边缘与否)。因此,您可以通过除以局部平均值(其中局部值是边缘大小的数量级)来标准化每个值。
答案 4 :(得分:6)
Hough变换使用“模型”来查找(通常)边缘检测图像中的某些特征,如您所知。在HoughCircles
的情况下,该模型是一个完美的圆。这意味着可能不存在参数组合,这将使其检测图片中更不规则和椭圆形的圆圈,而不会增加误报的数量。另一方面,由于潜在的投票机制,一个非封闭的完美圆圈或一个带有“凹痕”的完美圆圈可能会一直出现。所以取决于您的预期输出您可能会或可能不想使用此方法。
也就是说,我发现有一些东西可以帮助你实现这个功能:
HoughCircles
在内部拨打Canny
,所以我猜您可以将此电话留空。param1
(您称之为HIGH
)通常会在值200
附近初始化。它用作对Canny
:cv.Canny(processed, cannied, HIGH, HIGH/2)
的内部调用的参数。像这样自己运行Canny
可能有助于了解设置HIGH
如何影响Hough变换处理的图像。param2
(您调用LOW
)通常会在值100
附近初始化。它是Hough变换累加器的投票阈值。将其设置得更高意味着更多的假阴性,更多的误报。我相信这是你想要开始摆弄的第一个。参考:http://docs.opencv.org/3.0-beta/modules/imgproc/doc/feature_detection.html#houghcircles
更新re:实心圆圈:使用霍夫变换找到圆形后,您可以通过对边界颜色进行采样并将其与内部的一个或多个点进行比较来测试它们是否已填充。假设的圈子。或者,您可以将假定圆内的一个或多个点与给定的背景颜色进行比较。如果前一个比较成功,则圆圈被填充;如果失败,则圆圈被填充。
答案 5 :(得分:2)
好看的图片。我建议使用**Active Contours**
我希望通过这种方式可以解决问题。