Skip to content

raster module

Raster processing utilities for pysatgeo.

align_rasters(rasters, source_path, output_suffix, folder_name='Aligned')

Align raster files to the grid of the first raster and save them together.

Source code in pysatgeo/raster.py
def align_rasters(rasters, source_path, output_suffix, folder_name="Aligned"):
    """Align raster files to the grid of the first raster and save them together."""
    if not rasters:
        return False

    gdal = _require_gdal()
    parent_dir = os.path.dirname(source_path.rstrip(os.sep))
    aligned_dir = os.path.join(parent_dir, folder_name)
    os.makedirs(aligned_dir, exist_ok=True)

    command = ["gdalbuildvrt", "-te"]
    hDataset = gdal.Open(rasters[0], gdal.GA_ReadOnly)
    if hDataset is None:
        return False

    adfGeoTransform = hDataset.GetGeoTransform(can_return_null=True)
    if adfGeoTransform is None:
        return False

    for tif_file in rasters:
        base_filename = os.path.basename(tif_file)
        raster_stem = os.path.splitext(base_filename)[0]
        vrt_file = os.path.join(aligned_dir, f"{raster_stem}.vrt")

        dfGeoXUL = adfGeoTransform[0]
        dfGeoYUL = adfGeoTransform[3]
        dfGeoXLR = (
            adfGeoTransform[0]
            + adfGeoTransform[1] * hDataset.RasterXSize
            + adfGeoTransform[2] * hDataset.RasterYSize
        )
        dfGeoYLR = (
            adfGeoTransform[3]
            + adfGeoTransform[4] * hDataset.RasterXSize
            + adfGeoTransform[5] * hDataset.RasterYSize
        )
        xres = str(abs(adfGeoTransform[1]))
        yres = str(abs(adfGeoTransform[5]))

        _run_command(
            command
            + [
                str(dfGeoXUL),
                str(dfGeoYLR),
                str(dfGeoXLR),
                str(dfGeoYUL),
                "-q",
                "-tr",
                xres,
                yres,
                vrt_file,
                tif_file,
            ]
        )

        output_file = os.path.join(
            aligned_dir, _output_path_with_suffix(base_filename, output_suffix)
        )
        _run_command(["gdal_translate", "-q", vrt_file, output_file])
        os.remove(vrt_file)

    return True

align_rasters_in_place(folder_path, output_suffix)

Align all TIFF files in a folder and write the outputs beside the inputs.

Source code in pysatgeo/raster.py
def align_rasters_in_place(folder_path, output_suffix):
    """Align all TIFF files in a folder and write the outputs beside the inputs."""
    rasters = [
        os.path.join(folder_path, filename)
        for filename in os.listdir(folder_path)
        if filename.endswith(".tiff") or filename.endswith(".tif")
    ]

    if not rasters:
        print("No .tiff files found in the specified folder.")
        return False

    gdal = _require_gdal()

    command = ["gdalbuildvrt", "-te"]
    hDataset = gdal.Open(rasters[0], gdal.GA_ReadOnly)
    if hDataset is None:
        return False

    adfGeoTransform = hDataset.GetGeoTransform(can_return_null=True)
    if adfGeoTransform is None:
        return False

    for tif_file in rasters:
        base_filename = os.path.basename(tif_file)
        raster_stem = os.path.splitext(base_filename)[0]
        vrt_file = os.path.join(folder_path, f"{raster_stem}.vrt")

        dfGeoXUL = adfGeoTransform[0]
        dfGeoYUL = adfGeoTransform[3]
        dfGeoXLR = (
            adfGeoTransform[0]
            + adfGeoTransform[1] * hDataset.RasterXSize
            + adfGeoTransform[2] * hDataset.RasterYSize
        )
        dfGeoYLR = (
            adfGeoTransform[3]
            + adfGeoTransform[4] * hDataset.RasterXSize
            + adfGeoTransform[5] * hDataset.RasterYSize
        )
        xres = str(abs(adfGeoTransform[1]))
        yres = str(abs(adfGeoTransform[5]))

        _run_command(
            command
            + [
                str(dfGeoXUL),
                str(dfGeoYLR),
                str(dfGeoXLR),
                str(dfGeoYUL),
                "-q",
                "-tr",
                xres,
                yres,
                vrt_file,
                tif_file,
            ]
        )

        output_file = os.path.join(
            folder_path, _output_path_with_suffix(base_filename, output_suffix)
        )
        _run_command(["gdal_translate", "-q", vrt_file, output_file])
        os.remove(vrt_file)

    return True

