@@ -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+
231351def typical_validation (
232352 u : np .ndarray ,
233353 v : np .ndarray ,
0 commit comments