Raster Reader
Read rasters into the shared (source, tile) schema. GeoBrix offers two
interchangeable tiers: a lightweight pure-Python/PySpark reader (raster_gbx,
rasterio-backed, JAR-free, Serverless-safe) and a heavyweight GDAL-backed
reader (gdal). They emit the same schema, so swapping is a one-line
format(...) change — see Choosing an Execution Tier.
The heavyweight
gdalreader supports the full set of GDAL drivers (NetCDF, HDF5, COG, …); the lightweight reader covers the common raster path. The pairing is a corresponding general raster reader per tier, not a feature-identical one.
The lightweight (*_gbx) and heavyweight readers emit the same schema, but your
compute usually decides the tier: the lightweight tier needs no JAR or init script
and is the only option on Serverless, standard (shared), and ARM clusters. The
heavyweight tier requires a classic x86 cluster (JAR + GDAL init script); where it is
available it uses native GDAL on the JVM and tends to pull ahead on large workloads. See
the Benchmarking page for light-vs-heavy timings and methodology.
Options
Lightweight (raster_gbx)
The lightweight reader defaults to no splitting (splitStrategy=none):
one whole-image tile per file. Splitting is opt-in; COG creation is a
separate concern handled by the cog_gbx writer. See
COG Reader and COG Writer for the
prepare-then-read pipeline.
| Option | Default | Description |
|---|---|---|
virtualTiles | "true" | Default. Emit bytes-free virtual tiles — each row carries the source path + pixel window instead of raster bytes; pixels are read lazily when an operation needs them (the ingest-OOM-dissolving default for the light tier). Set "false" to materialize raster bytes into each row. See Virtual Tiles. |
splitStrategy | "none" | When to split large rasters: none (default — one whole-image tile per file), auto (resolve to serverless or classic by environment), serverless (opt-in, 64 MiB decoded budget per tile), classic (opt-in, 1 536 MiB decoded budget per tile). |
sizeInMB | "-1" | Power-user override: set a positive value to pin the per-tile decoded-memory budget in MiB. A positive value implies opt-in split regardless of splitStrategy. -1 = defer to splitStrategy. |
filterRegex | ".*" | When loading a directory, keep files whose full path matches this regex. |
clipPolygons | none | Area(s) of interest to clip to. One WKT/EWKT string for a single polygon, or a JSON-array string for a list, e.g. '["POLYGON((...))","POLYGON((...))"]' (raw WKB/EWKB bytes are accepted only from programmatic callers). One tile is emitted per polygon whose envelope intersects the raster; a polygon that misses the raster emits no tile. Materialized tiles are pre-clipped (pixels outside the polygon are NoData); virtual tiles carry the clip as a deferred instruction. Mutually exclusive with windows and tileSize. Pairs naturally with the COG Reader. |
windows | none | Pixel window(s) to read. A JSON 4-int array "[col,row,w,h]" for one, or a JSON array of them "[[..],[..]]" for a list. One tile per window; partial windows clip to the raster extent, fully-outside windows are skipped. Mutually exclusive with clipPolygons and tileSize. |
clipCrs | none | CRS for clipPolygons that lack an embedded SRID (e.g. "EPSG:27700"). Precedence: an EWKB/EWKT polygon's embedded SRID → clipCrs → the raster's CRS. |
tileSize | none | Cut the whole raster into a regular fixed-size pixel grid: "w,h" (e.g. "512,512") or a single "512" for square tiles. One tile per grid cell; edge cells are clamped to the extent. Mutually exclusive with clipPolygons and windows. Materialized tiles are guarded to the ~2 GB Spark-cell limit (a too-large tileSize raises a clear error); virtual tiles (virtualTiles=true) carry no bytes and are unguarded. |
overlapPercent | 0 | Overlap between adjacent tileSize cells, as a percentage of tile size (overlap_px = ceil(tile_dim · pct/100), step = tile_dim − overlap_px). Applies to tileSize only (an error otherwise). Default 0 = non-overlapping grid. |
tilesTable | none | Delta table to use as a tile index (see pre-computed tile inputs below). |
skipOrdering | "false" | Pass "true" to suppress the auto-sort-by-source-path that tilesTable reads apply by default. See tilesTable auto-order below. |
A hard tiling grid can slice straight through a spatial feature (a field, a building, an airport)
that straddles a seam, so no single tile sees it whole. Set overlapPercent so adjacent tileSize
cells overlap — a feature near a boundary then appears complete in at least one tile, which per-tile
analysis needs. Overlap only makes sense for a reader-chosen grid (tileSize); windows and
clipPolygons are extents you specify explicitly, so they take no overlap.
Light raster readers now emit virtual tiles by default (virtualTiles=true) — bytes-free
(path, window) references that read pixels lazily. Previously the reader materialized raster
bytes into every row. To restore materialized reads, pass .option("virtualTiles", "false").
A virtual tile passed to a heavyweight function must be materialized first — see
Virtual Tiles.
auto to nonePrior to this release the reader defaulted to splitStrategy=auto, which
auto-split large rasters on a decoded-memory budget. The default is now
none — one tile per file. To opt back into splitting, pass
.option("splitStrategy", "serverless") or .option("splitStrategy", "classic").
When splitting is enabled, split tiles are emitted as plain GeoTIFF.
COG output is a cog_gbx writer concern, not a reader concern: the
tileFormat, cogBlockSize, and cogOverviewResampling reader options
are removed. For COG-encoded output, use the
COG Writer to prepare master COGs, then read them with
the COG Reader.
Striped GeoTIFFs (no internal tiling) split into full-width row-bands. Internally-tiled sources split on a block-snapped grid.
Directory reads
Reading a directory plans every file (fast, stat-free listing — ~1.5 s at 10k files); for pre-computed windows or very large tile counts see Advanced: pre-computed tile inputs below.
Stock Spark file readers (spark.read.format("binaryFile"), text, parquet, …)
silently skip any file whose name starts with _ or . — Spark's hidden-file filter,
applied during directory listing. Some GeoBrix raster tiles are written with a leading
_ in their (hashed) name, so a stock reader can miss a large fraction of a directory.
Note that .option("pathGlobFilter", "*.tif") does not override this filter — the
filter runs before the glob, so _-prefixed files stay excluded (an explicit _* glob,
explicit _-file paths, and recursiveFileLookup do not help either).
To read every tile, enumerate the paths on the driver (which does not apply the filter) and build the DataFrame explicitly — ideal when you only need the file list, e.g. to feed a VRT builder:
from pathlib import Path
paths = [str(p) for p in Path(output_dir).rglob("*.tif")]
files_df = spark.createDataFrame([(p,) for p in paths], ["path"])
GeoBrix's own raster readers (raster_gbx / gtiff_gbx / cog_gbx) list files directly
and are not affected — this applies only to stock Spark file readers.
Opt-in split (serverless budget):
# Two-axis control: split strategy and output format.
df = (
spark.read.format("raster_gbx")
.option("splitStrategy", "serverless") # or: classic | none | auto
.option("tileFormat", "cog") # or: gtiff | auto
.option("cogBlockSize", "512") # tile size for COG internal grid (px)
.option("cogOverviewResampling", "AVERAGE") # overview resampling algorithm
.load("/Volumes/main/geobrix_samples/geobrix-examples/nyc/sentinel2")
)
READ_WITH_OPTIONS exampleThe sizeInMB option remains available as a power-user override:
# Options: sizeInMB (tile split threshold) + filterRegex (directory listing)
df = (spark.read.format("raster_gbx")
.option("sizeInMB", "16")
.option("filterRegex", r".*\.tif$")
.load("{SAMPLE_RASTER_PATH}"))
gtiff_gbx is raster_gbx with the GeoTIFF driver preset.
COG lane for large rasters
For large rasters, the recommended path is to prepare master COGs with the
cog_gbx writer and then clip windows with the cog_gbx reader. This
keeps the reader simple (no split, no re-encode) and lets GDAL's range-read
mechanism fetch only the bytes that intersect the AOI.
Who benefits from pre-built COG overviews:
rst_tilexyzandrst_xyzpyramid— the XYZ tile-serving pipeline uses rio-tiler, which automatically selects the appropriate COG overview level for the requested zoom.rst_resample*— the resample family triggers overview-level selection automatically when a COG source has pre-built overviews.
COG preparation + windowed read:
See COG Writer for the preparation step and COG Reader for the windowed read.
Force-writing COG output from a tile DataFrame: the gtiff_gbx writer
still accepts a cog=true option to re-encode any (source, tile) DataFrame
as COG when you already have tiles in memory:
# Force-convert to COG on write (any DataFrame with a tile column):
import tempfile
with tempfile.TemporaryDirectory() as out:
df.write.format("gtiff_gbx") \
.mode("overwrite") \
.option("cog", "true") \
.option("cogBlockSize", "512") \
.option("cogOverviewResampling", "AVERAGE") \
.option("cogCompression", "DEFLATE") \
.save(out)
cog_df = spark.read.format("raster_gbx").load(out)
print(cog_df.count(), "COG tiles written and read back")
Heavyweight (gdal)
| Option | Default | Description |
|---|---|---|
driver | auto-detected from extension | Explicitly specify the GDAL driver to use (regardless of extension). |
sizeInMB | "-1" | Default (<= 0) = no split: one whole-image tile per file. Set a positive MB value to split large files into multiple tiles for parallel processing. |
filterRegex | ".*" | Filter files by regex when reading from a directory. |
readSubdatasets | "false" | Read subdatasets if present (e.g. HDF, NetCDF). |
rasterAsGrid | "false" | Read as grid instead of tiles. |
retile | "false" | Retile rasters for optimal processing. |
tileSize | "256" | Tile size in pixels (if retiling enabled). |
Example — forcing the GDAL driver explicitly:
# Read with explicit driver (sample-data Volumes path)
df = spark.read.format("gdal") \
.option("driver", "GTiff") \
.load("{SAMPLE_RASTER_PATH}")
df.show()
+--------------------------------------------------+-----+
|path |tile |
+--------------------------------------------------+-----+
|/Volumes/.../nyc_sentinel2_red.tif |{...}|
+--------------------------------------------------+-----+
- Lightweight · raster_gbx
- Heavyweight · gdal
Register
# Register the lightweight raster DataSources (once per session)
from databricks.labs.gbx.ds.register import register
register(spark)
This is a key difference from the heavyweight tier. The heavyweight readers/writers
(gdal, gtiff_gdal, …) are auto-discovered from the JAR on the classpath via
Spark's JVM DataSourceRegister service loader, so spark.read.format("gdal")
works with no setup call. The lightweight readers/writers are Python Data
Source V2 sources, and Python has no classpath auto-discovery equivalent — so you
must register them explicitly with register(spark) (above) before using
format("raster_gbx") / format("gtiff_gbx") for reads or writes.
Importing databricks.labs.gbx.ds will opportunistically register them if a
Spark session is already active, but the explicit register(spark) call is the
reliable path, in case your session is created after imports. (This mirrors
the heavyweight gbx_rst_* SQL functions, which also require an explicit
register(spark).)
Read (catch-all)
# Catch-all lightweight reader (any rasterio-readable raster)
df = spark.read.format("raster_gbx").load("{SAMPLE_RASTER_PATH}")
df.show()
+--------------------------------------------------+-----+
|source |tile |
+--------------------------------------------------+-----+
|/Volumes/.../nyc_sentinel2_red.tif |{...}|
+--------------------------------------------------+-----+
gtiff_gbx is raster_gbx with the GeoTIFF driver preset. See the Lightweight GeoTIFF Reader for the named-reader page. See the page-level Options section below for the available reader options.
See Choosing an Execution Tier for the full tradeoff and the Benchmarking page for light-vs-heavy timings.
It is the lightweight counterpart of the heavyweight gdal reader, supporting Python and SQL bindings (not Scala).
The GDAL reader provides generic support for reading raster data formats through the GDAL library. This is the base reader that powers all raster format readers in GeoBrix.
GeoBrix is currently most focused on support for GeoTIFF format. While the GDAL reader can work with many formats, GeoTIFF receives the most testing and optimization. For other formats, your experience may vary depending on GDAL driver availability and maturity.
The generic gdal reader accepts any GDAL-supported format (~150+ drivers). Individual GDAL drivers have historically had parser vulnerabilities, so a malformed file from an untrusted source can exercise bugs deep inside the native library.
- Only load raster files from sources you trust. For third-party data, prefer validating with
gdalinfo(or a sandboxed job) before ingesting into production pipelines. - Network-capable drivers (
WMS,WMTS,WCS,WFS,HTTP,CSW,OGCAPI) are disabled by default because they can trigger outbound HTTP fetches at open time. To re-enable them, setspark.gdal.GDAL_SKIP=""(disable all skipping) or a narrower space-separated list in your Spark cluster config. See Installation for cluster configuration guidance.
Format Name
gdal
Overview
The GDAL reader is a generic raster data reader that can handle any format supported by GDAL. While GeoBrix provides named readers for common formats (GeoTIFF), you can use the GDAL reader directly for any available format.
Understanding Raster Formats: Raster data represents geographic information as a grid of cells (pixels), where each cell contains a value. Unlike vector data (points, lines, polygons), rasters are ideal for continuous phenomena like elevation, temperature, or satellite imagery. Common raster use cases include Digital Elevation Models (DEMs) for terrain analysis, multispectral satellite imagery for land cover classification, weather model outputs (temperature, precipitation), and aerial photography. Raster formats vary in their compression methods, band organization, and metadata capabilities—GeoTIFF is the most universal, NetCDF excels at multi-dimensional scientific data, HDF5 handles massive hierarchical datasets, and GRIB2 is standard for meteorological models.
Available Formats
The GDAL reader can work with many GDAL raster drivers, including:
- GeoTIFF (.tif, .tiff) - Most common geospatial raster format
- Cloud-Optimized GeoTIFF (COG) - Web-optimized GeoTIFF variant
- NetCDF (.nc) - Multi-dimensional scientific data
- HDF5 (.h5, .hdf) - Hierarchical data format
- GRIB/GRIB2 (.grb, .grib2) - Meteorological data
- JPEG2000 (.jp2) - High-compression imagery
- ENVI (.hdr) - Remote sensing format
- Zarr - Cloud-native array storage
- And 150+ more formats
Experience varies across GDAL formats. Not all formats are available by default—some require additional packages or drivers to be installed in your environment. Refer to GDAL for driver names.
Basic Usage
Python
# Read raster file (sample-data Volumes path)
df = spark.read.format("gdal").load("{SAMPLE_RASTER_PATH}")
df.show()
+--------------------------------------------------+-----+
|path |tile |
+--------------------------------------------------+-----+
|/Volumes/.../nyc_sentinel2_red.tif |{...}|
+--------------------------------------------------+-----+
Scala
val df = spark.read.format("gdal").load("/Volumes/main/default/geobrix_samples/geobrix-examples/nyc/sentinel2/nyc_sentinel2_red.tif")
+--------------------------------------------------+-----+
|path |tile |
+--------------------------------------------------+-----+
|/Volumes/.../nyc_sentinel2_red.tif |{...}|
+--------------------------------------------------+-----+
SQL
-- Read raster in SQL (sample-data Volumes path)
SELECT * FROM gdal.`{SAMPLE_RASTER_PATH}` LIMIT 10;
+--------------------------------------------------+-----+
|path |tile |
+--------------------------------------------------+-----+
|/Volumes/.../nyc_sentinel2_red.tif |{...}|
+--------------------------------------------------+-----+
Output Schema
root
|-- tile: struct (GeoBrix raster tile structure)
|-- cellid: bigint (grid cell ID, nullable)
|-- raster: binary (raster file content)
|-- metadata: map<string,string> (driver, extension, etc.)
The tile column contains the complete raster data structure. See Tile Structure for detailed field descriptions.
Named Readers vs GDAL
For common formats, GeoBrix provides named readers for convenience:
# Using named reader (recommended for GeoTIFF)
df = spark.read.format("gtiff_gdal").load("/path/to/file.tif")
# Using GDAL (works but less convenient)
df = spark.read.format("gdal").option("driver", "GTiff").load("/path/to/file.tif")
When to use each:
- Named readers (gtiff_gdal): Better for common formats, cleaner syntax
- GDAL: Useful for less common formats or when you need driver-specific options
Common Raster Formats Explained
GeoTIFF - The Universal Choice
Best for: General-purpose geospatial rasters, aerial imagery, DEMs
GeoTIFF combines TIFF image format with embedded geospatial metadata (coordinate system, geotransform). It's the de facto standard because it's simple, widely supported, and works everywhere. Cloud-Optimized GeoTIFF (COG) adds internal tiling and overviews for efficient cloud storage access.
# Standard GeoTIFF
df = spark.read.format("gtiff_gdal").load("/path/to/elevation.tif")
# Works with COG too
df = spark.read.format("gtiff_gdal").load("s3://bucket/cog-file.tif")
NetCDF - Multi-Dimensional Science Data
Best for: Climate models, oceanographic data, time-series rasters
NetCDF excels at storing multi-dimensional arrays with labeled dimensions (time, latitude, longitude, elevation). Common in scientific computing for weather forecasts, climate projections, and oceanographic measurements.
# NetCDF with multiple variables/subdatasets
df = spark.read.format("gdal") \
.option("driver", "NetCDF") \
.option("readSubdatasets", "true") \
.load("/path/to/climate_model.nc")
HDF5 - Massive Hierarchical Data
Best for: Large scientific datasets, satellite products (MODIS, Sentinel)
HDF5 (Hierarchical Data Format) handles extremely large datasets with complex internal structures. NASA and ESA use it for satellite products. Like NetCDF, it often contains multiple subdatasets.
# HDF5 from satellite products
df = spark.read.format("gdal") \
.option("driver", "HDF5") \
.option("readSubdatasets", "true") \
.load("/path/to/MOD13Q1.hdf")
GRIB/GRIB2 - Weather Models
Best for: Numerical weather prediction, meteorological data
GRIB (GRIdded Binary) is the standard format for weather model outputs from agencies like NOAA, ECMWF. Highly compressed and optimized for meteorological variables. Example uses NOAA HRRR weather data from sample-data (nyc/hrrr-weather).
The hrrr-weather dataset is included in the complete sample-data bundle, not the essential bundle. See Sample data for download options.
# GRIB2 weather data (sample-data HRRR)
df = spark.read.format("gdal") \
.option("driver", "GRIB") \
.load("{SAMPLE_HRRR_PATH}")
+--------------------------------------------------+-----+
|path |tile |
+--------------------------------------------------+-----+
|.../nyc/hrrr-weather/hrrr_nyc_....grib2 |{...}|
+--------------------------------------------------+-----+
Format Selection Guide
| Format | Size | Compression | Multi-Band | Time-Series | Cloud-Friendly |
|---|---|---|---|---|---|
| GeoTIFF | Good | Good | Yes | No | Yes (COG) |
| NetCDF | Excellent | Good | Yes | Yes | Moderate |
| HDF5 | Excellent | Good | Yes | Yes | Poor |
| GRIB2 | Excellent | Excellent | Yes | Yes | Poor |
| JPEG2000 | Excellent | Excellent | Yes | No | Moderate |
| Zarr | Excellent | Good | Yes | Yes | Excellent |
General Rules:
- Start with GeoTIFF (use COG for cloud)
- Use NetCDF for multi-dimensional scientific data
- Use HDF5 when required by data provider (e.g., MODIS)
- Use GRIB2 for weather models
- Use Zarr for cloud-native analysis at scale
Common functions: used vs excluded
See GBX Common Functions for the full catalog of shared file-access primitives. The table below shows which are active in this reader and which are not, and why.
| Common capability | Used here? | How / why |
|---|---|---|
list_local_files (session-free enumeration) | Used | All directory reads — recursive, include_hidden, extensions, path_glob_filter options are routed through this shared predicate. |
enumerate_files (FILE-tier enumeration) | Not in the DataSource | The DataSource is session-less on Connect and does not call enumerate_files; FILE-tier enumeration is only available through gbx_file_read at the function layer. |
gbx_file_read / gbx_file_write (FILE tier) | Not in the DataSource | The DataSource is FUSE-only (session-less on Connect); FILE reads go through gbx_file_read → rst_fromfile at the function layer. |
Advanced: pre-computed tile inputs (manifest / tile index)
The default directory read is fast — a stat-free listing plans ~10k files in ~1.5 s — so most reads need nothing special. The manifest and tilesTable options are an optional optimization for two specific cases:
- Pre-computed windows you already maintain — a manifest or tile index lets the reader use your saved file listing directly, skipping the directory walk entirely.
- Very large tile counts where even a fast listing accumulates across many repeated job runs.
Both options reduce plan time from the directory walk (~1.5 s at 10k files) to a single file or table read. See Benchmarking → Reader plan-time listing for the measured numbers.
manifestoption — supply a JSON or Parquet file listingpath+windowfor each tile; the reader skips the directory walk and header opens entirely.spark.read.format("raster_gbx") \
.option("manifest", "/Volumes/catalog/schema/vol/tiles.json") \
.option("virtualTiles", "true") \
.load("/Volumes/catalog/schema/vol/rasters")tilesTableoption — point to a Delta table that serves as a tile index: one row per tile with apathcolumn (+ optionalwindow/ dimension columns). It is a catalog of file references, not the tile pixels — the reader still opens each referenced file and extracts its window; it only skips discovering the files by walking the directory.spark.read.format("raster_gbx") \
.option("tilesTable", "geospatial.myschema.tile_index") \
.option("virtualTiles", "true") \
.load("/")- Fewer, larger COGs — consolidate small files into Cloud-Optimized GeoTIFFs using the COG Writer; the reader then opens a small number of large files whose headers are comparatively cheap.
A tile index is not a materialized-tile table. manifest / tilesTable catalog where the rasters are (paths, plus optional windows) — essentially a saved file listing. The raster bytes stay in files and are read lazily (especially with virtualTiles=true); the index only lets the reader skip re-walking the directory. That is different from an ingest that has already decoded rasters into a Delta table with a tile-struct/bytes column — if you have that, read it directly with spark.table(...); you don't need a reader at all.
tilesTable auto-order and skipOrdering
When tilesTable is set, the reader auto-orders tiles by source path before planning
partitions. This groups all tiles from the same source GeoTIFF into adjacent partitions, so a
worker that opens a large raster once can decode all its windows without reopening it. For a
table with many tiles per source file (a common windowed-GeoTIFF ingest), this amortization
meaningfully reduces open overhead — each file is opened once per partition instead of once per
tile.
Pass skipOrdering="true" when the table is already physically ordered (e.g. written with
layout="order" or layout="cluster") or when you apply ordering downstream:
spark.read.format("raster_gbx") \
.option("tilesTable", "geospatial.myschema.tile_index") \
.option("skipOrdering", "true") \
.option("virtualTiles", "true") \
.load("/")
Durable co-location: CLUSTER BY + OPTIMIZE
layout="order" (the default write layout) sorts rows by path at insert time, which groups
tiles per source for the lifetime of that write. New inserts and compaction can scatter them.
For a stable layout that survives compaction, use layout="cluster" at write time and run
OPTIMIZE afterward:
from databricks.labs.gbx.ds.file_gbx import gbx_file_write
gbx_file_write(
tiles_df,
target="geospatial.myschema.tile_index",
layout="cluster", # CLUSTER BY path in the DDL
file_mode="auto",
)
-- After initial write (or after bulk inserts), materialize clustering:
OPTIMIZE geospatial.myschema.tile_index;
With layout="cluster", OPTIMIZE physically co-locates tiles from the same source into the
same data files. Subsequent reads with tilesTable have source-adjacent rows from the
start — skipOrdering="true" is safe once the table has been optimized, and it avoids
re-sorting an already-ordered scan.
layout="order" (insert-time ORDER BY path) is the simpler default: no DDL change, no
OPTIMIZE required. It works well for tables that are built once. Use layout="cluster" when
the table is updated incrementally and you run periodic OPTIMIZE as part of maintenance.
Next Steps
- GeoTIFF Reader - Named reader for GeoTIFF format
- Raster Functions - Raster processing operations
- Quick Start - Get started with GeoBrix
Lightweight readers use the shared file_gbx file-access base for FILE / FUSE routing and enumeration — capability tiers, the no-gating rule, and layout options are described there.