Skip to content

Data Model

Data Entities

Root

The project's root. This is the entry point for the copick API. It allows access to information about the pickable objects and runs contained in the project.

Example Code - Print available objects and runs
"""Print all objects and runs in a copick project."""

import copick

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# List all available objects
obj_info = [(o.name, o.label) for o in root.pickable_objects]

print("Pickable objects in this project:")
for name, label in obj_info:
    print(f"  {name}: {label}")

# Execute a function on each run in the project
runs = root.runs

print("Runs in this project:")
for run in runs:
    print(f"Run: {run.name}")
    # Do something with the run

Refer to the API Reference for more information on the CopickRoot API.

Pickable Object

Objects are any entity that can be labeled inside a 3D image using points, meshes or dense segmentation masks. In most cases, these will be macromolecular complexes or other cellular structures, like membranes. They can also be more abstract entities like "contamination particles", "carbon edges", or "sample boundaries".

In the configuration file, each object is defined by a JSON object, that allows the user to specify the object's name, label, color, radius, and other properties.

Naming Conventions

Object names should never contain underscores!

Example Code - Read an object's density map.
"""Read a density map from an object's zarr-store into a numpy array."""

import copick
import numpy as np
import zarr

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the object named 'proteasome'
proteasome = root.get_object("proteasome")

# Read the density map for the object from its zarr-store
zarr_array = zarr.open(proteasome.zarr())["0"]
density_map = np.array(zarr_array)

Refer to the API Reference for more information on the CopickObject API.

Example Object Definition

The following is an example of a pickable object definition in the configuration file:

{
    "name": "proteasome",
    "is_particle": true,
    "pdb_id": "3J9I",
    "emdb_id": "1234",
    "identifier": "GO:0001234",
    "label": 1,
    "color": [255, 0, 0, 255],
    "radius": 60,
    "map_threshold": 0.0418,
    "metadata": {
        "source": "experimental_data",
        "confidence": 0.95,
        "notes": "High-resolution structure"
    }
}

  • name: The name of the object, which should be unique within one project.
  • is_particle: A boolean indicating whether the object can be represented by point annotations. By default, all objects can be represented by mesh annotations or dense segmentations.

  • pdb_id: The PDB ID of the object, if available.

  • emdb_id: The EMDB ID of the object, if available.
  • identifier: An ontology/database identifier for the object, if available. Supported namespaces are GO, UniProtKB, CHEBI, PDB (note the dash separator, e.g. PDB-1BXN), UBERON, CL, and CDPO. When using the data portal, this field is matched exactly against the annotation object_id to find matching annotations in the data portal.
  • label: An integer that indicates which numeric label should be used in segmentations to represent this object.
  • color: An array of four integers that represent the RGBA color of the object when rendered in a 3D viewer.
  • radius: An integer that represents the radius of the object in angstroms. This is used to determine the size of the object when rendering it as a sphere in a 3D viewer.
  • map_threshold: A float that represents the threshold value to use when a density map is used to represent the object. This is used to determine the isosurface level to use when rendering the object as a mesh. Density maps are discovered by the copick API by looking for files with the same name as the object in the Objects directory of the project's root.
  • metadata: An optional dictionary that can contain arbitrary key-value pairs for storing additional custom information about the object. This field allows users to attach project-specific metadata such as confidence scores, data sources, or processing notes. The key copick is reserved for copick's own extensions of the object definition (see Filament Objects below).

Filament Objects

A filament is a continuous, typically helical or tubular assembly, such as a microtubule or an actin filament, that is annotated with ordered points along its axis. An object is declared a filament by a FilamentSpec stored in its metadata under metadata["copick"]["filament"]:

{
    "name": "microtubule",
    "is_particle": true,
    "label": 2,
    "radius": 120,
    "metadata": {
        "copick": {
            "filament": {"polar": true, "helical_rise_a": 9.4}
        }
    }
}
  • polar: Whether the structure has a polarity (true for microtubules and actin). Omitted if not stated.
  • helical_rise_a, helical_twist_deg: Rise per subunit in angstrom and twist per subunit in degrees. Both are descriptive; tools never use them as a default sampling distance.

A filament object must have is_particle: true, because filaments are annotated with points; its radius is the tube radius. Copick validates the declaration when it reads the configuration. Clients that do not know filaments read the object as an ordinary particle, and keep the declaration when they rewrite the configuration because it lives in metadata.

