Zarr.NET  0.6.1
Zarr reader and writer in .NET
Loading...
Searching...
No Matches
Zarr.NET Documentation

Zarr.NET

A high-performance, fully async C# library for reading and writing OME-Zarr datasets with comprehensive support for multiscale images, labels, and High-Content Screening (HCS) plate data.

Features

Zarr v2 & v3 Support - Automatic version detection and handling
OME-Zarr 0.4 & 0.5 - Full spec compliance for multiscale images, labels, and HCS plates
Remote Access - Read from HTTP/HTTPS, S3
Physical Coordinates - ROI reading in real-world units (micrometers, seconds, etc.)
Compression - Blosc, Gzip, Zstandard (zstd) codec support
Memory Efficient - Chunk-level reading with streaming support
Type Safe - Strongly typed metadata models and coordinate transformations
Cross-Platform - .NET 10.0, works on Windows, Linux, macOS
Well-Architected - Clean separation of concerns, testable, extensible

Installation

# Via NuGet
dotnet add package ZarrNET

# Or clone and build locally
git clone https://github.com/BiologyTools/Zarr.NET.git
cd ZarrNET
dotnet build

Dependencies

Quick Start

Read a single 2D plane from a multiscale image

using ZarrNET;
using ZarrNET.Helpers;

await using var reader = await OmeZarrReader.OpenAsync("/path/to/dataset.zarr");
var image = reader.AsMultiscaleImage();
var level = await image.OpenResolutionLevelAsync(datasetIndex: 0);

// Read timepoint 0, channel 1, z-slice 5
var plane = await level.ReadPlaneAsync(t: 0, c: 1, z: 5);

// Get as typed 2D array
ushort[,] pixels = plane.As2DArray<ushort>();  // for uint16 data
Console.WriteLine($"Plane: {plane.Width}x{plane.Height}, max value: {pixels.Cast<ushort>().Max()}");

// Or get as byte array for interop
byte[] bytes = plane.ToBytes<ushort>(PixelFormat.Gray8);

Read using physical coordinates (ROI in microns)

using ZarrNET.Coordinates;

// Define ROI: 100µm x 100µm region at specific location
var roi = new PhysicalROI(
    origin: [0, 0, 5.0, 50.0, 50.0],    // t, c, z, y, x in physical units
    size:   [1, 1, 1.0, 100.0, 100.0]   // timepoint, channel, z-slice, 100µm x 100µm
);

var result = await level.ReadRegionAsync(roi);

Read from remote HTTP/S3 URLs

// Public HTTP server
var httpUrl = "https://example.com/datasets/image.zarr";
await using var reader = await OmeZarrReader.OpenAsync(httpUrl);

// S3 public bucket
var s3Url = "https://s3.amazonaws.com/bucket/image.zarr";
await using var s3Reader = await OmeZarrReader.OpenAsync(s3Url);

// Works exactly the same as local files
var image = reader.AsMultiscaleImage();
var plane = await level.ReadPlaneAsync(t: 0, c: 0, z: 0);

Custom HTTP configuration (auth, timeouts)

using OmeZarr.Core.Zarr.Store;

var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("Authorization", "Bearer YOUR_TOKEN");
httpClient.Timeout = TimeSpan.FromMinutes(10);

var store = new HttpZarrStore("https://example.com/data.zarr", httpClient);
await using var reader = await OmeZarrReader.OpenAsync(store);
var plate = reader.AsPlate();
Console.WriteLine($"Plate: {plate.PlateMetadata.Name}");
Console.WriteLine($"Wells: {plate.Wells.Count}");

// Open well B3, field 0
var well = await plate.OpenWellAsync("B", "3");
var field = await well.OpenFieldAsync(0);
var level = await field.OpenResolutionLevelAsync(0);

// Read a plane from the field
var plane = await level.ReadPlaneAsync(c: 0, z: 0);

Work with labels (segmentation masks)

var image = reader.AsMultiscaleImage();

if (await image.HasLabelsAsync())
{
    var labelGroup = await image.OpenLabelsAsync();
    Console.WriteLine($"Available labels: {string.Join(", ", labelGroup.LabelNames)}");

    var cellLabel = await labelGroup.OpenLabelAsync("cells");
    var labelLevel = await cellLabel.OpenResolutionLevelAsync(0);

    var labelPlane = await labelLevel.ReadPlaneAsync(t: 0, c: 0, z: 0);
    var labelIds = labelPlane.As1DArray<uint>();  // typically uint32

    var uniqueCells = labelIds.Distinct().Count(id => id != 0);
    Console.WriteLine($"Detected {uniqueCells} cells");
}

Architecture

The library is structured in clean, composable layers:

┌─────────────────────────────────────────────────────┐
│ OmeZarrReader (Public API)                          │
├─────────────────────────────────────────────────────┤
│ Node Tree Layer                                      │
│  - MultiscaleNode, PlateNode, WellNode, FieldNode   │
│  - ResolutionLevelNode, LabelNode                   │
├─────────────────────────────────────────────────────┤
│ Coordinate Transform Layer                          │
│  - PhysicalROI ↔ PixelRegion                        │
│  - CoordinateTransformService                       │
├─────────────────────────────────────────────────────┤
│ OME-Zarr Metadata Layer                             │
│  - MultiscaleMetadata, PlateMetadata, etc.          │
│  - OmeAttributesParser                              │
├─────────────────────────────────────────────────────┤
│ Zarr Array/Group Layer                              │
│  - ZarrArray (chunk reading, region extraction)     │
│  - ZarrGroup (tree navigation)                      │
├─────────────────────────────────────────────────────┤
│ Codec Pipeline                                       │
│  - BytesCodec, GzipCodec, ZstdCodec                 │
│  - CodecPipeline (ordered application)              │
├─────────────────────────────────────────────────────┤
│ Store Layer (I/O)                                    │
│  - IZarrStore, LocalFileSystemStore                 │
└─────────────────────────────────────────────────────┘

Design Principles

Supported Features

Data Types

The lower-level dtype parser also understands the core fixed-size integer and floating-point Zarr dtypes, including v2 NumPy dtype strings such as "<f4" / "<f8" / "<c8" / "<c16" and v3 data type names such as "float32" / "float64" / "complex64" / "complex128".

Compression

OME-Zarr Node Types

Zarr Versions

API Overview

Core Classes

OmeZarrReader

Entry point for opening datasets. Auto-detects node type.

await using var reader = await OmeZarrReader.OpenAsync(path);
var root = reader.OpenRoot();  // Auto-dispatch to correct type

// Or strongly-typed access
var image = reader.AsMultiscaleImage();
var plate = reader.AsPlate();
var well = reader.AsWell();

ResolutionLevelNode

Represents a single resolution level in a multiscale pyramid.

var level = await image.OpenResolutionLevelAsync(datasetIndex: 0);

// Properties
long[] shape = level.Shape;          // [t, c, z, y, x]
string dtype = level.DataType;       // "uint16", "float32", etc.
double[] pixelSize = level.GetPixelSize();  // Physical size per pixel

// Reading
var plane = await level.ReadPlaneAsync(t: 0, c: 0, z: 5);
var roi = await level.ReadRegionAsync(physicalROI);
var region = await level.ReadPixelRegionAsync(pixelRegion);

// Writing
await level.WriteRegionAsync(pixelRegion, data);

Low-level chunk API

For larger-than-memory and chunk-native workflows, open the underlying Zarr array and work directly with full chunks.

await using var store = new LocalFileSystemStore("/path/to/data.zarr");
var root = await ZarrGroup.OpenRootAsync(store);
var array = await root.OpenArrayAsync("0");

await foreach (var chunk in array.EnumerateChunksAsync())
{
    // Decoded path: full logical chunk buffer in array order.
    byte[] decoded = await array.ReadChunkDecodedAsync(chunk);
    await array.WriteChunkDecodedAsync(chunk, decoded);

    // Encoded path: copy compressed bytes when metadata/codecs are compatible.
    byte[]? encoded = await array.ReadChunkEncodedAsync(chunk);
    if (encoded is not null)
        await array.WriteChunkEncodedAsync(chunk, encoded);
}

ZarrChunkRef.Shape gives the valid in-array extent for edge chunks. Decoded chunk buffers are padded to the full effective chunk shape, matching the region reader's fill-value behaviour. Encoded chunk access is available for non-sharded arrays; sharded arrays store logical chunks inside shard objects.

PlaneResult

Result of reading a 2D plane with convenience extraction methods.

var plane = await level.ReadPlaneAsync(t: 0, c: 0, z: 0);

int width = plane.Width;
int height = plane.Height;

// Extract as typed arrays
ushort[,] pixels2D = plane.As2DArray<ushort>();
ushort[] pixels1D = plane.As1DArray<ushort>();

// Convert to specific formats
byte[] gray8 = plane.ToBytes<ushort>(PixelFormat.Gray8);
byte[] bgra32 = plane.ToBytes<ushort>(PixelFormat.Bgra32);

PhysicalROI and PixelRegion

Coordinate representations for ROI specification.

// Physical coordinates (microns, seconds, etc.)
var physicalROI = new PhysicalROI(
    origin: [0, 0, 5.0, 100.0, 200.0],
    size:   [1, 1, 2.0, 50.0, 50.0]
);

