Fix(Normalization): Denormalize with target stats when present#977
Conversation
There was a problem hiding this comment.
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!
| 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. |
There was a problem hiding this comment.
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.
| 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. |
| 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. |
There was a problem hiding this comment.
Again, do not mention "prior behaviour" in documentation. Replace this segment with something similar suggested in my previous comment
| 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.""" |
There was a problem hiding this comment.
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.
| norm = dataset.normalization | ||
| norm.target_means = [150.0, 300.0] | ||
| norm.target_stds = [30.0, 50.0] |
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
can you make the model output torch.ones? So that the result should be target_std + target_mean not just the target_mean
| 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.""" |
There was a problem hiding this comment.
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.
| norm = dataset.normalization | ||
| norm.target_mins = [0.0, 0.0] | ||
| norm.target_maxes = [1000.0, 2000.0] |
There was a problem hiding this comment.
Again, set the target stats when creating the configuration instead of patching it in here.
|
Thanks for the thorough review @melisande-c — addressed everything:
All normalization tests pass (63/63). |
|
Great thanks for the changes, can you fix the last pre-commit "line too long" error and then we are good to merge. |
Disclaimer
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_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: