Skip to content

Commit a499d14

Browse files
Merge pull request AntSimi#77 from CoriPegliasco/master
add ellipse fit
2 parents 64efe1b + d883b48 commit a499d14

File tree

10 files changed

+107
-58
lines changed

10 files changed

+107
-58
lines changed

examples/01_general_things/pet_storage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
# array field like contour/profile are 2D column.
3333

3434
# %%
35-
# Eddies files (zarr or netcdf) could be loaded with ```load_file``` method:
35+
# Eddies files (zarr or netcdf) can be loaded with ```load_file``` method:
3636
eddies_collections = EddiesObservations.load_file(get_demo_path("Cyclonic_20160515.nc"))
3737
eddies_collections.field_table()
3838
# offset and scale_factor are used only when data is stored in zarr or netCDF4

examples/14_generic_tools/pet_fit_contour.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,30 +15,30 @@
1515
from py_eddy_tracker import data
1616
from py_eddy_tracker.generic import coordinates_to_local, local_to_coordinates
1717
from py_eddy_tracker.observations.observation import EddiesObservations
18-
from py_eddy_tracker.poly import fit_circle_, fit_ellips
18+
from py_eddy_tracker.poly import fit_circle_, fit_ellipse
1919

2020
# %%
2121
# Load example identification file
2222
a = EddiesObservations.load_file(data.get_demo_path("Anticyclonic_20190223.nc"))
2323

2424

2525
# %%
26-
# Function to draw circle or ellips from parameter
26+
# Function to draw circle or ellipse from parameter
2727
def build_circle(x0, y0, r):
2828
angle = radians(linspace(0, 360, 50))
2929
x_norm, y_norm = cos(angle), sin(angle)
3030
return local_to_coordinates(x_norm * r, y_norm * r, x0, y0)
3131

3232

33-
def build_ellips(x0, y0, a, b, theta):
33+
def build_ellipse(x0, y0, a, b, theta):
3434
angle = radians(linspace(0, 360, 50))
3535
x = a * cos(theta) * cos(angle) - b * sin(theta) * sin(angle)
3636
y = a * sin(theta) * cos(angle) + b * cos(theta) * sin(angle)
3737
return local_to_coordinates(x, y, x0, y0)
3838

3939

4040
# %%
41-
# Plot fitted circle or ellips on stored contour
41+
# Plot fitted circle or ellipse on stored contour
4242
xs, ys = a.contour_lon_s, a.contour_lat_s
4343

4444
fig = plt.figure(figsize=(15, 15))
@@ -51,9 +51,9 @@ def build_ellips(x0, y0, a, b, theta):
5151
ax = fig.add_subplot(4, 4, j)
5252
ax.grid(), ax.set_aspect("equal")
5353
ax.plot(x, y, label="store", color="black")
54-
x0, y0, a, b, theta = fit_ellips(x_, y_)
54+
x0, y0, a, b, theta = fit_ellipse(x_, y_)
5555
x0, y0 = local_to_coordinates(x0, y0, x0_, y0_)
56-
ax.plot(*build_ellips(x0, y0, a, b, theta), label="ellips", color="green")
56+
ax.plot(*build_ellipse(x0, y0, a, b, theta), label="ellipse", color="green")
5757
x0, y0, radius, shape_error = fit_circle_(x_, y_)
5858
x0, y0 = local_to_coordinates(x0, y0, x0_, y0_)
5959
ax.plot(*build_circle(x0, y0, radius), label="circle", color="red", lw=0.5)

examples/16_network/pet_follow_particle.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _repr_html_(self, *args, **kwargs):
3333

