Skip to content

Add H&E tumor-region benchmark for public and private slides - #3

Open
hutaobo wants to merge 1 commit into
mainfrom
cursor/he-tumor-region-benchmark-23fe
Open

hutaobo wants to merge 1 commit into
mainfrom
cursor/he-tumor-region-benchmark-23fe

Conversation

@hutaobo

@hutaobo hutaobo commented Aug 28, 2026 •

Copy link
Copy Markdown
Owner

Summary

Adds a protocol-driven H&E pixel-level tumor-region benchmark so we can (1) reproduce the current strongest bulk/cell methods on public tiles and (2) rank those same methods on private H&E.

The comparison is frozen as a catalog, not a notebook:

  • Must-test bulk models: stain_threshold (weak baseline), plip_fulltile (current product), dino_nested_unet (published tumor-bulk SOTA), uni2_upernet (pathology FM dense head). Optional: uni2_unetr.
  • Separate cell track: segtme_uni2 (neoplastic class). Do not mix into bulk Dice.
  • Decision set: private_he. Public CAMELYON16 / TIGER WSIBULK are sanity checks only.

Foundation-model weights stay out of this repo. GPU boxes dump aligned masks; prediction_dirs scores them fairly next to the always-on heuristic.

How to use

spatho he-benchmark catalog
spatho he-benchmark init --output-dir /path/to/he_benchmark --with-synthetic-fixture
spatho he-benchmark ingest --dataset-id private_he --images HE_TILES --masks TUMOR_MASKS --output-dir /path/to/he_benchmark/datasets/private_he
spatho he-benchmark doctor --protocol /path/to/he_benchmark/protocol.json
spatho he-benchmark run --protocol /path/to/he_benchmark/protocol.json

Private GeoJSON contours can be rasterized with --image + --geojson. Cases without masks still get overlays and inter-model agreement.

Testing

  • python3 -m pytest tests/test_he_benchmark.py -q — 7 passed
  • Full suite: 90 passed
  • CLI smoke: synthetic private case, stain_threshold Dice 0.916 (recall 1.0; dark artifact is an expected false positive)

Synthetic overlay

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Sourcery 摘要

新增一个可复现的 H&E 肿瘤区域基准测试工作流,用于在公开 sanity 数据集和私有切片上比较分割模型。

新功能:

  • 新增由协议驱动的 H&E 肿瘤区域基准测试,覆盖整体分割,以及跨公开和私有数据集的独立肿瘤细胞赛道。
  • 新增 CLI 工作流,用于登记模型、初始化工作区、导入配对图像或 GeoJSON 标注、验证协议以及运行评估。
  • 支持预计算的基础模型掩码,并提供内置的染色阈值和 PLIP 基线,同时不捆绑受限访问的模型权重。
  • 生成以私有数据优先的公开排行榜、逐病例指标、叠加图和模型间一致性报告。

增强功能:

  • 定义冻结的模型和数据集目录,并通过评估规则保持整体分割指标与细胞指标分离,同时优先进行私有切片排名。

文档:

  • 记录 H&E 肿瘤区域基准测试工作流、模型赛道、数据导入格式、预测掩码契约、协议规则和输出内容。
  • 在 README 和文档导航中展示该基准测试。

测试:

  • 新增针对目录验证、分割指标、合成测试固件、数据导入、CLI 操作、排名、跳过的模型和报告生成的基准测试覆盖。
Original summary in English

Sourcery 总结

新增一个可复现的 H&E 肿瘤区域基准测试,用于在公开切片块上验证模型,并在私有切片上对模型进行排名。

新功能:

  • 新增基于协议的 H&E 肿瘤区域基准测试,覆盖整体分割任务,以及在私有和公开数据集上分别进行的肿瘤细胞轨道评测。
  • 提供 CLI 命令,用于登记模型、初始化工作区、导入配对掩码或 GeoJSON 标注、验证协议,以及运行评测;同时支持使用预先计算的预测结果。
  • 生成优先展示私有数据的排行榜、逐病例指标、模型一致性报告和可视化叠加图,同时允许受限的基础模型权重保留在外部。

增强功能:

  • 定义冻结的模型和数据集目录,采用统一的评测规则,并分别统计整体分割指标和细胞级指标。

文档:

  • 在 README 和项目文档中说明 H&E 基准测试的工作流程、支持的模型、数据格式、协议规则和输出内容。

测试:

  • 新增对目录验证、分割指标、合成测试数据、数据导入、CLI 工作流、排名、跳过的模型以及报告生成的基准测试覆盖。
Original summary in English

Summary by Sourcery

Add a reproducible H&E tumor-region benchmark for validating models on public tiles and ranking them on private slides.

New Features:

  • Add a protocol-driven H&E tumor-region benchmark covering bulk segmentation and a separate neoplastic-cell track across private and public datasets.
  • Provide CLI commands to catalog models, initialize workspaces, ingest paired masks or GeoJSON annotations, validate protocols, and run evaluations with optional precomputed predictions.
  • Generate private-first leaderboards, per-case metrics, model-agreement reports, and visual overlays while allowing gated foundation-model weights to remain external.

