Skip to content

Refuse negative mask labels (background was counted as an object) - #40

Merged
guijacquemet merged 2 commits into
mainfrom
claude/negative-mask-labels
Oct 30, 2026
Merged

guijacquemet merged 2 commits into
mainfrom
claude/negative-mask-labels

Conversation

@guijacquemet

Copy link
Copy Markdown
Contributor

Summary

Fixes #35.

  • masks.py: new check_labels(pixels, name) raises ValueError when a mask holds a negative value; read_mask calls it.
  • mask_scan.py: piece_counts and pair_counts call it too, so 0 is always the background (it can only be dropped as "the smallest value" once negatives are refused).
  • cli.py: the masks command catches the ValueError from scan_masks, prints it with the mask's path and exits 1. No report is written.
  • Docs: contract §15.1 and user guide §5/§9 say that objects have positive labels and that a negative label is refused when the pixels are read.

Why

At v0.3.1, if an int16 mask held -1, 0 and 3, piece_counts returned the labels [-1, 0, 3]. That made the background an object, and the mask kinds, object counts and mask_relationships.json came out wrong with no warning. The header check cannot see pixel values, so a signed dtype is still accepted there. The refusal happens when the pixels are read. This follows the existing rule that a mask which is not 2D when read raises an error.

Notes for review

  • The refusal is a ValueError, not a ValidationIssue, because the header-only find_masks never reads pixels.
  • Error codes and the manifest are unchanged.

Tests

  • New regression tests:
    • tests/test_mask_scan.py::test_negative_labels_are_refused_never_read_as_objects
    • tests/test_masks.py::test_masks_command_refuses_negative_labels
  • Both fail on main ("DID NOT RAISE ValueError" and exit code 0 instead of 1). On main, piece_counts of the -1/0/3 mask returns [-1, 0, 3].
  • Full suite (Python 3.13, zarr 3.4.2, notebook extra installed): 1053 passed, 1 skipped. The skip is test_discovery.py:472: running as root ignores file modes. On main the same environment gives 1051 passed, 1 skipped.
  • ruff check . and ruff format --check . are clean.
  • Not run: Windows, macOS, Python 3.11 / zarr 2.18.

Generated by Claude Code

…object

A signed mask holding a negative value made piece_counts keep 0 as a label
(0 was dropped only when it was the smallest value), so the background became
an object and the mask kinds, counts and mask_relationships.json were wrong
with no warning. Label 0 is the background and objects are positive
(contract §15): read_mask, piece_counts and pair_counts now raise ValueError
for negative labels, and the masks command prints the message and exits 1
without writing a report.

Fixes #35

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Scma6W3LC11s3qjXHZuGDm
…labels

# Conflicts:
#	src/morphobricks_preprocessing/mask_scan.py
#	src/morphobricks_preprocessing/masks.py
#	tests/test_mask_scan.py
#	tests/test_masks.py

Copy link
Copy Markdown
Contributor Author

Merged main (#42, #43) in: check_labels now runs on the plane #43's read_mask reads, so docstrings, contract §15 and the CLI comment describe both refusals, and both sides' tests are kept. I added a regression test showing that negative labels in a later time point of a TYX mask are refused. 1075 passed, 1 skipped (test_discovery.py:472, root), ruff clean, CI 7/7 green on b8a1998. Merging.


Generated by Claude Code

@guijacquemet
guijacquemet merged commit 7205829 into main Oct 30, 2026
7 checks passed
guijacquemet added a commit that referenced this pull request Oct 30, 2026
Version 0.3.2: data-flow audit fixes (PRs #40–#47)
@guijacquemet
guijacquemet deleted the claude/negative-mask-labels branch November 1, 2026 18:05
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.

A signed mask with a negative label makes the background an object

2 participants