Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Basal Melt GeoZarr Conversion

ESA

In order to display zarr files you hvae to manually build the pyramids similar to a cog file. You also have to link all of the data together, so that I viauliser keeps track of the projections and crs metadata. A common specification to work on this is GeoZarr - https://geozarr.org/ . Which is a composition of other existing conventions. To create a valid geozarr file we will follow the example of eopf-zarr. To convert a zarr file to a geozarr you have to :

  • Converert the dataset zarr into a tree, that represents the data at different scales

  • Specify how the groups link to each other

  • Specify the projection and transformation information for every grouop

We will follow the eopf-geozarr tree structure so that we can use the new open layers visualisations. This structure has a nested data tree structure where a dataset such as measurement/reflectance is a group in the tree has spatial, multiscale and projection infomation. Then the different subgroups are scaled versions of an original dataset, and each subgroup has only spatial information. The highest resolution subgroup is basically the same as teh orignal zarr file. We will use the basal_melt zarr file as an example.

Loading...

This example uses the basal melt zarr created in the other tutorials, all source variables use the x/y spatial dimensions, EPSG:3031, metre coordinates, 512 pixel chunks, and pyramid levels [1, 2, 4, 8, 16].

The output is an EOPF-style GeoZarr hierarchy :

/basal_melt
/basal_melt/0
/basal_melt/1
/basal_melt/2
/basal_melt/3
/basal_melt/4

Open the source Zarr

The input store already has x, y, one or more data variables, and spatial_ref. We open it lazily with xarray, attach the known CRS at dataset level, find every variable that uses the spatial dimensions, and convert the encoded nodata value to NaN before downsampling. That way coarsen(...).mean(skipna=True) ignores nodata pixels for every spatial variable.

Loading...
{'source': '/home/krasen/tiling_test/downloaded_data/basal_melt_map_racmo_firn_air_corrected_cog.zarr', 'data_variables': ['basal_melt'], 'spatial_data_variables': ['basal_melt'], 'shape': [6435, 6120], 'crs': 'EPSG:3031', 'bounds': [-3060277.25, -3217740.25, 3059722.75, 3217259.75], 'resolution': [1000.0, -1000.0], 'nodata_written': -9999.0}

Build the pyramid levels

The native dataset is level 0. Each following level halves the previous level in both spatial dimensions. Using Dataset.coarsen(...).mean(skipna=True) averages all spatial variables together and keeps their coordinate arrays aligned.

[{'level': 0, 'factor': 1, 'variables': ['basal_melt'], 'shape': [6435, 6120], 'bounds': [-3060277.25, -3217740.25, 3059722.75, 3217259.75], 'resolution': [1000.0, -1000.0]}, {'level': 1, 'factor': 2, 'variables': ['basal_melt'], 'shape': [3217, 3060], 'bounds': [-3060277.25, -3216740.25, 3059722.75, 3217259.75], 'resolution': [2000.0, -2000.0]}, {'level': 2, 'factor': 4, 'variables': ['basal_melt'], 'shape': [1608, 1530], 'bounds': [-3060277.25, -3214740.25, 3059722.75, 3217259.75], 'resolution': [4000.0, -4000.0]}, {'level': 3, 'factor': 8, 'variables': ['basal_melt'], 'shape': [804, 765], 'bounds': [-3060277.25, -3214740.25, 3059722.75, 3217259.75], 'resolution': [8000.0, -8000.0]}, {'level': 4, 'factor': 16, 'variables': ['basal_melt'], 'shape': [402, 382], 'bounds': [-3060277.25, -3214740.25, 3051722.75, 3217259.75], 'resolution': [16000.0, -16000.0]}]

Write the GeoZarr hierarchy

Each pyramid level is written as its own Zarr group under /basal_melt. The whole xarray Dataset is written at each level, so all source variables travel together with x, y, and spatial_ref metadata.

Most of the spatial metadata comes directly from rioxarray: rio.transform(), rio.bounds(), and the xarray dimension sizes.

