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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ [console_scripts]
2
+ tftree-manager = tftree_manager.manager:main
3
+ tftree-manager-server = tftree_manager.app:run
@@ -0,0 +1,8 @@
1
+ numpy>=1.20.0
2
+ PyYAML>=5.4.0
3
+ Flask>=2.0.0
4
+
5
+ [dev]
6
+ pytest
7
+ build
8
+ twine
@@ -0,0 +1,2 @@
1
+ dist
2
+ tftree_manager