Digital Reactor
画像・3D処理

3次元画像処理における、N種の神器

3次元画像処理における、N種の神器

はじめに

LiDARやRGB-Dカメラで点群を取り、写真の束から3Dモデルを起こし、結果を画面で確かめる。3次元データを扱う仕事はこの繰り返しですが、工程ごとに定番のツールが分かれていて、選び方を誤ると手戻りが増えます。OpenCVだけで押し切ろうとして点群処理で行き詰まったり、逆に単純な可視化にPyTorch3Dのような重いライブラリを持ち出したり、という場面を実務で何度か見てきました。ここでは、2D画像からの3D理解、3D再構成、点群・メッシュ処理、可視化という工程の順に、主要なPythonツールと使い分けの基準を整理します。コード例と、実際に運用して分かった注意点も添えます。

対象読者:

  • Pythonを用いて3次元データ処理に携わるエンジニアや研究者
  • コンピュータビジョン、ロボティクス、AR/VRなどの分野で3次元データ処理に関心のある方
  • 3次元処理の基礎から実践的なツールまで幅広く学びたい方

記事のポイント:

  • 3次元処理の主要な工程(2D画像からの3D情報抽出、3D再構成、点群処理、可視化)ごとに定番ツールを挙げ、連携させて一連の処理パイプラインを組む方法を示す。

2D画像からの3D理解

3次元処理の出発点は多くの場合、カメラで撮った2次元画像から物体の奥行きや位置関係を推定するところにあります。やり方は大きく2つに分かれます。画像の中の特徴的な点を手がかりにする方法と、既知のマーカーを環境に置いて検出する方法です。順に見ていきます。

特徴点検出・マッチング

2D画像から3次元情報を抽出する際、まず画像中の特徴的な点(特徴点)を検出し、異なる画像間で対応する特徴点を結びつける(マッチング)処理を行います。この対応関係から、カメラの位置関係や物体の3次元構造を推定できます。

OpenCV:SIFT・ORBで押さえる定番の特徴点検出

OpenCVはコンピュータビジョンの定番ライブラリで、特徴点検出ではSIFT (Scale-Invariant Feature Transform) やORB (Oriented FAST and Rotated BRIEF) といった実績のある手法が揃っています。照明や視点が多少変わっても検出が安定するため、まず候補に挙がる選択肢です。

import cv2
import numpy as np

def detect_and_match_features(img1, img2):
    # SIFTの初期化
    sift = cv2.SIFT_create()

    # 特徴点の検出とディスクリプタの計算
    kp1, des1 = sift.detectAndCompute(img1, None)
    kp2, des2 = sift.detectAndCompute(img2, None)

    # BFMatcherによるマッチング
    bf = cv2.BFMatcher()
    matches = bf.knnMatch(des1, des2, k=2)

    # Loweの比率テストでマッチングをフィルタリング
    good_matches = []
    for m, n in matches:
        if m.distance < 0.75 * n.distance:
            good_matches.append(m)

    return kp1, kp2, good_matches

# 画像の読み込みと特徴点検出
img1 = cv2.imread('image1.jpg', cv2.IMREAD_GRAYSCALE)
img2 = cv2.imread('image2.jpg', cv2.IMREAD_GRAYSCALE)
kp1, kp2, good_matches = detect_and_match_features(img1, img2)

特徴量には他にも種類があります。手法ごとの検出の安定性と速度は別記事で実測して比較しています

深層学習ベースの特徴点検出

深層学習を使った特徴点検出・マッチングも実用段階にあり、SuperPointとSuperGlueが代表例です。深層学習モデルが画像の特徴をより抽象的な水準で捉えるため、従来手法より検出とマッチングが安定し、視点が大きく変わる画像ペアにも対応できます。GPUがあれば速度も実用の範囲に収まります。ただし計算リソースを多く使い、実装もやや複雑になるため、視点変化が穏やかな一般的な用途ならSIFTやORBで十分なことが多いです。

マーカーベース3D認識

特徴点ベースの手法に対して、既知のマーカーをあらかじめ環境に貼っておき、それを検出して3D位置を推定する方法があります。何を探せばよいかが決まっているぶん検出の曖昧さがなく、位置推定が安定します。

OpenCV ArUco:印刷して貼るだけで使えるマーカー

ArUcoマーカーはOpenCVに組み込まれているマーカーシステムで、追加のライブラリなしに使えます。検出が速く、複数マーカーを同時に検出でき、カメラキャリブレーションとも組み合わせやすいのが利点です。

以下はArUcoマーカーを生成するコードです。

