用opencv编写鲁棒(颜色和大小不变)圆检测(基于Hough变换或其他功能)

mem*_*elf 48 c c++ python opencv computer-vision

我编写了以下非常简单的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)
Run Code Online (Sandbox Code Playgroud)

从以下两个例子中可以看出,"圈子发现质量"变化很大:

情况1:

输入1 detection1 后processed1

CASE2:

输入2 检测功能2 后processed2

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

因此我的问题是:使这种算法更加健壮的各种可能性是什么?它应该是大小和颜色不变的,以便检测具有不同颜色和不同尺寸的不同圆.也许使用霍夫变换不是最好的做事方式?有更好的方法吗?

fir*_*ant 37

以下是我作为视觉研究员的经验.从您的问题中,您似乎对可能的算法和方法感兴趣,而不仅仅是一段代码.首先我给一个快速和肮脏的Python脚本为您的样品图片和一些结果显示,以证明它可能解决您的问题.在解决了这些问题后,我尝试回答有关强大检测算法的问题.

快速结果

一些样本图像与检测到的圆(除了所有的图像从你从flickr.com下载并CC许可)(不改变/调谐任何参数,正好下面的代码被用于提取中的所有图像的圆): 检测到样本图像中的斑点1 检测到样本图像中的斑点2 很多圈子 flickr图像中的blob 1

代码(基于MSER Blob检测器)

以下是代码:

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()
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,它基于MSER blob探测器.除了简单映射到灰度之外,代码不会预处理图像.因此,期望在图像中遗漏那些微弱的黄色斑点.

理论

简而言之:除了仅提供两个没有描述它们的样本图像之外,您不会告诉我们您对该问题的了解.在这里,我解释为什么我认为在询问解决问题的有效方法之前获得有关问题的更多信息非常重要.

回到主要问题:这个问题的最佳方法是什么?我们将此视为搜索问题.为简化讨论,假设我们正在寻找具有给定大小/半径的圆.因此,问题归结为寻找中心.每个像素都是候选中心,因此,搜索空间包含所有像素.

P = {p1, ..., pn} 
P: search space
p1...pn: pixels
Run Code Online (Sandbox Code Playgroud)

要解决此搜索问题,应定义其他两个函数:

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
Run Code Online (Sandbox Code Playgroud)

假设算法的复杂性并不重要,可以使用穷举或强力搜索,其中E占据每个像素并传递给V.在实时应用中,减少搜索空间并优化V的计算效率非常重要. .

我们越来越接近主要问题了.我们如何定义V,更准确地说候选人应该测量哪些属性,以及如何解决将它们分成期望和不合需要的二分法问题.最常见的方法是找到一些属性,这些属性可用于根据属性的度量定义简单的决策规则.这就是你通过反复试验做的事情.您通过学习积极和消极的例子来编写分类器.这是因为您使用的方法不知道您想要做什么.您必须调整/调整决策规则的参数和/或预处理数据,以便减少方法用于二分问题的属性(期望候选者)的变化.您可以使用机器学习算法来查找给定示例集的最佳参数值.从决策树到遗传编程,你可以使用大量的学习算法来解决这个问题.您还可以使用学习算法查找多个圆检测算法的最佳参数值,并查看哪个算法可以提供更好的精度.这会对您只需要收集样本图像的学习算法带来主要负担.

另一种经常被忽视的提高稳健性的方法是利用额外的现成信息.如果你知道圆的颜色几乎没有额外的努力,你可以显着提高探测器的准确性.如果你知道圆圈在平面上的位置,并且想要检测成像圆圈,你应该记住这两组位置之间的转换由2D单应法描述.并且可以仅使用四个点来估计单应性.然后你可以提高坚固性以获得坚如磐石的方法.特定领域知识的价值往往被低估.以这种方式看待它,在第一种方法中,我们尝试基于有限数量的样本来估计一些决策规则.在第二种方法中,我们知道决策规则,只需要找到一种在算法中有效利用它们的方法.

摘要

总而言之,有两种方法可以提高解决方案的准确性/稳健性:

  1. 基于工具:通过使用机器学习算法,找到更易于使用的算法/使用更少数量的参数/调整算法/自动化此过程
  2. 基于信息:您使用所有现成​​的信息吗?在这个问题中,你没有提到你对这个问题的了解.

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


fra*_*xel 28

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

  1. 将图像分割为RGB然后处理.
  2. 预处理.
  3. 动态参数搜索.
  4. 添加约束.
  5. 确保您要检测的内容.

更详细:

1:如其他答案中所述,直接转换为灰度会丢弃太多信息 - 任何与背景亮度相似的圆圈都将丢失.将色彩通道隔离或在不同的色彩空间中考虑要好得多.这里有两种方法:单独执行HoughCircles每个预处理通道,然后组合结果,或处理通道,然后将它们组合,然后运行HoughCircles.在下面的尝试中,我尝试了第二种方法,分裂为RGB通道,处理,然后组合.在组合时要小心过度饱和图像,我cv.And用来避免这个问题(在这个阶段我的圈子总是在白色背景上的黑色戒指/光盘).

2:预处理非常棘手,而且通常最好用它来处理.我已经利用了AdaptiveThreshold一种非常强大的卷积方法,可以通过基于局部平均值对像素进行阈值处理来增强图像中的边缘(类似的过程也发生在哺乳动物视觉系统的早期路径中).这也很有用,因为它可以减少一些噪音.我dilate/erode只使用了一次传球.而且我还保留了其他参数.它似乎使用CannyHoughCircles确实有很大的帮助与发现"实心圆",所以可能最好保持在这个预处理是相当重的,并可能导致误报某种程度上更"滴状圈",但是对我们来说,这是或许可取吗?