Enhancements:

  • Define a frozen model and dataset catalog with consistent evaluation rules and separate bulk and cell metrics.

Documentation:

  • Document the H&E benchmark workflow, supported models, data formats, protocol rules, and outputs in the README and project documentation.

Tests:

  • Add benchmark coverage for catalog validation, segmentation metrics, synthetic fixtures, ingestion, CLI workflows, ranking, skipped models, and report generation.

Introduce a protocol-driven comparison of pixel-level tumor bulk models, including Dino-NestedUNet, UNI2 dense heads, SegTME, and the current PLIP/heuristic baselines. Users can ingest their own H&E tiles or GeoJSON, attach GPU-dumped masks, and rank models on private Dice while using public sets only as a sanity check.

Co-authored-by: Taobo Hu <hutaobo@users.noreply.github.com>
@hutaobo
hutaobo marked this pull request as ready for review August 28, 2026 14:36
Copilot AI lite review requested due to automatic review settings August 28, 2026 14:36
@sourcery-ai

sourcery-ai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

审查者指南

新增一个由协议驱动的 H&E 肿瘤区域基准测试,包含独立的整体和细胞赛道、配对数据集/GeoJSON 数据集导入、可选的预计算基础模型预测结果、内置基线、私有数据优先的报告、CLI 工作流、文档以及端到端测试。

H&E 基准测试运行时序列图

sequenceDiagram
    actor User
    participant CLI as spatho_cli
    participant Protocol as he_benchmark_protocol
    participant Models as model_adapters
    participant Metrics as metrics
    participant Report as benchmark_report

    User->>CLI: run --protocol
    CLI->>Protocol: run_benchmark(protocol)
    Protocol->>Protocol: load_protocol(protocol)
    Protocol->>Models: build_segmenters(models, prediction_dirs)
    loop enabled datasets and cases
        Protocol->>Models: available()
        Models-->>Protocol: prediction mask or skipped reason
        Protocol->>Metrics: case_metrics(pred, gt)
        Metrics-->>Protocol: Dice, IoU, precision, recall, HD95
    end
    Protocol->>Report: write_benchmark_report(results)
    Report-->>CLI: leaderboard, case metrics, overlays, agreement
    CLI-->>User: JSON run summary
Loading

基准测试数据集与预测结果实体关系图