root.new_object(name="microtubule", is_particle=True, radius=120, filament={"polar": True})
root.get_object("microtubule").is_filament  # True
copick add object -c config.json --name microtubule --object-type filament --radius 120 --polar

Picks of a filament object follow the filament conventions: instance_id is the filament ID, points are ordered along each filament, and the transform's +Z axis is the filament axis.

Run

A run is a collection of data that is associated with a particular location on the sample. Run objects allow access to any 3D image data, segmentations, and annotations that are associated with a particular location on the sample. Images are stored in groups based on their voxel spacing, while point annotations, mesh annotations, and dense segmentations are related to the run as a whole.

Example Code - List available segmentations for a run
"""Print all segmentations for a run in a copick project."""

import copick

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the run named 'TS_001'
run = root.get_run("TS_001")

# List all available segmentations for the run
segmentations = run.segmentations
for segmentation in segmentations:
    print(f"Segmentation: {segmentation.name}")

Refer to the API Reference for more information on the CopickRun API.

Voxel Spacing

A voxel spacing groups together all tomograms of a particular resolution. Voxel spacings are rounded to the third decimal place.

Example Code - List available tomograms for a voxel spacing
"""Print the list of tomograms for a given voxel spacing."""

import copick

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the run named 'TS_001'
run = root.get_run("TS_001")

# Get the voxel spacing with a resolution of 10 angstroms
voxel_spacing = run.get_voxel_spacing(10.000)

# List all available tomograms for the voxel spacing
tomograms = voxel_spacing.tomograms
for tomogram in tomograms:
    print(f"Tomogram: {tomogram.name}")

Refer to the API Reference for more information on the CopickVoxelSpacing API.

Image data

Tomogram

At each resolution, multiple tomograms can be stored. Tomograms are stored as OME-NGFF files, which are a zarr-based format that allows for efficient access to multiscale 3D image data. The filename of the zarr file allows relating the image to its reconstruction method or processing steps. Typical useful tomogram types are wbp, sirt, denoised, etc.

Example Code - Read a tomogram into a numpy array
"""Read a tomogram from a zarr-store into a numpy array."""

import copick
import numpy as np
import zarr

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the run named 'TS_001'
run = root.get_run("TS_001")

# Get the voxel spacing with a resolution of 10 angstroms
voxel_spacing = run.get_voxel_spacing(10.000)

# Get the tomogram named 'wbp'
tomogram = voxel_spacing.get_tomogram("wbp")

# Read the tomogram from its zarr-store
# Scale "0" is the unbinned tomogram
zarr_array = zarr.open(tomogram.zarr())["0"]
tomogram_data = np.array(zarr_array)

# Scale "1" is the tomogram binned by 2
zarr_array_bin2 = zarr.open(tomogram.zarr())["1"]
tomogram_data_bin2 = np.array(zarr_array_bin2)

Refer to the API Reference for more information on the CopickTomogram API.

Example tomogram file name

Tomograms are named according to the following pattern:

wbp.zarr

The wbp part of the filename is the type of tomogram. This could be wbp, sirt, denoised, etc.

Feature Map

Feature maps are stored as OME-NGFF files with relation to the tomogram they are computed from. Feature maps are stored as zarr files, and can be used to store any type of data that is computed from a tomogram. They may be useful for interactive segmentation tasks.

Example Code - Read a feature map into a numpy array
"""Read a feature map from a zarr-store into a numpy array."""

import copick
import numpy as np
import zarr

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the run named 'TS_001'
run = root.get_run("TS_001")

# Get the voxel spacing with a resolution of 10 angstroms
voxel_spacing = run.get_voxel_spacing(10.000)

# Get the tomogram named 'wbp'
tomogram = voxel_spacing.get_tomogram("wbp")

# Get the feature map named 'sobel'
feature_map = tomogram.get_features("sobel")

# Read the feature map from its zarr-store
zarr_array = zarr.open(feature_map.zarr())["0"]
feature_map_data = np.array(zarr_array)

Refer to the API Reference for more information on the CopickFeatures API.

Example feature map file name

Feature maps are named according to the following pattern:

wbp_density_features.zarr

The wbp part of the filename is the type of tomogram that the feature map was computed from. The sobel part of the filename is the type of feature that the feature map represents. This could be density, gradient, curvature, etc.

