Skip to content

Fixed normalized cross correlation#364

Open
TKaeufer wants to merge 9 commits intoOpenPIV:masterfrom
TKaeufer:master
Open

Fixed normalized cross correlation#364
TKaeufer wants to merge 9 commits intoOpenPIV:masterfrom
TKaeufer:master

Conversation

@TKaeufer
Copy link
Copy Markdown
Member

@TKaeufer TKaeufer commented Apr 20, 2026

See issue: Intensity normalization #363

Summary by Sourcery

Update intensity normalization and normalized cross-correlation to use zero-mean, unit-variance windows and adjust correlation scaling accordingly.

Enhancements:

  • Change intensity normalization to subtract the mean and divide by the standard deviation, producing zero-mean, unit-variance windows and using float64 for better numerical accuracy.
  • Adjust normalized cross-correlation scaling to divide by the correlation field size instead of precomputed window sizes.

Tests:

  • Update performance tests to expect float64 outputs from normalize_intensity and retain performance constraints.
  • Update signal-to-noise and FFT correlation tests to reflect new normalization behavior and relaxed bounds on normalized correlation values.

Modified normalization to correctly represent the zero-normalized cross correlation
Updated the normalization method to divide by the standard deviation instead of clipping negative values.
Update tolerance for normalized correlation assertion.
Change dtype from float32 to float64 for mean calculation.
Updated assertion for normalized correlation to allow larger errors. Since standard deviation is not converged on small window
Updated comments in normalize_intensity function to clarify the use of float64 for better accuracy and corrected spelling of 'standard deviation'.
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 20, 2026

Reviewer's Guide

Adjusts normalized cross-correlation to use zero-mean, unit-variance intensity normalization (float64) and correct normalization factors, updating associated tests and docstrings accordingly.

Sequence diagram for updated normalized cross-correlation in correlate_windows

sequenceDiagram
    actor User
    participant correlate_windows
    participant normalize_intensity
    participant fft_correlate_windows

    User->>correlate_windows: call correlate_windows(window_a, window_b, correlation_method)
    correlate_windows->>normalize_intensity: normalize_intensity(window_a)
    normalize_intensity-->>correlate_windows: window_a_norm (zero mean, unit variance)
    correlate_windows->>normalize_intensity: normalize_intensity(window_b)
    normalize_intensity-->>correlate_windows: window_b_norm (zero mean, unit variance)
    correlate_windows->>fft_correlate_windows: compute correlation(window_a_norm, window_b_norm, correlation_method)
    fft_correlate_windows-->>correlate_windows: corr_raw
    correlate_windows->>correlate_windows: corr = corr_raw/(corr.shape[-2]*corr.shape[-1])
    correlate_windows-->>User: corr
Loading

Sequence diagram for updated normalized cross-correlation in fft_correlate_images

sequenceDiagram
    actor User
    participant fft_correlate_images
    participant normalize_intensity

    User->>fft_correlate_images: call fft_correlate_images(image_a, image_b, correlation_method, normalized_correlation)
    fft_correlate_images->>normalize_intensity: normalize_intensity(image_a)
    normalize_intensity-->>fft_correlate_images: image_a_norm (zero mean, unit variance)
    fft_correlate_images->>normalize_intensity: normalize_intensity(image_b)
    normalize_intensity-->>fft_correlate_images: image_b_norm (zero mean, unit variance)
    fft_correlate_images->>fft_correlate_images: compute corr_raw via selected correlation_method
    alt normalized_correlation is True
        fft_correlate_images->>fft_correlate_images: corr = corr_raw/(corr.shape[-2]*corr.shape[-1])
    else normalized_correlation is False
        fft_correlate_images->>fft_correlate_images: corr = corr_raw
    end
    fft_correlate_images-->>User: corr
Loading

File-Level Changes

Change Details Files
Change intensity normalization to zero-mean, unit-variance using float64 in correlation pipelines.
  • Update normalize_intensity to subtract mean and divide by standard deviation, returning unclipped values with variance 1.
  • Switch internal computations and expected input type in normalize_intensity from float32 to float64 for improved numerical accuracy.
  • Adjust call sites in fft_correlate_images and correlate_windows to rely on the new normalization semantics and update comments accordingly.
openpiv/pyprocess.py
Correct normalized cross-correlation scaling and return values.
  • In fft_correlate_images, normalize correlation by the correlation array spatial size instead of a precomputed s2 factor and remove clipping of correlation values.
  • In correlate_windows, return correlation divided by spatial size when normalized correlation is requested.
