Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix(Normalization): Denormalize with target stats when present#977

Merged
melisande-c merged 8 commits into
CAREamics:mainfrom
parallelArchitect:fix-denormalize-uses-target-stats
Jul 2, 2026
Merged

Fix(Normalization): Denormalize with target stats when present#977
melisande-c merged 8 commits into
CAREamics:mainfrom
parallelArchitect:fix-denormalize-uses-target-stats

Conversation

@parallelArchitect

@parallelArchitect parallelArchitect commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Disclaimer

  • I am an AI agent.
  • I have used AI and I thoroughly reviewed every line.
  • I have not used AI extensively.

Description

Note

tldr: denormalize() now correctly uses target statistics instead of input statistics when both are provided.

Background - why do we need this PR?

Both denormalize() methods unconditionally used input statistics to reverse-normalize the model's output. The model predicts values in the target distribution, not the input distribution. When input and target have different channel counts, this causes a ValueError in broadcast_stats. When channel counts match, the bug is silent but produces wrong values — the output is scaled to the input range instead of the target range.

Overview - what changed?

denormalize() in both classes now checks for target stats first and falls back to input stats only when none were provided, preserving existing behavior.

Implementation - how did you implement the changes?

Added a conditional check at the top of each denormalize() method — if target_means/target_stds (or target_mins/target_maxes for RangeNormalization) are not None, use them; otherwise fall back to input stats. No changes to the normalization forward pass or any other methods.

Changes Made

denormalize() in src/careamics/dataset/normalization/mean_std_normalization.py
denormalize() in src/careamics/dataset/normalization/range_normalization.py

New features or files

  • test_denormalize_uses_target_stats_not_input_stats in tests/dataset/normalization/test_mean_std_normalization.py
    test_denormalize_uses_target_range_not_input_range in tests/dataset/normalization/test_minmax_normalization.py
    ...

How has this been tested?

Both regression tests reproduce the exact failure case from the issue (mismatched input/target channel counts), confirm the fix produces correct values, and verify backward compatibility when no target stats are provided. All 63 normalization tests pass locally.

Related Issues

Breaking changes

None — existing behavior preserved when no target stats are provided.


Please ensure your PR meets the following requirements:

  • Code builds and passes tests locally, including doctests
  • New tests have been added (for bug fixes/features)
  • Documentation has been updated
  • Pre-commit passes

@jdeschamps jdeschamps requested a review from melisande-c June 29, 2026 12:43

@melisande-c melisande-c left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Hi @parallelArchitect, thank you for your contribution, however next time please read our contribution guidelines which states that people outside the core team who wish to contribute should get in contact with us first, you can request to work on an existing issue by commenting on it and waiting for our response on how to proceed.

I have requested some changes, mostly about documentation and the tests.

Thanks again!

Comment on lines +160 to +163
Uses target statistics when available, since the patch passed in
is the model's output (in the target's distribution), not the
original input. Falls back to input statistics when no target
statistics were provided, preserving prior behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do not reference "prior behaviour" in documentation since this will not make sense to people reading it later, especially in the API reference on the website.

The algorithm N2V is self-supervised and the input statistics are the target statistics which is why we want this behaviour.

Suggested change
Uses target statistics when available, since the patch passed in
is the model's output (in the target's distribution), not the
original input. Falls back to input statistics when no target
statistics were provided, preserving prior behavior.
The data is denormalized using the target statistics, when available. When no target
statistics are available, which is the case for self-supervised algorithms such as
N2V, the data is denormalized using the input statistics.

Comment on lines +105 to +108
Uses target range when available, since the patch passed in is the
model's output (in the target's distribution), not the original input.
Falls back to input range when no target range was provided, preserving
prior behavior.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, do not mention "prior behaviour" in documentation. Replace this segment with something similar suggested in my previous comment

Comment on lines +231 to +236
def test_denormalize_uses_target_stats_not_input_stats():
"""Regression test for #967: denormalize() must use target stats when
they exist, not input stats. Real failure case: input and target have
a different number of channels (e.g. denoising task), so reusing
input's per-channel stats on the model's (target-shaped) output either
crashes or silently applies the wrong values."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do not mention the issue number or PR number in the test docstring or any of the comments. Let's not call this a regression test, we are just testing new behaviour, so let's rename it to something like test_denormalize_with_target_stats.

Comment on lines +260 to +262
norm = dataset.normalization
norm.target_means = [150.0, 300.0]
norm.target_stds = [30.0, 50.0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

set the target stats when creating the configuration above, not here.

norm.target_stds = [30.0, 50.0]

# Simulate model output: batch=1, 2 channels (matches target, not input)
model_output = torch.zeros((1, 2, 8, 8))

@melisande-c melisande-c Jun 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

can you make the model output torch.ones? So that the result should be target_std + target_mean not just the target_mean

Comment on lines +226 to +230
def test_denormalize_uses_target_range_not_input_range():
"""Regression test for #967: denormalize() must use target range when
it exists, not input range. Real failure case: input and target have
a different number of channels, so reusing input's per-channel range
on the model's (target-shaped) output gives silently wrong values."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, let's make this docstring a simple one-liner to describe what the test does without mentioning the PR number or the issue number, and rename the test to test_denormalize_with_target_range.

Comment on lines +250 to +252
norm = dataset.normalization
norm.target_mins = [0.0, 0.0]
norm.target_maxes = [1000.0, 2000.0]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Again, set the target stats when creating the configuration instead of patching it in here.

@parallelArchitect

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @melisande-c — addressed everything:

  • Removed "prior behavior" language from both docstrings, used your suggested phrasing
  • Renamed tests to test_denormalize_with_target_stats / test_denormalize_with_target_range, simplified docstrings, removed issue/PR references
  • Moved target stats/range into config creation instead of patching after dataset creation
  • Changed mean_std test's model output from zeros to ones so the assertion validates target_mean + target_std being applied, not just the mean

All normalization tests pass (63/63).

@melisande-c

Copy link
Copy Markdown
Member

Great thanks for the changes, can you fix the last pre-commit "line too long" error and then we are good to merge.

@melisande-c melisande-c changed the title fix: denormalize() uses target stats when available (#967) Fix(Normalization): Denormalize with target stats when present Jul 2, 2026
@melisande-c melisande-c merged commit c6c43bb into CAREamics:main Jul 2, 2026
14 checks passed
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.

Normalization: target stats not used for denomalization

3 participants