Annotation data

Point Annotations

Point annotations are stored as JSON files in the Picks directory of the run. Each file contains a list of points in angstrom coordinates that represent the location of a particular object in the tomogram. The filename of the JSON file allows relating the points to the user or tool that created them, as well as the object that they represent.

Naming Conventions

user_ids, session_ids, and object names should never contain underscores!

Example Code - Read point annotations from copick
"""Read points from a CopickPicks object."""

import copick
import numpy as np

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the first run in the project
run = root.runs[0]

# Get 'proteasome' picks of user 'alice'
picks = run.get_picks(object_name="proteasome", user_id="alice")[0]

# Get the points from the picks
point_arr = np.ndarray((len(picks.points), 3))
for idx, pt in enumerate(picks.points):
    point_arr[idx, :] = [pt.location.x, pt.location.y, pt.location.z]

Refer to the API Reference for more information on the CopickPicks API.

Example point file name

Point files are named according to the following pattern:

good.picker_0_proteasome.json

The good.picker part of the filename is the user or tool that created the points. The 0 part of the filename is the session id of the user or tool that created the points. The proteasome part of the filename is the name of the object that the points represent.

Each point has a location (angstrom), a transformation (a 4×4 matrix from the object's frame to the tomogram, whose translation is a shift added to the location), an instance_id (default 0) and a score (default 1.0). For filament objects, instance_id is the filament ID and the points are ordered along each filament; see Pick Geometry.

positions, transforms = picks.numpy()            # locations and 4x4 transforms
centres = picks.full_positions()                 # location + transform translation
picks.from_numpy(positions, transforms, instance_ids=ids, scores=scores)
ids, scores = picks.instance_ids(), picks.scores()

Mesh Annotations

Mesh annotations are stored as glb files in the Meshes directory of the run. Each file contains a 3D mesh, with vertices in angstrom coordinates, that represents the shape of a particular object in the tomogram. The filename of the glb file allows relating the mesh to the user or tool that created it, as well as the object that it represents.

Naming Conventions

user_ids, session_ids, and object names should never contain underscores!

Example Code - Read mesh annotations and visualize them in 3D
"""Read a mesh from a CopickMesh object and display it."""

import copick

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the first run in the project
run = root.runs[0]

# Get a membrane mesh from user 'bob'
mesh = run.get_meshes(object_name="membrane", user_id="bob")[0]

# Show the mesh
mesh.mesh.show()

Refer to the API Reference for more information on the CopickMesh API.

Example mesh file name

Mesh files are named according to the following pattern:

good.picker_0_proteasome.glb

The good.picker part of the filename is the user or tool that created the mesh. The 0 part of the filename is the session id of the user or tool that created the mesh. The proteasome part of the filename is the name of the object that the mesh represents.

Filaments

Traced filaments are stored as JSON files in the Filaments directory of the run, named like point annotations ([user_id]_[session_id]_[object_name].json). Each file holds the filaments of one object, each an ordered centreline (polyline) in angstrom coordinates with its own instance_id:

{
    "pickable_object_name": "microtubule",
    "user_id": "tracer",
    "session_id": "1",
    "voxel_spacing": 10.0,
    "unit": "angstrom",
    "version": 1,
    "filaments": [
        {"instance_id": 1, "points": [[100.0, 200.0, 50.0], [110.0, 200.5, 50.2], ...],
         "polarity_known": false, "score": 1.0, "radius": 120.0, "metadata": {}}
    ]
}
  • instance_id is at least 1 and unique within the file; picks sampled from a filament use it as their instance_id.
  • points are ordered along the filament, at least two, and should be no further apart than the voxel spacing the filament was traced at, so that linear interpolation follows the centreline.
  • polarity_known says whether the point order follows the structure's polarity (meaningful for objects whose filament spec has polar: true). Where it is true, the points run from the minus end to the plus end (microtubules), or from the pointed end to the barbed end (actin), so the +Z axis of a pick sampled in point order points toward the plus (barbed) end.
  • version is the version of this file format.

RELION export (copick export picks --output-format star) takes each filament pick's polarity from the filament with the same instance_id: in the Filaments under the picks' own URI by default, or in those named by --filaments-uri when the picks were sampled into another session. rlnAnglePsiFlipRatio is 0 for picks of filaments with polarity_known and 0.5 for the others. A pick whose filament is missing from those Filaments is an error, as is a --filaments-uri that matches no Filaments in a run. Picks without any Filaments (none under their own URI, and no --filaments-uri) all get 0.5.

The cryoET Data Portal has no filament annotations, so data-portal projects keep filaments in their overlay; a self-contained Croissant project (Mode A) cannot hold them.

filaments = run.new_filaments(object_name="microtubule", user_id="tracer", session_id="1")
filaments.from_numpy([centreline_1, centreline_2], voxel_spacing=10.0)  # (M, 3) arrays in angstrom
for line in run.get_filaments(object_name="microtubule")[0].numpy():
    ...
Editable curves

A filament can also carry the curve its points were made from, so that an editor can reopen the clicked control points or a fitting program's spline instead of re-deriving them from the dense points. The curve is an optional curve object on a filament, beside points; points stay the centreline every reader uses, and are regenerated from the curve whenever it changes.

{"instance_id": 1,
 "points": [[100.0, 200.0, 50.0], "... regenerated from the curve ..."],
 "curve": {"kind": "catmull-rom", "alpha": 0.5, "step": 10.0,
           "control_points": [[100.0, 200.0, 50.0], [180.0, 230.0, 55.0], [260.0, 240.0, 70.0]]}}
Field Kinds Rule
kind all catmull-rom (passes through its control points; interactive tracing), linear, or bspline (a fitted B-spline, stored exactly). Other values are reserved and kept as they are.
control_points all Ångström, ordered like points, finite. catmull-rom/linear: at least 2, consecutive points distinct. bspline: the coefficients, at least degree + 1.
step all > 0 Å: the spacing of the regenerated points, usually the voxel spacing.
alpha catmull-rom 0 to 1, default 0.5 (centripetal).
degree, knots bspline Degree 1 to 5; len(control_points) + degree + 1 non-decreasing knots, clamped (the end values repeat exactly degree + 1 times, interior values at most degree times).
smoothing bspline Optional, the fit's smoothing factor (scipy's s); descriptive.

Evaluating. linear joins the control points with straight segments. catmull-rom is the Barry–Goldman form of the Catmull-Rom spline with knot intervals |P_{i+1} − P_i|^alpha, and end points extended by 2·P_0 − P_1 and 2·P_n − P_{n−1}; it passes through every control point. bspline is evaluated with de Boor's algorithm over the domain [u_degree, u_n], exactly as scipy's splev evaluates a splprep fit; it starts and ends at its first and last coefficient.

Regenerating points. The curve is cut into pieces at its anchors: the control points for catmull-rom and linear, and the curve at each distinct knot for bspline. Each piece is sampled at m = max(16, ceil(16·ℓ/step)) equal parameter steps (ℓ: the segment's length, or the length of the span's control polygon), starting exactly at its anchor and ending at the next. Its length L along those samples is then split into N = max(1, ceil(L/step − 1e-9)) equal arc-length steps. The pieces' points are joined, and the curve's end is appended. So consecutive points are at most step apart and every anchor is one of the points. The reference implementation is copick.util.filaments, and tests/data/filament_curves.json holds reference cases that other implementations reproduce to within 10⁻³ Å.

Writers regenerate points whenever they change the curve, remove curve when they change points any other way, and reverse both together. copick does not store a curve of a known kind that no longer describes its points: it drops it, with a warning. Fitting programs store their spline as a bspline (CopickFilamentCurve.from_tck), scaling the coefficients if they fitted voxel coordinates, and record their settings in the filament's metadata["fit"].

Readers that do not edit ignore curve. A curve is current when the first and last points, and every anchor, lie within 0.01·step of the points; an editor only reuses a current curve. Without one, it derives catmull-rom control points from the points: starting from the two ends, it repeatedly adds the point farthest from the curve through the chosen ones until none is farther than the tolerance (half the voxel spacing by default; copick.util.filaments.control_points_from_polyline). An editor that does not support bspline, or needs to insert or delete one of its points, converts it the same way.

filaments = run.new_filaments(object_name="microtubule", user_id="tracer", session_id="1")
filaments.from_control_points([clicks_1, clicks_2], voxel_spacing=10.0)  # (n, 3) control points in angstrom

curve = filaments.editable_curve(instance_id=1)  # the stored curve, or one derived from the points
edited = filaments.get(1).with_control_points(moved_control_points)
filaments.filaments = [edited, filaments.get(2)]
filaments.store()

from scipy.interpolate import splprep
from copick.models import CopickFilamentCurve

tck, _ = splprep(centreline_voxels.T, s=5.0, k=3)
fitted = CopickFilamentCurve.from_tck(tck, step=10.0, smoothing=5.0, scale=10.0)
run.new_filaments(object_name="microtubule", user_id="fitter", session_id="0").from_curves([fitted])

Refer to the API Reference for the CopickFilaments API.

Dense Segmentations

Dense segmentations are stored as OME-NGFF files in the run. The directory and the filename of the zarr file relate the segmentation to the user or tool that created it and to what it represents, and record which of four types it is:

Type Directory Name Voxel values Filename ends in
binary Segmentations a pickable object 1 inside the object, 0 outside _[object_name].zarr
multilabel Segmentations any descriptive name the label of the pickable object at that voxel, 0 for background _[name]-multilabel.zarr
instance InstanceSegmentations a pickable object the ID of the object instance at that voxel, 0 for background _[object_name].zarr
panoptic PanopticSegmentations any descriptive name two channels: the object label and the instance ID within that object _[name].zarr

Types added after binary and multilabel live in their own directory, so copick versions and other clients that predate them never list them, instead of misreading their voxel values.

An instance segmentation holds every instance of one object, for example every microtubule or every vesicle in a run, and tells them apart: voxel value v > 0 belongs to the instance with instance_id == v. These are the same IDs that picks and filaments carry, so picks or filaments describing the same instances (from the same user and session) use the same IDs. 0 is background, matching picks, where instance_id 0 means "no instance".

seg = run.new_segmentation(10.0, "microtubule", "1", user_id="tracer", is_instance=True)
seg.from_numpy(labels)              # integer volume, 0 = background, n = instance n
seg.instance_ids()                  # array([1, 2, ...])
run.get_segmentations(is_instance=True)

A panoptic segmentation combines both in one volume of shape (2, Z, Y, X), with a leading OME-NGFF axis of type channel (scale [1, voxel, voxel, voxel]):

  • channel 0, label: the label of the pickable object at each voxel, 0 for background (the multilabel convention);
  • channel 1, instance: the instance ID within that object. IDs are per object, so a segment is the pair (label, instance ID): ribosome 1 and vesicle 1 are different segments, and each object's IDs match its picks, filaments and instance segmentations. Instance 0 on a labelled voxel is a region of that object that is not split into instances (a membrane, the cytosol); background voxels carry no instance ID.
seg = run.new_segmentation(10.0, "cell", "1", user_id="tracer", is_panoptic=True)
seg.from_numpy(np.stack([labels, instance_ids]))   # (2, Z, Y, X)
seg.numpy(channel="label")                          # (Z, Y, X)
seg.instance_ids(object_name="microtubule")        # IDs where the label is microtubule's
seg.segments()                                      # [("membrane", 0), ("microtubule", 1), ...]

Writes check that every label is 0 or a pickable object's label, and that no background voxel carries an instance ID. MRC, TIFF and EM hold one volume each, so a panoptic segmentation is exported to them one channel at a time.

Queries that do not name a type select binary and multilabel segmentations, the types every client reads: get_segmentations(...) and delete_segmentations(...) default to is_instance=False, is_panoptic=False (pass None for any type), and run.segmentations lists all of them. Segmentation URIs mark the type with ?multilabel=true, ?instance=true or ?panoptic=true (microtubule:tracer/1@10.0?instance=true); a URI without one selects binary and multilabel segmentations, so copick cp, mv, rm and export do too. copick sync segmentations and copick stats segmentations take --segmentation-type binary|multilabel|instance|panoptic|all.

Data types

Segmentation values are stored without loss. When no dtype is given, from_numpy chooses the smallest unsigned integer type that holds every value: uint8 and up for binary and multilabel segmentations, uint16 and up for instance and panoptic segmentations. A value that does not fit a requested dtype raises an error instead of wrapping.

Naming Conventions

user_ids, session_ids, and object names should never contain underscores! Object names should also not end in -multilabel, which marks a multilabel segmentation in a filename.

Example Code - Read a segmentation into a numpy array
"""Read a segmentation from a CopickSegmentation object."""

