tftree-manager 0.0.1__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.
- tftree_manager-0.0.1/MANIFEST.in +20 -0
- tftree_manager-0.0.1/PKG-INFO +79 -0
- tftree_manager-0.0.1/README.md +64 -0
- tftree_manager-0.0.1/pyproject.toml +42 -0
- tftree_manager-0.0.1/setup.cfg +4 -0
- tftree_manager-0.0.1/tftree_manager/__init__.py +20 -0
- tftree_manager-0.0.1/tftree_manager/app.py +162 -0
- tftree_manager-0.0.1/tftree_manager/graph_ops.py +81 -0
- tftree_manager-0.0.1/tftree_manager/manager.py +161 -0
- tftree_manager-0.0.1/tftree_manager/tftree_errors.py +35 -0
- tftree_manager-0.0.1/tftree_manager/transform.py +96 -0
- tftree_manager-0.0.1/tftree_manager/transform_math.py +57 -0
- tftree_manager-0.0.1/tftree_manager/yaml_io.py +100 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/PKG-INFO +79 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/SOURCES.txt +17 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/dependency_links.txt +1 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/entry_points.txt +3 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/requires.txt +8 -0
- tftree_manager-0.0.1/tftree_manager.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
prune test
|
|
3
|
+
prune test*
|
|
4
|
+
prune tests*
|
|
5
|
+
prune docs
|
|
6
|
+
prune examples
|
|
7
|
+
prune input*
|
|
8
|
+
prune output*
|
|
9
|
+
prune log*
|
|
10
|
+
prune logs*
|
|
11
|
+
prune tmp*
|
|
12
|
+
prune temp*
|
|
13
|
+
prune .git
|
|
14
|
+
prune .pytest_cache
|
|
15
|
+
prune .mypy_cache
|
|
16
|
+
prune __pycache__
|
|
17
|
+
global-exclude *.log
|
|
18
|
+
global-exclude *.pyc
|
|
19
|
+
global-exclude *.pyo
|
|
20
|
+
global-exclude .DS_Store
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tftree-manager
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TF 树梳理与变换管理:YAML 外参加载/保存、图路径与环检测、变换矩阵计算
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20.0
|
|
9
|
+
Requires-Dist: PyYAML>=5.4.0
|
|
10
|
+
Requires-Dist: Flask>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
Requires-Dist: twine; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# tftree-manager
|
|
17
|
+
|
|
18
|
+
TF 树梳理与变换管理:从 YAML 目录加载/保存外参与顺序,构建无向图、路径查找、环检测、变换矩阵计算。
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tftree-manager
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 功能特性
|
|
27
|
+
|
|
28
|
+
- 从 `yaml_dir/extrinsics/*.yaml` 与 `order_manifest.yaml` 加载/保存外参与顺序
|
|
29
|
+
- 无向图构建、BFS 路径查找、有向环检测
|
|
30
|
+
- 沿路径累积 4x4 变换矩阵、从根节点计算到所有可达节点
|
|
31
|
+
- 更新变换、求逆、重排顺序并写回 YAML
|
|
32
|
+
- 可选 Flask API 服务与命令行入口
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from tftree_manager import TFTreeManager
|
|
38
|
+
|
|
39
|
+
manager = TFTreeManager("/path/to/yaml_dir")
|
|
40
|
+
manager.load_from_yaml()
|
|
41
|
+
|
|
42
|
+
# 所有坐标系
|
|
43
|
+
frames = manager.get_all_frames()
|
|
44
|
+
|
|
45
|
+
# 两节点间变换矩阵
|
|
46
|
+
T = manager.get_transform("lidar", "camera")
|
|
47
|
+
|
|
48
|
+
# 从根节点计算到所有节点
|
|
49
|
+
transforms = manager.compute_all_transforms_from_root("world")
|
|
50
|
+
|
|
51
|
+
# 检测环
|
|
52
|
+
cycles = manager.detect_cycles()
|
|
53
|
+
|
|
54
|
+
# 保存
|
|
55
|
+
manager.save_to_yaml()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 命令行
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 以 YAML 目录为参数,加载并打印树与环信息
|
|
62
|
+
tftree-manager /path/to/yaml_dir
|
|
63
|
+
|
|
64
|
+
# 启动 Flask API 服务(默认 0.0.0.0:5000)
|
|
65
|
+
tftree-manager-server
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
环境变量 `TFTREE_DATA_DIR` 可指定 API 默认数据目录(默认 `./data`)。
|
|
69
|
+
|
|
70
|
+
## 依赖
|
|
71
|
+
|
|
72
|
+
- Python >= 3.10
|
|
73
|
+
- numpy >= 1.20.0
|
|
74
|
+
- PyYAML >= 5.4.0
|
|
75
|
+
- Flask >= 2.0.0(API 与 server 入口需要)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# tftree-manager
|
|
2
|
+
|
|
3
|
+
TF 树梳理与变换管理:从 YAML 目录加载/保存外参与顺序,构建无向图、路径查找、环检测、变换矩阵计算。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tftree-manager
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 功能特性
|
|
12
|
+
|
|
13
|
+
- 从 `yaml_dir/extrinsics/*.yaml` 与 `order_manifest.yaml` 加载/保存外参与顺序
|
|
14
|
+
- 无向图构建、BFS 路径查找、有向环检测
|
|
15
|
+
- 沿路径累积 4x4 变换矩阵、从根节点计算到所有可达节点
|
|
16
|
+
- 更新变换、求逆、重排顺序并写回 YAML
|
|
17
|
+
- 可选 Flask API 服务与命令行入口
|
|
18
|
+
|
|
19
|
+
## 快速开始
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from tftree_manager import TFTreeManager
|
|
23
|
+
|
|
24
|
+
manager = TFTreeManager("/path/to/yaml_dir")
|
|
25
|
+
manager.load_from_yaml()
|
|
26
|
+
|
|
27
|
+
# 所有坐标系
|
|
28
|
+
frames = manager.get_all_frames()
|
|
29
|
+
|
|
30
|
+
# 两节点间变换矩阵
|
|
31
|
+
T = manager.get_transform("lidar", "camera")
|
|
32
|
+
|
|
33
|
+
# 从根节点计算到所有节点
|
|
34
|
+
transforms = manager.compute_all_transforms_from_root("world")
|
|
35
|
+
|
|
36
|
+
# 检测环
|
|
37
|
+
cycles = manager.detect_cycles()
|
|
38
|
+
|
|
39
|
+
# 保存
|
|
40
|
+
manager.save_to_yaml()
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 命令行
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# 以 YAML 目录为参数,加载并打印树与环信息
|
|
47
|
+
tftree-manager /path/to/yaml_dir
|
|
48
|
+
|
|
49
|
+
# 启动 Flask API 服务(默认 0.0.0.0:5000)
|
|
50
|
+
tftree-manager-server
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
环境变量 `TFTREE_DATA_DIR` 可指定 API 默认数据目录(默认 `./data`)。
|
|
54
|
+
|
|
55
|
+
## 依赖
|
|
56
|
+
|
|
57
|
+
- Python >= 3.10
|
|
58
|
+
- numpy >= 1.20.0
|
|
59
|
+
- PyYAML >= 5.4.0
|
|
60
|
+
- Flask >= 2.0.0(API 与 server 入口需要)
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tftree-manager"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "TF 树梳理与变换管理:YAML 外参加载/保存、图路径与环检测、变换矩阵计算"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy>=1.20.0",
|
|
14
|
+
"PyYAML>=5.4.0",
|
|
15
|
+
"Flask>=2.0.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
dev = ["pytest", "build", "twine"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
tftree-manager = "tftree_manager.manager:main"
|
|
23
|
+
tftree-manager-server = "tftree_manager.app:run"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
exclude = [
|
|
27
|
+
"test*",
|
|
28
|
+
"tests*",
|
|
29
|
+
"docs*",
|
|
30
|
+
"examples*",
|
|
31
|
+
"input*",
|
|
32
|
+
"output*",
|
|
33
|
+
"log*",
|
|
34
|
+
"logs*",
|
|
35
|
+
"*.log",
|
|
36
|
+
"tmp*",
|
|
37
|
+
"temp*",
|
|
38
|
+
".pytest_cache*",
|
|
39
|
+
".mypy_cache*",
|
|
40
|
+
"__pycache__*",
|
|
41
|
+
"*.pyc",
|
|
42
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tftree-manager: TF 树梳理与变换管理。
|
|
3
|
+
"""
|
|
4
|
+
from .transform import Transform
|
|
5
|
+
from .tftree_errors import (
|
|
6
|
+
TFTreeError,
|
|
7
|
+
TFTreeNotFoundError,
|
|
8
|
+
TFTreeValidationError,
|
|
9
|
+
TFTreeKeyError,
|
|
10
|
+
)
|
|
11
|
+
from .manager import TFTreeManager
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Transform",
|
|
15
|
+
"TFTreeManager",
|
|
16
|
+
"TFTreeError",
|
|
17
|
+
"TFTreeNotFoundError",
|
|
18
|
+
"TFTreeValidationError",
|
|
19
|
+
"TFTreeKeyError",
|
|
20
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Flask API 服务:TF 树加载、查询、更新、保存。"""
|
|
2
|
+
import os
|
|
3
|
+
from flask import Flask, request, jsonify
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from .manager import TFTreeManager
|
|
7
|
+
|
|
8
|
+
app = Flask(__name__)
|
|
9
|
+
|
|
10
|
+
DATA_DIR = os.environ.get("TFTREE_DATA_DIR", "./data")
|
|
11
|
+
manager = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_manager():
|
|
15
|
+
global manager
|
|
16
|
+
if manager is None:
|
|
17
|
+
if not os.path.exists(DATA_DIR):
|
|
18
|
+
os.makedirs(DATA_DIR, exist_ok=True)
|
|
19
|
+
manager = TFTreeManager(DATA_DIR)
|
|
20
|
+
try:
|
|
21
|
+
manager.load_from_yaml()
|
|
22
|
+
except Exception:
|
|
23
|
+
pass
|
|
24
|
+
return manager
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.route('/api/load', methods=['POST'])
|
|
28
|
+
def load_data():
|
|
29
|
+
"""重新加载数据"""
|
|
30
|
+
path = request.json.get('path', DATA_DIR)
|
|
31
|
+
global manager
|
|
32
|
+
manager = TFTreeManager(path)
|
|
33
|
+
try:
|
|
34
|
+
manager.load_from_yaml()
|
|
35
|
+
return jsonify({"status": "success", "message": f"Loaded from {path}", "frames": list(manager.get_all_frames())})
|
|
36
|
+
except Exception as e:
|
|
37
|
+
return jsonify({"status": "error", "message": str(e)}), 400
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@app.route('/api/frames', methods=['GET'])
|
|
41
|
+
def get_frames():
|
|
42
|
+
"""获取所有坐标系"""
|
|
43
|
+
m = get_manager()
|
|
44
|
+
return jsonify({"frames": list(m.get_all_frames())})
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.route('/api/transforms', methods=['GET'])
|
|
48
|
+
def get_all_transforms():
|
|
49
|
+
"""获取所有已定义的变换"""
|
|
50
|
+
m = get_manager()
|
|
51
|
+
return jsonify({"transforms": m.order})
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@app.route('/api/transform', methods=['GET'])
|
|
55
|
+
def get_transform():
|
|
56
|
+
"""计算两个节点间的变换"""
|
|
57
|
+
source = request.args.get('source')
|
|
58
|
+
target = request.args.get('target')
|
|
59
|
+
if not source or not target:
|
|
60
|
+
return jsonify({"status": "error", "message": "Missing source or target"}), 400
|
|
61
|
+
|
|
62
|
+
m = get_manager()
|
|
63
|
+
T = m.get_transform(source, target)
|
|
64
|
+
if T is None:
|
|
65
|
+
return jsonify({"status": "error", "message": f"No path from {source} to {target}"}), 404
|
|
66
|
+
|
|
67
|
+
return jsonify({
|
|
68
|
+
"source": source,
|
|
69
|
+
"target": target,
|
|
70
|
+
"matrix": T.tolist(),
|
|
71
|
+
"translation": T[:3, 3].tolist(),
|
|
72
|
+
"rotation_matrix": T[:3, :3].tolist()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.route('/api/compute_all', methods=['GET'])
|
|
77
|
+
def compute_all():
|
|
78
|
+
"""从根节点计算所有变换"""
|
|
79
|
+
root = request.args.get('root')
|
|
80
|
+
if not root:
|
|
81
|
+
return jsonify({"status": "error", "message": "Missing root"}), 400
|
|
82
|
+
|
|
83
|
+
m = get_manager()
|
|
84
|
+
try:
|
|
85
|
+
transforms = m.compute_all_transforms_from_root(root)
|
|
86
|
+
result = {}
|
|
87
|
+
for frame, T in transforms.items():
|
|
88
|
+
result[frame] = {
|
|
89
|
+
"matrix": T.tolist(),
|
|
90
|
+
"translation": T[:3, 3].tolist()
|
|
91
|
+
}
|
|
92
|
+
return jsonify({"root": root, "transforms": result})
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return jsonify({"status": "error", "message": str(e)}), 400
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.route('/api/update', methods=['POST'])
|
|
98
|
+
def update_transform():
|
|
99
|
+
"""更新变换参数"""
|
|
100
|
+
data = request.json
|
|
101
|
+
source = data.get('source')
|
|
102
|
+
target = data.get('target')
|
|
103
|
+
translation = data.get('translation')
|
|
104
|
+
rotation = data.get('rotation')
|
|
105
|
+
|
|
106
|
+
if not source or not target:
|
|
107
|
+
return jsonify({"status": "error", "message": "Missing source or target"}), 400
|
|
108
|
+
|
|
109
|
+
m = get_manager()
|
|
110
|
+
try:
|
|
111
|
+
trans_np = np.array(translation) if translation else None
|
|
112
|
+
rot_np = np.array(rotation) if rotation else None
|
|
113
|
+
m.update_transform(source, target, trans_np, rot_np)
|
|
114
|
+
return jsonify({"status": "success"})
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return jsonify({"status": "error", "message": str(e)}), 400
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@app.route('/api/invert', methods=['POST'])
|
|
120
|
+
def invert_transform():
|
|
121
|
+
"""求逆变换"""
|
|
122
|
+
data = request.json
|
|
123
|
+
source = data.get('source')
|
|
124
|
+
target = data.get('target')
|
|
125
|
+
|
|
126
|
+
if not source or not target:
|
|
127
|
+
return jsonify({"status": "error", "message": "Missing source or target"}), 400
|
|
128
|
+
|
|
129
|
+
m = get_manager()
|
|
130
|
+
try:
|
|
131
|
+
m.invert_transform(source, target)
|
|
132
|
+
return jsonify({"status": "success"})
|
|
133
|
+
except Exception as e:
|
|
134
|
+
return jsonify({"status": "error", "message": str(e)}), 400
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.route('/api/save', methods=['POST'])
|
|
138
|
+
def save_data():
|
|
139
|
+
"""保存到 YAML"""
|
|
140
|
+
m = get_manager()
|
|
141
|
+
try:
|
|
142
|
+
m.save_to_yaml()
|
|
143
|
+
return jsonify({"status": "success", "message": f"Saved to {m.yaml_dir}"})
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return jsonify({"status": "error", "message": str(e)}), 500
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@app.route('/api/cycles', methods=['GET'])
|
|
149
|
+
def detect_cycles():
|
|
150
|
+
"""检测环"""
|
|
151
|
+
m = get_manager()
|
|
152
|
+
cycles = m.detect_cycles()
|
|
153
|
+
return jsonify({"cycles": cycles})
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run(host='0.0.0.0', port=5000, debug=True):
|
|
157
|
+
"""启动 Flask 服务(供命令行入口调用)。"""
|
|
158
|
+
app.run(host=host, port=port, debug=debug)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
if __name__ == '__main__':
|
|
162
|
+
run()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""
|
|
2
|
+
图操作:无向图构建、BFS 路径、有向环检测。纯函数,无 I/O。
|
|
3
|
+
"""
|
|
4
|
+
from typing import Dict, List, Optional, Set
|
|
5
|
+
|
|
6
|
+
from .transform import Transform
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def build_bidirectional_graph(transforms: Dict[str, Transform]) -> Dict[str, Set[str]]:
|
|
10
|
+
"""
|
|
11
|
+
根据 transforms 构建无向图邻接表(source<->target),用于路径查找与求逆。
|
|
12
|
+
"""
|
|
13
|
+
graph: Dict[str, Set[str]] = {}
|
|
14
|
+
for key, t in transforms.items():
|
|
15
|
+
s, tgt = t.source_frame, t.target_frame
|
|
16
|
+
if s not in graph:
|
|
17
|
+
graph[s] = set()
|
|
18
|
+
if tgt not in graph:
|
|
19
|
+
graph[tgt] = set()
|
|
20
|
+
graph[s].add(tgt)
|
|
21
|
+
graph[tgt].add(s)
|
|
22
|
+
return graph
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def find_path_bfs(
|
|
26
|
+
graph: Dict[str, Set[str]],
|
|
27
|
+
source: str,
|
|
28
|
+
target: str,
|
|
29
|
+
) -> Optional[List[str]]:
|
|
30
|
+
"""BFS 从 source 到 target 的路径,不存在返回 None。"""
|
|
31
|
+
if source not in graph or target not in graph:
|
|
32
|
+
return None
|
|
33
|
+
queue = [(source, [source])]
|
|
34
|
+
visited = {source}
|
|
35
|
+
while queue:
|
|
36
|
+
node, path = queue.pop(0)
|
|
37
|
+
if node == target:
|
|
38
|
+
return path
|
|
39
|
+
for neighbor in graph.get(node, []):
|
|
40
|
+
if neighbor not in visited:
|
|
41
|
+
visited.add(neighbor)
|
|
42
|
+
queue.append((neighbor, path + [neighbor]))
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_directed_cycles(transforms: Dict[str, Transform]) -> List[List[str]]:
|
|
47
|
+
"""
|
|
48
|
+
在有向图(仅 source->target)上检测所有环,返回环列表,每个环为节点 id 列表。
|
|
49
|
+
"""
|
|
50
|
+
directed: Dict[str, Set[str]] = {}
|
|
51
|
+
all_nodes: Set[str] = set()
|
|
52
|
+
for key, t in transforms.items():
|
|
53
|
+
s, tgt = t.source_frame, t.target_frame
|
|
54
|
+
all_nodes.add(s)
|
|
55
|
+
all_nodes.add(tgt)
|
|
56
|
+
if s not in directed:
|
|
57
|
+
directed[s] = set()
|
|
58
|
+
directed[s].add(tgt)
|
|
59
|
+
|
|
60
|
+
cycles = []
|
|
61
|
+
visited = set()
|
|
62
|
+
rec_stack = set()
|
|
63
|
+
|
|
64
|
+
def dfs(node: str, path: List[str]):
|
|
65
|
+
visited.add(node)
|
|
66
|
+
rec_stack.add(node)
|
|
67
|
+
path.append(node)
|
|
68
|
+
for neighbor in directed.get(node, []):
|
|
69
|
+
if neighbor not in visited:
|
|
70
|
+
dfs(neighbor, path.copy())
|
|
71
|
+
elif neighbor in rec_stack:
|
|
72
|
+
cycle_start = path.index(neighbor)
|
|
73
|
+
cycle = path[cycle_start:] + [neighbor]
|
|
74
|
+
cycles.append(cycle)
|
|
75
|
+
rec_stack.remove(node)
|
|
76
|
+
|
|
77
|
+
for node in sorted(all_nodes):
|
|
78
|
+
if node not in visited:
|
|
79
|
+
dfs(node, [])
|
|
80
|
+
|
|
81
|
+
return cycles
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TF Tree Manager - 仅调度:持有 transforms/order/graph,对外 API 委托给 transform、yaml_io、graph_ops、transform_math。
|
|
3
|
+
"""
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Dict, List, Optional, Set
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .transform import Transform
|
|
10
|
+
from .yaml_io import load_extrinsics_and_order, save_extrinsics_and_order
|
|
11
|
+
from .graph_ops import build_bidirectional_graph, find_path_bfs, detect_directed_cycles
|
|
12
|
+
from .transform_math import get_transform_along_path, compute_all_from_root
|
|
13
|
+
from .tftree_errors import TFTreeValidationError, TFTreeKeyError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TFTreeManager:
|
|
17
|
+
"""TF 树管理器:调度层,状态 + 委托。"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, yaml_dir: str):
|
|
20
|
+
self.yaml_dir = Path(yaml_dir)
|
|
21
|
+
self.transforms: Dict[str, Transform] = {}
|
|
22
|
+
self.order: List[str] = []
|
|
23
|
+
self.graph: Dict[str, Set[str]] = {}
|
|
24
|
+
|
|
25
|
+
def load_from_yaml(self):
|
|
26
|
+
"""从 YAML 目录加载外参与顺序,构建图。"""
|
|
27
|
+
self.transforms, self.order = load_extrinsics_and_order(self.yaml_dir)
|
|
28
|
+
self.graph = build_bidirectional_graph(self.transforms)
|
|
29
|
+
|
|
30
|
+
def _build_graph(self):
|
|
31
|
+
"""重建无向图(invert 后调用)。"""
|
|
32
|
+
self.graph = build_bidirectional_graph(self.transforms)
|
|
33
|
+
|
|
34
|
+
def detect_cycles(self) -> List[List[str]]:
|
|
35
|
+
"""有向图环检测,委托 graph_ops。"""
|
|
36
|
+
return detect_directed_cycles(self.transforms)
|
|
37
|
+
|
|
38
|
+
def get_transform(
|
|
39
|
+
self, source: str, target: str, root: Optional[str] = None
|
|
40
|
+
) -> Optional[np.ndarray]:
|
|
41
|
+
"""沿路径累积变换矩阵。"""
|
|
42
|
+
if source == target:
|
|
43
|
+
return np.eye(4)
|
|
44
|
+
path = find_path_bfs(self.graph, source, target)
|
|
45
|
+
if not path:
|
|
46
|
+
return None
|
|
47
|
+
return get_transform_along_path(self.transforms, path)
|
|
48
|
+
|
|
49
|
+
def compute_all_transforms_from_root(self, root: str) -> Dict[str, np.ndarray]:
|
|
50
|
+
"""从 root 到所有可达节点的变换。"""
|
|
51
|
+
if root not in self.graph:
|
|
52
|
+
raise TFTreeValidationError(f"根节点 {root} 不存在于图中", {"root": root})
|
|
53
|
+
return compute_all_from_root(self.transforms, self.graph, root)
|
|
54
|
+
|
|
55
|
+
def update_transform(
|
|
56
|
+
self,
|
|
57
|
+
source: str,
|
|
58
|
+
target: str,
|
|
59
|
+
translation: Optional[np.ndarray] = None,
|
|
60
|
+
rotation: Optional[np.ndarray] = None,
|
|
61
|
+
):
|
|
62
|
+
"""更新指定边的平移/旋转。"""
|
|
63
|
+
key = f"{source}->{target}"
|
|
64
|
+
if key not in self.transforms:
|
|
65
|
+
raise TFTreeKeyError(f"变换 {key} 不存在", {"key": key})
|
|
66
|
+
t = self.transforms[key]
|
|
67
|
+
if translation is not None:
|
|
68
|
+
t.translation = translation.copy()
|
|
69
|
+
if rotation is not None:
|
|
70
|
+
t.rotation = rotation.copy()
|
|
71
|
+
|
|
72
|
+
def invert_transform(self, source: str, target: str):
|
|
73
|
+
"""将边 source->target 求逆为 target->source。"""
|
|
74
|
+
key = f"{source}->{target}"
|
|
75
|
+
if key not in self.transforms:
|
|
76
|
+
raise TFTreeKeyError(f"变换 {key} 不存在", {"key": key})
|
|
77
|
+
t = self.transforms[key]
|
|
78
|
+
inv = t.inverse()
|
|
79
|
+
del self.transforms[key]
|
|
80
|
+
new_key = f"{target}->{source}"
|
|
81
|
+
self.transforms[new_key] = inv
|
|
82
|
+
if key in self.order:
|
|
83
|
+
idx = self.order.index(key)
|
|
84
|
+
self.order[idx] = new_key
|
|
85
|
+
self._build_graph()
|
|
86
|
+
|
|
87
|
+
def reorder_transforms(self, new_order: List[str]):
|
|
88
|
+
"""重排顺序列表。"""
|
|
89
|
+
if set(new_order) != set(self.order):
|
|
90
|
+
raise TFTreeValidationError(
|
|
91
|
+
"新顺序必须包含所有现有变换", {"order": list(self.order)}
|
|
92
|
+
)
|
|
93
|
+
self.order = new_order.copy()
|
|
94
|
+
|
|
95
|
+
def save_to_yaml(self):
|
|
96
|
+
"""写回 extrinsics 与 order_manifest。"""
|
|
97
|
+
save_extrinsics_and_order(self.yaml_dir, self.transforms, self.order)
|
|
98
|
+
|
|
99
|
+
def get_all_frames(self) -> Set[str]:
|
|
100
|
+
"""所有节点(frame)集合。"""
|
|
101
|
+
return set(self.graph.keys())
|
|
102
|
+
|
|
103
|
+
def print_tree(self, root: Optional[str] = None):
|
|
104
|
+
"""打印树结构(调试)。"""
|
|
105
|
+
if not self.graph:
|
|
106
|
+
print("图为空")
|
|
107
|
+
return
|
|
108
|
+
if root is None:
|
|
109
|
+
all_targets = set()
|
|
110
|
+
all_sources = set()
|
|
111
|
+
for key in self.transforms:
|
|
112
|
+
s, t = key.split("->")
|
|
113
|
+
all_sources.add(s)
|
|
114
|
+
all_targets.add(t)
|
|
115
|
+
roots = all_sources - all_targets
|
|
116
|
+
root = list(roots)[0] if roots else list(self.graph.keys())[0]
|
|
117
|
+
print(f"树结构(根节点: {root}):")
|
|
118
|
+
visited = set()
|
|
119
|
+
|
|
120
|
+
def print_node(node: str, level: int):
|
|
121
|
+
if node in visited:
|
|
122
|
+
print(" " * level + f"└─ {node} (已访问)")
|
|
123
|
+
return
|
|
124
|
+
visited.add(node)
|
|
125
|
+
print(" " * level + f"└─ {node}")
|
|
126
|
+
for neighbor in sorted(self.graph.get(node, [])):
|
|
127
|
+
if neighbor not in visited:
|
|
128
|
+
print_node(neighbor, level + 1)
|
|
129
|
+
|
|
130
|
+
print_node(root, 0)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main():
|
|
134
|
+
"""示例用法。"""
|
|
135
|
+
import sys
|
|
136
|
+
if len(sys.argv) < 2:
|
|
137
|
+
print("用法: tftree-manager <yaml_dir>")
|
|
138
|
+
return
|
|
139
|
+
yaml_dir = sys.argv[1]
|
|
140
|
+
manager = TFTreeManager(yaml_dir)
|
|
141
|
+
manager.load_from_yaml()
|
|
142
|
+
print(f"加载了 {len(manager.transforms)} 个变换")
|
|
143
|
+
print(f"坐标系: {manager.get_all_frames()}")
|
|
144
|
+
cycles = manager.detect_cycles()
|
|
145
|
+
if cycles:
|
|
146
|
+
print(f"检测到 {len(cycles)} 个环:")
|
|
147
|
+
for i, c in enumerate(cycles):
|
|
148
|
+
print(f" 环 {i+1}: {' -> '.join(c)}")
|
|
149
|
+
else:
|
|
150
|
+
print("未检测到环")
|
|
151
|
+
manager.print_tree()
|
|
152
|
+
frames = manager.get_all_frames()
|
|
153
|
+
if frames:
|
|
154
|
+
root = list(frames)[0]
|
|
155
|
+
result = manager.compute_all_transforms_from_root(root)
|
|
156
|
+
print(f"从根节点 {root} 到所有节点的变换: {len(result)} 个")
|
|
157
|
+
print()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
if __name__ == "__main__":
|
|
161
|
+
main()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TF Tree 专用异常:便于 D03 独立使用及上层(如 studio)统一捕获并转成 HTTP/JSON。
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TFTreeError(Exception):
|
|
8
|
+
"""树相关异常基类,携带 message 与可选 detail。"""
|
|
9
|
+
|
|
10
|
+
def __init__(self, message: str, detail: Optional[dict] = None):
|
|
11
|
+
self.message = message
|
|
12
|
+
self.detail = detail or {}
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
|
|
15
|
+
def to_dict(self) -> dict:
|
|
16
|
+
"""便于转成 JSON 响应。"""
|
|
17
|
+
out = {"message": self.message}
|
|
18
|
+
if self.detail:
|
|
19
|
+
out["detail"] = self.detail
|
|
20
|
+
return out
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TFTreeNotFoundError(TFTreeError):
|
|
24
|
+
"""资源不存在:如外参目录、文件缺失。"""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TFTreeValidationError(TFTreeError):
|
|
29
|
+
"""校验失败:如根节点不存在、顺序不包含全部变换、参数非法。"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TFTreeKeyError(TFTreeError):
|
|
34
|
+
"""变换 key 不存在(如 update/invert 时指定的 source->target)。"""
|
|
35
|
+
pass
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Transform 数据类:四元数/旋转矩阵、逆变换、序列化。
|
|
3
|
+
不依赖 manager,供 yaml_io、transform_math、manager 使用。
|
|
4
|
+
"""
|
|
5
|
+
import numpy as np
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Transform:
|
|
15
|
+
"""表示一个坐标变换"""
|
|
16
|
+
source_frame: str
|
|
17
|
+
target_frame: str
|
|
18
|
+
translation: np.ndarray # [x, y, z]
|
|
19
|
+
rotation: np.ndarray # [qx, qy, qz, qw] 四元数
|
|
20
|
+
|
|
21
|
+
def to_matrix(self) -> np.ndarray:
|
|
22
|
+
"""转换为4x4齐次变换矩阵"""
|
|
23
|
+
qx, qy, qz, qw = self.rotation
|
|
24
|
+
|
|
25
|
+
# 四元数转旋转矩阵
|
|
26
|
+
R = np.array([
|
|
27
|
+
[1 - 2*(qy**2 + qz**2), 2*(qx*qy - qz*qw), 2*(qx*qz + qy*qw)],
|
|
28
|
+
[2*(qx*qy + qz*qw), 1 - 2*(qx**2 + qz**2), 2*(qy*qz - qx*qw)],
|
|
29
|
+
[2*(qx*qz - qy*qw), 2*(qy*qz + qx*qw), 1 - 2*(qx**2 + qy**2)]
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
# 构建齐次变换矩阵
|
|
33
|
+
T = np.eye(4)
|
|
34
|
+
T[:3, :3] = R
|
|
35
|
+
T[:3, 3] = self.translation
|
|
36
|
+
return T
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def from_matrix(matrix: np.ndarray, source: str, target: str) -> "Transform":
|
|
40
|
+
"""从4x4齐次变换矩阵创建Transform"""
|
|
41
|
+
translation = matrix[:3, 3]
|
|
42
|
+
R = matrix[:3, :3]
|
|
43
|
+
|
|
44
|
+
# 旋转矩阵转四元数
|
|
45
|
+
trace = np.trace(R)
|
|
46
|
+
if trace > 0:
|
|
47
|
+
s = 0.5 / np.sqrt(trace + 1.0)
|
|
48
|
+
qw = 0.25 / s
|
|
49
|
+
qx = (R[2, 1] - R[1, 2]) * s
|
|
50
|
+
qy = (R[0, 2] - R[2, 0]) * s
|
|
51
|
+
qz = (R[1, 0] - R[0, 1]) * s
|
|
52
|
+
else:
|
|
53
|
+
if R[0, 0] > R[1, 1] and R[0, 0] > R[2, 2]:
|
|
54
|
+
s = 2.0 * np.sqrt(1.0 + R[0, 0] - R[1, 1] - R[2, 2])
|
|
55
|
+
qw = (R[2, 1] - R[1, 2]) / s
|
|
56
|
+
qx = 0.25 * s
|
|
57
|
+
qy = (R[0, 1] + R[1, 0]) / s
|
|
58
|
+
qz = (R[0, 2] + R[2, 0]) / s
|
|
59
|
+
elif R[1, 1] > R[2, 2]:
|
|
60
|
+
s = 2.0 * np.sqrt(1.0 + R[1, 1] - R[0, 0] - R[2, 2])
|
|
61
|
+
qw = (R[0, 2] - R[2, 0]) / s
|
|
62
|
+
qx = (R[0, 1] + R[1, 0]) / s
|
|
63
|
+
qy = 0.25 * s
|
|
64
|
+
qz = (R[1, 2] + R[2, 1]) / s
|
|
65
|
+
else:
|
|
66
|
+
s = 2.0 * np.sqrt(1.0 + R[2, 2] - R[0, 0] - R[1, 1])
|
|
67
|
+
qw = (R[1, 0] - R[0, 1]) / s
|
|
68
|
+
qx = (R[0, 2] + R[2, 0]) / s
|
|
69
|
+
qy = (R[1, 2] + R[2, 1]) / s
|
|
70
|
+
qz = 0.25 * s
|
|
71
|
+
|
|
72
|
+
rotation = np.array([qx, qy, qz, qw])
|
|
73
|
+
return Transform(source, target, translation, rotation)
|
|
74
|
+
|
|
75
|
+
def inverse(self) -> "Transform":
|
|
76
|
+
"""返回逆变换"""
|
|
77
|
+
T_inv = np.linalg.inv(self.to_matrix())
|
|
78
|
+
return Transform.from_matrix(T_inv, self.target_frame, self.source_frame)
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict:
|
|
81
|
+
"""转换为字典格式(用于保存YAML)"""
|
|
82
|
+
return {
|
|
83
|
+
'source_frame': self.source_frame,
|
|
84
|
+
'target_frame': self.target_frame,
|
|
85
|
+
'translation': {
|
|
86
|
+
'x': float(self.translation[0]),
|
|
87
|
+
'y': float(self.translation[1]),
|
|
88
|
+
'z': float(self.translation[2])
|
|
89
|
+
},
|
|
90
|
+
'rotation': {
|
|
91
|
+
'qx': float(self.rotation[0]),
|
|
92
|
+
'qy': float(self.rotation[1]),
|
|
93
|
+
'qz': float(self.rotation[2]),
|
|
94
|
+
'qw': float(self.rotation[3])
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
变换计算:沿路径累积矩阵、从 root 计算到所有节点。不负责 I/O 和图构建。
|
|
3
|
+
"""
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from .transform import Transform
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_transform_along_path(
|
|
11
|
+
transforms: Dict[str, Transform],
|
|
12
|
+
path: List[str],
|
|
13
|
+
) -> Optional[np.ndarray]:
|
|
14
|
+
"""
|
|
15
|
+
沿 path 累积变换矩阵,path 为节点序列 [n0, n1, ..., nk]。
|
|
16
|
+
若某条边不存在(正向与逆向都不存在)则返回 None。
|
|
17
|
+
"""
|
|
18
|
+
if len(path) < 2:
|
|
19
|
+
return np.eye(4)
|
|
20
|
+
T = np.eye(4)
|
|
21
|
+
for i in range(len(path) - 1):
|
|
22
|
+
from_frame = path[i]
|
|
23
|
+
to_frame = path[i + 1]
|
|
24
|
+
key = f"{from_frame}->{to_frame}"
|
|
25
|
+
if key in transforms:
|
|
26
|
+
T = T @ transforms[key].to_matrix()
|
|
27
|
+
else:
|
|
28
|
+
key_inv = f"{to_frame}->{from_frame}"
|
|
29
|
+
if key_inv in transforms:
|
|
30
|
+
T = T @ transforms[key_inv].inverse().to_matrix()
|
|
31
|
+
else:
|
|
32
|
+
return None
|
|
33
|
+
return T
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def compute_all_from_root(
|
|
37
|
+
transforms: Dict[str, Transform],
|
|
38
|
+
graph: Dict[str, set], # Set[str] per node
|
|
39
|
+
root: str,
|
|
40
|
+
) -> Dict[str, np.ndarray]:
|
|
41
|
+
"""
|
|
42
|
+
从 root 到图中所有可达节点的变换矩阵。
|
|
43
|
+
调用方需保证 root 在 graph 中;否则由调用方抛异常。
|
|
44
|
+
"""
|
|
45
|
+
from .graph_ops import find_path_bfs
|
|
46
|
+
|
|
47
|
+
result = {root: np.eye(4)}
|
|
48
|
+
for node in graph:
|
|
49
|
+
if node == root:
|
|
50
|
+
continue
|
|
51
|
+
path = find_path_bfs(graph, root, node)
|
|
52
|
+
if path is None:
|
|
53
|
+
continue
|
|
54
|
+
T = get_transform_along_path(transforms, path)
|
|
55
|
+
if T is not None:
|
|
56
|
+
result[node] = T
|
|
57
|
+
return result
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
YAML 读写:order_manifest + extrinsics 目录。
|
|
3
|
+
输入 yaml_dir,返回/接收 (transforms_dict, order_list),不持有图。
|
|
4
|
+
"""
|
|
5
|
+
import yaml
|
|
6
|
+
import numpy as np
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Tuple
|
|
9
|
+
|
|
10
|
+
from .transform import Transform
|
|
11
|
+
from .tftree_errors import TFTreeNotFoundError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_extrinsics_and_order(yaml_dir: Path) -> Tuple[Dict[str, "Transform"], List[str]]:
|
|
15
|
+
"""
|
|
16
|
+
从 yaml_dir 读取 order_manifest 与 extrinsics/*.yaml,返回 (transforms, order)。
|
|
17
|
+
外参目录不存在时抛出 TFTreeNotFoundError。
|
|
18
|
+
"""
|
|
19
|
+
extrinsics_dir = yaml_dir / "extrinsics"
|
|
20
|
+
if not extrinsics_dir.exists():
|
|
21
|
+
raise TFTreeNotFoundError(f"外参目录不存在: {extrinsics_dir}", {"path": str(extrinsics_dir)})
|
|
22
|
+
|
|
23
|
+
order: List[str] = []
|
|
24
|
+
order_file = yaml_dir / "order_manifest.yaml"
|
|
25
|
+
if order_file.exists():
|
|
26
|
+
with open(order_file, "r", encoding="utf-8") as f:
|
|
27
|
+
order_data = yaml.safe_load(f)
|
|
28
|
+
if order_data:
|
|
29
|
+
if "extrinsics_order" in order_data:
|
|
30
|
+
order = list(order_data["extrinsics_order"])
|
|
31
|
+
elif "extrinsics" in order_data:
|
|
32
|
+
for item in order_data["extrinsics"]:
|
|
33
|
+
rel_file = item.get("file", "")
|
|
34
|
+
path = yaml_dir / rel_file
|
|
35
|
+
if path.exists():
|
|
36
|
+
with open(path, "r", encoding="utf-8") as fp:
|
|
37
|
+
data = yaml.safe_load(fp) or {}
|
|
38
|
+
src = data.get("source_frame", "")
|
|
39
|
+
tgt = data.get("target_frame", "")
|
|
40
|
+
if src or tgt:
|
|
41
|
+
order.append(f"{src}->{tgt}")
|
|
42
|
+
|
|
43
|
+
transforms: Dict[str, Transform] = {}
|
|
44
|
+
for yaml_file in sorted(extrinsics_dir.glob("*.yaml")):
|
|
45
|
+
with open(yaml_file, "r", encoding="utf-8") as f:
|
|
46
|
+
data = yaml.safe_load(f)
|
|
47
|
+
if data:
|
|
48
|
+
source = data["source_frame"]
|
|
49
|
+
target = data["target_frame"]
|
|
50
|
+
translation = np.array([
|
|
51
|
+
data["translation"]["x"],
|
|
52
|
+
data["translation"]["y"],
|
|
53
|
+
data["translation"]["z"],
|
|
54
|
+
])
|
|
55
|
+
rotation = np.array([
|
|
56
|
+
data["rotation"]["qx"],
|
|
57
|
+
data["rotation"]["qy"],
|
|
58
|
+
data["rotation"]["qz"],
|
|
59
|
+
data["rotation"]["qw"],
|
|
60
|
+
])
|
|
61
|
+
t = Transform(source, target, translation, rotation)
|
|
62
|
+
key = f"{source}->{target}"
|
|
63
|
+
transforms[key] = t
|
|
64
|
+
if key not in order:
|
|
65
|
+
order.append(key)
|
|
66
|
+
|
|
67
|
+
return transforms, order
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def save_extrinsics_and_order(
|
|
71
|
+
yaml_dir: Path,
|
|
72
|
+
transforms: Dict[str, "Transform"],
|
|
73
|
+
order: List[str],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""
|
|
76
|
+
将 transforms 与 order 写回 yaml_dir:extrinsics/000.yaml 等 + order_manifest.yaml。
|
|
77
|
+
"""
|
|
78
|
+
extrinsics_dir = yaml_dir / "extrinsics"
|
|
79
|
+
extrinsics_dir.mkdir(exist_ok=True)
|
|
80
|
+
|
|
81
|
+
for yaml_file in extrinsics_dir.glob("*.yaml"):
|
|
82
|
+
yaml_file.unlink()
|
|
83
|
+
|
|
84
|
+
for i, key in enumerate(order):
|
|
85
|
+
if key not in transforms:
|
|
86
|
+
continue
|
|
87
|
+
transform = transforms[key]
|
|
88
|
+
filepath = extrinsics_dir / f"{i:03d}.yaml"
|
|
89
|
+
with open(filepath, "w", encoding="utf-8") as f:
|
|
90
|
+
yaml.dump(transform.to_dict(), f, default_flow_style=False, allow_unicode=True)
|
|
91
|
+
|
|
92
|
+
order_file = yaml_dir / "order_manifest.yaml"
|
|
93
|
+
order_data = {}
|
|
94
|
+
if order_file.exists():
|
|
95
|
+
with open(order_file, "r", encoding="utf-8") as f:
|
|
96
|
+
order_data = yaml.safe_load(f) or {}
|
|
97
|
+
order_data["extrinsics"] = [{"index": i, "file": f"extrinsics/{i:03d}.yaml"} for i in range(len(order))]
|
|
98
|
+
order_data["extrinsics_order"] = order
|
|
99
|
+
with open(order_file, "w", encoding="utf-8") as f:
|
|
100
|
+
yaml.dump(order_data, f, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tftree-manager
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: TF 树梳理与变换管理:YAML 外参加载/保存、图路径与环检测、变换矩阵计算
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: numpy>=1.20.0
|
|
9
|
+
Requires-Dist: PyYAML>=5.4.0
|
|
10
|
+
Requires-Dist: Flask>=2.0.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest; extra == "dev"
|
|
13
|
+
Requires-Dist: build; extra == "dev"
|
|
14
|
+
Requires-Dist: twine; extra == "dev"
|
|
15
|
+
|
|
16
|
+
# tftree-manager
|
|
17
|
+
|
|
18
|
+
TF 树梳理与变换管理:从 YAML 目录加载/保存外参与顺序,构建无向图、路径查找、环检测、变换矩阵计算。
|
|
19
|
+
|
|
20
|
+
## 安装
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install tftree-manager
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 功能特性
|
|
27
|
+
|
|
28
|
+
- 从 `yaml_dir/extrinsics/*.yaml` 与 `order_manifest.yaml` 加载/保存外参与顺序
|
|
29
|
+
- 无向图构建、BFS 路径查找、有向环检测
|
|
30
|
+
- 沿路径累积 4x4 变换矩阵、从根节点计算到所有可达节点
|
|
31
|
+
- 更新变换、求逆、重排顺序并写回 YAML
|
|
32
|
+
- 可选 Flask API 服务与命令行入口
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from tftree_manager import TFTreeManager
|
|
38
|
+
|
|
39
|
+
manager = TFTreeManager("/path/to/yaml_dir")
|
|
40
|
+
manager.load_from_yaml()
|
|
41
|
+
|
|
42
|
+
# 所有坐标系
|
|
43
|
+
frames = manager.get_all_frames()
|
|
44
|
+
|
|
45
|
+
# 两节点间变换矩阵
|
|
46
|
+
T = manager.get_transform("lidar", "camera")
|
|
47
|
+
|
|
48
|
+
# 从根节点计算到所有节点
|
|
49
|
+
transforms = manager.compute_all_transforms_from_root("world")
|
|
50
|
+
|
|
51
|
+
# 检测环
|
|
52
|
+
cycles = manager.detect_cycles()
|
|
53
|
+
|
|
54
|
+
# 保存
|
|
55
|
+
manager.save_to_yaml()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## 命令行
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 以 YAML 目录为参数,加载并打印树与环信息
|
|
62
|
+
tftree-manager /path/to/yaml_dir
|
|
63
|
+
|
|
64
|
+
# 启动 Flask API 服务(默认 0.0.0.0:5000)
|
|
65
|
+
tftree-manager-server
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
环境变量 `TFTREE_DATA_DIR` 可指定 API 默认数据目录(默认 `./data`)。
|
|
69
|
+
|
|
70
|
+
## 依赖
|
|
71
|
+
|
|
72
|
+
- Python >= 3.10
|
|
73
|
+
- numpy >= 1.20.0
|
|
74
|
+
- PyYAML >= 5.4.0
|
|
75
|
+
- Flask >= 2.0.0(API 与 server 入口需要)
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
tftree_manager/__init__.py
|
|
5
|
+
tftree_manager/app.py
|
|
6
|
+
tftree_manager/graph_ops.py
|
|
7
|
+
tftree_manager/manager.py
|
|
8
|
+
tftree_manager/tftree_errors.py
|
|
9
|
+
tftree_manager/transform.py
|
|
10
|
+
tftree_manager/transform_math.py
|
|
11
|
+
tftree_manager/yaml_io.py
|
|
12
|
+
tftree_manager.egg-info/PKG-INFO
|
|
13
|
+
tftree_manager.egg-info/SOURCES.txt
|
|
14
|
+
tftree_manager.egg-info/dependency_links.txt
|
|
15
|
+
tftree_manager.egg-info/entry_points.txt
|
|
16
|
+
tftree_manager.egg-info/requires.txt
|
|
17
|
+
tftree_manager.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|