web-dev-qa-db-ja.com

数独正方形の凸欠陥を削除する方法?

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

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

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

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

    例えば下記のとおり:

    enter image description here

    緑の線が数独の真の境界と正しく一致しているため、数独を正しくワープできるように注意してください。次の画像を確認してください)

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

    例:画像:

    enter image description here

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

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

問題:

この画像をチェックアウト

この画像で手順4を実行すると、次の結果が得られます。

enter image description here

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

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

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

私の質問:

数独の正しい境界、つまり赤い線OR)で画像をワープするにはどうすればよいですか?赤い線と緑の線の違いを取り除くにはどうすればよいですか?

176
Abid Rahman K

動作するソリューションがありますが、OpenCVに自分で翻訳する必要があります。 Mathematicaで書かれています。

最初のステップは、各ピクセルをクローズ操作の結果で分割することにより、画像の明るさを調整することです。

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

enter image description here

次のステップは、数独領域を見つけることです。そのため、背景を無視(マスクアウト)できます。そのために、私は連結成分分析を使用し、最大の凸面積を持つコンポーネントを選択します。

components = 
  ComponentMeasurements[
    ColorNegate@Binarize[srcAdjusted], {"ConvexArea", "Mask"}][[All, 
    2]];
largestComponent = Image[SortBy[components, First][[-1, 2]]]

enter image description here

この画像を埋めることで、数独グリッドのマスクを取得します。

mask = FillingTransform[largestComponent]

enter image description here

これで、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];

enter image description here

これらの画像からグリッド線を抽出するために、接続コンポーネント分析を再度使用します。グリッド線は数字よりもはるかに長いため、キャリパーの長さを使用して、グリッド線に接続されたコンポーネントのみを選択できます。それらを位置で並べ替えると、画像の垂直/水平グリッド線ごとに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]];

enter image description here

次に、垂直/水平グリッド線の各ペアを取得し、それらを膨張させ、ピクセルごとの交差を計算し、結果の中心を計算します。これらの点は、グリッド線の交点です。

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

enter image description here

最後のステップは、これらのポイントを介した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]

enter image description here

すべての操作は基本的な画像処理機能であるため、これはOpenCVでも可能です。スプラインベースの画像変換はもっと難しいかもしれませんが、私はあなたが本当にそれを必要とは思わない。おそらく、個々のセルで現在使用している透視変換を使用すると、十分な結果が得られます。

236
Niki

Nikieの答えは私の問題を解決しましたが、彼の答えはMathematicaにありました。だから私はここでそのOpenCV適応を与えるべきだと思った。しかし、実装後、OpenCVコードはnikieの数学コードよりもはるかに大きいことがわかりました。また、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)

結果:

Result of closing

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)

結果:

enter image description here

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

結果:

enter image description here

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

結果:

enter image description here

もちろん、これはあまり良くありません。

5。グリッドポイントの検索

res = cv2.bitwise_and(closex,closey)

結果:

enter image description here

6。欠陥の修正

ここでは、ニキーは何らかの補間を行いますが、これについてはあまり知識がありません。そして、このOpenCVに対応する関数が見つかりませんでした。 (多分そこにあるかもしれない、私は知らない)。

SciPyを使用してこれを行う方法を説明しているこのSOFを確認してください。SciPyは使用しません: 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))

ただし、結果の重心はソートされません。以下の画像をチェックして、注文を確認してください。

enter image description here

そこで、左から右、上から下に並べ替えます。

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

以下の順序を参照してください。

enter image description here

最後に、変換を適用し、サイズ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()

結果:

enter image description here

結果はnikieのものとほとんど同じですが、コードの長さが長くなります。おそらく、より良い方法がそこにありますが、それまではこれで問題ありません。

よろしくお願いします。

198
Abid Rahman K

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

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

5
sietschie

上記の方法は、数独ボードがまっすぐ立っている場合にのみ機能することを付け加えます。 (画像の境界線に垂直ではない線の場合、sobel操作(dxおよびdy)は引き続き機能しますが、線は両方の軸に対してエッジを持っているため、追加したいです。)

直線を検出できるようにするには、contourArea/boundingRectArea、左上および右下のポイントなどの輪郭またはピクセル単位の分析に取り組む必要があります...

編集:線形回帰を適用し、エラーをチェックすることにより、輪郭のセットが線を形成するかどうかを管理しました。ただし、直線の傾きが大きすぎる(つまり、> 1000)か、0に非常に近い場合、線形回帰のパフォーマンスは低下します。したがって、線形回帰の前に上記の比率テストを適用する(ほとんどの投票結果)のは論理的であり、私にとってはうまくいきました。

1
Ali Eren Çelik

角のない角を削除するために、ガンマ値0.8のガンマ補正を適用しました。

Before gamma correction

赤い円は、欠けているコーナーを示すために描かれています。

After gamma correction

コードは次のとおりです。

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の回答に追加されます。

1
Vardan Agarwal