Skip to content

Commit d2ec6ec

Browse files
fbanelliclaude
andcommitted
fix: apply validity flag in vectorized_sig2noise_ratio
'peak2peak[flag is True] = 0' indexes with the Python expression 'flag is True' (a constant False), which numpy treats as an empty boolean mask, so the assignment was a silent no-op and invalid windows (no signal, border peaks) leaked raw ratios instead of the intended 0. Same for peak2mean. Index with the flag array itself. The existing test only passed because of the no-op: on its 5x5 correlation maps the second peak always sits on the map border, so applying the flag zeroes every peak2peak ratio. Rebuild it on 16x16 maps with interior peaks and add an explicit regression test for the flag behaviour. Fixes #368 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5f7fd0f commit d2ec6ec

2 files changed

Lines changed: 62 additions & 15 deletions

File tree

openpiv/pyprocess.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,7 @@ def vectorized_sig2noise_ratio(correlation,
646646
out=np.zeros_like(peaks1),
647647
where=(peaks2 > 0.0)
648648
)
649-
peak2peak[flag is True] = 0 # replace invalid values
649+
peak2peak[flag] = 0 # replace invalid values
650650
return peak2peak
651651

652652
elif sig2noise_method == "peak2mean":
@@ -667,7 +667,7 @@ def vectorized_sig2noise_ratio(correlation,
667667
out=np.zeros_like(peaks1max),
668668
where=(peaks2mean > 0.0)
669669
)
670-
peak2mean[flag is True] = 0 # replace invalid values
670+
peak2mean[flag] = 0 # replace invalid values
671671
return peak2mean
672672
else:
673673
raise ValueError(f"sig2noise_method not supported: {sig2noise_method}")

openpiv/test/test_pyprocess.py

Lines changed: 60 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -213,31 +213,39 @@ def test_find_subpixel_peak_position():
213213
find_subpixel_peak_position(corr_gauss, subpixel_method="invalid_method")
214214

215215
def test_vectorized_sig2noise_ratio():
216-
"""Test vectorized_sig2noise_ratio function"""
217-
# Create a simple correlation map with a clear peak
218-
corr = np.zeros((3, 5, 5))
216+
"""Test vectorized_sig2noise_ratio function
219217
220-
# First correlation map: clear peak
221-
corr[0, 2, 2] = 1.0
222-
corr[0, :2, :] = 0.1
223-
corr[0, 3:, :] = 0.1
218+
The correlation maps are 16 x 16 with all peaks away from the map
219+
borders, so no window trips the validity flag (weak or border peaks)
220+
and every ratio stays positive.
221+
"""
222+
corr = np.full((3, 16, 16), 0.05)
224223

225-
# Second correlation map: two peaks
226-
corr[1, 2, 2] = 1.0
227-
corr[1, 0, 0] = 0.5
224+
# First correlation map: clear peak, faint interior second peak
225+
corr[0, 8, 8] = 1.0
226+
corr[0, 4, 4] = 0.1
228227

229-
# Third correlation map: noisy
230-
corr[2, 2, 2] = 0.3
231-
corr[2] = corr[2] + 0.1
228+
# Second correlation map: two interior peaks; the secondary one at
229+
# (8, 10) sits inside the width=2 exclusion box but outside the
230+
# width=1 box, so the ratio must depend on the chosen width
231+
corr[1, 8, 8] = 1.0
232+
corr[1, 8, 10] = 0.5
233+
corr[1, 4, 4] = 0.3
234+
235+
# Third correlation map: noisy, two interior peaks of similar height
236+
corr[2, 8, 8] = 0.4
237+
corr[2, 4, 12] = 0.35
232238

233239
# Test peak2peak method
234240
s2n_p2p = vectorized_sig2noise_ratio(corr, sig2noise_method='peak2peak', width=1)
235241
assert s2n_p2p.shape == (3,)
242+
assert np.all(s2n_p2p > 0) # no window is flagged
236243
assert s2n_p2p[0] > s2n_p2p[2] # Clear peak should have higher S2N than noisy
237244

238245
# Test peak2mean method
239246
s2n_p2m = vectorized_sig2noise_ratio(corr, sig2noise_method='peak2mean')
240247
assert s2n_p2m.shape == (3,)
248+
assert np.all(s2n_p2m > 0) # no window is flagged
241249
assert s2n_p2m[0] > s2n_p2m[2] # Clear peak should have higher S2N than noisy
242250

243251
# Test with different width
@@ -249,6 +257,45 @@ def test_vectorized_sig2noise_ratio():
249257
with pytest.raises(Exception):
250258
vectorized_sig2noise_ratio(corr, sig2noise_method='invalid_method')
251259

260+
def test_vectorized_sig2noise_ratio_flags_invalid_windows():
261+
"""Windows failing the validity checks must return a zero ratio.
262+
263+
Regression test: ``peak2peak[flag is True] = 0`` indexed with the
264+
Python expression ``flag is True`` (a constant ``False``), which numpy
265+
treats as an empty boolean mask, so the flag was silently never
266+
applied and invalid windows leaked raw ratios.
267+
"""
268+
corr = np.full((4, 16, 16), 0.05)
269+
270+
# Window 0: healthy interior peaks -> must stay positive
271+
corr[0, 8, 8] = 1.0
272+
corr[0, 4, 4] = 0.5
273+
274+
# Window 1: first peak on the map border
275+
corr[1, 0, 5] = 1.0
276+
corr[1, 8, 8] = 0.5
277+
278+
# Window 2: no signal (first peak below the 1e-3 threshold)
279+
corr[2] = 1e-5
280+
corr[2, 8, 8] = 5e-4
281+
282+
# Window 3: healthy first peak but second peak on the map border
283+
corr[3, 8, 8] = 1.0
284+
corr[3, 0, 3] = 0.9
285+
286+
s2n_p2p = vectorized_sig2noise_ratio(corr, sig2noise_method='peak2peak', width=1)
287+
assert s2n_p2p[0] > 0
288+
assert s2n_p2p[1] == 0 # border first peak
289+
assert s2n_p2p[2] == 0 # no signal
290+
assert s2n_p2p[3] == 0 # border second peak
291+
292+
# peak2mean only checks the first peak
293+
s2n_p2m = vectorized_sig2noise_ratio(corr, sig2noise_method='peak2mean')
294+
assert s2n_p2m[0] > 0
295+
assert s2n_p2m[1] == 0 # border first peak
296+
assert s2n_p2m[2] == 0 # no signal
297+
assert s2n_p2m[3] > 0 # second peak is irrelevant for peak2mean
298+
252299
def test_fft_correlate_images():
253300
"""Test fft_correlate_images function"""
254301
# Create simple test images

0 commit comments

Comments
 (0)