【画像処理】OpenCVを使って画像周りの余白削除(トリミング)を自動化してみた！（１）

#概要
画像を使おうと思ったら，周りの余白が邪魔で，削除してから使おう！なんて思ったことはありませんか？

私は，ソフトウエアで回路図を作成して，レポートに貼り付けようとしたら，余白が無駄にあって，
そのままでもいいけど，余白を削除してから貼り付けして・・・とやっていました．

が，いちいち余白を削除して・・・ってやっていると，**非常にめんどくさい！**

ということで，**画像の余白削除**をOpenCVを使って**自動化**してみました．


#動作環境
- Windows10(64bit)
- Python 3.7.2
- OpenCV 4.1.0 

#やりたいこと
上の画像を下の画像の緑の枠で切り取るのが目標です．少しわかりにくいかもしれませんが，下の画像では，上の画像の周りの余白を除いた部分が緑色の枠で囲われ，枠ぴったりに物体が収まっています．
ちなみにこれは非同期式6進ダウンカウンタです。

![6thDownCounter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433790/f4b0fdd1-3210-81d4-62ab-b7f1b5b28f33.png)

↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓

![6thDownCounter.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433790/9e0b8518-f982-322d-b250-c31679289001.png)

#どのようにして余白削除を行うのか
Pythonで画像処理を行う際には，PillowやNumpy，OpenCVなどのライブラリが使えます．
余白削除に関しても，Pillowを使ってあげることでも実装できます．

