【numpy基礎】マスクデータからBounding Boxをつくる。【備忘録】

2020.10.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

せーのでございます。

前回、UnrealCVというUnreal Engineのプラグインを使ってマスクデータを取る、ということをしました。

【小ネタ】Unreal Engine 4.22.3 + UnrealCVでsegmentation maskデータを取る方法。【備忘録】 | Developers.IO

今回はこのマスクデータからbounding boxを作ります。

つまり、ここから

こういって

こうなったので

こうする、ということですね。

やり方

前回で少し触れましたがマスクデータというのは開くとこんな感じになっています。

[False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
 ...
 ...
 ...
  False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False  True  True  True  True
   True  True  True  True  True  True  True  True  True  True  True  True
   True  True  True  True False False False  True  True  True  True False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
...
...
...

つまり画像のX軸、Y軸にそって1ピクセルずつ、マスクするか(True)しないか(False)が羅列されている二次元配列、ということです。

bounding boxを作るにはそれぞれの頂点の座標が必要です。

このTrueとFalseのデータ(boolean index、といいます)から4つの座標を取るには、なんとなく

  • Trueが出てくる最初の行:A
  • Trueが出てくる最後の行:B
  • Trueが出てくる最初の列:C
  • Trueが出てくる最後の列:D

を取れば、左上の座標は(C, A)、左下は(C、B)みたいな感じでいけるだろうな、という推測が立ちます。

後はこれをどう取るか、ということですが、for文ぐるぐる回すのも芸がないので、numpyをそれぞれ組み合わせて効率的に取得していきます。

やってみる

データを確認

前回のブログの最後のコードの続きになります。マスクデータが変数「mask」に格納されています。 今一度、データを確認してみます。

print(mask)
[False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
 ...
 ...
 ...
  False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
  False False False False False False False False  True  True  True  True
   True  True  True  True  True  True  True  True  True  True  True  True
   True  True  True  True False False False  True  True  True  True False
  False False False False False False False False False False False False
  False False False False False False False False False False False False
...
...
...

numpy.anyで各軸のマスク有無を抽出

まずは各軸(X軸方向、Y軸方向)に対してTrueがあるかどうかをnumpy.anyで探します。numpy.anyに対象データを入れると、指定した軸方向にTrueが一つでもあればTrueを返してくれます。

rows = np.any(mask, axis=1)
cols = np.any(mask, axis=0)

print(rows)
[False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True  True  True  True  True  True  True  True  True  True  True  True
  True False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False
 False False False False False False False False False False False False]

これはY軸方向にmaskを検索した結果になります。つまり一番上の行(Y:0)はFalse、つまりマスクされるデータはない、その次の行もない、、、、と進んでいって、146行目にTrueとなるマスクデータがあった、ということを表しています。

print(rows[146])
True

マスク画像を見てみると

確かにY:146あたりにキャラクターの頭がありますね。

numpy.whereでマスクのある境界のインデックスを取得

次にそれぞれのTrueとなっている座標をnumpy.whereで数値化します。numpy.whereは引数内の条件を満たす要素をのですが、第二引数をつけなければTrueとなるインデックスを返してくれます。

print(np.where(rows))
(array([145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157,
       158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170,
       171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183,
       184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196,
       197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209,
       210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222,
       223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235,
       236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248,
       249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261,
       262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274,
       275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287,
       288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300,
       301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313,
       314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326,
       327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339,
       340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352,
       353, 354, 355, 356, 357, 358, 359, 360], dtype=int64),)

これはつまりY:146~Y:360までの範囲でTrueになっている、ということです。そうするとこの配列の最初と最後の数値を取れば

  • Trueが出てくる最初の行:A
  • Trueが出てくる最後の行:B

の2つが取れそうです。また同様にX軸方向もできます。

y_min, y_max = np.where(rows)[0][[0, -1]]
x_min, x_max = np.where(cols)[0][[0, -1]]

print(y_min)
print(y_max)
print(x_min)
print(x_max)
145
360
248
361

pythonの配列は通常の[0]、[1]、、、以外にマイナス方向に進むと要素が逆から取れます。つまり[-1]は一番最後の要素、[-2]は最後から二番目の要素、、、という感じです。これも忘れがちです。
ちなみに、頭でnumpy.whereをしてしまって、その結果に対して一気に最小値と最大値を取得する、みたいな方法もできるのですが

mask_indexes = np.where(mask)
y_min = np.min(mask_indexes[0])
y_max = np.max(mask_indexes[0])
x_min = np.min(mask_indexes[1])
x_max = np.max(mask_indexes[1])

対象となる配列が二次元の状態のままnumpy.whereをかけるとコストが大きいので、numpy.anyを使ってX軸とY軸に分け、それぞれwhereをしてあげると、処理時間の短縮につながります。

matplotlibのpatchで四角を描写して画像に追加する

さて、最後はこれらの座標を使って元画像にbounding boxを描画してみます。
今回はplt(matplot library)を使って描画します。

pltではまず画像からsubplotを抽出し、それに対して四角(RectAngle)を描画します。

import matplotlib.patches as patches

rect = patches.Rectangle((x_min, y_min), x_max - x_min, y_max - y_min,
                                 linewidth=2,
                                 edgecolor=(0, 1, 0, 1),
                                 facecolor='none')

plt.imshow(im)
ax = plt.gca()
ax.add_patch(rect)

plt.show()

これで完成です。

まとめ

numpyを使ってマスクデータからbounding boxを作ってみました。関数の組み合わせパズルのようなものなのですが、numpyを使い慣れてないと絶対忘れそうなので備忘録として書いておきました。
みなさんもふと忘れたときはこのエントリに寄っていってください。