如何消除数独广场中的凸性缺陷?

Abi*_*n K 179 python opencv sudoku computer-vision

我正在做一个有趣的项目:使用OpenCV从输入图像中解决数独(如Google护目镜等).我完成了任务,但最后我发现了一个问题,我来到这里.

我使用OpenCV 2.3.1的Python API进行编程.

以下是我的所作所为:

  1. 阅读图片
  2. 找到轮廓
  3. 选择具有最大面积的那个(并且也有点等于正方形).
  4. 找到角点.

    例如,如下:

    在此输入图像描述

    (请注意,绿线正确地与Sudoku的真实边界重合,因此可以正确扭曲数独.查看下一张图片)

  5. 将图像扭曲成完美的正方形

    例如:

    在此输入图像描述

  6. 执行OCR(我使用我在OpenCV-Python中的简单数字识别OCR中给出的方法)

而且方法效果很好.

问题:

看看这个图像.

在此图像上执行第4步,结果如下:

在此输入图像描述

绘制的红线是原始轮廓,它是数独边界的真实轮廓.

绘制的绿线是近似轮廓,它将是扭曲图像的轮廓.

当然,在数独的上边缘绿线和红线之间存在差异.因此,在翘曲时,我没有得到数独的原始边界.

我的问题 :

如何在数独的正确边界上扭曲图像,即红线,或者如何消除红线和绿线之间的差异?在OpenCV中有没有这方法?

Nik*_*iki 239

我有一个有效的解决方案,但您必须自己将其转换为OpenCV.它是用Mathematica编写的.

第一步是通过将每个像素除以关闭操作的结果来调整图像中的亮度:

src = ColorConvert[Import["http://davemark.com/images/sudoku.jpg"], "Grayscale"];
white = Closing[src, DiskMatrix[5]];
srcAdjusted = Image[ImageData[src]/ImageData[white]]
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

下一步是找到数独区域,这样我就可以忽略(掩盖掉)背景.为此,我使用连通分量分析,并选择具有最大凸区域的组件:

components = 
  ComponentMeasurements[
    ColorNegate@Binarize[srcAdjusted], {"ConvexArea", "Mask"}][[All, 
    2]];
largestComponent = Image[SortBy[components, First][[-1, 2]]]
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

通过填充此图像,我获得了数独网格的掩码:

mask = FillingTransform[largestComponent]
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

现在,我可以使用二阶导数滤波器在两个单独的图像中找到垂直和水平线:

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

在此输入图像描述

我再次使用连通分量分析从这些图像中提取网格线.网格线比数字长得多,因此我可以使用卡尺长度来仅选择网格线连接的组件.按位置对它们进行排序,我为图像中的每个垂直/水平网格线获得2x10个蒙版图像:

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

在此输入图像描述

接下来,我取每对垂直/水平网格线,扩大它们,计算逐个像素的交点,并计算结果的中心.这些点是网格线交叉点:

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

在此输入图像描述

最后一步是为这些点定义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]
Run Code Online (Sandbox Code Playgroud)

在此输入图像描述

所有操作都是基本的图像处理功能,所以这在OpenCV中也是可行的.基于样条的图像转换可能更难,但我认为你并不需要它.可能使用现在在每个单独单元格上使用的透视变换将提供足够好的结果.

  • 哦,我的上帝 !!!!!!!!!这太棒了.这真的很棒.我将尝试在OpenCV中创建它.希望您能帮助我了解某些功能和术语的详细信息......谢谢. (3认同)
  • 惊人的答案!你在哪里得到了关闭除以图像亮度标准化的想法?我正在努力提高这种方法的速度,因为手机上的浮点除法非常缓慢.你有什么建议吗?@AbidRahmanK (2认同)
  • @1*:我认为它被称为“白色图像调整”。不要问我在哪里读到过它,它是一个标准的图像处理工具。这个想法背后的模型很简单:从(朗伯)表面反射的光量只是表面亮度乘以同一位置的白色物体反射的光量。估计同一位置的白色物体的表观亮度,将实际亮度除以该亮度,就可以得到表面的亮度。 (2认同)

Abi*_*n K 200

Nikie的答案解决了我的问题,但他的回答是在Mathematica.所以我认为我应该在这里进行OpenCV改编.但是在实现之后我可以看到OpenCV代码比nikie的mathematica代码要大得多.而且,我无法在OpenCV中找到由nikie完成的插值方法(尽管可以使用scipy完成,我会在时间到来时告诉它.)

1.图像预处理(关闭操作)

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

结果:

结果的结果

2.寻找数独广场并创建蒙版图像

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

结果:

在此输入图像描述

3.查找垂直线条

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

结果:

在此输入图像描述

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

结果:

在此输入图像描述

当然,这个并不是那么好.

5.寻找网格点

res = cv2.bitwise_and(closex,closey)
Run Code Online (Sandbox Code Playgroud)

结果:

在此输入图像描述

6.纠正缺陷

在这里,nikie进行了某种插值,我对此并不了解.我找不到这个OpenCV的任何相应功能.(可能就在那里,我不知道).

查看这个SOF,它解释了如何使用SciPy来实现这一点,我不想使用它:OpenCV中的图像转换

所以,在这里我采用了每个子方块的4个角,并对每个子角应用了warp Perspective.

为此,首先我们找到质心.

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

但是生成的质心将不会被排序.查看下面的图像以查看他们的订单:

在此输入图像描述

所以我们从左到右,从上到下对它们进行排序.

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

现在看下面他们的订单:

在此输入图像描述

最后,我们应用转换并创建一个大小为450x450的新图像.

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

结果:

在此输入图像描述

结果几乎与nikie相同,但代码长度很大.可能是,那里有更好的方法,但在那之前,这个工作正常.

关心ARK.

  • "我更喜欢我的应用程序崩溃,而不是得到错误的答案." < - 我也同意这100% (3认同)

sie*_*hie 5

您可以尝试使用某种基于网格的任意变形建模.由于数独已经是一个网格,这应该不会太难.

因此,您可以尝试检测每个3x3子区域的边界,然后单独扭曲每个区域.如果检测成功,它会给你一个更好的近似值.