assign_crs(input_tiffs, crs)

Assign a CRS to one raster path or a list of raster paths.

Source code in pysatgeo/raster.py
def assign_crs(input_tiffs, crs):
    """Assign a CRS to one raster path or a list of raster paths."""
    if not isinstance(input_tiffs, list):
        input_tiffs = [input_tiffs]

    outputs = []
    for tiff in input_tiffs:
        with rasterio.open(tiff) as src:
            profile = src.profile
            profile.update(crs=CRS.from_epsg(crs))
            data = src.read(1)
            output_file = os.path.splitext(tiff)[0] + f"_epsg{crs}.tif"

            with rasterio.open(output_file, "w", **profile) as dst:
                dst.write(data, 1)

        os.remove(tiff)
        os.rename(output_file, tiff)
        outputs.append(tiff)

    return outputs

clip_raster_all_pixels(raster_path, vector_path, output_path, all_touched=True)

Clip a raster while including every pixel touched by the mask geometry.

Source code in pysatgeo/raster.py
def clip_raster_all_pixels(raster_path, vector_path, output_path, all_touched=True):
    """Clip a raster while including every pixel touched by the mask geometry."""
    vector_data = gpd.read_file(vector_path)

    with rasterio.open(raster_path) as src:
        out_image, out_transform = mask(
            src, vector_data.geometry, crop=True, all_touched=all_touched
        )
        out_meta = src.meta.copy()

    out_meta.update(
        {
            "driver": "GTiff",
            "height": out_image.shape[1],
            "width": out_image.shape[2],
            "transform": out_transform,
        }
    )

    with rasterio.open(output_path, "w", **out_meta) as dest:
        dest.write(out_image)

    return output_path

clip_raster_by_masks(input_raster_path, mask_input, output_folder)

Clip one or many rasters by one or many mask layers.

Source code in pysatgeo/raster.py
def clip_raster_by_masks(input_raster_path, mask_input, output_folder):
    """Clip one or many rasters by one or many mask layers."""
    os.makedirs(output_folder, exist_ok=True)

    if os.path.isdir(input_raster_path):
        raster_files = [
            file_name
            for file_name in os.listdir(input_raster_path)
            if file_name.endswith((".tif", ".tiff"))
        ]
    elif os.path.isfile(input_raster_path) and input_raster_path.endswith(
        (".tif", ".tiff")
    ):
        raster_files = [os.path.basename(input_raster_path)]
    else:
        raise ValueError("Invalid raster input.")

    if os.path.isdir(mask_input):
        mask_files = [
            file_name
            for file_name in os.listdir(mask_input)
            if file_name.endswith((".shp", ".geojson"))
        ]
    elif os.path.isfile(mask_input) and mask_input.endswith((".shp", ".geojson")):
        mask_files = [os.path.basename(mask_input)]
    else:
        raise ValueError("Invalid mask input.")

    output_paths = []
    for raster_file in raster_files:
        raster_path = (
            os.path.join(input_raster_path, raster_file)
            if os.path.isdir(input_raster_path)
            else input_raster_path
        )
        input_raster_name = os.path.splitext(raster_file)[0]

        for mask_file in mask_files:
            mask_path = (
                os.path.join(mask_input, mask_file)
                if os.path.isdir(mask_input)
                else mask_input
            )
            mask_name = os.path.splitext(os.path.basename(mask_file))[0]

            mask_gdf = gpd.read_file(mask_path)
            raster = rioxarray.open_rasterio(raster_path, masked=True)
            clipped_raster = raster.rio.clip(mask_gdf.geometry, mask_gdf.crs, drop=True)

            output_file_name = f"{input_raster_name}_clipped_by_{mask_name}.tif"
            output_raster_path = os.path.join(output_folder, output_file_name)
            clipped_raster.rio.to_raster(output_raster_path)
            output_paths.append(output_raster_path)

    return output_paths

clip_raster_to_reference_extent(ground_truth_path, prediction_path)

Clip one raster to another raster's extent and dimensions.

