如何消除数独方块中的凸度缺陷?


192

我当时在做一个有趣的项目:使用OpenCV(如Google护目镜等)从输入图像中解决数独问题。我已经完成了任务,但是最后我遇到了一个小问题。

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

以下是我所做的:

  1. 读取图像
  2. 找到轮廓
  3. 选择面积最大的那个(也有些等同于正方形的那个)。
  4. 找到拐角点。

    例如下面给出:

    在此处输入图片说明

    请注意,绿线正确地与数独的真实边界重合,因此数独可以正确变形。请检查下一张图片)

  5. 使图像变形为完美的正方形

    例如图片:

    在此处输入图片说明

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

而且该方法效果很好。

问题:

退房 这张图片。

在此图像上执行步骤4会得到以下结果:

在此处输入图片说明

画出的红线是原始轮廓,是数独边界的真实轮廓。

画出的绿线是近似轮廓,它将是变形图像的轮廓。

数独顶部的绿线和红线之间当然有区别。因此,在扭曲时,我并没有获得数独的原始边界。

我的问题 :

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


1
您正在基于拐角点进行检测,红线和绿线一致。我不了解OpenCV,但是大概您想检测出这些拐角点之间的线,并据此进行弯曲。
Dougal 2012年

也许强制连接角点的线与图像中较重的黑色像素重合。也就是说,不要让绿线仅在拐角点之间找到一条直线,而要迫使它们穿越较重的黑色像素。我认为,这将使您的问题更加困难,而且我不知道有任何对您立即有用的OpenCV内置程序。
2012年

// @ Dougal:我认为画的绿线是红线的近似直线。因此它是这些角点之间的线。当我根据绿线翘曲时,在翘曲图像的顶部出现弯曲的红线。(我希望您能理解,我的解释似乎有点不好)
Abid Rahman K 2012年

@ EMS:我认为红线正好在数独的边界上。但是问题是,如何在数独的边界上精确地扭曲图像。(我的意思是,问题在于翘曲,即将弯曲的边界转换为精确的正方形,如我在第二张图片中所示)
Abid Rahman K

Answers:


251

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

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

src = ColorConvert[Import["http://davemark.com/images/sudoku.jpg"], "Grayscale"];
white = Closing[src, DiskMatrix[5]];
srcAdjusted = Image[ImageData[src]/ImageData[white]]

在此处输入图片说明

下一步是找到数独区域,因此我可以忽略(遮盖)背景。为此,我使用连接组件分析,然后选择凸面面积最大的组件:

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];

在此处输入图片说明

我再次使用连接的分量分析从这些图像中提取网格线。网格线比数字长得多,因此我可以使用卡尺长度来仅选择与网格线相连的组件。按位置对它们进行排序,对于图像中的每个垂直/水平网格线,我得到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]];

在此处输入图片说明

接下来,我将每对垂直/水平网格线进行放大,将它们放大,计算出像素间的交点,并计算结果的中心。这些点是网格线的交点:

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]

在此处输入图片说明

所有操作都是基本的图像处理功能,因此在OpenCV中也应该可行。基于样条的图像转换可能会更困难,但我认为您并不是真的需要它。可能使用您现在在每个单个单元格上使用的透视变换,将获得足够好的结果。


3
哦,我的上帝 !!!!!!!!!太棒了。这真的很棒。我将尝试在OpenCV中实现。希望您能帮助我提供某些功能和术语的详细信息...谢谢。
阿比德·拉赫曼

@arkiaz:我不是OpenCV专家,但是如果可以的话,我会帮助的。
Niki 2012年

您能否解释一下“关闭”功能的作用?我的意思是背景发生了什么?在文档中,它说关闭会消除盐和胡椒粉的噪音?正在关闭低通滤波器吗?
阿比德·拉赫曼K

2
惊人的答案!您从何处得到除以闭合值以归一化图像亮度的想法?我正在尝试提高此方法的速度,因为在手机上浮点除法的速度很慢。你有什么建议吗?@AbidRahmanK
1 ''

1
@ 1 *:我认为这称为“白色图像调整”。不要问我在哪里读到的,它是标准的图像处理工具。这个想法背后的模型很简单:从(朗伯)表面反射的光量只是表面亮度乘以同一位置的白色物体反射的光量。估计在相同位置的白色物体的视在亮度,将实际亮度除以该亮度,即可得到表面的亮度。
Niki

208

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)

结果:

结帐结果

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)

结果:

在此处输入图片说明

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()

结果:

在此处输入图片说明

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的任何相应功能。(也许在那里,我不知道)。

查看此SOF,它说明了如何使用SciPy进行此操作,我不想使用它:OpenCV中的图像转换

因此,在这里,我将每个子正方形的四个角用作每个变角透视图。

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

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))

但是结果质心将不会被排序。查看下图以查看其顺序:

在此处输入图片说明

因此,我们从左到右,从上到下对它们进行排序。

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))

现在看下面他们的命令:

在此处输入图片说明

最后,我们应用转换并创建尺寸为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()

结果:

在此处输入图片说明

结果几乎与nikie相同,但是代码长度很大。也许可以使用更好的方法,但是在那之前,这种方法行之有效。

