📐

3D座標変換の基礎まとめ

2024/09/17に公開

はじめに

座標変換関連の処理を業務で触ることになったのですが、触れるのが大学受験勉強以来だったので、基礎的な要素も含めて自分の復習用まとめます。

この記事はあくまでも個人的なメモとして作成されています。そのため、使用したライブラリやツールのバージョンについては特に記載しておりません。コードの動作環境に依存する可能性があるため、ご利用の際はご自身の環境に合わせたバージョンで適切にインストールしてお使いください。

以下の基本的な処理をpythoncppの両方でまとめます。

  • オイラー角 -> 回転行列
  • オイラー角 -> transform matrix
  • transform matrix -> delta pose
  • 極座標 -> 直交座標
  • 直交座標 -> PointCloud
  • PointCloud -> Depth Map

この記事の対象者

3D空間における座標変換の基礎や、それをPythonとcppで実装する方法を知りたい人向けです。

各処理について

オイラー角 -> 回転行列

python(Rotationを使用する場合)
import numpy as np
from scipy.spatial.transform import Rotation as R

def euler_to_rotation_matrix(roll, pitch, yaw):
    r = R.from_euler('xyz', [roll, pitch, yaw], degrees=False)
    rotation_matrix = r.as_matrix()  # 3x3の回転行列を取得

    return rotation_matrix

roll = np.radians(30)   # X軸周りの回転角(ロール)
pitch = np.radians(45)  # Y軸周りの回転角(ピッチ)
yaw = np.radians(60)    # Z軸周りの回転角(ヨー)

transform_matrix = euler_to_rotation_matrix(roll, pitch, yaw)
print(transform_matrix)
Python(Rotationを使用しない場合)
import numpy as np

def euler_to_rotation_matrix(roll, pitch, yaw):
    # Roll (X軸周りの回転)
    R_x = np.array([
        [1, 0, 0],
        [0, np.cos(roll), -np.sin(roll)],
        [0, np.sin(roll), np.cos(roll)]
    ])

    # Pitch (Y軸周りの回転)
    R_y = np.array([
        [np.cos(pitch), 0, np.sin(pitch)],
        [0, 1, 0],
        [-np.sin(pitch), 0, np.cos(pitch)]
    ])

    # Yaw (Z軸周りの回転)
    R_z = np.array([
        [np.cos(yaw), -np.sin(yaw), 0],
        [np.sin(yaw), np.cos(yaw), 0],
        [0, 0, 1]
    ])

    # 回転行列を合成
    R = np.dot(R_z, np.dot(R_y, R_x))

    return R

# オイラー角を指定(ラジアン)

roll = np.radians(30)   # X軸周りの回転角(ロール)
pitch = np.radians(45)  # Y軸周りの回転角(ピッチ)
yaw = np.radians(60)    # Z軸周りの回転角(ヨー)

# 回転行列を計算

rotation_matrix = euler_to_rotation_matrix(roll, pitch, yaw)
print("Rotation Matrix:")
print(rotation_matrix)
cpp(標準ライブラリ)
#include <cmath>
#include <array>


double DegreesToRadians(double degrees) {
    return degrees * M_PI / 180.0;
}

std::array<std::array<double, 3>, 3>euler_to_rotation_matrix(double roll, double pitch, double yaw) {
    // Roll (X軸周りの回転)
    std::array<std::array<double, 3>, 3> R_x = {{
        {1, 0, 0},
        {0, std::cos(roll), -std::sin(roll)},
        {0, std::sin(roll), std::cos(roll)}
    }};

    // Pitch (Y軸周りの回転)
    std::array<std::array<double, 3>, 3> R_y = {{
        {std::cos(pitch), 0, std::sin(pitch)},
        {0, 1, 0},
        {-std::sin(pitch), 0, std::cos(pitch)}
    }};

    // Yaw (Z軸周りの回転)
    std::array<std::array<double, 3>, 3> R_z = {{
        {std::cos(yaw), -std::sin(yaw), 0},
        {std::sin(yaw), std::cos(yaw), 0},
        {0, 0, 1}
    }};

    std::array<std::array<double, 3>, 3> R_zy;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            R_zy[i][j] = R_z[i][0] * R_y[0][j] + R_z[i][1] * R_y[1][j] + R_z[i][2] * R_y[2][j];
        }
    }

    std::array<std::array<double, 3>, 3> R_final;
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j) {
            R_final[i][j] = R_zy[i][0] * R_x[0][j] + R_zy[i][1] * R_x[1][j] + R_zy[i][2] * R_x[2][j];
        }
    }

    return R_final;
}