Source code in pysatgeo/raster.py
def clip_raster_to_reference_extent(ground_truth_path, prediction_path):
    """Clip one raster to another raster's extent and dimensions."""
    with rasterio.open(prediction_path) as pred_src:
        pred_extent = pred_src.bounds
        pred_box = box(*pred_extent)
        pred_width = pred_src.width
        pred_height = pred_src.height
        pred_transform = pred_src.transform

    with rasterio.open(ground_truth_path, "r+") as gt_src:
        ground_truth, _ = mask(gt_src, [pred_box], crop=True)
        if ground_truth.shape[1] != pred_height or ground_truth.shape[2] != pred_width:
            ground_truth_resized = np.resize(
                ground_truth, (ground_truth.shape[0], pred_height, pred_width)
            )
        else:
            ground_truth_resized = ground_truth

        clipped_gt_meta = gt_src.meta.copy()
        clipped_gt_meta.update(
            {
                "height": ground_truth_resized.shape[1],
                "width": ground_truth_resized.shape[2],
                "transform": pred_transform,
            }
        )

    with rasterio.open(ground_truth_path, "w", **clipped_gt_meta) as out_src:
        out_src.write(ground_truth_resized)

    return ground_truth_resized, clipped_gt_meta

clip_raster_with_vector(raster_file, geojson_directory, output_directory)

Clip one raster by every GeoJSON mask file in a directory.

Source code in pysatgeo/raster.py
def clip_raster_with_vector(raster_file, geojson_directory, output_directory):
    """Clip one raster by every GeoJSON mask file in a directory."""
    raster = rioxarray.open_rasterio(raster_file, masked=True)
    output_paths = []

    for file_name in os.listdir(geojson_directory):
        if file_name.endswith(".geojson"):
            geojson_path = os.path.join(geojson_directory, file_name)
            geojson = gpd.read_file(geojson_path).to_crs(raster.rio.crs)
            clipped_raster = raster.rio.clip(geojson.geometry, geojson.crs, drop=True)

            output_file = os.path.join(
                output_directory, file_name.replace(".geojson", "_clipped.tif")
            )
            clipped_raster.rio.to_raster(output_file, driver="GTiff")
            output_paths.append(output_file)

    return output_paths

clip_rasters_by_extent(input_folder, output_folder, mask_layer_path)

Clip all rasters in a folder to the bounding box of a mask layer.

Source code in pysatgeo/raster.py
def clip_rasters_by_extent(input_folder, output_folder, mask_layer_path):
    """Clip all rasters in a folder to the bounding box of a mask layer."""
    os.makedirs(output_folder, exist_ok=True)
    mask_bounds = gpd.read_file(mask_layer_path).total_bounds
    output_paths = []

    for file_name in os.listdir(input_folder):
        if file_name.endswith(".tif"):
            input_raster_path = os.path.join(input_folder, file_name)
            base_name, ext = os.path.splitext(file_name)
            output_raster_path = os.path.join(output_folder, f"{base_name}_clipped{ext}")

            raster = rioxarray.open_rasterio(input_raster_path, masked=True)
            clipped_raster = raster.rio.clip_box(*mask_bounds)
            clipped_raster.rio.to_raster(output_raster_path)
            output_paths.append(output_raster_path)

    return output_paths

merge_tiffs(input_dir, output_tif)

Merge all TIFF files in a directory into one output raster.

Source code in pysatgeo/raster.py
def merge_tiffs(input_dir, output_tif):
    """Merge all TIFF files in a directory into one output raster."""
    gdal = _require_gdal()
    tiff_files = glob.glob(os.path.join(input_dir, "*.tif"))
    if not tiff_files:
        return None

    vrt = gdal.BuildVRT("temp.vrt", tiff_files)
    gdal.Translate(output_tif, vrt)
    vrt = None
    return output_tif

polygonize_raster(input_tiff, output_vector=None, layer_name='layer', output_format='Parquet')

Polygonize a raster and write the features to a vector dataset.

