PythonとOpenCVを使って拡張現実(AR)アプリケーションを構築する方法を解説します。マーカー認識から3Dオブジェクトの重ね合わせまで、実践的なコード例とともに紹介します。
ARアプリケーション開発ツールの比較
| ツール/ライブラリ | 用途 | 特徴 | 難易度 |
|---|---|---|---|
| OpenCV + ArUco | マーカーベースAR | 軽量・高速 | 中 |
| OpenCV + MediaPipe | ハンドトラッキング | リアルタイム処理 | 中 |
| ARCore (Android) | モバイルAR | 平面検出・環境理解 | 高 |
| ARKit (iOS) | モバイルAR | 高精度トラッキング | 高 |
| Unity + Vuforia | ゲームAR | 3Dレンダリング | 高 |
ARアプリケーションの構成要素
| コンポーネント | 説明 | OpenCVでの実現方法 |
|---|---|---|
| カメラ入力 | リアルタイム映像取得 | cv2.VideoCapture() |
| マーカー検出 | 位置・姿勢の推定 | cv2.aruco モジュール |
| 座標変換 | 3D空間での位置計算 | cv2.solvePnP() |
| オブジェクト描画 | 仮想物体の表示 | cv2.drawContours() など |
| 合成 | カメラ映像との重ね合わせ | cv2.addWeighted() |
インストール
# OpenCVとcontribモジュール(ArUco含む)をインストール
pip install opencv-python opencv-contrib-python numpy
Step 1: カメラ映像の取得
import cv2
import numpy as np
def initialize_camera(camera_id: int = 0, width: int = 1280, height: int = 720):
"""カメラを初期化して返す"""
cap = cv2.VideoCapture(camera_id)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
if not cap.isOpened():
raise RuntimeError("カメラを開けませんでした")
return cap
def main():
cap = initialize_camera()
while True:
ret, frame = cap.read()
if not ret:
print("フレームを取得できませんでした")
break
# FPSを表示
cv2.putText(
frame,
f"Press 'q' to quit",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2
)
cv2.imshow('AR Application', frame)
# 'q'キーで終了
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
Step 2: ArUcoマーカーの生成
ArUcoマーカーは、ARアプリケーションで位置や姿勢を検出するために使用される二次元パターンです。
import cv2
import numpy as np
import os
def generate_aruco_markers(
dictionary_type: int = cv2.aruco.DICT_4X4_50,
marker_count: int = 5,
marker_size: int = 200,
output_dir: str = "markers"
):
"""ArUcoマーカーを生成して保存"""
# 出力ディレクトリを作成
os.makedirs(output_dir, exist_ok=True)
# ArUco辞書を取得
aruco_dict = cv2.aruco.getPredefinedDictionary(dictionary_type)
for marker_id in range(marker_count):
# マーカーを生成
marker_image = cv2.aruco.generateImageMarker(
aruco_dict,
marker_id,
marker_size
)
# 余白を追加(印刷時の認識向上)
border_size = 20
marker_with_border = cv2.copyMakeBorder(
marker_image,
border_size, border_size, border_size, border_size,
cv2.BORDER_CONSTANT,
value=255
)
# 保存
output_path = os.path.join(output_dir, f"marker_{marker_id}.png")
cv2.imwrite(output_path, marker_with_border)
print(f"マーカー {marker_id} を保存しました: {output_path}")
# マーカーを生成
generate_aruco_markers()
利用可能なArUco辞書
| 辞書タイプ | マーカーサイズ | マーカー数 | 用途 |
|---|---|---|---|
DICT_4X4_50 | 4x4 | 50 | 小規模プロジェクト |
DICT_5X5_100 | 5x5 | 100 | 中規模プロジェクト |
DICT_6X6_250 | 6x6 | 250 | 大規模プロジェクト |
DICT_7X7_1000 | 7x7 | 1000 | 非常に大規模 |
DICT_ARUCO_ORIGINAL | 5x5 | 1024 | オリジナル互換 |
Step 3: マーカーの検出
import cv2
import numpy as np
def detect_aruco_markers(frame, aruco_dict, detector_params):
"""フレームからArUcoマーカーを検出"""
# グレースケールに変換
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
# ArUcoディテクターを作成
detector = cv2.aruco.ArucoDetector(aruco_dict, detector_params)
# マーカーを検出
corners, ids, rejected = detector.detectMarkers(gray)
return corners, ids, rejected
def draw_detected_markers(frame, corners, ids):
"""検出されたマーカーを描画"""
if ids is not None:
# マーカーの輪郭を描画
cv2.aruco.drawDetectedMarkers(frame, corners, ids)
# 各マーカーの中心にIDを表示
for i, corner in enumerate(corners):
center = corner[0].mean(axis=0).astype(int)
cv2.putText(
frame,
f"ID: {ids[i][0]}",
tuple(center),
cv2.FONT_HERSHEY_SIMPLEX,
0.7,
(0, 0, 255),
2
)
return frame
def main():
cap = cv2.VideoCapture(0)
# ArUco設定
aruco_dict = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_4X4_50)
detector_params = cv2.aruco.DetectorParameters()
while True:
ret, frame = cap.read()
if not ret:
break
# マーカーを検出
corners, ids, rejected = detect_aruco_markers(frame, aruco_dict, detector_params)
# 検出結果を描画
frame = draw_detected_markers(frame, corners, ids)
# 検出数を表示
marker_count = len(ids) if ids is not None else 0
cv2.putText(
frame,
f"Detected markers: {marker_count}",
(10, 30),
cv2.FONT_HERSHEY_SIMPLEX,
1,
(0, 255, 0),
2
)
cv2.imshow('ArUco Detection', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == '__main__':
main()
Step 4: カメラキャリブレーション
正確な3D位置推定にはカメラキャリブレーションが必要です。
import cv2
import numpy as np
import glob
def calibrate_camera(
images_path: str,
board_size: tuple = (9, 6),
square_size: float = 0.025 # メートル単位
):
"""チェスボードパターンを使用してカメラをキャリブレーション"""
# 3D点の準備
objp = np.zeros((board_size[0] * board_size[1], 3), np.float32)
objp[:, :2] = np.mgrid[0:board_size[0], 0:board_size[1]].T.reshape(-1, 2)
objp *= square_size
objpoints = [] # 3D点
imgpoints = [] # 2D点
images = glob.glob(images_path)
for fname in images:
img = cv2.imread(fname)
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# チェスボードのコーナーを検出
ret, corners = cv2.findChessboardCorners(gray, board_size, None)
if ret:
objpoints.append(objp)
# サブピクセル精度でコーナーを改善
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
corners2 = cv2.cornerSubPix(gray, corners, (11, 11), (-1, -1), criteria)
imgpoints.append(corners2)
# キャリブレーション実行
ret, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
objpoints, imgpoints, gray.shape[::-1], None, None
)
return camera_matrix, dist_coeffs
# デフォルトのカメラパラメータ(キャリブレーションなしの場合)
def get_default_camera_params(frame_width: int, frame_height: int):
"""デフォルトのカメラパラメータを返す"""
focal_length = frame_width
center = (frame_width / 2, frame_height / 2)
camera_matrix = np.array([
[focal_length, 0, center[0]],
[0, focal_length, center[1]],
[0, 0, 1]
], dtype=np.float64)
dist_coeffs = np.zeros((4, 1))
return camera_matrix, dist_coeffs
Step 5: 3Dオブジェクトの描画
import cv2
import numpy as np
def draw_cube(frame, corners, ids, camera_matrix, dist_coeffs, marker_length=0.05):
"""マーカー上に3Dキューブを描画"""
if ids is None:
return frame
# マーカーの姿勢を推定
rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
corners, marker_length, camera_matrix, dist_coeffs
)
# キューブの頂点を定義(マーカーサイズに基づく)
half_size = marker_length / 2
cube_points = np.float32([
[-half_size, -half_size, 0],
[half_size, -half_size, 0],
[half_size, half_size, 0],
[-half_size, half_size, 0],
[-half_size, -half_size, -marker_length],
[half_size, -half_size, -marker_length],
[half_size, half_size, -marker_length],
[-half_size, half_size, -marker_length]
])
for i in range(len(ids)):
# 3D点を2D画像座標に投影
imgpts, _ = cv2.projectPoints(
cube_points, rvecs[i], tvecs[i], camera_matrix, dist_coeffs
)
imgpts = np.int32(imgpts).reshape(-1, 2)
# キューブの面を描画
# 底面(緑)
frame = cv2.drawContours(frame, [imgpts[:4]], -1, (0, 255, 0), 2)
# 上面(青)
frame = cv2.drawContours(frame, [imgpts[4:]], -1, (255, 0, 0), 2)
# 側面の辺(赤)
for j in range(4):
frame = cv2.line(frame, tuple(imgpts[j]), tuple(imgpts[j + 4]), (0, 0, 255), 2)
return frame
def draw_axis(frame, corners, ids, camera_matrix, dist_coeffs, marker_length=0.05):
"""マーカー上に3D座標軸を描画"""
if ids is None:
return frame
rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
corners, marker_length, camera_matrix, dist_coeffs
)
for i in range(len(ids)):
cv2.drawFrameAxes(
frame, camera_matrix, dist_coeffs,
rvecs[i], tvecs[i],
marker_length * 0.5
)
return frame
完全なARアプリケーション
import cv2
import numpy as np
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
class ARConfig:
"""AR設定クラス"""
camera_id: int = 0
frame_width: int = 1280
frame_height: int = 720
marker_length: float = 0.05 # メートル
dict_type: int = cv2.aruco.DICT_4X4_50
class ARApplication:
def __init__(self, config: ARConfig):
self.config = config
self.cap = None
self.aruco_dict = None
self.detector_params = None
self.camera_matrix = None
self.dist_coeffs = None
def initialize(self):
"""アプリケーションを初期化"""
# カメラを初期化
self.cap = cv2.VideoCapture(self.config.camera_id)
self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, self.config.frame_width)
self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, self.config.frame_height)
if not self.cap.isOpened():
raise RuntimeError("カメラを開けませんでした")
# ArUcoを初期化
self.aruco_dict = cv2.aruco.getPredefinedDictionary(self.config.dict_type)
self.detector_params = cv2.aruco.DetectorParameters()
# カメラパラメータを設定
self.camera_matrix, self.dist_coeffs = self._get_camera_params()
def _get_camera_params(self) -> Tuple[np.ndarray, np.ndarray]:
"""カメラパラメータを取得"""
focal_length = self.config.frame_width
center = (self.config.frame_width / 2, self.config.frame_height / 2)
camera_matrix = np.array([
[focal_length, 0, center[0]],
[0, focal_length, center[1]],
[0, 0, 1]
], dtype=np.float64)
dist_coeffs = np.zeros((4, 1))
return camera_matrix, dist_coeffs
def detect_markers(self, frame: np.ndarray):
"""マーカーを検出"""
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
detector = cv2.aruco.ArucoDetector(self.aruco_dict, self.detector_params)
return detector.detectMarkers(gray)
def draw_ar_content(self, frame: np.ndarray, corners, ids) -> np.ndarray:
"""ARコンテンツを描画"""
if ids is None:
return frame
# マーカーの姿勢を推定
rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
corners, self.config.marker_length,
self.camera_matrix, self.dist_coeffs
)
for i in range(len(ids)):
marker_id = ids[i][0]
# マーカーIDに応じて異なるオブジェクトを描画
if marker_id == 0:
self._draw_cube(frame, rvecs[i], tvecs[i], (0, 255, 0))
elif marker_id == 1:
self._draw_pyramid(frame, rvecs[i], tvecs[i], (255, 0, 0))
elif marker_id == 2:
cv2.drawFrameAxes(
frame, self.camera_matrix, self.dist_coeffs,
rvecs[i], tvecs[i], self.config.marker_length
)
else:
# デフォルト: 座標軸
cv2.drawFrameAxes(
frame, self.camera_matrix, self.dist_coeffs,
rvecs[i], tvecs[i], self.config.marker_length * 0.5
)
return frame
def _draw_cube(self, frame, rvec, tvec, color):
"""キューブを描画"""
size = self.config.marker_length
half = size / 2
points = np.float32([
[-half, -half, 0], [half, -half, 0],
[half, half, 0], [-half, half, 0],
[-half, -half, -size], [half, -half, -size],
[half, half, -size], [-half, half, -size]
])
imgpts, _ = cv2.projectPoints(
points, rvec, tvec, self.camera_matrix, self.dist_coeffs
)
imgpts = np.int32(imgpts).reshape(-1, 2)
# 面を描画
cv2.drawContours(frame, [imgpts[:4]], -1, color, 2)
cv2.drawContours(frame, [imgpts[4:]], -1, color, 2)
for j in range(4):
cv2.line(frame, tuple(imgpts[j]), tuple(imgpts[j + 4]), color, 2)
def _draw_pyramid(self, frame, rvec, tvec, color):
"""ピラミッドを描画"""
size = self.config.marker_length
half = size / 2
points = np.float32([
[-half, -half, 0], [half, -half, 0],
[half, half, 0], [-half, half, 0],
[0, 0, -size] # 頂点
])
imgpts, _ = cv2.projectPoints(
points, rvec, tvec, self.camera_matrix, self.dist_coeffs
)
imgpts = np.int32(imgpts).reshape(-1, 2)
# 底面
cv2.drawContours(frame, [imgpts[:4]], -1, color, 2)
# 側面
for j in range(4):
cv2.line(frame, tuple(imgpts[j]), tuple(imgpts[4]), color, 2)
def run(self):
"""メインループを実行"""
print("ARアプリケーションを開始します...")
print("'q' で終了、's' でスクリーンショット保存")
frame_count = 0
while True:
ret, frame = self.cap.read()
if not ret:
break
# マーカーを検出
corners, ids, _ = self.detect_markers(frame)
# 検出されたマーカーを描画
if ids is not None:
cv2.aruco.drawDetectedMarkers(frame, corners, ids)
frame = self.draw_ar_content(frame, corners, ids)
# 情報を表示
self._draw_info(frame, ids)
cv2.imshow('AR Application', frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
elif key == ord('s'):
cv2.imwrite(f'screenshot_{frame_count}.png', frame)
print(f"スクリーンショットを保存しました: screenshot_{frame_count}.png")
frame_count += 1
self.cleanup()
def _draw_info(self, frame, ids):
"""情報を表示"""
marker_count = len(ids) if ids is not None else 0
info_text = f"Markers: {marker_count} | Press 'q' to quit"
cv2.putText(
frame, info_text, (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2
)
def cleanup(self):
"""リソースを解放"""
if self.cap is not None:
self.cap.release()
cv2.destroyAllWindows()
def main():
config = ARConfig(
camera_id=0,
frame_width=1280,
frame_height=720,
marker_length=0.05
)
app = ARApplication(config)
app.initialize()
app.run()
if __name__ == '__main__':
main()
画像のオーバーレイ
マーカー上に画像を重ね合わせる方法です。
import cv2
import numpy as np
def overlay_image_on_marker(frame, corners, overlay_image):
"""マーカー上に画像をオーバーレイ"""
if corners is None or len(corners) == 0:
return frame
for corner in corners:
# マーカーの4つの角の座標
pts_dst = corner[0].astype(np.float32)
# オーバーレイ画像の4つの角
h, w = overlay_image.shape[:2]
pts_src = np.array([
[0, 0],
[w, 0],
[w, h],
[0, h]
], dtype=np.float32)
# ホモグラフィ行列を計算
matrix, _ = cv2.findHomography(pts_src, pts_dst)
# 画像を変換
warped = cv2.warpPerspective(
overlay_image, matrix,
(frame.shape[1], frame.shape[0])
)
# マスクを作成
mask = np.zeros((frame.shape[0], frame.shape[1]), dtype=np.uint8)
cv2.fillConvexPoly(mask, pts_dst.astype(np.int32), 255)
# 合成
mask_inv = cv2.bitwise_not(mask)
frame_bg = cv2.bitwise_and(frame, frame, mask=mask_inv)
overlay_fg = cv2.bitwise_and(warped, warped, mask=mask)
frame = cv2.add(frame_bg, overlay_fg)
return frame
まとめ
| ステップ | 内容 | 主要関数 |
|---|---|---|
| カメラ入力 | 映像取得 | cv2.VideoCapture() |
| マーカー生成 | ArUcoパターン作成 | cv2.aruco.generateImageMarker() |
| マーカー検出 | パターン認識 | cv2.aruco.ArucoDetector() |
| 姿勢推定 | 3D位置計算 | cv2.aruco.estimatePoseSingleMarkers() |
| 描画 | 3Dオブジェクト表示 | cv2.drawFrameAxes() |
OpenCVとArUcoマーカーを使うことで、比較的簡単にARアプリケーションを構築できます。さらに高度な機能(テクスチャマッピング、物理演算など)が必要な場合は、OpenGLやUnityとの連携を検討してください。