画像を矩形ではなくて輪郭に沿って切り抜きたい[python OpenCV]


## 目的
OpenCVで画像内のオブジェクトの輪郭を取得して、そこを切り取りたい。調べるといろんな輪郭でも、外接矩形・四角で切り取っているパターンしか見当たらない。なんとかオブジェクトだけをそのまま切り取りたい。

## 考え方
OpenCV + pythonでの画像の切り取りは img[ 10:100, 20:100 ]というように、元画像のピクセル範囲を指定して切り抜く方法しかなさそう。

なので、最初に外接矩形で切り抜き、さらに、外接矩形サイズでキャンバスを作っておいて、、
取得した輪郭情報にをデータ整理して、行毎に最大の値と最小の値を取得し配列にしておく。
先に作っておいた透明キャンバスに１行毎に、最小と最大の間にはいったら、切り抜き元の画像から取得してプロット...,
というのを行ごとに繰り返せばいけそう。

いけるべ！

## コード

```python
import cv2
import numpy as np
from pprint import pprint
from copy import deepcopy
import sys
import os


#入力画像をグレースケール変換＞２値化、二値化後の画像を返す
def getBinary(im):
    im_gray = cv2.cvtColor(im , cv2.COLOR_BGR2GRAY) 
    return cv2.threshold(im_gray, 0, 255, cv2.THRESH_BINARY_INV+cv2.THRESH_OTSU)[1] 

# 外接矩形の取得
def getBoundingRectAngle(contour):
    # 配列構造が多重になっているので、１回シンプルな配列にする (point [x y])
    contour = list(map(lambda point: point[0], contour))
    x_list = [ point[0] for point in contour ] # width
    y_list = [ point[1] for point in contour ] # height
    return  [min(x_list), max(x_list), min(y_list), max(y_list)]

# 切り取り    
def getPartImageByRect(img, rect):
    #[y軸、x軸]
    return img[ rect[2]:rect[3], rect[0]:rect[1],]

def getCropData(contour, main_axis=0):
    target_axis =  1  if main_axis == 0 else 0

    #axis = 0 = x 
    #axis = 1 = y 
    contour = list(map(lambda i: i[0], contour))
    

    axis_point_list = set([ point[main_axis] for point in contour ]) # unique
    arr = []

    for val in axis_point_list:
        #輪郭配列から yが特定値のxの配列取得
        target_axis_point_list = list(filter(lambda i: i[main_axis] == val, contour))
        tmp_list = [ i[target_axis] for i in target_axis_point_list ] # 
        arr.append( [ val, min(tmp_list), max(tmp_list)] )
    return arr

##y軸に沿ってx座標の範囲を取得していく
def doCropY(input_im,  points, rect) :
    height = rect[3]-rect[2]
    width = rect[1]-rect[0] 
    left = rect[0]    
    top = rect[2]
    output_im = np.zeros((height, width, 4), np.uint8)

    for point in points :
        for x in range(0, width) :
            #input画像 座標
            in_y = point[0]
            in_x = x + left
            in_x_min = point[1]
            in_x_max = point[2]

            # output画像座標
            out_y = point[0] - top
            out_x = x
            out_x_min = point[1] - left
            out_x_max = point[2] - left

            #x軸の最大最小の範囲だったら元画像から新画像にコピーする
            if( out_x_min < x  and x < out_x_max):
                output_im[out_y : out_y + 1, out_x : out_x + 1, ] = input_im[in_y : in_y + 1, in_x : in_x + 1, ]
    return output_im


## 一度抽出済みの画像に対して、
## x軸に沿ってy座標の範囲を取得していく
def doCropX(im, points, rect):
    height = rect[3]-rect[2]
    width = rect[1]-rect[0] 
    left = rect[0]    
    top = rect[2]

    for point in points :
        for y in range(0, height) :
            #input画像 座標
            y = y
            x = point[0] - left
            y_min = point[1] - top
            y_max = point[2] - top            
            #y軸の最大最小の範囲だったら元画像から新画像にコピーする
            if(  y < y_min  or y_max < y):
                im[y:y+1, x:x+1,] = [0,0,0,0] #透過
    return im

##########################
#  main
##########################
outdir = './out/'
tempdir = './temporary/'

#引数から画像パス取得
args = sys.argv
if(len(args) <= 1):
    print("need arg 1. input file path.")
    sys.exit()
src_file = args[1]
root, extention = os.path.splitext(src_file)

# 画像読み込み
im_src = cv2.imread(src_file, -1) #アルファチャンネル込
# cv2.imwrite(tempdir + "original" + extention, im_src)

#binaryにして輪郭を取得
im_bin = getBinary(im_src)

#contours = cv2.findContours(im_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)[0]
contours = cv2.findContours(im_bin, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)[0] # パスをすべて取得

for i, cnt in  enumerate(contours):
    #外接矩形の取得
    rect = getBoundingRectAngle(cnt)
    #y座標にxの左端、右端の範囲で切り取る
    crop_data  = getCropData(cnt, 1) #x軸基準
    im_out = doCropY(im_src, crop_data, rect)
    
    # # #x座標毎にyの上から下の範囲外を透過させる
    crop_data  = getCropData(cnt, 0) #x軸基準
    im_out = doCropX(im_out, crop_data, rect)

    cv2.imwrite(outdir + "out" + str(i) + extention, im_out)

```

## 動かす
`python3 crop.py ファイル名`

temporary/に中間ファイル出力
out/に切り出した画像ファイルを出力

## サンプル


#### 入力画像
<img style="{borderWidth:1, }" width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226284/a759c08f-658d-d1f0-f2bf-867830363fe6.png">


#### 切り出された画像
外接矩形の背景が透過されているというのは分かりづらいかもしれないですね...
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226284/085676d4-b699-7193-ccce-f54359e744f1.png">
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226284/c627521d-2c6a-61c4-9600-7c5210cf7e55.png">
<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226284/9ec5b654-6355-e5ba-dbb4-dd144841e8a6.png">

※下の画像については内部は透過されてません。うまいやり方ないかなぁ..

<img width="100" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226284/515601b3-dcfc-75f5-f511-7b78a5bf2c6c.png">