-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathToolbox.py
executable file
·3061 lines (2470 loc) · 105 KB
/
Toolbox.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
"""
This module contains utilities to work with satellite images
Author: Kilian Vos, Water Research Laboratory, University of New South Wales
COASTGUARD edits and updates: Freya Muir, University of Glasgow
"""
# load modules
import os
import glob
import numpy as np
import pickle
import math
from datetime import datetime, timedelta
from IPython.display import clear_output
# other modules
from osgeo import gdal, osr
import pandas as pd
import geopandas as gpd
import utm
from shapely import geometry, affinity
from shapely.geometry import Polygon, LineString, MultiPoint
import folium
from pyproj import Proj
from pyproj import transform as Transf
import skimage.transform as transform
import sklearn
import scipy
from scipy import interpolate
from scipy.stats import circmean, circstd, skew, kurtosis
from statsmodels.tsa.seasonal import seasonal_decompose
from astropy.convolution import convolve
import ee
import geemap
import pyfes
import netCDF4
from Toolshed import Image_Processing
np.seterr(all='ignore') # raise/ignore divisions by 0 and nans
###################################################################################################
# COORDINATES CONVERSION FUNCTIONS
###################################################################################################
def convert_pix2world(points, georef):
"""
Converts pixel coordinates (pixel row and column) to world projected
coordinates performing an affine transformation.
KV WRL 2018
Arguments:
-----------
points: np.array or list of np.array
array with 2 columns (row first and column second)
georef: np.array
vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale]
Returns:
-----------
points_converted: np.array or list of np.array
converted coordinates, first columns with X and second column with Y
"""
# make affine transformation matrix
aff_mat = np.array([[georef[1], georef[2], georef[0]],
[georef[4], georef[5], georef[3]],
[0, 0, 1]])
# create affine transformation
tform = transform.AffineTransform(aff_mat)
# if list of arrays
if type(points) is list:
points_converted = []
# iterate over the list
for i, arr in enumerate(points):
tmp = arr[:,[1,0]]
points_converted.append(tform(tmp))
# if single array
elif type(points) is np.ndarray:
tmp = points[:,[1,0]]
points_converted = tform(tmp)
else:
raise Exception('invalid input type')
return points_converted
def convert_world2pix(points, georef):
"""
Converts world projected coordinates (X,Y) to image coordinates
(pixel row and column) performing an affine transformation.
KV WRL 2018
Arguments:
-----------
points: np.array or list of np.array
array with 2 columns (X,Y)
georef: np.array
vector of 6 elements [Xtr, Xscale, Xshear, Ytr, Yshear, Yscale]
Xtr = xoff
Ytr = yoff
Returns:
-----------
points_converted: np.array or list of np.array
converted coordinates (pixel row and column)
"""
# make affine transformation matrix
aff_mat = np.array([[georef[1], georef[2], georef[0]],[georef[4], georef[5], georef[3]],[0, 0, 1]])
# create affine transformation
tform = transform.AffineTransform(aff_mat)
# if list of arrays
if type(points) is list:
points_converted = []
# iterate over the list
for i, arr in enumerate(points):
points_converted.append(tform.inverse(points))
# if single array
elif type(points) is np.ndarray:
points_converted = tform.inverse(points)
else:
print('invalid input type')
raise
return points_converted
def convert_epsg(points, epsg_in, epsg_out):
"""
Converts from one spatial reference to another using the epsg codes
KV WRL 2018
Arguments:
-----------
points: np.array or list of np.ndarray
array with 2 columns (rows first and columns second)
epsg_in: int
epsg code of the spatial reference in which the input is
epsg_out: int
epsg code of the spatial reference in which the output will be
Returns:
-----------
points_converted: np.array or list of np.array
converted coordinates from epsg_in to epsg_out
"""
# define input and output spatial references
inSpatialRef = osr.SpatialReference()
inSpatialRef.ImportFromEPSG(epsg_in)
outSpatialRef = osr.SpatialReference()
outSpatialRef.ImportFromEPSG(epsg_out)
# create a coordinates transform
coordTransform = osr.CoordinateTransformation(inSpatialRef, outSpatialRef)
# if list of arrays
if type(points) is list:
points_converted = []
# iterate over the list
for i, arr in enumerate(points):
points_converted.append(np.array(coordTransform.TransformPoints(arr)))
# if single array
elif type(points) is np.ndarray:
points_converted = np.array(coordTransform.TransformPoints(points))
else:
raise Exception('invalid input type')
return points_converted
def get_UTMepsg_from_wgs(lat, lon):
"""
Retrieves an epsg code from lat lon in wgs84
see https://stackoverflow.com/a/40140326/4556479
added by MDH 2023
Arguments:
-----------
lat: latitude in WGS84
lon: longitude in WGS84
Returns:
-----------
epsg_code: epsg code for best UTM zone
"""
utm_band = str((math.floor((lon + 180) / 6 ) % 60) + 1)
if len(utm_band) == 1:
utm_band = '0'+utm_band
if lat >= 0:
epsg_code = '326' + utm_band
return epsg_code
epsg_code = '327' + utm_band
return epsg_code
###################################################################################################
# IMAGE ANALYSIS FUNCTIONS
###################################################################################################
def nd_index(im1, im2, cloud_mask):
"""
Computes normalised difference index on 2 images (2D), given a cloud mask (2D).
KV WRL 2018
Arguments:
-----------
im1: np.array
first image (2D) with which to calculate the ND index
im2: np.array
second image (2D) with which to calculate the ND index
cloud_mask: np.array
2D cloud mask with True where cloud pixels are
Returns:
-----------
im_nd: np.array
Image (2D) containing the ND index
"""
# reshape the cloud mask
vec_mask = cloud_mask.reshape(im1.shape[0] * im1.shape[1])
# initialise with NaNs
vec_nd = np.ones(len(vec_mask)) * np.nan
# reshape the two images
vec1 = im1.reshape(im1.shape[0] * im1.shape[1])
vec2 = im2.reshape(im2.shape[0] * im2.shape[1])
# compute the normalised difference index
temp = np.divide(vec1[~vec_mask] - vec2[~vec_mask],
vec1[~vec_mask] + vec2[~vec_mask])
vec_nd[~vec_mask] = temp
# reshape into image
im_nd = vec_nd.reshape(im1.shape[0], im1.shape[1])
return im_nd
def savi_index(im1, im2, cloud_mask):
"""
Computes soil adjusted vegetation index on 2 bands (2D), given a cloud mask (2D).
FM 2022
Arguments:
-----------
im1: np.array
first image (2D) with which to calculate the ND index (should be NIR band)
im2: np.array
second image (2D) with which to calculate the ND index (should be Red band)
cloud_mask: np.array
2D cloud mask with True where cloud pixels are
Returns:
-----------
im_nd: np.array
Image (2D) containing the ND index
"""
# reshape the cloud mask
vec_mask = cloud_mask.reshape(im1.shape[0] * im1.shape[1])
# initialise with NaNs
vec_nd = np.ones(len(vec_mask)) * np.nan
# reshape the two images
nir = im1.reshape(im1.shape[0] * im1.shape[1])
red = im2.reshape(im2.shape[0] * im2.shape[1])
# compute the normalised difference index
temp = np.divide(nir[~vec_mask] - red[~vec_mask],
nir[~vec_mask] + red[~vec_mask] + 0.5) * (1 + 0.5)
vec_nd[~vec_mask] = temp
# reshape into image
im_nd = vec_nd.reshape(im1.shape[0], im1.shape[1])
return im_nd
def rbnd_index(im1, im2, im3, cloud_mask):
"""
Computes soil adjusted vegetation index on 2 bands (2D), given a cloud mask (2D).
FM 2022
Arguments:
-----------
im1: np.array
first image (2D) with which to calculate the ND index (should be NIR band)
im2: np.array
second image (2D) with which to calculate the ND index (should be Red band)
im3: np.array
third image (2D) with which to calculate the ND index (should be Blue band)
cloud_mask: np.array
2D cloud mask with True where cloud pixels are
Returns:
-----------
im_nd: np.array
Image (2D) containing the ND index
"""
# reshape the cloud mask
vec_mask = cloud_mask.reshape(im1.shape[0] * im1.shape[1])
# initialise with NaNs
vec_nd = np.ones(len(vec_mask)) * np.nan
# reshape the images
nir = im1.reshape(im1.shape[0] * im1.shape[1])
red = im2.reshape(im2.shape[0] * im2.shape[1])
blue = im3.reshape(im3.shape[0] * im3.shape[1])
# compute the normalised difference index
temp = np.divide(nir[~vec_mask] - (red[~vec_mask] + blue[~vec_mask]),
nir[~vec_mask] + (red[~vec_mask] + blue[~vec_mask]))
vec_nd[~vec_mask] = temp
# reshape into image
im_nd = vec_nd.reshape(im1.shape[0], im1.shape[1])
return im_nd
def image_std(image, radius):
"""
Calculates the standard deviation of an image, using a moving window of
specified radius. Uses astropy's convolution library'
Arguments:
-----------
image: np.array
2D array containing the pixel intensities of a single-band image
radius: int
radius defining the moving window used to calculate the standard deviation.
For example, radius = 1 will produce a 3x3 moving window.
Returns:
-----------
win_std: np.array
2D array containing the standard deviation of the image
"""
# convert to float
image = image.astype(float)
# first pad the image
image_padded = np.pad(image, radius, 'reflect')
# window size
win_rows, win_cols = radius*2 + 1, radius*2 + 1
# calculate std with uniform filters
win_mean = convolve(image_padded, np.ones((win_rows,win_cols)), boundary='extend',
normalize_kernel=True, nan_treatment='interpolate', preserve_nan=True)
win_sqr_mean = convolve(image_padded**2, np.ones((win_rows,win_cols)), boundary='extend',
normalize_kernel=True, nan_treatment='interpolate', preserve_nan=True)
win_var = win_sqr_mean - win_mean**2
win_std = np.sqrt(win_var)
# remove padding
win_std = win_std[radius:-radius, radius:-radius]
return win_std
def mask_raster(fn, mask):
"""
Masks a .tif raster using GDAL.
Arguments:
-----------
fn: str
filepath + filename of the .tif raster
mask: np.array
array of boolean where True indicates the pixels that are to be masked
Returns:
-----------
Overwrites the .tif file directly
"""
# open raster
raster = gdal.Open(fn, gdal.GA_Update)
# mask raster
for i in range(raster.RasterCount):
out_band = raster.GetRasterBand(i+1)
out_data = out_band.ReadAsArray()
out_band.SetNoDataValue(0)
no_data_value = out_band.GetNoDataValue()
out_data[mask] = no_data_value
out_band.WriteArray(out_data)
# close dataset and flush cache
raster = None
###################################################################################################
# UTILITIES
###################################################################################################
def find(item, lst):
"""
Iterate through list to find matching item.
FM 2022
Parameters
----------
item : TYPE
DESCRIPTION.
lst : TYPE
DESCRIPTION.
Returns
-------
start : TYPE
DESCRIPTION.
"""
start = 0
start = lst.index(item, start)
return start
def get_filepath(inputs,satname):
"""
Create filepath to the different folders containing the satellite images.
KV WRL 2018
Arguments:
-----------
inputs: dict with the following keys
'sitename': str
name of the site
'polygon': list
polygon containing the lon/lat coordinates to be extracted,
longitudes in the first column and latitudes in the second column,
there are 5 pairs of lat/lon with the fifth point equal to the first point:
```
polygon = [[[151.3, -33.7],[151.4, -33.7],[151.4, -33.8],[151.3, -33.8],
[151.3, -33.7]]]
```
'dates': list of str
list that contains 2 strings with the initial and final dates in
format 'yyyy-mm-dd':
```
dates = ['1987-01-01', '2018-01-01']
```
'sat_list': list of str
list that contains the names of the satellite missions to include:
```
sat_list = ['L5', 'L7', 'L8', 'S2']
```
'filepath_data': str
filepath to the directory where the images are downloaded
satname: str
short name of the satellite mission ('L5','L7','L8','S2')
Returns:
-----------
filepath: str or list of str
contains the filepath(s) to the folder(s) containing the satellite images
"""
sitename = inputs['sitename']
filepath_data = inputs['filepath']
# access the images
if satname == 'L5':
# access downloaded Landsat 5 images
filepath = os.path.join(filepath_data, sitename, satname, '30m')
elif satname == 'L7':
# access downloaded Landsat 7 images
filepath_pan = os.path.join(filepath_data, sitename, 'L7', 'pan')
filepath_ms = os.path.join(filepath_data, sitename, 'L7', 'ms')
filepath = [filepath_pan, filepath_ms]
elif satname == 'L8':
# access downloaded Landsat 8 images
filepath_pan = os.path.join(filepath_data, sitename, 'L8', 'pan')
filepath_ms = os.path.join(filepath_data, sitename, 'L8', 'ms')
filepath = [filepath_pan, filepath_ms]
elif satname == 'S2':
# access downloaded Sentinel 2 images
filepath10 = os.path.join(filepath_data, sitename, satname, '10m')
filepath20 = os.path.join(filepath_data, sitename, satname, '20m')
filepath60 = os.path.join(filepath_data, sitename, satname, '60m')
filepath = [filepath10, filepath20, filepath60]
return filepath
def get_filenames(filename, filepath, satname):
"""
Creates filepath + filename for all the bands belonging to the same image.
KV WRL 2018
Arguments:
-----------
filename: str
name of the downloaded satellite image as found in the metadata
filepath: str or list of str
contains the filepath(s) to the folder(s) containing the satellite images
satname: str
short name of the satellite mission
Returns:
-----------
fn: str or list of str
contains the filepath + filenames to access the satellite image
"""
if satname == 'L5':
fn = os.path.join(filepath, filename)
if satname == 'L7' or satname == 'L8':
filename_ms = filename.replace('pan','ms')
fn = [os.path.join(filepath[0], filename),
os.path.join(filepath[1], filename_ms)]
if satname == 'S2':
filename20 = filename.replace('10m','20m')
filename60 = filename.replace('10m','60m')
fn = [os.path.join(filepath[0], filename),
os.path.join(filepath[1], filename20),
os.path.join(filepath[2], filename60)]
return fn
def merge_output(output):
"""
Function to merge the output dictionary, which has one key per satellite mission
into a dictionary containing all the shorelines and dates ordered chronologically.
Arguments:
-----------
output: dict
contains the extracted shorelines and corresponding dates, organised by
satellite mission
Returns:
-----------
output_all: dict
contains the extracted shorelines in a single list sorted by date
"""
# initialize output dict
output_all = dict([])
satnames = list(output.keys())
for key in output[satnames[0]].keys():
output_all[key] = []
# create extra key for the satellite name
output_all['satname'] = []
# fill the output dict
for satname in list(output.keys()):
for key in output[satnames[0]].keys():
output_all[key] = output_all[key] + output[satname][key]
output_all['satname'] = output_all['satname'] + [str(_) for _ in np.tile(satname,
len(output[satname]['dates']))]
# sort chronologically
idx_sorted = sorted(range(len(output_all['dates'])), key=output_all['dates'].__getitem__)
for key in output_all.keys():
output_all[key] = [output_all[key][i] for i in idx_sorted]
return output_all
def remove_duplicates(output):
"""
Function to remove from the output dictionnary entries containing shorelines for
the same date and satellite mission. This happens when there is an overlap between
adjacent satellite images.
Arguments:
-----------
output: dict
contains output dict with shoreline and metadata
Returns:
-----------
output_no_duplicates: dict
contains the updated dict where duplicates have been removed
"""
# nested function
def duplicates_dict(lst):
"return duplicates and indices"
def duplicates(lst, item):
return [i for i, x in enumerate(lst) if x == item]
return dict((x, duplicates(lst, x)) for x in set(lst) if lst.count(x) > 1)
dates = output['dates']
# make a list with year/month/day
dates_str = [datetime.strptime(_,'%Y-%m-%d').strftime('%Y-%m-%d') for _ in dates]
# create a dictionnary with the duplicates
dupl = duplicates_dict(dates_str)
# if there are duplicates, only keep the first element
if dupl:
output_no_duplicates = dict([])
idx_remove = []
for k,v in dupl.items():
idx_remove.append(v[0])
idx_remove = sorted(idx_remove)
idx_all = np.linspace(0, len(dates_str)-1, len(dates_str))
idx_keep = list(np.where(~np.isin(idx_all,idx_remove))[0])
for key in output.keys():
output_no_duplicates[key] = [output[key][i] for i in idx_keep]
print('%d duplicates' % len(idx_remove))
return output_no_duplicates
else:
print('0 duplicates')
return output
def RemoveDuplicates(output):
"""
Function to remove entries containing shorelines for the same date and satellite mission.
This happens when there is an overlap between adjacent satellite images.
If there is an overlap, this function keeps the longest line from the duplicates.
FM Oct 2023
Arguments:
-----------
output: dict
contains output dict with shoreline and metadata
Returns:
-----------
output_no_duplicates: dict
contains the updated dict where duplicates have been removed
"""
# nested function
def duplicates_dict(dates_str):
"return duplicates and indices"
def duplicateIDs(dates_str, date_str):
# need to return output['idx'] and not just enumerate ID
# return [i for i, x in enumerate(dates_str) if x == date_str]
return [output['idx'][i] for i, x in enumerate(dates_str) if x == date_str]
# return dict((date_str, duplicates(dates_str, date_str)) for date_str in set(dates_str) if dates_str.count(date_str) > 1)
dupl = {}
for date_str in set(dates_str):
if dates_str.count(date_str) > 1:
dupl[date_str] = duplicateIDs(dates_str, date_str)
return dupl
def update_dupl(output):
dates = output['dates']
# make a list with year/month/day
dates_str = [datetime.strptime(_,'%Y-%m-%d').strftime('%Y-%m-%d') for _ in dates]
# create a dictionary with the duplicates
dupl = dict(sorted(duplicates_dict(dates_str).items(), key=lambda x:x[1]))
if dupl:
# Keep duplicates with different satnames
for key,IDval in list(dupl.items()): # has to be list so as not to change dict while looping
if len(set(output['satname'][IDval[0]:IDval[1]+1])) > 1:
del(dupl[key])
return dupl
dupl = update_dupl(output)
# if there are duplicates, only keep the element with the longest line
while dupl:
dupl = update_dupl(output)
dupcount = []
for key,IDval in list(dupl.items()):
dupcount.append(IDval[0])
lengths = []
# calculate lengths of duplicated line features
for v in IDval:
if len(output['veglines'][output['idx'].index(v)]) > 1: # multiline feature; take sum of line lengths
lengths.append(sum(output['veglines'][output['idx'].index(v)].length))
else:
if len(output['veglines'][output['idx'].index(v)].length) == 0: # empty geoms
lengths.append(0)
else:
lengths.append(output['veglines'][output['idx'].index(v)].length.iloc[0])
minlenID = lengths.index(min(lengths))
# keep the longest line (i.e. remove the shortest)
for okey in list(output.keys()):
# delete the other keys first, leaving idx for last
if okey != 'idx':
delID = output['idx'].index(IDval[minlenID])
del(output[okey][delID])
delID = output['idx'].index(IDval[minlenID])
del(output['idx'][delID])
print('%d duplicates' % len(dupcount))
else:
print('0 duplicates')
return output
def get_closest_datapoint(dates, dates_ts, values_ts):
"""
Extremely efficient script to get closest data point to a set of dates from a very
long time-series (e.g., 15-minutes tide data, or hourly wave data)
Make sure that dates and dates_ts are in the same timezone (also aware or naive)
KV WRL 2020
Arguments:
-----------
dates: list of datetimes
dates at which the closest point from the time-series should be extracted
dates_ts: list of datetimes
dates of the long time-series
values_ts: np.array
array with the values of the long time-series (tides, waves, etc...)
Returns:
-----------
values: np.array
values corresponding to the input dates
"""
# check if the time-series cover the dates
if dates[0] < dates_ts[0] or dates[-1] > dates_ts[-1]:
raise Exception('Time-series do not cover the range of your input dates')
# get closest point to each date (no interpolation)
temp = []
for i,date in enumerate(dates):
print('\rExtracting closest points: %d%%' % int((i+1)*100/len(dates)), end='')
temp.append(values_ts[find(min(item for item in dates_ts if item > date), dates_ts)])
values = np.array(temp)
return values
###################################################################################################
# CONVERSIONS FROM DICT TO GEODATAFRAME AND READ/WRITE GEOJSON
###################################################################################################
def polygon_from_kml(fn):
"""
Extracts coordinates from a .kml file.
KV WRL 2018
Arguments:
-----------
fn: str
filepath + filename of the kml file to be read
Returns:
-----------
polygon: list
coordinates extracted from the .kml file
"""
# read .kml file
with open(fn) as kmlFile:
doc = kmlFile.read()
# parse to find coordinates field
str1 = '<coordinates>'
str2 = '</coordinates>'
subdoc = doc[doc.find(str1)+len(str1):doc.find(str2)]
coordlist = subdoc.split('\n')
# read coordinates
polygon = []
for i in range(1,len(coordlist)-1):
polygon.append([float(coordlist[i].split(',')[0]), float(coordlist[i].split(',')[1])])
return [polygon]
def transects_from_geojson(filename):
"""
Reads transect coordinates from a .geojson file.
Arguments:
-----------
filename: str
contains the path and filename of the geojson file to be loaded
Returns:
-----------
transects: dict
contains the X and Y coordinates of each transect
"""
gdf = gpd.read_file(filename)
transects = dict([])
for i in gdf.index:
transects[gdf.loc[i,'name']] = np.array(gdf.loc[i,'geometry'].coords)
print('%d transects have been loaded' % len(transects.keys()))
return transects
def output_to_gdf(output, geomtype):
"""
Saves the mapped shorelines as a gpd.GeoDataFrame
KV WRL 2018
Arguments:
-----------
output: dict
contains the coordinates of the mapped shorelines + attributes
geomtype: str
'lines' for LineString and 'points' for Multipoint geometry
Returns:
-----------
gdf_all: gpd.GeoDataFrame
contains the shorelines + attirbutes
"""
# loop through the mapped shorelines
counter = 0
for i in range(len(output['shorelines'])):
# skip if there shoreline is empty
if len(output['shorelines'][i]) == 0:
continue
else:
# save the geometry depending on the linestyle
if geomtype == 'lines':
for j in range (len(output['shorelines'][i])):
abbba = []
abbba.append(output['shorelines'][i][j][1])
abbba.append(output['shorelines'][i][j][0])
output['shorelines'][i][j] = abbba
geom = geometry.LineString(output['shorelines'][i])
elif geomtype == 'points':
coords = output['shorelines'][i]
geom = geometry.MultiPoint([(coords[_,1], coords[_,0]) for _ in range(coords.shape[0])])
else:
raise Exception('geomtype %s is not an option, choose between lines or points'%geomtype)
# save into geodataframe with attributes
gdf = gpd.GeoDataFrame(geometry=gpd.GeoSeries(geom))
gdf.index = [i]
gdf.loc[i,'date'] = datetime.strftime(datetime.strptime(output['dates'][0],'%Y-%m-%d'),'%Y-%m-%d %H:%M:%S')
gdf.loc[i,'satname'] = output['satname'][i]
gdf.loc[i,'cloud_cover'] = output['cloud_cover'][i]
# store into geodataframe
if counter == 0:
gdf_all = gdf
else:
gdf_all = gdf_all.append(gdf)
counter = counter + 1
return gdf_all
def transects_to_gdf(transects):
"""
Saves the shore-normal transects as a gpd.GeoDataFrame
KV WRL 2018
Arguments:
-----------
transects: dict
contains the coordinates of the transects
Returns:
-----------
gdf_all: gpd.GeoDataFrame
"""
# loop through the mapped shorelines
for i,key in enumerate(list(transects.keys())):
# save the geometry + attributes
geom = geometry.LineString(transects[key])
gdf = gpd.GeoDataFrame(geometry=gpd.GeoSeries(geom))
gdf.index = [i]
gdf.loc[i,'name'] = key
# store into geodataframe
if i == 0:
gdf_all = gdf
else:
gdf_all = gdf_all.append(gdf)
return gdf_all
def get_image_bounds(fn):
"""
Returns a polygon with the bounds of the image in the .tif file
KV WRL 2020
Arguments:
-----------
fn: str
path to the image (.tif file)
Returns:
-----------
bounds_polygon: shapely.geometry.Polygon
polygon with the image bounds
"""
# nested functions to get the extent
# copied from https://gis.stackexchange.com/questions/57834/how-to-get-raster-corner-coordinates-using-python-gdal-bindings
def GetExtent(gt,cols,rows):
'Return list of corner coordinates from a geotransform'
ext=[]
xarr=[0,cols]
yarr=[0,rows]
for px in xarr:
for py in yarr:
x=gt[0]+(px*gt[1])+(py*gt[2])
y=gt[3]+(px*gt[4])+(py*gt[5])
ext.append([x,y])
yarr.reverse()
return ext
# load .tif file and get bounds
data = gdal.Open(fn, gdal.GA_ReadOnly)
gt = data.GetGeoTransform()
cols = data.RasterXSize
rows = data.RasterYSize
ext = GetExtent(gt,cols,rows)
return geometry.Polygon(ext)
def smallest_rectangle(polygon):
"""
Converts a polygon to the smallest rectangle polygon with sides parallel
to coordinate axes.
KV WRL 2020
Arguments:
-----------
polygon: list of coordinates
pair of coordinates for 5 vertices, in clockwise order,
first and last points must match
Returns:
-----------
polygon: list of coordinates
smallest rectangle polygon
"""
multipoints = geometry.Polygon(polygon[0])
polygon_geom = multipoints.envelope
coords_polygon = np.array(polygon_geom.exterior.coords)
polygon_rect = [[[_[0], _[1]] for _ in coords_polygon]]
return polygon_rect
def CreateFileStructure(sitename, sat_list):
"""
Create file structure for veg edge extraction.
FM 2022
Parameters
----------
sitename : str
Name of site for analysis.
sat_list : list
List of strings corresponding to satellite platforms to use.
Returns
-------
filepath : str
Filepath to project folder for desired sitename.
"""
filepath = os.path.join(os.getcwd(), 'Data')
direc = os.path.join(filepath, sitename)
if os.path.isdir(direc) is False:
os.mkdir(direc)
if 'PSScene4Band' in sat_list:
if os.path.isdir(direc+'/local_images') is False:
os.mkdir(direc+'/local_images')
os.mkdir(direc+'/local_images/PlanetScope')
os.mkdir(direc+'/AuxillaryImages')
os.mkdir(direc+'/local_images/PlanetScope/cloudmasks')