obichimav commited on
Commit
da66e75
·
verified ·
1 Parent(s): a2be35b

Upload 3 files

Browse files
Files changed (3) hide show
  1. chmloader.py +287 -0
  2. output/chm.tif +0 -0
  3. output/clipped_chm.tif +0 -0
chmloader.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import geopandas as gpd
3
+ from osgeo import gdal
4
+ import rasterio
5
+ from rasterio.mask import mask
6
+ import numpy as np
7
+ from rasterio.warp import calculate_default_transform, reproject, Resampling
8
+ from pyproj import CRS
9
+
10
+ def build_chm_srcs(target, tiles_zip='tiles/tiles.zip', local_chm_dir='chm_tiles'):
11
+ """
12
+ Get the S3 paths to the CHM tiles that cover a given target area. Download missing tiles from S3 if necessary.
13
+
14
+ Args:
15
+ target: GeoDataFrame or GeoSeries representing the target area.
16
+ tiles_zip: Path to the zip file containing the tiles.
17
+ local_chm_dir: Directory where CHM tiles are stored locally.
18
+
19
+ Returns:
20
+ list: A list of paths to the CHM tiles that cover the target area.
21
+ """
22
+ # Load the tiles from the zip file
23
+ gtiles = gpd.read_file(f'/vsizip/{tiles_zip}')
24
+
25
+ # Transform the target area to match the tiles' CRS
26
+ t_ext = target.to_crs(gtiles.crs).total_bounds
27
+
28
+ # Filter the tiles that intersect with the target area
29
+ filtered_tiles = gtiles.cx[t_ext[0]:t_ext[2], t_ext[1]:t_ext[3]]
30
+
31
+ # Construct paths to the CHM files using GDAL's /vsis3/ to access S3 without credentials
32
+ s3_paths = [
33
+ f'/vsis3/dataforgood-fb-data/forests/v1/alsgedi_global_v6_float/chm/{tile}.tif' for tile in filtered_tiles['tile']
34
+ ]
35
+
36
+ return s3_paths
37
+
38
+ def warp_util(srcs, target, res, filename, gdalwarp_options, gdal_config_options):
39
+ """
40
+ A gdalwarp utility function.
41
+
42
+ Args:
43
+ srcs (list): The source files to warp.
44
+ target: GeoDataFrame or GeoSeries representing the target area to warp to.
45
+ res (float or tuple): The resolution of the target area.
46
+ filename (str): The filename to save the warped data to.
47
+ gdalwarp_options (list): Options to pass to gdalwarp.
48
+ gdal_config_options (dict): GDAL configuration options to set before running gdalwarp.
49
+
50
+ Returns:
51
+ str: The path to the warped file.
52
+ """
53
+ # Set GDAL configuration options
54
+ for key, value in gdal_config_options.items():
55
+ gdal.SetConfigOption(key, value)
56
+
57
+ # Calculate resolution if not provided
58
+ if res is None:
59
+ bbox = target.total_bounds
60
+ xmin, ymin, xmax, ymax = bbox
61
+ res_x = 1.0
62
+ res_y = 1.0
63
+ dim_x = int((xmax - xmin) / res_x)
64
+ dim_y = int((ymax - ymin) / res_y)
65
+ else:
66
+ if isinstance(res, (int, float)):
67
+ res_x = res
68
+ res_y = res
69
+ elif isinstance(res, (tuple, list)) and len(res) == 2:
70
+ res_x, res_y = res
71
+ else:
72
+ raise ValueError("Resolution must be a single value or a tuple of two values.")
73
+ bbox = target.total_bounds
74
+ xmin, ymin, xmax, ymax = bbox
75
+ dim_x = int((xmax - xmin) / res_x)
76
+ dim_y = int((ymax - ymin) / res_y)
77
+
78
+ te_srs = target.crs.to_string()
79
+
80
+ # Add translation extent, target SRS, and output dimensions to gdalwarp options
81
+ gdalwarp_options += [
82
+ "-te", str(xmin), str(ymin), str(xmax), str(ymax),
83
+ "-t_srs", te_srs,
84
+ "-ts", str(dim_x), str(dim_y)
85
+ ]
86
+
87
+ # Perform the warp operation
88
+ gdal.Warp(
89
+ filename, srcs, options=gdalwarp_options
90
+ )
91
+
92
+ # Reset GDAL configuration options to their defaults after the operation
93
+ for key in gdal_config_options.keys():
94
+ gdal.SetConfigOption(key, None)
95
+
96
+ return filename
97
+
98
+ def download_chm(target, chm="tolan", res=None, filename="output_chm.tif", gdalwarp_options=None, gdal_config_options=None):
99
+ """
100
+ Download Canopy Height Model (CHM) data by mosaicking relevant CHM tiles.
101
+
102
+ Args:
103
+ target: GeoDataFrame or GeoSeries representing the target area.
104
+ chm (str): The CHM dataset to download (currently only 'tolan').
105
+ res (float): The resolution of the CHM data to download in meters.
106
+ filename (str): The filename to save the downloaded CHM data to.
107
+ gdalwarp_options (list): Options to pass to gdalwarp.
108
+ gdal_config_options (dict): GDAL configuration options to set before running gdalwarp.
109
+
110
+ Returns:
111
+ str: The path to the downloaded CHM data.
112
+ """
113
+ print("downloading CHM")
114
+ if gdalwarp_options is None:
115
+ gdalwarp_options = ["-r", "bilinear", "-overwrite"]
116
+ if gdal_config_options is None:
117
+ gdal_config_options = {
118
+ "GDAL_CACHEMAX": "512",
119
+ "GDAL_HTTP_MULTIPLEX": "YES",
120
+ "VSI_CACHE": "TRUE",
121
+ "AWS_NO_SIGN_REQUEST": "YES"
122
+ }
123
+
124
+ # Generate the paths to the source CHM tiles
125
+ srcs = build_chm_srcs(target)
126
+
127
+ # Warp and mosaic the CHM tiles into a single output file
128
+ chm_raster_path = warp_util(srcs, target, res, filename, gdalwarp_options, gdal_config_options)
129
+
130
+ return chm_raster_path
131
+
132
+
133
+ # Ensure the NoData Handling is Correct
134
+ # def clip_and_remove_zeros(tif_path, boundary, output_tif_path=None, nodata_value=None, save=False):
135
+ # with rasterio.open(tif_path) as src:
136
+ # out_image, out_transform = mask(src, boundary.geometry, crop=True)
137
+ # profile = src.profile.copy()
138
+
139
+ # # Determine the NoData value
140
+ # if nodata_value is None:
141
+ # if src.nodata is not None:
142
+ # nodata_value = src.nodata
143
+ # else:
144
+ # nodata_value = 0 # Set default NoData value
145
+
146
+ # # Mask out NoData values
147
+ # out_image = out_image.squeeze()
148
+ # out_image = np.ma.masked_equal(out_image, nodata_value)
149
+
150
+ # # Update profile for saving, if needed
151
+ # profile.update({
152
+ # 'dtype': src.dtypes[0],
153
+ # 'count': 1,
154
+ # 'nodata': nodata_value,
155
+ # 'compress': 'deflate'
156
+ # })
157
+
158
+ # if save and output_tif_path:
159
+ # with rasterio.open(output_tif_path, 'w', **profile) as dst:
160
+ # dst.write(out_image, 1)
161
+
162
+ # return out_image, out_transform, profile
163
+
164
+
165
+ def reproject_to_utm(tif_path):
166
+ """Reproject the raster file to UTM and save it with the same name."""
167
+ with rasterio.open(tif_path) as src:
168
+ # Get the CRS of the file
169
+ crs = src.crs
170
+ print(f"Original CRS of the TIFF file: {crs}")
171
+
172
+ if crs.is_geographic:
173
+ # Calculate the center point of the raster
174
+ center_lon, center_lat = (src.bounds.left + src.bounds.right) / 2, (src.bounds.bottom + src.bounds.top) / 2
175
+ print(f"Center of the raster: Longitude={center_lon}, Latitude={center_lat}")
176
+
177
+ # Determine UTM zone
178
+ utm_zone = int((center_lon + 180) // 6) + 1
179
+ hemisphere = 'north' if center_lat >= 0 else 'south'
180
+
181
+ # Define the UTM CRS
182
+ if hemisphere == 'north':
183
+ epsg_code = 32600 + utm_zone # UTM zone for Northern Hemisphere
184
+ else:
185
+ epsg_code = 32700 + utm_zone # UTM zone for Southern Hemisphere
186
+
187
+ utm_crs = CRS.from_epsg(epsg_code)
188
+ print(f"Determined UTM CRS: EPSG:{epsg_code}")
189
+
190
+ elif crs.is_projected:
191
+ # If CRS is already projected, attempt to retrieve its EPSG code
192
+ try:
193
+ epsg_code = crs.to_epsg() # Extract EPSG code
194
+ if epsg_code is not None:
195
+ print(f"The CRS is already projected. EPSG code: {epsg_code}")
196
+ utm_crs = CRS.from_epsg(epsg_code)
197
+ else:
198
+ # Fallback to using the WKT representation if EPSG is None
199
+ utm_crs = crs
200
+ print(f"The CRS is projected but has no EPSG code. Using WKT: {crs.to_wkt()}")
201
+ except Exception as e:
202
+ print(f"Unable to determine EPSG code or WKT: {e}")
203
+ utm_crs = crs
204
+ else:
205
+ print("The CRS type could not be determined or is not geographic.")
206
+ return tif_path # No reprojection needed, return the original path
207
+
208
+ # Prepare output path (same as input to overwrite)
209
+ output_path = tif_path
210
+
211
+ # Calculate the transform and dimensions for the reprojected image
212
+ transform, width, height = calculate_default_transform(
213
+ src.crs, utm_crs, src.width, src.height, *src.bounds
214
+ )
215
+
216
+ # Define the output profile for the new UTM raster
217
+ profile = src.profile.copy()
218
+ profile.update({
219
+ 'crs': utm_crs,
220
+ 'transform': transform,
221
+ 'width': width,
222
+ 'height': height
223
+ })
224
+
225
+ # Reproject the raster to UTM
226
+ with rasterio.open(output_path, 'w', **profile) as dst:
227
+ for i in range(1, src.count + 1):
228
+ reproject(
229
+ source=rasterio.band(src, i),
230
+ destination=rasterio.band(dst, i),
231
+ src_transform=src.transform,
232
+ src_crs=src.crs,
233
+ dst_transform=transform,
234
+ dst_crs=utm_crs,
235
+ resampling=Resampling.nearest
236
+ )
237
+
238
+ print(f"Reprojected file saved as: {output_path}")
239
+ return output_path # Return the path of the reprojected file
240
+
241
+
242
+ def clip_and_remove_zeros(tif_path, boundary, output_tif_path=None, nodata_value=None, save=False):
243
+ # Reproject the raster to UTM if necessary
244
+ reprojected_path = reproject_to_utm(tif_path)
245
+
246
+ # Open the reprojected raster file
247
+ with rasterio.open(reprojected_path) as src:
248
+ # Check if the CRS is UTM
249
+ if src.crs.is_projected and src.crs.to_epsg() and (32601 <= src.crs.to_epsg() <= 32660 or 32701 <= src.crs.to_epsg() <= 32760):
250
+ print(f"The raster is in UTM CRS: EPSG:{src.crs.to_epsg()}")
251
+ else:
252
+ print("Warning: The raster is not in a UTM CRS or its CRS could not be determined to be UTM.")
253
+
254
+ # Ensure the boundary is in the same CRS as the raster
255
+ if boundary.crs != src.crs:
256
+ print(f"Reprojecting boundary to match raster CRS: {src.crs}")
257
+ boundary = boundary.to_crs(src.crs)
258
+
259
+ # Clip the raster using the boundary geometry
260
+ out_image, out_transform = mask(src, boundary.geometry, crop=True)
261
+ profile = src.profile.copy()
262
+
263
+ # Determine the NoData value
264
+ if nodata_value is None:
265
+ if src.nodata is not None:
266
+ nodata_value = src.nodata
267
+ else:
268
+ nodata_value = 0 # Set default NoData value
269
+
270
+ # Mask out NoData values
271
+ out_image = out_image.squeeze()
272
+ out_image = np.ma.masked_equal(out_image, nodata_value)
273
+
274
+ # Update profile for saving, if needed
275
+ profile.update({
276
+ 'dtype': src.dtypes[0],
277
+ 'count': 1,
278
+ 'nodata': nodata_value,
279
+ 'compress': 'deflate'
280
+ })
281
+
282
+ if save and output_tif_path:
283
+ with rasterio.open(output_tif_path, 'w', **profile) as dst:
284
+ dst.write(out_image, 1)
285
+
286
+ return out_image, out_transform, profile
287
+
output/chm.tif ADDED
output/clipped_chm.tif ADDED