加具留矢流余

かぐるやるよ

BoundingBoxごと画像を回転・反転・リサイズ

物体検出の前処理だったりデータ水増しを使用とすると、どうしても回転、反転、リサイズなどの処理を行う必要が出てくる。 画像だけであれば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)

f:id:theflyingcat28:20200626211033p:plainf:id:theflyingcat28:20200626211037p:plainf:id:theflyingcat28:20200626211041p:plainf:id:theflyingcat28:20200626211044p:plainf:id:theflyingcat28:20200626211049p:plain
変換の推移(左から右に変換されていっている)

今回の3操作はannt-python にも実装されている。

mikebird28.hatenablog.jp mikebird28.hatenablog.jp