📐
3D座標変換の基礎まとめ
はじめに
座標変換関連の処理を業務で触ることになったのですが、触れるのが大学受験勉強以来だったので、基礎的な要素も含めて自分の復習用まとめます。
この記事はあくまでも個人的なメモとして作成されています。そのため、使用したライブラリやツールのバージョンについては特に記載しておりません。コードの動作環境に依存する可能性があるため、ご利用の際はご自身の環境に合わせたバージョンで適切にインストールしてお使いください。
以下の基本的な処理をpython
とcpp
の両方でまとめます。
- オイラー角 -> 回転行列
- オイラー角 -> 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