Documentation Python

PythonとOpenCVを使って拡張現実(AR)アプリケーションを構築する方法を解説します。マーカー認識から3Dオブジェクトの重ね合わせまで、実践的なコード例とともに紹介します。

ARアプリケーション開発ツールの比較

ツール/ライブラリ用途特徴難易度
OpenCV + ArUcoマーカーベースAR軽量・高速
OpenCV + MediaPipeハンドトラッキングリアルタイム処理
ARCore (Android)モバイルAR平面検出・環境理解
ARKit (iOS)モバイルAR高精度トラッキング
Unity + VuforiaゲームAR3Dレンダリング

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_504x450小規模プロジェクト
DICT_5X5_1005x5100中規模プロジェクト
DICT_6X6_2506x6250大規模プロジェクト
DICT_7X7_10007x71000非常に大規模
DICT_ARUCO_ORIGINAL5x51024オリジナル互換

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との連携を検討してください。

参考文献

円