From a7c1112ea5536f5962b62e2baf68f1cc5d8626b4 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:07:25 +1100 Subject: [PATCH 01/10] Make tensorstore optional, rename to open_zarr --- src/zarpaint/__init__.py | 4 +-- src/zarpaint/_tests/test_copy_data.py | 6 ++-- src/zarpaint/_zarpaint.py | 52 ++++++++++++++++++--------- src/zarpaint/reader.py | 6 ++-- 4 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/zarpaint/__init__.py b/src/zarpaint/__init__.py index 90821a2..9019c1f 100644 --- a/src/zarpaint/__init__.py +++ b/src/zarpaint/__init__.py @@ -3,7 +3,7 @@ except ImportError: __version__ = "unknown" -from ._zarpaint import create_labels, open_tensorstore +from ._zarpaint import create_labels, open_zarr from ._dims_chooser import DimsSorter, set_axis_labels from ._watershed import watershed_split from ._add_3d_points import add_points_3d_with_alt_click @@ -12,7 +12,7 @@ __all__ = [ 'create_labels', - 'open_tensorstore', + 'open_zarr', 'DimsSorter', 'set_axis_labels', 'watershed_split', diff --git a/src/zarpaint/_tests/test_copy_data.py b/src/zarpaint/_tests/test_copy_data.py index ad6b670..9d56a15 100644 --- a/src/zarpaint/_tests/test_copy_data.py +++ b/src/zarpaint/_tests/test_copy_data.py @@ -1,8 +1,6 @@ -from napari.layers import Labels from zarpaint import copy_data import numpy as np -import tensorstore as ts -from zarpaint import open_tensorstore +from zarpaint import open_zarr import zarr def test_copy_data(make_napari_viewer): @@ -19,7 +17,7 @@ def test_copy_data(make_napari_viewer): def test_copy_data_tensorstore(make_napari_viewer, tmp_path): viewer = make_napari_viewer() labels_layer1 = viewer.add_labels(np.random.randint(0, 2**23, size=(10, 20, 30))) - array2 = open_tensorstore(tmp_path/"example.zarr", shape=(2, 10, 20, 30), chunks=(1, 1, 20, 30)) + array2 = open_zarr(tmp_path / "example.zarr", shape=(2, 10, 20, 30), chunks=(1, 1, 20, 30)) labels_layer2 = viewer.add_labels(array2) viewer.dims.set_point(axis=0, value=1) widget = copy_data() diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index 9c0cab1..e4f7fd2 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -10,7 +10,11 @@ import numpy as np import pathlib from pathlib import Path -import tensorstore as ts +try: + import tensorstore as ts + tensorstore_available = True +except ModuleNotFoundError: + tensorstore_available = False import zarr import toolz as tz @@ -34,8 +38,7 @@ def _on_create_labels_init(widget): def create_ts_meta(labels_file: pathlib.Path, metadata): """Create bespoke metadata yaml file within zarr array.""" fn = os.path.join(labels_file, '.naparimeta.yml') - with open(fn, 'w') as fout: - for key, val in metadata.items(): + with open(fn, 'w') as fout: for key, val in metadata.items(): if type(val) == np.ndarray: if np.issubdtype(val.dtype, np.floating): metadata[key] = list(map(float, val)) @@ -54,7 +57,25 @@ def open_ts_meta(labels_file: pathlib.Path) -> dict: return meta -def open_tensorstore(labels_file: pathlib.Path, *, shape=None, chunks=None): +def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): + """Open a zarr file, with tensorstore if available, with zarr otherwise. + + If the file doesn't exist, it is created. + + Parameters + ---------- + labels_file : Path + The output file name. + shape : tuple of int + The shape of the array. + chunks : tuple of int + The chunk size of the array. + + Returns + ------- + data : ts.Array or zarr.Array + The array loaded from file. + """ if not os.path.exists(labels_file): zarr.open( str(labels_file), @@ -74,38 +95,35 @@ def open_tensorstore(labels_file: pathlib.Path, *, shape=None, chunks=None): dir, name = os.path.split(labels_file) labels_ts_spec = { 'driver': 'zarr', - 'kvstore': { - 'driver': 'file', - 'path': dir, - }, + 'kvstore': {'driver': 'file', 'path': dir}, 'path': name, 'metadata': metadata, } - data = ts.open(labels_ts_spec, create=False, open=True).result() + if tensorstore_available: + data = ts.open(labels_ts_spec, create=False, open=True).result() + else: + data = labels_temp return data @magic_factory( labels_file={'mode': 'w'}, widget_init=_on_create_labels_init, - ) -def create_labels( + )def create_labels( source_image: napari.layers.Image, labels_file: pathlib.Path, chunks='', ) -> napari.types.LayerDataTuple: """Create/load a zarr array as a labels layer based on image layer. - Parameters - ---------- + Parameters ---------- source_image : Image layer The image that we are segmenting. labels_file : pathlib.Path The path to the zarr file to be created. chunks : str, optional A string that can be evaluated as a tuple of ints specifying the chunk - size for the zarr file. If empty, they will be (128, 128) along the - last dimensions and (1) along any remaining dimensions. This argument + size for the zarr file. If empty, they will be (128, 128) along the last dimensions and (1) along any remaining dimensions. This argument has no effect if the file already exists. """ if chunks: @@ -120,7 +138,7 @@ def create_labels( else: # use default chunks = (1,) * (source_image.ndim - 2) + (128, 128) - layer_data = open_tensorstore( + layer_data = open_zarr( labels_file, shape=source_image.data.shape, chunks=chunks, @@ -384,4 +402,4 @@ def _save(self, viewer): # Split Objects -# ------------- +# ------------- \ No newline at end of file diff --git a/src/zarpaint/reader.py b/src/zarpaint/reader.py index 3c71953..bb72825 100644 --- a/src/zarpaint/reader.py +++ b/src/zarpaint/reader.py @@ -2,12 +2,10 @@ import os import pathlib -from ._zarpaint import open_tensorstore, open_ts_meta +from ._zarpaint import open_zarr, open_ts_meta def zarr_tensorstore(path: str | pathlib.Path): if (str(path).endswith('.zarr') and os.path.isdir(path) and '.zarray' in os.listdir(path)): - return lambda path: [ - (open_tensorstore(path), open_ts_meta(path), 'labels') - ] + return lambda p: [(open_zarr(p), open_ts_meta(path), 'labels')] From 96c9bee9368bd1d2e32ef199617d6377143a378a Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:07:50 +1100 Subject: [PATCH 02/10] Make tensorstore an optional dependency --- setup.cfg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index b330af1..ac3c224 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,6 @@ install_requires = qtpy scipy scikit-image - tensorstore toolz zarr @@ -56,5 +55,8 @@ testing = pytest-qt napari[pyqt5] +all = + tensorstore + [options.package_data] zarpaint = napari.yaml From 0358eb14905eb1cf711541cf64bdffae865bbe92 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:08:02 +1100 Subject: [PATCH 03/10] Remove unused imports --- src/zarpaint/_zarpaint.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index e4f7fd2..cc0f2b5 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -1,15 +1,11 @@ -from __future__ import annotations - import ast import os import yaml from magicgui import magic_factory -import dask.array as da import napari import numpy as np import pathlib -from pathlib import Path try: import tensorstore as ts tensorstore_available = True @@ -46,7 +42,6 @@ def create_ts_meta(labels_file: pathlib.Path, metadata): metadata[key] = list(map(int, val)) yaml.dump(metadata, fout) - def open_ts_meta(labels_file: pathlib.Path) -> dict: """Open bespoke metadata yaml file within zarr array, if present.""" fn = os.path.join(labels_file, '.naparimeta.yml') From 8ce40fe38c477c401113f70cf040ff4cf798f76d Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:08:36 +1100 Subject: [PATCH 04/10] Add docstring and simplify code --- src/zarpaint/_zarpaint.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index cc0f2b5..f2477b1 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -17,13 +17,15 @@ @tz.curry def _set_default_labels_path(widget, source_image): - if (hasattr(source_image, 'source') # napari <0.4.8 - and source_image.source.path is not None): + """Helper function to set the default output path next to source image. + + When the widget to create a labels layer is instantiated, it asks for + a filename. This function sets the default to be next to the original + file. + """ + if source_image.source.path is not None: source_path = pathlib.Path(source_image.source.path) - if source_path.suffix != '.zarr': - labels_path = source_path.with_suffix('.zarr') - else: - labels_path = source_path.with_suffix('.labels.zarr') + labels_path = source_path.with_suffix('.labels.zarr') widget.labels_file.value = labels_path @@ -48,8 +50,7 @@ def open_ts_meta(labels_file: pathlib.Path) -> dict: meta = {} if os.path.exists(fn): with open(fn, 'r') as fin: - meta = yaml.safe_load(fin) - return meta + meta = yaml.safe_load(fin) return meta def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): From 1f63256c020b746bf9b2affff294a6048aaa960e Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:08:52 +1100 Subject: [PATCH 05/10] Add docstring and simplify code --- src/zarpaint/_zarpaint.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index f2477b1..b93ebaa 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -30,13 +30,15 @@ def _set_default_labels_path(widget, source_image): def _on_create_labels_init(widget): + """Ensure changes to the source image change the default labels path.""" widget.source_image.changed.connect(_set_default_labels_path(widget)) def create_ts_meta(labels_file: pathlib.Path, metadata): """Create bespoke metadata yaml file within zarr array.""" fn = os.path.join(labels_file, '.naparimeta.yml') - with open(fn, 'w') as fout: for key, val in metadata.items(): + with open(fn, 'w') as fout: + for key, val in metadata.items(): if type(val) == np.ndarray: if np.issubdtype(val.dtype, np.floating): metadata[key] = list(map(float, val)) @@ -44,6 +46,7 @@ def create_ts_meta(labels_file: pathlib.Path, metadata): metadata[key] = list(map(int, val)) yaml.dump(metadata, fout) + def open_ts_meta(labels_file: pathlib.Path) -> dict: """Open bespoke metadata yaml file within zarr array, if present.""" fn = os.path.join(labels_file, '.naparimeta.yml') @@ -57,7 +60,6 @@ def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): """Open a zarr file, with tensorstore if available, with zarr otherwise. If the file doesn't exist, it is created. - Parameters ---------- labels_file : Path From 2012a03e7569b21310b57775e21c5d5c86614907 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:10:09 +1100 Subject: [PATCH 06/10] Formatting --- src/zarpaint/_zarpaint.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index b93ebaa..75c9e1b 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -53,13 +53,15 @@ def open_ts_meta(labels_file: pathlib.Path) -> dict: meta = {} if os.path.exists(fn): with open(fn, 'r') as fin: - meta = yaml.safe_load(fin) return meta + meta = yaml.safe_load(fin) + return meta def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): """Open a zarr file, with tensorstore if available, with zarr otherwise. If the file doesn't exist, it is created. + Parameters ---------- labels_file : Path @@ -107,21 +109,24 @@ def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): @magic_factory( labels_file={'mode': 'w'}, widget_init=_on_create_labels_init, - )def create_labels( + ) +def create_labels( source_image: napari.layers.Image, labels_file: pathlib.Path, chunks='', ) -> napari.types.LayerDataTuple: """Create/load a zarr array as a labels layer based on image layer. - Parameters ---------- + Parameters + ---------- source_image : Image layer The image that we are segmenting. labels_file : pathlib.Path The path to the zarr file to be created. chunks : str, optional A string that can be evaluated as a tuple of ints specifying the chunk - size for the zarr file. If empty, they will be (128, 128) along the last dimensions and (1) along any remaining dimensions. This argument + size for the zarr file. If empty, they will be (128, 128) along the + last dimensions and (1) along any remaining dimensions. This argument has no effect if the file already exists. """ if chunks: From cfe8d95c8608149c0f604915acc53d524e4c05ca Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Thu, 12 Oct 2023 00:10:30 +1100 Subject: [PATCH 07/10] Remove outdated unused class --- src/zarpaint/_zarpaint.py | 255 +------------------------------------- 1 file changed, 1 insertion(+), 254 deletions(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index 75c9e1b..9c747e0 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -152,257 +152,4 @@ def create_labels( 'translate': source_image.translate, } create_ts_meta(labels_file, layer_metadata) - return layer_data, layer_metadata, layer_type - - -class LabelCorrector: - def __init__( - self, - image_file, - labels_file, - time_index, - scale=(1, 1, 4), - c=2, - t=None - ): - """ - Correct labels to create a ground truth with five opperations, - each of which correspond to the following number key. - (1) toggle selection of points - to seed watershed to split - labels. - (2) watershed - recompute watershed for joined labels based on - chosen points. - (3) toggle pick label colour - choose the colour of the label - with which to paint. - (4) toggle label fill mode - for merging labels - (5) toggle paint mode - paint new label. - - Output can be saved to a new file by using Control-s. Note that this is - not necessary if using tensorstore, as annotations will be written to - file. However, this is still useful for generating separate copies of - GT data (if completing annotations of whole frames). If time_index is - None, the save operation will make a copy of the currently displayed - frame (dim = t) and the file will have a suffix of the form - "t_GT.zarr". - - If the labels_file parameter is given a dict, this will be assumed - to be JSON-like spec for tensorstore. - - ASSUMES IMAGES ARE (c, t, z, y, x) - - ASSUMES LABELS ARE (t, z, y, x) - - ASSUMES GT FILES ARE (z, y, x) - - Parameters - ---------- - image_file: str - Path to the image data (.zarr) - labels_file: str or dict - str: Path to the labels data (.zarr) - dict: tensorstore spec for opening the file - (also .zarr) - time_index: int or None - None: all frames - int: one frame - TODO: slice: selection of frames - (must reindex ts when slicing or napari has issues) - scale: tuple of int - scale of image and labels for napari - c: int - the channel to read from the image - t: int or none - position of the time index in the labels file - (if this exists). If ndim > 3 and None specified - the format will be assumed to be (c*, t, z, y, x) - - Attributes - ---------- - tensorstore: bool - Should the lables be accessed via tensorstore? - This is set to True when the inputted labels_file - is a dict (JSON-esque spec for ts.open) - gt_file: bool - Is this a file ending in the suffix _GT? - If so it is assumed to be the saved output of another - annotation session - time_index: slice, int, or None - Gives the point/s in time to be selected - labels: ndarray or tensorstore.TensorStore - image: dask array - scale: tuple of int - """ - - # switches - # -------- - # tensorstore -> open zarr with tensorstore - # affects how we need to open data - # gt_file -> already using a ground truth labels - # affects how we need to save and access the file - # i.e., indexes are thought to apply only to the image - # the ground truth is assumed to be the indexed image - self.tensorstore = isinstance(labels_file, dict) - self.gt_file = None # is reassigned to a bool within _get_path_info - - # Read/Write Info - # --------------- - self.labels_file = labels_file - self.time_index = time_index - self._save_path = self._get_path_info() - - # Lazy Data - # --------- - if time_index is None: - self.time_index = slice( - None - ) # ensure that the following two lines - # work - # Note: we assume a 5D array saved in ome-zarr order: tczyx - self.image = da.from_zarr(image_file)[self.time_index, c] - self.labels = self._open_labels() - - # Vis Info - # -------- - self.viewer = None - self.scale = scale - self.ndim = len(self.image.shape) - if self.ndim > 3 and t == None: - t = -4 - self.t = t - - # Init helpers - # ------------ - def _get_path_info(self): - labels_file = self.labels_file - # is the file storing a previously annotated and saved ground truth? - if isinstance(labels_file, str): - labels_path = labels_file - elif self.tensorstore: - # get the path str from spec - labels_path = os.path.join( - labels_file['kvstore']['path'], labels_file['path'] - ) - else: - m = f'labels_file parameter must be dict or list not {type(labels_file)}' - raise ValueError(m) - self.gt_file = labels_path.endswith('_GT.zarr') - save_path = self._get_save_path(labels_path) - return save_path - - # Init helpers - # ------------ - def _get_save_path(self, labels_path): - if self.gt_file: - # if working on a GT file save to the same name - save_path = labels_path - else: - # otherwise use the name of the file sans extension - # the self.save_path property will use the index on t - # to generate the rest of the name. - data_path = Path(labels_path) - save_path = os.path.join(data_path.parents[0], data_path.stem) - return save_path - - def _open_labels(self): - labels_file = self.labels_file - if self.tensorstore: - # labels file should be the spec dict for tensorstore - if not self.gt_file: - # we need to apply the slice and so need to construct - # the correct tuple of int / slices - labels = labels[self.time_index] - else: - labels = zarr.open(labels_file, mode='r+') - if not self.gt_file: - labels = labels[self.time_index] - return labels - - # CALL - # ---- - def __call__(self): - with napari.gui_qt(): - self.viewer = napari.Viewer() - self.viewer.add_image(self.image, name='Image', scale=self.scale) - self.viewer.add_labels( - self.labels, name='Labels', scale=self.scale - ) - self.viewer.add_points( - np.empty((0, len(self.labels.shape)), dtype=float), - scale=self.scale, - size=2 - ) - self.viewer.bind_key('1', self._points) - self.viewer.bind_key('2', self._watershed) - self.viewer.bind_key('3', self._select_colour) - self.viewer.bind_key('4', self._fill) - self.viewer.bind_key('5', self._paint) - self.viewer.bind_key('Shift-s', self._save) - - @property - def save_path(self): - if self.gt_file: - # no need to find the save path suffix, it is ex - return self._save_path - else: - if isinstance(self.time_index, int): - time = self.time_index - elif self.ndim > 3: - time = self.viewer.dims.current_step[self.t] - else: - time = 'unknown' - suffix = f"_t{time}_GT.zarr" - return self._save_path + suffix - - # Call helpers - # ------------ - def _points(self, viewer): - """ - Switch to points layer to split a label - """ - if viewer.layers['Points'].mode != 'add': - viewer.layers['Points'].mode = 'add' - else: - viewer.layers['Points'].mode = 'pan_zoom' - - def _select_colour(self, viewer): - """ - Select colour for painting - """ - if viewer.layers['Labels'].mode != 'pick': - viewer.layers['Labels'].mode = 'pick' - else: - viewer.layers['Labels'].mode = 'pan_zoom' - - def _fill(self, viewer): - """ - Switch napari labels layer to fill mode - """ - if viewer.layers['Labels'].mode != 'fill': - viewer.layers['Labels'].mode = 'fill' - else: - viewer.layers['Labels'].mode = 'pan_zoom' - - def _paint(self, viewer): - """ - Switch napari labels layer to paint mode - """ - if viewer.layers['Labels'].mode != 'paint': - viewer.layers['Labels'].mode = 'paint' - else: - viewer.layers['Labels'].mode = 'pan_zoom' - - def _save(self, viewer): - """ - Save the annotated time_index as a zarr file - """ - array = viewer.layers['Labels'].data - if len(array.shape) > 3: # save the current frame - time = self.viewer.dims.current_step[self.t] - idx = [slice(None)] * self.ndim - idx[self.t] = time - idx = tuple(idx) - array = array[idx] - zarr.save_array(self.save_path, np.array(array)) - print("Labels saved at:") - print(self.save_path) - - -# Split Objects -# ------------- \ No newline at end of file + return layer_data, layer_metadata, layer_type \ No newline at end of file From 41dac651c88a8c99b6496f8f0eb5878abd73dddf Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 13 Oct 2023 10:54:35 +1100 Subject: [PATCH 08/10] Update test workflow --- .github/workflows/test_and_deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 660fe9a..9fa0f47 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: platform: [ubuntu-latest, windows-latest, macos-latest] - python-version: [3.8, 3.9, '3.10'] + python-version: ['3.9', '3.10', '3.11'] steps: - uses: actions/checkout@v2 @@ -51,7 +51,7 @@ jobs: # this runs the platform-specific tests declared in tox.ini - name: Test with tox - uses: GabrielBB/xvfb-action@v1 + uses: aganders3/headless-gui@v1 with: run: python -m tox env: From b1b95b4b87e591b7bb83b233668a959e56fc4592 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 13 Oct 2023 11:04:24 +1100 Subject: [PATCH 09/10] Fix tox.ini --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index c05f671..bab4c0f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,9 @@ envlist = py{37,38,39}-{linux,macos,windows} [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 [gh-actions:env] PLATFORM = ubuntu-latest: linux @@ -21,7 +21,8 @@ platform = passenv = CI GITHUB_ACTIONS - DISPLAY XAUTHORITY + DISPLAY + XAUTHORITY NUMPY_EXPERIMENTAL_ARRAY_FUNCTION PYVISTA_OFF_SCREEN deps = From 3204a38e90996776ce9f9d9eb3742d8fa53decf8 Mon Sep 17 00:00:00 2001 From: Juan Nunez-Iglesias Date: Fri, 13 Oct 2023 11:11:40 +1100 Subject: [PATCH 10/10] Use mode='a' when opening zarr array --- src/zarpaint/_zarpaint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zarpaint/_zarpaint.py b/src/zarpaint/_zarpaint.py index 9c747e0..283b3c1 100644 --- a/src/zarpaint/_zarpaint.py +++ b/src/zarpaint/_zarpaint.py @@ -85,7 +85,7 @@ def open_zarr(labels_file: pathlib.Path, *, shape=None, chunks=None): chunks=chunks, ) # read some of the metadata for tensorstore driver from file - labels_temp = zarr.open(str(labels_file), mode='r') + labels_temp = zarr.open(str(labels_file), mode='a') metadata = { 'dtype': labels_temp.dtype.str, 'order': labels_temp.order,