int main() {
    double roll = DegreesToRadians(30);
    double pitch = DegreesToRadians(45);
    double yaw = DegreesToRadians(60);

    std::array<std::array<double, 3>, 3> rotation_matrix = euler_to_rotation_matrix(roll, pitch, yaw);

    return 0;
}
cpp(Eigen)
#include <Eigen/Dense>
#include <cmath>


double DegreesToRadians(double degrees) {
    return degrees * M_PI / 180.0;
}

Eigen::Matrix3d euler_to_rotation_matrix(double roll, double pitch, double yaw) {
    Eigen::Matrix3d rotation_matrix;
    // AngleAxisdは掛け算すると、内部的に回転行列に変換されてから掛け算される
    rotation_matrix = Eigen::AngleAxisd(roll, Eigen::Vector3d::UnitX()) *
                      Eigen::AngleAxisd(pitch, Eigen::Vector3d::UnitY()) *
                      Eigen::AngleAxisd(yaw, Eigen::Vector3d::UnitZ())

    // Eigen::Matrix3d R_x;
    // R_x << 1, 0, 0,
    //         0, std::cos(roll), -std::sin(roll),
    //         0, std::sin(roll), std::cos(roll);

    // Eigen::Matrix3d R_y;
    // R_y << std::cos(pitch), 0, std::sin(pitch),
    //        0, 1, 0,
    //        -std::sin(pitch), 0, std::cos(pitch);

    // Eigen::Matrix3d R_z;
    // R_z << std::cos(yaw), -std::sin(yaw), 0,
    //        std::sin(yaw), std::cos(yaw), 0,
    //        0, 0, 1;

    // Eigen::Matrix3d R = R_z * R_y * R_x;

    return rotation_matrix
}

int main() {
    double roll = DegreesToRadians(30);
    double pitch = DegreesToRadians(45);
    double yaw = DegreesToRadians(60);

    Eigen::Matrix3d rotation_matrix = euler_to_rotation_matrix(roll, pitch, yaw);

    return 0;
}

オイラー角 -> transform matrix

python
import numpy as np
from scipy.spatial.transform import Rotation as R

def euler_to_transform_matrix(roll, pitch, yaw, translation):
    # オイラー角を使って回転行列を作成
    r = R.from_euler('xyz', [roll, pitch, yaw], degrees=False)  # xyzの順序でオイラー角を指定
    rotation_matrix = r.as_matrix()  # 3x3の回転行列を取得

    # 4x4の変換行列を初期化
    transform_matrix = np.eye(4)

    # 回転行列を変換行列の一部にセット
    transform_matrix[:3, :3] = rotation_matrix

    # 並進ベクトルをセット (x, y, z)
    transform_matrix[:3, 3] = translation

    return transform_matrix

# 例: オイラー角と並進ベクトルを指定
roll = np.radians(30)   # X軸周りの回転角(ロール)
pitch = np.radians(45)  # Y軸周りの回転角(ピッチ)
yaw = np.radians(60)    # Z軸周りの回転角(ヨー)
translation = [1, 2, 3]  # 並進ベクトル (x, y, z)

# 変換行列を計算
transform_matrix = euler_to_transform_matrix(roll, pitch, yaw, translation)
print(transform_matrix)
cpp
#include <Eigen/Dense>
#include <cmath>


