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