目标:圆形物体缺陷检测,见下图,右边物体的右上角部分缺了一块,为不合格产品。
主要思想是通过坐标转换将圆形物体“拉”成方形,再经过均值滤波后与其相减便可得到缺口区域。如下:
从左至右依次是从图中抠出来的圆环,经过坐标系转换后的目标图像,对转换后的图像进行均值滤波,最后(2)(3)相减。
根据相减后的连通域的面积判断产品是否合格。
在halcon的例程中有一个这样的例子,实现并不难,弄明白笛卡尔坐标到极坐标转换和线性插值即可。
坐标转换套公式:
在目标图像中,以x方向为半径,即,以y方向为角度,即。以目标图像中的和的值计算出其在原图中的位置,得出其像素值。
def linearPolar(src, center, radius):
'''
x方向为半径(column)
y方向为角度(row), from 0 to 2 * pi
'''
dst = np.zeros(src.shape, dtype=np.uint8)
ascale, pscale = (2 * np.pi) / src.shape[1], radius / src.shape[0]
rho = list(map(lambda x : x * pscale, range(src.shape[1])))
for row in range(src.shape[0]):
phi = row * ascale
for col in range(src.shape[1]):
#calculate coordinate in src
x, y = rho[col] * cos(phi), rho[col] * sin(phi)
#calculate the gray value at point(col, row) by bilinear Interpolation
dst[row][col] = bilinearInterpolation(src, x + center[0], y + center[1])
return dst
需要注意的是求出的x,y并不一定为整数,为保证准确度,可以用线性插值的方法来进行计算。
说白了线性插值就是,已知一条直线,及任意一点的x坐标,求这个点的y坐标(详见维基百科)。
我这里用的是双线性插值,原理类似。代码如下:
def bilinearInterpolation(img, src_x, src_y):
val = 0
lCoor, rCoor = int(np.floor(src_x)), int(np.ceil(src_x))
tCoor, bCoor = int(np.floor(src_y)), int(np.ceil(src_y))
if lCoor < 0 or rCoor < 0 or tCoor < 0 or bCoor < 0:
return 0
#print(lCoor, rCoor, tCoor, bCoor)
if lCoor == rCoor and tCoor != bCoor: #deal with rim in y-axis
val1, val2 = img[tCoor][lCoor], img[bCoor][lCoor]
val = (bCoor - src_y) * val1 + (src_y - lCoor) * val2
elif tCoor == bCoor and lCoor != rCoor: #deal with rim in x-axis
val1, val2 = img[tCoor][lCoor], img[tCoor][rCoor]
val = (rCoor - src_x) * val1 + (src_x - lCoor) * val2
else: #bilinear
val11, val12 = img[tCoor][lCoor], img[tCoor][rCoor]
val21, val22 = img[bCoor][lCoor], img[bCoor][rCoor]
a, b, c, d = rCoor - src_x, src_x - lCoor, bCoor - src_y, src_y - tCoor
aa, bb = a * val11 + b * val21, a * val12 + b * val22
val = aa * c + bb * d
return val
其他部分:
dyn_threshold()这个函数用过halcon的肯定不会陌生,感觉挺好用的,于是根据函数说明重新写了一个,没有测试过(还是懒)。结果发现这个函数跟cv::absdiff()好像是差不多的一个意思。
另外linearPolar函数在opencv中已经有了现成的,直接拿来用就好。看了一下opencv的源码实现,模仿着写了一个,效果惨不忍睹,关键是还不知道是哪里的问题,可惜找不到那段代码了,不然也贴上来。
我在代码里还把产品掏成了圆环,多此一举没必要,但是也懒得改了,纯当练手。
def dyn_threshold(OrigImage, ThresholdImage, Offset, LightDark):
ret = np.zeros(OrigImage.shape, np.uint8)
def threshold(OrigImage, ThresholdImage, Expr):
for i in range(OrigImage.shape[0]):
for j in range(OrigImage.shape[1]):
ret[i][j] = Expr(OrigImage[i][j], ThresholdImage[i][j])
if 'light' == LightDark:
threshold(OrigImage, ThresholdImage, lambda o, t : 255 if o >= t + Offset else 0)
elif 'dark' == LightDark:
threshold(OrigImage, ThresholdImage, lambda o, t : 255 if o <= t - Offset else 0)
elif 'equal' == LightDark:
threshold(OrigImage, ThresholdImage, lambda o, t : 255 if o >= t + Offset and o <= t - Offset else 0)
elif 'not_equal' == LightDark: #gt - offset > go and go > gt + offset
threshold(OrigImage, ThresholdImage, lambda o, t : 255 if t - Offset > o or o > t + Offset else 0)
else:
pass
return ret
def imageProcess(img):
thresh, process = cv.threshold(img, 0, 255, cv.THRESH_OTSU)
_, contours, hierarchy = cv.findContours(process, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
plt.figure()
n = 1
for i, contour in enumerate(contours):
center, radius = cv.minEnclosingCircle(contour)
if radius < 100 or radius > 300:
continue
br = cv.boundingRect(contour)
roi = [0, 0, 0, 0]
offset, widen = 15, 30
inner_radius = 100
roi[0], roi[1], roi[2], roi[3] = br[0] - offset, br[1] - offset, br[2] + widen, br[3] + widen
product = np.copy(process[roi[1] : roi[1] + roi[3] + 1, roi[0] : roi[0] + roi[2] + 1])
inner = np.zeros(product.shape, np.uint8)
newCenter = (int(center[0] - roi[0]), int(center[1] - roi[1]))
cv.circle(inner, newCenter, inner_radius, (255), 1, 8)
cv.floodFill(inner, np.array([]), newCenter, 255)
annular = cv.bitwise_or(inner, product)
newCenter = (center[0] - roi[0], center[1] - roi[1])
polarAnnular = linearPolar(annular, newCenter, radius)
#polarAnnular = cv.linearPolar(annular, newCenter, radius, cv.INTER_LINEAR + cv.WARP_FILL_OUTLIERS)
meanImage = cv.blur(polarAnnular, (3, 271))
dyned = dyn_threshold(polarAnnular, meanImage, 70, 'not_equal')
_, contoursDyn, _ = cv.findContours(dyned, cv.RETR_LIST, cv.CHAIN_APPROX_NONE)
for j, contourDyn in enumerate(contoursDyn):
brDyn = cv.boundingRect(contourDyn)
if brDyn[2] < 20: #宽度筛选
continue
cv.putText(img, str(n) + "NG", (br[0] - offset, br[1] - offset), cv.FONT_HERSHEY_PLAIN, 8, (0), 3)
break
else:
cv.putText(img, str(n) + "OK", (br[0] - offset, br[1] - offset), cv.FONT_HERSHEY_PLAIN, 8, (0), 3)
plt.subplot(4, 10, n)
plt.imshow(polarAnnular)
plt.subplot(4, 10, n + 10)
plt.imshow(annular)
plt.subplot(4, 10, n + 20)
plt.imshow(dyned)
plt.subplot(4, 10, n + 30)
plt.imshow(meanImage)
n = n + 1
plt.figure()
plt.imshow(img)
plt.show()
if __name__ == '__main__':
img = cv.imread(".\\2018-08-12_18_00_51_957.bmp", cv.IMREAD_GRAYSCALE)
#img = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
imageProcess(img)
开发环境是python3+opencv3.4.1,感觉代码应该还有不少优化空间,过段时间再回来看看。