3434
def save(self, *args, **kwargs):
3535
if args[0].endswith("gif"):
36-
# In this case gif is use to create thumbnail which are not use but consume same time than video
36+
# In this case gif is used to create thumbnail which are not used but consumes same time than video
3737
# So we create an empty file, to save time
3838
with open(args[0], "w") as _:
3939
pass
@@ -147,7 +147,7 @@ def particle_candidate(x, y, c, eddies, t_start, i_target, pct, **kwargs):
147147
e = eddies.extract_with_mask(m_start)
148148
# to be able to get global index
149149
translate_start = where(m_start)[0]
150-
# Identify particle in eddies(only in core)
150+
# Identify particle in eddies (only in core)
151151
i_start = e.contains(x, y, intern=True)
152152
m = i_start != -1
153153
x, y, i_start = x[m], y[m], i_start[m]
@@ -158,9 +158,9 @@ def particle_candidate(x, y, c, eddies, t_start, i_target, pct, **kwargs):
158158
e_end = eddies.extract_with_mask(m_end)
159159
# to be able to get global index
160160
translate_end = where(m_end)[0]
161-
# Id eddies for each alive particle(in core and extern)
161+
# Id eddies for each alive particle (in core and extern)
162162
i_end = e_end.contains(x, y)
163-
# compute matrix and filled target array
163+
# compute matrix and fill target array
164164
get_matrix(i_start, i_end, translate_start, translate_end, i_target, pct)
165165

166166

@@ -169,7 +169,7 @@ def get_matrix(i_start, i_end, translate_start, translate_end, i_target, pct):
169169
nb_start, nb_end = translate_start.size, translate_end.size
170170
# Matrix which will store count for every couple
171171
count = zeros((nb_start, nb_end), dtype=nb_types.int32)
172-
# Number of particle in each origin observation
172+
# Number of particles in each origin observation
173173
ref = zeros(nb_start, dtype=nb_types.int32)
174174
# For each particle
175175
for i in range(i_start.size):
@@ -181,7 +181,7 @@ def get_matrix(i_start, i_end, translate_start, translate_end, i_target, pct):
181181
for i in range(nb_start):
182182
for j in range(nb_end):
183183
pct_ = count[i, j]
184-
# If there are particle from i to j
184+
# If there are particles from i to j
185185
if pct_ != 0:
186186
# Get percent
187187
pct_ = pct_ / ref[i] * 100.0

examples/16_network/pet_ioannou_2017_case.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
from matplotlib import pyplot as plt
1515
from matplotlib.animation import FuncAnimation
1616
from matplotlib.ticker import FuncFormatter
17-
from numpy import arange, where
17+
from numpy import arange, where, array, pi
1818

1919
import py_eddy_tracker.gui
2020
from py_eddy_tracker.appli.gui import Anim
2121
from py_eddy_tracker.data import get_demo_path
2222
from py_eddy_tracker.observations.network import NetworkObservations
2323

24+
from py_eddy_tracker.generic import coordinates_to_local, local_to_coordinates
25+
from py_eddy_tracker.poly import fit_ellipse
2426

2527
# %%
28+
29+
2630
class VideoAnimation(FuncAnimation):
2731
def _repr_html_(self, *args, **kwargs):
2832
"""To get video in html and have a player"""
@@ -188,3 +192,47 @@ def update_axes(ax, mappable=None):
188192
m = close_to_i3.scatter_timeline(ax, "shape_error_e", vmin=14, vmax=70, **kw)
189193
cb = update_axes(ax, m["scatter"])
190194
cb.set_label("Effective shape error")
195+
196+
# %%
197+
# Rotation angle
198+
# --------------
199+
# For each obs, fit an ellipse to the contour, with theta the angle from the x-axis,
200+
# a the semi ax in x direction and b the semi ax in y dimension
201+
202+
theta_ = list()
203+
a_ = list()
204+
b_ = list()
205+
for obs in close_to_i3:
206+
x, y = obs["contour_lon_s"], obs["contour_lat_s"]
207+
x0_, y0_ = x.mean(), y.mean()
208+
x_, y_ = coordinates_to_local(x, y, x0_, y0_)
209+
x0, y0, a, b, theta = fit_ellipse(x_, y_)
210+
theta_.append(theta)
211+
a_.append(a)
212+
b_.append(b)
213+
a_ = array(a_)
214+
b_ = array(b_)
215+
216+
# %%
217+
# Theta
218+
ax = timeline_axes()
219+
m = close_to_i3.scatter_timeline(ax, theta_, vmin=-pi / 2, vmax=pi / 2, cmap="hsv")
220+
cb = update_axes(ax, m["scatter"])
221+
222+
# %%
223+
# a
224+
ax = timeline_axes()
225+
m = close_to_i3.scatter_timeline(ax, a_ * 1e-3, vmin=0, vmax=80, cmap="Spectral_r")
226+
cb = update_axes(ax, m["scatter"])
227+
228+
# %%
229+
# b
230+
ax = timeline_axes()
231+
m = close_to_i3.scatter_timeline(ax, b_ * 1e-3, vmin=0, vmax=80, cmap="Spectral_r")
232+
cb = update_axes(ax, m["scatter"])
233+
234+
# %%
235+
# a/b
236+
ax = timeline_axes()
237+
m = close_to_i3.scatter_timeline(ax, a_ / b_, vmin=1, vmax=2, cmap="Spectral_r")
238+
cb = update_axes(ax, m["scatter"])

