diff --git a/webknossos/Changelog.md b/webknossos/Changelog.md index b8d143ca5..ffb9e9d17 100644 --- a/webknossos/Changelog.md +++ b/webknossos/Changelog.md @@ -15,6 +15,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section ### Breaking Changes ### Added +- Added context manager `VolumeLayer.edit` for creating and modifying volume annotations. [#1340](https://github.com/scalableminds/webknossos-libs/pull/1340) ### Changed diff --git a/webknossos/examples/WIP/merge_trees_at_closest_nodes.py b/webknossos/examples/WIP/merge_trees_at_closest_nodes.py index e89f21e68..503aff075 100644 --- a/webknossos/examples/WIP/merge_trees_at_closest_nodes.py +++ b/webknossos/examples/WIP/merge_trees_at_closest_nodes.py @@ -6,19 +6,19 @@ import webknossos as wk -nml = wk.Skeleton.load("trees-in-groups.nml") +skeleton = wk.Skeleton.load("trees-in-groups.nml") # Probably we want to keep groups and normal trees in distinct collections (groups/trees). # For many use-cases a common view groups_and_trees would be great, but not here: -for group in nml.groups.values(): # groups is a dict with the name as keys +for group in skeleton.groups.values(): # groups is a dict with the name as keys min_distance_graph = G = nx.Graph() for (tree_idx_a, tree_a), (tree_idx_b, tree_b) in combinations( enumerate(group.flattened_trees()), 2 ): pos_a = ( - tree_a.get_node_positions() * nml.voxel_size + tree_a.get_node_positions() * skeleton.voxel_size ) # or tree_a.get_node_positions_nm? - pos_b = tree_b.get_node_positions() * nml.voxel_size + pos_b = tree_b.get_node_positions() * skeleton.voxel_size node_idx_a, node_idx_b, distance = wk.geometry.closest_pair(pos_a, pos_b) G.add_edge((tree_idx_a, node_idx_a), (tree_idx_b, node_idx_b), weight=distance) new_edges = nx.algorithms.tree.mst.minimum_spanning_edges() @@ -35,7 +35,7 @@ final_tree.name = group.name final_tree.group = None - del nml.groups[group.name] + del skeleton.groups[group.name] # or group.delete() # The latter only works if everything is double-linked. @@ -44,4 +44,4 @@ # to do the double-linking. Simply dict-like insertions can't work then: # nml["tree-name"] = Tree() -nml.save("merged-trees.nml") +skeleton.save("merged-trees.nml") diff --git a/webknossos/examples/WIP/offline_merger_mode.py b/webknossos/examples/WIP/offline_merger_mode.py index b306622e8..8097c2feb 100644 --- a/webknossos/examples/WIP/offline_merger_mode.py +++ b/webknossos/examples/WIP/offline_merger_mode.py @@ -4,21 +4,21 @@ import webknossos as wk -# A merger mode nml with every tree corresponding to a new merged segment is available. +# A merger mode skeleton with every tree corresponding to a new merged segment is available. # All segments in which a node is placed should be merged and saved as a new dataset. -# for local nml: -nml = wk.open("merger-mode.nml") +# for local skeleton: +skeleton = wk.open("merger-mode.skeleton") # wk.Skeleton.load or wk.open_skeleton works, too (and is type-safe) # for online annotation: -annotation = wk.Annotation.download( - "https://webknossos.org/annotations/Explorational/6114d9410100009f0096c640" -) -nml = annotation.skeleton +skeleton = wk.Annotation.download( + "https://webknossos.org/annotations/Explorational/6114d9410100009f0096c640", + skip_volume_data=True, +).skeleton # should this save anything to disk, or just happen in memory? -dataset = wk.download(nml.dataset_name, organization=nml.dataset_organization) +dataset = wk.download(skeleton.dataset_name, organization=skeleton.dataset_organization) # asks for auth token, persisted into .env or similar config file (maybe use xdg-path?) # sub-part access via dicts or dict-like classes @@ -28,7 +28,7 @@ segmentation_data = view.read() -for tree in nml.trees(): # nml.trees() is a flattened iterator of all trees +for tree in skeleton.trees: # skeleton.trees() is a flattened iterator of all trees segment_ids_in_tree = set( segmentation_data[tuple(node.position - view.topleft)] for node in tree.nodes ) diff --git a/webknossos/examples/apply_merger_mode.py b/webknossos/examples/apply_merger_mode.py index 44ddcb0ac..aca48d989 100644 --- a/webknossos/examples/apply_merger_mode.py +++ b/webknossos/examples/apply_merger_mode.py @@ -12,8 +12,9 @@ def main() -> None: # Opening a merger mode annotation # #################################### - nml = wk.Annotation.download( - "https://webknossos.org/annotations/6748612b0100001101c81156" + skeleton = wk.Annotation.download( + "https://webknossos.org/annotations/6748612b0100001101c81156", + skip_volume_data=True, ).skeleton ############################################### @@ -33,7 +34,7 @@ def main() -> None: ############################## segment_id_mapping = {} - for tree in nml.flattened_trees(): + for tree in skeleton.flattened_trees(): base = None for node in tree.nodes: segment_id = in_mag1.read( @@ -44,7 +45,7 @@ def main() -> None: segment_id_mapping[segment_id] = base print( - f"Found {len(list(nml.flattened_trees()))} segment id groups with {len(segment_id_mapping)} nodes" + f"Found {len(list(skeleton.flattened_trees()))} segment id groups with {len(segment_id_mapping)} nodes" ) print(segment_id_mapping) diff --git a/webknossos/examples/learned_segmenter.py b/webknossos/examples/learned_segmenter.py index 566898a08..20224dba5 100644 --- a/webknossos/examples/learned_segmenter.py +++ b/webknossos/examples/learned_segmenter.py @@ -1,6 +1,4 @@ -import os from functools import partial -from tempfile import TemporaryDirectory import numpy as np from skimage import feature @@ -19,7 +17,6 @@ def main() -> None: # Step 1: Read the training data from the annotation and the dataset's color # layer (the data will be streamed from WEBKNOSSOS to our local computer) training_data_bbox = annotation.user_bounding_boxes[0] # type: ignore[index] - new_dataset_name = f"{annotation.dataset_name.replace(' ', '_')}_segmented" with wk.webknossos_context("https://webknossos.org"): dataset = annotation.get_remote_annotation_dataset() @@ -58,28 +55,15 @@ def main() -> None: assert segmentation.max() < 256 segmentation = segmentation.astype("uint8") - # Step 5: Bundle everything as a WEBKNOSSOS layer and upload to wK for viewing and further work - with TemporaryDirectory() as tempdir: - new_dataset = wk.Dataset( - tempdir, voxel_size=dataset.voxel_size, name=new_dataset_name - ) - segmentation_layer = new_dataset.add_layer( - "segmentation", - wk.SEGMENTATION_CATEGORY, - dtype_per_channel=segmentation.dtype, - largest_segment_id=int(segmentation.max()), - ) + # Step 5: Upload the segmentation to WEBKNOSSOS + print("Uploading segmentation…") + volume_layer = annotation.add_volume_layer("segmentation", dtype=segmentation.dtype) + with volume_layer.edit() as segmentation_layer: segmentation_layer.bounding_box = dataset.layers["color"].bounding_box segmentation_layer.add_mag(mag, compress=True).write(segmentation) segmentation_layer.downsample(sampling_mode="constant_z") - remote_ds = new_dataset.upload( - layers_to_link=[annotation.get_remote_base_dataset().layers["color"]] - if "PYTEST_CURRENT_TEST" not in os.environ - else None - ) - - url = remote_ds.url + url = annotation.upload() print(f"Successfully uploaded {url}") diff --git a/webknossos/examples/skeleton_path_length.py b/webknossos/examples/skeleton_path_length.py index d267c691a..97aad2191 100644 --- a/webknossos/examples/skeleton_path_length.py +++ b/webknossos/examples/skeleton_path_length.py @@ -6,9 +6,7 @@ def calculate_path_length(annotation_url: str, auth_token: str) -> None: with wk.webknossos_context(token=auth_token): # Download a annotation directly from the WEBKNOSSOS server - annotation = wk.Annotation.download( - annotation_url, - ) + annotation = wk.Annotation.download(annotation_url, skip_volume_data=True) skeleton = annotation.skeleton voxel_size = annotation.voxel_size diff --git a/webknossos/tests/cassettes/test_annotation/test_edited_volume_annotation_upload_download.yml b/webknossos/tests/cassettes/test_annotation/test_edited_volume_annotation_upload_download.yml new file mode 100644 index 000000000..b07d8fd00 --- /dev/null +++ b/webknossos/tests/cassettes/test_annotation/test_edited_volume_annotation_upload_download.yml @@ -0,0 +1,65 @@ +http_interactions: + - request: + method: POST + path: /api/v10/annotations/upload + headers: + host: localhost:9000 + accept: '*/*' + accept-encoding: gzip, deflate + connection: keep-alive + user-agent: python-httpx/0.27.2 + x-auth-token: >- + 1b88db86331a38c21a0b235794b9e459856490d70408bcffb767f64ade0f83d2bdb4c4e181b9a9a30cdece7cb7c65208cc43b6c1bb5987f5ece00d348b1a905502a266f8fc64f0371cd6559393d72e031d0c2d0cabad58cccf957bb258bc86f05b5dc3d4fff3d5e3d9c0389a6027d861a21e78e3222fb6c5b7944520ef21761e + content-length: '25047' + content-type: multipart/form-data; boundary=b5490089a8029e1d090d68296c07cd24 + body: + encoding: base64 + data: >- + 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 + response: + status: + code: 200 + headers: + cache-control: no-cache + referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin + x-permitted-cross-domain-policies: master-only + date: Fri, 05 Sep 2025 16:34:10 GMT + content-type: application/json + content-length: '124' + body: + encoding: utf8 + data: >- + {"annotation":{"typ":"Explorational","id":"68bb11020100008e00a06ec3"},"messages":[{"success":"Successfully + uploaded file"}]} + compression: none + - request: + method: GET + path: >- + /api/v10/annotations/68bb11020100008e00a06ec3/download?skipVolumeData=false&volumeDataZipFormat=zarr3 + headers: + host: localhost:9000 + accept: '*/*' + accept-encoding: gzip, deflate + connection: keep-alive + user-agent: python-httpx/0.27.2 + x-auth-token: >- + 1b88db86331a38c21a0b235794b9e459856490d70408bcffb767f64ade0f83d2bdb4c4e181b9a9a30cdece7cb7c65208cc43b6c1bb5987f5ece00d348b1a905502a266f8fc64f0371cd6559393d72e031d0c2d0cabad58cccf957bb258bc86f05b5dc3d4fff3d5e3d9c0389a6027d861a21e78e3222fb6c5b7944520ef21761e + body: + encoding: utf8 + data: '' + compression: none + response: + status: + code: 200 + headers: + cache-control: no-cache + referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin + content-disposition: attachment;filename="l4_sample__explorational__suser__94b271.zip" + x-permitted-cross-domain-policies: master-only + date: Fri, 05 Sep 2025 16:34:11 GMT + content-type: application/zip + content-length: '21136' + body: + encoding: base64 + data: >- + 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 diff --git a/webknossos/tests/test_annotation.py b/webknossos/tests/test_annotation.py index cc5f28ddd..a7d1084a8 100644 --- a/webknossos/tests/test_annotation.py +++ b/webknossos/tests/test_annotation.py @@ -4,8 +4,11 @@ import numpy as np import pytest +from cluster_tools import get_executor import webknossos as wk +from webknossos import Annotation, SegmentationLayer +from webknossos.annotation.volume_layer import VolumeLayerEditMode from webknossos.dataset import DataFormat from webknossos.geometry import BoundingBox, Vec3Int @@ -40,7 +43,10 @@ def test_annotation_from_wkw_zip_file() -> None: assert len(list(copied_annotation.get_volume_layer_names())) == 1 assert len(list(copied_annotation.skeleton.flattened_trees())) == 1 - copied_annotation.add_volume_layer(name="new_volume_layer") + copied_annotation.add_volume_layer( + name="new_volume_layer", + dtype=np.uint32, + ) assert len(list(copied_annotation.get_volume_layer_names())) == 2 copied_annotation.delete_volume_layer(volume_layer_name="new_volume_layer") assert len(list(copied_annotation.get_volume_layer_names())) == 1 @@ -368,3 +374,158 @@ def test_tree_metadata(tmp_path: Path) -> None: list(tmp_annotation.skeleton.flattened_trees())[0].metadata["test_tree"] == "test" ) + + +@pytest.mark.parametrize( + "edit_mode", [VolumeLayerEditMode.MEMORY, VolumeLayerEditMode.TEMPORARY_DIRECTORY] +) +@pytest.mark.parametrize("executor", ["sequential", "multiprocessing"]) +def test_edit_volume_annotation(edit_mode: VolumeLayerEditMode, executor: str) -> None: + dtype = np.uint32 + data = np.ones((1, 10, 10, 10), dtype=dtype) + ann = wk.Annotation( + name="my_annotation", + dataset_name="sample_dataset", + voxel_size=(11.2, 11.2, 25.0), + ) + + volume_layer = ann.add_volume_layer( + name="segmentation", + dtype=dtype, + ) + if edit_mode == VolumeLayerEditMode.MEMORY and executor == "multiprocessing": + with pytest.raises(ValueError, match="SequentialExecutor"): + with volume_layer.edit( + edit_mode=edit_mode, executor=get_executor(executor) + ) as seg_layer: + pass + else: + with volume_layer.edit( + edit_mode=edit_mode, executor=get_executor(executor) + ) as seg_layer: + assert isinstance(seg_layer, SegmentationLayer) + mag = seg_layer.add_mag(1) + mag.write(data, absolute_offset=(0, 0, 0), allow_resize=True) + with volume_layer.edit(edit_mode=edit_mode) as seg_layer: + assert len(seg_layer.mags) == 1 + mag = seg_layer.get_mag(1) + read_data = mag.read(absolute_offset=(0, 0, 0), size=(10, 10, 10)) + assert np.array_equal(data, read_data) + + +def test_edited_volume_annotation_format() -> None: + import zipfile + + import tensorstore + + path = TESTDATA_DIR / "annotations" / "l4_sample__explorational__suser__94b271.zip" + ann = Annotation.load(path) + data = np.ones(shape=(10, 10, 10)) + + volume_layer = ann.add_volume_layer( + name="segmentation", + dtype=np.uint32, + ) + with volume_layer.edit() as seg_layer: + mag_view = seg_layer.add_mag(1) + mag_view.write(data, allow_resize=True) + + save_path = TESTOUTPUT_DIR / "saved_annotation.zip" + ann.save(save_path) + unpack_dir = TESTOUTPUT_DIR / "unpacked_annotation" + with zipfile.ZipFile(save_path, "r") as zip_ref: + zip_ref.extractall(unpack_dir) + + # test for the format assumptions as mentioned in https://github.com/scalableminds/webknossos/issues/8604 + ts = tensorstore.open( + { + "driver": "zarr3", + "kvstore": { + "driver": "zip", + "path": "volumeAnnotationData/1/", + "base": { + "driver": "file", + "path": str(unpack_dir / "data_1_segmentation.zip"), + }, + }, + }, + create=False, + open=True, + ).result() + metadata = ts.spec().to_json()["metadata"] + + assert metadata["chunk_key_encoding"] == { + "configuration": {"separator": "."}, + "name": "default", + } + assert ["transpose", "bytes", "blosc"] == [ + codec["name"] for codec in metadata["codecs"] + ] + data_read = ts.read().result()[0, :10, :10, :10] + assert np.array_equal(data, data_read) + + +@pytest.mark.parametrize( + "edit_mode", [VolumeLayerEditMode.MEMORY, VolumeLayerEditMode.TEMPORARY_DIRECTORY] +) +def test_edited_volume_annotation_save_load(edit_mode: VolumeLayerEditMode) -> None: + data = np.ones((1, 10, 10, 10)) + + ann = wk.Annotation( + name="my_annotation", + dataset_name="sample_dataset", + voxel_size=(11.2, 11.2, 25.0), + ) + + volume_layer = ann.add_volume_layer(name="segmentation", dtype=np.uint32) + with volume_layer.edit(edit_mode=edit_mode) as seg_layer: + mag_view = seg_layer.add_mag(1) + mag_view.write(data, allow_resize=True) + + save_path = TESTOUTPUT_DIR / "annotation_saved.zip" + ann.save(save_path) + ann_loaded = Annotation.load(save_path) + + volume_layer_downloaded = ann_loaded.get_volume_layer("segmentation") + + with volume_layer_downloaded.edit(edit_mode=edit_mode) as seg_layer: + assert len(seg_layer.mags) == 1 + mag = seg_layer.get_mag(1) + read_data = mag.read(absolute_offset=(0, 0, 0), size=(10, 10, 10)) + assert np.array_equal(data, read_data) + + +@pytest.mark.use_proxay +def test_edited_volume_annotation_upload_download() -> None: + data = np.ones((1, 10, 10, 10)) + + ann = Annotation.load( + TESTDATA_DIR / "annotations" / "l4_sample__explorational__suser__94b271.zip" + ) + ann.organization_id = "Organization_X" + + volume_layer = ann.add_volume_layer( + name="segmentation", + dtype=np.uint32, + ) + with volume_layer.edit() as seg_layer: + mag_view = seg_layer.add_mag(1) + mag_view.write(data, allow_resize=True) + + url = ann.upload() + ann_downloaded = Annotation.download( + url, + ) + + assert {layer.name for layer in ann_downloaded._volume_layers} == { + "Volume", + "segmentation", + } + + volume_layer_downloaded = ann_downloaded.get_volume_layer("segmentation") + + with volume_layer_downloaded.edit() as seg_layer: + assert len(seg_layer.mags) == 1 + mag = seg_layer.get_mag(1) + read_data = mag.read(absolute_offset=(0, 0, 0), size=(10, 10, 10)) + assert np.array_equal(data, read_data) diff --git a/webknossos/tests/test_cli.py b/webknossos/tests/test_cli.py index ae5daf0d3..f41597829 100644 --- a/webknossos/tests/test_cli.py +++ b/webknossos/tests/test_cli.py @@ -660,7 +660,7 @@ def test_merge_fallback_no_fallback_layer( ) annotation._volume_layers = [ - webknossos.annotation._VolumeLayer( # type: ignore + webknossos.annotation.VolumeLayer( # type: ignore id=0, name=tmp_layer.name, fallback_layer_name=fallback_mag.layer.name, @@ -668,6 +668,7 @@ def test_merge_fallback_no_fallback_layer( segments={}, data_format=DataFormat.WKW, largest_segment_id=largest_segment_id, + voxel_size=tmp_dataset.voxel_size, ), ] diff --git a/webknossos/webknossos/annotation/annotation.py b/webknossos/webknossos/annotation/annotation.py index 4fa37c778..1a620ab34 100644 --- a/webknossos/webknossos/annotation/annotation.py +++ b/webknossos/webknossos/annotation/annotation.py @@ -39,24 +39,23 @@ * Volume annotation documentation: /webknossos/volume_annotation/index.html """ -import json import logging import re import warnings -from collections.abc import Iterable, Iterator, Sequence +from collections.abc import Iterable, Iterator from contextlib import AbstractContextManager, contextmanager, nullcontext from enum import Enum, unique from io import BytesIO from os import PathLike from pathlib import Path -from shutil import copyfileobj -from tempfile import TemporaryDirectory -from typing import BinaryIO, Literal, Union, cast, overload +from tempfile import NamedTemporaryFile, TemporaryDirectory +from typing import BinaryIO, Literal, Union, overload from zipfile import ZIP_DEFLATED, ZipFile from zlib import Z_BEST_SPEED import attr -from cluster_tools.executor_protocol import Executor +from cluster_tools import Executor +from numpy._typing import DTypeLike from upath import UPath from zipp import Path as ZipPath @@ -76,57 +75,17 @@ RemoteDataset, SegmentationLayer, ) -from ..dataset.defaults import PROPERTIES_FILE_NAME, SSL_CONTEXT -from ..dataset.properties import DatasetProperties, VoxelSize, dataset_converter +from ..dataset.defaults import SSL_CONTEXT +from ..dataset.properties import VoxelSize from ..geometry import NDBoundingBox, Vec3Int from ..skeleton import Skeleton from ..utils import get_executor_for_args, time_since_epoch_in_ms from ._nml_conversion import annotation_to_nml, nml_to_skeleton +from .volume_layer import SegmentInformation, VolumeLayer logger = logging.getLogger(__name__) Vector3 = tuple[float, float, float] -Vector4 = tuple[float, float, float, float] - - -MAG_RE = r"((\d+-\d+-)?\d+)" -SEP_RE = r"(\/|\\)" -CUBE_RE = rf"z\d+{SEP_RE}y\d+{SEP_RE}x\d+\.wkw" -ANNOTATION_WKW_PATH_RE = re.compile(rf"{MAG_RE}{SEP_RE}(header\.wkw|{CUBE_RE})") - - -@attr.define -class SegmentInformation: - name: str | None - anchor_position: Vec3Int | None - color: Vector4 | None - metadata: dict[str, str | int | float | Sequence[str]] - - -@attr.define -class _VolumeLayer: - id: int - name: str - fallback_layer_name: str | None - data_format: DataFormat - zip: ZipPath | None - segments: dict[int, SegmentInformation] - largest_segment_id: int | None - - def _default_zip_name(self) -> str: - return f"data_{self.id}_{self.name}.zip" - - -def _extract_zip_folder(zip_file: ZipFile, out_path: Path, prefix: str) -> None: - for zip_entry in zip_file.filelist: - if zip_entry.filename.startswith(prefix) and not zip_entry.is_dir(): - out_file_path = out_path / (zip_entry.filename[len(prefix) :]) - out_file_path.parent.mkdir(parents=True, exist_ok=True) - with ( - zip_file.open(zip_entry, "r") as zip_f, - out_file_path.open("wb") as out_f, - ): - copyfileobj(zip_f, out_f) @attr.define @@ -204,7 +163,7 @@ class Annotation: metadata: dict[str, str] = attr.Factory(dict) task_bounding_box: NDBoundingBox | None = None user_bounding_boxes: list[NDBoundingBox] = attr.Factory(list) - _volume_layers: list[_VolumeLayer] = attr.field(factory=list, init=False) + _volume_layers: list[VolumeLayer] = attr.field(factory=list, init=False) @classmethod def _set_init_docstring(cls) -> None: @@ -481,6 +440,11 @@ def download( ) annotation = Annotation._load_from_zip(BytesIO(file_body)) + volume_zip_root = NamedTemporaryFile(suffix=".zip").name + with ZipFile(volume_zip_root, "w"): + pass + annotation._write_volume_layers(Path(volume_zip_root)) + if _return_context: return annotation, context else: @@ -605,7 +569,7 @@ def _load_from_nml( @staticmethod def _parse_volumes( nml: wknml.Nml, possible_paths: list[ZipPath] | None - ) -> list[_VolumeLayer]: + ) -> list[VolumeLayer]: volume_layers = [] layers_with_not_found_location = [] layers_without_location = [] @@ -645,7 +609,7 @@ def _parse_volumes( metadata={i.key: i.value for i in segment.metadata}, ) volume_layers.append( - _VolumeLayer( + VolumeLayer( id=volume.id, name="Volume" if volume.name is None else volume.name, fallback_layer_name=volume.fallback_layer, @@ -657,6 +621,7 @@ def _parse_volumes( zip=volume_path, segments=segments, largest_segment_id=volume.largest_segment_id, + voxel_size=nml.parameters.scale.factor, ) ) assert len(set(i.id for i in volume_layers)) == len(volume_layers), ( @@ -689,6 +654,28 @@ def _load_from_zip(cls, content: str | PathLike | BinaryIO) -> "Annotation": with nml_paths[0].open(mode="rb") as f: return cls._load_from_nml(nml_paths[0].stem, f, possible_volume_paths=paths) + def _write_volume_layers(self, path: Path) -> None: + """ + Writes all volume layers with zip data to a single zip file at the specified location. + """ + + path.parent.mkdir(parents=True, exist_ok=True) + + with ZipFile( + path, + mode="w", + compression=ZIP_DEFLATED, + compresslevel=Z_BEST_SPEED, + ) as zf: + for layer in self._volume_layers: + if layer.zip is not None: + with layer.zip.open(mode="rb") as f: + zf.writestr(layer.zip.at, f.read()) + + for layer in self._volume_layers: + if layer.zip is not None: + layer.zip = ZipPath(path, layer.zip.at) + def save(self, path: str | PathLike) -> None: """Saves the annotation to a file. @@ -783,7 +770,7 @@ def merge_fallback_layer( ) volume_layer_name = annotation_volumes[0] - volume_layer = self._get_volume_layer(volume_layer_name=volume_layer_name) + volume_layer = self.get_volume_layer(volume_layer_name=volume_layer_name) fallback_layer_name = volume_layer.fallback_layer_name if fallback_layer_name is None: @@ -907,7 +894,6 @@ def _write_to_zip(self, zipfile: ZipFile) -> None: nml.write(buffer) nml_str = buffer.getvalue().decode("utf-8") zipfile.writestr(self.name + ".nml", nml_str) - for volume_layer in self._volume_layers: if volume_layer.zip is None: with BytesIO() as buffer: @@ -1043,9 +1029,10 @@ def get_volume_layer_names(self) -> Iterable[str]: def add_volume_layer( self, name: str, + dtype: DTypeLike, fallback_layer: Layer | str | None = None, volume_layer_id: int | None = None, - ) -> None: + ) -> VolumeLayer: """Adds a new volume layer to the annotation. Volume layers can be used to store segmentation data. Using fallback layers @@ -1053,6 +1040,7 @@ def add_volume_layer( Args: name: Name of the volume layer. + dtype: Datatype of the volume layer. fallback_layer: Optional reference to existing segmentation layer in WEBKNOSSOS. Can be Layer instance or layer name. volume_layer_id: Optional explicit ID for the layer. @@ -1065,12 +1053,16 @@ def add_volume_layer( Examples: ```python # Add basic layer - annotation.add_volume_layer("segmentation") + annotation.add_volume_layer("segmentation", dtype=np.uint32) # Add with fallback - annotation.add_volume_layer("segmentation", fallback_layer="base_segmentation") + annotation.add_volume_layer("segmentation", fallback_layer="base_segmentation", dtype=np.uint32) ``` """ + volume_zip_root = NamedTemporaryFile(suffix=".zip").name + with ZipFile(volume_zip_root, "w"): + pass + volume_zip_path = ZipPath(volume_zip_root, f"{name}.zip") if volume_layer_id is None: volume_layer_id = max((i.id for i in self._volume_layers), default=-1) + 1 @@ -1088,23 +1080,35 @@ def add_volume_layer( fallback_layer_name = str(fallback_layer) else: fallback_layer_name = None - self._volume_layers.append( - _VolumeLayer( - id=volume_layer_id, - name=name, - fallback_layer_name=fallback_layer_name, + volume_layer = VolumeLayer( + id=volume_layer_id, + name=name, + fallback_layer_name=fallback_layer_name, + data_format=DataFormat.Zarr3, + zip=volume_zip_path, + segments={}, + largest_segment_id=None, + voxel_size=self.voxel_size, + dtype=dtype, + ) + self._volume_layers.append(volume_layer) + + with TemporaryDirectory() as tempdir: + dataset = Dataset(tempdir, voxel_size=volume_layer.voxel_size) + dataset.add_layer( + volume_layer.layer_name, + SEGMENTATION_CATEGORY, data_format=DataFormat.Zarr3, - zip=None, - segments={}, - largest_segment_id=None, + dtype_per_channel=dtype, ) - ) + volume_layer._write_dir_to_zip(tempdir) + return volume_layer - def _get_volume_layer( + def get_volume_layer( self, volume_layer_name: str | None = None, volume_layer_id: int | None = None, - ) -> _VolumeLayer: + ) -> VolumeLayer: assert len(self._volume_layers) > 0, "No volume annotations present." if len(self._volume_layers) == 1: @@ -1184,7 +1188,7 @@ def delete_volume_layer( annotation.delete_volume_layer(volume_layer_id=2) ``` """ - layer_id = self._get_volume_layer( + layer_id = self.get_volume_layer( volume_layer_name=volume_layer_name, volume_layer_id=volume_layer_id, ).id @@ -1224,72 +1228,11 @@ def export_volume_layer_to_dataset( ``` """ - volume_layer = self._get_volume_layer( + volume_layer = self.get_volume_layer( volume_layer_name=volume_layer_name, volume_layer_id=volume_layer_id, ) - volume_zip_path = volume_layer.zip - - largest_segment_id = volume_layer.largest_segment_id - - assert volume_zip_path is not None, ( - "The selected volume layer data is not available and cannot be exported." - ) - - with volume_zip_path.open(mode="rb") as f: - data_zip = ZipFile(f) - if volume_layer.data_format == DataFormat.WKW: - wrong_files = [ - i.filename - for i in data_zip.filelist - if ANNOTATION_WKW_PATH_RE.search(i.filename) is None - ] - assert len(wrong_files) == 0, ( - f"The annotation contains unexpected files: {wrong_files}" - ) - data_zip.extractall(dataset.path / layer_name) - layer = cast( - SegmentationLayer, - dataset.add_layer_for_existing_files( - layer_name, - category=SEGMENTATION_CATEGORY, - largest_segment_id=largest_segment_id, - ), - ) - elif volume_layer.data_format == DataFormat.Zarr3: - datasource_properties = dataset_converter.structure( - json.loads(data_zip.read(PROPERTIES_FILE_NAME)), DatasetProperties - ) - assert len(datasource_properties.data_layers) == 1, ( - f"Volume data zip must contain exactly one layer, got {len(datasource_properties.data_layers)}" - ) - layer_properties = datasource_properties.data_layers[0] - internal_layer_name = layer_properties.name - layer_properties.name = layer_name - - _extract_zip_folder( - data_zip, dataset.path / layer_name, f"{internal_layer_name}/" - ) - - layer = cast( - SegmentationLayer, - dataset._add_existing_layer(layer_properties), - ) - - best_mag_view = layer.get_finest_mag() - - if largest_segment_id is None: - max_value = max( - ( - view.read().max() - for view in best_mag_view.get_views_on_disk(read_only=True) - ), - default=0, - ) - layer.largest_segment_id = int(max_value) - else: - layer.largest_segment_id = largest_segment_id - return layer + return volume_layer.export_to_dataset(dataset, layer_name) @contextmanager def temporary_volume_layer_copy( @@ -1372,7 +1315,7 @@ def get_volume_layer_segments( synced automatically. The annotation needs to be re-downloaded to update segment information. """ - layer = self._get_volume_layer( + layer = self.get_volume_layer( volume_layer_name=volume_layer_name, volume_layer_id=volume_layer_id, ) diff --git a/webknossos/webknossos/annotation/volume_layer.py b/webknossos/webknossos/annotation/volume_layer.py new file mode 100644 index 000000000..47af5626e --- /dev/null +++ b/webknossos/webknossos/annotation/volume_layer.py @@ -0,0 +1,320 @@ +import io +import json +import os +import re +import uuid +from argparse import Namespace +from collections.abc import Generator, Sequence +from contextlib import contextmanager +from enum import Enum +from pathlib import Path +from shutil import copyfileobj +from tempfile import TemporaryDirectory +from typing import Any, cast +from zipfile import ZIP_DEFLATED, ZipFile +from zlib import Z_BEST_SPEED + +import attr +from cluster_tools import Executor, SequentialExecutor +from numpy._typing import DTypeLike +from upath import UPath +from zipp import Path as ZipPath + +from ..cli._utils import DistributionStrategy +from ..dataset import ( + SEGMENTATION_CATEGORY, + DataFormat, + Dataset, + Layer, + SegmentationLayer, +) +from ..dataset._array import Zarr3Config +from ..dataset.defaults import PROPERTIES_FILE_NAME +from ..dataset.properties import DatasetProperties, dataset_converter +from ..geometry import Vec3Int +from ..utils import get_executor_for_args, is_fs_path + +Vector3 = tuple[float, float, float] +Vector4 = tuple[float, float, float, float] + + +MAG_RE = r"((\d+-\d+-)?\d+)" +SEP_RE = r"(\/|\\)" +CUBE_RE = rf"z\d+{SEP_RE}y\d+{SEP_RE}x\d+\.wkw" +ANNOTATION_WKW_PATH_RE = re.compile(rf"{MAG_RE}{SEP_RE}(header\.wkw|{CUBE_RE})") + + +@attr.define +class SegmentInformation: + name: str | None + anchor_position: Vec3Int | None + color: Vector4 | None + metadata: dict[str, str | int | float | Sequence[str]] + + +class VolumeLayerEditMode(Enum): + """Defines the edit mode for volume layers.""" + + TEMPORARY_DIRECTORY = "temporary_directory" # Use a temporary directory for edits + MEMORY = "memory" # Use an in-memory store for edits + + +VOLUME_ANNOTATION_ZARR3_CONFIG = Zarr3Config( + codecs=( + {"name": "transpose", "configuration": {"order": "F"}}, + { + "name": "bytes", + }, + { + "name": "blosc", + "configuration": { + "blocksize": 0, + "clevel": 5, + "cname": "lz4", + "shuffle": "shuffle", + "typesize": 1, + }, + }, + ), + chunk_key_encoding={ + "name": "default", + "configuration": {"separator": "."}, + }, +) + + +@attr.define +class VolumeLayer: + id: int + name: str + fallback_layer_name: str | None + data_format: DataFormat + zip: ZipPath | None + segments: dict[int, SegmentInformation] + largest_segment_id: int | None + voxel_size: Vector3 | None + dtype: DTypeLike | None = None + layer_name: str = "volumeAnnotationData" + + def _default_zip_name(self) -> str: + return f"data_{self.id}_{self.name}.zip" + + def _write_dir_to_zip(self, source: str) -> None: + """ + Write all files from the given source directory into the volume layer's zip archive. + + Parameters: + source: Path to the directory whose contents will be added to the zip archive. + """ + assert self.zip is not None + + volume_zip_buffer = io.BytesIO() + with ZipFile( + volume_zip_buffer, + mode="w", + compression=ZIP_DEFLATED, + compresslevel=Z_BEST_SPEED, + ) as volume_layer_zipfile: + for dirname, _, files in os.walk(source): + for file in files: + full_path = os.path.join(dirname, file) + arcname = os.path.relpath(full_path, source) + if ( + arcname == "zarr.json" + or arcname == self.layer_name + "/zarr.json" + ): + continue + volume_layer_zipfile.write(full_path, arcname) + + volume_zip_buffer.seek(0) + with ZipFile( + self.zip.root.filename, + mode="w", + compression=ZIP_DEFLATED, + compresslevel=Z_BEST_SPEED, + ) as annotation_zip: + annotation_zip.writestr(self.zip.at, volume_zip_buffer.read()) + + # updating self.zip.root.__lookup to include the new file + self.zip = ZipPath(self.zip.root.filename, self.zip.at) + assert self.zip.exists() + + @contextmanager + def edit( + self, + *, + edit_mode: VolumeLayerEditMode = VolumeLayerEditMode.TEMPORARY_DIRECTORY, + executor: Executor | None = None, + ) -> Generator[Layer | Any, None, None]: + """ + Context manager to edit the volume layer. + + Args: + edit_mode: Specifies the edit mode for the volume layer. + executor: Optional executor for parallel rechunking. + + """ + + if self.zip is None: + raise ValueError( + "VolumeLayer.zip is not specified but required for editing." + ) + + def _edit( + dataset_path: UPath, executor: Executor | None = None + ) -> Generator[Layer, None, None]: + dataset = Dataset(dataset_path, voxel_size=self.voxel_size) + assert self.zip is not None and self.zip.exists() + + if is_fs_path(dataset_path): + segmentation_layer = self.export_to_dataset( + dataset, layer_name=self.layer_name + ) + else: + # copy to temporary directory first, as tensorstore cannot read from MemoryFileSystem + with TemporaryDirectory() as tempdir: + temp_dataset = Dataset(tempdir, voxel_size=self.voxel_size) + temp_segmentation_layer = self.export_to_dataset( + temp_dataset, layer_name=self.layer_name + ) + segmentation_layer = cast( + SegmentationLayer, + dataset.add_layer_as_copy( + foreign_layer=temp_segmentation_layer, + ), + ) + + yield segmentation_layer + + with TemporaryDirectory() as rechunked_dir: + for mag_view in segmentation_layer.mags.values(): + mag_view.rechunk( + chunk_shape=mag_view.info.chunk_shape, + shard_shape=mag_view.info.chunk_shape, # same as chunk_shape to disable sharding + compress=VOLUME_ANNOTATION_ZARR3_CONFIG, + _progress_desc=f"Compressing {mag_view.layer.name} {mag_view.name}", + executor=executor, + target_path=rechunked_dir, + ) + self._write_dir_to_zip(rechunked_dir) + + fallback_executor_args = Namespace( + distribution_strategy=DistributionStrategy.SEQUENTIAL.value, + ) + with get_executor_for_args(fallback_executor_args, executor) as executor: + if edit_mode == VolumeLayerEditMode.TEMPORARY_DIRECTORY: + with TemporaryDirectory() as tmp_dir: + return _edit(UPath(tmp_dir), executor) + elif edit_mode == VolumeLayerEditMode.MEMORY: + if not isinstance(executor, SequentialExecutor): + raise ValueError( + "In-memory editing only supports SequentialExecutor to avoid data" + " corruption due to concurrent writes." + ) + path = UPath( + f"edit_{self.id}_{self.name}_{uuid.uuid4()}.zip", protocol="memory" + ) + try: + return _edit(path, executor) + finally: + if path.exists(): + path.rmdir(recursive=True) + else: + raise ValueError(f"Unsupported volume layer edit mode: {edit_mode}") + + def export_to_dataset( + self, + dataset: Dataset, + layer_name: str = "volume_layer", + ) -> SegmentationLayer: + """Exports the volume layer to a dataset as a SegmentationLayer. + + Args: + dataset: The target dataset to export to. + layer_name: Name of the layer in the dataset. + + Returns: + SegmentationLayer: The created segmentation layer. + + Raises: + AssertionError: If the volume layer is not set up correctly. + + Examples: + ```python + # Export volume layer to dataset + exported_layer = volume_layer.export_to_dataset(my_dataset, "my_volume_layer") + ``` + """ + + assert self.zip is not None, ( + "The selected volume layer data is not available and cannot be exported." + ) + + with self.zip.open(mode="rb") as f: + data_zip = ZipFile(f) + if self.data_format == DataFormat.WKW: + wrong_files = [ + i.filename + for i in data_zip.filelist + if ANNOTATION_WKW_PATH_RE.search(i.filename) is None + ] + assert len(wrong_files) == 0, ( + f"The annotation contains unexpected files: {wrong_files}" + ) + data_zip.extractall(dataset.path / layer_name) + layer = cast( + SegmentationLayer, + dataset.add_layer_for_existing_files( + layer_name, + category=SEGMENTATION_CATEGORY, + largest_segment_id=self.largest_segment_id, + ), + ) + elif self.data_format == DataFormat.Zarr3: + datasource_properties = dataset_converter.structure( + json.loads(data_zip.read(PROPERTIES_FILE_NAME)), DatasetProperties + ) + assert len(datasource_properties.data_layers) == 1, ( + f"Volume data zip must contain exactly one layer, got {len(datasource_properties.data_layers)}" + ) + layer_properties = datasource_properties.data_layers[0] + internal_layer_name = layer_properties.name + layer_properties.name = layer_name + + _extract_zip_folder( + data_zip, dataset.path / layer_name, f"{internal_layer_name}/" + ) + + layer = cast( + SegmentationLayer, + dataset._add_existing_layer(layer_properties), + ) + + if len(layer.mags) > 0: + best_mag_view = layer.get_finest_mag() + + if self.largest_segment_id is None: + max_value = max( + ( + view.read().max() + for view in best_mag_view.get_views_on_disk(read_only=True) + ), + default=0, + ) + layer.largest_segment_id = int(max_value) + else: + layer.largest_segment_id = self.largest_segment_id + + return layer + + +def _extract_zip_folder(zip_file: ZipFile, out_path: Path, prefix: str) -> None: + for zip_entry in zip_file.filelist: + if zip_entry.filename.startswith(prefix) and not zip_entry.is_dir(): + out_file_path = out_path / (zip_entry.filename[len(prefix) :]) + out_file_path.parent.mkdir(parents=True, exist_ok=True) + with ( + zip_file.open(zip_entry, "r") as zip_f, + out_file_path.open("wb") as out_f, + ): + copyfileobj(zip_f, out_f) diff --git a/webknossos/webknossos/client/api_client/wk_api_client.py b/webknossos/webknossos/client/api_client/wk_api_client.py index 1c1d5f935..7eae3dbbb 100644 --- a/webknossos/webknossos/client/api_client/wk_api_client.py +++ b/webknossos/webknossos/client/api_client/wk_api_client.py @@ -198,7 +198,9 @@ def annotation_download( ) -> tuple[bytes, str]: route = f"/annotations/{annotation_id}/download" return self._get_file( - route, query={"skipVolumeData": skip_volume_data}, retry_count=retry_count + route, + query={"skipVolumeData": skip_volume_data, "volumeDataZipFormat": "zarr3"}, + retry_count=retry_count, ) def annotation_upload( diff --git a/webknossos/webknossos/dataset/view.py b/webknossos/webknossos/dataset/view.py index a95d1bd3a..ff3060fc3 100644 --- a/webknossos/webknossos/dataset/view.py +++ b/webknossos/webknossos/dataset/view.py @@ -70,6 +70,7 @@ def __init__( mag: Mag, data_format: DataFormat, read_only: bool = False, + cached_array: BaseArray | None = None, ): """Initialize a View instance for accessing and manipulating dataset regions. @@ -96,7 +97,7 @@ def __init__( self._data_format = data_format self._bounding_box = bounding_box self._read_only = read_only - self._cached_array = None + self._cached_array = cached_array self._mag = mag @property @@ -797,6 +798,7 @@ def get_view( mag=self._mag, data_format=self._data_format, read_only=read_only, + cached_array=self._cached_array, ) def get_buffered_slice_writer(