Skip to content

Commit af14140

Browse files
committed
Add VRT (Virtual Raster Table) reader
Reads GDAL .vrt files by parsing the XML and assembling pixel data from the referenced source GeoTIFFs using windowed reads. Supported VRT features: - SimpleSource: direct pixel copy with source/destination rects - ComplexSource: scaling (ScaleRatio) and offset (ScaleOffset) - Source nodata masking - Multiple bands - GeoTransform and SRS/CRS propagation - Relative and absolute source file paths - Windowed reads (only fetches overlapping source regions) Usage: da = read_geotiff('mosaic.vrt') # auto-detected by extension da = read_vrt('mosaic.vrt') # explicit function da = read_vrt('mosaic.vrt', window=(0, 0, 100, 100)) # windowed read_geotiff auto-detects .vrt files and routes them through the VRT reader. The DataArray gets coordinates from the VRT's GeoTransform and CRS from the SRS tag. New module: _vrt.py with parse_vrt() and read_vrt() functions. 8 new tests: single tile, 2x1 mosaic, 2x2 mosaic, windowed read, CRS propagation, nodata, read_vrt API, and XML parser unit test.
1 parent cf3183d commit af14140

File tree

3 files changed

+620
-3
lines changed

3 files changed

+620
-3
lines changed

xrspatial/geotiff/__init__.py

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
from ._reader import read_to_array
2121
from ._writer import write
2222

23-
__all__ = ['read_geotiff', 'write_geotiff', 'open_cog', 'read_geotiff_dask']
23+
__all__ = ['read_geotiff', 'write_geotiff', 'open_cog', 'read_geotiff_dask',
24+
'read_vrt']
2425

2526

2627
def _wkt_to_epsg(wkt_or_proj: str) -> int | None:
@@ -102,12 +103,15 @@ def read_geotiff(source: str, *, window=None,
102103
overview_level: int | None = None,
103104
band: int | None = None,
104105
name: str | None = None) -> xr.DataArray:
105-
"""Read a GeoTIFF file into an xarray.DataArray.
106+
"""Read a GeoTIFF or VRT file into an xarray.DataArray.
107+
108+
VRT files (.vrt extension) are automatically detected and assembled
109+
from their source GeoTIFFs.
106110
107111
Parameters
108112
----------
109113
source : str
110-
File path or HTTP URL.
114+
File path, HTTP URL, or cloud URI (s3://, gs://, az://).
111115
window : tuple or None
112116
(row_start, col_start, row_stop, col_stop) for windowed reading.
113117
overview_level : int or None
@@ -122,6 +126,10 @@ def read_geotiff(source: str, *, window=None,
122126
xr.DataArray
123127
2D DataArray with y/x coordinates and geo attributes.
124128
"""
129+
# Auto-detect VRT files
130+
if source.lower().endswith('.vrt'):
131+
return read_vrt(source, window=window, band=band, name=name)
132+
125133
arr, geo_info = read_to_array(
126134
source, window=window,
127135
overview_level=overview_level, band=band,
@@ -486,6 +494,78 @@ def _read():
486494
return _read()
487495

488496

497+
def read_vrt(source: str, *, window=None,
498+
band: int | None = None,
499+
name: str | None = None) -> xr.DataArray:
500+
"""Read a GDAL Virtual Raster Table (.vrt) into an xarray.DataArray.
501+
502+
The VRT's source GeoTIFFs are read via windowed reads and assembled
503+
into a single array.
504+
505+
Parameters
506+
----------
507+
source : str
508+
Path to the .vrt file.
509+
window : tuple or None
510+
(row_start, col_start, row_stop, col_stop) for windowed reading.
511+
band : int or None
512+
Band index (0-based). None returns all bands.
513+
name : str or None
514+
Name for the DataArray.
515+
516+
Returns
517+
-------
518+
xr.DataArray
519+
"""
520+
from ._vrt import read_vrt as _read_vrt_internal
521+
522+
arr, vrt = _read_vrt_internal(source, window=window, band=band)
523+
524+
if name is None:
525+
import os
526+
name = os.path.splitext(os.path.basename(source))[0]
527+
528+
# Build coordinates from GeoTransform
529+
gt = vrt.geo_transform
530+
if gt is not None:
531+
origin_x, res_x, _, origin_y, _, res_y = gt
532+
if window is not None:
533+
r0, c0, r1, c1 = window
534+
r0 = max(0, r0)
535+
c0 = max(0, c0)
536+
else:
537+
r0, c0 = 0, 0
538+
height, width = arr.shape[:2]
539+
x = np.arange(width, dtype=np.float64) * res_x + origin_x + (c0 + 0.5) * res_x
540+
y = np.arange(height, dtype=np.float64) * res_y + origin_y + (r0 + 0.5) * res_y
541+
coords = {'y': y, 'x': x}
542+
else:
543+
coords = {}
544+
545+
attrs = {}
546+
547+
# CRS from VRT
548+
if vrt.crs_wkt:
549+
epsg = _wkt_to_epsg(vrt.crs_wkt)
550+
if epsg is not None:
551+
attrs['crs'] = epsg
552+
attrs['crs_wkt'] = vrt.crs_wkt
553+
554+
# Nodata from first band
555+
if vrt.bands:
556+
nodata = vrt.bands[0].nodata
557+
if nodata is not None:
558+
attrs['nodata'] = nodata
559+
560+
if arr.ndim == 3:
561+
dims = ['y', 'x', 'band']
562+
coords['band'] = np.arange(arr.shape[2])
563+
else:
564+
dims = ['y', 'x']
565+
566+
return xr.DataArray(arr, dims=dims, coords=coords, name=name, attrs=attrs)
567+
568+
489569
def plot_geotiff(da: xr.DataArray, **kwargs):
490570
"""Plot a DataArray using its embedded colormap if present.
491571

0 commit comments

Comments
 (0)