物体検出の前処理だったりデータ水増しを使用とすると、どうしても回転、反転、リサイズなどの処理を行う必要が出てくる。 画像だけであればOpenCV使ったりNumpy使えば、サクっとできるがバウンディングボックスだと若干めんどくさい。(特に回転) ライブラリを作る中で実装したので、それぞれの簡単な実装を紹介する。
BoundingBoxの表現として以下のようなBoxクラスを使う。x、yは画像左上の座標、w, hはBoundingBoxの幅と高さを表している。 画像の読み込みにはopencvを使い(height, width, channel)の形をしたnumpy配列として読み込む。 デフォルトだとEXIFでオリエンテーション情報を持っている場で合にはそれが適用されてしまうので、今回はcv2.IMREAD_IGNORE_ORIENTATIONで画像の回転に関する情報は無視する。 あと便利なので描画用の関数showを用意する。cv2にはデフォルトで透過した矩形を描く機能が無いので自前で用意する必要がある。
import cv2
import numpy as np
class Box:
def \_\_init\_\_(self, x, y, w, h):
self.x = x
self.y = y
self.w = w
self.h = h
def show(image, boxes, color, alpha):
image = image.copy()
for b in boxes:
overlay = image.copy()
p1 = (int(b.x), int(b.y))
p2 = (int(b.x + b.w), int(b.y + b.h))
cv2.rectangle(overlay, p1, p2, color, -1)
image = cv2.addWeighted(image, alpha, overlay, 1-alpha, 0)
cv2.imshow('img', image)
cv2.waitKey()
image = cv2.imread(filepath, cv2.IMREAD_IGNORE_ORIENTATION | cv2.IMREAD_COLOR)
今回の3つの操作の中だと一番めんどくさい気がする。画像の回転をAffine変換でやれば、そのときに変換行列を計算しているはずなのでBoundingBoxの座標変換にもそれを流用してしまうのが良い。 注意点として4隅の座標を変換すると、変換後のBoundingBoxが辺がx, y座標と平行にならない。そのため変換後の座標から画像の辺と平行になるよう再度x, y座標を計算しなおす必要がある。
def rotate(image, boxes, angle):
img = image.copy()
# 初期状態での高さと幅
h, w = img.shape[:2]
# sinとcosを駆使して回転後の画像サイズを計算
r = np.radians(angle)
s = np.abs(np.sin(r))
c = np.abs(np.cos(r))
nw = int(c*w + s*h)
nh = int(s*w + c*h)
# 回転行列を計算+サイズの変化に伴う補正を加える
center = (w/2, h/2)
rot_m = cv2.getRotationMatrix2D(center, angle, 1.0)
rot_m[0][2] = rot_m[0][2] + (nw - w) // 2
rot_m[1][2] = rot_m[1][2] + (nh - h) // 2
# アフィン変換を実行
img = cv2.warpAffine(img, rot_m, (nw, nh), flags=cv2.INTER_CUBIC)
new_boxes = []
for box in boxes:
# Boxの情報を座標に
coord_arr = np.array([
[box.x, box.y, 1], # Left-Top
[box.x, box.y+box.h, 1], # Left-Bottom
[box.x+box.w, box.y, 1], # Right-Top
[box.x+box.w, box.y+box.h, 1], # Right-Botto
])
# 回転行列と掛け合わせて座標変換
new_coord = rot_m.dot(coord_arr.T)
x_ls = new_coord[0] # x座標だけを抽出
y_ls = new_coord[1] # y座標だけを抽出
# 最小値最大値を用いて、新しい四隅の位置を計算
x = int(min(x_ls))
y = int(min(y_ls))
w = int(max(x_ls) - x)
h = int(max(y_ls) - y)
new_box = Box(x, y, w, h)
new_boxes.append(new_box)
return img, new_boxes
画像自体の反転はnumpyのflipudとfliplrで簡単にできる。BoundingBoxの計算も、xの値を左端までの距離から右端までの距離に、yの値を上端までの距離から下端までの距離に計算しなおすだけなので簡単。
def flip(image, boxes, flip_x=True, flip_y=False):
img = image.copy()
ih, iw = img.shape[:2]
# 引数に応じてどの軸で反転させるかを判断
# デフォルトではx, y軸双方で反転させる
if flip_x:
img = np.fliplr(img)
if flip_y:
img = np.flipud(img)
# BoundingBoxの座標を計算
new_boxes = []
for box in boxes:
x = box.x
y = box.y
if flip_x:
# 右端からの距離
x = iw - box.x - box.w
if flip_y:
# 下端からの距離
y = ih - box.y - box.h
new_box = Box(x, y, box.w, box.h)
new_boxes.append(new_box)
return img, new_boxes
def resize(image, boxes, width, height):
# 現在の高さと幅を取得しておく
c_height, c_width = image.shape[:2]
img = cv2.resize(image, (width, height))
# 圧縮する比率(rate)を計算
r_width = width / c_width
r_height = width / c_height
# 比率を使ってBoundingBoxの座標を修正
new_boxes = []
for box in boxes:
x = int(box.x * r_width)
y = int(box.y * r_height)
w = int(box.w * r_width)
h = int(box.h * r_height)
new_box = Box(x, y, w, h)
new_boxes.append(new_box)
return img, new_boxes
上の操作をすべて実行するとこんな感じで変換されていく。
image = cv2.imread(filepath, cv2.IMREAD_IGNORE_ORIENTATION | cv2.IMREAD_COLOR)
boxes = [Box(83, 108, 267, 124)]
show(image, boxes, (255, 0, 0), 0.5)
# Rotate
image, boxes = rotate(image, boxes, 90)
show(image, boxes, (255, 0, 0), 0.5)
# Tilt
image, boxes = rotate(image, boxes, 3)
show(image, boxes, (255, 0, 0), 0.5)
# Flip
image, boxes = flip(image, boxes)
show(image, boxes, (255, 0, 0), 0.5)
# Resize
image, boxes = resize(image, boxes, 500, 500)
show(image, boxes, (255, 0, 0), 0.5)
変換前後の比較
今回の3操作はannt-python にも実装されている。