// Pixel coordinates (array indices)
var pixelRegion = new PixelRegion(
    start: [0, 0, 10, 100, 200],
    end:   [1, 1, 12, 150, 250]
);

Examples

Iterate through a Z-stack

var axes = level.Multiscale.Axes;
var zIndex = Array.FindIndex(axes, a => a.Name.Equals("z", StringComparison.OrdinalIgnoreCase));
var numSlices = (int)level.Shape[zIndex];

for (int z = 0; z < numSlices; z++)
{
    var plane = await level.ReadPlaneAsync(t: 0, c: 0, z: z);
    var pixels = plane.As1DArray<ushort>();

    double meanIntensity = pixels.Average(p => (double)p);
    Console.WriteLine($"Z={z}: mean intensity = {meanIntensity:F1}");
}

Read all channels as separate planes

var cIndex = Array.FindIndex(axes, a => a.Name.Equals("c", StringComparison.OrdinalIgnoreCase));
var numChannels = (int)level.Shape[cIndex];

var channels = new List<ushort[,]>();
for (int c = 0; c < numChannels; c++)
{
    var plane = await level.ReadPlaneAsync(t: 0, c: c, z: 0);
    channels.Add(plane.As2DArray<ushort>());
}

// Create RGB composite, max projection, etc.

Choose optimal resolution level for display

var levels = await image.OpenAllResolutionLevelsAsync();
double targetMicronsPerPixel = 0.5;

var spatialAxisIndex = 3;  // y axis in t,c,z,y,x
var bestLevel = levels
    .Select((l, i) => (level: l, index: i, pixelSize: l.GetPixelSize()[spatialAxisIndex]))
    .OrderBy(l => Math.Abs(l.pixelSize - targetMicronsPerPixel))
    .First();

Console.WriteLine($"Using level {bestLevel.index} ({bestLevel.pixelSize:G4} µm/px)");

Process an entire HCS plate

var plate = reader.AsPlate();

foreach (var wellRef in plate.Wells)
{
    var well = await plate.OpenWellAsync(wellRef.Path);

    foreach (var fieldRef in well.Fields)
    {
        var field = await well.OpenFieldAsync(fieldRef.Path);
        var level = await field.OpenResolutionLevelAsync(0);

        // Process each field
        var plane = await level.ReadPlaneAsync(c: 0, z: 0);
        // ... analyze, segment, etc.
    }
}

Save plane as image file (using ImageSharp)

using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

var plane = await level.ReadPlaneAsync(t: 0, c: 0, z: 5);
byte[] pixels = plane.ToBytes<ushort>(PixelFormat.Gray8);

var img = Image.LoadPixelData<L8>(pixels, plane.Width, plane.Height);
img.SaveAsPng("output.png");

Performance Considerations

Chunk-Aligned Reads

For best performance, align ROI boundaries to chunk boundaries when possible:

var chunkShape = level.ZarrArray.Metadata.ChunkShape;  // e.g., [1, 1, 1, 96, 96]

// Good: aligned to chunk boundaries (multiples of 96)
var alignedROI = new PixelRegion(
    start: [0, 0, 0, 0, 0],
    end:   [1, 1, 1, 96, 192]  // 1x2 chunks
);

// Less efficient: crosses chunk boundaries
var unalignedROI = new PixelRegion(
    start: [0, 0, 0, 50, 50],
    end:   [1, 1, 1, 150, 150]  // touches 4 chunks
);

Resolution Level Selection

Use lower resolution levels for overview/navigation, full resolution for analysis:

// Level 0: Full resolution (slow, detailed)
// Level 3: 1/8 resolution (fast, overview)
var overview = await image.OpenResolutionLevelAsync(datasetIndex: 3);
var fullRes = await image.OpenResolutionLevelAsync(datasetIndex: 0);

Memory Management

For large datasets, process in tiles rather than loading entire planes:

int tileSize = 512;
for (int y = 0; y < height; y += tileSize)
{
    for (int x = 0; x < width; x += tileSize)
    {
        var tileRegion = new PixelRegion(
            start: [0, 0, 0, y, x],
            end:   [1, 1, 1, Math.Min(y + tileSize, height), Math.Min(x + tileSize, width)]
        );

        var tile = await level.ReadPixelRegionAsync(tileRegion);
        // Process tile...
    }
}

Limitations

Roadmap

Contributing

Contributions welcome! Please:

  1. Follow the existing code style (variable staging pattern, clear separation of concerns)
  2. Add tests for new features
  3. Update documentation
  4. Open an issue first for major changes

License

Acknowledgments

Support

Please use image.sc forum for discussion.

Built with ❤️ for the scientific imaging community