-
Notifications
You must be signed in to change notification settings - Fork 1
/
centerlines_rgi.py
2816 lines (2266 loc) · 95.1 KB
/
centerlines_rgi.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
""" Compute the centerlines according to Kienholz et al (2014) - with
modifications.
The output is a list of Centerline objects, stored as a list in a pickle.
The order of the list is important since the lines are
sorted per order (hydrological flow level), from the lower orders (upstream)
to the higher orders (downstream). Several tools later on rely on this order
so don't mess with it.
References::
Kienholz, C., Rich, J. L., Arendt, a. a., and Hock, R. (2014).
A new method for deriving glacier centerlines applied to glaciers in
Alaska and northwest Canada. The Cryosphere, 8(2), 503-519.
doi:10.5194/tc-8-503-2014
"""
# Built ins
import logging
import copy
from itertools import groupby
from collections import Counter
from packaging.version import Version
# External libs
import numpy as np
import pandas as pd
import shapely.ops
import scipy.signal
import shapely.geometry as shpg
from scipy.interpolate import RegularGridInterpolator
from scipy.ndimage import (gaussian_filter1d, distance_transform_edt,
label, find_objects)
# compute_centerlines_rgi
import rioxarray as rio
from glacier_centerlines.functions_rgi import (get_terminus_coord, profile, coordinate_change,
_make_costgrid,_filter_lines1,_filter_lines_slope1,
gaussian_blur, glacier_dir)
from functools import partial
try:
import skimage.draw as skdraw
except ImportError:
pass
from shapely.ops import transform as shp_trafo
import shapely.affinity as shpa
from oggm.utils._workflow import _chaikins_corner_cutting
# Optional libs
try:
import salem
except ImportError:
pass
try:
import geopandas as gpd
except ImportError:
pass
try:
from skimage import measure
from skimage.graph import route_through_array
except ImportError:
pass
# Locals
import oggm.cfg as cfg
from oggm.cfg import GAUSSIAN_KERNEL
from oggm import utils
from oggm.utils import (tuple2int, line_interpol, interp_nans, lazy_property,
SuperclassMeta)
from oggm import entity_task
from oggm.exceptions import (InvalidParamsError, InvalidGeometryError,
GeometryError, InvalidDEMError)
from oggm.core.gis import (_polygon_to_pix)#,gaussian_blur) --> problem with oggm blur
# Module logger
log = logging.getLogger(__name__)
# Variable needed later
LABEL_STRUCT = np.array([[0, 1, 0],
[1, 1, 1],
[0, 1, 0]])
class Centerline(object, metaclass=SuperclassMeta):
"""Geometry (line and widths) and flow rooting properties, but no thickness
"""
def __init__(self, line, dx=None, surface_h=None, orig_head=None,
rgi_id=None, map_dx=None):
""" Initialize a Centerline
Parameters
----------
line : :py:class:`shapely.geometry.LineString`
The geometrically calculated centerline
dx : float
Grid spacing of the initialised flowline in pixel coordinates
surface_h : :py:class:`numpy.ndarray`
elevation [m] of the points on ``line``
orig_head : :py:class:`shapely.geometry.Point`
geometric point of the lines head
rgi_id : str
The glacier's RGI identifier
map_dx : float
the map's grid resolution. Centerline.dx_meter = dx * map_dx
"""
self.line = None # Shapely LineString
self.head = None # Shapely Point
self.tail = None # Shapely Point
self.dis_on_line = None
self.nx = None
if line is not None:
self.set_line(line) # Init all previous properties
else:
self.nx = len(surface_h)
self.dis_on_line = np.arange(self.nx) * dx
self.order = None # Hydrological flow level (~ Strahler number)
# These are computed at run time by compute_centerlines
self.flows_to = None # pointer to a Centerline object (when available)
self.flows_to_point = None # point of the junction in flows_to
self.inflows = [] # list of Centerline instances (when available)
self.inflow_points = [] # junction points
# Optional attrs
self.dx = dx # dx in pixels (assumes the line is on constant dx
self.map_dx = map_dx # the pixel spacing
try:
self.dx_meter = self.dx * self.map_dx
except TypeError:
# For backwards compatibility we allow this for now
self.dx_meter = None
self._surface_h = surface_h
self._widths = None
self.is_rectangular = None
self.is_trapezoid = None
self.orig_head = orig_head # Useful for debugging and for filtering
self.geometrical_widths = None # these are kept for plotting and such
self.apparent_mb = None # Apparent MB, NOT weighted by width.
self.mu_star = None # the mu* associated with the apparent mb
self.mu_star_is_valid = False # if mu* leeds to good flux, keep it
self.flux = None # Flux (kg m-2)
self.flux_needs_correction = False # whether this branch was baaad
self.rgi_id = rgi_id # Useful if line is used with another glacier
def set_flows_to(self, other, check_tail=True, to_head=False):
"""Find the closest point in "other" and sets all the corresponding
attributes. Btw, it modifies the state of "other" too.
Parameters
----------
other : :py:class:`oggm.Centerline`
another flowline where self should flow to
"""
self.flows_to = other
if check_tail:
# Project the point and Check that its not too close
prdis = other.line.project(self.tail, normalized=False)
ind_closest = np.argmin(np.abs(other.dis_on_line - prdis)).item()
n = len(other.dis_on_line)
if n >= 9:
ind_closest = utils.clip_scalar(ind_closest, 4, n-5)
elif n >= 7:
ind_closest = utils.clip_scalar(ind_closest, 3, n-4)
elif n >= 5:
ind_closest = utils.clip_scalar(ind_closest, 2, n-3)
p = shpg.Point(other.line.coords[int(ind_closest)])
self.flows_to_point = p
elif to_head:
self.flows_to_point = other.head
else:
# just the closest
self.flows_to_point = _projection_point(other, self.tail)
other.inflow_points.append(self.flows_to_point)
other.inflows.append(self)
def set_line(self, line):
"""Update the Shapely LineString coordinate.
Parameters
----------
line : :py:class`shapely.geometry.LineString`
"""
self.nx = len(line.coords)
self.line = line
dis = [line.project(shpg.Point(co)) for co in line.coords]
self.dis_on_line = np.array(dis)
xx, yy = line.xy
self.head = shpg.Point(xx[0], yy[0])
self.tail = shpg.Point(xx[-1], yy[-1])
@lazy_property
def flows_to_indice(self):
"""Indices instead of geometry"""
ind = []
tofind = self.flows_to_point.coords[0]
for i, p in enumerate(self.flows_to.line.coords):
if p == tofind:
ind.append(i)
assert len(ind) == 1, 'We expect exactly one point to be found here.'
return ind[0]
@lazy_property
def inflow_indices(self):
"""Indices instead of geometries"""
inds = []
for p in self.inflow_points:
ind = [i for (i, pi) in enumerate(self.line.coords)
if (p.coords[0] == pi)]
inds.append(ind[0])
assert len(inds) == len(self.inflow_points), ('For every inflow point '
'there should be '
'exactly one inflow '
'indice')
return inds
@lazy_property
def normals(self):
"""List of (n1, n2) normal vectors at each point.
We use second order derivatives for smoother widths.
"""
pcoords = np.array(self.line.coords)
normals = []
# First
normal = np.array(pcoords[1, :] - pcoords[0, :])
normals.append(_normalize(normal))
# Second
normal = np.array(pcoords[2, :] - pcoords[0, :])
normals.append(_normalize(normal))
# Others
for (bbef, bef, cur, aft, aaft) in zip(pcoords[:-4, :],
pcoords[1:-3, :],
pcoords[2:-2, :],
pcoords[3:-1, :],
pcoords[4:, :]):
normal = np.array(aaft + 2*aft - 2*bef - bbef)
normals.append(_normalize(normal))
# One before last
normal = np.array(pcoords[-1, :] - pcoords[-3, :])
normals.append(_normalize(normal))
# Last
normal = np.array(pcoords[-1, :] - pcoords[-2, :])
normals.append(_normalize(normal))
return normals
@property
def widths(self):
"""Needed for overriding later"""
return self._widths
@property
def widths_m(self):
return self.widths * self.map_dx
@widths.setter
def widths(self, value):
self._widths = value
@property
def surface_h(self):
"""Needed for overriding later"""
return self._surface_h
@surface_h.setter
def surface_h(self, value):
self._surface_h = value
def set_apparent_mb(self, mb, mu_star=None):
"""Set the apparent mb and flux for the flowline.
MB is expected in kg m-2 yr-1 (= mm w.e. yr-1)
This should happen in line order, otherwise it will be wrong.
Parameters
----------
mu_star : float
if appropriate, the mu_star associated with this apparent mb
"""
self.apparent_mb = mb
self.mu_star = mu_star
# Add MB to current flux and sum
# no more changes should happen after that
flux_needs_correction = False
flux = np.cumsum(self.flux + mb * self.widths * self.dx)
# We filter only lines with two negative grid points,
# the rest we can cope with
if flux[-2] < 0:
flux_needs_correction = True
self.flux = flux
self.flux_needs_correction = flux_needs_correction
# Add to outflow. That's why it should happen in order
if self.flows_to is not None:
n = len(self.flows_to.line.coords)
ide = self.flows_to_indice
if n >= 9:
gk = GAUSSIAN_KERNEL[9]
self.flows_to.flux[ide-4:ide+5] += gk * flux[-1]
elif n >= 7:
gk = GAUSSIAN_KERNEL[7]
self.flows_to.flux[ide-3:ide+4] += gk * flux[-1]
elif n >= 5:
gk = GAUSSIAN_KERNEL[5]
self.flows_to.flux[ide-2:ide+3] += gk * flux[-1]
def _filter_heads(heads, heads_height, radius, polygon):
"""Filter the head candidates following Kienholz et al. (2014), Ch. 4.1.2
Parameters
----------
heads : list of shapely.geometry.Point instances
The heads to filter out (in raster coordinates).
heads_height : list
The heads altitudes.
radius : float
The radius around each head to search for potential challengers
polygon : shapely.geometry.Polygon class instance
The glacier geometry (in raster coordinates).
Returns
-------
a list of shapely.geometry.Point instances with the "bad ones" removed
"""
heads = copy.copy(heads)
heads_height = copy.copy(heads_height)
i = 0
# I think a "while" here is ok: we remove the heads forwards only
while i < len(heads):
head = heads[i]
pbuffer = head.buffer(radius)
inter_poly = pbuffer.intersection(polygon.exterior)
if inter_poly.type in ['MultiPolygon',
'GeometryCollection',
'MultiLineString']:
# In the case of a junction point, we have to do a check
# http://lists.gispython.org/pipermail/community/
# 2015-January/003357.html
if inter_poly.type == 'MultiLineString':
inter_poly = shapely.ops.linemerge(inter_poly)
if inter_poly.type != 'LineString':
# keep the local polygon only
for sub_poly in inter_poly.geoms:
if sub_poly.intersects(head):
inter_poly = sub_poly
break
elif inter_poly.type == 'LineString':
inter_poly = shpg.Polygon(np.asarray(inter_poly.xy).T)
elif inter_poly.type == 'Polygon':
pass
else:
extext = ('Geometry collection not expected: '
'{}'.format(inter_poly.type))
raise InvalidGeometryError(extext)
# Find other points in radius and in polygon
_heads = [head]
_z = [heads_height[i]]
for op, z in zip(heads[i+1:], heads_height[i+1:]):
if inter_poly.intersects(op):
_heads.append(op)
_z.append(z)
# If alone, go to the next point
if len(_heads) == 1:
i += 1
continue
# If not, keep the highest
_w = np.argmax(_z)
for head in _heads:
if not (head is _heads[_w]):
heads_height = np.delete(heads_height, heads.index(head))
heads.remove(head)
return heads, heads_height
def _filter_lines(lines, heads, k, r):
"""Filter the centerline candidates by length.
Kienholz et al. (2014), Ch. 4.3.1
Parameters
----------
lines : list of shapely.geometry.LineString instances
The lines to filter out (in raster coordinates).
heads : list of shapely.geometry.Point instances
The heads corresponding to the lines.
k : float
A buffer (in raster coordinates) to cut around the selected lines
r : float
The lines shorter than r will be filtered out.
Returns
-------
(lines, heads) a list of the new lines and corresponding heads
"""
olines = []
oheads = []
ilines = copy.copy(lines)
lastline = None
while len(ilines) > 0: # loop as long as we haven't filtered all lines
if len(olines) > 0: # enter this after the first step only
toremove = lastline.buffer(k) # buffer centerlines the last line
tokeep = []
for l in ilines:
# loop over all remaining lines and compute their diff
# to the last longest line
diff = l.difference(toremove)
if diff.type == 'MultiLineString':
# Remove the lines that have no head
diff = list(diff.geoms)
for il in diff:
hashead = False
for h in heads:
if il.intersects(h):
hashead = True
diff = il
break
if hashead:
break
else:
diff = None
# keep this head line only if it's long enough
if diff is not None and diff.length > r:
# Fun fact. The heads can be cut by the buffer too
diff = shpg.LineString(l.coords[0:2] + diff.coords[2:])
tokeep.append(diff)
ilines = tokeep
# it could happen that we're done at this point
if len(ilines) == 0:
break
# Otherwise keep the longest one and continue
lengths = np.array([])
for l in ilines:
lengths = np.append(lengths, l.length)
ll = ilines[np.argmax(lengths)]
ilines.remove(ll)
if len(olines) > 0:
# the cut line's last point is not guaranteed
# to on straight coordinates. Remove it
olines.append(shpg.LineString(np.asarray(ll.xy)[:, 0:-1].T))
else:
olines.append(ll)
lastline = ll
# add the corresponding head to each line
for l in olines:
for h in heads:
if l.intersects(h):
oheads.append(h)
break
return olines, oheads
def _filter_lines_slope(lines, heads, topo, gdir, min_slope):
"""Filter the centerline candidates by slope: if they go up, remove
Kienholz et al. (2014), Ch. 4.3.1
Parameters
----------
lines : list of shapely.geometry.LineString instances
The lines to filter out (in raster coordinates).
topo : the glacier topography
gdir : the glacier directory for simplicity
min_slope: rad
Returns
-------
(lines, heads) a list of the new lines and corresponding heads
"""
dx_cls = cfg.PARAMS['flowline_dx']
lid = int(cfg.PARAMS['flowline_junction_pix'])
sw = cfg.PARAMS['flowline_height_smooth']
# Bilinear interpolation
# Geometries coordinates are in "pixel centered" convention, i.e
# (0, 0) is also located in the center of the pixel
xy = (np.arange(0, gdir.grid.ny-0.1, 1),
np.arange(0, gdir.grid.nx-0.1, 1))
interpolator = RegularGridInterpolator(xy, topo)
olines = [lines[0]]
oheads = [heads[0]]
for line, head in zip(lines[1:], heads[1:]):
# The code below mimics what initialize_flowlines will do
# this is a bit smelly but necessary
points = line_interpol(line, dx_cls)
# For tributaries, remove the tail
points = points[0:-lid]
new_line = shpg.LineString(points)
# Interpolate heights
x, y = new_line.xy
hgts = interpolator((y, x))
# If smoothing, this is the moment
hgts = gaussian_filter1d(hgts, sw)
# Finally slope
slope = np.arctan(-np.gradient(hgts, dx_cls*gdir.grid.dx))
# And altitude range
z_range = np.max(hgts) - np.min(hgts)
# arbitrary threshold with which we filter the lines, otherwise bye bye
if np.sum(slope >= min_slope) >= 5 and z_range > 10:
olines.append(line)
oheads.append(head)
return olines, oheads
def _projection_point(centerline, point):
"""Projects a point on a line and returns the closest integer point
guaranteed to be on the line, and guaranteed to be far enough from the
head and tail.
Parameters
----------
centerline : Centerline instance
point : Shapely Point geometry
Returns
-------
(flow_point, ind_closest): Shapely Point and indice in the line
"""
prdis = centerline.line.project(point, normalized=False)
ind_closest = np.argmin(np.abs(centerline.dis_on_line - prdis)).item()
flow_point = shpg.Point(centerline.line.coords[int(ind_closest)])
return flow_point
def _join_lines(lines, heads):
"""Re-joins the lines that have been cut by _filter_lines
Compute the rooting scheme.
Parameters
----------
lines: list of shapely lines instances
Returns
-------
Centerline instances, updated with flow routing properties
"""
olines = [Centerline(l, orig_head=h) for l, h
in zip(lines[::-1], heads[::-1])]
nl = len(olines)
if nl == 1:
return olines
# per construction the line cannot flow in a line placed before in the list
for i, l in enumerate(olines):
last_point = shpg.Point(*l.line.coords[-1])
totest = olines[i+1:]
dis = [last_point.distance(t.line) for t in totest]
flow_to = totest[np.argmin(dis)]
flow_point = _projection_point(flow_to, last_point)
# Interpolate to finish the line, bute force:
# we interpolate 20 points, round them, remove consecutive duplicates
endline = shpg.LineString([last_point, flow_point])
endline = shpg.LineString([endline.interpolate(x, normalized=True)
for x in np.linspace(0., 1., num=20)])
# we keep all coords without the first AND the last
grouped = groupby(map(tuple, np.rint(endline.coords)))
endline = [x[0] for x in grouped][1:-1]
# We're done
l.set_line(shpg.LineString(l.line.coords[:] + endline))
l.set_flows_to(flow_to, check_tail=False)
# The last one has nowhere to flow
if i+2 == nl:
break
return olines[::-1]
def line_order(line):
"""Recursive search for the line's hydrological level.
Parameters
----------
line: a Centerline instance
Returns
-------
The line's order
"""
if len(line.inflows) == 0:
return 0
else:
levels = [line_order(s) for s in line.inflows]
return np.max(levels) + 1
def line_inflows(line, keep=True):
"""Recursive search for all inflows of the given line.
Parameters
----------
line: a Centerline instance
keep : bool
whether or not the line itself should be kept
Returns
-------
A list of lines (including the line itself) sorted in order
"""
out = set([line])
for l in line.inflows:
out = out.union(line_inflows(l))
out = np.array(list(out))
out = list(out[np.argsort([o.order for o in out])])
if not keep:
out.remove(line)
return out
def _make_costgrid(mask, ext, z):
"""Computes a costgrid following Kienholz et al. (2014) Eq. (2)
Parameters
----------
mask : numpy.array
The glacier mask.
ext : numpy.array
The glacier boundaries' mask.
z : numpy.array
The terrain height.
Returns
-------
numpy.array of the costgrid
"""
dis = np.where(mask, distance_transform_edt(mask), np.NaN)
z = np.where(mask, z, np.NaN)
dmax = np.nanmax(dis)
zmax = np.nanmax(z)
zmin = np.nanmin(z)
cost = ((dmax - dis) / dmax * cfg.PARAMS['f1']) ** cfg.PARAMS['a'] + \
((z - zmin) / (zmax - zmin) * cfg.PARAMS['f2']) ** cfg.PARAMS['b']
# This is new: we make the cost to go over boundaries
# arbitrary high to avoid the lines to jump over adjacent boundaries
cost[np.where(ext)] = np.nanmax(cost[np.where(ext)]) * 50
return np.where(mask, cost, np.Inf)
def _get_terminus_coord(gdir, ext_yx, zoutline):
"""This finds the terminus coordinate of the glacier.
There is a special case for marine terminating glaciers/
"""
perc = cfg.PARAMS['terminus_search_percentile']
deltah = cfg.PARAMS['terminus_search_altitude_range']
if gdir.is_tidewater and (perc > 0):
# There is calving
# find the lowest percentile
plow = np.percentile(zoutline, perc).astype(np.int64)
# the minimum altitude in the glacier
mini = np.min(zoutline)
# indices of where in the outline the altitude is lower than the qth
# percentile and lower than 100m higher, than the minimum altitude
ind = np.where((zoutline < plow) & (zoutline < (mini + deltah)))[0]
# We take the middle of this area
try:
ind_term = ind[np.round(len(ind) / 2.).astype(int)]
except IndexError:
# Sometimes the default perc is not large enough
try:
# Repeat
perc *= 2
plow = np.percentile(zoutline, perc).astype(np.int64)
mini = np.min(zoutline)
ind = np.where((zoutline < plow) &
(zoutline < (mini + deltah)))[0]
ind_term = ind[np.round(len(ind) / 2.).astype(int)]
except IndexError:
# Last resort
ind_term = np.argmin(zoutline)
else:
# easy: just the minimum
ind_term = np.argmin(zoutline)
return np.asarray(ext_yx)[:, ind_term].astype(np.int64)
def _normalize(n):
"""Computes the normals of a vector n.
Returns
-------
the two normals (n1, n2)
"""
nn = n / np.sqrt(np.sum(n*n))
n1 = np.array([-nn[1], nn[0]])
n2 = np.array([nn[1], -nn[0]])
return n1, n2
def _get_centerlines_heads(gdir, ext_yx, zoutline, single_fl,
glacier_mask, topo, geom, poly_pix):
# Size of the half window to use to look for local maximas
maxorder = np.rint(cfg.PARAMS['localmax_window'] / gdir.grid.dx)
maxorder = utils.clip_scalar(maxorder, 5., np.rint((len(zoutline) / 5.)))
heads_idx = scipy.signal.argrelmax(zoutline, mode='wrap',
order=int(maxorder))
if single_fl or len(heads_idx[0]) <= 1:
# small glaciers with one or less heads: take the absolute max
heads_idx = (np.atleast_1d(np.argmax(zoutline)),)
# Remove the heads that are too low
zglacier = topo[np.where(glacier_mask)]
head_threshold = np.percentile(zglacier, (1./3.)*100)
_heads_idx = heads_idx[0][np.where(zoutline[heads_idx] > head_threshold)]
if len(_heads_idx) == 0:
# this is for baaad ice caps where the outline is far off in altitude
_heads_idx = [heads_idx[0][np.argmax(zoutline[heads_idx])]]
heads_idx = _heads_idx
heads = np.asarray(ext_yx)[:, heads_idx]
heads_z = zoutline[heads_idx]
# careful, the coords are in y, x order!
heads = [shpg.Point(x, y) for y, x in zip(heads[0, :],
heads[1, :])]
# get radius of the buffer according to Kienholz eq. (1)
radius = cfg.PARAMS['q1'] * geom['polygon_area'] + cfg.PARAMS['q2']
radius = utils.clip_scalar(radius, 0, cfg.PARAMS['rmax'])
radius /= gdir.grid.dx # in raster coordinates
# Plus our criteria, quite useful to remove short lines:
radius += cfg.PARAMS['flowline_junction_pix'] * cfg.PARAMS['flowline_dx']
log.debug('(%s) radius in raster coordinates: %.2f',
gdir.rgi_id, radius)
# OK. Filter and see.
log.debug('(%s) number of heads before radius filter: %d',
gdir.rgi_id, len(heads))
heads, heads_z = _filter_heads(heads, heads_z, radius, poly_pix)
log.debug('(%s) number of heads after radius filter: %d',
gdir.rgi_id, len(heads))
return heads
def _line_extend(uline, dline, dx):
"""Adds a downstream line to a flowline
Parameters
----------
uline: a shapely.geometry.LineString instance
dline: a shapely.geometry.LineString instance
dx: the spacing
Returns
-------
(line, line) : two shapely.geometry.LineString instances. The first
contains the newly created (long) line, the second only the interpolated
downstream part (useful for other analyses)
"""
# First points is easy
points = [shpg.Point(c) for c in uline.coords]
if len(points) == 0:
# eb flowline
dpoints = [shpg.Point(dline.coords[0])]
points = [shpg.Point(dline.coords[0])]
else:
dpoints = []
# Continue as long as line is not finished
while True:
pref = points[-1]
pbs = pref.buffer(dx).boundary.intersection(dline)
if pbs.type in ['LineString', 'GeometryCollection']:
# Very rare
pbs = pref.buffer(dx+1e-12).boundary.intersection(dline)
if pbs.type == 'Point':
pbs = [pbs]
try:
# Shapely v2 compat
pbs = pbs.geoms
except AttributeError:
pass
# Out of the point(s) that we get, take the one farthest from the top
refdis = dline.project(pref)
tdis = np.array([dline.project(pb) for pb in pbs])
p = np.where(tdis > refdis)[0]
if len(p) == 0:
break
points.append(pbs[int(p[0])])
dpoints.append(pbs[int(p[0])])
return shpg.LineString(points), shpg.LineString(dpoints)
@entity_task(log, writes=['centerlines', 'gridded_data'])
def compute_centerlines(gdir, heads=None):
"""Compute the centerlines following Kienholz et al., (2014).
They are then sorted according to the modified Strahler number:
http://en.wikipedia.org/wiki/Strahler_number
This function does not initialize a :py:class:`oggm.Centerline` but
calculates routes along the topography and makes a
:py:class:`shapely.Linestring` object from them.
Parameters
----------
gdir : :py:class:`oggm.GlacierDirectory`
where to write the data
heads : list, optional
list of shapely.geometry.Points to use as line heads (default is to
compute them like Kienholz did)
"""
# Params
single_fl = not cfg.PARAMS['use_multiple_flowlines']
do_filter_slope = cfg.PARAMS['filter_min_slope']
min_slope = 'min_slope_ice_caps' if gdir.is_icecap else 'min_slope'
min_slope = np.deg2rad(cfg.PARAMS[min_slope])
# Force single flowline for ice caps
if gdir.is_icecap:
single_fl = True
if 'force_one_flowline' in cfg.PARAMS:
raise InvalidParamsError('`force_one_flowline` is deprecated')
# open
geom = gdir.read_pickle('geometries')
grids_file = gdir.get_filepath('gridded_data')
with utils.ncDataset(grids_file) as nc:
# Variables
glacier_mask = nc.variables['glacier_mask'][:]
glacier_ext = nc.variables['glacier_ext'][:]
topo = nc.variables['topo_smoothed'][:]
poly_pix = geom['polygon_pix']
# Find for local maximas on the outline
x, y = tuple2int(poly_pix.exterior.xy)
ext_yx = tuple(reversed(poly_pix.exterior.xy))
zoutline = topo[y[:-1], x[:-1]] # last point is first point
# For diagnostics
is_first_call = False
if heads is None:
# This is the default for when no filter is yet applied
is_first_call = True
heads = _get_centerlines_heads(gdir, ext_yx, zoutline, single_fl,
glacier_mask, topo, geom, poly_pix)
# Cost array
costgrid = _make_costgrid(glacier_mask, glacier_ext, topo)
# Terminus
t_coord = _get_terminus_coord(gdir, ext_yx, zoutline)
# Compute the routes
lines = []
for h in heads:
h_coord = np.asarray(h.xy)[::-1].astype(np.int64)
indices, _ = route_through_array(costgrid, h_coord, t_coord)
lines.append(shpg.LineString(np.array(indices)[:, [1, 0]]))
log.debug('(%s) computed the routes', gdir.rgi_id)
# Filter the shortest lines out
dx_cls = cfg.PARAMS['flowline_dx']
radius = cfg.PARAMS['flowline_junction_pix'] * dx_cls
radius += 6 * dx_cls
olines, oheads = _filter_lines(lines, heads, cfg.PARAMS['kbuffer'], radius)
log.debug('(%s) number of heads after lines filter: %d',
gdir.rgi_id, len(olines))
# Filter the lines which are going up instead of down
if do_filter_slope:
olines, oheads = _filter_lines_slope(olines, oheads, topo,
gdir, min_slope)
log.debug('(%s) number of heads after slope filter: %d',
gdir.rgi_id, len(olines))
# And rejoin the cut tails
olines = _join_lines(olines, oheads)
# Adds the line level
for cl in olines:
cl.order = line_order(cl)
# And sort them per order !!! several downstream tasks rely on this
cls = []
for i in np.argsort([cl.order for cl in olines]):
cls.append(olines[i])
# Final check
if len(cls) == 0:
raise GeometryError('({}) no valid centerline could be '
'found!'.format(gdir.rgi_id))
# Write the data
gdir.write_pickle(cls, 'centerlines')
if is_first_call:
# For diagnostics of filtered centerlines
gdir.add_to_diagnostics('n_orig_centerlines', len(cls))
###
from oggm import entity_task, cfg
import logging
def geoline_to_cls(lines):
"""
list of shapely.geometry.lines to be converted to Centerline list
Parameters
----------
lines : list of shapely.geometry.lines
list of shapely.geometry.lines
Returns
-------
list of centerlines instances.
"""
clss = []
for li in lines:
clss.append(Centerline(li))
cls = clss
return cls
def get_centerlines_rgi(dem_path, outline):
"""
Given outline and DEM get glacier centerlines.
Parameters
----------
dem_path : str
Gopography. Path to .tif file
outline_path : str
Glacier outlines. Path to shapefile.
Returns
-------
None.