1
+ #! /usr/bin/env python
2
+
3
+ import xarray as xr
4
+ import rasterio as rio
5
+ import rioxarray
6
+ import numpy as np
7
+ import os
8
+ from autoRIFT import autoRIFT
9
+ from scipy .interpolate import interpn
10
+ import pystac
11
+ import pystac_client
12
+ import stackstac
13
+ from dask .distributed import Client
14
+ import geopandas as gpd
15
+ from shapely .geometry import shape
16
+ import dask
17
+ import warnings
18
+ import argparse
19
+
20
+ # silence some warnings from stackstac and autoRIFT
21
+ warnings .filterwarnings ("ignore" , category = RuntimeWarning )
22
+ warnings .filterwarnings ("ignore" , category = UserWarning )
23
+
24
+ def download_s2 (img1_date , img2_date , bbox ):
25
+ '''
26
+ Download a pair of Sentinel-2 images acquired on given dates over a given bounding box
27
+ '''
28
+ # GDAL environment variables for better performance
29
+ os .environ ['AWS_REGION' ]= 'us-west-2'
30
+ os .environ ['GDAL_DISABLE_READDIR_ON_OPEN' ]= 'EMPTY_DIR'
31
+ os .environ ['AWS_NO_SIGN_REQUEST' ]= 'YES'
32
+
33
+ # We use the api from element84 to query the data
34
+ URL = "https://earth-search.aws.element84.com/v1"
35
+ catalog = pystac_client .Client .open (URL )
36
+
37
+ search = catalog .search (
38
+ collections = ["sentinel-2-l2a" ],
39
+ intersects = bbox ,
40
+ datetime = img1_date ,
41
+ query = {"eo:cloud_cover" : {"lt" : 10 }}, # less than 10% cloud cover
42
+ )
43
+
44
+ img1_items = search .item_collection ()
45
+ img1_full = stackstac .stack (img1_items )
46
+
47
+ search = catalog .search (
48
+ collections = ["sentinel-2-l2a" ],
49
+ intersects = bbox ,
50
+ datetime = img2_date ,
51
+ query = {"eo:cloud_cover" : {"lt" : 10 }}, # less than 10% cloud cover
52
+ )
53
+
54
+ # Check how many items were returned
55
+ img2_items = search .item_collection ()
56
+ img2_full = stackstac .stack (img2_items )
57
+
58
+ aoi = gpd .GeoDataFrame ({'geometry' :[shape (bbox )]})
59
+ # crop images to aoi
60
+ img1_clipped = img1_full .rio .clip_box (* aoi .total_bounds ,crs = 4326 )
61
+ img2_clipped = img2_full .rio .clip_box (* aoi .total_bounds ,crs = 4326 )
62
+
63
+ img1_ds = img1_clipped .to_dataset (dim = 'band' )
64
+ img2_ds = img2_clipped .to_dataset (dim = 'band' )
65
+
66
+ return img1_ds , img2_ds
67
+
68
+ def run_autoRIFT (img1 , img2 , skip_x = 3 , skip_y = 3 , min_x_chip = 16 , max_x_chip = 64 ,
69
+ preproc_filter_width = 3 , mpflag = 4 , search_limit_x = 30 , search_limit_y = 30 ):
70
+ '''
71
+ Configure and run autoRIFT feature tracking with Sentinel-2 data for large mountain glaciers
72
+ '''
73
+
74
+ obj = autoRIFT ()
75
+ obj .MultiThread = mpflag
76
+
77
+ obj .I1 = img1
78
+ obj .I2 = img2
79
+
80
+ obj .SkipSampleX = skip_x
81
+ obj .SkipSampleY = skip_y
82
+
83
+ # Kernel sizes to use for correlation
84
+ obj .ChipSizeMinX = min_x_chip
85
+ obj .ChipSizeMaxX = max_x_chip
86
+ obj .ChipSize0X = min_x_chip
87
+ # oversample ratio, balancing precision and performance for different chip sizes
88
+ obj .OverSampleRatio = {obj .ChipSize0X :16 , obj .ChipSize0X * 2 :32 , obj .ChipSize0X * 4 :64 }
89
+
90
+ # generate grid
91
+ m ,n = obj .I1 .shape
92
+ xGrid = np .arange (obj .SkipSampleX + 10 ,n - obj .SkipSampleX ,obj .SkipSampleX )
93
+ yGrid = np .arange (obj .SkipSampleY + 10 ,m - obj .SkipSampleY ,obj .SkipSampleY )
94
+ nd = xGrid .__len__ ()
95
+ md = yGrid .__len__ ()
96
+ obj .xGrid = np .int32 (np .dot (np .ones ((md ,1 )),np .reshape (xGrid ,(1 ,xGrid .__len__ ()))))
97
+ obj .yGrid = np .int32 (np .dot (np .reshape (yGrid ,(yGrid .__len__ (),1 )),np .ones ((1 ,nd ))))
98
+ noDataMask = np .invert (np .logical_and (obj .I1 [:, xGrid - 1 ][yGrid - 1 , ] > 0 , obj .I2 [:, xGrid - 1 ][yGrid - 1 , ] > 0 ))
99
+
100
+ # set search limits
101
+ obj .SearchLimitX = np .full_like (obj .xGrid , search_limit_x )
102
+ obj .SearchLimitY = np .full_like (obj .xGrid , search_limit_y )
103
+
104
+ # set search limit and offsets in nodata areas
105
+ obj .SearchLimitX = obj .SearchLimitX * np .logical_not (noDataMask )
106
+ obj .SearchLimitY = obj .SearchLimitY * np .logical_not (noDataMask )
107
+ obj .Dx0 = obj .Dx0 * np .logical_not (noDataMask )
108
+ obj .Dy0 = obj .Dy0 * np .logical_not (noDataMask )
109
+ obj .Dx0 [noDataMask ] = 0
110
+ obj .Dy0 [noDataMask ] = 0
111
+ obj .NoDataMask = noDataMask
112
+
113
+ print ("preprocessing images" )
114
+ obj .WallisFilterWidth = preproc_filter_width
115
+ obj .preprocess_filt_lap () # preprocessing with laplacian filter
116
+ obj .uniform_data_type ()
117
+
118
+ print ("starting autoRIFT" )
119
+ obj .runAutorift ()
120
+ print ("autoRIFT complete" )
121
+
122
+ # convert displacement to m
123
+ obj .Dx_m = obj .Dx * 10
124
+ obj .Dy_m = obj .Dy * 10
125
+
126
+ return obj
127
+
128
+ def prep_outputs (obj , img1_ds , img2_ds ):
129
+ '''
130
+ Interpolate pixel offsets to original dimensions, calculate total horizontal velocity
131
+ '''
132
+
133
+ # interpolate to original dimensions
134
+ x_coords = obj .xGrid [0 , :]
135
+ y_coords = obj .yGrid [:, 0 ]
136
+
137
+ # Create a mesh grid for the img dimensions
138
+ x_coords_new , y_coords_new = np .meshgrid (
139
+ np .arange (obj .I2 .shape [1 ]),
140
+ np .arange (obj .I2 .shape [0 ])
141
+ )
142
+
143
+ # Perform bilinear interpolation using scipy.interpolate.interpn
144
+ Dx_full = interpn ((y_coords , x_coords ), obj .Dx , (y_coords_new , x_coords_new ), method = "linear" , bounds_error = False )
145
+ Dy_full = interpn ((y_coords , x_coords ), obj .Dy , (y_coords_new , x_coords_new ), method = "linear" , bounds_error = False )
146
+ Dx_m_full = interpn ((y_coords , x_coords ), obj .Dx_m , (y_coords_new , x_coords_new ), method = "linear" , bounds_error = False )
147
+ Dy_m_full = interpn ((y_coords , x_coords ), obj .Dy_m , (y_coords_new , x_coords_new ), method = "linear" , bounds_error = False )
148
+
149
+ # add variables to img1 dataset
150
+ img1_ds = img1_ds .assign ({'Dx' :(['y' , 'x' ], Dx_full ),
151
+ 'Dy' :(['y' , 'x' ], Dy_full ),
152
+ 'Dx_m' :(['y' , 'x' ], Dx_m_full ),
153
+ 'Dy_m' :(['y' , 'x' ], Dy_m_full )})
154
+ # calculate x and y velocity
155
+ img1_ds ['veloc_x' ] = (img1_ds .Dx_m / (img2_ds .time .isel (time = 0 ) - img1_ds .time .isel (time = 0 )).dt .days )* 365.25
156
+ img1_ds ['veloc_y' ] = (img1_ds .Dy_m / (img2_ds .time .isel (time = 0 ) - img1_ds .time .isel (time = 0 )).dt .days )* 365.25
157
+
158
+ # calculate total horizontal velocity
159
+ img1_ds ['veloc_horizontal' ] = np .sqrt (img1_ds ['veloc_x' ]** 2 + img1_ds ['veloc_y' ]** 2 )
160
+
161
+ return img1_ds
162
+
163
+ def get_parser ():
164
+ parser = argparse .ArgumentParser (description = "Run autoRIFT to find pixel offsets for two Sentinel-2 images" )
165
+ parser .add_argument ("img1_date" , type = str , help = "date of the first Sentinel-2 image in format YYYY-mm-dd" )
166
+ parser .add_argument ("img2_date" , type = str , help = "date of the second Sentinel-2 image in format YYYY-mm-dd" )
167
+ return parser
168
+
169
+ def main ():
170
+ parser = get_parser ()
171
+ args = parser .parse_args ()
172
+
173
+ # hardcoding a bbox for now
174
+ bbox = {
175
+ "type" : "Polygon" ,
176
+ "coordinates" : [
177
+ [
178
+ [75.42382800808971 ,36.41082887114753 ],
179
+ [75.19442677164156 ,36.41082887114753 ],
180
+ [75.19442677164156 ,36.201076360872946 ],
181
+ [75.42382800808971 ,36.201076360872946 ],
182
+ [75.42382800808971 ,36.41082887114753 ]
183
+ ]
184
+ ],
185
+ }
186
+
187
+ # download Sentinel-2 images
188
+ img1_ds , img2_ds = download_s2 (args .img1_date , args .img2_date , bbox )
189
+ # grab near infrared band only
190
+ img1 = img1_ds .nir .squeeze ().values
191
+ img2 = img2_ds .nir .squeeze ().values
192
+
193
+ # scale search limit with temporal baseline assuming max velocity 1000 m/yr (100 px/yr)
194
+ search_limit_x = search_limit_y = round (((((img2_ds .time .isel (time = 0 ) - img1_ds .time .isel (time = 0 )).dt .days )* 100 )/ 365.25 ).item ())
195
+
196
+ # run autoRIFT feature tracking
197
+ obj = run_autoRIFT (img1 , img2 , search_limit_x = search_limit_x , search_limit_y = search_limit_y )
198
+ # postprocess offsets
199
+ ds = prep_outputs (obj , img1_ds , img2_ds )
200
+
201
+ # write out velocity to tif
202
+ os .makedirs ('glacier_velocity' , exist_ok = True )
203
+ ds .veloc_horizontal .rio .to_raster (f'./glacier_velocity/S2_{ args .img1_date } _{ args .img2_date } _horizontal_velocity.tif' )
204
+
205
+ if __name__ == "__main__" :
206
+ main ()
0 commit comments