首先进行轮廓检测。轮廓检测的原图像必须是二值化的图像,可以通过threshold或Canny等方法得到。同时,待检测的图像背景应为黑色而轮廓为白色,这也是上一篇中使用二值化时将图像反相的原因。
注意:在OpenCV 3.2版本后,函数不再对原图像进行修改而是将修改后的函数作为返回值的第一个,因此新版本中该函数有3个返回值而不是旧版本的2个,参考官方教程。但在函数说明中,又说3.2版本后不再对图像进行修改(modify),据本人观察返回值中图像与原图像没有区别。
在预处理后二值图像中使用 cv2.findContours
函数检测所有的轮廓,代码如下:
img, contours, hierarchy = cv2.findContours(img_thresh, cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
其中第二个参数选择最外层边框即可,选其他方式也可以,因为接下来会遍历找到的所有轮廓,并使用函数找到其中**面积最大**的轮廓,此轮廓即数独问题最外层边框,代码如下:
max_area = 0
biggest_contour = None
for cnt in contours:
area = cv2.contourArea(cnt)
if area > max_area:
max_area = area
biggest_contour = cnt
接下来我们要用一个掩膜( mask )屏蔽边框之外画面,使用 cv2.drawContours()
绘制轮廓,其中第五个参数 thickness 如果为负数则会填充轮廓内部,这里使用的 cv2.FILLED 值就等于 -1 。所以下面代码先是在全黑的图像上绘制了白色(255)的轮廓并填充轮廓内部,又用黑色(0)绘制了一次轮廓线进一步缩小掩膜,最后将掩膜与原图像进行与运算,得到纯净的数独画面。
mask = np.zeros(img_brightness_adjust.shape, np.uint8)
cv2.drawContours(mask, [biggest_contour], 0, 255, cv2.FILLED)
cv2.drawContours(mask, [biggest_contour], 0, 0, 2)
image_with_mask = cv2.bitwise_and(img_brightness_adjust, mask)
观察问题图像上部,存在边框线弯曲的问题,不利于下一步的数字提取。透视变换与放射变换是常用的图像校正方法,但如果将整个方框进行透视变换无法解决边框存在弯曲的问题。
将每个小方格分别进行透视变换,能够克服整体透视变换无法修正某一边弯曲的问题,可以将边框弯曲问题消除。主要过程是:分别检测竖直线与水平线,取得其交点,即小方格的顶点,利用这些顶点进行透视变换。
检测竖直线的关键步骤如下:
dx = cv2.Sobel(image_with_mask, cv2.CV_16S, 1, 0)
dx = cv2.convertScaleAbs(dx)
cv2.normalize(dx, dx, 0, 255, cv2.NORM_MINMAX)
ret, close = cv2.threshold(dx, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
kernelx = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 10))
close = cv2.morphologyEx(close, cv2.MORPH_DILATE, kernelx, iterations=1)
binary, contour, hierarchy = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contour:
x, y, w, h = cv2.boundingRect(cnt)
if h / w > 5:
cv2.drawContours(close, [cnt], 0, 255, -1)
else:
cv2.drawContours(close, [cnt], 0, 0, -1)
close = cv2.morphologyEx(close, cv2.MORPH_CLOSE, None, iterations=2)
closex = close.copy()
Sobel 算子中第三个和第四个参数分别为dx、dy,这里写1,0就代表在x方向求一阶导,y方向不求导。第二个参数 cv2.CV_16S 的解释如下,摘抄自 [这篇文章][1] 。 > 在Sobel函数的第二个参数这里使用了cv2.CV_16S。因为OpenCV文档中对Sobel算子的介绍中有这么一句:“in the case of 8-bit input images it will result in truncated derivatives”。即Sobel函数求完导数后会有负值,还有会大于255的值。而原图像是uint8,即8位无符号数,所以Sobel建立的图像位数不够,会有截断。因此要使用16位有符号的数据类型,即cv2.CV_16S。 如下图中第二个所示,sobel 算子处理后会呈现灰色的图像,因此之后还需要使用 `convertScaleAbs( )` 函数将图像转回原来的uint8类型,然后重新归一化到 0-255 范围内,并再次二值化以便进行形态学操作。 接下来使用闭操作时结构元素选择为 **竖直方向较长的长方形**,闭操作能连接断开的小线段,选择这样的结构元素这样能更好地连接竖直线,结果如第五张图所示。 最后再次使用轮廓检测,并判断所有轮廓的长宽比,保留高比宽比例大的轮廓,即为竖直线。 步骤细节如图所示:
使用同样的方法获得水平线,将水平与竖直线条进行与运算得到交叉点图像,结果为一个个小白块。
res = cv2.bitwise_and(closex, closey)
结果如下图所示:
再次进行轮廓检测,检测到一个个小白块。求每个轮廓外接矩形的质心,即数独小方格的顶点。
img_dots = cv2.cvtColor(img_brightness_adjust, cv2.COLOR_GRAY2BGR)
binary, contour, hierarchy = cv2.findContours(res, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
centroids = []
for cnt in contour:
if cv2.contourArea(cnt) > 20:
mom = cv2.moments(cnt)
(x, y) = int(mom['m10'] / mom['m00']), int(mom['m01'] / mom['m00'])
cv2.circle(img_dots, (x, y), 4, (0, 255, 0), -1)
centroids.append((x, y))
centroids = np.array(centroids, dtype=np.float32)
c = centroids.reshape((100, 2))
c2 = c[np.argsort(c[:, 1])]
b = np.vstack([c2[i * 10:(i + 1) * 10][np.argsort(c2[i * 10:(i + 1) * 10, 0])] for i in range(10)])
bm = b.reshape((10, 10, 2))
其中求质心的用到了矩(moments)的概念,可参考 [官方教程]()。 最后得到的小方格顶点,如下所示:
以上步骤应该也能用 Harris 角点检测来实现,有兴趣的可以试一下,不过内部线条比较淡而且有数字的干扰,可能需要其他的处理步骤。
最后使用小方格四个顶点将每个小方格透视变换为标准大小。
res2 = cv2.cvtColor(img_brightness_adjust, cv2.COLOR_GRAY2BGR)
output = np.zeros((450, 450, 3), np.uint8)
for i, j in enumerate(b):
ri = i // 10
ci = i % 10
if ci != 9 and ri != 9:
src = bm[ri:ri + 2, ci:ci + 2, :].reshape((4, 2))
dst = np.array([[ci * 50, ri * 50], [(ci + 1) * 50 - 1, ri * 50], [ci * 50, (ri + 1) * 50 - 1],
[(ci + 1) * 50 - 1, (ri + 1) * 50 - 1]], np.float32)
retval = cv2.getPerspectiveTransform(src, dst)
warp = cv2.warpPerspective(res2, retval, (450, 450))
output[ri * 50:(ri + 1) * 50 - 1, ci * 50:(ci + 1) * 50 - 1] = warp[ri * 50:(ri + 1) * 50 - 1,
ci * 50:(ci + 1) * 50 - 1].copy()
img_correct = cv2.cvtColor(output, cv2.COLOR_BGR2GRAY)
img_puzzle = cv2.adaptiveThreshold(img_correct, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY_INV, 5, 7)
img_puzzle = cv2.resize(img_puzzle, (SIZE_PUZZLE, SIZE_PUZZLE), interpolation=cv2.INTER_LINEAR)
最终结果如下图所示:
至此就得到了清晰、标准的数独问题图像,为下一步提取并识别数字做好了准备。