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)
def id2xy(idx: int, w: int, h: int) -> tuple[int, int]:
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
def xy2id(x: int, y: int, w: int) -> int:
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)
def read_raster_band(filename: str, band: int = 1) -> tuple[numpy.ndarray, int, int]:
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
def read_raster( filename: str, band: int = 1, data: bool = True, info: bool = True) -> tuple[typing.Optional[numpy.ndarray], typing.Optional[dict]]:
 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
def get_geotransform(raster_filename: str) -> tuple[float, float, float, float, float, float]:
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

def transform_coords_to_georef(x_pixel: int, y_line: int, GT: tuple) -> tuple[float, float]:
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

def transform_georef_to_coords(x_geo: int, y_geo: int, GT: tuple) -> tuple[float, float]:
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

def get_rlayer_info(layer: qgis._core.QgsRasterLayer) -> Dict[str, Any]:
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)
def get_rlayer_data(layer: qgis._core.QgsRasterLayer) -> numpy.ndarray:
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

def get_cell_sizeV2(filename: str, band: int = 1) -> tuple[float, float]:
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.

def get_cell_size(raster: osgeo.gdal.Dataset) -> tuple[float, float]:
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).
def mask_raster( raster_ds: osgeo.gdal.Dataset, band: int, polygons: list[osgeo.ogr.Geometry]) -> numpy.ndarray:
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.
def rasterize_polygons( polygons: list[osgeo.ogr.Geometry], width: int, height: int) -> numpy.ndarray:
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.
def stack_rasters( file_list: list[pathlib.Path], mask_polygon: Optional[list[osgeo.ogr.Geometry]] = None) -> tuple[numpy.ndarray, list[str]]:
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.
def write_raster( data, outfile='output.tif', driver_name='GTiff', authid='EPSG:3857', geotransform=(0, 1, 0, 0, 0, -1), nodata: Optional[int] = None, feedback=None, logger=None) -> bool:
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.
def get_projwin( transform: Tuple[float, float, float, float, float, float], width: int, height: int) -> Tuple[float, float, float, float]:
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)
def extent_to_projwin(extent: qgis._core.QgsRectangle) -> Tuple[float, float, float, float]:
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