3:正如您所注意到的,HoughCircles参数param2(您的参数LOW)需要针对每个图像进行调整才能获得最佳解决方案,实际上来自文档:

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

麻烦是每个图像的最佳点都不同.我认为这里最好的方法是设置一个条件并搜索不同的param2值,直到满足这个条件.您的图片显示非重叠的圆圈,而当图片param2太低时,我们通常会遇到重叠的圆圈.所以我建议搜索:

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

因此,我们一直在调用具有不同值的HoughCircles,param2直到满足为止.我在下面的示例中执行此操作,只需递增param2直到达到阈值假设.如果您执行二进制搜索以查找何时满足,则会更快(并且相当容易),但是您需要小心处理异常处理,因为opencv经常会为无辜的查看值param2(至少在我的安装).我们非常有用的不同条件是圈数.

4:我们可以在模型中添加更多约束吗?我们可以告诉我们的模型越多,我们可以轻松地检查圆圈.例如,我们知道吗:

  • 圈数. - 即使是上限或下限也很有帮助.
  • 圆圈,背景或"非圆圈"的可能颜色.
  • 他们的尺寸.
  • 他们可以在一个图像中的位置.

5:你图像中的一些斑点只能被称为圆圈!考虑你的第二张图片中的两个"非圆形斑点",我的代码找不到它们(好!),但是......如果我'photoshop'它们它们更圆形,我的代码可以找到它们......也许如果你想要检测不是圆圈的东西,Tim Lukins可能会有更好的方法.

问题

通过进行繁重的预处理AdaptiveThresholding和"Canny",图像中的特征可能会有很多失真,这可能导致错误的圆检测或不正确的半径报告.例如,处理后的大型实心圆盘可能会出现一个环,因此HughesCircles可能会找到内环.甚至文档也指出:

...通常该功能可以很好地检测圆圈的中心,但是可能无法找到正确的半径.

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

  • 在原始图像上,来自报告的圆心的光线跟踪,在一个扩展的十字架中(4条光线:上/下/左/右)
  • 在每个RGB通道中单独执行此操作
  • 以合理的方式为每条光线的每个通道组合此信息(例如,根据需要翻转,偏移,缩放等)
  • 取每条光线上前几个像素的平均值,用它来检测光线发生重大偏差的位置.
  • 这4个点是周长点的估计值.
  • 使用这四个估计值来确定更准确的半径和中心位置(!).
  • 这可以通过使用扩展环而不是四条射线来推广.

结果

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

检测第一张图片中的所有圈子:在此输入图像描述

应用canny过滤器之前预处理图像的外观(不同颜色的圆圈高度可见):在此输入图像描述

在第二张图像中检测除了两个(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)
Run Code Online (Sandbox Code Playgroud)


til*_*uki 12

啊,是的......圆形问题的旧颜色/大小不变量(AKA Hough变换太具体而且不健壮)......

在过去,我更多地依赖于OpenCV 的结构和形状分析功能.你可以从"样本"文件夹中得到一个非常好的想法 - 特别是fitellipse.pysquares.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)
Run Code Online (Sandbox Code Playgroud)

编辑:

只是更新说我相信所有这些答案的一个主题是,有许多进一步的假设和约束可以应用于你想要认为是循环的.我自己的答案没有任何借口 - 无论是低级预处理还是高级几何拟合.事实上,许多圆圈由于它们的绘制方式或图像的非仿射/投影变换而不是那么圆,并且具有如何渲染/捕获它们的其他属性(颜色,噪声,光照,边缘厚度) - 所有结果都只在一个图像中产生任意数量的候选圆圈.

有更复杂的技术.但他们会花你的钱.我个人喜欢@fraxel使用addaptive阈值的想法.这是快速,可靠和相当稳健的.然后,您可以使用椭圆轴的简单比率测试进一步测试最终轮廓(例如使用Hu矩)或配件 - 例如if((min(size)/ max(size))> 0.7).

与计算机视觉一样,实用主义,原则和修辞学之间存在着紧张关系.因为我喜欢告诉那些认为简历容易的人,但事实并非如此 - 这实际上是一个人工智能完全问题.在大多数情况下,你可以经常希望在这之外的最好的东西.


MSa*_*ers 9

仔细查看代码,我注意到以下内容:

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

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

  • Canny边缘检测.你的"开放"圆圈有两条边,一条内边和外边.由于它们相当接近,Canny高斯滤波器可能会将它们加在一起.如果没有,你将有两个边缘靠近在一起.也就是在Canny之前,你有开放和充实的圈子.然后,分别有0/2和1个边.由于Hough再次调用Canny,在第一种情况下可以将两条边平滑在一起(取决于初始宽度),这就是为什么核心Hough算法可以将开放和填充圆相同处理.

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

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


Eri*_*ric 6

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

也就是说,我看到的一些东西可能会帮助您实现此功能:

  1. HoughCirclesCanny内部打电话,所以我想你可以把这个电话留下来.
  2. param1(你调用的HIGH)通常初始化值为200.它用作内部调用的参数Canny:cv.Canny(processed, cannied, HIGH, HIGH/2).这样运行可能有助于Canny查看设置如何HIGH影响Hough变换处理的图像.
  3. param2(您调用的LOW)通常是围绕一个值初始化的100.它是Hough变换累加器的投票阈值.将其设置得更高意味着更多的假阴性,更多的误报.我相信这是你想要开始摆弄的第一个.

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

更新re:实心圆:在使用霍夫变换找到圆形后,您可以通过对边界颜色进行采样并将其与假定圆内的一个或多个点进行比较来测试它们是否被填充.或者,您可以将假定圆内的一个或多个点与给定的背景颜色进行比较.如果前一个比较成功,则圆圈被填充;如果失败,则在替代比较的情况下填充圆圈.