Metadata-Version: 2.4
Name: tftree-manager
Version: 0.0.12
Summary: TF 树梳理与变换管理：YAML/JSON 外参加载、图路径与环检测、直接关系重写、局部求逆、标定链路替换 UI、变换矩阵计算
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.20.0
Requires-Dist: PyYAML>=5.4.0
Requires-Dist: Flask>=2.0.0
Requires-Dist: mdrive4-json>=0.0.2
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"
Provides-Extra: ui
Requires-Dist: PySide6; extra == "ui"

# tftree-manager

**当前版本：0.0.12**

TF 树梳理与变换管理：从 YAML 目录或 mdrive4 JSON 目录加载/保存外参与顺序，构建无向图、路径查找、环检测、变换矩阵计算。

## 安装

```bash
pip install tftree-manager
```

## 功能特性

- 从 `yaml_dir/extrinsics/*.yaml` 与 `order_manifest.yaml` 加载/保存外参与顺序
- 从 mdrive4 JSON 目录读取 `source_frame->root_frame` 外参并导出 tftree YAML
- 无向图构建、BFS 路径查找、有向环检测
- 沿路径累积 4x4 变换矩阵、从根节点计算到所有可达节点
- 更新变换、求逆、删除有向边（`remove_transform`）、重排顺序并写回 YAML
- 相邻直接链路求逆（`invert_adjacent_relation`）：传入两个节点，自动识别当前真实方向并翻转保存
- 直接关系重写（`rewrite_direct_relations`）：删除被更新相机旧链路，按外部 YAML 声明方向写入新直接边，并在 `order_manifest.yaml` 记录原始计算链路 `link_nodes`
- 标定链路替换（`replace_link_for_calibration` / `launch_prune_ui`）：输入新标定外参，在当前唯一路径上选择一条既有链接更新，拓扑不变
- 输出模式（`compute` / `projection`）：graph 内部只存 compute，projection 仅在 `get_transform(..., output_mode="projection")` 输出时派生
- 可选 Flask API 服务与命令行入口

## 快速开始

```python
from tftree_manager import TFTreeManager

manager = TFTreeManager("/path/to/yaml_dir")
manager.load_from_yaml()

# 所有坐标系
frames = manager.get_all_frames()

# 两节点间变换矩阵，默认 compute，用于尺寸链/保存/写回
T_compute = manager.get_transform("lidar", "camera")

# lidar2cam 显示、PnP、投影使用 projection
T_projection = manager.get_transform("lidar", "camera", output_mode="projection")

# 从根节点计算到所有节点
transforms = manager.compute_all_transforms_from_root("world")

# 检测环
cycles = manager.detect_cycles()

# 保存
manager.save_to_yaml()
```

## mdrive4 JSON 输入

V0.0.12 中 `TFTreeManager.load_from_mdrive4_json(input_dir, root_frame="ego")`
消费 `mdrive4_json.load_numpy_calibration()`，并把 mdrive4-json camera
record 的格式轴系差异限制在 D03 输出模式边界。

规则：

- 复用 `mdrive4_json.load_numpy_calibration` 读取 JSON，按 mdrive4 JSON 原始语义解释为 `sensor_id->ego`，不在本库做求逆。
- Python API 默认 `root_frame="ego"` 仅为兼容旧调用；官方一致性用法请显式传 `root_frame="vrf_ground"`。这里的 `vrf_ground` 是后轴中心接地点，不等同 `vrf`。
- JSON 主 frame 优先来自 `frame_id`，缺失时回退文件名 stem；`camera_id`/文件名 alias 不会写入 tftree 边。
- JSON RPY 按“度制 ZYX”（`Rz(yaw) * Ry(pitch) * Rx(roll)`）转换为四元数 `[qx, qy, qz, qw]`。
- mdrive4-json camera 原始外参按 FLU 表达；tftree graph 内部保存 compute/FLU，不在加载时改写矩阵。
- camera metadata 写入 `order_manifest.yaml.frame_axes`：`compute_axes=FLU`、`projection_axes=RDF`。保存为 YAML 后再次加载仍可派生 projection。
- `get_transform(..., output_mode="compute")` 用于标定文件、尺寸链、保存和 JSON 写回；`get_transform(..., output_mode="projection")` 用于显示、PnP、lidar2cam 投影。
- `load_from_mdrive4_json(..., camera_frame_axes=None)` 不记录 camera axes metadata，仅用于 raw 诊断。
- PB/YAML 缺少 `frame_axes` metadata 时，compute/projection 一致，不做轴系转换；非 camera JSON 外参不做轴系转换。
- JSON 写回只允许由 D03 执行 compute -> JSON FLU 序列化；业务侧不要自行拼 camera JSON RPY/translation。
- V0.0.6 的 JSON 入口输出过 `ego->sensor_id`；需要旧方向语义时继续使用 V0.0.6。

Python 示例：

```python
from tftree_manager import TFTreeManager

manager = TFTreeManager("/path/to/yaml_output")
manager.load_from_mdrive4_json("/path/to/json_dir", root_frame="vrf_ground")
T_projection = manager.get_transform("at128p_front", "front_120", output_mode="projection")
manager.save_to_yaml()
```

CLI：

```bash
tftree-manager load-json /path/to/json_dir --output-dir /path/to/yaml_output
tftree-manager load-json /path/to/json_dir --output-dir /path/to/yaml_output --root-frame vrf_ground
```

官方 root 输出边示例：`front_30->vrf_ground`、`at128p_front->vrf_ground`、`ins->vrf_ground`。

## 输出模式合同

V0.0.12 固化两个稳定输出模式：

- `compute`：默认模式，用于标定文件、尺寸链、direct/manual link 存储、PB/YAML/JSON 写回。
- `projection`：用于 lidar2cam 显示、PnP、图像投影。JSON camera 的 projection 轴系为 OpenCV/RDF。

合同边界：

- graph 内部只存 `compute`，包括从 JSON 加载后的 camera FLU 外参、direct relation、manual link 和 YAML 工作区。
- `projection` 不落盘为 graph 状态，只在 `get_transform(..., output_mode="projection")`、`convert_transform_mode(...)` 或 `input_mode="projection"` 的 D03 API 边界派生/归一。
- JSON camera 的 `compute` 轴系固定为 FLU；JSON camera 的 `projection` 轴系固定为 OpenCV/RDF。
- PB/YAML 无 `order_manifest.yaml.frame_axes` metadata 时，D03 不推测相机轴系，`compute` 与 `projection` 返回一致。
- 下游 C01/C03/C05/F_cursor_skill 等业务侧只选择 `compute` 或 `projection` 语义，不自行做 FLU/RDF 转换。

API：

```python
from tftree_manager import OUTPUT_MODE_COMPUTE, OUTPUT_MODE_PROJECTION, TFTreeManager

manager = TFTreeManager("/path/to/yaml_or_json_derived")
manager.load_from_yaml()

T_compute = manager.get_transform("at128p_front", "front_120")
T_projection = manager.get_transform(
    "at128p_front",
    "front_120",
    output_mode=OUTPUT_MODE_PROJECTION,
)

T_compute_round_trip = manager.convert_transform_mode(
    "at128p_front",
    "front_120",
    T_projection,
    from_mode=OUTPUT_MODE_PROJECTION,
    to_mode=OUTPUT_MODE_COMPUTE,
)
```

PB/YAML 默认接口示例：

```python
from tftree_manager import OUTPUT_MODE_PROJECTION, TFTreeManager

manager = TFTreeManager("/path/to/pb_parsed_yaml")
manager.load_from_yaml()

# 不传 output_mode 等价 compute，保持 PB/YAML 标定语义。
T_pb_compute = manager.get_transform("lidar", "camera")

# 统一调用链路可显式传 projection；无 frame_axes metadata 时结果仍与 compute 一致。
T_pb_projection = manager.get_transform(
    "lidar",
    "camera",
    output_mode=OUTPUT_MODE_PROJECTION,
)
```

转换公式：

`T_target_projection_from_source_projection = R_target_projection_from_target_compute @ T_target_compute_from_source_compute @ R_source_compute_from_source_projection`

`upsert_direct_transform(..., input_mode="projection")` 可接收投影矩阵，D03 会先转换为 compute 后存储。

业务侧禁止项：

- 不要自行实现 FLU/RDF 或 OpenCV/RDF camera 轴系转换。
- 不要直接调用 `tftree_manager.frame_axes.convert_transform_source_axes`；该函数是 D03 内部边界实现，不是业务 API。
- 不要保留“调用新 API 失败后捕获 `TypeError` 再走旧矩阵语义”的 fallback；版本不满足时应直接升级/失败。
- 不要手写 JSON camera 写回序列化；JSON 写回必须走 D03，以保证 compute -> JSON FLU 一致。

## 相邻链路局部求逆

V0.0.6 新增 `TFTreeManager.invert_adjacent_relation(left, right, output_dir=None, save=True)`。

规则：

- 只允许两节点之间存在一条直接边；多跳路径不允许求逆。
- 若实际存在 `left->right`，则翻转为 `right->left`；若实际存在 `right->left`，则翻转为 `left->right`。
- 缺失节点、非相邻节点、同时存在 `A->B` 与 `B->A` 都会抛出 `TFTreeValidationError`。
- 推荐指定 `output_dir`，避免覆盖原始 YAML 工作目录。

```python
from tftree_manager import TFTreeManager

manager = TFTreeManager("/path/to/yaml_dir")
manager.load_from_yaml()
result = manager.invert_adjacent_relation("at128p_front", "vrf", output_dir="/path/to/inverted_yaml")
print(result.original_edge, result.output_edge)
```

## 直接关系重写

V0.0.4 新增 `TFTreeManager.rewrite_direct_relations(update_yaml_path, prune_frames=None, output_dir=None, save=True)`，用于处理 PB 解析后的 YAML 工作目录。C04 负责产出 IPM 自动计算后的直接关系 YAML，`pb_calibration` 继续负责 PB parse/build。

本库已提供的模板能力：

- 读取 `direct_relations.yaml`，校验 `direct_relations` 格式、重复边、反向重复边与无向闭环。
- 删除 `replace_frames` 或 `prune_frames` 命中的旧边，按 update YAML 顺序追加新的直接边。
- 写出重写后的 `extrinsics/*.yaml` 与 `order_manifest.yaml`，并在 `direct_relations_meta` 保留追溯元数据。
- 提供 Python API 与 CLI 两种触发方式。

调用方需要单独开发的接口/适配层：

- 直接关系 YAML 生成接口：业务侧根据 IPM、标定结果或其它计算结果生成 `direct_relations`，本库不负责计算外参。
- PB 工作目录准备接口：业务侧负责调用 `pb_calibration parse`，准备包含 `extrinsics/` 与 `order_manifest.yaml` 的 YAML 工作目录。
- 重写触发接口：业务侧可封装 Python API 或 CLI，把 YAML 工作目录、update YAML、输出目录与错误处理接入 C04 或其它流程。
- PB 回写接口：业务侧继续调用 `pb_calibration build`，把重写后的 YAML 转回 PB；本库不直接读写 PB。

输入 YAML 格式：

```yaml
direct_relations:
  - source_frame: at128p_front
    target_frame: front_120
    link_nodes: [at128p_front, vrf, front_120]
    translation: {x: 0.0, y: 0.0, z: 0.0}
    rotation: {qx: 0.0, qy: 0.0, qz: 0.0, qw: 1.0}
    replace_frames: [front_120]
```

规则：

- `source_frame -> target_frame` 是最终写入 `extrinsics/*.yaml` 的方向，不自动反转。
- `link_nodes` 仅作为原始计算链路元数据保存到 `order_manifest.yaml` 的 `direct_relations_meta`，不写入单条 extrinsic YAML。
- `link_nodes` 长度必须 >= 2，首节点等于 `source_frame`，尾节点等于 `target_frame`。
- 未提供 `replace_frames` 时默认删除 `target_frame` 相关旧边；也可通过 API/CLI 额外传入 `prune_frames`。
- 重写后会校验不存在 `A->B` 与 `B->A` 反向重复边，且无无向闭环；允许森林结构。

推荐接入策略：

- 有输出隔离需求时使用 `output_dir`，避免覆盖 `pb_calibration parse` 生成的原始 YAML。
- 单相机更新时 `replace_frames` 默认填目标相机 frame 即可；显式写出更利于排查。
- 多相机更新时每条 direct relation 都显式写 `replace_frames`，避免误删无关外参。
- 捕获到 `TFTreeValidationError` 时不要继续执行 PB build，应保留原 YAML 并把错误详情返回给上层流程。

Python 示例：

```python
from tftree_manager import TFTreeManager

manager = TFTreeManager("/path/to/yaml_dir")
manager.load_from_yaml()
result = manager.rewrite_direct_relations(
    "/path/to/direct_relations.yaml",
    output_dir="/path/to/rewritten_yaml",
)
print(result.added_relations[0].link_nodes)
```

## 命令行

```bash
# 以 YAML 目录为参数，加载并打印树与环信息
tftree-manager /path/to/yaml_dir

# 按 direct_relations YAML 重写直接边；默认写回 yaml_dir
tftree-manager rewrite-direct /path/to/yaml_dir /path/to/direct_relations.yaml

# 输出到新目录，不修改原 YAML 目录
tftree-manager rewrite-direct /path/to/yaml_dir /path/to/direct_relations.yaml --output-dir /path/to/rewritten_yaml

# 读取 mdrive4 JSON 并导出 YAML
tftree-manager load-json /path/to/json_dir --output-dir /path/to/yaml_output

# 自动识别 A/B 的直接边真实方向并求逆；推荐输出到新目录
tftree-manager invert-edge /path/to/yaml_dir at128p_front vrf --output-dir /path/to/inverted_yaml

# 额外删除指定 frame 相关旧边
tftree-manager rewrite-direct /path/to/yaml_dir /path/to/direct_relations.yaml --prune-frame front_120

# 启动 Flask API 服务（默认 0.0.0.0:5000）
tftree-manager-server
```

环境变量 `TFTREE_DATA_DIR` 可指定 API 默认数据目录（默认 `./data`）。

## 标定链路替换 UI

V0.0.7 中 `prune-ui` 的语义是“标定外参链路替换”：标定程序传入新外参 `source_frame->target_frame` 后，tftree 找到当前树中连接两端点的路径；若两端点直接相邻则自动更新该边；若是多跳路径，则打开 PySide6 UI，只允许用户选择路径上的一条既有链接进行替换。UI 只读取/保存 PB parse 后的 YAML 工作目录，不直接生成 PB，也不会新增或删除边。

### 其它程序接入

其它 Python 程序应把 `launch_prune_ui` 当作稳定公共入口；`tftree_manager.prune_ui.window.PruneWindow` 是当前内部 Qt 实现，暂不承诺作为可嵌入组件的稳定 API。调用方只需要准备三类输入：PB parse 后的 YAML 工作目录、新标定得到的 `Transform` 或 4x4 矩阵、以及这条新外参的 `source_frame/target_frame`。

```python
from pathlib import Path

import numpy as np

from tftree_manager import LinkReplacementResult, Transform, launch_prune_ui


def replace_calibrated_link(yaml_dir: Path, selected_edge: str | None = None) -> LinkReplacementResult:
    T_new = np.eye(4)
    T_new[:3, 3] = [1.2, 0.0, 0.3]
    calibrated = Transform.from_matrix(T_new, "at128p_front", "front_120")

    return launch_prune_ui(
        yaml_dir=str(yaml_dir),
        source_frame="at128p_front",
        target_frame="front_120",
        calibrated_transform=calibrated,
        selected_edge=selected_edge,
        save=True,
        output_dir=None,
        title="标定链路替换",
        layout_mode="auto",
    )
```

接入行为：

- `selected_edge` 不为空时走非 GUI 自动化路径，不需要安装 PySide6。
- `source_frame/target_frame` 直接相邻时自动更新唯一边，也不需要安装 PySide6。
- 多跳路径且未传 `selected_edge` 时才打开 PySide6 UI；图形界面依赖通过 `pip install "tftree-manager[ui]"` 安装。
- 返回值固定为 `LinkReplacementResult`，可检查 `replaced_edge`、`path`、`yaml_dir`、`saved`、`cancelled`、`direct_adjacent`。

Python API：

```python
from tftree_manager import Transform, launch_prune_ui

result = launch_prune_ui(
    yaml_dir="/path/to/yaml_dir",
    source_frame="at128p_front",
    target_frame="front_120",
    calibrated_transform=Transform.from_matrix(T_new, "at128p_front", "front_120"),
    title="标定链路替换",
    layout_mode="auto",
)
print(result.replaced_edge)
```

非 GUI API：

```python
from tftree_manager import TFTreeManager

manager = TFTreeManager("/path/to/yaml_dir")
manager.load_from_yaml()
result = manager.replace_link_for_calibration(
    "at128p_front",
    "front_120",
    T_new,
    selected_edge="vrf->front_120",
)
```

CLI：

```bash
tftree-manager prune-ui /path/to/yaml_dir \
  --update-yaml /path/to/new_calibration.yaml \
  --layout auto

# 自动化/测试入口：不打开 UI，直接更新选中路径边并覆盖 YAML
tftree-manager prune-ui /path/to/yaml_dir \
  --update-yaml /path/to/new_calibration.yaml \
  --selected-edge 'vrf->front_120' \
  --layout tree
```

UI 操作：

- 布局支持 `auto`、`tree`、`star`，右侧按钮可即时切换；`auto` 对 `vrf_ground` 星型图使用中心环绕，否则使用左根右展树形布局。
- 鼠标滚轮按当前位置缩放，右键按住拖拽平移画布。
- 左键点击路径边可选中；非路径边不可选。
- 左键依次点击两个直接相邻节点时，若两点之间的边属于标定路径，会自动选中该边；非相邻节点或非路径相邻边只更新节点高亮，不触发替换。

保存策略：

- 默认直接覆盖 `yaml_dir/extrinsics/*.yaml` 与 `order_manifest.yaml`，不自动备份。
- 保存前校验拓扑不存在反向重复边和无向闭环。
- 只更新被选中的一条既有链接，保留原 key 方向与边顺序，`calib_json/` 等辅助目录不参与修改。

## 依赖

- Python >= 3.10
- numpy >= 1.20.0
- PyYAML >= 5.4.0
- Flask >= 2.0.0（API 与 server 入口需要）
- mdrive4-json >= 0.0.1（`load-json` 与 `load_from_mdrive4_json` 需要）
- PySide6（仅 `prune-ui` 图形界面需要，可通过 `pip install tftree-manager[ui]` 安装）

## License

MIT