Source code in pysatgeo/raster.py
def polygonize_raster(
    input_tiff, output_vector=None, layer_name="layer", output_format="Parquet"
):
    """Polygonize a raster and write the features to a vector dataset."""
    format_extensions = {
        "GPKG": ".gpkg",
        "GeoJSON": ".geojson",
        "Parquet": ".parquet",
        "Shapefile": ".shp",
    }

    if output_vector is None:
        base_name, _ = os.path.splitext(input_tiff)
        output_vector = base_name + format_extensions.get(output_format, ".gpkg")

    gdal_polygonize_script = _find_script_on_path("gdal_polygonize.py")

    command = [
        sys.executable,
        gdal_polygonize_script,
        input_tiff,
        "-f",
        output_format,
        output_vector,
        layer_name,
    ]
    subprocess.run(command, check=True, text=True, capture_output=True)
    return output_vector

reproject_clip_resample_tiff(input_tiff=None, output_tiff=None, aoi_shapefile=None, target_srs=None, target_res_x=None, target_res_y=None, resampling_method=None, clip=False, clip_by_extent=False, no_data=None)

Reproject, optionally clip, and resample a raster with gdalwarp.

Source code in pysatgeo/raster.py
def reproject_clip_resample_tiff(
    input_tiff=None,
    output_tiff=None,
    aoi_shapefile=None,
    target_srs=None,
    target_res_x=None,
    target_res_y=None,
    resampling_method=None,
    clip=False,
    clip_by_extent=False,
    no_data=None,
):
    """Reproject, optionally clip, and resample a raster with `gdalwarp`."""
    if not input_tiff:
        raise ValueError("input_tiff must be provided")

    if not output_tiff:
        base, ext = os.path.splitext(input_tiff)
        output_tiff = f"{base}_new.tif"

    cmd_reproject = ["gdalwarp"]

    if target_srs:
        cmd_reproject.extend(["-t_srs", str(target_srs)])
    if target_res_x and target_res_y:
        cmd_reproject.extend(["-tr", str(target_res_x), str(target_res_y)])
    if resampling_method:
        cmd_reproject.extend(["-r", str(resampling_method)])
    if clip:
        if aoi_shapefile is not None:
            if clip_by_extent:
                # Extract the extent if it is a GeoDataFrame or load it from a shapefile
                if isinstance(aoi_shapefile, gpd.geodataframe.GeoDataFrame):
                    bounds = aoi_shapefile.total_bounds
                else:
                    aoi_gdf = gpd.read_file(aoi_shapefile)
                    bounds = aoi_gdf.total_bounds
                cmd_reproject.extend(["-te", *(str(value) for value in bounds)])
            else:
                if isinstance(aoi_shapefile, gpd.geodataframe.GeoDataFrame):
                    raise TypeError(
                        "aoi_shapefile must be a file path when clip_by_extent is False"
                    )
                cmd_reproject.extend(["-cutline", aoi_shapefile, "-crop_to_cutline"])
        else:
            raise ValueError("AOI shapefile must be provided if clip is True")
    if no_data is not None:
        cmd_reproject.extend(["-dstnodata", str(no_data)])

    cmd_reproject.extend([input_tiff, output_tiff])

    _run_command(cmd_reproject)
    return output_tiff

split_raster_bands(file_path, directory_path)

Split a multiband raster into separate single-band rasters.

Source code in pysatgeo/raster.py
def split_raster_bands(file_path, directory_path):
    """Split a multiband raster into separate single-band rasters."""
    output_paths = []
    with rasterio.open(file_path, "r+") as src:
        src.nodata = 0
        for band_idx in range(1, src.count + 1):
            band_data = src.read(band_idx)
            output_tiff_name = (
                f"{os.path.splitext(os.path.basename(file_path))[0]}_B{band_idx}.tif"
            )
            output_tiff_path = os.path.join(directory_path, output_tiff_name)

            profile = src.profile
            profile.update(count=1, dtype=rasterio.float64)

            with rasterio.open(output_tiff_path, "w", **profile) as dst:
                dst.write(band_data, 1)

            output_paths.append(output_tiff_path)

    return output_paths

ssm_nan_fix(raster_dir)

Fix SSM rasters by capping values and applying the expected scale factor.

Source code in pysatgeo/raster.py
def ssm_nan_fix(raster_dir):
    """Fix SSM rasters by capping values and applying the expected scale factor."""
    gdal = _require_gdal()

    for filename in os.listdir(raster_dir):
        if filename.endswith(".tiff"):
            tiff_path = os.path.join(raster_dir, filename)
            dataset = gdal.Open(tiff_path, gdal.GA_Update)
            if dataset is None:
                continue

            raster_array = dataset.ReadAsArray()
            raster_array[raster_array > 200] = 255
            raster_array = raster_array.astype(float)
            raster_array[raster_array != 255] /= 2
            dataset.GetRasterBand(1).WriteArray(raster_array)
            dataset = None

    return True

