# 手术室耗材监控系统 基于 **手术室主视角录像(MP4)** 与 **术中商品 Excel**,对术中耗材可能出现的时段进行端到端推断:先做 **Phase 1 — 全局时序切分**(VideoSwin 视频特征 + ActionFormer),再对每个时段做 **Phase 2 — 细粒度段内推断**(Ultralytics YOLO:手部 ROI、好坏帧、耗材 Top-K、撕膜模型门控);可选地在全部段落跑完后执行 **撕膜相邻段合并**(`tear_merge`),最终将每个 ActionFormer 段对应的主结果写成 **TSV**。 本项目定位为 **离线推理包**:配置集中在 YAML,Python 源码中不写死业务路径;`src/` 负责解析配置与编排,`code/` 为算法脚本与第三方子树的导出目录。 --- ## 目录 - [能力与适用场景](#能力与适用场景) - [流水线概览](#流水线概览) - [环境与依赖](#环境与依赖) - [安装](#安装) - [准备权重与输入](#准备权重与输入) - [运行方式](#运行方式) - [配置文件逐项说明](#配置文件逐项说明) - [输出文件与列含义](#输出文件与列含义) - [目录结构](#目录结构) - [与上游开发仓库的关系](#与上游开发仓库的关系) - [ActionFormer 子项目](#actionformer-子项目) - [常见问题与排错](#常见问题与排错) - [许可证](#许可证) --- ## 能力与适用场景 - **输入**:单路术中主视角视频;Excel 中含商品名称等与视频画面中文案/条码对应的字段,流程会用它构建「名称 → 商品编码」映射;可选 JSON 白名单限制耗材分类的输出类别集合。 - **输出**:对每个 ActionFormer 时段一行,给出耗材 **Top 1/2/3** 名称与置信度,以及映射后的 **product_id**;默认不写出撕膜类名列(见下文)。 - **典型用途**:手术视频质检、耗材使用回溯、与院内物资系统对接前的中间结果导出。 不在本 README 范围内的内容:模型训练脚本、数据采集规范、院内部署与鉴权——这些通常在全量开发仓库中维护。 --- ## 流水线概览 ### Phase 1:时段提案(ActionSegmenter) 1. 在工作目录 `work/input/` 下放一份视频的副本(文件名与stem一致)。 2. 调用 `run_feature_extraction`:用 **torchvision Swin3D-T** 预训练 backbone 抽取视频特征,写出 `features/*.npy` 与 `meta.json`。 3. 生成单视频推理所需的 JSON/YAML(`infer_single.json` / `infer_single.yaml`)。 4. 子进程调用 **ActionFormer** 的 `eval.py`,载入 `weights.actionformer` 的检查点,得到 `eval_results.pkl`。 5. 从 pkl 解析时段列表,依次应用: - 分数阈值 `phase1.af_min_score` - 互斥贪心合并(与同仓库逻辑一致) - 最短时长过滤 `phase1.af_min_seg_seconds` ### Phase 2:段内细粒度推断 对每个存活时段 `(start_sec, end_sec)`: - 用 OpenCV `VideoCapture` 在区间内按 `phase2.frame_stride` 抽样帧。 - **手部检测**:`weights.hand`,置信度 `phase2.det_conf`,检测框经 `phase2.pad_ratio` 等与 `HandRoiGrouper` 合并规则处理。 - **FineGrainedClassifier**:在同一 ROI 链路中依次调用好坏帧、耗材分类、撕膜模型;阈值由 `classification.*` 与重试逻辑控制;耗材输出会按 Excel/JSON **白名单** 过滤允许的类别索引。 若某段推断失败(如无法得到有效手部/分类结果),仍会输出一行,`top1_name` 等字段会填入 **可读原因字符串**(见「输出文件」)。 ### 可选:撕膜相邻段合并(`tear_merge`) 当 `tear_merge.merge_adjacent_tear: true` 时,在全部段初步写表后: - 用撕膜模型对相邻段是否在「撕膜」行为上达成一致做门控;可用 `tear_merge_weights` 指定与推理阶段不同的权重。 - 合并后的段落可能 **复用已缓存的细胞格**(同起止时间与 key);若不存在缓存则会对合并窗 **全量再推理一次**(日志会有 `[tear_merge] 合并窗段全量重推理`)。 ### 简要数据流(概念图) ```mermaid flowchart LR V[MP4 + Excel] --> P1[VideoSwin 特征] P1 --> AF[ActionFormer 提案时段] AF --> P2[YOLO 手/好坏帧/耗材/撕膜] P2 --> TM{撕膜合并?} TM -->|是| OUT[TSV] TM -->|否| OUT ``` --- ## 环境与依赖 | 组件 | 说明 | |------|------| | **操作系统** | Linux 为主(开发验证环境);若在 Windows/macOS 上跑,需注意路径与子进程脚本兼容性。 | | **Python** | 建议 **3.10+**(与 PyTorch 2.x、第三方库的常见组合一致)。 | | **CUDA / PyTorch** | 需 **与你的显卡驱动匹配的** PyTorch / torchvision。**务必先**按 [PyTorch 官方指引](https://pytorch.org/get-started/locally/) 安装 `torch`、`torchvision`,再 `pip install -r requirements.txt`,避免 pip 拉上不匹配的 cu 版本。 | | **磁盘** | 工作目录中会暂存特征 `.npy`、中间 json/yaml、pkl;长视频与工作目录设置在固定路径时注意预留空间。 | `requirements.txt` 中的 **Python 包**(节选含义): - `torch` / `torchvision`:特征与后端(版本行仅作下限,实际应与你的 CUDA 环境一致)。 - `ultralytics`:YOLOv8 系 API,加载 `.pt` 手部与分类权重。 - `opencv-python`:读视频、`seek`/抽帧。 - `pandas`、`openpyxl`:读取 Excel。 - `PyYAML`:解析 `configs/*.yaml`。 此外还有 **editable 安装的本地包**: - `code/actionformer_release/libs/utils`:含 **NMS 等 C++/CPU 扩展**,ActionFormer `eval.py` 依赖;必须在仓库根目录执行 `pip install -e ...`(见下文)。 --- ## 安装 ```bash git clone <你的仓库 URL> cd <仓库目录> # 推荐使用 venv / conda;以下为 venv 示例 python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate # 1)先安装与 CUDA 匹配的 torch / torchvision(略,见官网) # 2)再安装本仓库列出的其余依赖 pip install -r requirements.txt # 3)编译安装 ActionFormer 侧 NMS 扩展(在仓库根目录执行) pip install -e code/actionformer_release/libs/utils ``` 要点: - **`pip install -e`** 的安装目录必须是 **`code/actionformer_release/libs/utils`**(该目录下有 `setup.py`),且在 **已从仓库根 `import torch` 无报错** 的前提下执行。 - 若升级了 PyTorch,有时需要对该扩展 **重新安装**。 --- ## 准备权重与输入 ### 权重(放入 `weights/`,或由 YAML 指到任意绝对路径) 本流程需要 **5 个** Ultralytics/YOLO 或 ActionFormer 检查点文件: | YAML 配置键 | 建议文件名 | 作用 | |-------------|------------|------| | `weights.actionformer` | `actionformer_epoch_045.pth.tar` | Phase 1 ActionFormer 推理 | | `weights.hand` | `hand_detect.pt` | 手部检测 | | `weights.goodbad` | `goodbad_frame.pt` | 好坏帧二分类 | | `weights.haocai` | `haocai_classify.pt` | 耗材多类分类 | | `weights.tear` | `tear_classify.pt` | 撕膜相关(段内门控 + 可选合并阶段) | **VideoSwin** 使用 torchvision 自带的 **Swin3D-T** ImageNet/Kinetics 侧预训练权重,**不**要求用户再提供单独权重文件。 若你仍能从完整开发仓库拷贝文件,可参考以下 **相对路径**(来自内部交接文档,仅供找回文件时对照;完整仓库不一定随本 pack 分发): | 配置键 | 建议文件名 | 开发仓库中参考路径(若存在) | |--------|------------|------------------------------| | `weights.actionformer` | `actionformer_epoch_045.pth.tar` | `code/video_clip_cls/runs/actionformer_ckpt/haocai_main_perspective_videoswin_haocai_main_perspective_videoswin/epoch_045.pth.tar` | | `weights.hand` | `hand_detect.pt` | `code/hand_detection/runs/hand_det_y11s_multiframe-better/weights/best.pt` | | `weights.goodbad` | `goodbad_frame.pt` | `code/goodORbad_frame/runs/goodbad_frame_y11m_e50/weights/best.pt` | | `weights.haocai` | `haocai_classify.pt` | `code/haocai_classify/runs/haocai_cls_41cls_goodframe_721_e50_p8-0.96/weights/best.pt` | | `weights.tear` | `tear_classify.pt` | `code/tear_classify/runs/tear_cls_bicls_y11m_e50_ds0511/weights/best.pt` | ### Excel 与白名单 - **`io.excel`**:用于 `e2e.load_product_code_map` 等逻辑构建 **耗材名称 ↔ 编码**;表格结构需与你的训练/线上约定一致。 - **`io.whitelist_json: null`**:启用 **从 Excel 第一张工作表的 C 列**读取一串「允许出现的商品名称」,再映射到分类 head 的允许类别索引(与线上一致)。 - **`io.whitelist_json`** 若为路径:JSON 形如 `{"allowed_names":["名称A","名称B",...]}`。 ### 视频路径建议 配置里 **`io.video` / `io.excel` / `io.out`** 既可写 **相对路径**(相对 **仓库根**,即包含 `main.py` 的那一层),也可写绝对路径。 上传至 GitHub 的仓库中建议 **不要将个人机上的绝对路径** 提交进 `configs/default_config.yaml`;可使用 `input/xxx.mp4` 占位,每台机器本地化修改或使用独立 `configs/local.yaml`(并加入 `.gitignore`)。 --- ## 运行方式 ```bash # 默认读取 configs/default_config.yaml python main.py # 指定配置文件 python main.py --config configs/my_run.yaml ``` **命令行接口**(`main.py`): | 参数 | 含义 | |------|------| | `--config` | YAML 配置文件路径;默认 `configs/default_config.yaml`。 | 运行前,`config.py` 会将 YAML 中相对路径一律 **-resolve 为基于仓库根的绝对路径**;`runtime.python` 为 `null` 时使用 **`sys.executable`** 调用子进程(特征提取与 ActionFormer eval),从而在 venv 内保持一致解释器。 **环境变量**(入口已设默认): - `OPENCV_FFMPEG_LOGLEVEL`:降低 OpenCV FFmpeg 后端日志冗长度;可按需在外层 shell 覆盖。 --- ## 配置文件逐项说明 主模板见 [`configs/default_config.yaml`](configs/default_config.yaml)。以下与 `src/config.py` 的映射一致。 ### `io` | 键 | 类型 | 说明 | |----|------|------| | `video` | 路径 | 输入 MP4 | | `excel` | 路径 | 输入 Excel | | `out` | 路径 | 输出 TSV(父目录会自动创建) | | `whitelist_json` | `null` 或路径 | `null` 表示从 Excel 第一表 C 列读白名单;否则为 JSON 文件路径 | ### `weights` 五个模型文件路径,缺失时进程会在校验阶段报错并打印「缺少 xxx」。 ### `runtime` | 键 | 说明 | |----|------| | `work_dir` | `null`:使用系统临时目录(进程结束默认删除);非 `null`:固定中间结果目录 | | `keep_work_dir` | `true` 且无 `work_dir` 时,使用 `tempfile.mkdtemp` 并 **保留** 目录(日志会打印路径) | | `python` | `null`/`""`:子进程使用当前解释器;否则填绝对路径可用于多环境隔离 | ### `device` | 键 | 说明 | |----|------| | `type` | `cuda` 或 `cpu`(CPU 仅能验证逻辑,速度慢) | | `half` | `true` 时向 Ultralytics `predict` 传入半精度以降低显存(依 GPU 支持情况而定) | ### `phase1`(ActionFormer + 特征) | 键 | 说明 | |----|------| | `af_min_score` | 保留提案段的分数下限 | | `af_min_seg_seconds` | 段长(秒)下限;`<=0` 可关闭最短段过滤 | | `feat_batch_size` | VideoSwin 特征批大小;显存紧张时可设为 `1` | ### `phase2`(解码与检测) | 键 | 说明 | |----|------| | `seek_margin_sec` | 打开段落时在起止两侧的 seek 留白,减轻关键帧不齐带来的边界效应 | | `frame_stride` | 抽帧步长,`>=1` | | `det_conf` | 手部检测置信度阈值 | | `pad_ratio` | 手部框缩放/填充比例(与 `_pad_box` 等一致) | | `imgsz_det` | YOLO 检测输入分辨率 | | `merge_iou_gt` | ROI 合并:IoU 相关阈值 | | `merge_center_dist_max_px` | 合并:中心距离像素上限,`null` 表示不用该项 | | `merge_center_dist_max_frac_diag` | 合并:中心距离相对对角线比例上限,`null` 表示不用 | ### `classification` | 键 | 说明 | |----|------| | `imgsz_cls` | 分类支路输入尺寸 | | `good_top1_conf_threshold` | 「好帧」Top1 置信度门槛 | | `good_top1_retry_threshold` | 重试时使用的好帧阈值(与段内 retry 逻辑配合) | | `haocai_min_conf` | 耗材 Top1 主阈值 | | `haocai_min_conf_retry` | 第二次尝试的耗材阈值;**`<=0` 关闭**二段重试;若 **≥ `haocai_min_conf`** 则配置会被视为等价于关闭重试 | | `empty_cache_every` | 每隔若干步 `torch.cuda.empty_cache()`,`0` 表示不调用 | ### `tear_merge` | 键 | 说明 | |----|------| | `merge_adjacent_tear` | 是否在写表前做相邻撕膜导向的合并 | | `tear_merge_weights` | `null` 则用 `weights.tear`;否则指定合并专用权重路径 | | `tear_merge_class` | 撕膜类别名(与 `tear` 模型 `names` 对齐),默认 `"tearing"` | | `tear_merge_head_sec` | 合并判定时观察的段首秒数 | | `tear_merge_prob` | 撕膜概率阈值 | | `tear_merge_min_frames` | 最少帧数要求 | | `tear_merge_verbose` | 是否打印冗余日志 | | `tear_merge_full_frame` | `true` 时合并路径用全帧而非手部分支(按需实验) | ### `output` | 键 | 说明 | |----|------| | `legacy_12_col_only` | `true`(默认):**12 列** TSV,**不含** `tear_top1_name`、`tear_top2_name`;`false`:14 列,保留撕膜 Top2 名称列 | ### `doctor_identity` | 键 | 说明 | |----|------| | `enabled` | 是否启用医生身份识别后处理;`true` 时会在结果文件末尾追加 `医生信息:...` | | `checkpoint` | 医生识别模型权重路径,默认 `doctor_identity_package/doctor_info.pth` | | `labels_csv` | `person_id -> 医生姓名` 映射表,默认 `doctor_identity_package/labels.csv` | | `middle_seconds` | 取视频中间窗口长度(秒),默认 `10.0` | | `sample_fps` | 中间窗口采样帧率,默认 `3.0` | | `pad_frac` | 人体框外扩比例,默认 `0.15` | --- ## 输出文件与列含义 - **分隔符**:制表符 **Tab**。 - **编码**:UTF-8。 - **表头**:与 `legacy_12_col_only` 一致。 **12 列(默认)**:`rank`、`start_sec`、`end_sec`、`product_id_top1`、`top1_name`、`top1_conf`、`product_id_top2`、`top2_name`、`top2_conf`、`product_id_top3`、`top3_name`、`top3_conf`。 **14 列**:在末尾增加 `tear_top1_name`、`tear_top2_name`。 说明: - `product_id_*` 由 Excel(商品表)映射;若耗材名存在但表中无映射,仍会输出名称,编码为空,_stderr 可能出现 **「商品表无名称…」** 警告。 - 推断失败行仍会占用一行:`top1_name`(及对应 conf 为空)常为 **可读失败原因**,便于离线筛查。 - 文件末尾会额外追加一行医生识别结果,格式固定为:`医生信息:XXX`。例如:`医生信息:付玉峰 (id=24503, conf=0.9969)`。 --- ## 目录结构 ``` . ├── main.py # CLI:读 YAML,调用编排 ├── requirements.txt ├── README.md # 本文件 ├── README_handoff.md # 简短交接备忘(可与本 README 互为补充) ├── configs/ │ └── default_config.yaml # 默认配置模板 ├── src/ │ ├── __init__.py │ ├── paths.py # 将 code/video_clip_cls/scripts 等加入 sys.path │ ├── config.py # YAML -> argparse.Namespace(SimpleNamespace) │ ├── pack_utils.py # Excel 白名单加载等共用工具 │ ├── orchestrator.py # PipelineManager:主流程 │ └── actionformer_utils.py # Phase1 ActionSegmenter ├── weights/ # 用户提供 .pth.tar / .pt(勿提交大文件时用 .gitignore) ├── input/ ├── output/ ├── data/ └── code/ # 算法导出树(一般不改) ├── repo_root.py ├── dataset.py ├── video_clip_cls/ # VideoSwin 提特征、e2e、pipeline 脚本… │ └── ... └── actionformer_release/ # ActionFormer 上游 + 本项目 yaml / train / eval ├── README.md ├── INSTALL.md └── LICENSE ``` `src/paths.py` 会校验 `code/repo_root.py` 存在,并依次 prepend:`video_clip_cls/infer_single_0506`、`video_clip_cls/scripts`、`code` 根,以便 `import run_haocai_actionformer_consumables_e2e`、`import pipeline.segment_processor` 等板块与原版脚本一致。 --- ## 与上游开发仓库的关系 - 本 **`code/`** 目录常为从更大研发仓库 **剪出的子树**;**编排入口**已从散落的 argparse 收口为:**YAML → `src/config.py` → `src/orchestrator.py`**。 - 若在段内推断或合并逻辑上与历史行为不一致,可对照 **`code/video_clip_cls/scripts/main_pipeline.py`**(同一 `PipelineManager` 思想)。 - 更偏「给谁解压就能跑」的清单式说明可见 [`README_handoff.md`](README_handoff.md)。 --- ## ActionFormer 子项目 [`code/actionformer_release/`](code/actionformer_release/) 基于 ActionFormer 公开代码演进而来: - **训练 / 数据集 / FAQ**:见其 [`README.md`](code/actionformer_release/README.md)、[`FAQ.md`](code/actionformer_release/FAQ.md)、[`INSTALL.md`](code/actionformer_release/INSTALL.md)。 - **许可证**:[`LICENSE`](code/actionformer_release/LICENSE)。 本 README 不重述上游论文公式与训练指令;仅强调 **推理路径**依赖 `nms_1d_cpu` 的可编辑安装。 --- ## 常见问题与排错 | 现象 | 可能原因与处理 | |------|----------------| | Import / `nms` 相关错误(eval 阶段) | 未安装扩展:在仓库根执行 `pip install -e code/actionformer_release/libs/utils`;确认先能 `python -c "import torch"`。 | | `找不到视频` / `找不到 Excel` | 检查 YAML 路径;相对路径是相对 **仓库根**,不是当前 shell 工作目录层级错误。 | | `缺少手部检测` 等 | `weights/` 下文件名与 YAML 不一致,或拷贝不完整。 | | CUDA OOM | 减小 `feat_batch_size`;设 `device.half: true`;或缩短试运行视频长度。 | | CPU 太慢 | Phase1 特征 + AF 在长视频上单核/单进程极慢属预期;仅建议短 clip 冒烟。 | | OpenCV 无法打开 MP4 | 检查文件是否损坏、编码是否罕见;必要时用 ffmpeg 重封装为 H.264 MP4。 | | 商品编码总为空 | 检查 Excel 表结构是否与 `load_product_code_map` 假设一致;名称字符串是否与模型输出完全一致(空格、别名)。 | --- ## 许可证 - **ActionFormer 相关代码**:以 [`code/actionformer_release/LICENSE`](code/actionformer_release/LICENSE) 为准。 - **本仓库其余部分**:若在 GitHub 公开,建议在仓库根 **自行添加** `LICENSE`(公司内部可保留专有许可),并与 **PyTorch / Ultralytics / OpenCV** 等依赖的开源许可证要求自行核对后再分发模型权重。 若需英文版 README、Dockerfile 或示例 `configs/ci_smoke.yaml`,可以单独提出要求再补。