目的
OpenCVで画像内のオブジェクトの輪郭を取得して、そこを切り取りたい。調べるといろんな輪郭でも、外接矩形・四角で切り取っているパターンしか見当たらない。なんとかオブジェクトだけをそのまま切り取りたい。
考え方
OpenCV + pythonでの画像の切り取りは img[ 10:100, 20:100 ]というように、元画像のピクセル範囲を指定して切り抜く方法しかなさそう。
なので、最初に外接矩形で切り抜き、さらに、外接矩形サイズでキャンバスを作っておいて、、
取得した輪郭情報にをデータ整理して、行毎に最大の値と最小の値を取得し配列にしておく。
先に作っておいた透明キャンバスに1行毎に、最小と最大の間にはいったら、切り抜き元の画像から取得してプロット...,
というのを行ごとに繰り返せばいけそう。
いけるべ!
コード
import cv2
import numpy as np
from pprint import pprint
from copy import deepcopy
import sys
import os
#入力画像をグレースケール変換>2値化、二値化後の画像を返す
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):
# 配列構造が多重になっているので、1回シンプルな配列にする (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/に切り出した画像ファイルを出力
サンプル
入力画像
切り出された画像
外接矩形の背景が透過されているというのは分かりづらいかもしれないですね...
※下の画像については内部は透過されてません。うまいやり方ないかなぁ..