打开 CV 平凡圆检测——如何得到最小二乘而不是轮廓?
Posted
技术标签:
【中文标题】打开 CV 平凡圆检测——如何得到最小二乘而不是轮廓?【英文标题】:Open CV trivial circle detection -- how to get least squares instead of a contour? 【发布时间】:2020-06-02 23:08:22 【问题描述】:我的目标是通过显微镜准确测量孔的直径。工作流程是:拍摄图像,进行拟合,拟合,将半径(以像素为单位)转换为 mm,写入 csv
这是我用于测量孔直径的图像处理脚本的输出。我遇到了一个问题,我的圆形拟合似乎优先匹配轮廓,而不是最小二乘法。
我也可以在这样的情况下平均多次拟合:
我的问题是我喜欢快速扫描以确保圆圈适合。权衡是我拥有的拟合越多,拟合越现实,我拥有的越少就越容易确保数字正确。我的圈子并不总是像这个圈子那样漂亮和圆,所以这对我很重要。
如果您可以看一下并告诉我如何在大约 5 个圆上执行更多最小二乘法,这是我的脚本拟合圆的部分。我不想使用最小圆检测,因为流体正在流过这个孔,所以我希望它更像是水力直径——谢谢!
(thresh, blackAndWhiteImage0) = cv2.threshold(img0, 100, 255, cv2.THRESH_BINARY) #make black + white
median0 = cv2.medianBlur(blackAndWhiteImage0, 151) #get rid of noise
circles0 = cv2.HoughCircles(median0,cv2.HOUGH_GRADIENT,1,minDist=5,param1= 25, param2=10, minRadius=min_radius_small,maxRadius=max_radius_small) #fit circles to image
【问题讨论】:
minEnclosureCircle 和 RANSAC 圆检测都应该适合您的用例 “更像是水力直径”——据我所知,这是 4 倍面积/周长。为此,您只需要孔的轮廓(无论如何,它不是一个精确的圆,因为它看起来)。你检查过这种方法吗? 【参考方案1】:这是另一种拟合圆的方法,方法是使用连接组件从二进制图像中获取等效圆心和半径,并使用 Python/OpenCV/Skimage 从该图像中绘制圆。
输入:
import cv2
import numpy as np
from skimage import measure
# load image and set the bounds
img = cv2.imread("dark_circle.png")
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# blur
blur = cv2.GaussianBlur(gray, (3,3), 0)
# threshold
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# apply morphology open with a circular shaped kernel
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
binary = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
# find contour and draw on input (for comparison with circle)
cnts = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
c = cnts[0]
result = img.copy()
cv2.drawContours(result, [c], -1, (0, 255, 0), 1)
# find radius and center of equivalent circle from binary image and draw circle
# see https://scikit-image.org/docs/dev/api/skimage.measure.html#skimage.measure.regionprops
# Note: this should be the same as getting the centroid and area=cv2.CC_STAT_AREA from cv2.connectedComponentsWithStats and computing radius = 0.5*sqrt(4*area/pi) or approximately from the area of the contour and computed centroid via image moments.
regions = measure.regionprops(binary)
circle = regions[0]
yc, xc = circle.centroid
radius = circle.equivalent_diameter / 2.0
print("radius =",radius, " center =",xc,",",yc)
xx = int(round(xc))
yy = int(round(yc))
rr = int(round(radius))
cv2.circle(result, (xx,yy), rr, (0, 0, 255), 1)
# write result to disk
cv2.imwrite("dark_circle_fit.png", result)
# display it
cv2.imshow("image", img)
cv2.imshow("thresh", thresh)
cv2.imshow("binary", binary)
cv2.imshow("result", result)
cv2.waitKey(0)
结果显示轮廓(绿色)与圆形拟合(红色)相比:
圆半径和圆心:
radius = 117.6142467296168 center = 220.2169911178609 , 150.26823599797507
最小二乘拟合方法(在轮廓点和圆之间)可以使用 Scipy 获得。例如,参见:
https://gist.github.com/lorenzoriano/6799568
https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.curve_fit.html
【讨论】:
对我来说,最小二乘与质心之间的误差将小于我将像素转换为 mm 的误差。感谢您的解决方案,我认为这比我的脚本更强大【参考方案2】:我建议像nathancy's answer 一样计算一个掩码,然后简单地计算他计算的掩码opening
中的像素数(这是对孔面积的无偏估计),然后翻译使用radius = sqrt(area/pi)
将区域设置为半径。这将为您提供与孔面积相同的圆的半径,并对应于一种获得最佳拟合圆的方法。
获得最佳拟合圆的另一种方法是获取孔的轮廓(如cv.findContours
在 nethancy 的答案中返回的 cnts
),finding its centroid,然后计算每个顶点到质心。这将大约*对应于圆与孔周长的最小二乘拟合。
* 我说大概是因为轮廓的顶点是轮廓的近似值,并且这些顶点之间的距离可能不均匀。不过错误应该很小。
这是使用DIPlib 的代码示例(披露:我是作者)(注意:下面的import PyDIP
语句要求您安装DIPlib,您不能使用pip
安装它,Windows 有二进制版本在 GitHub 页面上,否则您需要从源代码构建它)。
import PyDIP as dip
import imageio
import math
img = imageio.imread('https://i.stack.imgur.com/szvc2.jpg')
img = dip.Image(img[:,2600:-1])
img.SetPixelSize(0.01, 'mm') # Use your actual values!
bin = ~dip.OtsuThreshold(dip.Gauss(img, [3]))
bin = dip.Opening(bin, 25)
#dip.Overlay(img, bin - dip.BinaryErosion(bin, 1, 3)).Show()
msr = dip.MeasurementTool.Measure(dip.Label(bin), features=['Size', 'Radius'])
#print(msr)
print('Method 1:', math.sqrt(msr[1]['Size'][0] / 3.14), 'mm')
print('Method 2:', msr[1]['Radius'][1], 'mm')
MeasurementTool.Measure
函数计算'Size'
,即面积;和'Radius'
,它返回每个边界像素和质心之间距离的最大值、平均值、最小值和标准差。从'Radius'
,我们取第二个值,平均半径。
这个输出:
Method 1: 7.227900647539411 mm
Method 2: 7.225178113501325 mm
但请注意,我分配了一个随机像素大小(每像素 0.01 毫米),您需要填写正确的像素到毫米的转换值。
请注意这两个估计值非常接近。这两种方法都是好的、无偏的估计。第一种方法的计算成本更低。
【讨论】:
【参考方案3】:我的一个建议是查看cv2.fitEllipse()
希望您可以使用椭圆宽度/高度之间的纵横比来区分奇数。
【讨论】:
那么我的“直径”就可以是 sqr(a*b) 看看这里:docs.opencv.org/2.4/modules/imgproc/doc/… 看来我只能用一组点来做到这一点 是的:分割图像后(过滤它以便很容易地将孔(前景)与背景分开),您使用findContours 来获取这些点。在您的情况下,您将需要简化的仅外部轮廓(RETR_EXTERNAL
)。我将执行以下操作:1. 获取我在上面链接的示例代码,2. 将图像换成你的,3. 更改 findContours 参数(和其他参数)以最适合你的图像。完成后,使用更多图像进行测试并调整稳健性。 HTH【参考方案4】:
一种方法是Gaussian blur
然后Otsu's threshold图像得到二值图像。从这里开始,我们使用elliptical shaped kernel 执行morphological opening。此步骤将有效去除微小的噪声粒子。为了得到一个很好的圆估计,我们找到轮廓并使用cv2.minEnclosingCircle()
,它也给了我们中心点和半径。这是一个可视化:
输入图像(截图)
二值图像
变形打开
结果->
带半径的结果
Radius: 122.11396026611328
从这里您可以根据您的校准比例将半径(以像素为单位)转换为毫米
代码
import cv2
import numpy as np
# Load image, convert to grayscale, Gaussian blur, then Otsu's threshold
image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (3,3), 0)
thresh = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
# Morph open with a elliptical shaped kernel
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel, iterations=2)
# Find contours and draw minimum enclosing circle
cnts = cv2.findContours(opening, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
for c in cnts:
((x, y), r) = cv2.minEnclosingCircle(c)
cv2.circle(image, (int(x), int(y)), int(r), (36, 255, 12), 2)
print('Radius: '.format(r))
cv2.imshow('thresh', thresh)
cv2.imshow('opening', opening)
cv2.imshow('image', image)
cv2.waitKey()
【讨论】:
您对我的输出与我的问题相同,因为圆圈内的“白色”空间不等于圆圈外的“黑色”空间。向底部它们大致相等,然后随着您向上移动圆圈,空白区域越来越大。这有意义吗?以上是关于打开 CV 平凡圆检测——如何得到最小二乘而不是轮廓?的主要内容,如果未能解决你的问题,请参考以下文章