Skip to content

Commit 309e3d2

Browse files
authored
Merge pull request OpenPIV#327 from nepomnyi/master
Added normalized median filter to validation.py (i.e. the 2005 version of Westerweel's median filter).
2 parents 9bba977 + 7b53966 commit 309e3d2

3 files changed

Lines changed: 177 additions & 1 deletion

File tree

186 KB
Loading

openpiv/test/test_validation.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,62 @@ def test_local_median_validation(u_threshold=3, N=3):
7676
assert flag[N, N]
7777

7878

79+
def test_local_norm_median_validation():
80+
""" test normalized local median
81+
82+
Args:
83+
threshold (int, optional): _description_.
84+
N (int, optional): _description_.
85+
"""
86+
87+
# The idea is the following. Let's create an array of the size of one filtering kernel.
88+
# Calculate normalized median filter by hand and using local_norm_median_val(). See if
89+
# the results are comparable. As outlined in the description of local_norm_median_val(),
90+
# follow the paper J. Westerweel, F. Scarano, "Universal outlier detection for PIV data",
91+
# Experiments in fluids, 39(6), p.1096-1100, 2005, equation 2, to perform calculations
92+
# by hand.
93+
94+
# Case study:
95+
u = np.asarray([[1,2,3],[4,5,6],[7,8,9]])
96+
v = np.asarray([[2,3,4],[5,6,7],[8,9,10]])
97+
ε = 0.1
98+
thresh = 2
99+
100+
# Calculations by hand (according to equation 2 in the referenced article).
101+
u0 = 5
102+
v0 = 6
103+
# Median formula for even number of observations (the center is excluded)
104+
# that are placed in the ascending order (just like the given u and v arrays):
105+
# Median = [(n/2)th term + ((n/2) + 1)th term]/2 (https://www.cuemath.com/data/median/).
106+
um = 5 # = (4 + 6) / 2
107+
vm = 6 # = (5 + 7) / 2
108+
# Arrays of residuals given by the formula rui = |ui-um| and rvi = |vi-vm|
109+
rui = np.asarray([[4,3,2],[1,0,1],[2,3,4]])
110+
rvi = np.asarray([[4,3,2],[1,0,1],[2,3,4]])
111+
# Median residuals values (center excluded) must be calculated for ascending arrays:
112+
ruiascend = [1, 1, 2, 2, 3, 3, 4, 4]
113+
rviascend = [1, 1, 2, 2, 3, 3, 4, 4]
114+
rum = 2.5 # = (2 + 3) / 2
115+
rvm = 2.5 # = (2 + 3) / 2
116+
# Now implement equation 2 from the referenced article:
117+
r0ast_u = np.abs(u0 - um) / (rum + ε)
118+
r0ast_v = np.abs(v0 - vm) / (rvm + ε)
119+
# The method of comparison to the threshold is given at the very end of the referenced
120+
# article in the MATLAB code:
121+
byHand = (r0ast_u**2 + r0ast_v**2)**0.5 > thresh
122+
# Here, we calculated byHand for the velocity vector (u0,v0) coordinates of which are
123+
# located at u[1,1] and v[1,1].
124+
125+
# Calculations using local_norm_median_val() function.
126+
byFunc = validation.local_norm_median_val(u, v, ε, thresh)
127+
# Here by func returns a boolean matrix containing either True or False for each velocity
128+
# vector (ui,vi).
129+
130+
# Compare the two results:
131+
assert byHand == byFunc[1,1]
132+
133+
134+
79135
def test_global_val():
80136
""" tests global validation
81137
"""

openpiv/validation.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def local_median_val(
202202
Returns
203203
-------
204204
205-
flag : boolean 2d np.ndarray
205+
ind : boolean 2d np.ndarray
206206
a boolean array. True elements corresponds to outliers.
207207
208208
"""
@@ -228,6 +228,126 @@ def local_median_val(
228228
return ind
229229

230230

231+
def local_norm_median_val(
232+
u: np.ndarray,
233+
v: np.ndarray,
234+
ε: float,
235+
threshold: float,
236+
size: int=1
237+
)->np.ndarray:
238+
"""This function is adapted from OpenPIV's implementation of
239+
validation.local_median_val(). validation.local_median_val() is,
240+
basically, Westerweel's original median filter (with some changes).
241+
The current function builts upon validation.local_median_val() and implements
242+
improved Westerweel's median filter (normalized filter) as described
243+
in 2007 edition of the German PIV book (paragraph 6.1.5) and in Westerweel's
244+
article J. Westerweel, F. Scarano, "Universal outlier detection for PIV data",
245+
Experiments in fluids, 39(6), p.1096-1100, 2005.
246+
For the list of parameters, see the referenced article, equation 2 on p.1097.
247+
The current function implements equation 2 from the referenced article in a
248+
manner shown in the MATLAB script at the end of the article, on p.1100.
249+
250+
This validation method tests for the spatial consistency of the data.
251+
Vectors are classified as outliers and replaced with Nan (Not a Number) if
252+
the absolute difference with the local median is greater than a user
253+
specified threshold. The median is computed for both velocity components.
254+
255+
The image masked areas (obstacles, reflections) are marked as masked array:
256+
u = np.ma.masked(u, flag = image_mask)
257+
and it should not be replaced by the local median, but remain masked.
258+
259+
260+
Parameters
261+
----------
262+
u : 2d np.ndarray
263+
a two dimensional array containing the u velocity component
264+
265+
v : 2d np.ndarray
266+
a two dimensional array containing the v velocity component
267+
268+
ε : float
269+
minimum normalization level (see the referenced article, eqn.2)
270+
271+
threshold : float
272+
the threshold to determine whether the vector is valid or not
273+
274+
size: int
275+
the representative size of the kernel of the median filter, the
276+
actual size of the kernel is (2*size+1, 2*size+1) - i.e., it's the
277+
number of interrogation windows away from the interrogation
278+
window of interest
279+
280+
Returns
281+
-------
282+
283+
ind : boolean 2d np.ndarray
284+
a boolean array; true elements corresponds to outliers
285+
286+
"""
287+
if np.ma.is_masked(u):
288+
masked_u = np.where(~u.mask, u.data, np.nan)
289+
masked_v = np.where(~v.mask, v.data, np.nan)
290+
else:
291+
masked_u = u
292+
masked_v = v
293+
294+
um = generic_filter(masked_u,
295+
np.nanmedian,
296+
mode='constant',
297+
cval=np.nan,
298+
size=(2*size+1, 2*size+1)
299+
)
300+
vm = generic_filter(masked_v,
301+
np.nanmedian,
302+
mode='constant',
303+
cval=np.nan,
304+
size=(2*size+1, 2*size+1)
305+
)
306+
307+
def rfunc(x):
308+
"""
309+
Implementation of r from the cited article (see the description of
310+
the function above). x is the array within the filtering kernel.
311+
I.e., every element of x is a velocity vector ui or vi.
312+
This function must return a scalar: https://stackoverflow.com/a/14060024/10073233
313+
"""
314+
# copied from here: https://stackoverflow.com/a/60166608/10073233
315+
y = x.copy() # need this step, because np.put() below changes the array in place,
316+
# and we can end up with a situation when the entire filtering kernel
317+
# is comprised of NaNs resulting in NumPy RuntimeWarning: All-NaN slice encountered
318+
np.put(y, y.size//2, np.nan) # put NaN in the middle to avoid using
319+
# the middle in the calculations
320+
ym = np.nanmedian(y) # Um for the current filtering window
321+
rm = np.nanmedian(np.abs(np.subtract(y,ym))) # median of |ui-um| or |vi-vm|
322+
return rm
323+
324+
rm_u = generic_filter(masked_u,
325+
rfunc,
326+
mode='constant',
327+
cval=np.nan,
328+
size=(2*size+1, 2*size+1)
329+
)
330+
rm_v = generic_filter(masked_v,
331+
rfunc,
332+
mode='constant',
333+
cval=np.nan,
334+
size=(2*size+1, 2*size+1)
335+
)
336+
337+
r0ast_u = np.divide(np.abs(np.subtract(masked_u,um)), np.add(rm_u,ε)) # r0ast stands for r_0^* -
338+
# see formula 2 in the
339+
# referenced article
340+
# (see description of the function)
341+
r0ast_v = np.divide(np.abs(np.subtract(masked_v,vm)), np.add(rm_v,ε)) # r0ast stands for r_0^* -
342+
# see formula 2 in the
343+
# referenced article
344+
# (see description of the function)
345+
346+
ind = (np.sqrt(np.add(np.square(r0ast_u),np.square(r0ast_v)))) > threshold
347+
348+
return ind
349+
350+
231351
def typical_validation(
232352
u: np.ndarray,
233353
v: np.ndarray,

0 commit comments

Comments
 (0)