Skip to content

Utilities

align_rasters(input_images, output_images, *, resampling_method='bilinear', tap=False, resolution=None, window_size=None, debug_logs=False, cache=None, image_threads=None, io_threads=None, tile_threads=None, concurrent_processing_backend='process_pool', dask_scheduler=None, resume_from_outputs='no')

Aligns multiple rasters to a common resolution and grid using specified resampling.

Parameters:

Name Type Description Default
input_images (str | List[str], required)

Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/.tif", "/input/folder" (assumes .tif), ["/input/one.tif", "/input/two.tif"].

required
output_images (str | List[str], required)

Defines output files from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Local.tif), ["/input/one.tif", "/input/two.tif"].

required
resampling_method Literal['nearest', 'bilinear', 'cubic']

"nearest" | "bilinear" | "cubic".

'bilinear'
tap bool

If True, snap output extent to target-aligned pixels (GDAL -tap behavior).

False
resolution Resolution

Shared pixel size strategy (highest, average, lowest), positive int or float pixel size in CRS units, or None to preserve native resolution.

None
window_size WindowSize

Tile size for output blocks; used for GTiff creation options.

None
debug_logs DebugLogs

Verbose logging.

False
cache Cache

Cache for processing.

None
image_threads Threads

Python-level parallelism over images (e.g., ("process", 4)).

None
io_threads Threads

Sets GDAL_NUM_THREADS for internal GDAL multithreading (int or str).

None
tile_threads Threads

Sets GTiff/COG writer NUM_THREADS and Warp’s NUM_THREADS (int or str).

None
concurrent_processing_backend ConcurrentProcessingBackend

Use a local process pool or an existing Dask cluster.

'process_pool'
dask_scheduler DaskScheduler

Existing Dask scheduler as ("file", path) or ("address", address).

None

Returns:

Type Description
None

List[str]: Paths to the locally adjusted output raster images.

Source code in spectralmatch/utils.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def align_rasters(
    input_images: Universal.SearchFolderOrListFiles,
    output_images: Universal.CreateInFolderOrListFiles,
    *,
    resampling_method: Literal["nearest", "bilinear", "cubic"] = "bilinear",
    tap: bool = False,
    resolution: Universal.Resolution = None,
    window_size: Universal.WindowSize = None,
    debug_logs: Universal.DebugLogs = False,
    cache: Universal.Cache = None,
    image_threads: Universal.Threads = None,
    io_threads: Universal.Threads = None,
    tile_threads: Universal.Threads = None,
    concurrent_processing_backend: Universal.ConcurrentProcessingBackend = "process_pool",
    dask_scheduler: Universal.DaskScheduler = None,
    resume_from_outputs: Literal["no", "yes", "validate"] = "no",
) -> None:
    """
    Aligns multiple rasters to a common resolution and grid using specified resampling.

    Args:
        input_images (str | List[str], required): Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/*.tif", "/input/folder" (assumes *.tif), ["/input/one.tif", "/input/two.tif"].
        output_images (str | List[str], required): Defines output files from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Local.tif), ["/input/one.tif", "/input/two.tif"].
        resampling_method: "nearest" | "bilinear" | "cubic".
        tap: If True, snap output extent to target-aligned pixels (GDAL -tap behavior).
        resolution: Shared pixel size strategy (highest, average, lowest), positive int or float pixel size in CRS units, or None to preserve native resolution.
        window_size: Tile size for output blocks; used for GTiff creation options.
        debug_logs: Verbose logging.
        cache: Cache for processing.
        image_threads: Python-level parallelism over images (e.g., ("process", 4)).
        io_threads: Sets GDAL_NUM_THREADS for internal GDAL multithreading (int or str).
        tile_threads: Sets GTiff/COG writer NUM_THREADS and Warp’s NUM_THREADS (int or str).
        concurrent_processing_backend: Use a local process pool or an existing Dask cluster.
        dask_scheduler: Existing Dask scheduler as ("file", path) or ("address", address).

    Returns:
        List[str]: Paths to the locally adjusted output raster images.
    """
    _print_step_start("align_rasters")

    Universal._validate(
        input_images=input_images,
        output_images=output_images,
        debug_logs=debug_logs,
        window_size=window_size,
        cache=cache,
        image_threads=image_threads,
        io_threads=io_threads,
        tile_threads=tile_threads,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler,
    )
    UtilsValidation._validate_align_rasters(
        resampling_method=resampling_method,
        tap=tap,
        resolution=resolution,
    )

    input_image_paths = _resolve_paths(
        "search", input_images, kwargs={"default_file_pattern": "*.tif"}
    )
    output_image_paths = _resolve_paths(
        "create",
        output_images,
        kwargs={
            "paths_or_bases": input_image_paths,
            "default_file_pattern": "$_Align.tif",
        },
    )
    input_image_names = [
        os.path.splitext(os.path.basename(p))[0] for p in input_image_paths
    ]
    reusable_output_paths = _resolve_reusable_output_paths(
        output_image_paths,
        resume_mode=resume_from_outputs,
        debug_logs=debug_logs,
        step_name="align_rasters",
    )
    if len(reusable_output_paths) == len(output_image_paths):
        return output_image_paths

    # Setup gdal
    _set_gdal_cache(cache, debug_logs)
    _set_gdal_workers(io_threads, debug_logs)

    # Setup parallel
    image_backend = "thread" # "process" or "thread"
    image_threads_on, image_thread_workers = _resolve_parallel_config(
        image_threads, concurrent_processing_backend, dask_scheduler
    )
    tile_thread_on, tile_thread_workers = _resolve_parallel_config(tile_threads)


    if debug_logs:
        print(f"{len(input_image_paths)} rasters to align")

    # Check requirements
    _check_raster_requirements(
        input_image_paths,
        debug_logs,
        check_geotransform=True,
        check_crs=True,
        check_bands=True,
        check_nodata=True,
    )

    # Get target resolution
    target_res = compute_resolution(input_image_paths, resolution)

    if debug_logs:
        print(f"Target resolution: {target_res}")

    # Prepare per-image args
    window_size = _resolve_window_size(window_size, input_image_paths[0], debug_logs)
    args = [
        (
            input_image_names[i],
            input_image_paths[i],
            output_image_paths[i],
            target_res,
            resampling_method,
            tap,
            window_size,
            tile_thread_workers,
            debug_logs,
            resume_from_outputs,
        )
        for i in range(len(input_image_paths))
        if output_image_paths[i] not in reusable_output_paths
    ]

    _run_image_tasks(
        _align_process_image, args,
        input_paths=[arg[1] for arg in args],
        output_paths=[arg[2] for arg in args],
        parallel=image_threads_on,
        backend=image_backend, workers=image_thread_workers,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler, executor_factory=_get_executor,
    )
    return output_image_paths