double DegreesToRadians(double degrees) {
    return degrees * M_PI / 180.0;
}

Eigen::Matrix3d euler_to_rotation_matrix(double roll, double pitch, double yaw) {
    Eigen::Matrix3d rotation_matrix;
    rotation_matrix = Eigen::AngleAxisd(roll, Eigen::Vector3d::UnitX()) *
                      Eigen::AngleAxisd(pitch, Eigen::Vector3d::UnitY()) *
                      Eigen::AngleAxisd(yaw, Eigen::Vector3d::UnitZ())

    Eigen::Matrix4d transform_matrix = Eigen::Matrix4d::Identity();

    transform_matrix.block<3, 3>(0, 0) = rotation_matrix;

    transform_matrix.block<3, 1>(0, 3) = translation;

    return rotation_matrix
}

int main() {
    double roll = DegreesToRadians(30);
    double pitch = DegreesToRadians(45);
    double yaw = DegreesToRadians(60);

    Eigen::Matrix3d rotation_matrix = euler_to_rotation_matrix(roll, pitch, yaw);

    return 0;
}

transform matrix -> delta pose

python
import numpy as np

def compute_delta_pose(T1, T2):
    T1_inv = np.linalg.inv(T1)
    delta_pose = np.dot(T1_inv, T2)

    return delta_pose

T1 = np.array([
    [np.cos(np.pi / 4), -np.sin(np.pi / 4), 0, 1],
    [np.sin(np.pi / 4), np.cos(np.pi / 4), 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
])

T2 = np.array([
    [np.cos(np.pi / 2), -np.sin(np.pi / 2), 0, 2],
    [np.sin(np.pi / 2), np.cos(np.pi / 2), 0, 0],
    [0, 0, 1, 0],
    [0, 0, 0, 1],
])

delta_pose = compute_delta_pose(T1, T2)

print(f"Delta Pose: {delta_pose}")

cpp
#include <iostream>
#include <Eigen/Dense>

Eigen::Matrix4d compute_delta_pose(const Eigen::Matrix4d& T1, const Eigen::Matrix4d& T2) {
    Eigen::Matrix4d T1_inv = T1.inverse();

    Eigen::Matrix4d delta_pose = T1_inv * T2;

    return delta_pose;

int main() {
    Eigen::Matrix4d T1 = Eigen::Matrix4d::Identity();
    T1(0, 3) = 1.0;
    T1.block<3, 3>(0, 0) = Eigen::AngleAxisd(M_PI / 4, Eigen::Vector3d::UnitZ()).toRotationMatrix();

    Eigen::Matrix4d T2 = Eigen::Matrix4d::Identity();
    T2(0, 3) = 2.0;
    T2.block<3, 3>(0, 0) = Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitZ()).toRotationMatrix();

    Eigen::Matrix4d delta_pose = compute_delta_pose(T1, T2);

    std::cout << "Delta Pose:\n" << delta_pose << std::endl;

    Eigen::Matrix3d delta_rotation = delta_pose.block<3, 3>(0, 0);
    std::cout << "Delta Rotation (relative rotation matrix):\n" << delta_rotation << std::endl;

    Eigen::Vector3d delta_translation = delta_pose.block<3, 1>(0, 3);
    std::cout << "Delta Translation (relative translation vector):\n" << delta_translation.transpose() << std::endl;

    return 0;
}

}

極座標 -> 直交座標

python
import numpy as np

def degrees_to_radians(degrees):
    return np.radians(degrees)

def spherical_to_cartesian(range_data, azimuth_deg, elevation_deg):
    azimuth_rad = degrees_to_radians(azimuth_deg)
    elevation_rad = degrees_to_radians(elevation_deg)

    x = range_data * np.cos(elevation_rad) * np.cos(azimuth_rad)
    y = range_data * np.cos(elevation_rad) * np.sin(azimuth_rad)
    z = range_data * np.sin(elevation_rad)

    return x, y, z

