Skip to content

same_objects is a one-to-one matching; size and plane-count warnings follow the confirmed roles (#67, #74) - #131

Merged
guijacquemet merged 9 commits into
mainfrom
claude/fix-sameobjects-roles
Nov 2, 2026
Merged

guijacquemet merged 9 commits into
mainfrom
claude/fix-sameobjects-roles

Conversation

@guijacquemet

@guijacquemet guijacquemet commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #67, fixes #74

Both issues are fully fixed and tested. Version stays 0.4.0rc3. The branch is merged with main at feec007 (#129, #130).

#67: same_objects uses a one-to-one matching

Defect. pair_counts counted every child and parent that had any partner at or above same_objects_iou. A 2:1 split therefore counted as 2/2 children and 1/1 parents matched, which gave same_objects.

Rule (deterministic, documented in contract §15.1 and plan question 67). For each plane, the matches are a maximum bipartite matching of the child/parent pairs whose IoU is at least the threshold. It is computed with scipy.sparse.csgraph.maximum_bipartite_matching (Hopcroft-Karp) on a sparse graph, so no K x K matrix is built. scipy was already a dependency.

  • Each object is matched at most once.
  • Every maximum matching has the same size, so the counts do not depend on label numbers or tie-breaking.
  • Above IoU 0.5 an object can overlap only one partner that much, so every eligible pair is matched. At the default threshold, real one-to-one segmentations give the same counts as before.

New evidence keys: children_not_matched, parents_not_matched, children_over_iou_with_several_parents, parents_over_iou_with_several_children. children_matched_one_to_one and parents_matched_one_to_one are kept, and are now equal. mask_relationships.json is now report version 2. The CLI masks command and the notebook both use scan_masks, so they get the same rule.

HPMEC (MorphoRelate ea19bb0, data/hpmec_perfusion, read-only; outputs in the scratch dir). The report is the same before and after, apart from the new keys and the version. This was checked again after the merge with main:

Run Before After
default masks nuclei inside cells 146/146, cells 0/1/2/3+ nuclei = 0/146/0/0; mito grouped_by cells 146/146; same_objects [] identical
+ a copy of cells as a 4th type (scratch copy) cells/cellscopy same_objects, matched 146/146 identical, not_matched 0/0, several 0/0
--same-objects-iou 0.1 nuclei-cells matched 115/115, mito-cells 107/107 identical, several-partner counts 0

#74: size and plane-count warnings follow the confirmed roles

Defect. different_sizes and different_plane_counts compared header letters, including those of plain stacks, whose letters are not their roles. AxisReview.combined_issues kept these warnings after confirmation, so they were stale. Example: plain (Y, X, C) arrays read as QYX and confirmed as YXC missed a real height difference (16 vs 32) and reported "planes along Q".

Behaviour now:

  • Before confirmation (check, preflight): the axes each header names are compared, and a plain stack is left out. Nothing is guessed.
  • After confirmation: combined_issues (used by run, the notebook's run and the conversion plan) drops the header versions. It recomputes both warnings with each image's confirmed roles (metadata.dimension_issues).

The checks moved to a new pure module, size_checks.py, so metadata.py shrinks. One existing expectation changed (test_channel_count_follows_the_confirmed_channel_axis): IYX confirmed with I as C no longer reports different_plane_counts, which is correct. The notebook's axis card still shows only the axis review's own issues; the corrected warnings appear in the run result (plan question 68).

Plan questions: #128 (64, 65) and #133 (66) merged first, so this PR uses 67 and 68 (checked in review after merging main 491150a); #132 follows after them.

Tests that fail on main (checked against feec007)

  • tests/test_mask_matching.py: all 27 fail on main (7 tests, plus 20 brute-force reference cases).
    • test_a_split_segmentation_is_not_the_same_objects fails on the relation itself: main says same_objects, with matched 16/8.
    • The other tests fail on the missing counts or the missing one_to_one_matches.
  • tests/test_role_sizes.py: 8 of 8 fail on main (QYX→YXC, QYX→ZYX/TYX, IYX→TYX, named+plain, plain TIFF + OME-Zarr, run_experiment).
  • tests/test_axes.py::test_channel_count_follows_the_confirmed_channel_axis (updated) fails on main.

Gate (local, after merging main feec007)

  • ruff check and ruff format --check pass (186 files).
  • tools/check.py --rules passes (exit 0).
  • pytest with .[dev,notebook,zarr]: 1887 passed, 1 skipped (review gate after its fixes: 1889 passed, 1 skipped) (test_discovery.py:665, running as root, as expected).

§9 figures (against main feec007)

  • File lines: mask_scan.py 519→574, metadata.py 779→747 (756 before review), axes.py 464→477, size_checks.py 139 (new, pure).
  • New and changed functions, by executable lines: pair_counts 47, one_to_one_matches 5, _match_counts 13, dimension_issues 11, combined_issues 5, plane_count_issues 25, image_size_issues 14; check_metadata 97→96. Every function has at most 4 parameters, annotations and docstrings.
  • Baseline: 43→41 problem entries. Removed: _check_image_sizes and _check_plane_counts (undocstringed). Lowered: check_metadata 97→96 and metadata.py 779→756. Nothing added.
  • # noqa: unchanged (ANN 53, PLR0913 3).
  • No performance claim is made, so no benchmark was run.

Review fixes

https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc


Generated by Claude Code

The one-to-one counts took every object with any partner at the IoU
threshold, so two children halving one parent counted as two matches and
could give same_objects. The matches are now a maximum bipartite matching
of the pairs at or above the threshold (Hopcroft-Karp on a sparse graph,
per plane); its size is unique, so the counts are deterministic. The
evidence adds the unmatched objects and the objects with several partners
at the threshold. Report version 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
different_sizes and different_plane_counts compared the header letters,
also of plain stacks whose letters are not their roles: a plain QYX stack
of (Y, X, C) arrays confirmed as YXC was compared by its X and channels
(a real height difference missed) and gave "planes along Q". Preflight
now leaves plain stacks out; AxisReview.combined_issues drops the header
versions and recomputes both warnings with each image's confirmed roles
(header letters for images with an axis error or without a mapping). The
checks move to the pure size_checks module (metadata.py shrinks).

Docs: contract 11 and 15.1, user guide, plan questions 60 and 61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Conflicts: plan questions (main has 60 to 63; this branch's questions
become 66 and 67, 64 and 65 being taken by #128) and the code-rule
layers of pyproject.toml (size_checks and ngff_scale, both pure).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet marked this pull request as ready for review November 2, 2026 20:18
Plan questions 64 and 65 are now #128's; this PR's become 66 and 67.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
Plan question 66 is now #133's; this PR's become 67 and 68. Baseline: this PR's
lower metadata.py figures, #133's removal of the physical_sizes entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet merged commit 86121db into main Nov 2, 2026
7 checks passed
@guijacquemet
guijacquemet deleted the claude/fix-sameobjects-roles branch November 2, 2026 21:11
@guijacquemet
guijacquemet restored the claude/fix-sameobjects-roles branch November 2, 2026 21:11

Copy link
Copy Markdown
Contributor Author

Independent review: merged as 86121db (merge commit; branch restored onto the PR head 4d2d0e2).

Verified

  • [Audit/Correctness] same_objects counts many-to-one IoU overlaps as one-to-one matches #67: one_to_one_matches is a true maximum matching. It matched a brute force on 400 random bipartite graphs (0 mismatches). pair_counts on 300 random 6x6 label pairs at IoU 0.1/0.25/0.34/0.5 gave matched, unmatched and several-partner counts equal to a brute force over the IoU edges (0 mismatches). The matching counts don't change when labels are renumbered at random. The issue's 2:1 example now gives 1 match, 1 child unmatched and 1 parent with several children. Every new test in test_mask_matching.py (27) fails on old main, and 8 of the original test_role_sizes.py tests do too.
  • Report version 2 is documented in contract §15.1, the plan and the user guide. No code in the other bricks under /home/user reads same_objects or mask_relationships.json. The only mention outside this repo is MorphoFeature docs/decisions/m0-decision-ledger.md (D79, a document). Nothing outside this repo was edited.
  • HPMEC (MorphoRelate ea19bb0, read-only), masks at IoU 0.5 and 0.1 on main vs. branch: the reports are identical apart from report_version and the four new keys. Several-partner counts are 0 everywhere.
  • [Audit/Correctness] Dataset size/plane-count warnings ignore confirmed axes of plain TIFF stacks #74: the warnings are recomputed from the confirmed per-file roles, and the header versions are dropped, so no stale or duplicated warnings remain. Plain stacks are left out before confirmation. Mixed named, plain and OME-Zarr sets are covered. An OME-Zarr image whose confirmed roles contradict its OME-NGFF axes (no file mapping) and a TIFF with an axis error are measured by their headers. I added tests for both.

Review commits

§9 figures (touched files)

  • Largest touched file: metadata.py at 747 lines. It is a baseline exemption that shrank from 779 on old main. mask_scan.py is 574 lines, axes.py 477 and size_checks.py 139 (new, pure).
  • Largest function touched: check_metadata, 96 executable lines (baseline, 97→96). Among new functions the largest is pair_counts at 47.
  • Highest complexity touched: 7 (check_metadata). New functions are at most 4.
  • Most parameters: 4.
  • Every function is annotated, and the public ones have docstrings.
  • Baseline: only shrank (43→41 problem entries, lines 779→747). # noqa counts in the changed modules are unchanged at 0.
  • No performance claim, so no benchmark.

Gate (own worktree and uv venv, .[dev,notebook,zarr], after the last merge of main 491150a)

Not addressed (existing behaviour, outside #67's scope)
pair_counts picks each child's best parent by lexsort((-shared, kid)). When a child lies exactly half in each of two parents, the lower parent index wins, so inside and parents_with_N_children can depend on label numbering. The matching counts don't. No issue was filed; this is left for the owner to triage.


Generated by Claude Code

guijacquemet pushed a commit that referenced this pull request Nov 2, 2026
Conflict in plan §12 only: main's questions 67 and 68 kept, PP-12's
question renumbered 69, its references in §10 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G3qp9AxjpGPw9xfWAUJNqc
@guijacquemet
guijacquemet deleted the claude/fix-sameobjects-roles branch November 14, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants