Skip to content

Commit 75018fe

Browse files
committed
adding script for image correlation workflow
1 parent aed21b6 commit 75018fe

File tree

3 files changed

+1745
-0
lines changed

3 files changed

+1745
-0
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: image-correlation
2+
channels:
3+
- conda-forge
4+
- defaults
5+
dependencies:
6+
- rioxarray
7+
- geopandas
8+
- matplotlib
9+
- ipykernel
10+
- seaborn
11+
- netcdf4
12+
- autorift
13+
- pystac-client
14+
- stackstac
15+
- dask
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
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

Comments
 (0)