- Concurrent writers no longer race on creating a group:
get_group(create_mode=True)is a get-or-create, andatomic_add_imagecreates the well group under the plate lock. Two workers adding to the same well could fail withNgioFileExistsError. - Windows: concurrent reads of a metadata file are retried too.
v1.0.0only matched the conflict when the error carried a Win32 code, whichos.replacesets butopen()does not.
atomic_add_image/atomic_remove_imagewarn on Windows that their lock is best-effort:filelockcan hand it to two writers at once, so concurrent ones can lose an update —v1.0.0lost it silently. A single writer is unaffected.- Lock files moved to a
<store>.ngio-locks/directory beside the store, one per group path. Nothing is written inside the Zarr store any more, and groups differing only after a dot (foo.bar,foo.baz) no longer share a lock. A≤1.0.0writer takes the old paths, so upgrade all writers to a plate together.
- Linting moves from
pre-committoprek, a drop-in reimplementation..pre-commit-config.yamlis unchanged apart from dropping aci:block for pre-commit.ci, which was never enabled.pixi run -e dev lintis still the entry point;pre-commit autoupdatebecomesprek auto-update. - CI no longer depends on any Node 20 action, which GitHub now warns about on every run.
pre-commit/actionis maintenance-only and pinsactions/cache@v4internally, so it is replaced byj178/prek-action;JasonEtco/create-an-issueisusing: node20, so the scheduled-failure issue reporter moves to a local composite action at.github/actions/report-failure(and.github/TEST_FAIL_TEMPLATE.mdgoes with it).
First stable release. Everything deprecated in v0.5.0 (each warned "will be removed in ngio=0.6") is now removed — that release became 1.0.0.
| Removed | Use instead |
|---|---|
OmeZarrContainer.image_meta |
.meta |
.levels_paths |
.level_paths |
.set_channel_percentiles(a, b) |
.set_channel_windows_with_percentiles(percentiles=(a, b)) |
version= on the plate/well create and derive functions |
ngff_version= |
check_type= on get_table |
get_table_as(name, TableCls) or get_*_table(name) |
pixel_size=, xy_pixelsize= on create/derive |
pixelsize= (plus z_spacing=, time_spacing=) |
xy_scaling_factor=, z_scaling_factor= |
scaling_factors= |
labels=, channel_labels=, channel_wavelengths=, channel_colors=, channel_active= |
channels_meta= |
wavelength_id=, start=, end=, percentiles=, colors=, active= on set_channel_meta |
channel_meta=ChannelsMeta(...) |
pixelsize is now required on create_empty_ome_zarr and create_ome_zarr_from_array. The pixel_size= argument on the getters is unaffected: pixel_size selects a pyramid level, pixelsize is a value written on create.
Same call, different result:
derive_imageinheritsdtype,dimension_separatorandcompressorsfrom the reference image instead of forcinguint16— deriving from afloat32image no longer silently downcasts it.add_tableandwrite_tablekeep the source table's backend instead of rewriting it asanndata_v1(#207). Passbackend=to convert.- Opening a container no longer reads every pyramid level (
validate_arrays=Falseby default), so a bad array fails on first access rather than at open. open_imageandopen_labeldefault tostrict=False, matching every other getter.list_roi_tablesreturns[]instead of raising when there are no tables.get_masked_label(path=...)resolves the masking label at the label's own pixel size, matchingget_masked_image.PixelSizes with differenttime_units now compare unequal, and==against a non-PixelSizereturnsNotImplemented.
conctatenate_tables → concatenate_tables, set_axes_unit → set_axes_units, levels_paths= → level_paths=, validate_paths= → validate_arrays=, ngio.experimental.iterators → ngio.iterators, and every *_async plate/table function → the sync form with max_workers=.
create_empty_plate(store, ngff_version="0.4") # was version=
create_empty_ome_zarr(store, pixelsize=0.5, # was xy_pixelsize=
scaling_factors=(1.0, 2.0, 2.0)) # was *_scaling_factor=
ome_zarr.get_table_as("roi", RoiTable) # was check_type="roi_table"
ome_zarr.derive_image(store, channels_meta=["DAPI"], pixelsize=(ps.y, ps.x))
ome_zarr.set_channel_meta(channel_meta=ChannelsMeta.default_init(labels=["DAPI"]))
from ngio import SegmentationIterator # was ngio.experimental
plate.get_images(max_workers=8) # was get_images_async()- The iterators are stable API:
from ngio import SegmentationIterator. - Configurable IO retries:
NgioConfig.io_retryplus thengio.utils.retry_iodecorator. ngio's ownNgioErrors are never retried. See the Configuration page. ngio.utils.NgioStorewraps every zarr store ngio opens and applies that retry policy to all IO.ZipStoreis now supported.max_workers=on the sync plate and table APIs replaces the separate async surface;Nonekeeps the serial behaviour.- A larger public namespace, including
MaskedImage,MaskedLabel,Channel,S3FSConfig,derive_ome_zarr_plate,__version__, theget_ngio_*_metareaders and every error class.AbstractBaseTable,ImplementedTablesandwrite_tableare exported fromngio.tables, so a custom table type can be registered without private imports. NgioTableValidationErrornow subclassesNgioValidationError, soexcept ValueErrorcatches it like its siblings; newNgioKeyError.
- Dask writes could silently drop data.
da.store(..., lock=False)let two blocks read-modify-write the same chunk — or shard, when the target is sharded — concurrently, losing one update. This hit every region write that was not chunk-aligned and every sharded target, including pyramid consolidation. Allda.storecalls now share a lock; block compute stays parallel. - Windows: concurrent access to a store no longer fails with
PermissionError: [WinError 5]/[WinError 32]. A concurrent reader ofzarr.jsoncould break a writer's atomic rename; store operations now absorb these transient conflicts with a short bounded retry. No behaviour change on Linux or macOS. import ngiono longer raisesAttributeErrorwhen an s3fs older than 2026.2.0 is installed.concatenate_image_tablesbuilt a wrong index: unnamed, and duplicated undermode="lazy".Roi.union/intersectiondropped ROI name""and label0;Roi.from_valuesnow validates its inputs.- Plate and well metadata
add_*/remove_*mutated the receiver instead of returning a copy. AxesSetup.from_ordered_listsilently dropped a non-canonical axis in some orders.- Grid iterator ROIs now get unique names, and
by_chunkswith overlap ≥ chunk size raisesNgioValueError.
- Ship
src/ngio/py.typed. The PEP 561 marker was missing, so downstream type checkers ignored ngio's annotations. - Real lower bounds on every dependency, exercised by a
test-min-depsCI leg:zarr>=3.1.6,numpy>=2.0,fsspec>=2025.3,anndata>=0.12.5,ome-zarr-models>=1.4and the rest.pandas3.x andanndata0.13 are now allowed, therequires-pythonupper cap is gone, and unusedrequests/distributedare dropped. - New
s3extra:pip install ngio[s3].
- Rebuilt on Zensical with every code block executed at build time, plus new landing, glossary and Configuration pages.
- Fix saving empty tables (#99). Empty tables now round-trip through both backends instead of raising a cryptic pandas error: (1) an empty ROI/masking table (zero ROIs) keeps its schema columns; (2)
_validate_cast_index_dtype_dfcasts an empty index to the requestedstr/inttype instead of rejecting it; (3) an empty ROI table with no backend materializes as an empty table rather than raising; and (4)convert_pandas_to_anndatano longer drops the numeric columns of a zero-row table (it now checks for zero columns rather thanDataFrame.empty, which is also true for a zero-row frame). - Fix
write_tablewriting an empty table (or raisingFileNotFoundError) when given a table returned byopen_tablewhose data had not yet been loaded.write_tablenow materializes the table data before swapping to the destination backend, mirroringTablesContainer.add, so a table opened from one store can be copied to another viawrite_tablewith its data intact. - Fix tables with missing (
None/NaN) values in string columns failing to serialize to the AnnData backend. The_check_for_mixed_typesand_check_for_supported_typesguards now ignore missing values and classify a column from its non-null contents, so a string column containingNone(or an all-missing column) is accepted — matching AnnData's native handling, which stores such columns as a categorical withNaNfor the missing entries. This surfaced when copying a condition table (e.g.get_table+add_table), which re-serializes through the default AnnData backend. Note: the round trip normalizes missing-containing string columns fromobject/Nonetocategory/NaN.
- Add a global
NgioConfig/get_config()configuration system, loaded from~/.ngio_config.jsonby default or a path set via theNGIO_CONFIG_PATHenv var (.jsonfile). Both are exported from the top-levelngiopackage. - Add configurable s3fs retry handling:
NgioConfig.s3fs.custom_retry_markerslists error substrings that trigger a retry via a customs3fs.set_custom_error_handler, applied through the newngio.utils.refresh_s3fs_config(). The motivating use case is AWS clock-skew errors, but any error substring can be configured.
- Migrate the S3 store test harness from a
moto[server]subprocess toaiomotoin server mode (aiomoto[pandas]in thetestextra). This also fixes a CI import crash on Python 3.13/3.14:aiomotocapsaiobotocore/motoand floorss3fs, so the universal (multi-platform) solve no longer backtrackss3fsto the ancient0.4.2(which lacksset_custom_error_handlerand crashedimport ngioat module load). CSV and Parquet table backends now round-trip on the S3 store under the mock.
- Fix
derive_label(andOmeZarrContainer.derive_label) rejecting an explicitshapethat omits the channel axis. When the reference image has acaxis andchannels_policyremoves or overrides it ("squeeze","singleton", or an integer), the up-front shape-length check failed before the channel policy was applied. The provided shape is now normalized to the reference dimensionality before pyramid computation, so a channel-less shape (e.g.(z, y, x)for a(c, z, y, x)image) is accepted.channels_policy="same"still requires the full shape. - Fix
is_group_listablewrongly reportingTruefor stores that cannot actually be listed (e.g. HTTP hosts without a directory index): zarr >= 3.1.6 swallows the listing error onFsspecStoreand yields an empty listing instead of raising. The check now verifies that the group's own metadata document (zarr.json/.zgroup) — which must exist for any group that was successfully opened — appears in the store listing, distinguishing a broken listing from a genuinely empty group on any store type. copy_groupnow raises an error when the source listing does not contain the group's metadata document, instead of silently producing an empty copy from a non-listable store.
- Harden GitHub Actions and scan workflows through
zizmor. - Rename the
pre-commitpixi dev task tolint: the old name shadowed thepre-commitbinary inpixi run, and its trailinggit add -usilently staged the working tree and masked hook failures in the task's exit code.
- Fix loading v0.4 HCS plates where the
versionkey is absent from the plate-level metadata: the v0.4. V0.4 decoder now explicitly inject the version into the plate dict before constructingPlateWithVersion, so missing orNoneversion values no longer cause a validation error.
- Remove redundant
versionfield fromNgioPlateMeta: the field is now a@computed_fieldproperty that delegates toself.plate.version, eliminating the need to keep two copies of the NGFF version in sync. The public.versionattribute andmodel_dump()output are unchanged.
- Remove eager uniqueness check on
wavelength_idinChannelsMeta.default_init: duplicatewavelength_idvalues are now allowed at creation time.get_channel_idxraises a clear error if a lookup by an ambiguouswavelength_idis attempted, directing users to select by label instead.
- Replace
da.to_zarrwithda.store(..., lock=False)in pyramid writes (_on_disk_dask_zoom,_on_disk_coarsen) and region slice writes (_ops_slices). Dask >=2025.11'sto_zarrre-derives chunks vianormalize_chunks(chunks="auto", ...)and emits aPerformanceWarning(treated as error by ngio's filterwarnings) when the result is not a multiple of the target's chunks;da.storewrites blocks 1:1. - Copy object/string-dtype zarr arrays directly when consolidating groups: dask >=2025.11 raises
NotImplementedErrorfrom auto-chunking for these dtypes, so they bypass dask and are copied via numpy. - Set
auto_shard_zarr_v3together withzarr_write_formatonanndata's global settings via a new_update_anndata_global_settingshelper, so reading/writing tables works correctly when mixing zarr v2 and v3 in the same session on anndata 0.12.
- Pin
anndatato>=0.12.0,<0.13.0. - Unpin
dask(remove the<2025.11.0upper bound introduced in v0.4.5).
- Fix AnnData reading over HTTP when directory listing is disabled: skip optional Zarr groups (
uns,obsm,varm, etc.) that cannot be discovered without listing. - Fix
ngff_versionnot being propagated when deriving a plate:derive_plate()andderive_ome_zarr_plate()now defaultngff_versiontoNoneand inherit the source plate's version when no version is explicitly provided.
- Change tolerance when converting Roi to pixel coordinates to avoid machine precision dependent rounding issues.
- Improve testing for ZoomTransform.
- Remove broad warnings filter for all tests.
- Replace custom logger warnings with standard Python warnings for better integration with user applications.
- Add docstrings to
ChannelSelectionModelto allow for correct json schema generation.
- Fix translation check in
_ngio_to_v04_multiscaleand_ngio_to_v05_multiscale: translations were incorrectly dropped when all values were negative or when positive and negative values cancelled out. - Fix shape compatibility check in
_check_compatibility_of_shapes: integer indices in the slicing tuple now correctly reduce the expected shape rank instead of inserting a spurious size-1 dimension.
Roinow supports dict-like slice access:roi["x"]returns the slice for axis"x"and raisesKeyErrorif the axis is not present.Roi.get(axis_name, default=None)now accepts an explicitdefaultvalue, following thedict.getconvention.- New
Roi.update_slice(name, new_slice)method: replaces the slice for an existing axis or appends a new one. Returns a newRoiinstance. - New
Roi.remove_slice(name)method: removes the slice for a named axis. Returns a newRoiinstance. RaisesNgioValueErrorif the axis is not present.
- Pin
mkdocsto version <2.0 to avoid build errors in CI due to breaking changes in mkdocs v2, and incompatibility with material design theme.
- Remove file locking remove in
ZarrGroupHandler, which was not used anywhere and is unnecessary in new lockfile release. - Correctly set Zarr array dtype to array dtype in
create_ome_zarr_from_array
- Fix bug in AnnData backend where "raw" entry with encoding-type "null" is written by default in newer anndata versions, which causes compatibility issues with older anndata versions. Now the "raw" entry is removed after writing if it has encoding-type "null".
- Fix critical bug in masking roi image handling causing incorrect results when image and mask have different pixel sizes.
- Fix bug in loading masking roi images when paths other than default are used.
- Fix bug causing incorrect channel metadata when creating an image.
- Fix correctly setting the space and time units when creating an image.
- Fix minor bug in
set_channel_windows_with_percentilesmethod.
- Improve logging consistency across the codebase.
- Add support for OME-NGFF v0.5
- Move to zarr-python v3
- API to delete labels and tables from OME-Zarr containers and HCS plates.
- Allow to explicitly set axes order when building masking roi tables.
- New metadata modification APIs for
Image,Label, andOmeZarrContainer:set_channel_labels- Update channel labelsset_channel_colors- Update channel colorsset_channel_windows- Update channel display windows (start/end values)set_channel_windows_with_percentiles- Update display windows based on data percentilesset_axes_names- Rename axes in the metadataset_axes_unit- Set space and time units for axesset_name- Set the image/label name in metadata
- Add translation support in all image/label creation and derivation APIs.
- New
Roimodels, now supporting arbitrary axes. - The
compressorargument has been renamed tocompressorsin all relevant functions and methods to reflect the support for multiple compressors in zarr v3. - The
versionargument has been renamed tongff_versionin all relevant functions and methods to specify the OME-NGFF version. - Remove the
parallel_safeargument from all zarr related functions and methods. The locking mechanism is now handled internally and only depends on thecache. - Remove the unused
parentargument fromZarrGroupHandler. - Internal changes to
ZarrGroupHandlerto support cleanup unused apis. - Remove
ngio_loggerin favor of standard warnings module.
The Roi class now uses a flexible slice-based model supporting arbitrary axes:
# Old (v0.4)
roi = Roi(x=34.1, y=10, x_length=321.6, y_length=330)
# New (v0.5)
roi = Roi.from_values(slices={"x": (34.1, 321.6), "y": (10, 330)}, name=None)
# Accessing coordinates
# Old: roi.x, roi.y, roi.x_length, roi.y_length
# New: roi.get("x").start, roi.get("y").start, roi.get("x").length, roi.get("y").length# compressor → compressors
# Old (v0.4)
create_empty_ome_zarr(..., compressor=Blosc())
# New (v0.5)
create_empty_ome_zarr(..., compressors=Blosc())
# version → ngff_version
# Old (v0.4)
create_empty_ome_zarr(..., version="0.4")
# New (v0.5)
create_empty_ome_zarr(..., ngff_version="0.4")parallel_safe: No longer needed, locking is handled internallyngio_logger: Use Python's standardwarningsmodule instead
- Standardized all deprecation warnings to indicate removal in
ngio=0.6. - Deprecated
set_channel_percentilesmethod, useset_channel_windows_with_percentilesinstead.
- Fix bug in
consolidatefunction when using coarsening mode with non power-of-two shapes. - Fix HCS plate column name formatting to use standardized zero-padding (e.g., column
3is now stored as"03"). - Fix
_stringify_columnnot passingnum_digitsparameter to_format_int_column.
- Fix incorrect and incomplete docstrings across the codebase:
compute_masking_roi: Added Args/Returns, fixed description (supports 2D, 3D, 4D).lazy_compute_slices: Added Args/Returns sections.LabelsContainer.list: Fixed description (was "Create the /labels group").build_masking_roi_table: Added Args/Returns sections.TablesContainer: Fixed class and method descriptions (were referencing labels instead of tables).NgioPlateMeta.add_well: Fixed description (was "Add an image to the well").NgioPlateMeta.derive: Fixed type annotation in docstring (NgffVersion→NgffVersions).- Added missing docstrings to several HCS helper functions.
- Fix bug adding time axis to masking roi tables.
- Fix channel selection from
wavelength_id - Fix table opening mode to stop writing groups when opening in append mode.
- Pin Dask to version <2025.11 to avoid errors when writing zarr pyramids with dask (see dask/dask#12159 (comment))
- Fix bug in channel visualization when using hex colors with leading '#'.
- Remove strict range check in channel window.
- Fix bug in deriving labels and image from OME-Zarr with non standard path names.
- Add missing pillow dependency.
- Update pixi workspace config.
- Make roi.to_slicing_dict(pixel_size) always require pixel_size argument for consistency with other roi methods.
- Make PixelSize object a Pydantic model to allow for serialization.
- Improve robustness when rounding Rois to pixel coordinates.
- Fix bug in zoom transform when input axes contain unknown axes (e.g. virtual axes). Now unknown axes are treated as virtual axes and set to 1 in the target shape.
- Add Iterators for image processing pipelines
- Add support for time in rois and roi-tables
- Building masking roi tables expanded to time series data
- Add zoom transformation
- Add support for rescaling on-the-fly masks for masked images
- Big refactor of the io pipeline to support iterators and lazy loading
- Add support for customize dimension separators and compression codecs
- Simplify AxesHandler and Dataset Classes
- The image-like
get_*api have been slightly changed. Now if a single int is passed as slice_kwargs, it is interpreted as a single index. So the dimension is automatically squeezed. - Remove the
get_*_delayedmethods, now data cam only be loaded as numpy or dask array.Use theget_as_daskmethod instead, which returns a dask array that can be used with dask delayed. - A new model for channel selection is available. Now channels can be selected by name, index or with
ChannelSelectionModelobject. - Change
table_namekeyword argument tonamefor consistency in all table concatenation functions, e.g.concatenate_image_tables,concatenate_image_tables_as, etc. - Change to
Dimensionclass.get_shapeandget_canonical_shapehave been removed,getuses new keyword argumentsdefaultinstead ofstrict. - Image like objects now have a more clean API to load data. Instead of
get_arrayandset_array, they now useget_as_numpy, andget_as_daskfor delayed arrays. - Also for
get_roinow specific methods are available. For ROI objects, theget_roi_as_numpy, andget_roi_as_daskmethods. - Table ops moved to
ngio.images - int
labelas an explicit attribute inRoiobjects (previously only in stored in name and relying on convention) - Slight changes to
ImageandLabelobjects. Some minor attributes have been renamed for consistency.
- Add
t_secondandlen_t_secondto ROI tables and masking ROI tables
- Remove path normalization for images in wells. While the spec requires paths to be alphanumeric, this patch removes the normalization to allow for arbitrary image paths.
- allow to write as
anndata_v1for backward compatibility with older ngio versions.
- improve dataset download process and streamline the CI workflows
- change table backend default to
anndata_v1for backward compatibility. This will be chaanged again when ngiov0.2.xis no longer supported.