fire2a.raster
👋🌎 🌲🔥 This is the raster module docstring
List all gdal available drivers: $ python -c "from osgeo import gdal;print(' '.join(sorted([gdal.GetDriver(i).GetDescription() for i in range(gdal.GetDriverCount())])))"
1#!python3 2"""👋🌎 🌲🔥 3This is the raster module docstring 4 5List all gdal available drivers: 6$ python -c "from osgeo import gdal;print('\n'.join(sorted([gdal.GetDriver(i).GetDescription() for i in range(gdal.GetDriverCount())])))" 7""" 8__author__ = "Fernando Badilla" 9__revision__ = "$Format:%H$" 10 11import logging 12from pathlib import Path 13from typing import Any, Dict, List, Optional, Tuple, Union 14 15import numpy as np 16from osgeo import gdal, ogr 17from qgis.core import QgsRasterLayer 18 19from .utils import fprint, qgis2numpy_dtype 20 21logger = logging.getLogger(__name__) 22 23 24def id2xy(idx: int, w: int, h: int) -> tuple[int, int]: 25 """Transform a pixel or cell index, into x,y coordinates. 26 In GIS, the origin is at the top-left corner, read from left to right, top to bottom. 27 If your're used to matplotlib, the y-axis is inverted. 28 Also as numpy array, the index of the pixel is [y, x]. 29 30 Args: 31 param idx: index of the pixel or cell (0,..,w*h-1) 32 param w: width of the image or grid 33 param h: height of the image or grid (not really used!) 34 35 Returns: 36 tuple: (x, y) coordinates of the pixel or cell 37 """ # fmt: skip 38 return idx % w, idx // w 39 40 41def xy2id(x: int, y: int, w: int) -> int: 42 """Transform a x,y coordinates into a pixel or cell index. 43 In GIS, the origin is at the top-left corner, read from left to right, top to bottom. 44 If your're used to matplotlib, the y-axis is inverted. 45 Also as numpy array, the index of the pixel is [y, x]. 46 47 Args: 48 param x: width or horizontal coordinate of the pixel or cell 49 param y: height or vertical coordinate of the pixel or cell 50 param w: width of the image or grid 51 52 Returns: 53 int: index of the pixel or cell (0,..,w\*h-1) 54 """ # fmt: skip 55 return y * w + x 56 57 58def read_raster_band(filename: str, band: int = 1) -> tuple[np.ndarray, int, int]: 59 """Read a raster file and return the data as a numpy array, along width and height. 60 61 Args: 62 param filename: name of the raster file 63 param band: band number to read (default 1) 64 65 Returns: 66 tuple: (data, width, height) 67 68 Raises: 69 FileNotFoundError: if the file is not found 70 """ # fmt: skip 71 dataset = gdal.Open(filename, gdal.GA_ReadOnly) 72 if dataset is None: 73 raise FileNotFoundError(filename) 74 return dataset.GetRasterBand(band).ReadAsArray(), dataset.RasterXSize, dataset.RasterYSize 75 76 77def read_raster( 78 filename: str, band: int = 1, data: bool = True, info: bool = True 79) -> tuple[Union[np.ndarray, None], Union[dict, None]]: 80 """Read a raster file and return the data as a numpy array. 81 Along raster info: transform, projection, raster count, raster width, raster height. 82 83 Args: 84 param filename: name of the raster file 85 param band: band number to read (default 1) 86 param data: if True, return the data as a numpy array (default True) 87 param info: if True, return the raster info (default True) 88 89 Return tuple: (data, info) 90 data: numpy 2d array with the raster data 91 info: dictionary with keys: 92 - Transform: geotransform parameters 93 - Projection: projection string 94 - RasterCount: number of bands 95 - RasterXSize: width of the raster 96 - RasterYSize: height of the raster 97 - DataType: data type of the raster 98 - NoDataValue: no data value of the raster 99 - Minimum: minimum value of the raster 100 - Maximum: maximum value of the raster 101 102 Raises: 103 FileNotFoundError: if the file is not found 104 """ # fmt: skip 105 dataset = gdal.Open(filename, gdal.GA_ReadOnly) 106 if dataset is None: 107 raise FileNotFoundError(filename) 108 raster_band = dataset.GetRasterBand(band) 109 data_output = raster_band.ReadAsArray() if data else None 110 111 if info: 112 rmin = raster_band.GetMinimum() 113 rmax = raster_band.GetMaximum() 114 if not rmin or not rmax: 115 (rmin, rmax) = raster_band.ComputeRasterMinMax(True) 116 117 info_output = ( 118 { 119 "Transform": dataset.GetGeoTransform(), 120 "Projection": dataset.GetProjection(), 121 "RasterCount": dataset.RasterCount, 122 "RasterXSize": dataset.RasterXSize, 123 "RasterYSize": dataset.RasterYSize, 124 "DataType": gdal.GetDataTypeName(raster_band.DataType), 125 "NoDataValue": raster_band.GetNoDataValue(), 126 "Minimum": rmin, 127 "Maximum": rmax, 128 } 129 if info 130 else None 131 ) 132 return data_output, info_output 133 134 135def get_geotransform(raster_filename: str) -> tuple[float, float, float, float, float, float]: 136 """ Get geotransform from raster file. 137 Args: 138 raster_filename (str): 139 140 Returns: 141 tuple: geotransform 142 GT[0] x-coordinate of the upper-left corner of the upper-left pixel. 143 GT[1] w-e pixel resolution / pixel width. 144 GT[2] row rotation (typically zero). 145 GT[3] y-coordinate of the upper-left corner of the upper-left pixel. 146 GT[4] column rotation (typically zero). 147 GT[5] n-s pixel resolution / pixel height (negative value for a north-up image). 148 149 reference: https://gdal.org/tutorials/geotransforms_tut.html 150 """ # fmt: skip 151 dataset = gdal.Open(raster_filename, gdal.GA_ReadOnly) 152 if dataset is None: 153 raise Exception(f"Data set is None, could not open {raster_filename}") 154 return dataset.GetGeoTransform() 155 156 157def transform_coords_to_georef(x_pixel: int, y_line: int, GT: tuple) -> tuple[float, float]: 158 """ Transform pixel coordinates to georeferenced coordinates. 159 Args: 160 x_pixel (int): x pixel coordinate. 161 y_line (int): y pixel coordinate. 162 GT (tuple): geotransform, see get_geotransform(filename) 163 164 Returns: 165 tuple: x_geo, y_geo. 166 167 reference: https://gdal.org/tutorials/geotransforms_tut.html 168 """ # fmt: skip 169 x_geo = GT[0] + x_pixel * GT[1] + y_line * GT[2] 170 y_geo = GT[3] + x_pixel * GT[4] + y_line * GT[5] 171 return x_geo, y_geo 172 173 174def transform_georef_to_coords(x_geo: int, y_geo: int, GT: tuple) -> tuple[float, float]: 175 """Inverse of transform_coords_to_georef. 176 177 import sympy 178 a, b, c, d, e, f, g, i, j, x, y = sympy.symbols('a, b, c, d, e, f, g, i, j, x, y', real=True) 179 sympy.linsolve([a+i*b+j*c - x,d+i*e+j*f-y],(i,j)) 180 {((-a*f + c*d - c*y + f*x)/(b*f - c*e), (a*e - b*d + b*y - e*x)/(b*f - c*e))} 181 182 Args: 183 x_geo (int): x georeferenced coordinate. 184 y_geo (int): y georeferenced coordinate. 185 GT (tuple): geotransform, see get_geotransform(filename) 186 187 Returns: 188 tuple: x_pixel, y_line. 189 190 TODO Raises: 191 Exception: if x_pixel or y_line are not integer coordinates. by tolerance? 192 193 reference: https://gdal.org/tutorials/geotransforms_tut.html 194 """ 195 a, b, c, d, e, f = GT 196 x, y = x_geo, y_geo 197 i, j = (-a * f + c * d - c * y + f * x) / (b * f - c * e), (a * e - b * d + b * y - e * x) / (b * f - c * e) 198 # if i % 1 != 0 or j % 1 != 0: 199 # raise Exception("Not integer coordinates!") 200 return int(i), int(j) 201 202 203def get_rlayer_info(layer: QgsRasterLayer): 204 """Get raster layer info: width, height, extent, crs, cellsize_x, cellsize_y, nodata list, number of bands. 205 206 Args: 207 layer (QgsRasterLayer): A raster layer 208 Returns: 209 dict: raster layer info 210 """ 211 provider = layer.dataProvider() 212 ndv = [] 213 for band in range(1, layer.bandCount() + 1): 214 ndv += [None] 215 if provider.sourceHasNoDataValue(band): 216 ndv[-1] = provider.sourceNoDataValue(band) 217 return { 218 "width": layer.width(), 219 "height": layer.height(), 220 "extent": layer.extent(), 221 "crs": layer.crs(), 222 "cellsize_x": layer.rasterUnitsPerPixelX(), 223 "cellsize_y": layer.rasterUnitsPerPixelY(), 224 "nodata": ndv, 225 "bands": layer.bandCount(), 226 "file": layer.publicSource(), 227 } 228 229 230def get_rlayer_data(layer: QgsRasterLayer): 231 """Get raster layer data (EVERY BAND) as numpy array; Also returns nodata value, width and height 232 The user should check the shape of the data to determine if it is a single band or multiband raster. 233 len(data.shape) == 2 for single band, len(data.shape) == 3 for multiband. 234 235 Args: 236 layer (QgsRasterLayer): A raster layer 237 238 Returns: 239 data (np.array): Raster data as numpy array 240 nodata (None | list): No data value 241 width (int): Raster width 242 height (int): Raster height 243 244 FIXME? can a multiband raster have different nodata values and/or data types for each band? 245 TODO: make a band list as input 246 """ 247 provider = layer.dataProvider() 248 if layer.bandCount() == 1: 249 block = provider.block(1, layer.extent(), layer.width(), layer.height()) 250 nodata = None 251 if block.hasNoDataValue(): 252 nodata = block.noDataValue() 253 np_dtype = qgis2numpy_dtype(provider.dataType(1)) 254 data = np.frombuffer(block.data(), dtype=np_dtype).reshape(layer.height(), layer.width()) 255 # return data, nodata, np_dtype 256 else: 257 data = [] 258 nodata = [] 259 np_dtypel = [] 260 for i in range(layer.bandCount()): 261 block = provider.block(i + 1, layer.extent(), layer.width(), layer.height()) 262 nodata += [None] 263 if block.hasNoDataValue(): 264 nodata[-1] = block.noDataValue() 265 np_dtypel += [qgis2numpy_dtype(provider.dataType(i + 1))] 266 data += [np.frombuffer(block.data(), dtype=np_dtypel[-1]).reshape(layer.height(), layer.width())] 267 # would different data types bug this next line? 268 data = np.array(data) 269 # return data, nodata, np_dtypl 270 return data 271 272 273def get_cell_sizeV2(filename: str, band: int = 1) -> tuple[float, float]: 274 # TODO: deprecate this function 275 _, info = read_raster(filename, band=band, data=False, info=True) 276 return info["RasterXSize"], info["RasterYSize"] 277 278 279def get_cell_size(raster: gdal.Dataset) -> tuple[float, float]: 280 """Get the cell size(s) of a raster. 281 PLANNED DEPRECATION 282 283 Args: 284 raster (gdal.Dataset | str): The GDAL dataset or path to the raster. 285 286 Returns: 287 float | tuple[float, float]: The cell size(s) as a single float or a tuple (x, y). 288 """ # fmt: skip 289 if isinstance(raster, str): 290 ds = gdal.Open(raster, gdal.GA_ReadOnly) 291 elif isinstance(raster, gdal.Dataset): 292 ds = raster 293 else: 294 raise ValueError("Invalid input type for raster") 295 296 # Get the affine transformation parameters 297 affine = ds.GetGeoTransform() 298 299 if affine[1] != -affine[5]: 300 # If x and y cell sizes are not equal 301 cell_size = (affine[1], -affine[5]) # Return as a tuple 302 else: 303 cell_size = affine[1] # Return as a single float 304 305 return cell_size 306 307 308def mask_raster(raster_ds: gdal.Dataset, band: int, polygons: list[ogr.Geometry]) -> np.ndarray: 309 """Mask a raster with polygons using GDAL. 310 311 Args: 312 raster_ds (gdal.Dataset): GDAL dataset of the raster. 313 band (int): Band index of the raster. 314 polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for masking. 315 316 Returns: 317 np.array: Masked raster data as a NumPy array. 318 """ # fmt: skip 319 320 # Get the mask as a NumPy boolean array 321 mask_array = rasterize_polygons(polygons, raster_ds.RasterXSize, raster_ds.RasterYSize) 322 323 # Read the original raster data 324 original_data = band.ReadAsArray() # FIXME: wrong type hint : int has no attribute ReadAsArray 325 326 # Apply the mask 327 masked_data = np.where(mask_array, original_data, np.nan) 328 329 return masked_data 330 331 332def rasterize_polygons(polygons: list[ogr.Geometry], width: int, height: int) -> np.ndarray: 333 """Rasterize polygons to a boolean array. 334 335 Args: 336 polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for rasterization. 337 geo_transform (tuple): GeoTransform parameters for the raster. 338 width (int): Width of the raster. 339 height (int): Height of the raster. 340 341 Returns: 342 mask_array (np.array): Rasterized mask as a boolean array. 343 """ # fmt: skip 344 345 mask_array = np.zeros((height, width), dtype=bool) 346 347 # Create an in-memory layer to hold the polygons 348 mem_driver = ogr.GetDriverByName("Memory") 349 mem_ds = mem_driver.CreateDataSource("memData") 350 mem_layer = mem_ds.CreateLayer("memLayer", srs=None, geom_type=ogr.wkbPolygon) 351 352 for geometry in polygons: 353 mem_feature = ogr.Feature(mem_layer.GetLayerDefn()) 354 mem_feature.SetGeometry(geometry.Clone()) 355 mem_layer.CreateFeature(mem_feature) 356 357 # Rasterize the in-memory layer and update the mask array 358 gdal.RasterizeLayer(mask_array, [1], mem_layer, burn_values=[1]) 359 360 mem_ds = None # Release the in-memory dataset 361 362 return mask_array 363 364 365def stack_rasters( 366 file_list: list[Path], mask_polygon: Union[list[ogr.Geometry], None] = None 367) -> tuple[np.ndarray, list[str]]: 368 """Stack raster files from a list into a 3D NumPy array. 369 370 Args: 371 file_list (list[Path]): List of paths to raster files. 372 mask_polygon (list[ogr.Geometry], optional): List of OGR geometries for masking. Defaults to None. 373 374 Returns: 375 np.array: Stacked raster array. 376 list: List of layer names corresponding to the rasters. 377 """ # fmt: skip 378 array_list = [] 379 cell_sizes = set() 380 layer_names = [] 381 382 for raster_path in file_list: 383 layer_name = raster_path.stem 384 layer_names.append(layer_name) 385 386 ds = gdal.Open(str(raster_path)) 387 if ds is None: 388 raise ValueError(f"Failed to open raster file: {raster_path}") 389 390 band = ds.GetRasterBand(1) 391 392 if mask_polygon: 393 flatten_array = mask_raster(ds, band, mask_polygon) 394 else: 395 flatten_array = band.ReadAsArray() 396 397 array_list.append(flatten_array) 398 cell_sizes.add(get_cell_size(ds)) 399 400 assert len(cell_sizes) == 1, f"There are rasters with different cell sizes: {cell_sizes}" 401 stacked_array = np.stack(array_list, axis=0) # type: np.array 402 print(stacked_array.shape) 403 return stacked_array, layer_names 404 405 406def write_raster( 407 data, 408 outfile="output.tif", 409 driver_name="GTiff", 410 authid="EPSG:3857", 411 geotransform=(0, 1, 0, 0, 0, -1), 412 nodata: int | None = None, 413 feedback=None, 414 logger=None, # logger default ? 415): 416 """Write a raster file from a numpy array. 417 418 To spatially match another raster, get authid and geotransform using: 419 from fire2a.raster import read_raster 420 _,info = read_raster(filename, data=False, info=True). 421 authid = info["Transform"] 422 geotransform = info["Projection"]. 423 424 Args: 425 data (np.array): numpy array to write as raster 426 outfile (str, optional): output raster filename. Defaults to "output.tif". 427 driver_name (str, optional): GDAL driver name. Defaults to "GTiff". 428 authid (str, optional): EPSG code. Defaults to "EPSG:3857". 429 geotransform (tuple, optional): geotransform parameters. Defaults to (0, 1, 0, 0, 0, 1). 430 feedback (Optional, optional): qgsprocessing.feedback object. Defaults to None. 431 logger ([type], optional): logging.logger object. Defaults to None. 432 Returns: 433 bool: True if the raster was written successfully, False otherwise. 434 """ 435 436 try: 437 from fire2a.processing_utils import get_output_raster_format 438 439 driver_name = get_output_raster_format(outfile, feedback=feedback) 440 except Exception as e: 441 fprint( 442 f"Couln't get output raster format: {e}, defaulting to GTiff", 443 level="warning", 444 feedback=feedback, 445 logger=logger, 446 ) 447 driver_name = "GTiff" 448 H, W = data.shape 449 ds = gdal.GetDriverByName(driver_name).Create(outfile, W, H, 1, gdal.GDT_Float32) 450 ds.SetGeoTransform(geotransform) 451 ds.SetProjection(authid) 452 band = ds.GetRasterBand(1) 453 if 0 != band.WriteArray(data): 454 fprint("WriteArray failed", level="warning", feedback=feedback, logger=logger) 455 return False 456 if nodata and data[data == nodata].size > 0: 457 band.SetNoDataValue(nodata) 458 # TBD : always returns 1? 459 # if 0 != band.SetNoDataValue(nodata): 460 # fprint("Set NoData failed", level="warning", feedback=feedback, logger=logger) 461 # return False 462 ds.FlushCache() 463 ds = None 464 return True 465 466 467if __name__ == "__main__": 468 file_list = list(Path().cwd().glob("*.asc")) 469 print(file_list) 470 array = stack_rasters(file_list) 471 print(array)
25def id2xy(idx: int, w: int, h: int) -> tuple[int, int]: 26 """Transform a pixel or cell index, into x,y coordinates. 27 In GIS, the origin is at the top-left corner, read from left to right, top to bottom. 28 If your're used to matplotlib, the y-axis is inverted. 29 Also as numpy array, the index of the pixel is [y, x]. 30 31 Args: 32 param idx: index of the pixel or cell (0,..,w*h-1) 33 param w: width of the image or grid 34 param h: height of the image or grid (not really used!) 35 36 Returns: 37 tuple: (x, y) coordinates of the pixel or cell 38 """ # fmt: skip 39 return idx % w, idx // w
Transform a pixel or cell index, into x,y coordinates.
In GIS, the origin is at the top-left corner, read from left to right, top to bottom.
If your're used to matplotlib, the y-axis is inverted.
Also as numpy array, the index of the pixel is [y, x].
Args:
param idx: index of the pixel or cell (0,..,w*h-1)
param w: width of the image or grid
param h: height of the image or grid (not really used!)
Returns: tuple: (x, y) coordinates of the pixel or cell
42def xy2id(x: int, y: int, w: int) -> int: 43 """Transform a x,y coordinates into a pixel or cell index. 44 In GIS, the origin is at the top-left corner, read from left to right, top to bottom. 45 If your're used to matplotlib, the y-axis is inverted. 46 Also as numpy array, the index of the pixel is [y, x]. 47 48 Args: 49 param x: width or horizontal coordinate of the pixel or cell 50 param y: height or vertical coordinate of the pixel or cell 51 param w: width of the image or grid 52 53 Returns: 54 int: index of the pixel or cell (0,..,w\*h-1) 55 """ # fmt: skip 56 return y * w + x
Transform a x,y coordinates into a pixel or cell index.
In GIS, the origin is at the top-left corner, read from left to right, top to bottom.
If your're used to matplotlib, the y-axis is inverted.
Also as numpy array, the index of the pixel is [y, x].
Args:
param x: width or horizontal coordinate of the pixel or cell
param y: height or vertical coordinate of the pixel or cell
param w: width of the image or grid
Returns: int: index of the pixel or cell (0,..,w*h-1)
59def read_raster_band(filename: str, band: int = 1) -> tuple[np.ndarray, int, int]: 60 """Read a raster file and return the data as a numpy array, along width and height. 61 62 Args: 63 param filename: name of the raster file 64 param band: band number to read (default 1) 65 66 Returns: 67 tuple: (data, width, height) 68 69 Raises: 70 FileNotFoundError: if the file is not found 71 """ # fmt: skip 72 dataset = gdal.Open(filename, gdal.GA_ReadOnly) 73 if dataset is None: 74 raise FileNotFoundError(filename) 75 return dataset.GetRasterBand(band).ReadAsArray(), dataset.RasterXSize, dataset.RasterYSize
Read a raster file and return the data as a numpy array, along width and height.
Args:
param filename: name of the raster file
param band: band number to read (default 1)
Returns: tuple: (data, width, height)
Raises: FileNotFoundError: if the file is not found
78def read_raster( 79 filename: str, band: int = 1, data: bool = True, info: bool = True 80) -> tuple[Union[np.ndarray, None], Union[dict, None]]: 81 """Read a raster file and return the data as a numpy array. 82 Along raster info: transform, projection, raster count, raster width, raster height. 83 84 Args: 85 param filename: name of the raster file 86 param band: band number to read (default 1) 87 param data: if True, return the data as a numpy array (default True) 88 param info: if True, return the raster info (default True) 89 90 Return tuple: (data, info) 91 data: numpy 2d array with the raster data 92 info: dictionary with keys: 93 - Transform: geotransform parameters 94 - Projection: projection string 95 - RasterCount: number of bands 96 - RasterXSize: width of the raster 97 - RasterYSize: height of the raster 98 - DataType: data type of the raster 99 - NoDataValue: no data value of the raster 100 - Minimum: minimum value of the raster 101 - Maximum: maximum value of the raster 102 103 Raises: 104 FileNotFoundError: if the file is not found 105 """ # fmt: skip 106 dataset = gdal.Open(filename, gdal.GA_ReadOnly) 107 if dataset is None: 108 raise FileNotFoundError(filename) 109 raster_band = dataset.GetRasterBand(band) 110 data_output = raster_band.ReadAsArray() if data else None 111 112 if info: 113 rmin = raster_band.GetMinimum() 114 rmax = raster_band.GetMaximum() 115 if not rmin or not rmax: 116 (rmin, rmax) = raster_band.ComputeRasterMinMax(True) 117 118 info_output = ( 119 { 120 "Transform": dataset.GetGeoTransform(), 121 "Projection": dataset.GetProjection(), 122 "RasterCount": dataset.RasterCount, 123 "RasterXSize": dataset.RasterXSize, 124 "RasterYSize": dataset.RasterYSize, 125 "DataType": gdal.GetDataTypeName(raster_band.DataType), 126 "NoDataValue": raster_band.GetNoDataValue(), 127 "Minimum": rmin, 128 "Maximum": rmax, 129 } 130 if info 131 else None 132 ) 133 return data_output, info_output
Read a raster file and return the data as a numpy array. Along raster info: transform, projection, raster count, raster width, raster height.
Args: param filename: name of the raster file param band: band number to read (default 1) param data: if True, return the data as a numpy array (default True) param info: if True, return the raster info (default True)
Return tuple: (data, info) data: numpy 2d array with the raster data info: dictionary with keys: - Transform: geotransform parameters - Projection: projection string - RasterCount: number of bands - RasterXSize: width of the raster - RasterYSize: height of the raster - DataType: data type of the raster - NoDataValue: no data value of the raster - Minimum: minimum value of the raster - Maximum: maximum value of the raster
Raises: FileNotFoundError: if the file is not found
136def get_geotransform(raster_filename: str) -> tuple[float, float, float, float, float, float]: 137 """ Get geotransform from raster file. 138 Args: 139 raster_filename (str): 140 141 Returns: 142 tuple: geotransform 143 GT[0] x-coordinate of the upper-left corner of the upper-left pixel. 144 GT[1] w-e pixel resolution / pixel width. 145 GT[2] row rotation (typically zero). 146 GT[3] y-coordinate of the upper-left corner of the upper-left pixel. 147 GT[4] column rotation (typically zero). 148 GT[5] n-s pixel resolution / pixel height (negative value for a north-up image). 149 150 reference: https://gdal.org/tutorials/geotransforms_tut.html 151 """ # fmt: skip 152 dataset = gdal.Open(raster_filename, gdal.GA_ReadOnly) 153 if dataset is None: 154 raise Exception(f"Data set is None, could not open {raster_filename}") 155 return dataset.GetGeoTransform()
Get geotransform from raster file. Args: raster_filename (str):
Returns: tuple: geotransform GT[0] x-coordinate of the upper-left corner of the upper-left pixel. GT[1] w-e pixel resolution / pixel width. GT[2] row rotation (typically zero). GT[3] y-coordinate of the upper-left corner of the upper-left pixel. GT[4] column rotation (typically zero). GT[5] n-s pixel resolution / pixel height (negative value for a north-up image).
reference: https://gdal.org/tutorials/geotransforms_tut.html
158def transform_coords_to_georef(x_pixel: int, y_line: int, GT: tuple) -> tuple[float, float]: 159 """ Transform pixel coordinates to georeferenced coordinates. 160 Args: 161 x_pixel (int): x pixel coordinate. 162 y_line (int): y pixel coordinate. 163 GT (tuple): geotransform, see get_geotransform(filename) 164 165 Returns: 166 tuple: x_geo, y_geo. 167 168 reference: https://gdal.org/tutorials/geotransforms_tut.html 169 """ # fmt: skip 170 x_geo = GT[0] + x_pixel * GT[1] + y_line * GT[2] 171 y_geo = GT[3] + x_pixel * GT[4] + y_line * GT[5] 172 return x_geo, y_geo
Transform pixel coordinates to georeferenced coordinates. Args: x_pixel (int): x pixel coordinate. y_line (int): y pixel coordinate. GT (tuple): geotransform, see get_geotransform(filename)
Returns: tuple: x_geo, y_geo.
reference: https://gdal.org/tutorials/geotransforms_tut.html
175def transform_georef_to_coords(x_geo: int, y_geo: int, GT: tuple) -> tuple[float, float]: 176 """Inverse of transform_coords_to_georef. 177 178 import sympy 179 a, b, c, d, e, f, g, i, j, x, y = sympy.symbols('a, b, c, d, e, f, g, i, j, x, y', real=True) 180 sympy.linsolve([a+i*b+j*c - x,d+i*e+j*f-y],(i,j)) 181 {((-a*f + c*d - c*y + f*x)/(b*f - c*e), (a*e - b*d + b*y - e*x)/(b*f - c*e))} 182 183 Args: 184 x_geo (int): x georeferenced coordinate. 185 y_geo (int): y georeferenced coordinate. 186 GT (tuple): geotransform, see get_geotransform(filename) 187 188 Returns: 189 tuple: x_pixel, y_line. 190 191 TODO Raises: 192 Exception: if x_pixel or y_line are not integer coordinates. by tolerance? 193 194 reference: https://gdal.org/tutorials/geotransforms_tut.html 195 """ 196 a, b, c, d, e, f = GT 197 x, y = x_geo, y_geo 198 i, j = (-a * f + c * d - c * y + f * x) / (b * f - c * e), (a * e - b * d + b * y - e * x) / (b * f - c * e) 199 # if i % 1 != 0 or j % 1 != 0: 200 # raise Exception("Not integer coordinates!") 201 return int(i), int(j)
Inverse of transform_coords_to_georef.
import sympy a, b, c, d, e, f, g, i, j, x, y = sympy.symbols('a, b, c, d, e, f, g, i, j, x, y', real=True) sympy.linsolve([a+ib+jc - x,d+ie+jf-y],(i,j)) {((-af + cd - cy + fx)/(bf - ce), (ae - bd + by - ex)/(bf - ce))}
Args: x_geo (int): x georeferenced coordinate. y_geo (int): y georeferenced coordinate. GT (tuple): geotransform, see get_geotransform(filename)
Returns: tuple: x_pixel, y_line.
TODO Raises: Exception: if x_pixel or y_line are not integer coordinates. by tolerance?
reference: https://gdal.org/tutorials/geotransforms_tut.html
204def get_rlayer_info(layer: QgsRasterLayer): 205 """Get raster layer info: width, height, extent, crs, cellsize_x, cellsize_y, nodata list, number of bands. 206 207 Args: 208 layer (QgsRasterLayer): A raster layer 209 Returns: 210 dict: raster layer info 211 """ 212 provider = layer.dataProvider() 213 ndv = [] 214 for band in range(1, layer.bandCount() + 1): 215 ndv += [None] 216 if provider.sourceHasNoDataValue(band): 217 ndv[-1] = provider.sourceNoDataValue(band) 218 return { 219 "width": layer.width(), 220 "height": layer.height(), 221 "extent": layer.extent(), 222 "crs": layer.crs(), 223 "cellsize_x": layer.rasterUnitsPerPixelX(), 224 "cellsize_y": layer.rasterUnitsPerPixelY(), 225 "nodata": ndv, 226 "bands": layer.bandCount(), 227 "file": layer.publicSource(), 228 }
Get raster layer info: width, height, extent, crs, cellsize_x, cellsize_y, nodata list, number of bands.
Args: layer (QgsRasterLayer): A raster layer Returns: dict: raster layer info
231def get_rlayer_data(layer: QgsRasterLayer): 232 """Get raster layer data (EVERY BAND) as numpy array; Also returns nodata value, width and height 233 The user should check the shape of the data to determine if it is a single band or multiband raster. 234 len(data.shape) == 2 for single band, len(data.shape) == 3 for multiband. 235 236 Args: 237 layer (QgsRasterLayer): A raster layer 238 239 Returns: 240 data (np.array): Raster data as numpy array 241 nodata (None | list): No data value 242 width (int): Raster width 243 height (int): Raster height 244 245 FIXME? can a multiband raster have different nodata values and/or data types for each band? 246 TODO: make a band list as input 247 """ 248 provider = layer.dataProvider() 249 if layer.bandCount() == 1: 250 block = provider.block(1, layer.extent(), layer.width(), layer.height()) 251 nodata = None 252 if block.hasNoDataValue(): 253 nodata = block.noDataValue() 254 np_dtype = qgis2numpy_dtype(provider.dataType(1)) 255 data = np.frombuffer(block.data(), dtype=np_dtype).reshape(layer.height(), layer.width()) 256 # return data, nodata, np_dtype 257 else: 258 data = [] 259 nodata = [] 260 np_dtypel = [] 261 for i in range(layer.bandCount()): 262 block = provider.block(i + 1, layer.extent(), layer.width(), layer.height()) 263 nodata += [None] 264 if block.hasNoDataValue(): 265 nodata[-1] = block.noDataValue() 266 np_dtypel += [qgis2numpy_dtype(provider.dataType(i + 1))] 267 data += [np.frombuffer(block.data(), dtype=np_dtypel[-1]).reshape(layer.height(), layer.width())] 268 # would different data types bug this next line? 269 data = np.array(data) 270 # return data, nodata, np_dtypl 271 return data
Get raster layer data (EVERY BAND) as numpy array; Also returns nodata value, width and height The user should check the shape of the data to determine if it is a single band or multiband raster. len(data.shape) == 2 for single band, len(data.shape) == 3 for multiband.
Args: layer (QgsRasterLayer): A raster layer
Returns: data (np.array): Raster data as numpy array nodata (None | list): No data value width (int): Raster width height (int): Raster height
FIXME? can a multiband raster have different nodata values and/or data types for each band? TODO: make a band list as input
280def get_cell_size(raster: gdal.Dataset) -> tuple[float, float]: 281 """Get the cell size(s) of a raster. 282 PLANNED DEPRECATION 283 284 Args: 285 raster (gdal.Dataset | str): The GDAL dataset or path to the raster. 286 287 Returns: 288 float | tuple[float, float]: The cell size(s) as a single float or a tuple (x, y). 289 """ # fmt: skip 290 if isinstance(raster, str): 291 ds = gdal.Open(raster, gdal.GA_ReadOnly) 292 elif isinstance(raster, gdal.Dataset): 293 ds = raster 294 else: 295 raise ValueError("Invalid input type for raster") 296 297 # Get the affine transformation parameters 298 affine = ds.GetGeoTransform() 299 300 if affine[1] != -affine[5]: 301 # If x and y cell sizes are not equal 302 cell_size = (affine[1], -affine[5]) # Return as a tuple 303 else: 304 cell_size = affine[1] # Return as a single float 305 306 return cell_size
Get the cell size(s) of a raster. PLANNED DEPRECATION
Args: raster (gdal.Dataset | str): The GDAL dataset or path to the raster.
Returns: float | tuple[float, float]: The cell size(s) as a single float or a tuple (x, y).
309def mask_raster(raster_ds: gdal.Dataset, band: int, polygons: list[ogr.Geometry]) -> np.ndarray: 310 """Mask a raster with polygons using GDAL. 311 312 Args: 313 raster_ds (gdal.Dataset): GDAL dataset of the raster. 314 band (int): Band index of the raster. 315 polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for masking. 316 317 Returns: 318 np.array: Masked raster data as a NumPy array. 319 """ # fmt: skip 320 321 # Get the mask as a NumPy boolean array 322 mask_array = rasterize_polygons(polygons, raster_ds.RasterXSize, raster_ds.RasterYSize) 323 324 # Read the original raster data 325 original_data = band.ReadAsArray() # FIXME: wrong type hint : int has no attribute ReadAsArray 326 327 # Apply the mask 328 masked_data = np.where(mask_array, original_data, np.nan) 329 330 return masked_data
Mask a raster with polygons using GDAL.
Args: raster_ds (gdal.Dataset): GDAL dataset of the raster. band (int): Band index of the raster. polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for masking.
Returns: np.array: Masked raster data as a NumPy array.
333def rasterize_polygons(polygons: list[ogr.Geometry], width: int, height: int) -> np.ndarray: 334 """Rasterize polygons to a boolean array. 335 336 Args: 337 polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for rasterization. 338 geo_transform (tuple): GeoTransform parameters for the raster. 339 width (int): Width of the raster. 340 height (int): Height of the raster. 341 342 Returns: 343 mask_array (np.array): Rasterized mask as a boolean array. 344 """ # fmt: skip 345 346 mask_array = np.zeros((height, width), dtype=bool) 347 348 # Create an in-memory layer to hold the polygons 349 mem_driver = ogr.GetDriverByName("Memory") 350 mem_ds = mem_driver.CreateDataSource("memData") 351 mem_layer = mem_ds.CreateLayer("memLayer", srs=None, geom_type=ogr.wkbPolygon) 352 353 for geometry in polygons: 354 mem_feature = ogr.Feature(mem_layer.GetLayerDefn()) 355 mem_feature.SetGeometry(geometry.Clone()) 356 mem_layer.CreateFeature(mem_feature) 357 358 # Rasterize the in-memory layer and update the mask array 359 gdal.RasterizeLayer(mask_array, [1], mem_layer, burn_values=[1]) 360 361 mem_ds = None # Release the in-memory dataset 362 363 return mask_array
Rasterize polygons to a boolean array.
Args: polygons (list[ogr.Geometry]): List of OGR geometries representing polygons for rasterization. geo_transform (tuple): GeoTransform parameters for the raster. width (int): Width of the raster. height (int): Height of the raster.
Returns: mask_array (np.array): Rasterized mask as a boolean array.
366def stack_rasters( 367 file_list: list[Path], mask_polygon: Union[list[ogr.Geometry], None] = None 368) -> tuple[np.ndarray, list[str]]: 369 """Stack raster files from a list into a 3D NumPy array. 370 371 Args: 372 file_list (list[Path]): List of paths to raster files. 373 mask_polygon (list[ogr.Geometry], optional): List of OGR geometries for masking. Defaults to None. 374 375 Returns: 376 np.array: Stacked raster array. 377 list: List of layer names corresponding to the rasters. 378 """ # fmt: skip 379 array_list = [] 380 cell_sizes = set() 381 layer_names = [] 382 383 for raster_path in file_list: 384 layer_name = raster_path.stem 385 layer_names.append(layer_name) 386 387 ds = gdal.Open(str(raster_path)) 388 if ds is None: 389 raise ValueError(f"Failed to open raster file: {raster_path}") 390 391 band = ds.GetRasterBand(1) 392 393 if mask_polygon: 394 flatten_array = mask_raster(ds, band, mask_polygon) 395 else: 396 flatten_array = band.ReadAsArray() 397 398 array_list.append(flatten_array) 399 cell_sizes.add(get_cell_size(ds)) 400 401 assert len(cell_sizes) == 1, f"There are rasters with different cell sizes: {cell_sizes}" 402 stacked_array = np.stack(array_list, axis=0) # type: np.array 403 print(stacked_array.shape) 404 return stacked_array, layer_names
Stack raster files from a list into a 3D NumPy array.
Args: file_list (list[Path]): List of paths to raster files. mask_polygon (list[ogr.Geometry], optional): List of OGR geometries for masking. Defaults to None.
Returns: np.array: Stacked raster array. list: List of layer names corresponding to the rasters.
407def write_raster( 408 data, 409 outfile="output.tif", 410 driver_name="GTiff", 411 authid="EPSG:3857", 412 geotransform=(0, 1, 0, 0, 0, -1), 413 nodata: int | None = None, 414 feedback=None, 415 logger=None, # logger default ? 416): 417 """Write a raster file from a numpy array. 418 419 To spatially match another raster, get authid and geotransform using: 420 from fire2a.raster import read_raster 421 _,info = read_raster(filename, data=False, info=True). 422 authid = info["Transform"] 423 geotransform = info["Projection"]. 424 425 Args: 426 data (np.array): numpy array to write as raster 427 outfile (str, optional): output raster filename. Defaults to "output.tif". 428 driver_name (str, optional): GDAL driver name. Defaults to "GTiff". 429 authid (str, optional): EPSG code. Defaults to "EPSG:3857". 430 geotransform (tuple, optional): geotransform parameters. Defaults to (0, 1, 0, 0, 0, 1). 431 feedback (Optional, optional): qgsprocessing.feedback object. Defaults to None. 432 logger ([type], optional): logging.logger object. Defaults to None. 433 Returns: 434 bool: True if the raster was written successfully, False otherwise. 435 """ 436 437 try: 438 from fire2a.processing_utils import get_output_raster_format 439 440 driver_name = get_output_raster_format(outfile, feedback=feedback) 441 except Exception as e: 442 fprint( 443 f"Couln't get output raster format: {e}, defaulting to GTiff", 444 level="warning", 445 feedback=feedback, 446 logger=logger, 447 ) 448 driver_name = "GTiff" 449 H, W = data.shape 450 ds = gdal.GetDriverByName(driver_name).Create(outfile, W, H, 1, gdal.GDT_Float32) 451 ds.SetGeoTransform(geotransform) 452 ds.SetProjection(authid) 453 band = ds.GetRasterBand(1) 454 if 0 != band.WriteArray(data): 455 fprint("WriteArray failed", level="warning", feedback=feedback, logger=logger) 456 return False 457 if nodata and data[data == nodata].size > 0: 458 band.SetNoDataValue(nodata) 459 # TBD : always returns 1? 460 # if 0 != band.SetNoDataValue(nodata): 461 # fprint("Set NoData failed", level="warning", feedback=feedback, logger=logger) 462 # return False 463 ds.FlushCache() 464 ds = None 465 return True
Write a raster file from a numpy array.
To spatially match another raster, get authid and geotransform using: from fire2a.raster import read_raster _,info = read_raster(filename, data=False, info=True). authid = info["Transform"] geotransform = info["Projection"].
Args: data (np.array): numpy array to write as raster outfile (str, optional): output raster filename. Defaults to "output.tif". driver_name (str, optional): GDAL driver name. Defaults to "GTiff". authid (str, optional): EPSG code. Defaults to "EPSG:3857". geotransform (tuple, optional): geotransform parameters. Defaults to (0, 1, 0, 0, 0, 1). feedback (Optional, optional): qgsprocessing.feedback object. Defaults to None. logger ([type], optional): logging.logger object. Defaults to None. Returns: bool: True if the raster was written successfully, False otherwise.