import cv2
import numpy as np

def generate_aruco_marker(marker_id=1, size=200):
    # マーカー辞書の指定(6X6_250を使用)
    dictionary = cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_6X6_250)

    # マーカー画像の生成
    marker_image = np.zeros((size, size), dtype=np.uint8)
    marker_image = cv2.aruco.drawMarker(dictionary, marker_id, size, marker_image, 1)

    # マーカーの周りに白い余白を追加
    margin = 20
    final_image = np.ones((size + 2*margin, size + 2*margin), dtype=np.uint8) * 255
    final_image[margin:margin+size, margin:margin+size] = marker_image

    return final_image

# マーカーの生成と保存
marker = generate_aruco_marker(marker_id=1, size=200)
cv2.imwrite('aruco_marker.png', marker)

このコードでは、6x6ピクセルの内部パターンを持つArUcoマーカーを生成し、マーカーの周りに余白を追加しています。余白を追加することで、印刷時の認識性能が向上します。

マーカーからカメラの姿勢を推定するコードは次のようになります。

import cv2
import numpy as np

def detect_aruco_markers(image):
    # 辞書は、マーカー生成時のものと一致させる必要あり
    dictionary = cv2.aruco.getPredefinedDictionary(aruco.DICT_6X6_250)
    parameters = cv2.aruco.DetectorParameters()

    # マーカーの検出
    corners, ids, rejected = cv2.aruco.detectMarkers(
        image, dictionary, parameters=parameters
    )

    if ids is not None:
        # カメラパラメータ(キャリブレーション済みと仮定)
        camera_matrix = np.array([[fx, 0, cx],
                                [0, fy, cy],
                                [0, 0, 1]])
        dist_coeffs = np.zeros((4,1))  # 歪み係数

        # マーカーサイズ(メートル単位)
        marker_size = 0.05

        # 各マーカーの姿勢を推定
        rvecs, tvecs, _ = cv2.aruco.estimatePoseSingleMarkers(
            corners, marker_size, camera_matrix, dist_coeffs
        )

        return corners, ids, rvecs, tvecs

    return corners, ids, None, None

# カメラからの画像取得と処理
cap = cv2.VideoCapture(0)
ret, frame = cap.read()
if ret:
    corners, ids, rvecs, tvecs = detect_aruco_markers(frame)

    # 検出結果の描画
    if ids is not None:
        cv2.aruco.drawDetectedMarkers(frame, corners, ids)

        # 座標軸の描画
        for i in range(len(ids)):
            cv2.drawFrameAxes(frame, camera_matrix, dist_coeffs,
                            rvecs[i], tvecs[i], 0.03)

このコードは、カメラから取得した画像に対してArUcoマーカーの検出を行い、検出されたマーカーの3次元位置と姿勢(回転ベクトル rvecs と並進ベクトル tvecs で表現)を推定します。cv2.drawFrameAxes 関数を使うと、推定した姿勢に基づいて座標軸を描画し、結果を目で確認できます。

実装上の注意点:

  • 辞書セル数(6X6の部分)が多い(目安として、5x5以上)ほど誤り訂正が強くなり認識が安定しますが、遠距離では読み取りにくくなり、相対的に高解像度のカメラが要ります。

  • 辞書サイズ(250の部分)は、大きいほど多くのIDを持たせられるものの、誤り訂正が弱くなるというトレードオフがあります。

  • tvecsrvecsは、それぞれカメラから見たマーカーの座標になります。そのままでは使いにくいので、これらの値を4x4の変換行列にする関数を次のように定義しておくと扱いやすくなります。

    def params_to_transform(tvec, rvec):
    transform = np.eye(4)
    transform[:3, :3] = cv2.Rodrigues(rvec)[0]
    transform[:3, 3] = tvec.flatten()
    return transform

私の経験に基づく話ですが、チェッカーボードによるキャリブレーションの代わりに、ArUcoを並べたボードを使うことがあります。精度はチェッカーボードによるものの方が高いのですが、特にカメラの配置が並行ではないときに、チェッカーボードだと交点の順番を認識しにくいのに対し、ArUcoだと認識できるというメリットがあります。

AprilTag:小さい・遠いマーカーに強い代替システム

ArUcoの代替として、AprilTagというマーカーシステムもあります。マーカーが小さい場合や遠い場合の検出精度がArUcoより高く、偽陽性が少ないのが特徴です。ROS (Robot Operating System) での実績も豊富です。ただしOpenCVには標準で組み込まれていないため、別途ライブラリを導入する必要があります。