src/py_eddy_tracker/dataset/grid.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -609,17 +609,17 @@ def eddy_identification(
609609
:param str vname: Grid name of v speed component
610610
:param datetime.datetime date: Date which will be stored in object to date data
611611
:param float,int step: Height between two layers in m
612-
:param float,int shape_error: Maximal error allowed for outter contour in %
612+
:param float,int shape_error: Maximal error allowed for outermost contour in %
613613
:param int sampling: Number of points to store contours and speed profile
614614
:param str sampling_method: Method to resample 'uniform' or 'visvalingam'
615615
:param (int,int),None pixel_limit:
616-
Min and max number of pixels inside the inner and the outer contour to be considered as an eddy
616+
Min and max number of pixels inside the inner and the outermost contour to be considered as an eddy
617617
:param float,None precision: Truncate values at the defined precision in m
618618
:param str force_height_unit: Unit used for height unit
619619
:param str force_speed_unit: Unit used for speed unit
620620
:param dict kwargs: Argument given to amplitude
621621
622-
:return: Return a list of 2 elements: Anticyclone and Cyclone
622+
:return: Return a list of 2 elements: Anticyclones and Cyclones
623623
:rtype: py_eddy_tracker.observations.observation.EddiesObservations
624624
625625
.. minigallery:: py_eddy_tracker.GridDataset.eddy_identification
@@ -729,7 +729,8 @@ def eddy_identification(
729729
for contour in contour_paths:
730730
if contour.used:
731731
continue
732-
# FIXME : center could be not in contour and fit on raw sampling
732+
# FIXME : center could be outside the contour due to the fit
733+
# FIXME : warning : the fit is made on raw sampling
733734
_, _, _, aerr = contour.fit_circle()
734735

735736
# Filter for shape
@@ -752,6 +753,7 @@ def eddy_identification(
752753
):
753754
continue
754755

756+
# Test the number of pixels within the outermost contour
755757
# FIXME : Maybe limit max must be replace with a maximum of surface
756758
if (
757759
contour.nb_pixel < pixel_limit[0]
@@ -760,6 +762,9 @@ def eddy_identification(
760762
contour.reject = 3
761763
continue
762764

765+
# Here the considered contour passed shape_error test, masked_pixels test,
766+
# values strictly above (AEs) or below (CEs) the contour, number_pixels test)
767+
763768
# Compute amplitude
764769
reset_centroid, amp = self.get_amplitude(
765770
contour,

src/py_eddy_tracker/eddy_feature.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
class Amplitude(object):
3232
"""
3333
Class to calculate *amplitude* and counts of *local maxima/minima*
34-
within a closed region of a sea level anomaly field.
34+
within a closed region of a sea surface height field.
3535
"""
3636

3737
EPSILON = 1e-8
@@ -66,8 +66,8 @@ def __init__(
6666
:param array data:
6767
:param float interval:
6868
:param int mle: maximum number of local maxima in contour
69-
:param int nb_step_min: number of interval to consider like an eddy
70-
:param int nb_step_to_be_mle: number of interval to be consider like another maxima
69+
:param int nb_step_min: number of intervals to consider an eddy
70+
:param int nb_step_to_be_mle: number of intervals to be considered as an another maxima
7171
"""
7272

7373
# Height of the contour
@@ -102,8 +102,9 @@ def __init__(
102102
self.nb_pixel = i_x.shape[0]
103103

104104
# Only pixel in contour
105+
# FIXME : change sla by ssh as the grid can be adt?
105106
self.sla = data[contour.pixels_index]
106-
# Amplitude which will be provide
107+
# Amplitude which will be provided
107108
self.amplitude = 0
108109
# Maximum local extrema accepted
109110
self.mle = mle
@@ -115,9 +116,10 @@ def within_amplitude_limits(self):
115116
def all_pixels_below_h0(self, level):
116117
"""
117118
Check CSS11 criterion 1: The SSH values of all of the pixels
118-
are below a given SSH threshold for cyclonic eddies.
119+
are below (above) a given SSH threshold for cyclonic (anticyclonic)
120+
eddies.
119121
"""
120-
# In some case pixel value must be very near of contour bounds
122+
# In some cases pixel value may be very close to the contour bounds
121123
if self.sla.mask.any() or ((self.sla.data - self.h_0) > self.EPSILON).any():
122124
return False
123125
else:
@@ -293,10 +295,10 @@ class Contours(object):
293295
294296
Attributes:
295297
contour:
296-
A matplotlib contour object of high-pass filtered SLA
298+
A matplotlib contour object of high-pass filtered SSH
297299
298300
eddy:
299-
A tracklist object holding the SLA data
301+
A tracklist object holding the SSH data
300302
301303
grd:
302304
A grid object
@@ -406,7 +408,7 @@ def __init__(self, x, y, z, levels, wrap_x=False, keep_unclose=False):
406408
fig = Figure()
407409
ax = fig.add_subplot(111)
408410
if wrap_x:
409-
logger.debug("wrapping activate to compute contour")
411+
logger.debug("wrapping activated to compute contour")
410412
x = concatenate((x, x[:1] + 360))
411413
z = ma.concatenate((z, z[:1]))
412414
logger.debug("X shape : %s", x.shape)
@@ -602,8 +604,8 @@ def display(
602604
Must be 'shape_error', 'x', 'y' or 'radius'.
603605
If define display_criterion is not use.
604606
bins argument must be define
605-
:param array bins: bins use to colorize contour
606-
:param str cmap: Name of cmap to use for field display
607+
:param array bins: bins used to colorize contour
608+
:param str cmap: Name of cmap for field display
607609
:param dict kwargs: look at :py:meth:`matplotlib.collections.LineCollection`
608610
609611
.. minigallery:: py_eddy_tracker.Contours.display
@@ -688,7 +690,7 @@ def display(
688690
ax.autoscale_view()
689691

690692
def label_contour_unused_which_contain_eddies(self, eddies):
691-
"""Select contour which contain several eddies"""
693+
"""Select contour containing several eddies"""
692694
if eddies.sign_type == 1:
693695
# anticyclonic
694696
sl = slice(None, -1)

src/py_eddy_tracker/featured_tracking/old_tracker_reference.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,13 @@ def cost_function(records_in, records_out, distance):
2121
return distance
2222

2323
def mask_function(self, other, distance):
24-
"""We mask link with ellips and ratio"""
25-
# Compute Parameter of ellips
24+
"""We mask link with ellipse and ratio"""
25+
# Compute Parameter of ellipse
2626
minor, major = 1.05, 1.5
27-
y = self.basic_formula_ellips_major_axis(
27+
y = self.basic_formula_ellipse_major_axis(
2828
self.lat, degrees=True, c0=minor, cmin=minor, cmax=major, lat1=23, lat2=5
2929
)
30-
# mask from ellips
30+
# mask from ellipse
3131
mask = self.shifted_ellipsoid_degrees_mask(
3232
other, minor=minor, major=y # Minor can be bigger than major??
3333
)

src/py_eddy_tracker/observations/network.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,6 @@ def relatives(self, obs, order=2):
513513
else:
514514
segments_connexion[seg][0] = i_slice
515515

516-
517516
if i_p != -1:
518517

519518
if p_seg not in segments_connexion:
@@ -531,12 +530,7 @@ def relatives(self, obs, order=2):
531530
segments_connexion[seg][1].append(n_seg)
532531
segments_connexion[n_seg][1].append(seg)
533532

534-
535-
i_obs = (
536-
[obs]
537-
if not hasattr(obs, "__iter__")
538-
else obs
539-
)
533+
i_obs = [obs] if not hasattr(obs, "__iter__") else obs
540534
import numpy as np
541535

542536
distance = zeros(segment.size, dtype=np.uint16) - 1

0 commit comments

Comments
 (0)