def internal_singlepart_to_multipart( vector: Union[str, ogr.DataSource], out_path: Optional[str] = None, overwrite: bool = True, add_index: bool = True, process_layer: int = -1, ) -> str: type_check(vector, [str, ogr.DataSource], "vector") type_check(out_path, [str], "out_path", allow_none=True) type_check(overwrite, [bool], "overwrite") type_check(add_index, [bool], "add_index") type_check(process_layer, [int], "process_layer") vector_list, path_list = ready_io_vector(vector, out_path, overwrite=overwrite) ref = open_vector(vector_list[0]) out_name = path_list[0] out_format = path_to_driver_vector(out_name) driver = ogr.GetDriverByName(out_format) overwrite_required(out_name, overwrite) metadata = internal_vector_to_metadata(ref) remove_if_overwrite(out_name, overwrite) destination: ogr.DataSource = driver.CreateDataSource(out_name) for index, layer_meta in enumerate(metadata["layers"]): if process_layer != -1 and index != process_layer: continue name = layer_meta["layer_name"] geom = layer_meta["column_geom"] sql = f"SELECT ST_Collect({geom}) AS geom FROM {name};" result = ref.ExecuteSQL(sql, dialect="SQLITE") destination.CopyLayer(result, name, ["OVERWRITE=YES"]) if add_index: vector_add_index(destination) destination.FlushCache() return out_name
def internal_vector_to_disk( vector: Union[str, ogr.DataSource], out_path: str, overwrite: bool = True, ) -> str: """OBS: Internal. Single output. Copies a vector source to disk. """ type_check(vector, [str, ogr.DataSource], "vector") type_check(out_path, [str], "out_path") type_check(overwrite, [bool], "overwrite") overwrite_required(out_path, overwrite) datasource = open_vector(vector) metadata = internal_vector_to_metadata(vector) if not os.path.dirname(os.path.abspath(out_path)): raise ValueError( f"Output folder does not exist. Please create first. {out_path}") driver = ogr.GetDriverByName(path_to_driver_vector(out_path)) if driver is None: raise Exception(f"Error while parsing driver for: {vector}") remove_if_overwrite(out_path, overwrite) copy = driver.CreateDataSource(out_path) for layer_idx in range(metadata["layer_count"]): layer_name = metadata["layers"][layer_idx]["layer_name"] copy.CopyLayer(datasource.GetLayer(layer_idx), str(layer_name), ["OVERWRITE=YES"]) # Flush to disk copy = None return out_path
def ready_io_vector( vector: Union[List[Union[str, ogr.DataSource]], str, ogr.DataSource], out_path: Optional[Union[List[str], str]], overwrite: bool = True, add_uuid: bool = False, prefix: str = "", postfix: str = "", ) -> Tuple[List[str], List[str]]: type_check(vector, [list, str, ogr.DataSource], "vector") type_check(out_path, [list, str], "out_path", allow_none=True) type_check(overwrite, [bool], "overwrite") type_check(prefix, [str], "prefix") type_check(postfix, [str], "postfix") vector_list = to_vector_list(vector) if isinstance(out_path, list): if len(vector_list) != len(out_path): raise ValueError( "The length of vector_list must equal the length of the out_path" ) # Check if folder exists and is required. if len(vector_list) > 1 and isinstance(out_path, str): if not os.path.dirname(os.path.abspath(out_path)): raise ValueError( f"Output folder does not exist. Please create first. {out_path}" ) # Generate output names path_list: List[str] = [] for index, in_vector in enumerate(vector_list): metadata = internal_vector_to_metadata(in_vector) name = metadata["name"] if add_uuid: uuid = uuid4().int else: uuid = "" if out_path is None: path = f"/vsimem/{prefix}{name}{uuid}{postfix}.gpkg" elif isinstance(out_path, str): if folder_exists(out_path): path = os.path.join(out_path, f"{prefix}{name}{uuid}{postfix}.tif") else: path = out_path elif isinstance(out_path, list): if out_path[index] is None: path = f"/vsimem/{prefix}{name}{uuid}{postfix}.tif" elif isinstance(out_path[index], str): path = out_path[index] else: raise ValueError(f"Unable to parse out_path: {out_path}") else: raise ValueError(f"Unable to parse out_path: {out_path}") overwrite_required(path, overwrite) path_list.append(path) return (vector_list, path_list)
def extract_patches( raster: Union[List[Union[str, gdal.Dataset]], str, gdal.Dataset], out_dir: Optional[str] = None, prefix: str = "", postfix: str = "_patches", size: int = 32, offsets: Union[list, None] = [], generate_border_patches: bool = True, generate_zero_offset: bool = True, generate_grid_geom: bool = True, clip_geom: Optional[Union[str, ogr.DataSource, gdal.Dataset]] = None, clip_layer_index: int = 0, verify_output=True, verification_samples=100, overwrite=True, epsilon: float = 1e-9, verbose: int = 1, ) -> tuple: """Extracts square tiles from a raster. Args: raster (list of rasters | path | raster): The raster(s) to convert. **kwargs: out_dir (path | none): Folder to save output. If None, in-memory arrays and geometries are outputted. prefix (str): A prefix for all outputs. postfix (str): A postfix for all outputs. size (int): The size of the tiles in pixels. offsets (list of tuples): List of offsets to extract. Example: offsets=[(16, 16), (16, 0), (0, 16)]. Will offset the initial raster and extract from there. generate_border_patches (bool): The tiles often do not align with the rasters which means borders are trimmed somewhat. If generate_border_patches is True, an additional tile is added where needed. generate_zero_offset (bool): if True, an offset is inserted at (0, 0) if none is present. generate_grid_geom (bool): Output a geopackage with the grid of tiles. clip_geom (str, raster, vector): Clip the output to the intersections with a geometry. Useful if a lot of the target area is water or similar. epsilon (float): How much for buffer the arange array function. This should usually just be left alone. verbose (int): If 1 will output messages on progress. Returns: A tuple with paths to the generated items. (numpy_array, grid_geom) """ type_check(raster, [str, list, gdal.Dataset], "raster") type_check(out_dir, [str], "out_dir", allow_none=True) type_check(prefix, [str], "prefix") type_check(postfix, [str], "postfix") type_check(size, [int], "size") type_check(offsets, [list], "offsets", allow_none=True) type_check(generate_grid_geom, [bool], "generate_grid_geom") type_check( clip_geom, [str, ogr.DataSource, gdal.Dataset], "clip_layer_index", allow_none=True, ) type_check(clip_layer_index, [int], "clip_layer_index") type_check(overwrite, [bool], "overwrite") type_check(epsilon, [float], "epsilon") type_check(verbose, [int], "verbose") in_rasters = to_raster_list(raster) if out_dir is not None and not os.path.isdir(out_dir): raise ValueError(f"Output directory does not exists: {out_dir}") if not rasters_are_aligned(in_rasters): raise ValueError( "Input rasters must be aligned. Please use the align function.") output_geom = None metadata = internal_raster_to_metadata(in_rasters[0]) if verbose == 1: print("Generating blocks..") # internal offset array. Avoid manipulating the og array. if offsets is None: offsets = [] in_offsets = [] if generate_zero_offset and (0, 0) not in offsets: in_offsets.append((0, 0)) for offset in offsets: if offset != (0, 0): if not isinstance(offset, (list, tuple)) or len(offset) != 2: raise ValueError( f"offset must be a list or tuple of two integers. Recieved: {offset}" ) in_offsets.append((offset[0], offset[1])) border_patches_needed_x = True border_patches_needed_y = True if clip_geom is not None: border_patches_needed_x = False border_patches_needed_y = False shapes = [] for offset in in_offsets: block_shape = shape_to_blockshape(metadata["shape"], (size, size), offset) if block_shape[0] * size == metadata["width"]: border_patches_needed_x = False if block_shape[1] * size == metadata["height"]: border_patches_needed_y = False shapes.append(block_shape) if generate_border_patches: cut_x = (metadata["width"] - in_offsets[0][0]) - (shapes[0][0] * size) cut_y = (metadata["height"] - in_offsets[0][1]) - (shapes[0][1] * size) if border_patches_needed_x and cut_x > 0: shapes[0][0] += 1 if border_patches_needed_y and cut_y > 0: shapes[0][1] += 1 # calculate the offsets all_rows = 0 offset_rows = [] for i in range(len(shapes)): row = 0 for j in range(len(shapes[i])): if j == 0: row = int(shapes[i][j]) else: row *= int(shapes[i][j]) offset_rows.append(row) all_rows += row offset_rows_cumsum = np.cumsum(offset_rows) if generate_grid_geom is True or clip_geom is not None: if verbose == 1: print("Calculating grid cells..") mask = np.arange(all_rows, dtype="uint64") ulx, uly, _lrx, _lry = metadata["extent"] pixel_width = abs(metadata["pixel_width"]) pixel_height = abs(metadata["pixel_height"]) xres = pixel_width * size yres = pixel_height * size dx = xres / 2 dy = yres / 2 # Ready clip geom outside of loop. if clip_geom is not None: clip_ref = open_vector( internal_reproject_vector(clip_geom, metadata["projection_osr"])) clip_layer = clip_ref.GetLayerByIndex(clip_layer_index) meta_clip = internal_vector_to_metadata(clip_ref) # geom_clip = meta_clip["layers"][clip_layer_index]["column_geom"] clip_extent = meta_clip["extent_ogr"] # clip_adjust = [ # clip_extent[0] - clip_extent[0] % xres, # x_min # (clip_extent[1] - clip_extent[1] % xres) + xres, # x_max # clip_extent[2] - clip_extent[2] % yres, # y_min # (clip_extent[3] - clip_extent[3] % yres) + yres, # y_max # ] coord_grid = np.empty((all_rows, 2), dtype="float64") # tiled_extent = [None, None, None, None] row_count = 0 for idx in range(len(in_offsets)): x_offset = in_offsets[idx][0] y_offset = in_offsets[idx][1] x_step = shapes[idx][0] y_step = shapes[idx][1] x_min = (ulx + dx) + (x_offset * pixel_width) x_max = x_min + (x_step * xres) y_max = (uly - dy) - (y_offset * pixel_height) y_min = y_max - (y_step * yres) # if clip_geom is not None: # if clip_adjust[0] > x_min: # x_min = clip_adjust[0] + (x_offset * pixel_width) # if clip_adjust[1] < x_max: # x_max = clip_adjust[1] + (x_offset * pixel_width) # if clip_adjust[2] > y_min: # y_min = clip_adjust[2] - (y_offset * pixel_height) # if clip_adjust[3] < y_max: # y_max = clip_adjust[3] - (y_offset * pixel_height) # if idx == 0: # tiled_extent[0] = x_min # tiled_extent[1] = x_max # tiled_extent[2] = y_min # tiled_extent[3] = y_max # else: # if x_min < tiled_extent[0]: # tiled_extent[0] = x_min # if x_max > tiled_extent[1]: # tiled_extent[1] = x_max # if y_min < tiled_extent[2]: # tiled_extent[2] = y_min # if y_max > tiled_extent[3]: # tiled_extent[3] = y_max # y is flipped so: xmin --> xmax, ymax -- ymin to keep same order as numpy array x_patches = round((x_max - x_min) / xres) y_patches = round((y_max - y_min) / yres) xr = np.arange(x_min, x_max, xres)[0:x_step] if xr.shape[0] < x_patches: xr = np.arange(x_min, x_max + epsilon, xres)[0:x_step] elif xr.shape[0] > x_patches: xr = np.arange(x_min, x_max - epsilon, xres)[0:x_step] yr = np.arange(y_max, y_min + epsilon, -yres)[0:y_step] if yr.shape[0] < y_patches: yr = np.arange(y_max, y_min - epsilon, -yres)[0:y_step] elif yr.shape[0] > y_patches: yr = np.arange(y_max, y_min + epsilon, -yres)[0:y_step] if generate_border_patches and idx == 0: if border_patches_needed_x: xr[-1] = xr[-1] - ( (xr[-1] + dx) - metadata["extent_dict"]["right"]) if border_patches_needed_y: yr[-1] = yr[-1] - ( (yr[-1] - dy) - metadata["extent_dict"]["bottom"]) oxx, oyy = np.meshgrid(xr, yr) oxr = oxx.ravel() oyr = oyy.ravel() offset_length = oxr.shape[0] coord_grid[row_count:row_count + offset_length, 0] = oxr coord_grid[row_count:row_count + offset_length, 1] = oyr row_count += offset_length offset_rows_cumsum[idx] = offset_length offset_rows_cumsum = np.cumsum(offset_rows_cumsum) coord_grid = coord_grid[:row_count] # Output geometry driver = ogr.GetDriverByName("GPKG") patches_path = f"/vsimem/patches_{uuid4().int}.gpkg" patches_ds = driver.CreateDataSource(patches_path) patches_layer = patches_ds.CreateLayer("patches_all", geom_type=ogr.wkbPolygon, srs=metadata["projection_osr"]) patches_fdefn = patches_layer.GetLayerDefn() og_fid = "og_fid" field_defn = ogr.FieldDefn(og_fid, ogr.OFTInteger) patches_layer.CreateField(field_defn) if clip_geom is not None: clip_feature_count = meta_clip["layers"][clip_layer_index][ "feature_count"] spatial_index = rtree.index.Index(interleaved=False) for _ in range(clip_feature_count): clip_feature = clip_layer.GetNextFeature() clip_fid = clip_feature.GetFID() clip_feature_geom = clip_feature.GetGeometryRef() xmin, xmax, ymin, ymax = clip_feature_geom.GetEnvelope() spatial_index.insert(clip_fid, (xmin, xmax, ymin, ymax)) fids = 0 mask = [] for tile_id in range(coord_grid.shape[0]): x, y = coord_grid[tile_id] if verbose == 1: progress(tile_id, coord_grid.shape[0], "Patch generation") x_min = x - dx x_max = x + dx y_min = y - dx y_max = y + dx tile_intersects = True grid_geom = None poly_wkt = None if clip_geom is not None: tile_intersects = False if not ogr_bbox_intersects([x_min, x_max, y_min, y_max], clip_extent): continue intersections = list( spatial_index.intersection((x_min, x_max, y_min, y_max))) if len(intersections) == 0: continue poly_wkt = f"POLYGON (({x_min} {y_max}, {x_max} {y_max}, {x_max} {y_min}, {x_min} {y_min}, {x_min} {y_max}))" grid_geom = ogr.CreateGeometryFromWkt(poly_wkt) for fid1 in intersections: clip_feature = clip_layer.GetFeature(fid1) clip_geom = clip_feature.GetGeometryRef() if grid_geom.Intersects(clip_geom): tile_intersects = True continue if tile_intersects: ft = ogr.Feature(patches_fdefn) if grid_geom is None: poly_wkt = f"POLYGON (({x_min} {y_max}, {x_max} {y_max}, {x_max} {y_min}, {x_min} {y_min}, {x_min} {y_max}))" grid_geom = ogr.CreateGeometryFromWkt(poly_wkt) ft_geom = ogr.CreateGeometryFromWkt(poly_wkt) ft.SetGeometry(ft_geom) ft.SetField(og_fid, int(fids)) ft.SetFID(int(fids)) patches_layer.CreateFeature(ft) ft = None mask.append(tile_id) fids += 1 if verbose == 1: progress(coord_grid.shape[0], coord_grid.shape[0], "Patch generation") mask = np.array(mask, dtype=int) if generate_grid_geom is True: if out_dir is None: output_geom = patches_ds else: raster_basename = metadata["basename"] geom_name = f"{prefix}{raster_basename}_geom_{str(size)}{postfix}.gpkg" output_geom = os.path.join(out_dir, geom_name) overwrite_required(output_geom, overwrite) remove_if_overwrite(output_geom, overwrite) if verbose == 1: print("Writing output geometry..") internal_vector_to_disk(patches_ds, output_geom, overwrite=overwrite) if verbose == 1: print("Writing numpy array..") output_blocks = [] for raster in in_rasters: base = None basename = None output_block = None if out_dir is not None: base = os.path.basename(raster) basename = os.path.splitext(base)[0] output_block = os.path.join(out_dir + f"{prefix}{basename}{postfix}.npy") metadata = internal_raster_to_metadata(raster) if generate_grid_geom is True or clip_geom is not None: output_shape = (row_count, size, size, metadata["band_count"]) else: output_shape = (all_rows, size, size, metadata["band_count"]) input_datatype = metadata["datatype"] output_array = np.empty(output_shape, dtype=input_datatype) # if clip_geom is not None: # ref = raster_to_array(raster, filled=True, extent=tiled_extent) # else: ref = raster_to_array(raster, filled=True) for k, offset in enumerate(in_offsets): start = 0 if k > 0: start = offset_rows_cumsum[k - 1] blocks = None if (k == 0 and generate_border_patches and (border_patches_needed_x or border_patches_needed_y)): blocks = array_to_blocks( ref, (size, size), offset, border_patches_needed_x, border_patches_needed_y, ) else: blocks = array_to_blocks(ref, (size, size), offset) output_array[start:offset_rows_cumsum[k]] = blocks if generate_grid_geom is False and clip_geom is None: if out_dir is None: output_blocks.append(output_array) else: output_blocks.append(output_block) np.save(output_block, output_array) else: if out_dir is None: output_blocks.append(output_array[mask]) else: output_blocks.append(output_block) np.save(output_block, output_array[mask]) if verify_output and generate_grid_geom: test_extraction( in_rasters, output_blocks, output_geom, samples=verification_samples, grid_layer_index=0, verbose=verbose, ) if len(output_blocks) == 1: output_blocks = output_blocks[0] return (output_blocks, output_geom)
def array_to_raster( array: Union[np.ndarray, np.ma.MaskedArray], reference: Union[str, gdal.Dataset], out_path: Optional[str] = None, overwrite: bool = True, creation_options: Union[list, None] = None, ) -> str: """Turns a numpy array into a GDAL dataset or exported as a raster using a reference raster. Args: array (np.ndarray): The numpy array to convert reference (path or Dataset): A reference on which to base the geographical extent and projection of the raster. **kwargs: out_path (path): The location to save the raster. If None is supplied an in memory raster is returned. filetype is infered from the extension. overwrite (bool): Specifies if the file already exists, should it be overwritten? creation_options (list): A list of options for the GDAL creation. Only used if an outpath is specified. Defaults are: "TILED=YES" "NUM_THREADS=ALL_CPUS" "BIGG_TIF=YES" "COMPRESS=LZW" Returns: If an out_path has been specified, it returns the path to the newly created raster file. """ type_check(array, [np.ndarray, np.ma.MaskedArray], "array") type_check(reference, [str, gdal.Dataset], "reference") type_check(out_path, [str], "out_path", allow_none=True) type_check(overwrite, [bool], "overwrite") type_check(creation_options, [list], "creation_options", allow_none=True) # Verify the numpy array if (not isinstance(array, (np.ndarray, np.ma.MaskedArray)) or array.size == 0 or array.ndim < 2 or array.ndim > 3): raise ValueError(f"Input array is invalid {array}") # Parse the driver driver_name = "GTiff" if out_path is None else path_to_driver_raster( out_path) if driver_name is None: raise ValueError(f"Unable to parse filetype from path: {out_path}") driver = gdal.GetDriverByName(driver_name) if driver is None: raise ValueError( f"Error while creating driver from extension: {out_path}") # How many bands? bands = 1 if array.ndim == 3: bands = array.shape[2] overwrite_required(out_path, overwrite) metadata = raster_to_metadata(reference) reference_nodata = metadata["nodata_value"] # handle nodata. GDAL python throws error if conversion in not explicit. if reference_nodata is not None: reference_nodata = float(reference_nodata) if (reference_nodata).is_integer() is True: reference_nodata = int(reference_nodata) # Handle nodata input_nodata = None if np.ma.is_masked(array) is True: input_nodata = array.get_fill_value( ) # type: ignore (because it's a masked array.) destination_dtype = numpy_to_gdal_datatype(array.dtype) # Weird double issue with GDAL and numpy. Cast to float or int if input_nodata is not None: input_nodata = float(input_nodata) if (input_nodata).is_integer() is True: input_nodata = int(input_nodata) output_name = None if out_path is None: output_name = f"/vsimem/array_to_raster_{uuid4().int}.tif" else: output_name = out_path if metadata["width"] != array.shape[1] or metadata[ "height"] != array.shape[0]: print("WARNING: Input array and raster are not of equal size.") remove_if_overwrite(out_path, overwrite) destination = driver.Create( output_name, array.shape[1], array.shape[0], bands, destination_dtype, default_options(creation_options), ) destination.SetProjection(metadata["projection"]) destination.SetGeoTransform(metadata["transform"]) for band_idx in range(bands): band = destination.GetRasterBand(band_idx + 1) if bands > 1 or array.ndim == 3: band.WriteArray(array[:, :, band_idx]) else: band.WriteArray(array) if input_nodata is not None: band.SetNoDataValue(input_nodata) elif reference_nodata is not None: band.SetNoDataValue(reference_nodata) return output_name
def ready_io_raster( raster: Union[List[Union[str, gdal.Dataset]], str, gdal.Dataset], out_path: Optional[Union[List[str], str]], overwrite: bool = True, prefix: str = "", postfix: str = "", uuid: bool = False, ) -> Tuple[List[str], List[str]]: """ Prepares a raster or a list of rasters for writing for writing. """ type_check(raster, [list, str, gdal.Dataset], "raster") type_check(out_path, [list, str], "out_path", allow_none=True) type_check(overwrite, [bool], "overwrite") type_check(prefix, [str], "prefix") type_check(postfix, [str], "postfix") raster_list = get_raster_path(raster, return_list=True) if isinstance(out_path, list): if len(raster_list) != len(out_path): raise ValueError( "The length of raster_list must equal the length of the out_path" ) # Check if folder exists and is required. test_out_path = [out_path] if not isinstance(out_path, list) else out_path for path in test_out_path: if isinstance(path, str): if "vsimem" in path: continue if os.path.basename(path) == "" and not os.path.isdir( os.path.abspath(path)): raise ValueError( f"Output folder does not exist. Please create first. {path}" ) elif os.path.dirname(path) == "" or not os.path.isdir( os.path.dirname(path)): raise ValueError( f"Output folder does not exist. Please create first. {os.path.dirname(path)}" ) # Generate output names path_list: List[str] = [] for index, in_raster in enumerate(raster_list): metadata = raster_to_metadata(in_raster) name = metadata["name"] path_id = "" if uuid is False else uuid4().int if out_path is None: path = f"/vsimem/{prefix}{name}{path_id}{postfix}.tif" elif isinstance(out_path, str): if folder_exists(out_path): path = os.path.join(out_path, f"{prefix}{name}{path_id}{postfix}.tif") else: path = out_path elif isinstance(out_path, list): if out_path[index] is None: path = f"/vsimem/{prefix}{name}{path_id}{postfix}.tif" elif isinstance(out_path[index], str): path = out_path[index] else: raise ValueError(f"Unable to parse out_path: {out_path}") else: raise ValueError(f"Unable to parse out_path: {out_path}") overwrite_required(path, overwrite) path_list.append(path) return (raster_list, path_list)
def stack_rasters( rasters: List[Union[str, gdal.Dataset]], out_path: Optional[str] = None, overwrite: bool = True, dtype: Optional[str] = None, creation_options: Union[list, None] = None, ) -> str: """Stacks a list of rasters. Must be aligned.""" type_check(rasters, [list], "rasters") type_check(out_path, [str], "out_path", allow_none=True) type_check(overwrite, [bool], "overwrite") type_check(dtype, [str], "dtype", allow_none=True) type_check(creation_options, [list], "creation_options", allow_none=True) if not rasters_are_aligned(rasters, same_extent=True): raise ValueError("Rasters are not aligned. Try running align_rasters.") overwrite_required(out_path, overwrite) # Ensures that all the input rasters are valid. raster_list = get_raster_path(rasters, return_list=True) if out_path is not None and path_to_ext(out_path) == ".vrt": raise ValueError("Please use stack_rasters_vrt to create vrt files.") # Parse the driver driver_name = "GTiff" if out_path is None else path_to_driver_raster( out_path) if driver_name is None: raise ValueError(f"Unable to parse filetype from path: {out_path}") driver = gdal.GetDriverByName(driver_name) if driver is None: raise ValueError( f"Error while creating driver from extension: {out_path}") output_name = None if out_path is None: output_name = f"/vsimem/stacked_rasters_{uuid4().int}.tif" else: output_name = out_path raster_dtype = raster_to_metadata(raster_list[0])["datatype_gdal_raw"] datatype = translate_datatypes( dtype) if dtype is not None else raster_dtype nodata_values: List[Union[int, float, None]] = [] nodata_missmatch = False nodata_value = None total_bands = 0 metadatas = [] for raster in raster_list: metadata = raster_to_metadata(raster) metadatas.append(metadata) nodata_value = metadata["nodata_value"] total_bands += metadata["band_count"] if nodata_missmatch is False: for ndv in nodata_values: if nodata_missmatch: continue if metadata["nodata_value"] != ndv: print( "WARNING: NoDataValues of input rasters do not match. Removing nodata." ) nodata_missmatch = True nodata_values.append(metadata["nodata_value"]) if nodata_missmatch: nodata_value = None remove_if_overwrite(out_path, overwrite) destination = driver.Create( output_name, metadatas[0]["width"], metadatas[0]["height"], total_bands, datatype, default_options(creation_options), ) destination.SetProjection(metadatas[0]["projection"]) destination.SetGeoTransform(metadatas[0]["transform"]) bands_added = 0 for index, raster in enumerate(raster_list): metadata = metadatas[index] band_count = metadata["band_count"] array = raster_to_array(raster) for band_idx in range(band_count): dst_band = destination.GetRasterBand(bands_added + 1) dst_band.WriteArray(array[:, :, band_idx]) if nodata_value is not None: dst_band.SetNoDataValue(nodata_value) bands_added += 1 return output_name