3D再構成

複数の画像の対応関係から3次元モデルを組み立てる工程が3D再構成です。ここでは、写真測量の定番であるStructure from Motion (SfM) と、深層学習による3D形状推定の2つを取り上げます。

COLMAP:写真の束から3Dモデルを起こす

COLMAPは、Structure from Motion (SfM) と Multi-View Stereo (MVS) という技術を使って、複数の写真から3次元モデルを生成するツールです。SfMは、異なる視点から撮影された画像間の対応点を手がかりに、カメラの位置・姿勢とシーンの3次元構造を同時に推定します。MVSは、SfMで推定したカメラパラメータを基に、より密な3次元点群を生成します。オープンソースでありながらパラメータを細かく調整でき、GUIでの対話操作とコマンドラインでのバッチ処理の両方に対応します。ただし入力画像の質が悪いと正しく処理できないことが多く、撮影にはそれなりの注意が要ります。

撮影のコツとして、被写体の周りを30度ずつ移動しながら撮り、各写真で60-80%程度のオーバーラップを確保すると、良好な結果が得られやすくなります。可能な限り固定焦点レンズを使い、ISO感度は低めに設定してノイズを抑えます。光沢のある表面や透明な物体は特徴点のマッチングを乱すので避けてください。

再構成が途中で失敗する場合は、特徴点が少ない写真を除外してみてください。テクスチャが荒い場合は、露出過度な写真を除外すると改善することがあります。スケールが不安定な場合は、基線長(カメラ間の距離)を適度に確保するように撮影します。メモリが足りないときは、--PatchMatchStereo.max_image_size 2000 のようにオプションを指定して、処理する画像のサイズを制限できます。

PyTorch3D:3D深層学習の研究開発向けライブラリ

PyTorch3Dは、Facebook AI Research (FAIR) が開発した、深層学習を用いた3D処理のためのライブラリです。GPUで効率よく動く3D操作関数と、微分可能なレンダラーを備えます。従来の3次元処理と深層学習を組み合わせる研究開発向けで、この記事で挙げるツールの中ではいちばん応用的な位置づけです。

用途は例えば次のとおりです。

  • 3D物体認識・セグメンテーション。PointNet++やDGCNNなどの点群ベースモデルや、Mesh R-CNNなどのメッシュベースモデル、3D物体検出のための損失関数を実装できます。
  • 3D形状生成。VAE (Variational Autoencoder) やGAN (Generative Adversarial Network) による形状生成のほか、単眼画像からの3D形状推定、テクスチャ合成にも応用できます。
  • 微分可能なレンダリングを使った、3Dメッシュの最適化、ニューラルレンダリング、逆レンダリング。

実装上の注意点として、メッシュ処理はGPUメモリを消費しやすいので、バッチサイズは小さめから始めます。点群は必要に応じてダウンサンプリングし、メッシュの面数も適度に削減します。メモリ使用量を監視しながらモデルとデータのサイズを調整すると、学習と推論が安定します。

3Dデータ処理

3次元データは、主に点群(ポイントクラウド)とメッシュという形式で表現されます。点群は3次元空間内の点の集合で、LiDARセンサーやRGB-Dカメラなどから直接取得できます。メッシュは頂点・辺・面で構成される多面体で、3Dモデリングソフトウェアや3Dスキャナなどで生成されます。ここでは点群とメッシュの処理に使うツールを取り上げます。

Open3D:点群処理はまずこれから

Open3Dは3D点群処理のライブラリで、読み込みから可視化まで一通りの機能が揃っています。C++で実装されたコア部分をPythonから呼べるため、処理速度と書きやすさを両立できます。研究開発から実用的なアプリケーションまで幅広く使われています。

主な機能は次のとおりです。

  • 点群の読み込み・保存(主要なファイル形式に対応)
  • 各点の法線ベクトルの推定
  • 点群を意味のある領域に分割するセグメンテーション
  • 密度を調整するダウンサンプリング
  • 近傍点探索(k近傍法=kNN、半径探索=rNN)
  • 3次元データの可視化

点群処理の基本形は次のようになります。

import open3d as o3d
import numpy as np

# ノイズを含む球面上の点群を生成
def create_noisy_sphere(n_points=1000, radius=1.0, noise_std=0.1):
    # 球面上の点を生成
    theta = np.random.uniform(0, 2*np.pi, n_points)
    phi = np.random.uniform(0, np.pi, n_points)

    x = radius * np.sin(phi) * np.cos(theta)
    y = radius * np.sin(phi) * np.sin(theta)
    z = radius * np.cos(phi)

    # ノイズを追加
    points = np.vstack((x, y, z)).T + np.random.normal(0, noise_std, (n_points, 3))

    # Open3D点群オブジェクトを作成
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)
    return pcd