import copick
import numpy as np
import zarr

# Initialize the root object from a configuration file
root = copick.from_file("path/to/config.json")

# Get the first run in the project
run = root.runs[0]

# Get 'proteasome' segmentation of user 'alice'
segmentation = run.get_segmentations(name="proteasome", user_id="alice")[0]

# Get the segmentation array from the segmentation
seg_zarr = zarr.open(segmentation.zarr())["0"]
seg = np.array(seg_zarr)

Refer to the API Reference for more information on the CopickSegmentation API.

Example segmentation file names

Segmentation files are named according to the following pattern:

10.000_good.picker_0_proteasome.zarr

The 10.000 part of the filename is the voxel spacing of the tomogram that the segmentation was created from. The good.picker part of the filename is the user or tool that created the segmentation. The 0 part of the filename is the session id of the user or tool that created the segmentation. The proteasome part of the filename is the name of the object that the segmentation represents. This is a binary segmentation.

10.000_good.picker_0_segmentation-multilabel.zarr

The 10.000 part of the filename is the voxel spacing of the tomogram that the segmentation was created from. The good.picker part of the filename is the user or tool that created the segmentation. The 0 part of the filename is the session id of the user or tool that created the segmentation. The segmentation part of the filename is an arbitrary name that describes the segmentation. This is a multilabel segmentation, thus all objects in the project could be represented in this segmentation.

InstanceSegmentations/10.000_tracer_1_microtubule.zarr

An instance segmentation of the microtubule object by the tool tracer in session 1: each microtubule's voxels hold its instance ID.

cryoET Data Portal

In data-portal projects, the portal's SegmentationMask annotations appear as binary segmentations and its volumetric InstanceSegmentationMask annotations as instance segmentations (user data-portal, session = the annotation file ID). The portal's point-based InstanceSegmentation annotations are not segmentations and are not shown.

On-disk Data Model

The on-disk data model of copick is as follows:

📁 copick_root
├─ 📄 copick_config.json
├─ 📁 Objects
│  └─ 📄 [pickable_object_name].zarr
└─ 📁 ExperimentRuns
   └─ 📁 [run_name] (index: src/io/copick_models.py:CopickPicks.runs)
      ├─ 📁 VoxelSpacing[xx.yyy]/
      │  ├─ 📁 [tomotype].zarr/
      │  │  └─ [OME-NGFF spec at 100%, 50% and 25% scale]
      │  └─ 📁 [tomotype]_[feature_type]_features.zarr/
      │     └─ [OME-NGFF spec at 100% scale]
      ├─ 📁 VoxelSpacing[x2.yy2]/
      │  ├─ 📁 [tomotype].zarr/
      │  │  └─ [OME-NGFF spec at 100%, 50% and 25% scale]
      │  └─ 📁 [tomotype]_[feature_type]_features.zarr/
      │     └─ [OME-NGFF spec at 100% scale]
      ├─ 📁 Picks/
      │  └─ 📄 [user_id | tool_name]_[session_id | 0]_[object_name].json
      ├─ 📁 Meshes/
      │  └─ 📄 [user_id | tool_name]_[session_id | 0]_[object_name].glb
      ├─ 📁 Filaments/
      │  └─ 📄 [user_id | tool_name]_[session_id | 0]_[object_name].json
      ├─ 📁 Segmentations/
      │  ├─ 📁 [xx.yyy]_[user_id | tool_name]_[session_id | 0]_[object_name].zarr
      │  │   └─ [OME-NGFF spec at 100% scale, 50% and 25% scale]
      │  └─ 📁 [xx.yyy]_[user_id | tool_name]_[session_id | 0]_[name]-multilabel.zarr
      │      └─ [OME-NGFF spec at 100% scale, 50% and 25% scale]
      ├─ 📁 InstanceSegmentations/
      │  └─ 📁 [xx.yyy]_[user_id | tool_name]_[session_id | 0]_[object_name].zarr
      │      └─ [OME-NGFF spec at 100% scale, 50% and 25% scale]
      └─ 📁 PanopticSegmentations/
         └─ 📁 [xx.yyy]_[user_id | tool_name]_[session_id | 0]_[name].zarr
             └─ [OME-NGFF spec, axes c/z/y/x, at 100% scale, 50% and 25% scale]