Pillowによる実装は以下の記事参照．
[【画像処理】Pillowを使って画像周りの余白削除(トリミング)を自動化してみた！（２）](https://qiita.com/trami/items/b5b7b317cb83100406ec)

しかし今回は，マシンラーニングやディープラーニングでもよく使われるOpenCVを使って余白削除を行っていこうと思います．

ざっくり，実装の流れを説明すると，

1.画像を読み込む
2.色空間を二値化する
3.輪郭を抽出する
4.輪郭の中から座標が最小になるものと最大になるものをx,yそれぞれ取得し，それらを通る長方形で画像を切り出す．

1~3は物体認識や文字認識でもよく使う手法かなと思います．

#余白を削除する関数
まずは，余白を削除する関数を定義します．これがこのプログラムの核となる部分です．

```py
# 余白を削除する関数
def crop(image): #引数は画像の相対パス
    # 画像の読み込み
    img = cv2.imread(image)

    # 周りの部分は強制的にトリミング
    h, w = img.shape[:2]
    h1, h2 = int(h * 0.05), int(h * 0.95)
    w1, w2 = int(w * 0.05), int(w * 0.95)
    img = img[h1: h2, w1: w2]
    # cv2.imshow('img', img)

    # Grayscale に変換
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    # cv2.imshow('gray', gray)

    # 色空間を二値化
    img2 = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)[1]
    # cv2.imshow('img2', img2)

    # 輪郭を抽出
    contours = cv2.findContours(img2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]

    # 輪郭の座標をリストに代入していく
    x1 = [] #x座標の最小値
    y1 = [] #y座標の最小値
    x2 = [] #x座標の最大値
    y2 = [] #y座標の最大値
    for i in range(1, len(contours)):# i = 1 は画像全体の外枠になるのでカウントに入れない
        ret = cv2.boundingRect(contours[i])
        x1.append(ret[0])
        y1.append(ret[1])
        x2.append(ret[0] + ret[2])
        y2.append(ret[1] + ret[3])

    # 輪郭の一番外枠を切り抜き
    x1_min = min(x1)
    y1_min = min(y1)
    x2_max = max(x2)
    y2_max = max(y2)
    cv2.rectangle(img, (x1_min, y1_min), (x2_max, y2_max), (0, 255, 0), 3)

    crop_img = img2[y1_min:y2_max, x1_min:x2_max]
    # cv2.imshow('crop_img', crop_img)

    return img, crop_img
```

#解説
では解説をしていきます．

- 画像の読み込み

```py
    img = cv2.imread(image)
```

- 画像の強制切り出し

以下は，本来は必要ないのですが，今回の画像は一番外側に座標のようなものが入り込んでしまっているため，輪郭抽出でそこを抽出しないようにするため，強制的にそこの部分を削除しています．
shape[]は，行数，列数，チャネル数を返すので，最初の２つのみ取得します．
４行目で，スライスをして画像の周りを強制的に削除しています．

```py
    h, w = img.shape[:2]
    h1, h2 = int(h * 0.05), int(h * 0.95)
    w1, w2 = int(w * 0.05), int(w * 0.95)
    img = img[h1: h2, w1: w2]
```



- グレイスケール変換

```py
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
```

- 色空間の二値化

色空間を二値化をします．thresholdは閾値はしきいちと読み，変わり目のような意味合いの言葉です．
threshold()関数は第１引数にグレイスケールの画像を，第２引数に閾値を，
第３引数に閾値以上の値をもつ値に対して割り当てる値を指定し，第４引数にはどのようなしきい値処理を行うのかを指定します．
２つ目の返り値がしきい値処理された二値画像なので，[1]で２つ目の返り値のみ取得します．

```py
    img2 = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)[1]
```

- 輪郭抽出

輪郭の抽出を行います．contourは輪郭という意味です．
findContours()関数は第1引数に入力画像を，第2引数に抽出モードを，第3引数に近似手法を指定します．
抽出モードは，RETR_LIST, RETR_EXTERNAL, RETR_CCOMP, RETR_TREEから選びます．
返り値なのですが，個々で注意が必要です.
**findContours()関数は，OpenCVのバージョンによって返り値が異なります．**
**OpenCV3までは返り値が3つでしたが，OpenCV4では返り値が2つに減っています．**
OpenCV4では，一つ目の返り値に抽出された輪郭のリストを，2つ目の返り値に階層構造のリストを返します．
なので，[0]とすることで，輪郭のリストを受け取ります．

```py
    contours = cv2.findContours(img2, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)[0]
```

- 輪郭の座標をリストに代入

輪郭の座標をそれぞれのリストに追加していきます．ここで，i=1は画像全体の外枠になるため除外します．
boundingRectは輪郭の長方形の，左上のx,y座標を第1，第2の返り値に，幅と高さを第3，第4引数に返すので，
それをretリストに代入します．retはreturnの略です．
物体認識などでは，ここで小さすぎる輪郭や大きすぎる輪郭を除去することもあります．

```py
    x1 = [] #x座標の最小値
    y1 = [] #y座標の最小値
    x2 = [] #x座標の最大値
    y2 = [] #y座標の最大値
    for i in range(1, len(contours)):
        ret = cv2.boundingRect(contours[i])
        x1.append(ret[0])
        y1.append(ret[1])
        x2.append(ret[0] + ret[2])
        y2.append(ret[1] + ret[3])
```

- 一番外側の長方形で切り取る
先ほど作成したいリストからそれぞれの最小値，最大値を取得します．
それらを2頂点とする長方形で画像を切り抜けば，余白を除く全ての部分がうまく切り取ることができます．
rectangle()関数は，第1引数に画像を，第2，第3引数に切り取る長方形の左上と右下の座標を指定し，
第4引数に長方形の線の色をRGBの8ビットで指定し，第5引数で，線の太さを指定します．
6行目で，画像を求めた長方形サイズでスライスして切り抜いています．

```py
    x1_min = min(x1)
    y1_min = min(y1)
    x2_max = max(x2)
    y2_max = max(y2)
    cv2.rectangle(img, (x1_min, y1_min), (x2_max, y2_max), (0, 255, 0), 3)

    crop_img = img2[y1_min:y2_max, x1_min:x2_max]
```

あとはこの関数の返り値に対して，画像を表示したり保存したりすれば，所望の画像を保存することができます．

#複数の画像に対して処理をまとめて行う
では複数の画像に対して，一気に余白削除をやってみようと思います．
余白を削除したい画像がimages_for_trimディレクトリにあり，拡張子は.pngであるとします．
そして保存先のディレクトリはtrimmed_imagesとします．

```py
INPUTDIR = 'images_fot_trim'
OUTPUTDIR = 'trimmed_images'
EXT = 'png'

# 編集後の画像の保存ディレクトリの作成
if not os.path.isdir(OUTPUTDIR):
    os.mkdir(OUTPUTDIR)

# INPUTDIR内の全ての画像に対してループ
for image in glob.glob(INPUTDIR + '/*.' + EXT):
    img, crop_img = crop(image)
    
    # 相対パスの部分を削除
    image = os.path.basename(image)

    # 切り取る長方形とともに元の画像を表示
    cv2.imshow(image, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    # 切り取った画像を保存
    cv2.imwrite(OUTPUTDIR + '/' + image, crop_img) 
```

#まとめ
いかがだったでしょうか？OpenCVを使って，画像を二値化・輪郭抽出から不要な周りの余白を削除してみました．今回のプログラムには物体認識などでも大事なものが多く含まれていた気がします．これを発展させていくことで，物体認識もできそうですね．
プログラムに関する建設的なフィードバックもお待ちしています！