# 点群の処理例
def process_point_cloud(pcd):
    # 法線の推定
    pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(
        radius=0.1, max_nn=30))

    # ダウンサンプリング(ボクセルグリッド)
    downsampled = pcd.voxel_down_sample(voxel_size=0.05)

    # 統計的外れ値除去
    cleaned, _ = downsampled.remove_statistical_outlier(
        nb_neighbors=20, std_ratio=2.0)

    return cleaned

# k近傍点の探索と処理
def process_knn(pcd, k=5):
    # KDTreeの構築
    pcd_tree = o3d.geometry.KDTreeFlann(pcd)

    # 点群をNumPy配列として取得
    points = np.asarray(pcd.points)

    # 各点について処理
    for i in range(len(points)):
        # k近傍点を探索
        # 戻り値: (成功/失敗, インデックスのリスト, 距離のリスト)
        _, idx, dist = pcd_tree.search_knn_vector_3d(pcd.points[i], k)

        # 近傍点の重心を計算
        neighbors = points[idx[1:]]  # 自分自身を除く
        centroid = np.mean(neighbors, axis=0)

        # 近傍点との平均距離を計算
        mean_dist = np.mean(np.sqrt(dist[1:]))  # 自分自身を除く

        # ここで近傍点の情報を使った処理を追加できます
        # 例:局所的な特徴量の計算、スムージング、など

# 使用例
pcd = create_noisy_sphere()
cleaned_pcd = process_point_cloud(pcd)
process_knn(cleaned_pcd)

パラメータで押さえておきたい点がいくつかあります。

  • 法線推定の radius は、法線の計算に使う近傍点の範囲を決めます。大きすぎると細部が失われ、小さすぎるとノイズを拾います。max_nn は考慮する近傍点の最大数で、通常は30-50程度を起点に点群の密度に応じて調整します。ここの設定は、後続のセグメンテーションや特徴量記述の精度に響きます。
  • KDTreeは構築に時間がかかるため、繰り返し使う場合は再利用します。k近傍法(kNN)と半径探索(rNN)は用途で使い分けます。kNNは点の密度が不均一な場合に向き、rNNは局所的な形状特徴の計算に向きます(ただし取得される点の数が不定になるため、扱いにくい場合があります)。
  • 大規模な点群を扱う場合は、必要に応じてダウンサンプリングし、処理済みの中間データを適宜削除するなど、メモリ使用量に気を配ってください。

可視化・レンダリング

処理した3次元データは、目で見て確かめないと不具合に気づけません。法線の向きが反転している、外れ値除去で形が欠けた、といった問題は数値だけ眺めていても分からず、表示してみて見つかることが多いです。発表やデモのためだけでなく、日々の確認の道具として、可視化ツールを1つは手元に置いておく価値があります。

CloudCompare:出力した点群ファイルをすぐ見たいときに

フリーの点群可視化・加工ソフトウェアです。GUIで、点群のノイズ除去、切り抜き、回転・平行移動、法線計算、メッシュ作成など、一通りのことができます。点群の処理が主なので、メッシュの処理を本格的にGUIでやりたい場合には、MeshLabもお勧めできます。どちらもフリーソフトです。

Open3D:スクリプトから点群を表示・保存する

Open3Dは点群処理だけでなく、インタラクティブな可視化にも対応します。APIがシンプルで、点群とメッシュの両方を表示できます。オフスクリーンレンダリング(ウィンドウを表示せずに画像ファイル等へ出力する方式)にも対応しているので、バッチ処理で大量のデータを画像化する用途にも向きます。Depthセンサーなどで取得した点群をその場で確認したいときにも重宝します。

可視化の基本的な例を示します。

import open3d as o3d
import numpy as np

