Skip to content

feat: add geozarr reader - #908

Merged
vincentsarago merged 19 commits into
mainfrom
feat/geozarr-reader
Apr 21, 2026
Merged

feat: add geozarr reader#908
vincentsarago merged 19 commits into
mainfrom
feat/geozarr-reader

Conversation

@vincentsarago

@vincentsarago vincentsarago commented Apr 11, 2026

Copy link
Copy Markdown
Member
from obstore.store import HTTPStore
import zarr
from zarr.storage import ObjectStore

from rio_tiler.experimental.zarr import GeoZarrReader

store = HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")
zarr_store = ObjectStore(store=store, read_only=True)

group = await zarr.api.asynchronous.open_group(store=zarr_store, mode="r")
ds = GeoZarrReader(input=group)

vars = await ds.variables
vars["b02"]

>> [{'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r10m/b02 shape=(10980, 10980) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 10980,
  'width': 10980,
  'transform': Affine(10.0, 0.0, 600000.0,
         0.0, -10.0, 8000040.0)},
 {'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r20m/b02 shape=(5490, 5490) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 5490,
  'width': 5490,
  'transform': Affine(20.0, 0.0, 600000.0,
         0.0, -20.0, 8000040.0)},
 {'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r60m/b02 shape=(1830, 1830) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 1830,
  'width': 1830,
  'transform': Affine(60.0, 0.0, 600000.0,
         0.0, -60.0, 8000040.0)},
 {'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r120m/b02 shape=(915, 915) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 915,
  'width': 915,
  'transform': Affine(120.0, 0.0, 600000.0,
         0.0, -120.0, 8000040.0)},
 {'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r360m/b02 shape=(305, 305) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 305,
  'width': 305,
  'transform': Affine(360.0, 0.0, 600000.0,
         0.0, -360.0, 8000040.0)},
 {'array': <AsyncArray object_store://HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr/measurements/reflectance")/r720m/b02 shape=(152, 152) dtype=float32>,
  'crs': CRS.from_epsg(32625),
  'height': 152,
  'width': 152,
  'transform': Affine(720.0, 0.0, 600000.0,
         0.0, -720.0, 8000040.0)}]

img = await ds.preview(variables="b02", max_size=128)
img 

ImageData(array=masked_array(
  data=[[[0.853752076625824, 0.7639507055282593, 0.7750680446624756,
          ..., 0.2121896743774414, -0.08321993798017502,
          -0.0998888909816742],
         [0.7293742895126343, 0.7273194789886475, 0.8645895719528198,
          ..., 0.919400691986084, 0.642178475856781, 0.2491092085838318],
         [0.8349562287330627, 0.893208384513855, 0.9366021156311035,
          ..., 0.7356111407279968, 0.6698201894760132,
          0.7265465259552002],
         ...,
         [0.9428333044052124, 0.9167847633361816, 0.9088020920753479,
          ..., --, --, --],
         [1.042079210281372, 0.9973312616348267, 0.9550909996032715,
          ..., --, --, --],
         [0.9809249639511108, 1.0116347074508667, 1.052645206451416,
          ..., --, --, --]]],
  mask=[[[False, False, False, ..., False, False, False],
         [False, False, False, ..., False, False, False],
         [False, False, False, ..., False, False, False],
         ...,
         [False, False, False, ...,  True,  True,  True],
         [False, False, False, ...,  True,  True,  True],
         [False, False, False, ...,  True,  True,  True]]],
  fill_value=np.float64(1e+20),
  dtype=float32), assets=[], bounds=BoundingBox(left=600000.0, bottom=7890600.0, right=709440.0, top=8000040.0), crs=CRS.from_epsg(32625), metadata={}, nodata=None, scales=[1.0], offsets=[0.0], band_names=['b1'], band_descriptions=['b1'], dataset_statistics=None, cutline_mask=None, alpha_mask=None)
``

Comment thread rio_tiler/experimental/zarr.py Outdated
self.height = spatial_shape[0]
self.width = spatial_shape[1]

self.bounds = array_bounds(self.height, self.width, self.transform)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For rio-tiler we need bound and the shape of the dataset to be able to derive the min/max zoom.

This is specific to the tiler usage where the user needs to know where the dataset is and which zoom level they can load the data to before reading the arrays

Comment thread rio_tiler/experimental/zarr.py Outdated
Comment thread rio_tiler/experimental/zarr.py
Comment thread rio_tiler/experimental/zarr.py
Comment thread rio_tiler/experimental/zarr.py Outdated
@vincentsarago

vincentsarago commented Apr 17, 2026

Copy link
Copy Markdown
Member Author

With latest commits we can now open a root zarr store even If there is not geo metadata

the variables= options should now be in form of {group}:{array}

from obstore.store import HTTPStore
import zarr
from zarr.storage import ObjectStore

from rio_tiler.experimental.zarr import GeoZarrReader

from matplotlib.pyplot import imshow

# Create Obstore Store
store = HTTPStore("https://s3.explorer.eopf.copernicus.eu/esa-zarr-sentinel-explorer-fra/tests-output/sentinel-2-l2a/S2B_MSIL2A_20260216T142149_N0512_R096_T25WFV_20260216T165051.zarr")
zarr_store = ObjectStore(store=store, read_only=True)

# # Get Group and Array
geozarr = await zarr.api.asynchronous.open_group(store=zarr_store, mode="r")

reader = GeoZarrReader(geozarr)
variables = await reader.list_variables()
print(variables)
>>> ['measurements/reflectance:b01', 'measurements/reflectance:b02', 'measurements/reflectance:b03', 'measurements/reflectance:b04', 'measurements/reflectance:b05', 'measurements/reflectance:b06', 'measurements/reflectance:b07', 'measurements/reflectance:b08', 'measurements/reflectance:b09', 'measurements/reflectance:b11', 'measurements/reflectance:b12', 'measurements/reflectance:b8a', 'quality/atmosphere/r10m:aot', 'quality/atmosphere/r10m:wvp', 'quality/atmosphere/r20m:aot', 'quality/atmosphere/r20m:wvp', 'quality/atmosphere/r60m:aot', 'quality/atmosphere/r60m:wvp', 'quality/mask/r10m:b02', 'quality/mask/r10m:b03', 'quality/mask/r10m:b04', 'quality/mask/r10m:b08', 'quality/mask/r20m:b05', 'quality/mask/r20m:b06', 'quality/mask/r20m:b07', 'quality/mask/r20m:b11', 'quality/mask/r20m:b12', 'quality/mask/r20m:b8a', 'quality/mask/r60m:b01', 'quality/mask/r60m:b09', 'quality/mask/r60m:b10', 'quality/probability/r20m:cld', 'quality/probability/r20m:snw']


img = await reader.preview(variables=["measurements/reflectance:b02"], max_size=128)

if ":" in variable:
group_name, variable = variable.split(":")[0:2]

return group_name, variable, None

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is prepping for the future if we can select dimensions within a AsyncArray.

we could in theory have variable being set at {group}:{array}:time=slice(0, 1)

@vincentsarago
vincentsarago marked this pull request as ready for review April 21, 2026 13:12
@vincentsarago
vincentsarago merged commit 6d3c5fb into main Apr 21, 2026
7 checks passed
@vincentsarago
vincentsarago deleted the feat/geozarr-reader branch April 21, 2026 14:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants