Metadata-Version: 2.4
Name: socbag-manager
Version: 0.0.9
Summary: soc bag 目录发现、路径解析与 L2/L3 标准格式归口
License-Expression: MIT
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: PyYAML>=5.4.0
Requires-Dist: transform-base>=0.0.5
Requires-Dist: tftree-manager>=0.1.8
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: build; extra == "dev"
Requires-Dist: twine; extra == "dev"

# socbag-manager

soc bag 目录的**发现、路径解析与 L2/L3 数据格式归口**库。

当 `data_source.json` 声明 `topic_channel_inspection_artifact` 时，L2 标准化会把它与 `topic_channel_inspection.json` 作为不可分割的 metadata pair，校验 SOC 内相对路径和 SHA-256，并在标准 L2 保留完全一致的 sidecar 字节。静态 L3 根目录保留这两个文件，同时继续在 `env/` 归档；动态流程不创建 L3。

V0.0.8 起，L2 标准化与 L2→L3 静态单帧提取由 socbag-manager 统一管理；V0.0.9 起，INS 坐标、姿态和高度计算全部归口 `transform-base>=0.0.5`。原始 `.record` / `.mcap` 解码仍由现有 Docker / mkit 工具负责。

## 版本与输入模式（V0.0.9）

| 模式 | 输入含义 | 目录结构 |
|------|----------|----------|
| **multi** | 多 `base_paths` + `bag_path` | `camera/{name}/data/{frame}.png`、`lidar/{name}/pcd/{frame}.pcd` |
| **single** | 单一路径（如 `.../static_data`）或 bag 根 | `camera/{sensor}_{frame}.png`、`lidar/{sensor}_{frame}.pcd`（平铺） |

通过配置 **input_mode: "multi" \| "single"** 决定使用哪套发现与解析逻辑，两种格式分别对应、互不混用。

- **multi**：与 V0.0.3/V0.0.4 一致，在多数据根下按 priority 查找同一 bag；子目录 `data`/`pcd`。
- **single**：将给定路径视为父目录时用 `discover_bags_under` 扫描其下符合约定的 soc 子目录；每个 bag 根内为**平铺文件**（如 `camera/front_120_000005.png`）。可选依赖 bag 根下 `extract_manifest.yaml`（`query_frame`、`extracted[].sensor/type`）做发现。

**single 目录示例**：`.../static_data/20260208-160115_soc2/` 下存在 `camera/front_120_000005.png`、`lidar/at128p_front_000005.pcd` 等；可含 `extract_manifest.yaml`。

V0.0.6 起，single 平铺帧号按数字等价匹配但保留真实文件帧号：用户传入 `5`、`000005` 或 `0000000005`，都可解析到磁盘上的 `front_120_0000000005.jpg`、`at128p_front_0000000005.pcd`；`discover_sequences_from_bag_root(..., layout="single")` 仍返回真实文件帧号。

V0.0.7 起，single 布局下即使 `extract_manifest.yaml` 只有 `query_frame` / `matched_frame_stems` / `copied_files` 而没有 `extracted` 列表，camera/lidar 发现与路径解析也会回退扫描 `camera/*.jpg|png`、`lidar/*.pcd`。

V0.0.8 新增 L2/L3 格式归口：

- `pb_baseline` L2：`lidar/{sensor}/pcd/{frame}.pcd`、`camera/{sensor}/data/{frame}.png|jpg` 或 `camera/{mapped}_{cameraN}_{frame}.jpg|png`、`ins/by/by_ins_data*.csv`。
- `mdrive4_mcap` L2：`normalize_l2_soc_to_pb_baseline(..., output_dir=None)` 默认在原 SOC 目录归档 `org_data/`，同级生成 PB-baseline 布局与 `normalize_manifest.json`。
- `mdrive4-json` raw/org L2 标准化只读取 `ins/` 当前目录的 `*.pb.txt`，合并为 `ins/by/by_ins_data_utm.csv`、`ins/by/by_ins_data.tum.csv`；时间只使用 `header.tick / 1e6`，不回退 `header.timestamp`；每条记录通过 D01/PyProj 使用 GRS80 计算自身 UTM zone/hemisphere，z/up 使用 `position.height + geoid_undulation`，姿态固定为 `Rz_Ry_Rx` (`qz * qy * qx`)；只写入 `INS_POS_TYPE_RTK_FIXED` 且字段完整的记录。全部记录 zone/hemisphere 一致时才生成 `by_ins_data_enu.csv`，否则 manifest 写入 `skipped_mixed_utm_zones`。raw INS pbtxt 仍只保留在 `org_data/ins/`。
- IMU 只读取 `imu/` 当前目录的 `*.pb.txt`，使用 `header.timestamp / 1e6` 排序并输出无表头七列 CSV；时间保留 6 位小数，其余数据保留 9 位小数。
- `mdrive4-json` org → `pb_base`：`convert_to_pb_base` / `socbag-org2pb-base` 作为 legacy/debug 入口保留；支持 `front30/1/{frame}.jpg`、`lidar/{sensor}/pcd/{frame}.pcd` 等 mkit org 结构，输出 `org2pb_base_manifest.json,target_format=pb_base`。
- L3 单帧输出：`lidar/{sensor}_{real_frame}.pcd`、`camera/{sensor}_{real_frame}.png|jpg`、`env/`、`extract_manifest.yaml`。
- 默认策略：完整 MCAP workflow 只消费 `02_parsed_frames/{vehicle_model_id}/{type}/{soc_name}` 这一处 L2 完成态；该目录内 `org_data/` 保存 raw mkit 输出，同级满足 PB-baseline。
- 下游 L3、分类、报告第一批标准只消费 `pb_baseline/normalize_manifest.json`。`pb_base/org2pb_base_manifest.json` 是 legacy/debug 产物，不能直接作为 `extract_l2_to_l3` 或报告输入；需要显式转换到 PB-baseline 合同后再消费。

### mdrive4-json INS 时间戳合并维护锁

`socbag_manager.mdrive4_ins_trajectory.write_mdrive4_ins_csvs_from_pbtxt(...)` 是 `mdrive4-json` raw/org INS pbtxt 合并到 PB-baseline `ins/by` 的当前固化入口。该入口的时间戳合同如下：

- 输入目录应是 raw/org INS pbtxt 目录，例如标准化后的 `org_data/ins/`。
- 只读取当前目录 `*.pb.txt`，不消费子目录，再按解析出的时间排序输出。
- 输出时间戳只来自 `header.tick / 1_000_000.0`，并格式化为六位小数。
- 不允许回退使用 `header.timestamp`；缺 `header.tick` 的记录应计入 `missing_header_tick` 并跳过。
- 只合并 `INS_POS_TYPE_RTK_FIXED` 且经纬高、欧拉角字段完整的记录。

2026-07-08 已用 `ECAR_HW4_XZT500021/dynamic_data/20260703-213808_mcap` 的 `org_data/ins` 做 timestamp 一致性验证：8425 个 INS pbtxt 与指定 C++ 输出在六位小数 timestamp key 上完全重叠，`cpp_only=0`、`socbag_only=0`，时间范围均为 `1783085888.437000 ~ 1783085972.677000`。V0.0.9 保留该时间合同，但 z 使用修正后的 `height + geoid_undulation`，不复刻参考 C++ 的漏解析。

## 目录约定与 soc 规则

- **soc 规则**（multi/single 共用）：目录被视为 soc bag 当且仅当：**同时存在 `camera/` 与 `lidar/` 子目录，且该目录总大小 ≥ 阈值（默认 1MB）**。不依赖文件夹名；名称模式 `bag_name_pattern` 为可选过滤。
- **multi**：`{bag_root}/camera/{camera_name}/data/{sequence}.png`（或 .jpg），`{bag_root}/lidar/{lidar_name}/pcd/{sequence}.pcd`。
- **single**：`{bag_root}/camera/{sensor}_{frame}.png`，`{bag_root}/lidar/{sensor}_{frame}.pcd`。

## 安装

```bash
pip install socbag-manager
```

## 统一设置

所有 API 基于 `SocbagManagerSettings`，可从代码构造或 YAML 加载（兼容 C03 `base_paths`/`priority_base_path` 与 C04 `data_dirs`/`default_main_dir`）：

```python
from socbag_manager import SocbagManagerSettings

settings = SocbagManagerSettings(
    base_paths={"pandar": "/data/pandar", "at128": "/data/at128"},
    priority_source="pandar",
    bag_name_pattern="*_soc*",
    input_mode="multi",  # 或 "single"
)
# 或
settings = SocbagManagerSettings.from_yaml("/path/to/config.yaml")
```

## 主要 API

- **发现（multi）**: `get_ordered_base_paths`, `discover_bags`, `discover_bags_from_scan_root`, `discover_cameras`, `discover_lidars`, `discover_sequences`
- **发现（single）**: `discover_bags_under(parent_dir, bag_name_pattern="*_soc*", max_depth=1, min_size_mb=None)`；`discover_sequences_from_bag_root(bag_root, layout=...)`, `discover_cameras_from_bag_root`, `discover_lidars_from_bag_root`（传 `layout="single"` 或由 settings.input_mode 决定）
- **规则**：`is_soc_bag_dir(dir_path, min_size_mb)` — 判定目录是否符合 soc 解构
- **路径解析**: `resolve_camera_paths`, `resolve_lidar_paths`, `resolve_frame_paths`（按 input_mode 选 multi/single 路径）；`resolve_frame_paths_from_bag_root(bag_root, sequence, ..., layout=...)`
- **候选枚举**: `list_frame_candidates`, `choose_one_candidate`（`CandidatePath`）
- **目录树**: `TreeNode`, `build_folder_tree(settings, scope="roots"|"bags"|"sequences", ...)`
- **L2/L3 格式归口**:
  - `SocbagFormat`
  - `inspect_l2_soc(soc_dir, source_format="auto")`
  - `normalize_l2_soc_to_pb_baseline(input_dir, source_format=..., vehicle_model_id=..., input_data_type=...)`
  - `read_l2_frame(soc_dir, frame, source_format="pb_baseline")`
  - `write_l3_single_frame(frame_bundle, output_dir, env_policy=...)`
  - `extract_l2_to_l3(data_dir, single_frame_root, vehicle_model_id=..., input_data_type=..., query_frame=...)`

## 使用示例

### multi 模式

```python
from socbag_manager import (
    SocbagManagerSettings,
    discover_bags,
    discover_sequences,
    resolve_frame_paths,
    build_folder_tree,
)

settings = SocbagManagerSettings.from_yaml("config.yaml")
bag_names = discover_bags(settings)
sequences = discover_sequences(settings, bag_names[0])
paths = resolve_frame_paths(settings, bag_names[0], sequences[0])
# paths["images"], paths["pcds"]

tree_roots = build_folder_tree(settings, scope="bags")
```

### single 模式

```python
from socbag_manager import (
    SocbagManagerSettings,
    discover_bags_under,
    discover_sequences_from_bag_root,
    resolve_frame_paths_from_bag_root,
)

settings = SocbagManagerSettings(
    base_paths={"main": "/path/to/static_data_parent"},
    input_mode="single",
)
static_data_dir = "/media/mini/T9/03_single_frame/AB6_HW3_XZB600019/static_data"
bag_roots = discover_bags_under(static_data_dir, max_depth=1, min_size_mb=0)
for bag_root in bag_roots:
    sequences = discover_sequences_from_bag_root(bag_root, layout="single")
    if sequences:
        paths = resolve_frame_paths_from_bag_root(bag_root, sequences[0], layout="single")
        # paths["images"], paths["pcds"] 与 multi 格式一致
```

YAML 可选：`input_mode: single`、`static_data_dir: /path/to/static_data`（加载后可用 `discover_bags_under(settings.static_data_dir)`）。

### L2 标准化与 L3 单帧提取

```python
from socbag_manager import (
    SocbagFormat,
    extract_l2_to_l3,
    normalize_l2_soc_to_pb_baseline,
)

normalize_l2_soc_to_pb_baseline(
    "/path/to/mdrive4_l2/soc1",
    source_format="mdrive4-json",
    vehicle_model_id="CAR_MODEL",
    input_data_type="mdrive4-json",
    output_dir=None,
)

extract_l2_to_l3(
    "/path/to/mdrive4_l2/soc1",
    "/path/to/static_data",
    vehicle_model_id="CAR_MODEL",
    input_data_type="mdrive4-json",
    query_frame="000005",
    source_format="pb_baseline",
)
```

CLI：

```bash
socbag-normalize-l2 /path/to/l2/soc1 \
  --source-format mdrive4_mcap \
  --vehicle-model-id CAR_MODEL \
  --input-data-type mdrive4-json

socbag-extract-single-frame /path/to/l2/soc1 /path/to/static_data \
  --vehicle-model-id CAR_MODEL \
  --input-data-type mdrive4-json \
  --query-frame 000005 \
  --source-format pb_baseline
```

NAS workflow 建议：

- PB `.record` 解码后调用 `socbag-normalize-l2 --source-format pb_baseline` 做校验和 `normalize_manifest.json` 补齐，再调用 `socbag-extract-single-frame`。
- Mdrive4 `.mcap` 经 mkit parse 后，调用 `socbag-normalize-l2 --source-format mdrive4-json` 在原 SOC 目录转换：原始内容移动到 `org_data/`，同级生成 PB-baseline `camera/`、`lidar/`、`ins/`、`imu/`、`gnss/` 与 `normalize_manifest.json`；其中 `ins/by/by_ins_data_enu.csv` 由标准化阶段从 raw INS pbtxt 生成，供后续 INS 分类直接消费。
- `socbag-org2pb-base` / `convert_to_pb_base` 仅作为 legacy/debug 入口保留，不作为分类、L3 或报告输入；发现 `org2pb_base_manifest.json` 时应停止并提示转换，而不是自动当作 PB-baseline。
- 动态链路只要求 L2 标准化时可复用 inspect/normalize；是否进入 L3 由上层 guard 决定。

## 约束

- 发现/解析接口仅返回路径，不读取/解析图像或点云文件。
- L2/L3 归口接口会移动/硬链接文件和写 manifest，但不解析图像或点云内容；mdrive4 默认就地标准化不创建 sibling `*_pb_baseline` 或 L3 下 `_normalized_l2`。
- 依赖：Python >=3.10，PyYAML，transform-base>=0.0.5。

## 开发与测试

```bash
pip install -e ".[dev]"
python -m pytest tests/ -v
```