关于方舟。


4
“我希望应用程序崩溃而不是得到错误的答案。” <-我也同意这100%
Viktor Sehr 2012年

谢谢,它的真实答案是妮基。但这是在mathematica中进行的,因此我将其转换为OpenCV。因此,我认为真正的答案已经得到足够的支持
Abid Rahman K

没看到您也发布了问题:)
Viktor Sehr 2012年

是的 问题也是我的。我和妮基的答案只有到最后才有所不同。他在mathematica中获得了某种插值函数,该函数不在numpy或opencv中(但在Scipy中存在,但是我不想在这里使用Scipy)
Abid Rahman K

我收到错误消息: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类型错误:长()参数必须是字符串或数字,而不是'builtin_function_or_method'
user898678

6

您可以尝试对任意扭曲使用某种基于网格的建模。而且,由于数独已经是一个网格,所以它应该不会太难。

因此,您可以尝试检测每个3x3子区域的边界,然后分别对每个区域进行变形。如果检测成功,它将为您提供更好的近似值。


1

我想补充一点,上述方法仅在数独板直立时才有效,否则高度/宽度(反之亦然)的比率测试很可能会失败,并且您将无法检测数独的边缘。(我还想补充一点,如果不垂直于图像边界的线,则sobel操作(dx和dy)将仍然有效,因为线相对于两个轴仍然具有边缘。)

为了能够检测直线,您应该进行轮廓分析或逐像素分析,例如ContourArea / boundingRectArea,左上角和右下角点...

编辑:我设法通过应用线性回归并检查错误来检查一组轮廓是否形成一条线。但是,当直线的斜率太大(即> 1000)或非常接近0时,线性回归的效果较差。因此,在线性回归之前应用上述比率测试(在大多数赞成的答案中)是合乎逻辑的,对我来说确实有用。


1

为了去除未切割的角,我应用了伽玛校正,其伽玛值为0.8。

伽玛校正之前

绘制红色圆圈以显示缺少的角。

伽玛校正后

代码是:

gamma = 0.8
invGamma = 1/gamma
table = np.array([((i / 255.0) ** invGamma) * 255
                  for i in np.arange(0, 256)]).astype("uint8")
cv2.LUT(img, table, img)

如果缺少某些关键点,这是对Abid Rahman的回答的补充。


0

我认为这是一个很棒的帖子,也是ARK的一个很好的解决方案。很好地布置和解释。

我正在研究类似的问题,并完成了整个工作。进行了一些更改(例如,xrange到range,cv2.findContours中的参数),但是应该可以立即使用(Python 3.5,Anaconda)。

这是上述元素的汇编,并添加了一些缺少的代码(即,标记点)。

'''

/programming/10196198/how-to-remove-convexity-defects-in-a-sudoku-square

'''

import cv2
import numpy as np

img = cv2.imread('test.png')

winname="raw image"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,100)


img = cv2.GaussianBlur(img,(5,5),0)

winname="blurred"
cv2.namedWindow(winname)
cv2.imshow(winname, img)
cv2.moveWindow(winname, 100,150)

gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
mask = np.zeros((gray.shape),np.uint8)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))

winname="gray"
cv2.namedWindow(winname)
cv2.imshow(winname, gray)
cv2.moveWindow(winname, 100,200)

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)

winname="res2"
cv2.namedWindow(winname)
cv2.imshow(winname, res2)
cv2.moveWindow(winname, 100,250)

 #find elements
thresh = cv2.adaptiveThreshold(res,255,0,1,19,2)
img_c, 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)

winname="puzzle only"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,300)

# vertical lines
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)

img_d, 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()

winname="vertical lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_d)
cv2.moveWindow(winname, 100,350)

# find horizontal lines
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)

img_e, 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()

winname="horizontal lines"
cv2.namedWindow(winname)
cv2.imshow(winname, img_e)
cv2.moveWindow(winname, 100,400)


# intersection of these two gives dots
res = cv2.bitwise_and(closex,closey)

winname="intersections"
cv2.namedWindow(winname)
cv2.imshow(winname, res)
cv2.moveWindow(winname, 100,450)

# text blue
textcolor=(0,255,0)
# points green
pointcolor=(255,0,0)

# find centroids and sort
img_f, 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
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))

# make copy
labeled_in_order=res2.copy()

for index, pt in enumerate(b):
    cv2.putText(labeled_in_order,str(index),tuple(pt),cv2.FONT_HERSHEY_DUPLEX, 0.75, textcolor)
    cv2.circle(labeled_in_order, tuple(pt), 5, pointcolor)

winname="labeled in order"
cv2.namedWindow(winname)
cv2.imshow(winname, labeled_in_order)
cv2.moveWindow(winname, 100,500)

# create final

output = np.zeros((450,450,3),np.uint8)
for i,j in enumerate(b):
    ri = int(i/10) # row index
    ci = i%10 # column index
    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()

winname="final"
cv2.namedWindow(winname)
cv2.imshow(winname, output)
cv2.moveWindow(winname, 600,100)

cv2.waitKey(0)
cv2.destroyAllWindows()
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.