transform-base 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- transform_base-0.0.3/PKG-INFO +93 -0
- transform_base-0.0.3/README.md +79 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/pyproject.toml +3 -2
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base/__init__.py +18 -0
- transform_base-0.0.3/transform_base/matrix_ops.py +534 -0
- transform_base-0.0.3/transform_base.egg-info/PKG-INFO +93 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base.egg-info/SOURCES.txt +1 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base.egg-info/requires.txt +1 -0
- transform_base-0.0.2/PKG-INFO +0 -60
- transform_base-0.0.2/README.md +0 -47
- transform_base-0.0.2/transform_base.egg-info/PKG-INFO +0 -60
- {transform_base-0.0.2 → transform_base-0.0.3}/MANIFEST.in +0 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/setup.cfg +0 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base/transform.py +0 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base.egg-info/dependency_links.txt +0 -0
- {transform_base-0.0.2 → transform_base-0.0.3}/transform_base.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transform-base
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: 欧拉角、旋转矩阵、四元数、角轴及多格式矩阵计算基础库
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20
|
|
9
|
+
Requires-Dist: PyYAML>=6.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
13
|
+
Requires-Dist: twine; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# transform-base
|
|
16
|
+
|
|
17
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX),并提供多输入格式解析、矩阵乘法/求逆/比较/方程求解的通用 API。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install transform-base
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 功能特性
|
|
26
|
+
|
|
27
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
28
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
29
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
30
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
31
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
32
|
+
- **多格式输入解析**:支持旋转矩阵+平移、四元数+平移、欧拉角+平移、4x4 齐次矩阵、JSON/proto/flex 四元数格式
|
|
33
|
+
- **矩阵计算**:支持统一格式输出、矩阵比较、`A = C @ B` / `A = B @ D` 求解
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from transform_base import (
|
|
39
|
+
normalize_angle,
|
|
40
|
+
standard_euler_to_rotation_matrix,
|
|
41
|
+
rotation_matrix_to_standard_euler,
|
|
42
|
+
ExtrinsicParam,
|
|
43
|
+
parse_transform_input,
|
|
44
|
+
transform_from_input,
|
|
45
|
+
transform_to_all_formats,
|
|
46
|
+
compare_transforms,
|
|
47
|
+
solve_transform_equations,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
51
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
52
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
53
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
54
|
+
|
|
55
|
+
# 外参:欧拉角 + 平移
|
|
56
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
57
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
58
|
+
ext_inv = ext.inverse()
|
|
59
|
+
|
|
60
|
+
# 多格式输入 -> 外参
|
|
61
|
+
raw = '''
|
|
62
|
+
"translation": {"x": 1, "y": 2, "z": 3},
|
|
63
|
+
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.70710678, "qw": 0.70710678}
|
|
64
|
+
'''
|
|
65
|
+
parsed = parse_transform_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
66
|
+
ext_from_input = transform_from_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
67
|
+
all_formats = transform_to_all_formats(ext_from_input)
|
|
68
|
+
|
|
69
|
+
# 比较与方程求解
|
|
70
|
+
cmp_result = compare_transforms(ext, ext_inv.inverse())
|
|
71
|
+
solve_result = solve_transform_equations(ext, ext_from_input)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 依赖
|
|
75
|
+
|
|
76
|
+
- Python >= 3.10
|
|
77
|
+
- numpy >= 1.20
|
|
78
|
+
- PyYAML >= 6.0
|
|
79
|
+
|
|
80
|
+
## 支持的输入格式
|
|
81
|
+
|
|
82
|
+
- `rotation_matrix_translation`
|
|
83
|
+
- `quaternion_translation`
|
|
84
|
+
- `euler_degree_translation`
|
|
85
|
+
- `homogeneous_matrix_4x4`
|
|
86
|
+
- `json_rotation_translation`
|
|
87
|
+
- `json_rotation_translation_qxyzw`
|
|
88
|
+
- `translation_rotation_quaternion`
|
|
89
|
+
- `rotation_translation_flex`
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# transform-base
|
|
2
|
+
|
|
3
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX),并提供多输入格式解析、矩阵乘法/求逆/比较/方程求解的通用 API。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install transform-base
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
14
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
15
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
16
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
17
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
18
|
+
- **多格式输入解析**:支持旋转矩阵+平移、四元数+平移、欧拉角+平移、4x4 齐次矩阵、JSON/proto/flex 四元数格式
|
|
19
|
+
- **矩阵计算**:支持统一格式输出、矩阵比较、`A = C @ B` / `A = B @ D` 求解
|
|
20
|
+
|
|
21
|
+
## 快速开始
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from transform_base import (
|
|
25
|
+
normalize_angle,
|
|
26
|
+
standard_euler_to_rotation_matrix,
|
|
27
|
+
rotation_matrix_to_standard_euler,
|
|
28
|
+
ExtrinsicParam,
|
|
29
|
+
parse_transform_input,
|
|
30
|
+
transform_from_input,
|
|
31
|
+
transform_to_all_formats,
|
|
32
|
+
compare_transforms,
|
|
33
|
+
solve_transform_equations,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
37
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
38
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
39
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
40
|
+
|
|
41
|
+
# 外参:欧拉角 + 平移
|
|
42
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
43
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
44
|
+
ext_inv = ext.inverse()
|
|
45
|
+
|
|
46
|
+
# 多格式输入 -> 外参
|
|
47
|
+
raw = '''
|
|
48
|
+
"translation": {"x": 1, "y": 2, "z": 3},
|
|
49
|
+
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.70710678, "qw": 0.70710678}
|
|
50
|
+
'''
|
|
51
|
+
parsed = parse_transform_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
52
|
+
ext_from_input = transform_from_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
53
|
+
all_formats = transform_to_all_formats(ext_from_input)
|
|
54
|
+
|
|
55
|
+
# 比较与方程求解
|
|
56
|
+
cmp_result = compare_transforms(ext, ext_inv.inverse())
|
|
57
|
+
solve_result = solve_transform_equations(ext, ext_from_input)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 依赖
|
|
61
|
+
|
|
62
|
+
- Python >= 3.10
|
|
63
|
+
- numpy >= 1.20
|
|
64
|
+
- PyYAML >= 6.0
|
|
65
|
+
|
|
66
|
+
## 支持的输入格式
|
|
67
|
+
|
|
68
|
+
- `rotation_matrix_translation`
|
|
69
|
+
- `quaternion_translation`
|
|
70
|
+
- `euler_degree_translation`
|
|
71
|
+
- `homogeneous_matrix_4x4`
|
|
72
|
+
- `json_rotation_translation`
|
|
73
|
+
- `json_rotation_translation_qxyzw`
|
|
74
|
+
- `translation_rotation_quaternion`
|
|
75
|
+
- `rotation_translation_flex`
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -4,13 +4,14 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "transform-base"
|
|
7
|
-
version = "0.0.
|
|
8
|
-
description = "
|
|
7
|
+
version = "0.0.3"
|
|
8
|
+
description = "欧拉角、旋转矩阵、四元数、角轴及多格式矩阵计算基础库"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"numpy>=1.20",
|
|
14
|
+
"PyYAML>=6.0",
|
|
14
15
|
]
|
|
15
16
|
|
|
16
17
|
[project.optional-dependencies]
|
|
@@ -13,6 +13,16 @@ from transform_base.transform import (
|
|
|
13
13
|
axis_angle_to_standard_euler,
|
|
14
14
|
ExtrinsicParam,
|
|
15
15
|
)
|
|
16
|
+
from transform_base.matrix_ops import (
|
|
17
|
+
SUPPORTED_INPUT_FORMATS,
|
|
18
|
+
compare_transforms,
|
|
19
|
+
detect_transform_input_format,
|
|
20
|
+
parse_transform_input,
|
|
21
|
+
solve_transform_equations,
|
|
22
|
+
transform_from_data,
|
|
23
|
+
transform_from_input,
|
|
24
|
+
transform_to_all_formats,
|
|
25
|
+
)
|
|
16
26
|
|
|
17
27
|
__all__ = [
|
|
18
28
|
"normalize_angle",
|
|
@@ -27,4 +37,12 @@ __all__ = [
|
|
|
27
37
|
"standard_euler_to_axis_angle",
|
|
28
38
|
"axis_angle_to_standard_euler",
|
|
29
39
|
"ExtrinsicParam",
|
|
40
|
+
"SUPPORTED_INPUT_FORMATS",
|
|
41
|
+
"detect_transform_input_format",
|
|
42
|
+
"parse_transform_input",
|
|
43
|
+
"transform_from_data",
|
|
44
|
+
"transform_from_input",
|
|
45
|
+
"transform_to_all_formats",
|
|
46
|
+
"compare_transforms",
|
|
47
|
+
"solve_transform_equations",
|
|
30
48
|
]
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import Any, Callable, Dict, Iterable, Tuple
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .transform import ExtrinsicParam, normalize_angle
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
SUPPORTED_INPUT_FORMATS: tuple[str, ...] = (
|
|
14
|
+
"rotation_matrix_translation",
|
|
15
|
+
"quaternion_translation",
|
|
16
|
+
"euler_degree_translation",
|
|
17
|
+
"homogeneous_matrix_4x4",
|
|
18
|
+
"json_rotation_translation",
|
|
19
|
+
"json_rotation_translation_qxyzw",
|
|
20
|
+
"translation_rotation_quaternion",
|
|
21
|
+
"rotation_translation_flex",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_FLOAT_RE = r"([-\d.eE+]+)"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def compact_whitespace(text: str) -> str:
|
|
29
|
+
"""Collapse whitespace outside of quoted strings."""
|
|
30
|
+
result: list[str] = []
|
|
31
|
+
in_string = False
|
|
32
|
+
escape = False
|
|
33
|
+
quote = ""
|
|
34
|
+
for c in text:
|
|
35
|
+
if escape:
|
|
36
|
+
result.append(c)
|
|
37
|
+
escape = False
|
|
38
|
+
continue
|
|
39
|
+
if c == "\\" and in_string:
|
|
40
|
+
result.append(c)
|
|
41
|
+
escape = True
|
|
42
|
+
continue
|
|
43
|
+
if c in ("'", '"'):
|
|
44
|
+
if not in_string:
|
|
45
|
+
in_string = True
|
|
46
|
+
quote = c
|
|
47
|
+
elif quote == c:
|
|
48
|
+
in_string = False
|
|
49
|
+
quote = ""
|
|
50
|
+
result.append(c)
|
|
51
|
+
continue
|
|
52
|
+
if in_string:
|
|
53
|
+
result.append(c)
|
|
54
|
+
continue
|
|
55
|
+
if c in " \t\r\n":
|
|
56
|
+
if result and result[-1] != " ":
|
|
57
|
+
result.append(" ")
|
|
58
|
+
continue
|
|
59
|
+
result.append(c)
|
|
60
|
+
return "".join(result).strip()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _prepare_text(raw: str, filter_mode: bool) -> str:
|
|
64
|
+
text = (raw or "").strip()
|
|
65
|
+
if not text:
|
|
66
|
+
return text
|
|
67
|
+
return compact_whitespace(text) if filter_mode else text
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _coerce_float_list(value: Any, expected_len: int | None, field_name: str) -> list[float]:
|
|
71
|
+
if not isinstance(value, (list, tuple)):
|
|
72
|
+
raise ValueError(f"{field_name} must be a list/tuple")
|
|
73
|
+
out = [float(np.float64(item)) for item in value]
|
|
74
|
+
if expected_len is not None and len(out) != expected_len:
|
|
75
|
+
raise ValueError(f"{field_name} must contain {expected_len} values")
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _coerce_float_matrix(
|
|
80
|
+
value: Any,
|
|
81
|
+
rows: int,
|
|
82
|
+
cols: int,
|
|
83
|
+
field_name: str,
|
|
84
|
+
) -> list[list[float]]:
|
|
85
|
+
if not isinstance(value, (list, tuple)) or len(value) != rows:
|
|
86
|
+
raise ValueError(f"{field_name} must be a {rows}x{cols} matrix")
|
|
87
|
+
out: list[list[float]] = []
|
|
88
|
+
for row in value:
|
|
89
|
+
if not isinstance(row, (list, tuple)) or len(row) != cols:
|
|
90
|
+
raise ValueError(f"{field_name} must be a {rows}x{cols} matrix")
|
|
91
|
+
out.append([float(np.float64(item)) for item in row])
|
|
92
|
+
return out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _find_first(data: Dict[str, Any], names: Iterable[str]) -> Any:
|
|
96
|
+
for name in names:
|
|
97
|
+
if name in data:
|
|
98
|
+
return data[name]
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _parse_yaml_object(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
103
|
+
candidates: list[str] = []
|
|
104
|
+
stripped = (raw or "").strip()
|
|
105
|
+
if stripped:
|
|
106
|
+
candidates.append(stripped)
|
|
107
|
+
compacted = _prepare_text(raw, filter_mode)
|
|
108
|
+
if compacted and compacted not in candidates:
|
|
109
|
+
candidates.append(compacted)
|
|
110
|
+
if not candidates:
|
|
111
|
+
raise ValueError("input is empty")
|
|
112
|
+
|
|
113
|
+
last_error: Exception | None = None
|
|
114
|
+
for text in candidates:
|
|
115
|
+
try:
|
|
116
|
+
data = yaml.safe_load(text)
|
|
117
|
+
except yaml.YAMLError as exc:
|
|
118
|
+
last_error = exc
|
|
119
|
+
continue
|
|
120
|
+
if isinstance(data, dict):
|
|
121
|
+
return data
|
|
122
|
+
last_error = ValueError("input root must be a mapping")
|
|
123
|
+
raise ValueError(f"invalid YAML/JSON input: {last_error}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _wrap_json_fragment(text: str) -> str:
|
|
127
|
+
s = text.strip()
|
|
128
|
+
if not s:
|
|
129
|
+
return "{}"
|
|
130
|
+
if s.startswith("{"):
|
|
131
|
+
return s
|
|
132
|
+
return "{" + s + "}"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_json_object(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
136
|
+
text = _wrap_json_fragment(_prepare_text(raw, filter_mode))
|
|
137
|
+
try:
|
|
138
|
+
data = json.loads(text)
|
|
139
|
+
except json.JSONDecodeError as exc:
|
|
140
|
+
raise ValueError(f"invalid JSON input: {exc}") from exc
|
|
141
|
+
if not isinstance(data, dict):
|
|
142
|
+
raise ValueError("JSON root must be an object")
|
|
143
|
+
return data
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _get_float(d: Dict[str, Any], key: str, default: float = 0.0) -> float:
|
|
147
|
+
if key not in d or d.get(key) is None:
|
|
148
|
+
return default
|
|
149
|
+
try:
|
|
150
|
+
value = float(np.float64(d[key]))
|
|
151
|
+
except (TypeError, ValueError) as exc:
|
|
152
|
+
raise ValueError(f"field {key} is not a valid float") from exc
|
|
153
|
+
if not np.isfinite(value):
|
|
154
|
+
raise ValueError(f"field {key} is not finite")
|
|
155
|
+
return value
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_rotation_matrix_translation(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
159
|
+
try:
|
|
160
|
+
data = _parse_yaml_object(raw, filter_mode)
|
|
161
|
+
rotation = _find_first(data, ("rotation_matrix", "R", "rotation"))
|
|
162
|
+
translation = _find_first(data, ("translation", "t"))
|
|
163
|
+
if rotation is not None and translation is not None:
|
|
164
|
+
return {
|
|
165
|
+
"rotation_matrix": _coerce_float_matrix(rotation, 3, 3, "rotation_matrix"),
|
|
166
|
+
"translation": _coerce_float_list(translation, 3, "translation"),
|
|
167
|
+
}
|
|
168
|
+
except ValueError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
row_pattern = re.compile(r"\[\s*([-\d.\s+eE]+)\s*\]")
|
|
172
|
+
r_match = re.search(r"\bR\s*:\s*", raw, re.IGNORECASE)
|
|
173
|
+
t_match = re.search(r"\bt\s*:\s*", raw, re.IGNORECASE)
|
|
174
|
+
if r_match is None or t_match is None or t_match.start() <= r_match.end():
|
|
175
|
+
raise ValueError("expected YAML/JSON rotation_matrix+translation or R:/t: matrix blocks")
|
|
176
|
+
|
|
177
|
+
r_block = raw[r_match.end():t_match.start()].strip()
|
|
178
|
+
t_block = raw[t_match.end():].strip()
|
|
179
|
+
r_rows = row_pattern.findall(r_block)
|
|
180
|
+
t_rows = row_pattern.findall(t_block)
|
|
181
|
+
if len(r_rows) != 3:
|
|
182
|
+
raise ValueError("R block must contain 3 rows")
|
|
183
|
+
rotation_matrix = [[float(np.float64(v)) for v in row.split()] for row in r_rows]
|
|
184
|
+
if any(len(row) != 3 for row in rotation_matrix):
|
|
185
|
+
raise ValueError("each R row must contain 3 values")
|
|
186
|
+
translation = [float(np.float64(v)) for row in t_rows for v in row.split()]
|
|
187
|
+
if len(translation) != 3:
|
|
188
|
+
raise ValueError("t block must contain 3 values")
|
|
189
|
+
return {"rotation_matrix": rotation_matrix, "translation": translation}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _parse_quaternion_translation(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
193
|
+
data = _parse_yaml_object(raw, filter_mode)
|
|
194
|
+
quaternion = _find_first(data, ("quaternion_xyzw", "quaternion", "quat", "q"))
|
|
195
|
+
translation = _find_first(data, ("translation", "t"))
|
|
196
|
+
if quaternion is None or translation is None:
|
|
197
|
+
raise ValueError("expected quaternion_xyzw/quaternion/quat/q and translation/t")
|
|
198
|
+
return {
|
|
199
|
+
"quaternion_xyzw": _coerce_float_list(quaternion, 4, "quaternion_xyzw"),
|
|
200
|
+
"translation": _coerce_float_list(translation, 3, "translation"),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _parse_euler_degree_translation(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
205
|
+
data = _parse_yaml_object(raw, filter_mode)
|
|
206
|
+
euler = _find_first(data, ("euler_degree_xyz", "euler_degree", "euler_deg"))
|
|
207
|
+
translation = _find_first(data, ("translation", "t"))
|
|
208
|
+
order = _find_first(data, ("euler_cal_order", "order", "euler_order")) or "ZYX"
|
|
209
|
+
if euler is None or translation is None:
|
|
210
|
+
raise ValueError("expected euler_degree_xyz/euler_degree/euler_deg and translation/t")
|
|
211
|
+
return {
|
|
212
|
+
"euler_degree_xyz": _coerce_float_list(euler, 3, "euler_degree_xyz"),
|
|
213
|
+
"translation": _coerce_float_list(translation, 3, "translation"),
|
|
214
|
+
"euler_cal_order": str(order),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_homogeneous_matrix_4x4(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
219
|
+
data = _parse_yaml_object(raw, filter_mode)
|
|
220
|
+
matrix = _find_first(data, ("homogeneous_matrix_4x4", "T", "extrinsic", "pose"))
|
|
221
|
+
if matrix is None:
|
|
222
|
+
raise ValueError("expected homogeneous_matrix_4x4/T/extrinsic/pose")
|
|
223
|
+
return {
|
|
224
|
+
"homogeneous_matrix_4x4": _coerce_float_matrix(matrix, 4, 4, "homogeneous_matrix_4x4"),
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _translation_from_dict(trans: Dict[str, Any]) -> list[float]:
|
|
229
|
+
return [_get_float(trans, "x"), _get_float(trans, "y"), _get_float(trans, "z")]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_json_rotation_translation(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
233
|
+
obj = _load_json_object(raw, filter_mode)
|
|
234
|
+
rotation = obj.get("rotation")
|
|
235
|
+
translation = obj.get("translation")
|
|
236
|
+
if not isinstance(rotation, dict) or not isinstance(translation, dict):
|
|
237
|
+
raise ValueError("expected rotation and translation JSON objects")
|
|
238
|
+
if any(key in rotation for key in ("qx", "qy", "qz", "qw")):
|
|
239
|
+
raise ValueError("rotation uses qx/qy/qz/qw; use json_rotation_translation_qxyzw")
|
|
240
|
+
quat = [
|
|
241
|
+
_get_float(rotation, "x"),
|
|
242
|
+
_get_float(rotation, "y"),
|
|
243
|
+
_get_float(rotation, "z"),
|
|
244
|
+
_get_float(rotation, "w"),
|
|
245
|
+
]
|
|
246
|
+
return {"quaternion_xyzw": quat, "translation": _translation_from_dict(translation)}
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _parse_json_rotation_translation_qxyzw(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
250
|
+
obj = _load_json_object(raw, filter_mode)
|
|
251
|
+
rotation = obj.get("rotation")
|
|
252
|
+
translation = obj.get("translation")
|
|
253
|
+
if not isinstance(rotation, dict) or not isinstance(translation, dict):
|
|
254
|
+
raise ValueError("expected rotation and translation JSON objects")
|
|
255
|
+
quat = [
|
|
256
|
+
_get_float(rotation, "qx"),
|
|
257
|
+
_get_float(rotation, "qy"),
|
|
258
|
+
_get_float(rotation, "qz"),
|
|
259
|
+
_get_float(rotation, "qw"),
|
|
260
|
+
]
|
|
261
|
+
return {"quaternion_xyzw": quat, "translation": _translation_from_dict(translation)}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _extract_proto_translation(text: str) -> list[float] | None:
|
|
265
|
+
match = re.search(r"translation\s*\{([^}]+)\}", text, re.DOTALL | re.IGNORECASE)
|
|
266
|
+
if match is None:
|
|
267
|
+
return None
|
|
268
|
+
block = match.group(1)
|
|
269
|
+
x_match = re.search(r"x\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
270
|
+
y_match = re.search(r"y\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
271
|
+
z_match = re.search(r"z\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
272
|
+
if not (x_match and y_match and z_match):
|
|
273
|
+
return None
|
|
274
|
+
return [
|
|
275
|
+
float(np.float64(x_match.group(1))),
|
|
276
|
+
float(np.float64(y_match.group(1))),
|
|
277
|
+
float(np.float64(z_match.group(1))),
|
|
278
|
+
]
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _extract_proto_rotation(text: str) -> list[float] | None:
|
|
282
|
+
match = re.search(r"rotation\s*\{([^}]+)\}", text, re.DOTALL | re.IGNORECASE)
|
|
283
|
+
if match is None:
|
|
284
|
+
return None
|
|
285
|
+
block = match.group(1)
|
|
286
|
+
qx_match = re.search(r"qx\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
287
|
+
qy_match = re.search(r"qy\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
288
|
+
qz_match = re.search(r"qz\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
289
|
+
qw_match = re.search(r"qw\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
290
|
+
if qx_match and qy_match and qz_match and qw_match:
|
|
291
|
+
return [
|
|
292
|
+
float(np.float64(qx_match.group(1))),
|
|
293
|
+
float(np.float64(qy_match.group(1))),
|
|
294
|
+
float(np.float64(qz_match.group(1))),
|
|
295
|
+
float(np.float64(qw_match.group(1))),
|
|
296
|
+
]
|
|
297
|
+
w_match = re.search(r"w\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
298
|
+
x_match = re.search(r"x\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
299
|
+
y_match = re.search(r"y\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
300
|
+
z_match = re.search(r"z\s*:\s*" + _FLOAT_RE, block, re.IGNORECASE)
|
|
301
|
+
if not (w_match and x_match and y_match and z_match):
|
|
302
|
+
return None
|
|
303
|
+
return [
|
|
304
|
+
float(np.float64(x_match.group(1))),
|
|
305
|
+
float(np.float64(y_match.group(1))),
|
|
306
|
+
float(np.float64(z_match.group(1))),
|
|
307
|
+
float(np.float64(w_match.group(1))),
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _parse_translation_rotation_quaternion(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
312
|
+
text = _prepare_text(raw, filter_mode)
|
|
313
|
+
translation = _extract_proto_translation(text)
|
|
314
|
+
rotation = _extract_proto_rotation(text)
|
|
315
|
+
if translation is None or rotation is None:
|
|
316
|
+
raise ValueError("expected proto style translation {x,y,z} and rotation {qx,qy,qz,qw}")
|
|
317
|
+
return {"quaternion_xyzw": rotation, "translation": translation}
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _parse_rotation_translation_flex(raw: str, filter_mode: bool) -> Dict[str, Any]:
|
|
321
|
+
try:
|
|
322
|
+
obj = _load_json_object(raw, filter_mode)
|
|
323
|
+
rotation = obj.get("rotation")
|
|
324
|
+
translation = obj.get("translation")
|
|
325
|
+
if isinstance(rotation, dict) and isinstance(translation, dict):
|
|
326
|
+
if any(key in rotation for key in ("qx", "qy", "qz", "qw")):
|
|
327
|
+
quat = [
|
|
328
|
+
_get_float(rotation, "qx"),
|
|
329
|
+
_get_float(rotation, "qy"),
|
|
330
|
+
_get_float(rotation, "qz"),
|
|
331
|
+
_get_float(rotation, "qw"),
|
|
332
|
+
]
|
|
333
|
+
elif any(key in rotation for key in ("w", "x", "y", "z")):
|
|
334
|
+
quat = [
|
|
335
|
+
_get_float(rotation, "x"),
|
|
336
|
+
_get_float(rotation, "y"),
|
|
337
|
+
_get_float(rotation, "z"),
|
|
338
|
+
_get_float(rotation, "w"),
|
|
339
|
+
]
|
|
340
|
+
else:
|
|
341
|
+
raise ValueError("rotation JSON object uses unsupported keys")
|
|
342
|
+
return {"quaternion_xyzw": quat, "translation": _translation_from_dict(translation)}
|
|
343
|
+
except ValueError:
|
|
344
|
+
pass
|
|
345
|
+
return _parse_translation_rotation_quaternion(raw, filter_mode)
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
_PARSERS: dict[str, Callable[[str, bool], Dict[str, Any]]] = {
|
|
349
|
+
"rotation_matrix_translation": _parse_rotation_matrix_translation,
|
|
350
|
+
"quaternion_translation": _parse_quaternion_translation,
|
|
351
|
+
"euler_degree_translation": _parse_euler_degree_translation,
|
|
352
|
+
"homogeneous_matrix_4x4": _parse_homogeneous_matrix_4x4,
|
|
353
|
+
"json_rotation_translation": _parse_json_rotation_translation,
|
|
354
|
+
"json_rotation_translation_qxyzw": _parse_json_rotation_translation_qxyzw,
|
|
355
|
+
"translation_rotation_quaternion": _parse_translation_rotation_quaternion,
|
|
356
|
+
"rotation_translation_flex": _parse_rotation_translation_flex,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def detect_transform_input_format(raw: str, filter_mode: bool = True) -> str:
|
|
361
|
+
for format_name in SUPPORTED_INPUT_FORMATS:
|
|
362
|
+
try:
|
|
363
|
+
_PARSERS[format_name](raw, filter_mode)
|
|
364
|
+
return format_name
|
|
365
|
+
except ValueError:
|
|
366
|
+
continue
|
|
367
|
+
raise ValueError("unable to detect a supported input format")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def parse_transform_input(
|
|
371
|
+
raw: str,
|
|
372
|
+
format_name: str | None = None,
|
|
373
|
+
filter_mode: bool = True,
|
|
374
|
+
) -> Dict[str, Any]:
|
|
375
|
+
if format_name is None:
|
|
376
|
+
format_name = detect_transform_input_format(raw, filter_mode=filter_mode)
|
|
377
|
+
parser = _PARSERS.get(format_name)
|
|
378
|
+
if parser is None:
|
|
379
|
+
raise ValueError(
|
|
380
|
+
f"unsupported format_name: {format_name}. "
|
|
381
|
+
f"supported={', '.join(SUPPORTED_INPUT_FORMATS)}"
|
|
382
|
+
)
|
|
383
|
+
return parser(raw, filter_mode)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def transform_from_data(
|
|
387
|
+
data: Dict[str, Any],
|
|
388
|
+
euler_order: str | None = None,
|
|
389
|
+
) -> ExtrinsicParam:
|
|
390
|
+
translation = _find_first(data, ("translation", "t"))
|
|
391
|
+
translation_vec = (
|
|
392
|
+
_coerce_float_list(translation, 3, "translation") if translation is not None else [0.0, 0.0, 0.0]
|
|
393
|
+
)
|
|
394
|
+
resolved_order = str(
|
|
395
|
+
euler_order
|
|
396
|
+
or _find_first(data, ("euler_cal_order", "order", "euler_order"))
|
|
397
|
+
or "ZYX"
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
if _find_first(data, ("rotation_matrix", "R", "rotation")) is not None:
|
|
401
|
+
rotation_matrix = _coerce_float_matrix(
|
|
402
|
+
_find_first(data, ("rotation_matrix", "R", "rotation")),
|
|
403
|
+
3,
|
|
404
|
+
3,
|
|
405
|
+
"rotation_matrix",
|
|
406
|
+
)
|
|
407
|
+
return ExtrinsicParam(rotation_matrix=rotation_matrix, translation=translation_vec, euler_order=resolved_order)
|
|
408
|
+
|
|
409
|
+
if _find_first(data, ("quaternion_xyzw", "quaternion", "quat", "q")) is not None:
|
|
410
|
+
quaternion = _coerce_float_list(
|
|
411
|
+
_find_first(data, ("quaternion_xyzw", "quaternion", "quat", "q")),
|
|
412
|
+
4,
|
|
413
|
+
"quaternion_xyzw",
|
|
414
|
+
)
|
|
415
|
+
return ExtrinsicParam(quaternion=quaternion, translation=translation_vec, euler_order=resolved_order)
|
|
416
|
+
|
|
417
|
+
if _find_first(data, ("euler_degree_xyz", "euler_degree", "euler_deg")) is not None:
|
|
418
|
+
euler = _coerce_float_list(
|
|
419
|
+
_find_first(data, ("euler_degree_xyz", "euler_degree", "euler_deg")),
|
|
420
|
+
3,
|
|
421
|
+
"euler_degree_xyz",
|
|
422
|
+
)
|
|
423
|
+
return ExtrinsicParam(euler=euler, translation=translation_vec, euler_order=resolved_order)
|
|
424
|
+
|
|
425
|
+
if _find_first(data, ("homogeneous_matrix_4x4", "T", "extrinsic", "pose")) is not None:
|
|
426
|
+
matrix = _coerce_float_matrix(
|
|
427
|
+
_find_first(data, ("homogeneous_matrix_4x4", "T", "extrinsic", "pose")),
|
|
428
|
+
4,
|
|
429
|
+
4,
|
|
430
|
+
"homogeneous_matrix_4x4",
|
|
431
|
+
)
|
|
432
|
+
return ExtrinsicParam.from_homogeneous_matrix(np.array(matrix, dtype=float), euler_order=resolved_order)
|
|
433
|
+
|
|
434
|
+
raise ValueError("input data does not contain a supported transform representation")
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def transform_from_input(
|
|
438
|
+
raw: str,
|
|
439
|
+
format_name: str | None = None,
|
|
440
|
+
filter_mode: bool = True,
|
|
441
|
+
euler_order: str | None = None,
|
|
442
|
+
) -> ExtrinsicParam:
|
|
443
|
+
data = parse_transform_input(raw, format_name=format_name, filter_mode=filter_mode)
|
|
444
|
+
return transform_from_data(data, euler_order=euler_order)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def transform_to_all_formats(ext: ExtrinsicParam) -> Dict[str, Any]:
|
|
448
|
+
axis, angle_rad = ext.get_axis_angle()
|
|
449
|
+
return {
|
|
450
|
+
"rotation_matrix": ext.rotation_matrix.tolist(),
|
|
451
|
+
"quaternion_xyzw": ext.get_quaternion().tolist(),
|
|
452
|
+
"euler_degree_xyz": ext.get_standard_euler().tolist(),
|
|
453
|
+
"euler_cal_order": getattr(ext, "euler_order", "ZYX"),
|
|
454
|
+
"translation": ext.translation.tolist(),
|
|
455
|
+
"axis_angle": {
|
|
456
|
+
"axis": axis.tolist(),
|
|
457
|
+
"angle_rad": float(angle_rad),
|
|
458
|
+
"angle_deg": float(normalize_angle(np.rad2deg(angle_rad))),
|
|
459
|
+
},
|
|
460
|
+
"homogeneous_matrix_4x4": ext.get_homogeneous_matrix().tolist(),
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def compare_transforms(
|
|
465
|
+
a: ExtrinsicParam,
|
|
466
|
+
b: ExtrinsicParam,
|
|
467
|
+
tolerance: float = 1e-6,
|
|
468
|
+
rotation_angle_tolerance: float = 1e-6,
|
|
469
|
+
) -> Dict[str, Any]:
|
|
470
|
+
r_rel = a.rotation_matrix @ b.rotation_matrix.T
|
|
471
|
+
trace_r = np.clip(np.trace(r_rel), -1.0, 3.0)
|
|
472
|
+
cos_theta = np.clip((trace_r - 1.0) / 2.0, -1.0, 1.0)
|
|
473
|
+
rotation_angle_diff_rad = float(np.arccos(cos_theta))
|
|
474
|
+
rotation_angle_diff_deg = float(np.rad2deg(rotation_angle_diff_rad))
|
|
475
|
+
rotation_element_max_diff = float(np.max(np.abs(a.rotation_matrix - b.rotation_matrix)))
|
|
476
|
+
translation_max_diff = float(np.max(np.abs(a.translation - b.translation)))
|
|
477
|
+
translation_norm = float(np.linalg.norm(a.translation))
|
|
478
|
+
translation_relative_diff = (
|
|
479
|
+
translation_max_diff / (translation_norm + 1e-10) if translation_norm > 1e-10 else 0.0
|
|
480
|
+
)
|
|
481
|
+
homogeneous_matrix_max_diff = float(
|
|
482
|
+
np.max(np.abs(a.get_homogeneous_matrix() - b.get_homogeneous_matrix()))
|
|
483
|
+
)
|
|
484
|
+
rotation_equal = rotation_angle_diff_rad <= rotation_angle_tolerance
|
|
485
|
+
translation_equal = translation_max_diff <= tolerance
|
|
486
|
+
return {
|
|
487
|
+
"is_equal": bool(rotation_equal and translation_equal),
|
|
488
|
+
"rotation_element_max_diff": rotation_element_max_diff,
|
|
489
|
+
"rotation_angle_diff_rad": rotation_angle_diff_rad,
|
|
490
|
+
"rotation_angle_diff_deg": rotation_angle_diff_deg,
|
|
491
|
+
"translation_max_diff": translation_max_diff,
|
|
492
|
+
"translation_relative_diff": float(translation_relative_diff),
|
|
493
|
+
"homogeneous_matrix_max_diff": homogeneous_matrix_max_diff,
|
|
494
|
+
"tolerance": float(tolerance),
|
|
495
|
+
"rotation_angle_tolerance_rad": float(rotation_angle_tolerance),
|
|
496
|
+
"rotation_equal": bool(rotation_equal),
|
|
497
|
+
"translation_equal": bool(translation_equal),
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def solve_transform_equations(
|
|
502
|
+
a: ExtrinsicParam,
|
|
503
|
+
b: ExtrinsicParam,
|
|
504
|
+
tolerance: float = 1e-6,
|
|
505
|
+
rotation_angle_tolerance: float = 1e-6,
|
|
506
|
+
) -> Dict[str, Any]:
|
|
507
|
+
b_inverse = b.inverse()
|
|
508
|
+
c = a @ b_inverse
|
|
509
|
+
d = b_inverse @ a
|
|
510
|
+
verify_c = compare_transforms(c @ b, a, tolerance=tolerance, rotation_angle_tolerance=rotation_angle_tolerance)
|
|
511
|
+
verify_d = compare_transforms(b @ d, a, tolerance=tolerance, rotation_angle_tolerance=rotation_angle_tolerance)
|
|
512
|
+
return {
|
|
513
|
+
"b_inverse": transform_to_all_formats(b_inverse),
|
|
514
|
+
"equation_1": {
|
|
515
|
+
"formula": "A = C @ B",
|
|
516
|
+
"solution": "C = A @ B^-1",
|
|
517
|
+
"C_matrix": transform_to_all_formats(c),
|
|
518
|
+
"verification": {
|
|
519
|
+
"formula": "C @ B",
|
|
520
|
+
"equals_A": bool(verify_c["is_equal"]),
|
|
521
|
+
"details": verify_c,
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
"equation_2": {
|
|
525
|
+
"formula": "A = B @ D",
|
|
526
|
+
"solution": "D = B^-1 @ A",
|
|
527
|
+
"D_matrix": transform_to_all_formats(d),
|
|
528
|
+
"verification": {
|
|
529
|
+
"formula": "B @ D",
|
|
530
|
+
"equals_A": bool(verify_d["is_equal"]),
|
|
531
|
+
"details": verify_d,
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: transform-base
|
|
3
|
+
Version: 0.0.3
|
|
4
|
+
Summary: 欧拉角、旋转矩阵、四元数、角轴及多格式矩阵计算基础库
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20
|
|
9
|
+
Requires-Dist: PyYAML>=6.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest; extra == "dev"
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
13
|
+
Requires-Dist: twine; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# transform-base
|
|
16
|
+
|
|
17
|
+
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX),并提供多输入格式解析、矩阵乘法/求逆/比较/方程求解的通用 API。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install transform-base
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 功能特性
|
|
26
|
+
|
|
27
|
+
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
28
|
+
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
29
|
+
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
30
|
+
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
31
|
+
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
32
|
+
- **多格式输入解析**:支持旋转矩阵+平移、四元数+平移、欧拉角+平移、4x4 齐次矩阵、JSON/proto/flex 四元数格式
|
|
33
|
+
- **矩阵计算**:支持统一格式输出、矩阵比较、`A = C @ B` / `A = B @ D` 求解
|
|
34
|
+
|
|
35
|
+
## 快速开始
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from transform_base import (
|
|
39
|
+
normalize_angle,
|
|
40
|
+
standard_euler_to_rotation_matrix,
|
|
41
|
+
rotation_matrix_to_standard_euler,
|
|
42
|
+
ExtrinsicParam,
|
|
43
|
+
parse_transform_input,
|
|
44
|
+
transform_from_input,
|
|
45
|
+
transform_to_all_formats,
|
|
46
|
+
compare_transforms,
|
|
47
|
+
solve_transform_equations,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
51
|
+
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
52
|
+
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
53
|
+
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
54
|
+
|
|
55
|
+
# 外参:欧拉角 + 平移
|
|
56
|
+
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
57
|
+
p_out = ext.transform_point([1, 0, 0])
|
|
58
|
+
ext_inv = ext.inverse()
|
|
59
|
+
|
|
60
|
+
# 多格式输入 -> 外参
|
|
61
|
+
raw = '''
|
|
62
|
+
"translation": {"x": 1, "y": 2, "z": 3},
|
|
63
|
+
"rotation": {"qx": 0.0, "qy": 0.0, "qz": 0.70710678, "qw": 0.70710678}
|
|
64
|
+
'''
|
|
65
|
+
parsed = parse_transform_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
66
|
+
ext_from_input = transform_from_input(raw, format_name="json_rotation_translation_qxyzw")
|
|
67
|
+
all_formats = transform_to_all_formats(ext_from_input)
|
|
68
|
+
|
|
69
|
+
# 比较与方程求解
|
|
70
|
+
cmp_result = compare_transforms(ext, ext_inv.inverse())
|
|
71
|
+
solve_result = solve_transform_equations(ext, ext_from_input)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 依赖
|
|
75
|
+
|
|
76
|
+
- Python >= 3.10
|
|
77
|
+
- numpy >= 1.20
|
|
78
|
+
- PyYAML >= 6.0
|
|
79
|
+
|
|
80
|
+
## 支持的输入格式
|
|
81
|
+
|
|
82
|
+
- `rotation_matrix_translation`
|
|
83
|
+
- `quaternion_translation`
|
|
84
|
+
- `euler_degree_translation`
|
|
85
|
+
- `homogeneous_matrix_4x4`
|
|
86
|
+
- `json_rotation_translation`
|
|
87
|
+
- `json_rotation_translation_qxyzw`
|
|
88
|
+
- `translation_rotation_quaternion`
|
|
89
|
+
- `rotation_translation_flex`
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT
|
transform_base-0.0.2/PKG-INFO
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: transform-base
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: 欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: numpy>=1.20
|
|
9
|
-
Provides-Extra: dev
|
|
10
|
-
Requires-Dist: pytest; extra == "dev"
|
|
11
|
-
Requires-Dist: build; extra == "dev"
|
|
12
|
-
Requires-Dist: twine; extra == "dev"
|
|
13
|
-
|
|
14
|
-
# transform-base
|
|
15
|
-
|
|
16
|
-
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
17
|
-
|
|
18
|
-
## 安装
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install transform-base
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## 功能特性
|
|
25
|
-
|
|
26
|
-
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
27
|
-
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
28
|
-
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
29
|
-
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
30
|
-
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
31
|
-
|
|
32
|
-
## 快速开始
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
from transform_base import (
|
|
36
|
-
normalize_angle,
|
|
37
|
-
standard_euler_to_rotation_matrix,
|
|
38
|
-
rotation_matrix_to_standard_euler,
|
|
39
|
-
ExtrinsicParam,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
43
|
-
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
44
|
-
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
45
|
-
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
46
|
-
|
|
47
|
-
# 外参:欧拉角 + 平移
|
|
48
|
-
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
49
|
-
p_out = ext.transform_point([1, 0, 0])
|
|
50
|
-
ext_inv = ext.inverse()
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## 依赖
|
|
54
|
-
|
|
55
|
-
- Python >= 3.10
|
|
56
|
-
- numpy >= 1.20
|
|
57
|
-
|
|
58
|
-
## License
|
|
59
|
-
|
|
60
|
-
MIT
|
transform_base-0.0.2/README.md
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# transform-base
|
|
2
|
-
|
|
3
|
-
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
4
|
-
|
|
5
|
-
## 安装
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
pip install transform-base
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## 功能特性
|
|
12
|
-
|
|
13
|
-
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
14
|
-
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
15
|
-
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
16
|
-
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
17
|
-
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
18
|
-
|
|
19
|
-
## 快速开始
|
|
20
|
-
|
|
21
|
-
```python
|
|
22
|
-
from transform_base import (
|
|
23
|
-
normalize_angle,
|
|
24
|
-
standard_euler_to_rotation_matrix,
|
|
25
|
-
rotation_matrix_to_standard_euler,
|
|
26
|
-
ExtrinsicParam,
|
|
27
|
-
)
|
|
28
|
-
|
|
29
|
-
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
30
|
-
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
31
|
-
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
32
|
-
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
33
|
-
|
|
34
|
-
# 外参:欧拉角 + 平移
|
|
35
|
-
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
36
|
-
p_out = ext.transform_point([1, 0, 0])
|
|
37
|
-
ext_inv = ext.inverse()
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
## 依赖
|
|
41
|
-
|
|
42
|
-
- Python >= 3.10
|
|
43
|
-
- numpy >= 1.20
|
|
44
|
-
|
|
45
|
-
## License
|
|
46
|
-
|
|
47
|
-
MIT
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: transform-base
|
|
3
|
-
Version: 0.0.2
|
|
4
|
-
Summary: 欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
Requires-Dist: numpy>=1.20
|
|
9
|
-
Provides-Extra: dev
|
|
10
|
-
Requires-Dist: pytest; extra == "dev"
|
|
11
|
-
Requires-Dist: build; extra == "dev"
|
|
12
|
-
Requires-Dist: twine; extra == "dev"
|
|
13
|
-
|
|
14
|
-
# transform-base
|
|
15
|
-
|
|
16
|
-
欧拉角、旋转矩阵、四元数、角轴及外参变换基础库,支持多种旋转顺序(XYZ/XZY/YXZ/YZX/ZXY/ZYX)。
|
|
17
|
-
|
|
18
|
-
## 安装
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
pip install transform-base
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## 功能特性
|
|
25
|
-
|
|
26
|
-
- **角度归一化**:`normalize_angle` 将角度归一化到 [-180, 180]
|
|
27
|
-
- **欧拉角 ↔ 旋转矩阵**:支持 6 种旋转顺序(order),默认 ZYX
|
|
28
|
-
- **旋转矩阵 ↔ 四元数**:`[x, y, z, w]` 格式
|
|
29
|
-
- **旋转矩阵 ↔ 角轴**:Rodrigues 公式
|
|
30
|
-
- **外参类 ExtrinsicParam**:欧拉/旋转矩阵/四元数/角轴多种初始化,齐次矩阵、点变换、复合与逆
|
|
31
|
-
|
|
32
|
-
## 快速开始
|
|
33
|
-
|
|
34
|
-
```python
|
|
35
|
-
from transform_base import (
|
|
36
|
-
normalize_angle,
|
|
37
|
-
standard_euler_to_rotation_matrix,
|
|
38
|
-
rotation_matrix_to_standard_euler,
|
|
39
|
-
ExtrinsicParam,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
# 欧拉角 -> 旋转矩阵 -> 欧拉角
|
|
43
|
-
euler = [10.0, 20.0, 30.0] # roll, pitch, yaw (度)
|
|
44
|
-
R = standard_euler_to_rotation_matrix(*euler, order="ZYX")
|
|
45
|
-
euler_back = rotation_matrix_to_standard_euler(R, order="ZYX")
|
|
46
|
-
|
|
47
|
-
# 外参:欧拉角 + 平移
|
|
48
|
-
ext = ExtrinsicParam(euler=[0, 0, 90], translation=[1, 0, 0])
|
|
49
|
-
p_out = ext.transform_point([1, 0, 0])
|
|
50
|
-
ext_inv = ext.inverse()
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## 依赖
|
|
54
|
-
|
|
55
|
-
- Python >= 3.10
|
|
56
|
-
- numpy >= 1.20
|
|
57
|
-
|
|
58
|
-
## License
|
|
59
|
-
|
|
60
|
-
MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|