stack_rasters(tiff_files, output_tiff, aoi_shapefile=None, chunk_size=None, operation=None)

Clip and stack rasters, then aggregate them with a mean or sum.

Source code in pysatgeo/raster.py
def stack_rasters(
    tiff_files, output_tiff, aoi_shapefile=None, chunk_size=None, operation=None
):
    """Clip and stack rasters, then aggregate them with a mean or sum."""
    if not tiff_files:
        raise ValueError("tiff_files must contain at least one raster path")

    if operation not in {"mean", "sum"}:
        raise ValueError("Invalid operation. Choose 'mean' or 'sum'")

    gdal = _require_gdal()

    if aoi_shapefile:
        aoi = gpd.read_file(aoi_shapefile)
        polygon_geometry = [aoi.geometry.iloc[0]]
    else:
        polygon_geometry = None

    raster_arrays = []
    no_data_values = []
    scale_factors = []

    for tiff in tiff_files:
        ds = gdal.Open(tiff)
        if ds is None:
            raise ValueError(f"Could not open raster: {tiff}")

        scale = ds.GetRasterBand(1).GetScale()
        if scale is None:
            scale = 1.0
        scale_factors.append(scale)

        raster = rioxarray.open_rasterio(tiff, chunks=chunk_size).astype("float32")
        print(f"Starting the processing... {tiff}")
        no_data = raster.rio.nodata
        no_data_values.append(no_data)

        if "time" in raster.dims:
            raster = raster.squeeze(dim="time", drop=True)

        try:
            if polygon_geometry:
                processed_raster = raster.rio.clip(
                    polygon_geometry, aoi.crs, drop=True, invert=False
                )
            else:
                processed_raster = raster

            if no_data is not None:
                processed_raster = processed_raster.where(
                    processed_raster != no_data, other=np.nan
                )

            raster_arrays.append(processed_raster.load())
        finally:
            raster.close()
            ds = None

    stacked_rasters = xr.concat(raster_arrays, dim="band")

    if operation == "mean":
        result_raster = stacked_rasters.where(~np.isnan(stacked_rasters)).mean(
            dim="band"
        )

        average_scale = np.mean(scale_factors)
        if average_scale != 1.0:
            result_raster = result_raster * average_scale

    elif operation == "sum":
        result_raster = stacked_rasters.where(~np.isnan(stacked_rasters)).sum(
            dim="band"
        )

        total_scale = np.mean(scale_factors)
        if total_scale != 1.0:
            result_raster = result_raster * total_scale

    if aoi_shapefile:
        result_raster.rio.write_crs(aoi.crs, inplace=True)

    result_no_data_value = no_data_values[0]
    result_raster.rio.write_nodata(result_no_data_value, inplace=True)

    result_raster = result_raster.where(
        ~np.isnan(result_raster), other=result_no_data_value
    )
    result_raster.rio.to_raster(output_tiff)
    print(f"Stacked {operation} Raster saved at {output_tiff}")

    return output_tiff

subset_raster_into_parts(input_raster, num_parts)

Divide a raster into vertically stacked equal parts.

Source code in pysatgeo/raster.py
def subset_raster_into_parts(input_raster, num_parts):
    """Divide a raster into vertically stacked equal parts."""
    gdal = _require_gdal()
    src_ds = gdal.Open(input_raster)
    if src_ds is None:
        raise ValueError(f"Failed to open raster file: {input_raster}")

    width = src_ds.RasterXSize
    height = src_ds.RasterYSize
    part_height = height // num_parts
    output_files = []

    for i in range(num_parts):
        ulx = 0
        uly = i * part_height
        lrx = width
        lry = (i + 1) * part_height if i < num_parts - 1 else height
        output_file = f"{os.path.splitext(input_raster)[0]}_part_{i + 1}.tif"
        output_files.append(output_file)
        gdal.Translate(
            output_file,
            src_ds,
            srcWin=[ulx, uly, lrx - ulx, lry - uly],
            format="GTiff",
        )

    src_ds = None
    return output_files