[{'path': 'basal_melt/0', 'factor': 1, 'variables': ['basal_melt', 'spatial_ref'], 'shape': [6435, 6120], 'transform': [1000.0, 0.0, -3060277.25, 0.0, -1000.0, 3217259.75], 'bbox': [-3060277.25, -3217740.25, 3059722.75, 3217259.75]}, {'path': 'basal_melt/1', 'factor': 2, 'variables': ['basal_melt', 'spatial_ref'], 'shape': [3217, 3060], 'transform': [2000.0, 0.0, -3060277.25, 0.0, -2000.0, 3217259.75], 'bbox': [-3060277.25, -3216740.25, 3059722.75, 3217259.75]}, {'path': 'basal_melt/2', 'factor': 4, 'variables': ['basal_melt', 'spatial_ref'], 'shape': [1608, 1530], 'transform': [4000.0, 0.0, -3060277.25, 0.0, -4000.0, 3217259.75], 'bbox': [-3060277.25, -3214740.25, 3059722.75, 3217259.75]}, {'path': 'basal_melt/3', 'factor': 8, 'variables': ['basal_melt', 'spatial_ref'], 'shape': [804, 765], 'transform': [8000.0, 0.0, -3060277.25, 0.0, -8000.0, 3217259.75], 'bbox': [-3060277.25, -3214740.25, 3059722.75, 3217259.75]}, {'path': 'basal_melt/4', 'factor': 16, 'variables': ['basal_melt', 'spatial_ref'], 'shape': [402, 382], 'transform': [16000.0, 0.0, -3060277.25, 0.0, -16000.0, 3217259.75], 'bbox': [-3060277.25, -3214740.25, 3051722.75, 3217259.75]}]

Add parent multiscale metadata

The parent /basal_melt group links the resolution groups together. It stores the GeoZarr conventions, the multiscale layout, the source file path, the source data variable names, and the CRS/spatial metadata shared by the pyramid.

{'zarr_conventions': [{'uuid': 'd35379db-88df-4056-af3a-620245f8e347', 'schema_url': 'https://raw.githubusercontent.com/zarr-conventions/multiscales/refs/tags/v1/schema.json', 'spec_url': 'https://github.com/zarr-conventions/multiscales/blob/v1/README.md', 'name': 'multiscales', 'description': 'Multiscale layout of zarr datasets'}, {'uuid': '689b58e2-cf7b-45e0-9fff-9cfc0883d6b4', 'schema_url': 'https://raw.githubusercontent.com/zarr-conventions/spatial/refs/tags/v1/schema.json', 'spec_url': 'https://github.com/zarr-conventions/spatial/blob/v1/README.md', 'name': 'spatial:', 'description': 'Spatial coordinate information'}, {'uuid': 'f17cb550-5864-4468-aeb7-f3180cfb622f', 'schema_url': 'https://raw.githubusercontent.com/zarr-experimental/geo-proj/refs/tags/v1/schema.json', 'spec_url': 'https://github.com/zarr-experimental/geo-proj/blob/v1/README.md', 'name': 'proj:', 'description': 'Coordinate reference system information for geospatial data'}], 'multiscales': {'layout': [{'asset': '0', 'spatial:shape': [6435, 6120], 'spatial:transform': [1000.0, 0.0, -3060277.25, 0.0, -1000.0, 3217259.75]}, {'asset': '1', 'spatial:shape': [3217, 3060], 'spatial:transform': [2000.0, 0.0, -3060277.25, 0.0, -2000.0, 3217259.75], 'derived_from': '0', 'transform': {'scale': [2.0, 2.0], 'translation': [0.0, 0.0]}}, {'asset': '2', 'spatial:shape': [1608, 1530], 'spatial:transform': [4000.0, 0.0, -3060277.25, 0.0, -4000.0, 3217259.75], 'derived_from': '1', 'transform': {'scale': [2.0, 2.0], 'translation': [0.0, 0.0]}}, {'asset': '3', 'spatial:shape': [804, 765], 'spatial:transform': [8000.0, 0.0, -3060277.25, 0.0, -8000.0, 3217259.75], 'derived_from': '2', 'transform': {'scale': [2.0, 2.0], 'translation': [0.0, 0.0]}}, {'asset': '4', 'spatial:shape': [402, 382], 'spatial:transform': [16000.0, 0.0, -3060277.25, 0.0, -16000.0, 3217259.75], 'derived_from': '3', 'transform': {'scale': [2.0, 2.0], 'translation': [0.0, 0.0]}}], 'resampling_method': 'average'}, 'title': 'basal_melt GeoZarr pyramid', 'source_file': '/home/krasen/tiling_test/downloaded_data/basal_melt_map_racmo_firn_air_corrected_cog.zarr', 'data_variables': ['basal_melt'], 'spatial:dimensions': ['y', 'x'], 'spatial:bbox': [-3060277.25, -3217740.25, 3059722.75, 3217259.75], 'spatial:transform_type': 'affine', 'spatial:registration': 'pixel', 'proj:code': 'EPSG:3031'}

Validate using the geozarr_toolkit

GeoZarr multiscale group: /basal_melt
Detected conventions: ['spatial', 'proj', 'multiscales']

Parent multiscale group validation:
  [PASS] spatial
  [PASS] proj
  [PASS] multiscales
  [PASS] zarr_conventions

Resolution group validation:

/basal_melt/0
  detected: ['spatial', 'proj']
  [PASS] spatial
  [PASS] proj
  [PASS] zarr_conventions

/basal_melt/1
  detected: ['spatial', 'proj']
  [PASS] spatial
  [PASS] proj
  [PASS] zarr_conventions

/basal_melt/2
  detected: ['spatial', 'proj']
  [PASS] spatial
  [PASS] proj
  [PASS] zarr_conventions

/basal_melt/3
  detected: ['spatial', 'proj']
  [PASS] spatial
  [PASS] proj
  [PASS] zarr_conventions

/basal_melt/4
  detected: ['spatial', 'proj']
  [PASS] spatial
  [PASS] proj
  [PASS] zarr_conventions
/tmp/ipykernel_77256/1472056715.py:1: RuntimeWarning: Failed to open Zarr store with consolidated metadata, but successfully read with non-consolidated metadata. This is typically much slower for opening a dataset. To silence this warning, consider:
1. Consolidating metadata in this existing store with zarr.consolidate_metadata().
2. Explicitly setting consolidated=False, to avoid trying to read consolidate metadata, or
3. Explicitly setting consolidated=True, to raise an error in this case instead of falling back to try reading non-consolidated metadata.
  display(xr.open_datatree(OUTPUT_ZARR))
Loading...

Visualising GeoZarr data

To actually visualise the data you need a viewer that reads geozarr datasets such as open payers 10.8 or above. And you need to serve the files through http or s3. So, youll need:

1. Upload the data to an s3 server, or simulate an http server locally using python. For example run a script like this in the directory with the geozarr file:

from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer

class CORSRequestHandler(SimpleHTTPRequestHandler):
    def end_headers(self):
        self.send_header("Access-Control-Allow-Origin", "*")
        self.send_header("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS")
        self.send_header("Access-Control-Allow-Headers", "Range, Content-Type")
        self.send_header("Access-Control-Expose-Headers", "Content-Length, Content-Range, Accept-Ranges")
        super().end_headers()

    def do_OPTIONS(self):
        self.send_response(204)
        self.end_headers()

if __name__ == "__main__":
    server = ThreadingHTTPServer(("localhost", 8000), CORSRequestHandler)
    print("Serving with CORS at http://localhost:8000")
    server.serve_forever()

2. Create an html file that creates the open layers Geozar layer viewer that points to the file. For example, something that contains:

 const source = new GeoZarr({
      url: GEOZARR_URL,
      bands: [{name: BAND_NAME, group: DATA_GROUP}],
      projection,
      transition: 0,
    });

    const geozarrLayer = new WebGLTileLayer({
      source,
      opacity: OPACITY,
      style: basalMeltStyle(),
    });

    const graticule = new Graticule({
      strokeStyle: new Stroke({color: "rgba(31, 45, 56, 0.24)", width: 1}),
      showLabels: true,
      wrapX: false,
    });

    const map = new Map({
      target: "map",
      layers: [createAntarcticBasemap(), geozarrLayer, graticule],
      view: getView(
        source,
        withLowerResolutions(1),
        withHigherResolutions(2),
        withExtentCenter(),
        withZoom(1.71),
      ),
    });