211

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

OpenCV2.3.1のPythonAPIを使用してプログラミングを行いました。

以下は私がしたことです:

  1. 画像を読む
  2. 輪郭を見つける
  3. 面積が最大のものを選択します(また、正方形にいくらか相当します)。
  4. コーナーポイントを見つけます。

    例:以下に示します:

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

    ここで、緑色の線が数独の真の境界と正しく一致しているため、数独が正しく歪む可能性があることに注意してください。次の画像を確認してください)

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

    例:画像:

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

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

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

問題:

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

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

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

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

描かれた緑色の線は、歪んだ画像の輪郭となる近似輪郭です。

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

私の質問 :

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

4

6 に答える 6

268

機能するソリューションがありますが、それを自分で 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 でもできるはずです。スプライン ベースの画像変換は難しいかもしれませんが、本当に必要ではないと思います。おそらく、個々のセルで現在使用している透視変換を使用すると、十分な結果が得られます。

于 2012-04-19T11:22:54.750 に答える
223

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. Sudoku Square の検索とマスク画像の作成

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 に対応する関数が見つかりませんでした。(そこにあるかもしれませんが、わかりません)。

私が使いたくない 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()

結果 :

ここに画像の説明を入力

結果はnikieのものとほぼ同じですが、コード長が大きいです。より良い方法が利用できるかもしれませんが、それまではこれで問題ありません。

よろしくアーク。

于 2012-07-06T16:58:32.210 に答える
6

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

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

于 2012-04-18T06:54:09.950 に答える
1

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

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

編集:線形回帰を適用してエラーをチェックすることで、一連の等高線​​が線を形成するかどうかをチェックできました。ただし、線形回帰は、直線の傾きが大きすぎる (つまり >1000) 場合、または 0 に非常に近い場合にうまく機能しません。

于 2018-09-27T08:20:31.073 に答える