compute_overviews(input_images_paths, *, output_image_paths=None, window_scales=(2, 4, 8, 16, 32), cache=None, image_threads=None, io_threads=None, tile_threads=None, concurrent_processing_backend='process_pool', dask_scheduler=None, debug_logs=False)

Compute and attach GDAL overviews for one or more raster images.

Parameters:

Name Type Description Default
input_images_paths (str | List[str], required)

Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/.tif", "/input/folder" (assumes .tif), ["/input/one.tif", "/input/two.tif"].

required
output_image_paths str | List[str] | None

Defines output files as None to update input images or from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Global.tif), ["/input/one.tif", "/input/two.tif"].

None
window_scales tuple[int, ...] | None

Overview decimation factors, default (2, 4, 8, 16, 32); None or an empty tuple skips overview creation.

(2, 4, 8, 16, 32)
cache Cache

GDAL cache size configuration.

None
image_threads Threads

Number of parallel workers for image-level processing.

None
io_threads Threads

GDAL IO worker configuration.

None
tile_threads Threads

GDAL internal threads for overview computation.

None
concurrent_processing_backend ConcurrentProcessingBackend

Use a local process pool or an existing Dask cluster.

'process_pool'
dask_scheduler DaskScheduler

Existing Dask scheduler as ("file", path) or ("address", address).

None
debug_logs bool

Enable verbose logging.

False

Returns:

Type Description

List[str]: Paths of images that received overviews.

Source code in spectralmatch/utils.py
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
def compute_overviews(
    input_images_paths: Universal.SearchFolderOrListFiles,
    *,
    output_image_paths: Universal.CreateInFolderOrListFiles | None = None,
    window_scales: tuple[int, ...] | None = (2, 4, 8, 16, 32),
    cache: Universal.Cache = None,
    image_threads: Universal.Threads = None,
    io_threads: Universal.Threads = None,
    tile_threads: Universal.Threads = None,
    concurrent_processing_backend: Universal.ConcurrentProcessingBackend = "process_pool",
    dask_scheduler: Universal.DaskScheduler = None,
    debug_logs: bool = False,
):
    """
    Compute and attach GDAL overviews for one or more raster images.

    Args:
        input_images_paths (str | List[str], required): Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/*.tif", "/input/folder" (assumes *.tif), ["/input/one.tif", "/input/two.tif"].
        output_image_paths (str | List[str] | None): Defines output files as None to update input images or from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Global.tif), ["/input/one.tif", "/input/two.tif"].
        window_scales: Overview decimation factors, default (2, 4, 8, 16, 32); None or an empty tuple skips overview creation.
        cache: GDAL cache size configuration.
        image_threads: Number of parallel workers for image-level processing.
        io_threads: GDAL IO worker configuration.
        tile_threads: GDAL internal threads for overview computation.
        concurrent_processing_backend: Use a local process pool or an existing Dask cluster.
        dask_scheduler: Existing Dask scheduler as ("file", path) or ("address", address).
        debug_logs: Enable verbose logging.

    Returns:
        List[str]: Paths of images that received overviews.
    """
    _print_step_start("compute_overviews")
    if debug_logs: print(f"Input images: {input_images_paths}")
    if debug_logs and output_image_paths: print(f"Output images: {output_image_paths}")
    if debug_logs: print(f"Window scales: {window_scales}")

    Universal._validate(
        input_images=input_images_paths,
        window_scales=window_scales,
        cache=cache,
        image_threads=image_threads,
        io_threads=io_threads,
        tile_threads=tile_threads,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler,
    )

    # Paths
    input_paths = _resolve_paths(
        "search",
        input_images_paths,
        kwargs={"default_file_pattern": "*.tif"},
    )

    if output_image_paths is None:
        target_paths = input_paths
    else:
        target_paths = _resolve_paths(
            "create",
            output_image_paths,
            kwargs={
                "paths_or_bases": input_paths,
                "default_file_pattern": "$.tif",
            },
        )
    if not window_scales and output_image_paths is None:
        return target_paths

    # GDAL config
    _set_gdal_cache(cache, debug_logs)
    _set_gdal_workers(io_threads, debug_logs)

    image_backend = "thread"
    image_threads_on, image_workers = _resolve_parallel_config(
        image_threads, concurrent_processing_backend, dask_scheduler
    )
    tile_thread_on, tile_workers = _resolve_parallel_config(tile_threads)


    args = [(source, target, window_scales, tile_thread_on, tile_workers, debug_logs) for source, target in zip(input_paths, target_paths)]
    _run_image_tasks(
        _copy_and_compute_overviews, args,
        input_paths=input_paths, output_paths=target_paths,
        parallel=image_threads_on, backend=image_backend, workers=image_workers,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler, executor_factory=_get_executor,
    )

    return target_paths

compute_resolution(paths, strategy)

Resolve a shared square/named resolution, or preserve native grids with None.

Source code in spectralmatch/utils.py
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
def compute_resolution(
    paths: list[str],
    strategy: Universal.Resolution,
    ) -> Tuple[float, float] | None:
    """Resolve a shared square/named resolution, or preserve native grids with None."""
    UtilsValidation._validate_align_rasters(resolution=strategy)
    if strategy is None:
        return None
    if isinstance(strategy, (int, float)):
        return float(strategy), float(strategy)
    res = []
    for p in paths:
        ds = gdal.Open(p, gdal.GA_ReadOnly)
        gt = ds.GetGeoTransform()
        res.append((math.hypot(gt[1], gt[4]), math.hypot(gt[2], gt[5])))
        ds = None
    res_arr = np.asarray(res, dtype=float)
    if strategy == "highest":
        return float(res_arr[:, 0].min()), float(res_arr[:, 1].min())
    if strategy == "lowest":
        return float(res_arr[:, 0].max()), float(res_arr[:, 1].max())
    return float(res_arr[:, 0].mean()), float(res_arr[:, 1].mean())

mask_rasters(input_images, output_images, vector_mask=None, window_size=None, debug_logs=False, cache=None, image_threads=None, io_threads=None, tile_threads=None, concurrent_processing_backend='process_pool', dask_scheduler=None, include_touched_pixels=False, custom_nodata_value=None, resume_from_outputs='no')

Applies a vector-based mask to one or more rasters using GDAL Warp.

Parameters:

Name Type Description Default
input_images (str | List[str], required)

Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/.tif", "/input/folder" (assumes .tif), ["/input/one.tif", "/input/two.tif"].

required
output_images (str | List[str], required)

Defines output files from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Local.tif), ["/input/one.tif", "/input/two.tif"].

required
vector_mask VectorMask

Tuple ('include'|'exclude', vector_path, optional field name).

None
window_size int | None

Tile size for processing tiles. Defaults to None.

None
debug_logs bool

If True, prints progress. Defaults to False.

False
cache int | Tuple[int, str] | None

Controls GDAL cache size. Examples: 2048 (MB), (2, "GB"). Set None to use GDAL’s default. Applied via GDAL_CACHEMAX. window_parallel_workers (Tuple[Literal["process"], Literal["cpu"] | int] | None = None): Parallelization strategy at the window level within each image. Same format as image_parallel_workers. Threads are not supported. Set to None to disable.

None
image_threads Literal['cpu'] | int | None

Parallelism for per-image operations. "cpu" to get number of cores, int to assign number, and None to disable image level parallelism.

None
io_threads Literal['cpu'] | int | None

Parallelism for IO operations. "cpu" to get number of cores, int to assign number, and None to disable io level parallelism.

None
tile_threads Literal['cpu'] | int | None

"cpu" to get number of cores, int to assign number, and None to disable tile level parallelism.

None
concurrent_processing_backend ConcurrentProcessingBackend

Use a local process pool or an existing Dask cluster.

'process_pool'
dask_scheduler DaskScheduler

Existing Dask scheduler as ("file", path) or ("address", address).

None
include_touched_pixels bool

If True, uses all touched pixels for cutline mask.

False
custom_nodata_value float | int | None

Overrides detected NoData value. Defaults to None.

None

Returns:

Name Type Description
list list

Output image paths after masking.

Source code in spectralmatch/utils.py
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
def mask_rasters(
    input_images: Universal.SearchFolderOrListFiles,
    output_images: Universal.CreateInFolderOrListFiles,
    vector_mask: Universal.VectorMask = None,
    window_size: Universal.WindowSize = None,
    debug_logs: Universal.DebugLogs = False,
    cache: Universal.Cache = None,
    image_threads: Universal.Threads = None,
    io_threads: Universal.Threads = None,
    tile_threads: Universal.Threads = None,
    concurrent_processing_backend: Universal.ConcurrentProcessingBackend = "process_pool",
    dask_scheduler: Universal.DaskScheduler = None,
    include_touched_pixels: bool = False,
    custom_nodata_value: Universal.CustomNodataValue = None,
    resume_from_outputs: Literal["no", "yes", "validate"] = "no",
    ) -> list:
    """
    Applies a vector-based mask to one or more rasters using GDAL Warp.

    Args:
        input_images (str | List[str], required): Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/*.tif", "/input/folder" (assumes *.tif), ["/input/one.tif", "/input/two.tif"].
        output_images (str | List[str], required): Defines output files from a template path, folder, or list of paths (with the same length as the input). Specify like: "/input/files/$.tif", "/input/folder" (assumes $_Local.tif), ["/input/one.tif", "/input/two.tif"].
        vector_mask (Universal.VectorMask, optional): Tuple ('include'|'exclude', vector_path, optional field name).
        window_size (int | None): Tile size for processing tiles. Defaults to None.
        debug_logs (bool, optional): If True, prints progress. Defaults to False.
        cache (int | Tuple[int, str] | None, optional): Controls GDAL cache size. Examples: 2048 (MB), (2, "GB"). Set None to use GDAL’s default. Applied via GDAL_CACHEMAX.        window_parallel_workers (Tuple[Literal["process"], Literal["cpu"] | int] | None = None): Parallelization strategy at the window level within each image. Same format as image_parallel_workers. Threads are not supported. Set to None to disable.
        image_threads (Literal["cpu"] | int | None): Parallelism for per-image operations. "cpu" to get number of cores, int to assign number, and None to disable image level parallelism.
        io_threads (Literal["cpu"] | int | None): Parallelism for IO operations. "cpu" to get number of cores, int to assign number, and None to disable io level parallelism.
        tile_threads (Literal["cpu"] | int | None): "cpu" to get number of cores, int to assign number, and None to disable tile level parallelism.
        concurrent_processing_backend: Use a local process pool or an existing Dask cluster.
        dask_scheduler: Existing Dask scheduler as ("file", path) or ("address", address).
        include_touched_pixels (bool, optional): If True, uses all touched pixels for cutline mask.
        custom_nodata_value (float | int | None, optional): Overrides detected NoData value. Defaults to None.

    Returns:
        list: Output image paths after masking.
    """

    _print_step_start("mask_rasters")

    Universal._validate(
        input_images=input_images,
        output_images=output_images,
        debug_logs=debug_logs,
        vector_mask=vector_mask,
        window_size=window_size,
        image_threads=image_threads,
        io_threads=io_threads,
        tile_threads=tile_threads,
        custom_nodata_value=custom_nodata_value,
        cache=cache,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler,
    )
    UtilsValidation._validate_mask_rasters(
        include_touched_pixels=include_touched_pixels,
    )

    input_image_paths = _resolve_paths(
        "search", input_images, kwargs={"default_file_pattern": "*.tif"}
    )
    output_image_paths = _resolve_paths(
        "create",
        output_images,
        kwargs={"paths_or_bases": input_image_paths, "default_file_pattern": "$_Mask.tif"},
    )
    reusable_output_paths = _resolve_reusable_output_paths(
        output_image_paths,
        resume_mode=resume_from_outputs,
        debug_logs=debug_logs,
        step_name="mask_rasters",
    )
    if len(reusable_output_paths) == len(output_image_paths):
        return output_image_paths

    input_image_names = [
        os.path.splitext(os.path.basename(p))[0] for p in input_image_paths
    ]

    _set_gdal_cache(cache, debug_logs)
    _set_gdal_workers(io_threads, debug_logs)

    # Determine multiprocessing and worker count
    image_backend = "thread" # "thread" or "process"
    image_threads_on, image_thread_workers = _resolve_parallel_config(
        image_threads, concurrent_processing_backend, dask_scheduler
    )
    tile_thread_on, tile_thread_workers = _resolve_parallel_config(tile_threads)

    args = [
        (
            input_image_paths[i],
            output_image_paths[i],
            input_image_names[i],
            vector_mask,
            debug_logs,
            include_touched_pixels,
            custom_nodata_value,
            tile_thread_workers,
            tile_thread_on,
            resume_from_outputs,
        )
        for i in range(len(input_image_paths))
        if output_image_paths[i] not in reusable_output_paths
    ]

    _run_image_tasks(
        _mask_raster_process_image, args,
        input_paths=[arg[0] for arg in args],
        output_paths=[arg[1] for arg in args],
        parallel=image_threads_on,
        backend=image_backend, workers=image_thread_workers,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler, executor_factory=_get_executor,
    )

    return output_image_paths

merge_rasters(input_images, output_image_path, *, output_tiles=False, cache=None, image_threads=None, io_threads=None, tile_threads=None, debug_logs=False, output_dtype=None, custom_nodata_value=None, resolution='highest', window_size=None, overlap=0, build_overviews=False, window_scales=(2, 4, 8, 16, 32), resampling_method='nearest', custom_tiles_csv=None, create_vrts='MergedImage.vrt', concurrent_processing_backend=None, dask_scheduler=None, resume_from_outputs='no')

Merge rasters into one GeoTIFF or a folder of GeoTIFF tiles using gdal_retile.

Parameters:

Name Type Description Default
input_images (str | List[str], required)

Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/.tif", "/input/folder" (assumes .tif), ["/input/one.tif", "/input/two.tif"].

required
output_image_path str

Output file, or output folder when output_tiles=True.

required
output_tiles bool

Create separate GeoTIFF files with gdal_retile. Defaults to False.

False
cache Cache

GDAL cache size in GB, or None for the GDAL default.

None
image_threads Threads

Workers across output tiles (positive int, "cpu", or None). Requires output_tiles=True. Each pyramid level finishes before the next starts.

None
io_threads Literal['cpu'] | int | None

Parallelism for IO operations. "cpu" to get number of cores, int to assign number, and None to disable io level parallelism.

None
tile_threads Literal['cpu'] | int | None

"cpu" to get number of cores, int to assign number, and None to disable tile level parallelism.

None
debug_logs bool

If True, prints progress. Defaults to False.

False
output_dtype str | None

Data type for output rasters. Defaults to input image dtype.

None
custom_nodata_value float | int | None

Overrides detected NoData value. Defaults to None.

None
resolution Literal['highest', 'average', 'lowest'] | int | float

Strategy (highest, average, lowest) or a positive int or float specifying square output pixels in CRS units for either merge mode; default highest.

'highest'
window_size WindowSize

In tile mode, output tile width/height in pixels (-ps), default 256. In single-file mode, internal TIFF block size, which must be a multiple of 16.

None
overlap int

Overlap in pixels between adjacent output tiles (-overlap). Requires tile mode; must be nonnegative and smaller than window_size.

0
build_overviews bool

Build internal overviews for one file, or external pyramid tiles in numbered subfolders using -levels in tile mode.

False
window_scales tuple[int, ...] | None

Overview factors, default (2, 4, 8, 16, 32). Tile mode requires consecutive powers of two starting at 2 and passes their count to -levels, capped to avoid zero-sized pyramid rasters. None or an empty tuple disables overviews.

(2, 4, 8, 16, 32)
resampling_method Literal['nearest', 'near', 'bilinear', 'cubic', 'cubicspline', 'lanczos']

nearest (or near), bilinear, cubic, cubicspline, or lanczos. Used for VRT resolution changes and Translate, and passed to retile -r.

'nearest'
custom_tiles_csv str | None

Optional .csv filename inside the output folder (-csv). GDAL writes a headerless, semicolon-delimited tile index with columns tilename;minx;maxx;miny;maxy in the output CRS. A separate index with the same filename is written in each pyramid subfolder. Tile mode only.

None
create_vrts str

Filename of the full-resolution VRT in the output folder, default "MergedImage.vrt". Tile mode also creates a VRT with this name in each generated pyramid folder and links those VRTs as overviews; references are relative so the folder can be moved. Only tile mode accepts a custom name; single-file mode ignores the default.

'MergedImage.vrt'
concurrent_processing_backend ConcurrentProcessingBackend | None

Tile mode only: process_pool (default when omitted) or dask. Dask workers must share access to input/output paths.

None
dask_scheduler DaskScheduler

Tile mode only: existing Dask scheduler as ("file", path) or ("address", address).

None
resume_from_outputs Literal['no', 'yes', 'validate']

"no" overwrites outputs (omits -resume); "yes" skips existing files (-resume); "validate" checks existing tiles with the raster validation helper, removes invalid tiles, then uses -resume. Resume assumes the inputs, grid, and processing options are unchanged.

'no'

Returns:

Name Type Description
str str

Path of the merged raster or the output tile folder.

Source code in spectralmatch/utils.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def merge_rasters(
    input_images: Universal.SearchFolderOrListFiles,
    output_image_path: str,
    *,
    output_tiles: bool = False,
    cache: Universal.Cache = None,
    image_threads: Universal.Threads = None,
    io_threads: Universal.Threads = None,
    tile_threads: Universal.Threads = None,
    debug_logs: Universal.DebugLogs = False,
    output_dtype: Universal.CustomOutputDtype = None,
    custom_nodata_value: Universal.CustomNodataValue = None,
    resolution: Literal["highest", "average", "lowest"] | int | float = "highest",
    window_size: Universal.WindowSize = None,
    overlap: int = 0,
    build_overviews: bool = False,
    window_scales: tuple[int, ...] | None = (2, 4, 8, 16, 32),
    resampling_method: Literal["nearest", "near", "bilinear", "cubic", "cubicspline", "lanczos"] = "nearest",
    custom_tiles_csv: str | None = None,
    create_vrts: str = "MergedImage.vrt",
    concurrent_processing_backend: Universal.ConcurrentProcessingBackend | None = None,
    dask_scheduler: Universal.DaskScheduler = None,
    resume_from_outputs: Literal["no", "yes", "validate"] = "no",
) -> str:
    """
    Merge rasters into one GeoTIFF or a folder of GeoTIFF tiles using gdal_retile.

    Args:
        input_images (str | List[str], required): Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/*.tif", "/input/folder" (assumes *.tif), ["/input/one.tif", "/input/two.tif"].
        output_image_path (str): Output file, or output folder when output_tiles=True.
        output_tiles: Create separate GeoTIFF files with gdal_retile. Defaults to False.
        cache: GDAL cache size in GB, or None for the GDAL default.
        image_threads: Workers across output tiles (positive int, "cpu", or None). Requires output_tiles=True. Each pyramid level finishes before the next starts.
        io_threads (Literal["cpu"] | int | None): Parallelism for IO operations. "cpu" to get number of cores, int to assign number, and None to disable io level parallelism.
        tile_threads (Literal["cpu"] | int | None): "cpu" to get number of cores, int to assign number, and None to disable tile level parallelism.
        debug_logs (bool, optional): If True, prints progress. Defaults to False.
        output_dtype (str | None, optional): Data type for output rasters. Defaults to input image dtype.
        custom_nodata_value (float | int | None, optional): Overrides detected NoData value. Defaults to None.
        resolution: Strategy (highest, average, lowest) or a positive int or float specifying square output pixels in CRS units for either merge mode; default highest.
        window_size: In tile mode, output tile width/height in pixels (-ps), default 256. In single-file mode, internal TIFF block size, which must be a multiple of 16.
        overlap: Overlap in pixels between adjacent output tiles (-overlap). Requires tile mode; must be nonnegative and smaller than window_size.
        build_overviews: Build internal overviews for one file, or external pyramid tiles in numbered subfolders using -levels in tile mode.
        window_scales: Overview factors, default (2, 4, 8, 16, 32). Tile mode requires consecutive powers of two starting at 2 and passes their count to -levels, capped to avoid zero-sized pyramid rasters. None or an empty tuple disables overviews.
        resampling_method: nearest (or near), bilinear, cubic, cubicspline, or lanczos. Used for VRT resolution changes and Translate, and passed to retile -r.
        custom_tiles_csv: Optional .csv filename inside the output folder (-csv). GDAL writes a headerless, semicolon-delimited tile index with columns tilename;minx;maxx;miny;maxy in the output CRS. A separate index with the same filename is written in each pyramid subfolder. Tile mode only.
        create_vrts: Filename of the full-resolution VRT in the output folder, default "MergedImage.vrt". Tile mode also creates a VRT with this name in each generated pyramid folder and links those VRTs as overviews; references are relative so the folder can be moved. Only tile mode accepts a custom name; single-file mode ignores the default.
        concurrent_processing_backend: Tile mode only: process_pool (default when omitted) or dask. Dask workers must share access to input/output paths.
        dask_scheduler: Tile mode only: existing Dask scheduler as ("file", path) or ("address", address).
        resume_from_outputs: "no" overwrites outputs (omits -resume); "yes" skips existing files (-resume); "validate" checks existing tiles with the raster validation helper, removes invalid tiles, then uses -resume. Resume assumes the inputs, grid, and processing options are unchanged.

    Returns:
        str: Path of the merged raster or the output tile folder.

    """

    _print_step_start("merge_rasters")
    Universal._validate(
        input_images=input_images,
        debug_logs=debug_logs,
        cache=cache,
        image_threads=image_threads,
        io_threads=io_threads,
        tile_threads=tile_threads,
        output_dtype=output_dtype,
        window_size=window_size,
        custom_nodata_value=custom_nodata_value,
        concurrent_processing_backend="process_pool" if concurrent_processing_backend is None else concurrent_processing_backend,
        dask_scheduler=dask_scheduler,
    )
    UtilsValidation._validate_merge_rasters(
        resolution=resolution,
        output_tiles=output_tiles,
        output_image_path=output_image_path,
        image_threads=image_threads,
        concurrent_processing_backend=concurrent_processing_backend,
        dask_scheduler=dask_scheduler,
        overlap=overlap,
        window_size=window_size,
        window_scales=window_scales,
        build_overviews=build_overviews,
        resampling_method=resampling_method,
        custom_tiles_csv=custom_tiles_csv,
        create_vrts=create_vrts,
        resume_from_outputs=resume_from_outputs,
    )
    if not output_tiles and _existing_outputs_are_reusable(
        [output_image_path],
        resume_mode=resume_from_outputs,
        debug_logs=debug_logs,
        step_name="merge_rasters",
    ):
        return output_image_path

    # Setup parallel
    tile_thread_on, tile_thread_workers = _resolve_parallel_config(tile_threads)

    input_image_paths = _resolve_paths(
        "search", input_images, kwargs={"default_file_pattern": "*.tif"}
    )
    if not input_image_paths:
        raise ValueError("No input rasters found to merge.")
    input_image_paths = [os.path.abspath(path) for path in input_image_paths]
    _print_image_start(input_image_paths, output_image_path, image_id=os.path.basename(output_image_path.rstrip(os.sep)))

    # Dtype
    output_dtype = _gdal_dtype_str_to_enum(_resolve_gdal_dtype(output_dtype, input_image_paths[0]))

    _set_gdal_cache(cache, debug_logs)
    _set_gdal_workers(io_threads, debug_logs)

    if debug_logs:
        print(f"Building VRT from {len(input_image_paths)} rasters")

    numeric_resolution = isinstance(resolution, (int, float))
    vrt_opts = gdal.BuildVRTOptions(
        resolution="user" if numeric_resolution else resolution,
        xRes=float(resolution) if numeric_resolution else None,
        yRes=float(resolution) if numeric_resolution else None,
        srcNodata=custom_nodata_value,
        VRTNodata=custom_nodata_value,
        resampleAlg=resampling_method,
    )

    vrt_ds = gdal.BuildVRT("", input_image_paths, options=vrt_opts)
    if vrt_ds is None:
        raise RuntimeError("GDAL could not build the merge mosaic VRT.")

    creation_options = [
        "TILED=YES",
        "BIGTIFF=YES",
        "COMPRESS=ZSTD",
    ]

    if window_size and not output_tiles:
        creation_options += [
            f"BLOCKXSIZE={window_size}",
            f"BLOCKYSIZE={window_size}",
        ]

    if tile_thread_workers is not None and str(tile_thread_workers).strip():
        creation_options.append(f"NUM_THREADS={tile_thread_workers}")

    if output_tiles:
        os.makedirs(output_image_path, exist_ok=True)
        if resume_from_outputs == "validate":
            existing_tiles = [
                os.path.join(folder, name)
                for folder, _, names in os.walk(output_image_path)
                for name in names
                if name.lower().endswith((".tif", ".tiff"))
            ]
            reusable = _resolve_reusable_output_paths(
                existing_tiles, resume_mode="validate", debug_logs=debug_logs,
                step_name="merge_rasters",
            )
            for path in existing_tiles:
                if path not in reusable:
                    os.remove(path)
                    for suffix in (".aux.xml", ".ovr", ".msk"):
                        if os.path.isfile(path + suffix):
                            os.remove(path + suffix)

        levels = len(window_scales or ()) if build_overviews else 0
        levels = min(levels, int(math.log2(min(vrt_ds.RasterXSize, vrt_ds.RasterYSize))))
        # Store the VRT on the shared output filesystem so process/Dask workers
        # can open it independently. Its stable basename preserves resume names.
        with tempfile.TemporaryDirectory(prefix=".merge_rasters-", dir=os.path.abspath(output_image_path)) as temp_dir:
            vrt_path = os.path.join(temp_dir, "mosaic.vrt")
            saved_vrt = gdal.Translate(vrt_path, vrt_ds, format="VRT")
            saved_vrt = None
            argv = [
                "gdal_retile", "-of", "GTiff", "-ot", gdal.GetDataTypeName(output_dtype),
                "-ps", str(window_size or 256), str(window_size or 256),
                "-overlap", str(overlap),
                "-r", "near" if resampling_method == "nearest" else resampling_method,
                "-targetDir", os.path.abspath(output_image_path),
            ]
            for option in creation_options:
                argv.extend(["-co", option])
            if levels:
                argv.extend(["-levels", str(levels)])
            if debug_logs:
                argv.append("-v")
            if custom_tiles_csv is not None:
                argv.extend(["-csv", custom_tiles_csv])
            if resume_from_outputs != "no":
                argv.append("-resume")
            argv.append(vrt_path)
            _run_gdal_retile(
                argv, image_threads, concurrent_processing_backend or "process_pool",
                dask_scheduler, cache, io_threads, debug_logs,
            )
        _create_tile_vrts(output_image_path, create_vrts, vrt_ds, window_size or 256, overlap, levels, resampling_method)
        vrt_ds = None
        _print_image_completed(input_image_paths, 1, 1, image_id=os.path.basename(output_image_path.rstrip(os.sep)))
        return output_image_path

    translate_opts = gdal.TranslateOptions(
        format="GTiff",
        outputType=output_dtype,
        noData=custom_nodata_value,
        creationOptions=creation_options,
        resampleAlg=resampling_method,
    )

    merged_dataset = gdal.Translate(
        destName=output_image_path,
        srcDS=vrt_ds,
        options=translate_opts,
    )
    if merged_dataset is None:
        raise RuntimeError(f"GDAL could not write the merged raster: {output_image_path}")
    merged_dataset = None

    vrt_ds = None

    if build_overviews and window_scales: compute_overviews(
        input_images_paths=output_image_path,
        window_scales=window_scales,
        cache=cache,
        io_threads=io_threads,
        tile_threads=tile_threads,
        debug_logs=debug_logs,
        )
    _print_image_completed(input_image_paths, 1, 1, image_id=os.path.basename(output_image_path))
    return output_image_path

merge_vectors(input_vectors, merged_vector_path, method, debug_logs=False, create_name_attribute=None)

Merge multiple vector files using the specified geometric method.

Parameters:

Name Type Description Default
input_vectors str | List[str]

Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/.gpkg", "/input/folder" (assumes .gpkg), ["/input/one.tif", "/input/two.tif"].

required
merged_vector_path str

Path to save merged output.

required
method Literal['intersection', 'union', 'keep']

Merge strategy.

required
debug_logs bool

If True, print debug information.

False
create_name_attribute Optional[Tuple[str, str]]

Tuple of (field_name, separator) to add a combined name field.

None

Returns:

Type Description
None

None

Source code in spectralmatch/utils.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
def merge_vectors(
    input_vectors: Universal.SearchFolderOrListFiles,
    merged_vector_path: str,
    method: Literal["intersection", "union", "keep"],
    debug_logs: bool = False,
    create_name_attribute: Optional[Tuple[str, str]] = None,
) -> None:
    """
    Merge multiple vector files using the specified geometric method.

    Args:
        input_vectors (str | List[str]): Defines input files from a glob path, folder, or list of paths. Specify like: "/input/files/*.gpkg", "/input/folder" (assumes *.gpkg), ["/input/one.tif", "/input/two.tif"].
        merged_vector_path (str): Path to save merged output.
        method (Literal["intersection", "union", "keep"]): Merge strategy.
        debug_logs (bool): If True, print debug information.
        create_name_attribute (Optional[Tuple[str, str]]): Tuple of (field_name, separator) to add a combined name field.

    Returns:
        None
    """
    _print_step_start("merge_vectors")

    os.makedirs(os.path.dirname(merged_vector_path), exist_ok=True)
    input_vector_paths = _resolve_paths(
        "search", input_vectors, kwargs={"default_file_pattern": "*.gpkg"}
    )

    geoms = []
    input_names = []

    for path in input_vector_paths:
        _print_image_start(path, merged_vector_path)
        gdf = gpd.read_file(path)
        if create_name_attribute:
            name = os.path.splitext(os.path.basename(path))[0]
            input_names.append(name)
        geoms.append(gdf)

    combined_name_value = None
    if create_name_attribute:
        field_name, sep = create_name_attribute
        combined_name_value = sep.join(input_names)

    if method == "keep":
        merged_dfs = []
        field_name = create_name_attribute[0] if create_name_attribute else None
        for path in input_vector_paths:
            gdf = gpd.read_file(path)
            if field_name:
                name = os.path.splitext(os.path.basename(path))[0]
                gdf[field_name] = name
            merged_dfs.append(gdf)
        merged = gpd.GeoDataFrame(
            pd.concat(merged_dfs, ignore_index=True), crs=merged_dfs[0].crs
        )

    elif method == "union":
        merged = gpd.GeoDataFrame(pd.concat(geoms, ignore_index=True), crs=geoms[0].crs)
        if create_name_attribute:
            merged[field_name] = combined_name_value

    elif method == "intersection":
        merged = geoms[0]
        for gdf in geoms[1:]:
            shared_cols = set(merged.columns).intersection(gdf.columns) - {"geometry"}
            gdf = gdf.drop(columns=shared_cols)
            merged = gpd.overlay(merged, gdf, how="intersection", keep_geom_type=True)
        if create_name_attribute:
            merged[field_name] = combined_name_value

    else:
        raise ValueError(f"Unsupported merge method: {method}")

    merged.to_file(merged_vector_path)
    for completed, path in enumerate(input_vector_paths, 1):
        _print_image_completed(path, completed, len(input_vector_paths))