openpiv/pyprocess.py
Update tests to reflect new normalization behavior and numeric expectations.
  • Modify performance test to treat float64 as the non-converting fast path and assert float64 outputs for both float and uint8 inputs.
  • Update signal-to-noise ratio expectations in test_sig2noise_ratio to the new mean and maximum values under corrected normalization.
  • Relax upper-bound assertion in test_fft_correlate_images to allow correlation values up to 1.5 due to non-converged std in small windows.
openpiv/test/test_performance.py
openpiv/test/test_process.py
openpiv/test/test_pyprocess.py

Possibly linked issues

  • Intensity normalization #363: PR removes clipping in normalize_intensity and uses mean/std normalization, fixing the intensity clipping issue.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • In normalize_intensity, you set the mean’s dtype to np.float64 but rely on the default for std; consider passing dtype=np.float64 to std as well for consistency and to avoid subtle numerical differences across NumPy versions.
  • The normalized correlation check in test_fft_correlate_images now allows values up to 1.5; if possible, it would be better to tighten this bound or add a short comment/logic tying the threshold to window size or expected variance so it doesn’t silently mask genuine regressions.
  • In test_normalize_intensity_performance, the assertion message text still refers to (float32) even though the function now returns float64; updating the message would avoid confusion when the test fails.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `normalize_intensity`, you set the mean’s `dtype` to `np.float64` but rely on the default for `std`; consider passing `dtype=np.float64` to `std` as well for consistency and to avoid subtle numerical differences across NumPy versions.
- The normalized correlation check in `test_fft_correlate_images` now allows values up to 1.5; if possible, it would be better to tighten this bound or add a short comment/logic tying the threshold to window size or expected variance so it doesn’t silently mask genuine regressions.
- In `test_normalize_intensity_performance`, the assertion message text still refers to `(float32)` even though the function now returns `float64`; updating the message would avoid confusion when the test fails.

## Individual Comments

### Comment 1
<location path="openpiv/pyprocess.py" line_range="851" />
<code_context>
         print(f"correlation method {correlation_method } is not implemented")

-    return corr
+    return corr/(corr.shape[-2]*corr.shape[-1])


</code_context>
<issue_to_address>
**issue (bug_risk):** Consider whether `correlate_windows` should always normalize by window size or offer a flag for raw vs. normalized correlation.

This change always scales by window area, unlike `fft_correlate_images`, where normalization is controlled by `normalized_correlation`. If callers currently rely on raw correlation magnitudes, this becomes a backward-incompatible change and may cause subtle issues (e.g., double-normalization or misread peak heights). Consider adding a `normalized_correlation` parameter here too, or verifying that all call sites expect normalized output.
</issue_to_address>

### Comment 2
<location path="openpiv/pyprocess.py" line_range="755-756" />
<code_context>
     """Normalize interrogation window or strided image of many windows,
-       by removing the mean intensity value per window and clipping the
-       negative values to zero
+       by removing the mean intensity value per window and dividing by 
+       the standard deviation. Note: for small signals the standdeviation
+       might not be full converged. Also numpy docs recommend float64 for
+       better accuracy: 
+       https://numpy.org/doc/stable/reference/generated/numpy.std.html

</code_context>
<issue_to_address>
**nitpick (typo):** Fix typos and phrasing in the normalize_intensity docstring for clarity.

Use "standard deviation" and "might not be fully converged" here to improve clarity of the docstring.

```suggestion
def normalize_intensity(window):
    """Normalize an interrogation window or a strided image of many windows
       by removing the mean intensity per window and dividing by the
       standard deviation.

       Note: for small signals the standard deviation might not be fully
       converged. NumPy also recommends using float64 for improved
       numerical accuracy:
       https://numpy.org/doc/stable/reference/generated/numpy.std.html