erDiagram
    DATASET ||--o{ CASE : contains
    CASE ||--o| GROUND_TRUTH_MASK : has
    CASE ||--o{ PREDICTION_MASK : receives
    MODEL ||--o{ PREDICTION_MASK : produces

    DATASET {
        string dataset_id
        string kind
    }
    CASE {
        string case_id
        string image
        string split
        boolean qualitative_only
    }
    GROUND_TRUTH_MASK {
        string mask_path
    }
    PREDICTION_MASK {
        string model_id
        string mask_path
    }
    MODEL {
        string model_id
        string track
    }
Loading

H&E 基准测试工作区与评估流程图

flowchart LR
    A["spatho he-benchmark init"] --> B["protocol.json + catalog.json"]
    B --> C["ingest paired images/masks or GeoJSON"]
    C --> D["cases.jsonl"]
    D --> E["doctor --protocol"]
    E --> F["run --protocol"]
    P["GPU models"] --> Q["Aligned per-case masks"]
    Q --> F
    F --> G["Private-first leaderboard"]
    F --> H["Per-case metrics"]
    F --> I["Overlays + model agreement"]
Loading

文件级变更

变更 详细信息 文件
引入冻结的目录和协议模型,将整体肿瘤分割与肿瘤细胞评估分开,并优先进行私有切片排名。
  • 定义必需和可选的模型条目、数据集角色、赛道和评估规则。
  • 新增针对模型、数据集、预测目录、指标和部分执行的协议字段校验。
  • 解析相对于协议的数据集和预测路径,并记录协议/目录快照。
src/spatho/he_benchmark/catalog.py
src/spatho/he_benchmark/protocol.py
src/spatho/he_benchmark/__init__.py
新增对配对图像/掩码输入、GeoJSON 肿瘤轮廓、掩码加载以及合成冒烟测试装置的数据集准备支持。
  • 根据文件名主干索引配对的图像和掩码,并生成 cases.jsonl 及元数据。
  • 将带标签的肿瘤 GeoJSON 多边形(包括多多边形和孔洞)栅格化到图像像素上。
  • 创建一个 64×64 的合成私有案例,用于无权重 CLI 测试。
src/spatho/he_benchmark/datasets.py
实现可插拔的基准测试模型适配器,包括始终可用的染色启发式方法,以及可选的预计算或外部模型预测结果。
  • 运行内置的染色阈值基线和粗粒度 PLIP 全图块适配器。
  • 使用案例 ID 或图像主干名,从预测目录加载对齐的 PNG/NPY 掩码。
  • 对于由检查点支持但当前不可用的模型,将其报告为已跳过,而不是捆绑受限权重。
src/spatho/he_benchmark/models.py
新增二值分割指标,以及跨私有和公共数据集的基准测试执行与报告功能。
  • 计算 Dice、IoU、precision、recall、accuracy 和 HD95,并提供逐案例及均值/标准差汇总。
  • 生成私有数据优先/公共数据集排行榜、案例 CSV/JSON 指标、已跳过模型诊断、叠加图以及模型间一致性结果。
  • 提供 doctor 校验和运行编排,并对没有真实标注的案例进行仅定性处理。
src/spatho/he_benchmark/metrics.py
src/spatho/he_benchmark/report.py
src/spatho/he_benchmark/protocol.py
通过 CLI 和项目文档公开基准测试功能,并提供端到端测试覆盖。
  • 新增 catalog、init、ingest、doctor 和 run 子命令。
  • 记录模型赛道、导入格式、预测约定、协议规则和输出内容。
  • 测试目录校验、指标、合成执行、数据导入、CLI 行为、排名、跳过逻辑和报告生成。
src/spatho/cli.py
README.md
docs/DEVELOPMENT_GUIDE.md
docs/HE_TUMOR_REGION_BENCHMARK.md
docs/index.md
tests/test_he_benchmark.py

提示和命令

与 Sourcery 交互

  • 触发新的审查: 在拉取请求中评论 @sourcery-ai review。
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 回复审查评论,请 Sourcery 根据该评论创建 issue。你也可以使用 @sourcery-ai issue 回复审查评论,以从中创建 issue。
  • 生成拉取请求标题: 在拉取请求标题的任意位置写入 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title,以随时生成或重新生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中你希望摘要出现的位置写入 @sourcery-ai summary,即可随时在指定位置生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary,以随时生成或重新生成摘要。
  • 生成审查者指南: 在拉取请求中评论 @sourcery-ai guide,即可随时生成或重新生成审查者指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,即可解决所有 Sourcery 评论。如果你已经处理完所有评论且不想再看到它们,这一功能会很有用。
  • 关闭所有 Sourcery 审查: 在拉取请求中评论 @sourcery-ai dismiss,即可关闭所有现有的 Sourcery 审查。如果你想以新的审查重新开始,这一功能尤其有用——别忘了评论 @sourcery-ai review 以触发新的审查!

自定义你的体验

访问你的控制面板以:

  • 启用或禁用审查功能,例如 Sourcery 生成的拉取请求摘要、审查者指南等。
  • 更改审查语言。
  • 添加、删除或编辑自定义审查说明。
  • 调整其他审查设置。

获取帮助

Original review guide in English

Reviewer's Guide

Adds a protocol-driven H&E tumor-region benchmark with separate bulk and cell tracks, paired/GeoJSON dataset ingestion, optional precomputed foundation-model predictions, built-in baselines, private-first reporting, CLI workflows, documentation, and end-to-end tests.

Sequence diagram for the H&E benchmark run

sequenceDiagram
    actor User
    participant CLI as spatho_cli
    participant Protocol as he_benchmark_protocol
    participant Models as model_adapters
    participant Metrics as metrics
    participant Report as benchmark_report

    User->>CLI: run --protocol
    CLI->>Protocol: run_benchmark(protocol)
    Protocol->>Protocol: load_protocol(protocol)
    Protocol->>Models: build_segmenters(models, prediction_dirs)
    loop enabled datasets and cases
        Protocol->>Models: available()
        Models-->>Protocol: prediction mask or skipped reason
        Protocol->>Metrics: case_metrics(pred, gt)
        Metrics-->>Protocol: Dice, IoU, precision, recall, HD95
    end
    Protocol->>Report: write_benchmark_report(results)
    Report-->>CLI: leaderboard, case metrics, overlays, agreement
    CLI-->>User: JSON run summary
Loading

Entity relationship diagram for benchmark datasets and predictions

erDiagram
    DATASET ||--o{ CASE : contains
    CASE ||--o| GROUND_TRUTH_MASK : has
    CASE ||--o{ PREDICTION_MASK : receives
    MODEL ||--o{ PREDICTION_MASK : produces

    DATASET {
        string dataset_id
        string kind
    }
    CASE {
        string case_id
        string image
        string split
        boolean qualitative_only
    }
    GROUND_TRUTH_MASK {
        string mask_path
    }
    PREDICTION_MASK {
        string model_id
        string mask_path
    }
    MODEL {
        string model_id
        string track
    }
Loading

Flow diagram for H&E benchmark workspace and evaluation

flowchart LR
    A["spatho he-benchmark init"] --> B["protocol.json + catalog.json"]
    B --> C["ingest paired images/masks or GeoJSON"]
    C --> D["cases.jsonl"]
    D --> E["doctor --protocol"]
    E --> F["run --protocol"]
    P["GPU models"] --> Q["Aligned per-case masks"]
    Q --> F
    F --> G["Private-first leaderboard"]
    F --> H["Per-case metrics"]
    F --> I["Overlays + model agreement"]
Loading

File-Level Changes

Change Details Files
Introduces a frozen catalog and protocol model that separates bulk tumor segmentation from neoplastic-cell evaluation and prioritizes private-slide ranking.
  • Defines required and optional model entries, dataset roles, tracks, and evaluation rules.
  • Adds validated protocol fields for models, datasets, prediction directories, metrics, and partial execution.
  • Resolves protocol-relative dataset and prediction paths and records protocol/catalog snapshots.
src/spatho/he_benchmark/catalog.py
src/spatho/he_benchmark/protocol.py
src/spatho/he_benchmark/__init__.py
Adds dataset preparation for paired image/mask inputs, GeoJSON tumor contours, mask loading, and synthetic smoke-test fixtures.
  • Indexes paired images and masks by filename stem and emits cases.jsonl plus metadata.
  • Rasterizes labeled tumor GeoJSON polygons, including multipolygons and holes, onto image pixels.
  • Creates a 64×64 synthetic private case for no-weight CLI testing.
src/spatho/he_benchmark/datasets.py
Implements pluggable benchmark model adapters with an always-available stain heuristic and optional precomputed or external model predictions.
  • Runs the built-in stain-threshold baseline and coarse PLIP full-tile adapter.
  • Loads aligned PNG/NPY masks from prediction directories using case ID or image stem.
  • Reports unavailable checkpoint-backed models as skipped rather than bundling gated weights.
src/spatho/he_benchmark/models.py
Adds binary segmentation metrics and benchmark execution/reporting across private and public datasets.
  • Computes Dice, IoU, precision, recall, accuracy, and HD95 with per-case and mean/std summaries.
  • Generates private-first/public leaderboards, case CSV/JSON metrics, skipped-model diagnostics, overlays, and inter-model agreement.
  • Provides doctor validation and run orchestration with qualitative-only handling for cases without ground truth.
src/spatho/he_benchmark/metrics.py
src/spatho/he_benchmark/report.py
src/spatho/he_benchmark/protocol.py
Exposes the benchmark through the CLI and project documentation, with end-to-end test coverage.
  • Adds catalog, init, ingest, doctor, and run subcommands.
  • Documents model tracks, ingest formats, prediction contracts, protocol rules, and outputs.
  • Tests catalog validation, metrics, synthetic execution, ingestion, CLI behavior, ranking, skipping, and reporting.
src/spatho/cli.py
README.md
docs/DEVELOPMENT_GUIDE.md
docs/HE_TUMOR_REGION_BENCHMARK.md
docs/index.md
tests/test_he_benchmark.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

你好——我发现了 5 个问题

面向 AI Agent 的提示
请处理这次代码审查中的评论:

## 各条评论

### 评论 1
<location path="src/spatho/he_benchmark/protocol.py" line_range="271" />
<code_context>
+
+    leaderboard = _build_leaderboard(case_rows, protocol.models)
+    agreement = _inter_model_agreement(predictions_by_case, protocol.models)
+    report = write_benchmark_report(
+        output_dir=output_dir,
+        protocol=json.loads(protocol.model_dump_json()),
</code_context>
<issue_to_address>
**问题 (bug_risk):** `run_benchmark` 调用 `_inter_model_agreement(predictions_by_case, protocol.models)` 时缺少必需的 `model_ids` 参数,因此每次基准测试在完成用例评分后都会抛出 `TypeError`,且永远不会写入报告。

**建议修复:** 仅当相应地减少函数签名中的参数时,才将 `protocol.models` 作为第二个参数传入;否则,请根据预期 API 提供缺失的第三个参数。
</issue_to_address>

### 评论 2
<location path="src/spatho/he_benchmark/models.py" line_range="107-121" />
<code_context>
+    def available(self) -> tuple[bool, str]:
</code_context>
<issue_to_address>
**问题 (bug_risk):** 当 `PLIPZeroShotContourClassifier` 成功导入时,`PlipFullTileSegmenter.available()` 会报告 PLIP 可运行;但 `predict()` 需要一个不存在的 `classify_array` 方法,导致每个 PLIP 用例都抛出 `RuntimeError`,而不是生成掩码或跳过该模型。

**触发条件:** 启用 `plip_fulltile` 且 `pathology_ai_service` 依赖成功导入时。

**建议修复:** 让分割器适配分类器实际的 `classify` 请求/响应 API,或者在兼容的推理适配器存在之前,让 `available()` 返回 false。
</issue_to_address>

### 评论 3
<location path="src/spatho/he_benchmark/protocol.py" line_range="39" />
<code_context>
+
+    name: str = Field(default="he_tumor_region_v1", min_length=1)
+    tile_size_px: int = Field(default=1024, ge=64, le=4096)
+    models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2"])
+    datasets: list[DatasetRef] = Field(default_factory=list)
+    prediction_dirs: dict[str, Path] = Field(default_factory=dict)
</code_context>
<issue_to_address>
**问题 (broader_impact):** `init_benchmark` 创建的默认协议遗漏了 `plip_fulltile`,尽管模型目录将其标记为必需的 bulk 模型,且基准测试说明称其为必须测试的基线。因此,文档所述的初始化并运行流程不会评估当前产品基线。

**触发条件:** 用户运行 `he-benchmark init`,随后直接运行生成的协议,且未手动编辑模型列表时。

**建议修复:** 将 `plip_fulltile` 加入默认模型列表,或者明确将生成的协议标记为不完整,并要求用户添加该模型。

```suggestion
    models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2", "plip_fulltile"])
```
</issue_to_address>

### 评论 4
<location path="src/spatho/he_benchmark/protocol.py" line_range="292-310" />
<code_context>
+    return summary
+
+
+def _build_leaderboard(case_rows: list[dict[str, Any]], model_ids: list[str]) -> list[dict[str, Any]]:
+    board: list[dict[str, Any]] = []
+    for kind in ("private", "public"):
+        for model_id in model_ids:
+            rows = [
+                row
+                for row in case_rows
+                if row["model_id"] == model_id and row.get("kind") == kind and not row.get("qualitative_only")
+            ]
+            if not rows:
+                continue
+            summary = summarize_metrics(rows)
+            spec = MODEL_CATALOG.get(model_id, {})
+            board.append(
+                {
+                    "kind": kind,
+                    "model_id": model_id,
+                    "track": spec.get("track"),
+                    "role": spec.get("role"),
+                    **summary,
+                }
</code_context>
<issue_to_address>
**问题 (bug_risk):** `_build_leaderboard` 遍历协议中的每个模型,并且不考虑 `spec["track"]`,将它们全部添加到同一个 private/public 排行榜中。因此,`segtme_uni2` 的肿瘤细胞 Dice 会与 bulk 肿瘤模型一起排名,而不是在单独的细胞表中报告。

**触发条件:** 协议包含 `segtme_uni2`,且至少有一个用例具有真实标注时。

**建议修复:** 按 track 对排行榜行进行分区,并将肿瘤细胞 track 与 `pixel_tumor_bulk` 分开呈现。
</issue_to_address>

### 评论 5
<location path="src/spatho/he_benchmark/protocol.py" line_range="148-151" />
<code_context>
+        n_cases = 0
+        n_masks = 0
+        if exists:
+            cases = read_cases_jsonl(dataset.cases_path)
+            n_cases = len(cases)
+            n_masks = sum(1 for case in cases if case.get("mask"))
+            n_scored += n_masks
+        else:
+            if dataset.enabled:
</code_context>
<issue_to_address>
**问题 (bug_risk):** 只要 JSON 记录中的 `mask` 字段为真值,`doctor_benchmark` 就会将该用例计为已评分,而不会检查所引用的图像和掩码文件是否存在。因此,doctor 可能会报告协议已准备就绪,但随后 `iter_cases` 会因 `FileNotFoundError` 崩溃。

**触发条件:** `cases.jsonl` 包含过期或无效的图像/掩码路径时。

**建议修复:** 在 doctor 检查期间解析并验证每个引用的图像和掩码路径,并在允许运行前将缺失文件报告为问题。
</issue_to_address>

Sourcery 评估

需要人工审查。 有 5 个发现需要优先处理;如果基准测试逻辑或掩码处理存在错误,可能会写入误导性的 Dice/排行榜报告,并导致用户选择错误的肿瘤区域模型。回滚会移除该功能,而修正后的代码可以重新生成范围受限的本地报告和预测结果。

阻塞性发现:src/spatho/he_benchmark/protocol.py:271、src/spatho/he_benchmark/models.py:121、src/spatho/he_benchmark/protocol.py:39、src/spatho/he_benchmark/protocol.py:310、src/spatho/he_benchmark/protocol.py:151


Sourcery 对开源项目免费——如果你喜欢我们的审查,请考虑分享 ✨
帮助我变得更有用!请在每条评论上点击 👍 或 👎,我会利用反馈改进审查结果。
Original comment in English

Hey - I've found 5 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/spatho/he_benchmark/protocol.py" line_range="271" />
<code_context>
+
+    leaderboard = _build_leaderboard(case_rows, protocol.models)
+    agreement = _inter_model_agreement(predictions_by_case, protocol.models)
+    report = write_benchmark_report(
+        output_dir=output_dir,
+        protocol=json.loads(protocol.model_dump_json()),
</code_context>
<issue_to_address>
**issue (bug_risk):** `run_benchmark` calls `_inter_model_agreement(predictions_by_case, protocol.models)` without the required `model_ids` argument, so every benchmark run raises `TypeError` after scoring cases and never writes its report.

**Suggested fix:** Pass `protocol.models` as the second argument only if the function signature is reduced accordingly, or supply the missing third argument according to the intended API.
</issue_to_address>

### Comment 2
<location path="src/spatho/he_benchmark/models.py" line_range="107-121" />
<code_context>
+    def available(self) -> tuple[bool, str]:
</code_context>
<issue_to_address>
**issue (bug_risk):** `PlipFullTileSegmenter.available()` reports PLIP as runnable when `PLIPZeroShotContourClassifier` imports, but `predict()` requires a nonexistent `classify_array` method and raises `RuntimeError` for every PLIP case instead of producing a mask or skipping the model.

**Triggers:** When `plip_fulltile` is enabled and the `pathology_ai_service` dependency imports successfully.

**Suggested fix:** Adapt the segmenter to the classifier's actual `classify` request/response API, or make `available()` return false until a compatible inference adapter exists.
</issue_to_address>

### Comment 3
<location path="src/spatho/he_benchmark/protocol.py" line_range="39" />
<code_context>
+
+    name: str = Field(default="he_tumor_region_v1", min_length=1)
+    tile_size_px: int = Field(default=1024, ge=64, le=4096)
+    models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2"])
+    datasets: list[DatasetRef] = Field(default_factory=list)
+    prediction_dirs: dict[str, Path] = Field(default_factory=dict)
</code_context>
<issue_to_address>
**issue (broader_impact):** The default protocol created by `init_benchmark` omits `plip_fulltile`, even though the catalog marks it as a required bulk model and the benchmark description says it is a must-test baseline, so the documented init-and-run workflow never evaluates the current product baseline.

**Triggers:** When users run `he-benchmark init` and then run the generated protocol without manually editing its model list.

**Suggested fix:** Include `plip_fulltile` in the default model list, or explicitly mark the generated protocol as incomplete and require users to add it.

```suggestion
    models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2", "plip_fulltile"])
```
</issue_to_address>

### Comment 4
<location path="src/spatho/he_benchmark/protocol.py" line_range="292-310" />
<code_context>
+    return summary
+
+
+def _build_leaderboard(case_rows: list[dict[str, Any]], model_ids: list[str]) -> list[dict[str, Any]]:
+    board: list[dict[str, Any]] = []
+    for kind in ("private", "public"):
+        for model_id in model_ids:
+            rows = [
+                row
+                for row in case_rows
+                if row["model_id"] == model_id and row.get("kind") == kind and not row.get("qualitative_only")
+            ]
+            if not rows:
+                continue
+            summary = summarize_metrics(rows)
+            spec = MODEL_CATALOG.get(model_id, {})
+            board.append(
+                {
+                    "kind": kind,
+                    "model_id": model_id,
+                    "track": spec.get("track"),
+                    "role": spec.get("role"),
+                    **summary,
+                }
</code_context>
<issue_to_address>
**issue (bug_risk):** `_build_leaderboard` iterates over every protocol model and appends them to the same private/public leaderboard regardless of `spec["track"]`, so `segtme_uni2` neoplastic-cell Dice is ranked alongside bulk tumor models instead of being reported in a separate cell table.

**Triggers:** Whenever `segtme_uni2` is included in a protocol and at least one case has ground truth.

**Suggested fix:** Partition leaderboard rows by track and render the neoplastic-cell track separately from `pixel_tumor_bulk`.
</issue_to_address>

### Comment 5
<location path="src/spatho/he_benchmark/protocol.py" line_range="148-151" />
<code_context>
+        n_cases = 0
+        n_masks = 0
+        if exists:
+            cases = read_cases_jsonl(dataset.cases_path)
+            n_cases = len(cases)
+            n_masks = sum(1 for case in cases if case.get("mask"))
+            n_scored += n_masks
+        else:
+            if dataset.enabled:
</code_context>
<issue_to_address>
**issue (bug_risk):** `doctor_benchmark` counts a case as scored whenever its JSON record has a truthy `mask` field, without checking that the referenced image and mask files exist, so doctor can report a ready protocol that subsequently crashes in `iter_cases` with `FileNotFoundError`.

**Triggers:** When `cases.jsonl` contains stale or invalid image/mask paths.

**Suggested fix:** Resolve and validate every referenced image and mask path during doctor checks, and report missing files as issues before allowing a run.
</issue_to_address>

Sourcery assessment

Needs a human reviewer. 5 findings to address first, and if the benchmark logic or mask handling is wrong, it could write misleading Dice/leaderboard reports and cause users to choose the wrong tumor-region model. Reverting removes the feature, and the bounded local reports and predictions can be regenerated with corrected code.

Blocking findings: src/spatho/he_benchmark/protocol.py:271, src/spatho/he_benchmark/models.py:121, src/spatho/he_benchmark/protocol.py:39, src/spatho/he_benchmark/protocol.py:310, src/spatho/he_benchmark/protocol.py:151


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


leaderboard = _build_leaderboard(case_rows, protocol.models)
agreement = _inter_model_agreement(predictions_by_case, protocol.models)
report = write_benchmark_report(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): run_benchmark 调用 _inter_model_agreement(predictions_by_case, protocol.models) 时缺少必需的 model_ids 参数,因此每次基准测试在完成用例评分后都会抛出 TypeError,且永远不会写入报告。

建议修复: 仅当相应地减少函数签名中的参数时,才将 protocol.models 作为第二个参数传入;否则,请根据预期 API 提供缺失的第三个参数。

Original comment in English

issue (bug_risk): run_benchmark calls _inter_model_agreement(predictions_by_case, protocol.models) without the required model_ids argument, so every benchmark run raises TypeError after scoring cases and never writes its report.

Suggested fix: Pass protocol.models as the second argument only if the function signature is reduced accordingly, or supply the missing third argument according to the intended API.

Comment on lines +107 to +121
def available(self) -> tuple[bool, str]:
try:
from pathology_ai_service.core import PLIPZeroShotContourClassifier
except Exception as exc:
return False, f"PLIP classifier unavailable: {exc}"
return True, "pathology_ai_service.PLIPZeroShotContourClassifier"

def predict(self, image: np.ndarray, *, case: dict[str, Any]) -> np.ndarray:
from pathology_ai_service.core import PLIPZeroShotContourClassifier

classifier = PLIPZeroShotContourClassifier()
rgb = _as_rgb(image)
result = classifier.classify_array(rgb) if hasattr(classifier, "classify_array") else None
if result is None:
raise RuntimeError("PLIP classifier does not expose classify_array; skip this model.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): 当 PLIPZeroShotContourClassifier 成功导入时,PlipFullTileSegmenter.available() 会报告 PLIP 可运行;但 predict() 需要一个不存在的 classify_array 方法,导致每个 PLIP 用例都抛出 RuntimeError,而不是生成掩码或跳过该模型。

触发条件: 启用 plip_fulltile 且 pathology_ai_service 依赖成功导入时。

建议修复: 让分割器适配分类器实际的 classify 请求/响应 API,或者在兼容的推理适配器存在之前,让 available() 返回 false。

Original comment in English

issue (bug_risk): PlipFullTileSegmenter.available() reports PLIP as runnable when PLIPZeroShotContourClassifier imports, but predict() requires a nonexistent classify_array method and raises RuntimeError for every PLIP case instead of producing a mask or skipping the model.

Triggers: When plip_fulltile is enabled and the pathology_ai_service dependency imports successfully.

Suggested fix: Adapt the segmenter to the classifier's actual classify request/response API, or make available() return false until a compatible inference adapter exists.


name: str = Field(default="he_tumor_region_v1", min_length=1)
tile_size_px: int = Field(default=1024, ge=64, le=4096)
models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (broader_impact): init_benchmark 创建的默认协议遗漏了 plip_fulltile,尽管模型目录将其标记为必需的 bulk 模型,且基准测试说明称其为必须测试的基线。因此,文档所述的初始化并运行流程不会评估当前产品基线。

触发条件: 用户运行 he-benchmark init,随后直接运行生成的协议,且未手动编辑模型列表时。

建议修复: 将 plip_fulltile 加入默认模型列表,或者明确将生成的协议标记为不完整,并要求用户添加该模型。

Suggested change
models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2"])
models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2", "plip_fulltile"])
Original comment in English

issue (broader_impact): The default protocol created by init_benchmark omits plip_fulltile, even though the catalog marks it as a required bulk model and the benchmark description says it is a must-test baseline, so the documented init-and-run workflow never evaluates the current product baseline.

Triggers: When users run he-benchmark init and then run the generated protocol without manually editing its model list.

Suggested fix: Include plip_fulltile in the default model list, or explicitly mark the generated protocol as incomplete and require users to add it.

Suggested change
models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2"])
models: list[str] = Field(default_factory=lambda: ["stain_threshold", "dino_nested_unet", "uni2_upernet", "segtme_uni2", "plip_fulltile"])

Comment on lines +292 to +310
def _build_leaderboard(case_rows: list[dict[str, Any]], model_ids: list[str]) -> list[dict[str, Any]]:
board: list[dict[str, Any]] = []
for kind in ("private", "public"):
for model_id in model_ids:
rows = [
row
for row in case_rows
if row["model_id"] == model_id and row.get("kind") == kind and not row.get("qualitative_only")
]
if not rows:
continue
summary = summarize_metrics(rows)
spec = MODEL_CATALOG.get(model_id, {})
board.append(
{
"kind": kind,
"model_id": model_id,
"track": spec.get("track"),
"role": spec.get("role"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): _build_leaderboard 遍历协议中的每个模型,并且不考虑 spec["track"],将它们全部添加到同一个 private/public 排行榜中。因此,segtme_uni2 的肿瘤细胞 Dice 会与 bulk 肿瘤模型一起排名,而不是在单独的细胞表中报告。

触发条件: 协议包含 segtme_uni2,且至少有一个用例具有真实标注时。

建议修复: 按 track 对排行榜行进行分区,并将肿瘤细胞 track 与 pixel_tumor_bulk 分开呈现。

Original comment in English

issue (bug_risk): _build_leaderboard iterates over every protocol model and appends them to the same private/public leaderboard regardless of spec["track"], so segtme_uni2 neoplastic-cell Dice is ranked alongside bulk tumor models instead of being reported in a separate cell table.

Triggers: Whenever segtme_uni2 is included in a protocol and at least one case has ground truth.

Suggested fix: Partition leaderboard rows by track and render the neoplastic-cell track separately from pixel_tumor_bulk.

Comment on lines +148 to +151
cases = read_cases_jsonl(dataset.cases_path)
n_cases = len(cases)
n_masks = sum(1 for case in cases if case.get("mask"))
n_scored += n_masks

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

问题 (bug_risk): 只要 JSON 记录中的 mask 字段为真值,doctor_benchmark 就会将该用例计为已评分,而不会检查所引用的图像和掩码文件是否存在。因此,doctor 可能会报告协议已准备就绪,但随后 iter_cases 会因 FileNotFoundError 崩溃。

触发条件: cases.jsonl 包含过期或无效的图像/掩码路径时。

建议修复: 在 doctor 检查期间解析并验证每个引用的图像和掩码路径,并在允许运行前将缺失文件报告为问题。

Original comment in English

issue (bug_risk): doctor_benchmark counts a case as scored whenever its JSON record has a truthy mask field, without checking that the referenced image and mask files exist, so doctor can report a ready protocol that subsequently crashes in iter_cases with FileNotFoundError.

Triggers: When cases.jsonl contains stale or invalid image/mask paths.

Suggested fix: Resolve and validate every referenced image and mask path during doctor checks, and report missing files as issues before allowing a run.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are a few concrete operational/API issues (GeoJSON rasterization memory blow-up risk, masks_dir validation, and unnecessary in-memory prediction retention) that should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a protocol-driven, CLI-accessible H&E pixel-level tumor-region benchmarking workflow to compare multiple segmentation approaches (heuristic, PLIP baseline, and external/foundation-model mask dumps) with private-slide ranking prioritized and public sets used for sanity checks.

Changes:

  • Introduces the spatho he-benchmark CLI (catalog/init/ingest/doctor/run) plus a protocol model and report generation.
  • Adds dataset ingestion utilities (paired image/mask dirs, single image + GeoJSON rasterization) and pixel-level metrics (Dice/IoU/HD95/etc.).
  • Adds documentation and a new pytest suite for the benchmark workflow.
File summaries
File Description
tests/test_he_benchmark.py Adds end-to-end and unit tests for catalog, metrics, ingestion, and CLI smoke runs.
src/spatho/he_benchmark/report.py Writes leaderboard/case/agreement artifacts and overlay rendering utilities.
src/spatho/he_benchmark/protocol.py Implements protocol schema, init/doctor/run pipeline, leaderboard + agreement computation.
src/spatho/he_benchmark/models.py Adds segmenter adapters (heuristic, PLIP, external mask dirs, checkpoint placeholder).
src/spatho/he_benchmark/metrics.py Adds binary-mask conversion, overlap metrics, and HD95 computation.
src/spatho/he_benchmark/datasets.py Adds ingestion helpers, array IO, synthetic fixture, and GeoJSON rasterization.
src/spatho/he_benchmark/catalog.py Defines the frozen model/dataset catalog and protocol rules.
src/spatho/he_benchmark/init.py Exposes benchmark public API symbols.
src/spatho/cli.py Wires he-benchmark subcommands into the main CLI.
README.md Documents the new benchmark CLI entry points.
docs/index.md Adds the benchmark page to the docs landing page and toctree.
docs/HE_TUMOR_REGION_BENCHMARK.md Provides detailed usage docs, protocol rules, and prediction mask contract.
docs/DEVELOPMENT_GUIDE.md Lists the new CLI command in the development guide.
Review details

Suppressed comments (1)

src/spatho/he_benchmark/datasets.py:193

  • ingest_paired_directories silently treats a missing --masks directory as “no masks” (qualitative-only) because _index_by_stem returns an empty dict when the directory doesn’t exist. If the caller explicitly provides masks_dir, fail fast so a typo/path issue doesn’t produce an all-qualitative dataset.
    images_dir = Path(images_dir).expanduser().resolve()
    output_dir = Path(output_dir).expanduser().resolve()
    image_index = _index_by_stem(images_dir, IMAGE_SUFFIXES)
    mask_index = _index_by_stem(Path(masks_dir).expanduser().resolve(), MASK_SUFFIXES) if masks_dir else {}
    if not image_index:
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +111 to +113
ys, xs = np.mgrid[0:height, 0:width]
points = np.stack([xs.ravel(), ys.ravel()], axis=1)
mask = np.zeros(points.shape[0], dtype=bool)
Comment on lines +235 to +248
case_id = str(loaded["case_id"])
predictions_by_case.setdefault(case_id, {})
wrote_overlay = False
for model_id, segmenter in runnable.items():
pred = segmenter.predict(image, case=loaded)
predictions_by_case[case_id][model_id] = pred
row: dict[str, Any] = {
"case_id": case_id,
"dataset_id": dataset.dataset_id,
"kind": dataset.kind,
"model_id": model_id,
"qualitative_only": bool(gt is None or loaded.get("qualitative_only")),
"image": loaded.get("image"),
}
Comment on lines +155 to +158
"skipped_models_json": str(skipped_json),
"protocol_snapshot_json": str(protocol_json),
"n_overlays": str(len(overlays)),
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants