みなさん、こんにちは。どんぶラッコです。
今回は、OpenCVを用いて 表の場所を認識するプログラムを書いてみましょう!
と言ってもどういうことをするのかいまいちイメージがつきませんよね?
つまり、

このような表があったら…

このように、表の形になっている位置を取得することができるサンプルです。
x座標, y座標, 幅, 高さ を 辞書型でエクスポートしてあげると、より何をしているのかわかりやすいかもしれません。

今回は、このように表の位置を取得できるプログラムを一緒に書いていこうと思います!
今回参考にしたサイト
この記事を執筆するに当たって多くのサイトを参考にしたので、予めご紹介しておきます。
- cv2.Canny(): Canny法によるエッジ検出の調整をいい感じにする – Qiita
- OpenCV – Canny 法で画像からエッジを検出する方法 – Pystyle
- 【Python】OpenCVで輪郭の検出 – findContours(), drawContours()
- OpenCVでの表のセルの認識方法 – teratail
- OpenCV: Contours
特にteratailのサイトのanswersには非常に助けられました!
ライブラリのインストール・インポート
まずopencvをインストールしていない場合は予めインストールしておきましょう。
# opencvをインストールしていない場合
pip install opencv-python続いて今回使用するライブラリです。 cv2 というのが opencvのライブラリです。pip installした時の名前と違うので注意が必要です。
import cv2
from IPython.display import Image, display
import numpy as np
import copy
import json関数の作成
後ほど使うことになる関数を予め作成しておきます。
# cf.) https://pystyle.info/opencv-canny/
def convert_nparr_to_image(img):
    ret, encoded = cv2.imencode(".jpg", img)
    display(Image(encoded))後ほど説明しますが、今回は画像データをnumpy配列で取り扱います。これはjpgに配列をencodeして表示してくれるプログラムです。 jupyterlab ( notebook )でコーディングしている方は確認用に使いましょう。
def create_pos_data(contour):
    x, y, w, h = cv2.boundingRect(contour)
    return {'x': x, 'y': y, 'width': w, 'height': h}contour ( 輪郭 ) 情報から x, y, width, height の情報を取り出します。 JSON形式にエクスポートするときに使います。
イメージを読み込む
下準備が終わったところで、画像を読み込みましょう。
img = cv2.imread('./assets/sample.jpg')
height, width, channels = img.shape
gray_img = cv2.imread('./assets/sample.jpg', cv2.IMREAD_GRAYSCALE)cv2.imread() が画像を読み込むためのメソッドです。
img.shape には 高さ, 幅, チャンネル情報が格納されているので、 それぞれ height, width, channels に分割代入しています。
3行目ではcv2.IMREAD_GRAYSCALEオプションを指定して同じ画像を読み込んでいますね。このオプションを渡すことで、渡した画像を白黒にしてくれます。
読み込んだ img 変数を確認してみると、1ピクセル毎のRGB情報が多重配列で格納されている様子が確認できます。


Cannyを使ってエッジを検出する
threshold1 = 800
threshold2 = 800
edges = cv2.Canny(gray_img, threshold1, threshold2)cv2.Canny() はエッジ検出をするためのメソッドです。
公式によると、こんな使い分けがあるようです。
Python:cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]]) → edgesC:void cvCanny(const CvArr* image, CvArr* edges, double threshold1,
| Parameters: | image – single-channel 8-bit input image.edges – output edge map; it has the same size and type as image.threshold1 – first threshold for the hysteresis procedure.threshold2 – second threshold for the hysteresis procedure. | 
|---|
こちらのQiita記事の言葉をお借りすると、thresholdが大きいほどエッジが検出されにくく、小さいほどエッジが検出されやすくなります。
違いを確認してみましょう。
threshhold を 10に設定してみた場合:

threshhold を 1000 に設定してみた場合:

今回は500で設定してみました。
膨張処理
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2, 2))
dilates = cv2.dilate(edges, kernel)次に膨張処理をかけます。
平たく言うと、線を太くします。

こうすることで境界線をわかりやすくしているんですね。
輪郭検出
contours, hierarchy = cv2.findContours(dilates, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)さあ、いよいよ輪郭検出のロジックをかけていきます。
輪郭検出には cv2.findContours() を使います。
引数にはそれぞれ、 元画像, 輪郭の抽出モード, 輪郭検出方法 を指定します。
この関数の挙動については、参考サイトでもご紹介したこちらのサイトがわかりやすかったです。
元画像に検出結果を描画 & JSONエクスポート
# エクスポート用イメージ配列を作成しておく
export_img = copy.deepcopy(img)
# JSON用の辞書型配列
export_array = {
    'width': width,
    'height': height,
    'results': []
}
for i in range(len(contours)):
    # 色を指定する
    color = np.random.randint(0, 255, 3).tolist()
    
    if cv2.contourArea(contours[i]) < 3000:
        continue  # 面積が小さいものは除く
    
    # 階層が第1じゃなかったら ... 
    if hierarchy[0][i] != -1:
        # 配列に追加
        export_array['results'].append(create_pos_data(contours[i]))
        # 画像に当該の枠線を追加
        cv2.drawContours(export_img, contours, i, color, 3)
 != -1:
        # 配列に追加
        export_array['results'].append(create_pos_data(contours[i]))
        # 画像に当該の枠線を追加
        cv2.drawContours(export_img, contours, i, color, 3)  ここでのポイントは cv2.contourArea() と cv2.drawContours() です。
cv2.contourArea() では、検出した輪郭の大きさを算出してくれます。ある程度の大きさのものに絞ることで、ノイズを除去しています。
cv2.drawContours() を使うことで、画像に検出した輪郭の枠線を描画していっています。
あとは、export_array 変数に地道に検出結果を append しています。
jsonにエクスポートする場合は、 json.dump() を使えば完了です。
fw = open('assets/output.json','w')
json.dump(export_array,fw,indent=2)
fw.close()駆け足ではありますが、表検出のロジックでした!みなさんも試してみてください♪






















