The Fool In The Valleyの雑記帳

-- 好奇心いっぱいのおじいちゃんが綴るよしなし事 --

数独でPythonを4

Python-OpenCVの開発環境の準備ができたのでプログラムの作成を開始しました。
目標とするプログラムは、

  1. 数独の問題を取り込む処理
  2. 数独の問題を解く処理

の2段階に分けて考えることができます。1はPCに接続したWebカメラ数独の画像を撮影し、その画像から数独の問題を抽出する処理ですが、その処理をさらに分解すると、①撮影した画像から正対した数独の問題の画像だけを取り出す画像処理の部分と、②その画像から各セルの画像を切り出して文字認識をする部分からなります。①の画像処理の部分は、具体的には以下のようなステップで行います。

以下は、この画像処理についての備忘のためにメモです。
f:id:tfitv:20200809090138p:plain

Webカメラで静止画を読み込む

使用したカメラはLogicoolのC525n。これをPCのUSB端子に接続する。
ビデオカメラからの映像を読み込むために、OpenCVにはVideoCaptureというクラスが用意されていて、引数にカメラの番号nを指定して呼び出すとそのインスタンスが生成される。そのフレームサイズは下記のようなプログラムを実行すると640×480であることがわかる。

import cv2
capture = cv2.VideoCapture(0)
width = capture.get(cv2.CAP_PROP_FRAME_WIDTH)
height = capture.get(cv2.CAP_PROP_FRAME_HEIGHT)
print('width x height =', width, 'x', height)

VideoCapture: ビデオカメラ、ビデオファイルを扱うためのクラス。
VideoCapture(n): 映像を読み込む場合は引数にカメラの番号nを指定する。通常は0が内蔵カメラで、内蔵カメラがないときは繋いだカメラで最初のものが0になるようだ。
capture: カメラの番号0からの映像を読み込むためのインスタンス

公式サイトにあるビデオの使い方のチュートリアルを参考にすると、次のプログラムでビデオカメラで撮影した画像を取り込み、ディスプレイに表示することができる。

import cv2
capture = cv2.VideoCapture(n)
while(True):
    ret, sudoku_color = capture.read()
    cv2.imshow('sudoku_color',sudoku_color)
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

read(): ビデオフレームを読み込むメソッド
sudoku_color : ndarray(縦のドット数, 横のドット数, 3)の3次元配列
imshow(フレーム名, ndarray): ndarray型の画像をフレーム名のウィンドウに表示する。
waitKey(1): 1msec処理を待つ関数でこれを実行すると画像が表示される。
ord:文字をUnicode値に変換する関数。
'q'が打たれるとWhile loopの外に出て、画像がndarray型のframeに保存される。

グレイスケールへ変換する

カラーで撮影された画像sudoku_colorを後の処理のためにグレースケール画像sudoku_gray に変換する。グレースケール画像 はndarray(縦のドット数, 横のドット数) の2次元配列である。

sudoku_gray = cv2.cvtColor(sudoku_color, cv2.COLOR_BGR2GRAY)

cvtColor(ndarray(480,640,3), cv2.COLOR_BGR2GRAY) :
sudoku_gray :  ndarray(480,640)
f:id:tfitv:20200810210558j:plain

白黒二値変換し反転する

グレースケール画像をスレッショルドで白黒二値化して反転させる。

ret, sudoku_bin_inv = cv2.threshold(sudoku_gray, 150, 255, cv2.THRESH_BINARY_INV)

threshold(sudoku_gray, それ以上を最大値にする閾値, 指定する最大値, cv2.THRESH_BINARY_INV)
ret, sudoku_bin_inv :戻り値はタプル型。
ret : float
sudoku_bin_inv : ndarray(480,640)
f:id:tfitv:20200810210617j:plain

輪郭を抽出する

非ゼロが隣接してつながった領域をオブジェクトとしてみなし、その輪郭をすべて抽出する。前の画像では白でつながった領域の一つ一つがオブジェクトになる。OpenCVを使うとその処理が次の一文で実行できるので強力である。この例では545個の輪郭が検出されている。

contours, hierarchy = cv2.findContours(sudoku_bin_inv, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)

findContours(対象画像, 輪郭検出モード, 輪郭の近似方法を指定)
contours : オブジェクトの輪郭座標を保持している配列。
hierarchy : オブジェクトの階層構造情報を保持している配列。

最大輪郭を取り出す

i番目の輪郭の座標情報を引数にして、その輪郭が囲む面積で計算する関数が用意されている。抽出された輪郭の中で、数独の問題全体をカバーする輪郭が最大の大きさを持っているはずである。そこで、その関数を利用してすべての抽出された輪郭の中で、最大の面積をもつ輪郭を求める。

for i in range(len(contours)):     
        if cv2.contourArea(contours[i]) > area_max:
            area_max = cv2.contourArea(contours[i])
            ID_max = i

contours : オブジェクトの輪郭座標を保持している配列。
len(contours) : 輪郭の数 = 545
contourArea(contours[i]) : i番目の輪郭の面積
area_max : 輪郭の最大の面積。この例では85297.0
ID_max : 最大の面積を持つ輪郭のインデックス。この例では473
最大の面積をもつ473番目の輪郭を緑で表示すると次の画像のようになる。
f:id:tfitv:20200810210654j:plain

四角形で近似する

複雑な形状をした輪郭を,単純な形状によって近似する。ここで、epsilonに適当な値を設定すると、数族の枠は四角形で近似することができる。

epsilon = 0.1*cv2.arcLength(contours ,True)
approx = cv2.approxPolyDP(contours ,epsilon,True)

contours : オブジェクトの輪郭座標を保持している配列。
epsilon : 近似の精度を表すパラメータ
approx : 四角形で近似された輪郭の座標を保持している配列。

コーナーを抽出する

輪郭の四角形の傾きを調べ、角の座標を左上から右回りにリストpts1に保存する。
pts1 : [4,2]
f:id:tfitv:20200810221216j:plain

正方形に透視変換する

輪郭の四角形の角pts1をpts2=[[0,0],[450,0],[0,450],[450,450]に透視変換する

M = cv2.getPerspectiveTransform(pts1,pts2)
warp = cv2.warpPerspective(src,M,dst)
ret, warp_bin = cv2.threshold(warp, 150, 255, cv2.THRESH_BINARY)

pts1 : 入力画像の四角形の頂点
pts2 : 出力画像の四角形の頂点
getPerspectiveTransform(pts1,pts2) : 変換行列Mの計算
src : 入力画像
dst : 出力画像サイズ ここでは(450,450)
warp_bin : 歪を補正した白黒二値画像
f:id:tfitv:20200810221055j:plain

数独でPythonを3 ← → 数独でPythonを5