```
</issue_to_address>

### Comment 3
<location path="openpiv/test/test_performance.py" line_range="34-43" />
<code_context>

 def test_normalize_intensity_performance():
     """Test that normalize_intensity avoids unnecessary conversions."""
-    # Test with float32 input (should not convert)
-    window_float = np.random.rand(50, 64, 64).astype(np.float32)
+    # Test with float64 input (should not convert)
+    window_float = np.random.rand(50, 64, 64).astype(np.float64)

     start = time.time()
     result = pyprocess.normalize_intensity(window_float)
     elapsed_float = time.time() - start

-    assert result.dtype == np.float32
+    assert result.dtype == np.float64

     # Test with uint8 input (needs conversion)
</code_context>
<issue_to_address>
**suggestion (testing):** Add functional tests for normalize_intensity statistical properties and zero-variance windows

This test now only covers dtype and timing, but the function’s new guarantees (zero mean, unit variance, and zero-std handling) aren’t verified. Please add a separate, non-performance test (e.g. in test_pyprocess.py or a dedicated test_normalize_intensity.py) that:

- Checks that each output window from random input has mean ≈ 0 and std ≈ 1 within a reasonable tolerance.
- Verifies that a constant-intensity window (std == 0) produces all zeros, matching the np.divide(..., where=(tmp != 0)) behavior.
- Optionally confirms that integer inputs (e.g. uint8) behave consistently with float inputs.

This will ensure the new normalization behavior is properly covered by tests.
</issue_to_address>

### Comment 4
<location path="openpiv/test/test_pyprocess.py" line_range="292" />
<code_context>
 def test_fft_correlate_images():
</code_context>
<issue_to_address>
**suggestion (testing):** Add targeted tests for correlate_windows normalization and scaling by correlation field size

The new behavior normalizes `correlate_windows` by `corr.shape[-2] * corr.shape[-1]`, and `fft_correlate_images` now relies on `normalize_intensity` with zero-mean/unit-variance. The current tests only cover this indirectly.

Please add a focused test for `correlate_windows` that:
- Correlates identical windows of different sizes and asserts that the peak normalized correlation is comparable (i.e., does not scale with window area).
- Optionally verifies any flag that enables/disables normalization.

This will directly validate the new normalization and scaling behavior across window sizes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread openpiv/pyprocess.py
print(f"correlation method {correlation_method } is not implemented")

return corr
return corr/(corr.shape[-2]*corr.shape[-1])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Consider whether correlate_windows should always normalize by window size or offer a flag for raw vs. normalized correlation.

This change always scales by window area, unlike fft_correlate_images, where normalization is controlled by normalized_correlation. If callers currently rely on raw correlation magnitudes, this becomes a backward-incompatible change and may cause subtle issues (e.g., double-normalization or misread peak heights). Consider adding a normalized_correlation parameter here too, or verifying that all call sites expect normalized output.

Comment thread openpiv/pyprocess.py
Comment on lines 755 to 756
def normalize_intensity(window):
"""Normalize interrogation window or strided image of many windows,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nitpick (typo): Fix typos and phrasing in the normalize_intensity docstring for clarity.

Use "standard deviation" and "might not be fully converged" here to improve clarity of the docstring.

Suggested change
def normalize_intensity(window):
"""Normalize interrogation window or strided image of many windows,
def normalize_intensity(window):
"""Normalize an interrogation window or a strided image of many windows
by removing the mean intensity per window and dividing by the
standard deviation.
Note: for small signals the standard deviation might not be fully
converged. NumPy also recommends using float64 for improved
numerical accuracy:
https://numpy.org/doc/stable/reference/generated/numpy.std.html

Comment on lines 34 to +43
def test_normalize_intensity_performance():
"""Test that normalize_intensity avoids unnecessary conversions."""
# Test with float32 input (should not convert)
window_float = np.random.rand(50, 64, 64).astype(np.float32)
# Test with float64 input (should not convert)
window_float = np.random.rand(50, 64, 64).astype(np.float64)

start = time.time()
result = pyprocess.normalize_intensity(window_float)
elapsed_float = time.time() - start

assert result.dtype == np.float32
assert result.dtype == np.float64
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add functional tests for normalize_intensity statistical properties and zero-variance windows

This test now only covers dtype and timing, but the function’s new guarantees (zero mean, unit variance, and zero-std handling) aren’t verified. Please add a separate, non-performance test (e.g. in test_pyprocess.py or a dedicated test_normalize_intensity.py) that:

  • Checks that each output window from random input has mean ≈ 0 and std ≈ 1 within a reasonable tolerance.
  • Verifies that a constant-intensity window (std == 0) produces all zeros, matching the np.divide(..., where=(tmp != 0)) behavior.
  • Optionally confirms that integer inputs (e.g. uint8) behave consistently with float inputs.

This will ensure the new normalization behavior is properly covered by tests.

@@ -290,7 +290,8 @@ def test_fft_correlate_images():
assert corr_norm.shape[0] == 3 # Should have same batch size

# Normalized correlation should have values between -1 and 1
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Add targeted tests for correlate_windows normalization and scaling by correlation field size

The new behavior normalizes correlate_windows by corr.shape[-2] * corr.shape[-1], and fft_correlate_images now relies on normalize_intensity with zero-mean/unit-variance. The current tests only cover this indirectly.

Please add a focused test for correlate_windows that:

  • Correlates identical windows of different sizes and asserts that the peak normalized correlation is comparable (i.e., does not scale with window area).
  • Optionally verifies any flag that enables/disables normalization.

This will directly validate the new normalization and scaling behavior across window sizes.

@alexlib
Copy link
Copy Markdown
Member

alexlib commented Apr 21, 2026

@TKaeufer - have you tested these changes?

@TKaeufer
Copy link
Copy Markdown
Member Author

TKaeufer commented Apr 21, 2026 via email

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.

2 participants