def visualize_point_cloud_with_features():
    # ノイズを含む球面上の点群を生成
    pcd = create_noisy_sphere()

    # 法線の推定
    pcd.estimate_normals(search_param=o3d.geometry.KDTreeSearchParamHybrid(
        radius=0.1, max_nn=30))

    # 点群の色付け(法線方向に基づく)
    normals = np.asarray(pcd.normals)
    colors = (normals + 1) / 2  # 法線を[0, 1]の範囲にスケーリング
    pcd.colors = o3d.utility.Vector3dVector(colors)

    # 座標軸の作成
    coordinate_frame = o3d.geometry.TriangleMesh.create_coordinate_frame(
        size=0.5, origin=[0, 0, 0])

    # ビジュアライザの初期化
    vis = o3d.visualization.Visualizer()
    vis.create_window()

    # レンダリングオプションの設定
    opt = vis.get_render_option()
    opt.background_color = np.asarray([0.1, 0.1, 0.1])  # 暗めの背景色
    opt.point_size = 2.0  # 点のサイズを大きめに

    # ジオメトリの追加
    vis.add_geometry(pcd)
    vis.add_geometry(coordinate_frame)

    # カメラパラメータの設定
    ctr = vis.get_view_control()
    ctr.set_zoom(0.8)
    ctr.set_front([1, -0.5, -0.5])
    ctr.set_lookat([0, 0, 0])
    ctr.set_up([0, 0, 1])

    # 画像として保存
    vis.capture_screen_image("point_cloud_visualization.png")

    # インタラクティブな表示(必要な場合)
    vis.run()
    vis.destroy_window()

# 可視化の実行
visualize_point_cloud_with_features()

PyVista:スカラー場やグリッドを図にするならVTK系

PyVistaは、可視化ツールキットVTK (Visualization Toolkit) をPythonから扱いやすくしたライブラリです。VTKは3次元グラフィックスと可視化のオープンソース基盤で、長年の実績があります。その機能を短いコードで呼び出せるのがPyVistaの持ち味です。

  • 等値面、断面、ストリームライン、カラーマップの細かい調整、マルチビューレイアウトなど、可視化の手法が豊富です。
  • 構造化/非構造化グリッド、スカラー場・ベクトル場、時系列データなど、科学技術計算で扱うデータ形式に対応しています。
  • Jupyter NotebookやJupyterLab上でのインタラクティブな3D表示、ウィジェットによる操作、プログラムからのアニメーション生成に対応しており、探索的な分析や結果の共有がしやすくなります。

3D可視化の例を示します。

import pyvista as pv
import numpy as np

# プロッターの初期化(1行2列のサブプロット)
plotter = pv.Plotter(shape=(1, 2), off_screen=True, window_size=[1200, 400])

# 3次元スカラー場の生成
x, y, z = np.mgrid[-5:5:100j, -5:5:100j, -5:5:100j]
scalar_field = np.sin(np.sqrt(x**2 + y**2 + z**2))
grid = pv.StructuredGrid(x, y, z)
grid["values"] = scalar_field.flatten()

# 左側のサブプロットに等値面を表示
plotter.subplot(0, 0)
plotter.add_mesh(grid.contour([0.2]), opacity=0.5,
                cmap="viridis", show_scalar_bar=True)
plotter.add_text("Isosurface at value = 0.2", position="upper_edge")

# 右側のサブプロットに直交スライスを表示
plotter.subplot(0, 1)
plotter.add_mesh(grid.slice_orthogonal(), cmap="viridis")
plotter.add_text("Orthogonal Slices", position="upper_edge")

# カメラ位置の設定
plotter.view_isometric()
plotter.camera.zoom(1.5)

# 画像として保存
plotter.savefig("pyvista_visualization.png")

このコードは、NumPyで生成した3次元スカラー場をPyVistaで可視化する例です。等値面(特定の値を持つ面)と直交スライス(座標軸に垂直な断面)を表示し、結果を画像ファイルとして保存しています。数値計算の結果をこうした図に落とす用途に向いています。

Webブラウザでの3次元データ表示

Webブラウザ上で3次元データを表示したい場合は、JavaScript/WebGLベースのThree.jsが有力です。軽量で、Webアプリケーションへの組み込みに向いています。

まとめ

工程ごとに見てきたとおり、特徴点やマーカーの検出はOpenCV、写真からの再構成はCOLMAP、点群処理はOpen3D、確認用の表示はCloudCompareかOpen3D、計算結果のグリッドを図にするならPyVista、というのが基本の使い分けです。深層学習を絡めるときだけSuperPoint系やPyTorch3Dを持ち出せば足り、最初から重い構成にする必要はありません。自動運転のLiDARデータ処理でも、ロボットビジョンの物体認識でも、AR/VRコンテンツの作成でも、この組み合わせの中でほとんどの工程がまかなえます。

まずは手元のデータが画像・点群・メッシュのどれか、つまずいている工程がどこかを見極め、そこに合うツールから試すのが近道です。点群を持っているならCloudCompareで開いて眺めるところから、写真しかないならCOLMAPに数十枚を通すところから始められます。

関連記事

← 技術ブログ一覧へ