如何消除数独广场的凸面缺陷?
我正在做一个有趣的项目:使用OpenCV从input图像解决Sudoku(如谷歌护目镜等)。 我已经完成了任务,但最终我发现了一个小问题,我来到这里。
我使用OpenCV 2.3.1的Python API编程。
以下是我所做的:
- 阅读图像
- find轮廓
- select最大面积的(也有点相当于平方)。
-
findangular点。
例如下面给出:
( 注意这里的绿线与Sudoku的真正边界正好相符,所以Sudoku可以正确地扭曲 。请检查下一张图片)
-
扭曲形象到一个完美的广场
如图像:
-
执行OCR(为此我使用了OpenCV-Python中简单数字识别OCR中给出的方法)
该方法运作良好。
问题:
看看这个图片。
执行此图像上的步骤4给出以下结果:
绘制的红线是数独边界真实轮廓的原始轮廓。
绘制的绿色线是近似的轮廓,它将是变形图像的轮廓线。
当然,在数独的上边缘,绿线和红线之间是有区别的。 所以在歪曲的时候,我没有得到数独的原始界限。
我的问题 :
我怎样才能在Sudoku的正确边界,即红线,或怎样才能消除红线和绿线之间的区别? 在OpenCV中有没有这样的方法?
我有一个可行的解决scheme,但是你必须自己把它翻译成OpenCV。 它是用Mathematica写的。
第一步是调整图像中的亮度,将每个像素除以closures操作的结果:
src = ColorConvert[Import["http://davemark.comhttp://img.dovov.comsudoku.jpg"], "Grayscale"]; white = Closing[src, DiskMatrix[5]]; srcAdjusted = Image[ImageData[src]/ImageData[white]]
下一步是find数独区域,所以我可以忽略(掩盖)背景。 为此,我使用连通分量分析,并select具有最大凸面积的组件:
components = ComponentMeasurements[ ColorNegate@Binarize[srcAdjusted], {"ConvexArea", "Mask"}][[All, 2]]; largestComponent = Image[SortBy[components, First][[-1, 2]]]
通过填充这个图像,我得到了一个数独网格的面具:
mask = FillingTransform[largestComponent]
现在,我可以使用二阶导数滤波器来查找两个单独图像中的垂直和水平线:
lY = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {2, 0}], {0.02, 0.05}], mask]; lX = ImageMultiply[MorphologicalBinarize[GaussianFilter[srcAdjusted, 3, {0, 2}], {0.02, 0.05}], mask];
我再次使用连接组件分析从这些图像中提取网格线。 网格线比数字长得多,所以我可以使用卡尺长度来只select网格线连接的组件。 按位置对它们进行sorting,得到图像中每个垂直/水平网格线的2×10个蒙版图像:
verticalGridLineMasks = SortBy[ComponentMeasurements[ lX, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 2]], #[[2, 1]] &][[All, 3]]; horizontalGridLineMasks = SortBy[ComponentMeasurements[ lY, {"CaliperLength", "Centroid", "Mask"}, # > 100 &][[All, 2]], #[[2, 2]] &][[All, 3]];
接下来,我把每一对垂直/水平网格线,扩大他们,计算逐个像素的交集,并计算结果的中心。 这些点是网格线的交点:
centerOfGravity[l_] := ComponentMeasurements[Image[l], "Centroid"][[1, 2]] gridCenters = Table[centerOfGravity[ ImageData[Dilation[Image[h], DiskMatrix[2]]]* ImageData[Dilation[Image[v], DiskMatrix[2]]]], {h, horizontalGridLineMasks}, {v, verticalGridLineMasks}];
最后一步是通过这些点为X / Y映射定义两个插值函数,并使用这些函数对图像进行变换:
fnX = ListInterpolation[gridCenters[[All, All, 1]]]; fnY = ListInterpolation[gridCenters[[All, All, 2]]]; transformed = ImageTransformation[ srcAdjusted, {fnX @@ Reverse[#], fnY @@ Reverse[#]} &, {9*50, 9*50}, PlotRange -> {{1, 10}, {1, 10}}, DataRange -> Full]
所有的操作都是基本的image processingfunction,所以在OpenCV中也是可以的。 基于样条的图像转换可能会更困难,但我不认为你真的需要它。 可能使用现在在每个单元格上使用的透视变换将会给出足够好的结果。
尼基的答案解决了我的问题,但他的答案是在Mathematica。 所以我认为我应该在这里给它的OpenCV适应。 但是在实现之后,我可以看到OpenCV代码比nikie的mathematica代码大得多。 另外,在OpenCV中我找不到nikie的插值方法(虽然可以用scipy来完成,但是我会在时间到来的时候告诉它)。
1.图像预处理(closures操作)
import cv2 import numpy as np img = cv2.imread('dave.jpg') img = cv2.GaussianBlur(img,(5,5),0) gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY) mask = np.zeros((gray.shape),np.uint8) kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11)) close = cv2.morphologyEx(gray,cv2.MORPH_CLOSE,kernel1) div = np.float32(gray)/(close) res = np.uint8(cv2.normalize(div,div,0,255,cv2.NORM_MINMAX)) res2 = cv2.cvtColor(res,cv2.COLOR_GRAY2BGR)
结果:
2.find数独广场和创build蒙版图像
thresh = cv2.adaptiveThreshold(res,255,0,1,19,2) contour,hier = cv2.findContours(thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE) max_area = 0 best_cnt = None for cnt in contour: area = cv2.contourArea(cnt) if area > 1000: if area > max_area: max_area = area best_cnt = cnt cv2.drawContours(mask,[best_cnt],0,255,-1) cv2.drawContours(mask,[best_cnt],0,0,2) res = cv2.bitwise_and(res,mask)
结果:
3.find垂直线
kernelx = cv2.getStructuringElement(cv2.MORPH_RECT,(2,10)) dx = cv2.Sobel(res,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) close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernelx,iterations = 1) contour, hier = 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()
结果:
4.查找水平线
kernely = cv2.getStructuringElement(cv2.MORPH_RECT,(10,2)) dy = cv2.Sobel(res,cv2.CV_16S,0,2) dy = cv2.convertScaleAbs(dy) cv2.normalize(dy,dy,0,255,cv2.NORM_MINMAX) ret,close = cv2.threshold(dy,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU) close = cv2.morphologyEx(close,cv2.MORPH_DILATE,kernely) contour, hier = cv2.findContours(close,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) for cnt in contour: x,y,w,h = cv2.boundingRect(cnt) if w/h > 5: cv2.drawContours(close,[cnt],0,255,-1) else: cv2.drawContours(close,[cnt],0,0,-1) close = cv2.morphologyEx(close,cv2.MORPH_DILATE,None,iterations = 2) closey = close.copy()
结果:
当然,这个不太好。
5.查找网格点
res = cv2.bitwise_and(closex,closey)
结果:
6.纠正缺陷
在这里,nikie做了一些插补,关于这个插补我没有太多的知识。 而我找不到这个OpenCV的任何相应的function。 (可能是在那里,我不知道)。
看看这个SOF解释了如何使用SciPy来做到这一点,我不想使用它: 在OpenCV中的图像转换
所以,我在这里每个子广场的四个angular落,并应用经向透视。
为此,首先我们find质心。
contour, hier = cv2.findContours(res,cv2.RETR_LIST,cv2.CHAIN_APPROX_SIMPLE) centroids = [] for cnt in contour: mom = cv2.moments(cnt) (x,y) = int(mom['m10']/mom['m00']), int(mom['m01']/mom['m00']) cv2.circle(img,(x,y),4,(0,255,0),-1) centroids.append((x,y))
但是结果的质心不会被sorting。 看看下面的图片,看看他们的顺序:
所以我们从左到右,从上到下sorting。
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 xrange(10)]) bm = b.reshape((10,10,2))
现在看下面的命令:
最后,我们应用这个转换,创build一个尺寸为450×450的新图像。
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()
结果:
结果几乎与nikie相同,但代码长度很大。 可能会有更好的方法可用,但在此之前,这工作正常。
问候ARK。
你可以尝试使用某种基于网格的任意变形build模。 由于数独已经是一个网格,所以不应该太难。
因此,您可以尝试检测每个3×3分区的边界,然后分别翘曲每个区域。 如果检测成功,它会给你一个更好的近似值。