360度画像AIアノテーション完全実践ガイド~分割・逆変換・重複排除・自動可視化のリアル~

360度カメラ(equirectangular画像)を使ったAIアノテーションは、普通の2D画像解析とは本質的に異なります。
なぜなら、360度画像は投影歪みが強く、また1物体が複数パッチにまたがるため、単純な矩形やラベルだけでは「位置がズレる」「重複する」などの問題が多発します。
ここでは、現場実装レベルで
・パッチ分割
・推論座標の360度画像逆変換
・重複物体のマージ
・HTML可視化
まで、要点とコードをフル網羅で解説します。
1. なぜ360度画像アノテーションは特殊なのか
- equirectangular画像は上下端や端部で歪みが大きく、通常の矩形アノテが通用しない
- YOLOやSAMなど通常物体検出AIをそのまま使うと、認識精度もアノテ精度も大幅に落ちる
- 1物体が複数方向から撮られる=複数パッチで「多重検出」が必ず起こる
結論:
パッチ分割→個別推論→逆変換→マージ(重複排除)という段階を踏むのが“唯一まともな現実解”です。
パッチ分割→個別推論→逆変換→マージ(重複排除)という段階を踏むのが“唯一まともな現実解”です。
2. パッチ分割(equirectangular→perspective)
2-1. パッチ分割設計
- 水平方向(yaw): 0, 45, 90, …, 315度
- 垂直方向(pitch): -60, -30, 0, 30, 60度
- FOV(視野角)は90度が定番
- 隣り合うパッチで必ず重複(オーバーラップ)する設計
2-2. Python実装例
import cv2
import py360convert
import os
import itertools
input_file = "scene_360.jpg"
output_dir = "patches"
os.makedirs(output_dir, exist_ok=True)
fov_deg = 90
out_hw = (640, 640)
yaw_list = list(range(0, 360, 45)) # 8方向
pitch_list = [-60, -30, 0, 30, 60] # 5段
img_360 = cv2.imread(input_file)
for u_deg, v_deg in itertools.product(yaw_list, pitch_list):
persp_img = py360convert.e2p(
img_360,
fov_deg=fov_deg,
u_deg=u_deg,
v_deg=v_deg,
out_hw=out_hw
)
fname = f"patch_yaw{u_deg}_pitch{v_deg}.jpg"
cv2.imwrite(os.path.join(output_dir, fname), persp_img)
3. パッチ画像で物体検出 → アノテーションtxt生成
各パッチ画像に対して、物体検出を行い、YOLO形式(txt)で結果を保存します。この処理は弊社製ツールを利用しました。
例:patch_yaw90_pitch0.jpg → patch_yaw90_pitch0.txt
クラス x_center y_center width height
(例)0 0.512 0.440 0.20 0.12
4. 検出点の「360度画像座標」への逆変換
4-1. なぜ逆変換が必要か
YOLO等は「分割したパッチ内のピクセル座標」で検出点を返しますが、360度画像にマークを描くには元のequirectangular座標に戻す必要があるためです。
4-2. p2e関数(パッチ→360画像座標 逆変換)
import numpy as np
def p2e(x, y, fov_deg, u_deg, v_deg, w_p, h_p, w_e, h_e):
nx = (x / w_p - 0.5) * 2
ny = (y / h_p - 0.5) * 2
fov = np.deg2rad(fov_deg)
z = 1 / np.tan(fov / 2)
vec = np.stack([nx, -ny, -z * np.ones_like(nx)], axis=-1)
vec = vec / np.linalg.norm(vec, axis=-1, keepdims=True)
yaw = np.deg2rad(u_deg)
pitch = np.deg2rad(v_deg)
Ryaw = np.array([
[np.cos(yaw), 0, np.sin(yaw)],
[0, 1, 0],
[-np.sin(yaw), 0, np.cos(yaw)]
])
Rpitch = np.array([
[1, 0, 0],
[0, np.cos(pitch), -np.sin(pitch)],
[0, np.sin(pitch), np.cos(pitch)]
])
R = Ryaw @ Rpitch
vec_rot = vec @ R.T
theta = np.arctan2(vec_rot[..., 0], -vec_rot[..., 2])
phi = np.arcsin(vec_rot[..., 1])
x_e = (theta / (2 * np.pi) + 0.5) * w_e
y_e = (0.5 - phi / np.pi) * h_e
return x_e, y_e
5. 逆変換→検出点を360度画像へマーク
- パッチ名(yaw, pitch)から中心方位を取得
- txtから(x_center, y_center, class)を取得
- YOLOの相対値をピクセル値に変換→p2eでequirectangular座標へ
- 360画像上にマークを描画(cv2.circleなど)
for patch_path in glob.glob('patches/patch_yaw*_pitch*.jpg'):
# yaw, pitch取得省略
txt_path = patch_path.replace('.jpg', '.txt')
# 読み込み省略
x_p = float(x_center) * patch_hw[0]
y_p = float(y_center) * patch_hw[1]
x_e, y_e = p2e(x_p, y_p, fov_deg, yaw, pitch, patch_hw[0], patch_hw[1], w_e, h_e)
all_points.append((x_e, y_e, int(class_id)))
6. 重複物体の自動マージ(DBSCANクラスタリング)
単純な距離しきい値では「ゴミ」や「重複=分割画像で同じ対象物を違った角度から計算してしまい、多重に中心点を生成してしまう」が多く残るため、DBSCANによるクラスタリングが現場での正攻法です。
DBSCANによる重複排除ロジック
from sklearn.cluster import DBSCAN
def merge_points_dbscan(points, eps=30, min_samples=2):
if not points:
return []
arr = np.array([[x, y, class_id] for x, y, class_id in points])
merged = []
for class_id in np.unique(arr[:,2]):
arr_c = arr[arr[:,2]==class_id]
coords = arr_c[:,:2]
if len(coords) == 0:
continue
clustering = DBSCAN(eps=eps, min_samples=min_samples).fit(coords)
labels = clustering.labels_
for lbl in set(labels):
if lbl == -1:
continue
idxs = np.where(labels == lbl)[0]
cx, cy = np.mean(coords[idxs], axis=0)
merged.append((cx, cy, int(class_id)))
return merged
7. 統合スクリプト(フルバージョン)
import cv2
import numpy as np
import glob
import re
import os
from sklearn.cluster import DBSCAN
# p2e関数・merge_points_dbscan関数は上記参照
base_img = 'scene_360.jpg'
patch_dir = 'patches'
fov_deg = 90
patch_hw = (640, 640)
output_img = 'scene_360_annotated.jpg'
eps = 30
min_samples = 2
img_360 = cv2.imread(base_img)
h_e, w_e = img_360.shape[:2]
pattern = re.compile(r'patch_yaw(-?\d+)_pitch(-?\d+).jpg')
all_points = []
for patch_path in glob.glob(os.path.join(patch_dir, 'patch_yaw*_pitch*.jpg')):
m = pattern.search(patch_path)
if not m:
continue
yaw, pitch = int(m.group(1)), int(m.group(2))
txt_path = patch_path.replace('.jpg', '.txt')
if not os.path.exists(txt_path):
continue
with open(txt_path, 'r') as f:
for line in f:
vals = line.strip().split()
if len(vals) < 5:
continue
class_id, x_center, y_center, w, h = vals
x_p = float(x_center) * patch_hw[0]
y_p = float(y_center) * patch_hw[1]
x_e, y_e = p2e(
x_p, y_p,
fov_deg, yaw, pitch,
patch_hw[0], patch_hw[1],
w_e, h_e
)
all_points.append((x_e, y_e, int(class_id)))
print(f"全パッチ合計検出点数: {len(all_points)}")
merged_points = merge_points_dbscan(all_points, eps=eps, min_samples=min_samples)
print(f"クラスタ代表点(=物体数): {len(merged_points)}")
for x_e, y_e, class_id in merged_points:
center = (int(round(x_e)), int(round(y_e)))
color = (0, 0, 255)
cv2.circle(img_360, center, 10, color, 2)
cv2.imwrite(output_img, img_360)
print(f"{output_img} に保存しました。")
このスクリプトを実行することで、360度画像上に「重複排除済み」の物体アノテーションが描画されます。
scikit-learn
(DBSCANのため)もインストールしてください。
8. 応用:色分けや矩形アノテーション、Web可視化
- クラス番号ごとに色分けする(
cv2.circle
のcolor引数を辞書等で切り替え) - 矩形も出したい場合は四隅(xmin,ymin/xmax,ymax)全て逆変換→
cv2.rectangle
等で描画 - 結果JSON化し、Pannellumなど360度ビューアのhotspot情報に流し込む
9. よくある疑問とトラブルシューティング
- Q. 点が多すぎる/消えない!
DBSCAN epsやmin_samplesを上げてみてください。それでも駄目な場合は検出精度そのものやFOV/分割間隔を見直してください。 - Q. なぜこの手順が必要?
360度画像は投影上“歪む”ため、どんなAIも「普通に矩形アノテするだけ」では正しく検出・可視化できないため。 - Q. py360convertにp2eがない!
このページの自作p2e関数で十分実用可能です。
総まとめ:360度画像のAIアノテは、「パッチ分割→推論→逆変換→DBSCANマージ」が実用現場の鉄板パターンのような気がします。
時間軸を考えれば、動画等にも応用も可能なので、カスタマイズ・相談はいつでもどうぞ。
時間軸を考えれば、動画等にも応用も可能なので、カスタマイズ・相談はいつでもどうぞ。