2727"""
2828
2929import logging
30- from numpy import ones , where , empty , array , concatenate , ma , zeros
30+ from numpy import where , empty , array , concatenate , ma , zeros , unique , round
3131from scipy .ndimage import minimum_filter
32- from scipy .ndimage import binary_erosion
3332from matplotlib .figure import Figure
3433from .tools import index_from_nearest_path , index_from_nearest_path_with_pt_in_bbox
3534
@@ -39,7 +38,7 @@ class Amplitude(object):
3938 Class to calculate *amplitude* and counts of *local maxima/minima*
4039 within a closed region of a sea level anomaly field.
4140 """
42-
41+ EPSILON = 1e-8
4342 __slots__ = (
4443 'h_0' ,
4544 'grid_extract' ,
@@ -48,7 +47,6 @@ class Amplitude(object):
4847 'contour' ,
4948 'interval' ,
5049 'amplitude' ,
51- 'local_extrema_inds' ,
5250 'mle' ,
5351 )
5452
@@ -73,7 +71,6 @@ def __init__(self, contour, contour_height, data, interval):
7371 self .sla = data [contour .pixels_index ]
7472 # Amplitude which will be provide
7573 self .amplitude = 0
76- self .local_extrema_inds = None
7774 # Maximum local extrema accepted
7875 self .mle = 1
7976
@@ -98,54 +95,96 @@ def all_pixels_below_h0(self, level):
9895 Check CSS11 criterion 1: The SSH values of all of the pixels
9996 are below a given SSH threshold for cyclonic eddies.
10097 """
101- if (self .sla > self .h_0 ).any () or (hasattr (self .sla , 'mask' ) and self .sla .mask .any ()):
98+ # In some case pixel value must be very near of contour bounds
99+ if ((self .sla - self .h_0 ) > self .EPSILON ).any () or (hasattr (self .sla , 'mask' ) and self .sla .mask .any ()):
102100 return False
103101 else :
104- self ._set_local_extrema (1 )
105- lmi_i , lmi_j = where (self .local_extrema_inds )
106- m = self .mask [lmi_i , lmi_j ]
107- lmi_i , lmi_j = lmi_i [m ], lmi_j [m ]
108- nb_real_extrema = ((level - self .grid_extract [lmi_i , lmi_j ]) >= 2 * self .interval ).sum ()
109- if nb_real_extrema > self .mle :
110- return False
111- amp_min = level - (2 * self .interval )
112- index = self .grid_extract [lmi_i , lmi_j ].argmin ()
113- jmin_ , imin_ = lmi_j [index ], lmi_i [index ]
114- if self .grid_extract [imin_ , jmin_ ] <= amp_min :
115- self ._set_cyc_amplitude ()
102+ # All local extrema index on th box
103+ lmi_i , lmi_j = self ._set_local_extrema (1 )
116104 slice_x , slice_y = self .contour .bbox_slice
117- return imin_ + slice_x .start , jmin_ + slice_y .start
105+ if len (lmi_i ) == 1 :
106+ i , j = lmi_i [0 ] + slice_x .start , lmi_j [0 ] + slice_y .start
107+ else :
108+ # Verify if several extrema are seriously below contour
109+ nb_real_extrema = ((level - self .grid_extract [lmi_i , lmi_j ]) >= 2 * self .interval ).sum ()
110+ if nb_real_extrema > self .mle :
111+ return False
112+ index = self .grid_extract [lmi_i , lmi_j ].argmin ()
113+ i , j = lmi_i [index ] + slice_x .start , lmi_j [index ] + slice_y .start
114+ self ._set_cyc_amplitude ()
115+ return i , j
118116
119117 def all_pixels_above_h0 (self , level ):
120118 """
121119 Check CSS11 criterion 1: The SSH values of all of the pixels
122120 are above a given SSH threshold for anticyclonic eddies.
123121 """
124- if (self .sla < self .h_0 ).any () or (hasattr (self .sla , 'mask' ) and self .sla .mask .any ()):
122+ # In some case pixel value must be very near of contour bounds
123+ if ((self .sla - self .h_0 ) < - self .EPSILON ).any () or (hasattr (self .sla , 'mask' ) and self .sla .mask .any ()):
125124 # i.e.,with self.amplitude == 0
126125 return False
127126 else :
128- self ._set_local_extrema (- 1 )
129- lmi_i , lmi_j = where (self .local_extrema_inds )
130- m = self .mask [lmi_i , lmi_j ]
131- lmi_i , lmi_j = lmi_i [m ], lmi_j [m ]
132- nb_real_extrema = ((self .grid_extract [lmi_i , lmi_j ] - level ) >= 2 * self .interval ).sum ()
133- if nb_real_extrema > self .mle :
134- return False
135- amp_min = level + (2 * self .interval )
136- index = self .grid_extract [lmi_i , lmi_j ].argmax ()
137- jmin_ , imin_ = lmi_j [index ], lmi_i [index ]
138- if self .grid_extract [imin_ , jmin_ ] >= amp_min :
139- self ._set_cyc_amplitude ()
127+
128+ # All local extrema index on th box
129+ lmi_i , lmi_j = self ._set_local_extrema (- 1 )
140130 slice_x , slice_y = self .contour .bbox_slice
141- return imin_ + slice_x .start , jmin_ + slice_y .start
131+ if len (lmi_i ) == 1 :
132+ i , j = lmi_i [0 ] + slice_x .start , lmi_j [0 ] + slice_y .start
133+ else :
134+ # Verify if several extrema are seriously above contour
135+ nb_real_extrema = ((self .grid_extract [lmi_i , lmi_j ] - level ) >= 2 * self .interval ).sum ()
136+ if nb_real_extrema > self .mle :
137+ return False
138+ index = self .grid_extract [lmi_i , lmi_j ].argmax ()
139+ i , j = lmi_i [index ] + slice_x .start , lmi_j [index ] + slice_y .start
140+ self ._set_cyc_amplitude ()
141+ return i , j
142142
143143 def _set_local_extrema (self , sign ):
144144 """
145145 Set count of local SLA maxima/minima within eddy
146146 """
147- # mask of minima
148- self .local_extrema_inds = self .detect_local_minima (self .grid_extract * sign )
147+ # index of minima on whole grid extract
148+ i_x , i_y = self .detect_local_minima (self .grid_extract * sign )
149+ # Only index in contour
150+ m = self .mask [i_x , i_y ]
151+ i_x , i_y = i_x [m ], i_y [m ]
152+ # Verify if some extramum is contigus
153+ nb_extrema = len (i_x )
154+ if nb_extrema > 1 :
155+ # Group
156+ nb_group = 1
157+ gr = zeros (nb_extrema , dtype = 'u2' )
158+ for i1 , (i , j ) in enumerate (zip (i_x [:- 1 ], i_y [:- 1 ])):
159+ for i2 , (k , l ) in enumerate (zip (i_x [i1 + 1 :], i_y [i1 + 1 :])):
160+ if (abs (i - k ) + abs (j - l )) == 1 :
161+ i2_ = i2 + i1 + 1
162+ if gr [i1 ] == 0 and gr [i2_ ] == 0 :
163+ # Nobody was link with a know group
164+ gr [i1 ] = nb_group
165+ gr [i2_ ] = nb_group
166+ nb_group += 1
167+ elif gr [i1 ] == 0 and gr [i2_ ] != 0 :
168+ # i2 is link not i1
169+ gr [i1 ] = gr [i2_ ]
170+ elif gr [i2_ ] == 0 and gr [i1 ] != 0 :
171+ # i1 is link not i2
172+ gr [i2_ ] = gr [i1 ]
173+ else :
174+ # there already linked in two different group
175+ # we replace group from i1 with group from i2
176+ gr [gr == gr [i1 ]] = gr [i2_ ]
177+ m = gr != 0
178+ grs = unique (gr [m ])
179+ # all non grouped extremum
180+ i_x_new , i_y_new = list (i_x [~ m ]), list (i_y [~ m ])
181+ for gr_ in grs :
182+ m = gr_ == gr
183+ # Choose barycentre of group
184+ i_x_new .append (round (i_x [m ].mean (axis = 0 )).astype ('i2' ))
185+ i_y_new .append (round (i_y [m ].mean (axis = 0 )).astype ('i2' ))
186+ return i_x_new , i_y_new
187+ return i_x , i_y
149188
150189 @staticmethod
151190 def detect_local_minima (grid ):
@@ -155,20 +194,13 @@ def detect_local_minima(grid):
155194 the pixel's value is the neighborhood maximum, 0 otherwise)
156195 http://stackoverflow.com/questions/3684484/peak-detection-in-a-2d-array/3689710#3689710
157196 """
158- # Equivalent
159- neighborhood = ones ((3 , 3 ), dtype = 'bool' )
197+ # To don't perturbate filter
160198 if hasattr (grid , 'mask' ):
161199 grid [grid .mask ] = 2e10
162200 # Get local mimima
163- detected_minima = minimum_filter (
164- grid , footprint = neighborhood ) == grid
165- background = (grid == 0 )
166- # Aims ?
167- eroded_background = binary_erosion (
168- background , structure = neighborhood , border_value = 1 )
169- detected_minima ^= eroded_background
170- # mask of minima
171- return detected_minima
201+ detected_minima = minimum_filter (grid , size = 3 ) == grid
202+ # index of minima
203+ return where (detected_minima )
172204
173205
174206class Contours (object ):
0 commit comments