x, y, z = spherical_to_cartesian(10, 30, 60)

cpp
#include <iostream>
#include <cmath>

double degreesToRadians(double degress) {
    return degrees * M_PI / 180.0;
}

void sphericalToCartesian(double range, double azimuth_deg, double elevation_deg, double &x, double &y, double &z) {
    double azimuth_rad = degreesToRadians(azimuth_deg);
    double elevation_rad = degreesToRadians(elevation_deg);

    x = range * cos(elevation_rad) * cos(azimuth_rad);
    y = range * cos(elevation_rad) * sin(azimuth_rad);
    z = range * sin(elevation_rad);
}

int main() {
    double range = 10.0;
    double azimuth_deg = 30.0;
    double elevation_deg = 15.0;

    double x, y, z;

    sphericalToCartesian(range, azimuth_deg, elevation_deg, x, y, z);

    std::cout << "3D座標: X=" << x << ", Y=" << y << ", Z=" << z << std::endl;

    return 0;
}

直交座標 -> PointCloud

python
import open3d as o3d
import numpy as np

x = [1, 1, 1, 1]
y = [2, 3, 4, 5]
z = [1, 1, 1, 1]
intensities = [10, 20, 30, 40] # 光の反射強度

points = [
    [x, y, z] for x, y, z in zip(z, y, z)
]

# 反射強度をOpen3Dで扱えるようにするため、強度をカラーとして設定
# 光の反射強度を0-1の範囲に正規化し、グレースケールカラーとして設定
pcd = o3d.geometry.PointCloud()
pcd = o3d.utility.Vector3dVector(points)

normalizer = 255.0 / np.max(intensities)
colors = np.tile(intensities.reshape(-1, 1) * normalizer, (1, 3))
pcd.colors = o3d.utility.Vector3dVector(colors)

o3d.io.write_point_cloud("point_cloud_with_intensity_open3d.ply", pcd)

o3d.visualization.draw_geometries([pcd])
cpp
#include <open3d/Open3D.h>
#include <iostream>
#include <vector>
#include <cmath>

int main() {
    std::vector<double> x = {1, 1, 1, 1};
    std::vector<double> y = {2, 3, 4, 5};
    std::vector<double> z = {1, 1, 1, 1};
    std::vector<double> intensity_list = {100, 80, 90, 60}

    std::vector<Eigen::Vector3d> points;
    std::vector<Eigen::Vector3d> colors;

    double max_intensity = *std::max_element(intensity_list.begin(), intensity_list.end());

    for (size_t i = 0; i < x.size(); ++i) {
        points.emplace_back(x[i], y[i], z[i])

        double normalized_intensity = intensity_list[i] / max_intensity;
        colors.emplace_back(normalized_intensity, normalized_intensity, normalized_intensity)
    }

    auto pcd = std::make_shared<open3d::geometry::PointCloud>();

    pcd->points_ = points;
    pcd->colors_ = colors;

    open3d::io::WritePointCloud("point_cloud_with_intensity_open3d.ply", *pcd);
    open3d::visualization::DrawGeometries({pcd});

    return 0;
}

PointCloud -> Depth Map

python
import numpy as np
import matplotlib.pyplot as plt

def points_to_depth_image(points, K, R, t, image_size=(480, 640)):

    # カメラ座標系への変換
    points_cam = np.dot(R, points.T) + t.reshape(3, 1)

    # 奥行き方向(Z)が0以下の点は除外
    valid_cam = points_cam[2, :] > 0
    points_cam = points_cam[:, valid_indices]

    # カメラ行列を使って2D平面に投影
    points_2d = np.dot(K, points_cam)

    # 正規化してピクセル座標に変換
    points_2d = points_2d[:2, :] / points_2d[2, :]

    # 深度(Z値)を取り出す
    depths = points_cam[2, :]

    # 画像を初期化する
    depth_image = np.zeros(image_size)

    # 2Dピクセル座標に深度をマッピング
    image_width = image_size[1]
    image_height = image_size[0]
    for i in range(points_2d.shape[1]):
        u, v = int(points_2d[0, i]), int(points_2d[1, i])
        if 0 <= u < image_width and 0 <= v < image_height:
            depth_image[v, u] = depths[i]

    return depth_image


