Repository navigation
feat(dynacell): foreground-limited pixel metrics (opt-in) - #517
Merged
Merged
Conversation
foreground_weight builds a soft [0, 1] weight map from the GT alone (smooth_otsu: Gaussian smooth -> Otsu -> Gaussian feather, physical sigmas; otsu: Spotlight v1's raw Otsu mask). foreground_pixel_metrics scores FG_PCC, FG_SI_NRMSE, FG_SI_PSNR and FG_SI_SSIM against it with a weighted least-squares affine fit, and FG_SI_SSIM by normalized convolution, so nothing is zeroed or eroded. compute_pixel_metrics adds the columns only when given a foreground config. A unit weight reproduces PCC/SI_SSIM/SI_NRMSE/SI_PSNR to <= 1e-6 on the golden crop. Empty foreground -> NaN with FG_frac 0; a constant prediction scores FG_PCC 0 instead of vanishing from a mean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…split Four of the 24 lite A549 SEC61B/TOMM20 mock GT FOVs start with two all-zero planes. Otsu then splits padding from signal and calls ~94% of the volume foreground, and smoothing blurs the padding into its neighbours, which form the same outlier class (0.90 at 0.5 um). Exactly constant planes are now left out of the threshold and of the foreground, and smoothing averages over signal planes only (normalized convolution) when padding is present; otherwise it is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
pixel_metrics.foreground {enabled: false, source, smooth_sigma_um,
feather_sigma_um} turns on the FG_* columns. Null sigmas take the
per-target defaults chosen on DynaCell-lite figures
(FOREGROUND_SIGMAS_UM: 0.5 um smoothing for nucleus, membrane and
mitochondria, 1.0 um for ER; 0.5 um feather). The resolved recipe is
stamped in metrics_provenance.json, and the final-metrics cache gate
requires both the matching stamp and the FG_* columns, so turning the
columns on or off, or changing a sigma, recomputes. The eval prints how
many (FOV, t) rows had no GT foreground.
Off by default: pixel/mask CSV+NPY and the provenance sidecar are
byte-identical to ba71f4c on the cache-only fixture.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
FG_PCC, FG_SI_SSIM and FG_SI_PSNR join HIGHER_IS_BETTER, so to_latex bolds them in the direction of their whole-image counterparts (FG_SI_NRMSE stays lower-is-better). They stay out of PIXEL_METRICS: default tables are unchanged and FG columns appear only when asked for. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
In float64 a unit weight reproduces PCC/SI_SSIM/SI_NRMSE/SI_PSNR to machine precision (<= 7e-16 on a real A549 volume, CPU and GPU), so the golden-crop invariant now runs at 1e-12 there and at 1e-6 in float32, where both sides only round. Mutating the SSIM window (truncate 4.0) or K2 by 3e-6 relative fails both cases. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…e feather weighs dycheck normalizes by the mask-pixel count and averages over every pixel; FG_SI_SSIM normalizes by the Gaussian weight mass and averages with the weight. A feathered weight also weighs the object edges, so the FG columns reward object placement as well as interior agreement (synthetic: uncorrelated interior texture inside the right blobs scores FG_PCC 0.83 with the 0.5 um feather, -0.03 on a hard mask). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Foreground edge cases can include padded planes or terminate evaluation with division-by-zero errors.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Adds opt-in foreground-limited pixel metrics to DynaCell evaluation.
Changes:
- Implements weighted foreground metric computation.
- Adds configuration, provenance, and cache invalidation.
- Adds reporting support and comprehensive tests.
| File | Description |
|---|---|
test_reporting_tables_extended.py |
Tests foreground metric ranking. |
test_evaluation_pipeline.py |
Updates pipeline stubs. |
test_evaluation_foreground_pipeline.py |
Tests pipeline integration and caching. |
test_evaluation_foreground_metrics.py |
Tests foreground metric behavior. |
reporting/tables.py |
Defines foreground ranking direction. |
evaluation/README.md |
Documents the feature. |
evaluation/provenance.py |
Records foreground recipes. |
evaluation/provenance_test.py |
Tests provenance matching. |
evaluation/pipeline.py |
Integrates foreground scoring and cache validation. |
evaluation/metrics.py |
Implements foreground weights and metrics. |
evaluation/_configs/eval.yaml |
Adds default-off configuration. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The unit-weight test cannot tell how the weight enters the metrics: with a weight of 1 everywhere, dropping the local weight-mass normalization in the SSIM, averaging the SSIM map unweighted over w > 0, or using an unweighted Pearson over w > 0 all give the same values. Score a small volume with a weight spanning [0, 1] against an independent per-voxel implementation of the definitions (weighted Gaussian-window moments normalized by local weight mass, weighted map average over skimage's crop, weighted Pearson, weighted affine fit). Each of those three mutations now fails the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…SIM border crop _weighted_ssim divided by the weight left after skimage's border crop. A GT whose foreground lies entirely in the outer planes (e.g. a bright patch in z 0-2 of a 16-plane stack) leaves no weight there and raised ZeroDivisionError, killing the FOV. Return NaN for FG_SI_SSIM in that case; the other FG_* columns do not crop and stay defined. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
foreground_weight zeroed the padded planes in the binary mask, then feathered it, which blurred the neighbouring planes' foreground back into the padding (up to ~0.4 weight per voxel, 8-13% of the total weight with three padded planes). Those planes carry no GT signal, so the FG_* columns scored the prediction against padding. Zero them again after feathering. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…eground A target constant where weight > 0.5 but varying in the feather passes the weighted-std guard, so data_range came out 0 and FG_SI_NRMSE raised ZeroDivisionError. The SI columns are relative to that range and are undefined there; return NaN for them, as the degenerate-input policy says, and keep FG_PCC, which the weighted std still defines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…ance floor _CONSTANT_VARIANCE = 1e-12 compared the raw prediction variance with an absolute threshold, so FG_PCC was not affine invariant: a correct centred prediction scaled by 1e-7 scored FG_PCC 0 instead of its unscaled value. Call the prediction constant only when every voxel with weight > 0 holds the same value (or the weighted variance underflows to 0), which is scale-free and still scores a collapsed prediction FG_PCC 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
foreground_weight ignores smooth_sigma_um under source "otsu", yet _foreground_settings still resolved it from FOREGROUND_SIGMAS_UM and stamped it: an otsu run on a target with no default raised for a value it never uses, and changing that unused value invalidated the cache. Resolve and stamp smooth_sigma_um only for smooth_otsu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
An unknown pixel_metrics.foreground.source or a negative sigma was only rejected inside foreground_weight, i.e. per FOV after the segmenter and feature extractors had loaded. Validate both in _foreground_settings and resolve it first thing in _final_metrics_cache_valid, ahead of its early returns: evaluate_model and the grouped driver both call that gate before any model load, so a bad recipe now fails in seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
…und recipe
The pixel_foreground stamp held only {source, smooth_sigma_um,
feather_sigma_um}, but FG_* values also depend on dynacell code (padding
exclusion, hard threshold, SSIM window and crop, affine fit, data range)
and on pixel_metrics.spacing, which turns the physical sigmas into voxel
sigmas. A fix to that code, or a spacing change, would have reused caches
scored the old way. Add FOREGROUND_METRICS_VERSION to metrics.py (bump on
any value-changing change) and stamp it and the spacing beside the recipe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
metrics_provenance_matches required the recorded pixel_foreground to equal the run's, so a run with the columns off rejected a cache written with them on and reran the whole final-metrics pass for nothing: every column it asks for is there, and the extra FG_* columns cost it nothing. Compare the stamp only when the run asks for FG columns. The README and eval.yaml also said toggling the block recomputes "the pixel metrics"; the gate reruns the whole final-metrics pass, and only turning it on or changing it does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
A grouped condition overlay could set pixel_metrics.foreground.*, so one bucket could hold conditions scored with and without the FG_* columns, or with different recipes, and a cross-condition table would compare FG values over different regions without any error. Require every condition to share the base config's pixel_metrics.foreground, checked beside the model-loading invariants. Only that block: pixel_metrics.spacing still follows each condition's dataset_ref. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
The README and the pipeline test said the FG_* columns are appended, but compute_pixel_metrics adds them right after the SI_* columns, so in a production row they precede Spectral_PCC, the FSC columns, PerCell_* and MicroMS3IM. State the real position, and make the test check only that the row holds every previous column plus the FG_* ones; it passed by order only because its fixture computes none of the later columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
FG_frac is the GT foreground fraction, a descriptor of the evaluation data that is identical across models scored on the same GT, but to_latex treated every column outside HIGHER_IS_BETTER as lower-is-better and bolded its minimum as the "best" model. Skip columns in a new UNRANKED set, holding FG_frac, when bolding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds opt-in foreground-limited pixel metrics to the dynacell evaluation:
FG_PCC,FG_SI_SSIM,FG_SI_NRMSE,FG_SI_PSNRandFG_fracinpixel_metrics.csv, behind a newpixel_metrics.foregroundblock that is off by default. With it off, the pixel/mask CSVs, NPYs, provenance file and a GPU spectral run are byte-identical toba71f4cb.Motivation: Spotlight v1 (arXiv:2507.05383) reports PSNR/MicroMS3IM/FRC on the foreground because the background dominates the voxel count. Its evaluation code zeroes the background of both images and runs the whole-volume metric. That creates sharp edges, and it inflates PSNR by exactly 10·log10(1/FG_frac). This PR measures the foreground region without modifying the images.
Design
w, from the GT only, so every model is scored on the same region.win [0, 1].target_name. Grouped leaves settarget_namedirectly and never load the target config group.source: otsu, Spotlight's mask) is available for comparison.FG_PCCis a w-weighted Pearson.FG_SI_NRMSEandFG_SI_PSNRuse a w-weighted scale-invariant affine fit and weighted MSE, withdata_rangetaken from the GT inside the hard foreground.FG_SI_SSIM: normalized (partial) convolution SSIM in the spirit of dycheck (Gao et al. 2022).metrics.pystays import-light, as in perf(dynacell/eval): keep torch off the light evaluation import path #510.FG_PCC,FG_SI_SSIMandFG_SI_PSNRrank higher-is-better. FG columns are not added to the default metric lists.Validation
Caveats
feather_sigma_um: 0scores the interior only.dynacell evaluaterun with FG on has been done yet. It is covered by the cache-only pipeline fixture and offline scoring.provenance_test.pyare order-dependent when run in one process afterlive_pipeline_module()users. The defaulttestpathsnever hits this.Tests
-n 11): 3568 passed, 175 skipped, 0 failed.provenance_test23 passed when run alone.🤖 Generated with Claude Code
https://claude.ai/code/session_01Gwn9hwgT8uJSoWaCxCAm8J