数独の正方形の凸状欠陥を取り除く方法は?


192

私は楽しいプロジェクトをしていた:OpenCVを使用して(Googleゴーグルなどのように)入力画像から数独を解く。そして、私はタスクを完了しましたが、最後に私がここに来た小さな問題を見つけました。

OpenCV 2.3.1のPython APIを使用してプログラミングを行いました。

以下は私がやったことです:

  1. 画像を読む
  2. 輪郭を見つける
  3. 最大の面積を持つものを選択します(また、正方形と多少同等です)。
  4. コーナーポイントを見つけます。

    たとえば、以下のとおりです。

    ここに画像の説明を入力してください

    緑の線が数独の真の境界と正確に一致しているため、数独を正しく歪めることができます。次の画像を確認してください)

  5. 画像を完全な正方形にワープします

    例:画像:

    ここに画像の説明を入力してください

  6. OCRを実行します(OpenCV-PythonのSimple Digit Recognition OCRで指定した方法を使用しました)

そして、方法はうまくいきました。

問題:

この画像をチェックしてください

この画像に対して手順4を実行すると、以下の結果が得られます。

ここに画像の説明を入力してください

描かれた赤い線は、数独境界の真の輪郭である元の輪郭です。

描かれた緑の線は近似された輪郭であり、ワープされた画像の輪郭になります。

もちろん、数独の上端で緑の線と赤の線に違いがあります。したがって、ワープしている間、私は数独の元の境界を取得していません。

私の質問 :

数独の正しい境界、つまり赤い線で画像を歪めるにはどうすればよいですか、またはどのように赤い線と緑の線の違いを取り除くことができますか?OpenCVでこれを行う方法はありますか?


1
赤と緑の線が一致するコーナーポイントに基づいて検出を行っています。私はOpenCVを知りませんが、おそらくそれらのコーナーポイント間のラインを検出し、それに基づいてワープする必要があります。
Dougal

おそらく、コーナーポイントを結ぶ線を画像内の太い黒のピクセルと一致させます。つまり、緑の線でコーナーポイント間の直線を見つけるだけでなく、濃い黒のピクセルを横切るように強制します。これはあなたの問題をかなり難しくするだろうと私は思います、そしてあなたにすぐに役立つOpenCVビルトインについては知りません。
e

@ Dougal:描かれた緑の線は、赤い線の近似直線だと思います。つまり、これらのコーナーポイント間のラインです。緑の線に従ってワープすると、ワープした画像の上部に赤い曲線が表示されます。(私は私の説明が少し悪いようだ、あなたは理解してほしい)
ABIDラーマンK

@ EMS:描かれた赤い線が数独の境界にあると思います。しかし問題は、数独の境界でイメージを正確に歪める方法です。(つまり、問題はワーピングにあります。つまり、2番目の画像に示すように、湾曲した境界線を正確な正方形に変換します)
Abid Rahman K

回答:


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]

ここに画像の説明を入力してください

これで、2次微分フィルターを使用して、2つの別々の画像で垂直線と水平線を見つけることができます。

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マッピングの2つの補間関数を定義し、これらの関数を使用して画像を変換することです。

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で作ってみます。特定の機能と用語についての詳細を教えていただければ幸いです...ありがとうございます。
Abid Rahman K

@arkiaz:私はOpenCVのエキスパートではありませんが、できればお手伝いします。
ニキ

「クロージング」機能とは何ですか?バックグラウンドで何が起こっているのですか?ドキュメントでは、閉じると塩とコショウのノイズが除去されると書かれていますか?ローパスフィルターを閉じていますか?
Abid Rahman 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.欠陥を修正する

ここで、ニキエはある種の補間を行いますが、それについて私はあまり知識がありません。そして、このOpenCVに対応する関数を見つけることができませんでした。(そこにあるかもしれませんが、わかりません)。

私が使いたくないSciPyを使用してこれを行う方法を説明しているこのSOFを確認してください。OpenCVでの画像変換

そこで、ここでは各サブスクエアの4つのコーナーを取り、それぞれにワープパースペクティブを適用しました。

そのために、最初に重心を見つけます。

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

結果:

ここに画像の説明を入力してください

結果はニキエのものとほとんど同じですが、コードの長さが長くなります。より良い方法が利用できるかもしれませんが、それまでは、これで問題なく動作します。

ARKよろしく。


4
「私は間違った答えを得るよりもアプリケーションをクラッシュさせる方を好みます。」<-私もこの100%に同意します
Viktor Sehr 2012

おかげで、その真の答えはニキーによって与えられます。しかし、それはMathematicaにあったので、OpenCVに変換しました。したがって、本当の答えには十分な賛成票があると思います
アビッドラーマンK

ああ、あなたも質問を投稿したのを見ません
Viktor Sehr

うん。質問も私のものです。鉱山とニキエの答えは最後だけ違う。彼はnumpyまたはopencvにないMathematicaにある種の補間関数を持っています(しかし、それは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 TypeError:long()引数は、「builtin_function_or_method」ではなく、文字列または数値でなければなりません
user898678

6

任意のワープのある種のグリッドベースのモデリングを使用してみることができます。そして、数独はすでにグリッドなので、それほど難しくはないはずです。

したがって、各3x3サブリージョンの境界を検出して、各リージョンを個別にワープすることができます。検出が成功した場合、より良い近似が得られます。


1

上記の方法は、数独ボードがまっすぐに立っている場合にのみ機能することを追加したいと思います。そうでない場合、高さ/幅(またはその逆)比率テストはおそらく失敗し、数独のエッジを検出できなくなります。(画像の境界に対して垂直でない線でも、線が両方の軸に対してエッジを持つため、ソーベル操作(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()
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.