points = np.array([
    [1, 1, 1],
    [1, 2, 1],
    [1, 3, 1],
    [1, 4, 1],
    [1, 5, 1]
])

# カメラの内部パラメータ行列
K = np.array([[800, 0, 320],
              [0, 800, 240],
              [0, 0, 1]])

# カメラの外部パラメータ
R = np.eye(3)
t = np.array([0, 0, 0])

depth_image = project_points_to_image_and_depth(points, K, R, t)

plt.imshow(depth_image, cmap="plasma")
plt.colorbar(label="Depth (Z-axis)")
plt.title('Depth Image from LiDAR Point Cloud')
plt.xlabel('X (pixels)')
plt.ylabel('Y (pixels)')
plt.show()

cpp
#include <iostream>
#include <Eigen/Dense>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace Eigen;


cv::Mat points_to_depth_image(const MatrixXd& points, const Matrix3d& K, const Matrix3d& R, const Vector3d& t, const cv::Size& image_size) {
    // カメラ座標系への変換
    MatrixXd points_cam = (R * points.transpose()).colwise() + t;

    // 奥行き方向(Z)が0以下の点を除外
    std::vector<int> valid_indices;
    for (int i = 0; i < points_cam.cols(); ++i) {
        if (points_cam(2, i) > 0) {
            valid_indices.push_back(i);
        }
    }

    // 有効な点群のみを取得
    MatrixXd points_cam_valid(3, valid_indices.size());
    for (size_t i = 0; i < valid_indices.size(); ++i) {
        points_cam_valid.col(i) = points_cam.col(valid_indices[i]);
    }

    // カメラ行列を使って2D平面に投影
    MatrixXd points_2d = K * points_cam_valid;

    // 正規化してピクセル座標に変換
    for (size_t i = 0; i < points_2d.cols(); ++i) {
        points_2d(0, i) /= points_2d(2, i);
        points_2d(1, i) /= points_2d(2, i);
    }

    // 深度を取得
    VectorXd depths = points_cam_valid.row(2);

    // 画像を初期化する
    cv::Mat depth_image = cv::Mat::zeros(image_size, CV_32F);
    // cv::Mat depth_image = cv::Mat::zeros(image_size, CV_64F); // 高精度画像が欲しい場合

    // 2Dピクセル座標に深度をマッピング
    for (size_t i = 0; i < depths.cols(); ++i) {
        // 画像平面上の座標を示す時横軸は u, 縦軸は v で表すらしい(x, y の表記は3D空間の座標を表す時に使用)
        int u = static_cast<int>(points_2d(0, i));
        int v = static_cast<int>(points_2d(1, i));

        if (0 <= u && u < image_size.width && 0 <= v && v < image_size.height) {
            depth_image.at<float>(v, u) = depths(i)
            // depth_image.at<double>(v, u) = depths(i) // 高精度画像を使用する場合
        }
    }

    return depth_image;
}

int main() {
    MatrixXd points(5, 3);
    points << 1, 1, 1,
              1, 2, 1,
              1, 3, 1,
              1, 4, 1,
              1, 5, 1;

    Matrix3d K;
    K << 800, 0, 320,
         0, 800, 240,
         0, 0, 1;

    Matrix3d R = Matrix3d::Identity();
    Vector3d t(0, 0, 0);

    cv::Size image_size(640, 480);

    cv::Mat depth_image = points_to_depth_image(points, K, R, T, image_size)

    cv::imshow("Depth Image from LiDAR Point Cloud", depth_image)
    cv::waitKey(0);

    return 0
}

Discussion