Repository: fractal-analytics-platform/fractal-tasks-core (language: python)

Issue:
Refactor core-library modules
`lib_*` prefix is a leftover from when core-library and tasks where in the same namespace, but I think it now just makes every import longer with no real benefit.

Such a change is not at all urgent, but I also think it will only break the packages that do depend on fractal-tasks-core (ref https://github.com/fractal-analytics-platform/fractal-tasks-core/issues/599) and then it should be fairly harmless.


Discussion / hints:
I’m fine with the idea, but let’s combine it with any other reorg needed. For example: do we want to make some core functions importable from the top level?
Any other changes like reprg of the current lib files (are there any that should be merged or split?) would also make sense.

In that way, task packages then only need to update once.

Besides combining this, in favor of changing sooner rather than later! Because the number of packages may grow soon
> I’m fine with the idea, but let’s combine it with any other reorg needed.

Fully agreed.

My first guess is that we may refactor the tables-related package structure, based on the shape that V1 specs will take (#582).

> For example: do we want to make some core functions importable from the top level?

I'm generally in favor. Let's find out for which functions it would make sense.

Fix patch:
diff --git a/.gitignore b/.gitignore
index c1837b515..1e966c9b6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
 .pytest_cache/
+tree.sh
 .coverage*
 __pycache__
 *.cpython-*.pyc
diff --git a/examples/01_cardio_tiny_dataset/run_workflow.py b/examples/01_cardio_tiny_dataset/run_workflow.py
index 52b5807dc..9e808792c 100644
--- a/examples/01_cardio_tiny_dataset/run_workflow.py
+++ b/examples/01_cardio_tiny_dataset/run_workflow.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
 
diff --git a/examples/01_cardio_tiny_dataset/run_workflow_partial_channel_inputs.py b/examples/01_cardio_tiny_dataset/run_workflow_partial_channel_inputs.py
index 7aad46603..b8bbb10d4 100644
--- a/examples/01_cardio_tiny_dataset/run_workflow_partial_channel_inputs.py
+++ b/examples/01_cardio_tiny_dataset/run_workflow_partial_channel_inputs.py
@@ -16,7 +16,7 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
+from fractal_tasks_core.channels import OmeroChannel
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
 
diff --git a/examples/02_cardio_tiny_dataset_sparse/run_workflow.py b/examples/02_cardio_tiny_dataset_sparse/run_workflow.py
index 9c2cb3f70..b3f4d49c7 100644
--- a/examples/02_cardio_tiny_dataset_sparse/run_workflow.py
+++ b/examples/02_cardio_tiny_dataset_sparse/run_workflow.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
 
diff --git a/examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py b/examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
index 892c28cf1..7675a3931 100644
--- a/examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
+++ b/examples/03_cardio_tiny_dataset_with_illumination_correction/run_workflow.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.illumination_correction import (
     illumination_correction,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
index 0e987aaec..c2a60754d 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_1.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
index 4cb05e70d..dfb8e2ae3 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_2.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
index 2014f9bcd..56bb1713f 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_3.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
index 319c856bf..ceffbb3e4 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_4.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
index dd2120ac5..ffc09b6c1 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_5.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
index 872c52f80..685b41dd2 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_6.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
diff --git a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
index 94082d7b0..688d45ebd 100644
--- a/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
+++ b/examples/04_cardio_tiny_dataset_napari_worfklows/run_example_workflow_7.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
diff --git a/examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py b/examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
index 5059b15a5..48ec87056 100644
--- a/examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
+++ b/examples/05_cardio_tiny_dataset_cellpose/run_example_cellpose.py
@@ -16,8 +16,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
diff --git a/examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py b/examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py
index 01c3aa35e..fe5a579be 100644
--- a/examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py
+++ b/examples/06_cellpose_dev_example/run_example_cellpose_dual_channel.py
@@ -15,8 +15,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
diff --git a/examples/08_subset_of_data/run_example_single_channel.py b/examples/08_subset_of_data/run_example_single_channel.py
index 4e92c3d62..a6f0d8ca6 100644
--- a/examples/08_subset_of_data/run_example_single_channel.py
+++ b/examples/08_subset_of_data/run_example_single_channel.py
@@ -2,8 +2,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py b/examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py
index f27107a8b..820b8eacf 100644
--- a/examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py
+++ b/examples/08_subset_of_data/run_example_single_channel_few_Z_planes.py
@@ -2,8 +2,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py b/examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py
index 87ea73e7a..a78ffdfb4 100644
--- a/examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py
+++ b/examples/08_subset_of_data/run_example_single_channel_single_Z_plane.py
@@ -2,8 +2,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py b/examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py
index f0a48312f..f5f029ac0 100644
--- a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py
+++ b/examples/08_subset_of_data/run_example_two_channel_two_Z_planes.py
@@ -2,8 +2,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py b/examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
index eace529aa..3b8927068 100644
--- a/examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
+++ b/examples/08_subset_of_data/run_example_two_channel_two_Z_planes_single_FOV.py
@@ -2,8 +2,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/09_secondary_labeling/01_images_to_mip.py b/examples/09_secondary_labeling/01_images_to_mip.py
index debaf0171..5dbdeb5b2 100644
--- a/examples/09_secondary_labeling/01_images_to_mip.py
+++ b/examples/09_secondary_labeling/01_images_to_mip.py
@@ -3,8 +3,8 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import Window
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import Window
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
diff --git a/examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py b/examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py
index edc138a37..26bb86589 100644
--- a/examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py
+++ b/examples/09_secondary_labeling/02_cellpose_two_steps_segmentation.py
@@ -4,7 +4,7 @@
 
 from devtools import debug
 
-from fractal_tasks_core.lib_input_models import Channel
+from fractal_tasks_core.channels import ChannelInputModel
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
@@ -31,7 +31,7 @@
         output_path=zarr_path_mip,
         metadata=metadata,
         component=component,
-        channel=Channel(wavelength_id="A01_C01"),
+        channel=ChannelInputModel(wavelength_id="A01_C01"),
         level=2,
         relabeling=True,
         diameter_level0=400.0,
@@ -53,7 +53,7 @@
         output_path=zarr_path_mip,
         metadata=metadata,
         component=component,
-        channel=Channel(wavelength_id="A01_C01"),
+        channel=ChannelInputModel(wavelength_id="A01_C01"),
         level=2,
         relabeling=True,
         diameter_level0=20.0,
diff --git a/examples/09_secondary_labeling/check_ROI_table.py b/examples/09_secondary_labeling/check_ROI_table.py
index 73f54db1a..edad6883e 100644
--- a/examples/09_secondary_labeling/check_ROI_table.py
+++ b/examples/09_secondary_labeling/check_ROI_table.py
@@ -1,6 +1,6 @@
 import sys
 
-from fractal_tasks_core.lib_regions_of_interest import _inspect_ROI_table
+from fractal_tasks_core.roi import _inspect_ROI_table
 
 
 table = sys.argv[1]
diff --git a/examples/tools/lib_inspect_ROI_table.py b/examples/tools/lib_inspect_ROI_table.py
index cc77daf52..0281b8587 100644
--- a/examples/tools/lib_inspect_ROI_table.py
+++ b/examples/tools/lib_inspect_ROI_table.py
@@ -2,7 +2,7 @@
 
 import anndata as ad
 
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
 
diff --git a/examples/tools/show_FOV_ROIs/show_FOV_ROIs.py b/examples/tools/show_FOV_ROIs/show_FOV_ROIs.py
index f6fe7360e..3e7e3204c 100644
--- a/examples/tools/show_FOV_ROIs/show_FOV_ROIs.py
+++ b/examples/tools/show_FOV_ROIs/show_FOV_ROIs.py
@@ -3,7 +3,9 @@
 """
 import matplotlib.pyplot as plt
 
-from fractal_tasks_core.lib_metadata_parsing import parse_yokogawa_metadata
+from fractal_tasks_core.cellvoyager.metadata import (
+    parse_yokogawa_metadata,
+)
 from fractal_tasks_core.lib_ROI_overlaps import run_overlap_check
 
 
diff --git a/fractal_tasks_core/__FRACTAL_MANIFEST__.json b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
index add1596ac..6428adb2f 100644
--- a/fractal_tasks_core/__FRACTAL_MANIFEST__.json
+++ b/fractal_tasks_core/__FRACTAL_MANIFEST__.json
@@ -393,12 +393,12 @@
             "description": "Pyramid level of the image to be segmented. Choose `0` to process at full resolution."
           },
           "channel": {
-            "$ref": "#/definitions/Channel",
+            "$ref": "#/definitions/ChannelInputModel",
             "title": "Channel",
             "description": "Primary channel for segmentation; requires either `wavelength_id` (e.g. `A01_C01`) or `label` (e.g. `DAPI`)."
           },
           "channel2": {
-            "$ref": "#/definitions/Channel",
+            "$ref": "#/definitions/ChannelInputModel",
             "title": "Channel2",
             "description": "Second channel for segmentation (in the same format as `channel`). If specified, cellpose runs in dual channel mode. For dual channel segmentation of cells, the first channel should contain the membrane marker, the second channel should contain the nuclear marker."
           },
@@ -505,8 +505,8 @@
         ],
         "additionalProperties": false,
         "definitions": {
-          "Channel": {
-            "title": "Channel",
+          "ChannelInputModel": {
+            "title": "ChannelInputModel",
             "description": "A channel which is specified by either `wavelength_id` or `label`.",
             "type": "object",
             "properties": {
@@ -709,8 +709,8 @@
         ],
         "additionalProperties": false,
         "definitions": {
-          "Channel": {
-            "title": "Channel",
+          "ChannelInputModel": {
+            "title": "ChannelInputModel",
             "description": "A channel which is specified by either `wavelength_id` or `label`.",
             "type": "object",
             "properties": {
@@ -746,9 +746,9 @@
                 "description": "Label name (for label inputs only)."
               },
               "channel": {
-                "$ref": "#/definitions/Channel",
+                "$ref": "#/definitions/ChannelInputModel",
                 "title": "Channel",
-                "description": "Channel object (for image inputs only)."
+                "description": "`ChannelInputModel` object (for image inputs only)."
               }
             },
             "required": [
diff --git a/fractal_tasks_core/__init__.py b/fractal_tasks_core/__init__.py
index a236d4d4d..162effc15 100644
--- a/fractal_tasks_core/__init__.py
+++ b/fractal_tasks_core/__init__.py
@@ -1,5 +1,6 @@
 import logging
 
+
 logging.basicConfig(
     level=logging.INFO, format="%(asctime)s; %(levelname)s; %(message)s"
 )
diff --git a/fractal_tasks_core/cellvoyager/__init__.py b/fractal_tasks_core/cellvoyager/__init__.py
new file mode 100644
index 000000000..74723f83a
--- /dev/null
+++ b/fractal_tasks_core/cellvoyager/__init__.py
@@ -0,0 +1,3 @@
+"""
+Subpackage with utilities for tasks converting CellVoyager images to OME-Zarr.
+"""
diff --git a/fractal_tasks_core/lib_parse_filename_metadata.py b/fractal_tasks_core/cellvoyager/filenames.py
similarity index 70%
rename from fractal_tasks_core/lib_parse_filename_metadata.py
rename to fractal_tasks_core/cellvoyager/filenames.py
index b7fe7e9b4..41b5cf274 100644
--- a/fractal_tasks_core/lib_parse_filename_metadata.py
+++ b/fractal_tasks_core/cellvoyager/filenames.py
@@ -10,10 +10,54 @@
 # Institute for Biomedical Research and Pelkmans Lab from the University of
 # Zurich.
 """
-Extract metadata from image filename.
+Auxiliary functions related to filenames of Yokogawa-microscope images.
 """
+import logging
 import re
+from glob import glob
 from pathlib import Path
+from typing import Sequence
+
+
+def glob_with_multiple_patterns(
+    *,
+    folder: str,
+    patterns: Sequence[str] = None,
+) -> set[str]:
+    """
+    List all the items (files and folders) in a given folder that
+    simultaneously match a series of glob patterns.
+
+    Args:
+        folder: Base folder where items will be searched.
+        patterns: If specified, the list of patterns (defined as in
+            https://docs.python.org/3/library/fnmatch.html) that item
+            names will match with.
+    """
+
+    # Sanitize base-folder path
+    if folder.endswith("/"):
+        actual_folder = folder[:-1]
+    else:
+        actual_folder = folder[:]
+
+    # If not pattern is specified, look for *all* items in the base folder
+    if not patterns:
+        patterns = ["*"]
+
+    # Combine multiple glob searches (via set intersection)
+    logging.info(f"[glob_with_multiple_patterns] {patterns=}")
+    items = None
+    for pattern in patterns:
+        new_matches = glob(f"{actual_folder}/{pattern}")
+        if items is None:
+            items = set(new_matches)
+        else:
+            items = items.intersection(new_matches)
+    items = items or set()
+    logging.info(f"[glob_with_multiple_patterns] Found {len(items)} items")
+
+    return items
 
 
 def _get_plate_name(plate_prefix: str) -> str:
diff --git a/fractal_tasks_core/lib_metadata_parsing.py b/fractal_tasks_core/cellvoyager/metadata.py
similarity index 100%
rename from fractal_tasks_core/lib_metadata_parsing.py
rename to fractal_tasks_core/cellvoyager/metadata.py
diff --git a/fractal_tasks_core/lib_channels.py b/fractal_tasks_core/channels.py
similarity index 92%
rename from fractal_tasks_core/lib_channels.py
rename to fractal_tasks_core/channels.py
index 4b79487a7..24a27dd7b 100644
--- a/fractal_tasks_core/lib_channels.py
+++ b/fractal_tasks_core/channels.py
@@ -26,8 +26,7 @@
 
 if __OME_NGFF_VERSION__ != "0.4":
     NotImplementedError(
-        f"OME NGFF {__OME_NGFF_VERSION__} is not supported "
-        "in `lib_channels.py`"
+        f"OME NGFF {__OME_NGFF_VERSION__} is not supported " "in `channels.py`"
     )
 
 
@@ -101,6 +100,40 @@ def valid_hex_color(cls, v, values):
         return v
 
 
+class ChannelInputModel(BaseModel):
+    """
+    A channel which is specified by either `wavelength_id` or `label`.
+
+    This model is similar to `OmeroChannel`, but it is used for
+    task-function arguments (and for generating appropriate JSON schemas).
+
+    Attributes:
+        wavelength_id: Unique ID for the channel wavelength, e.g. `A01_C01`.
+        label: Name of the channel.
+    """
+
+    wavelength_id: Optional[str] = None
+    label: Optional[str] = None
+
+    @validator("label", always=True)
+    def mutually_exclusive_channel_attributes(cls, v, values):
+        """
+        Check that either `label` or `wavelength_id` is set.
+        """
+        wavelength_id = values.get("wavelength_id")
+        label = v
+        if wavelength_id and v:
+            raise ValueError(
+                "`wavelength_id` and `label` cannot be both set "
+                f"(given {wavelength_id=} and {label=})."
+            )
+        if wavelength_id is None and v is None:
+            raise ValueError(
+                "`wavelength_id` and `label` cannot be both `None`"
+            )
+        return v
+
+
 class ChannelNotFoundError(ValueError):
     """
     Custom error for when `get_channel_from_list` fails,
diff --git a/fractal_tasks_core/dev/lib_args_schemas.py b/fractal_tasks_core/dev/lib_args_schemas.py
index 7bf53be84..bad4b4231 100644
--- a/fractal_tasks_core/dev/lib_args_schemas.py
+++ b/fractal_tasks_core/dev/lib_args_schemas.py
@@ -47,11 +47,19 @@
 _Schema = dict[str, Any]
 
 FRACTAL_TASKS_CORE_PYDANTIC_MODELS = [
-    ("fractal_tasks_core", "lib_channels.py", "OmeroChannel"),
-    ("fractal_tasks_core", "lib_channels.py", "Window"),
-    ("fractal_tasks_core", "lib_input_models.py", "Channel"),
-    ("fractal_tasks_core", "lib_input_models.py", "NapariWorkflowsInput"),
-    ("fractal_tasks_core", "lib_input_models.py", "NapariWorkflowsOutput"),
+    ("fractal_tasks_core", "channels.py", "OmeroChannel"),
+    ("fractal_tasks_core", "channels.py", "Window"),
+    ("fractal_tasks_core", "channels.py", "ChannelInputModel"),
+    (
+        "fractal_tasks_core",
+        "tasks/napari_workflows_wrapper_models.py",
+        "NapariWorkflowsInput",
+    ),
+    (
+        "fractal_tasks_core",
+        "tasks/napari_workflows_wrapper_models.py",
+        "NapariWorkflowsOutput",
+    ),
 ]
 
 
diff --git a/fractal_tasks_core/labels.py b/fractal_tasks_core/labels.py
new file mode 100644
index 000000000..2dff5ac88
--- /dev/null
+++ b/fractal_tasks_core/labels.py
@@ -0,0 +1,134 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Module which currently only hosts `prepare_label_group`.
+"""
+import logging
+from typing import Any
+from typing import Optional
+
+import zarr.hierarchy
+from pydantic.error_wrappers import ValidationError
+
+from fractal_tasks_core.ngff import NgffImageMeta
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
+
+
+def prepare_label_group(
+    image_group: zarr.hierarchy.Group,
+    label_name: str,
+    label_attrs: dict[str, Any],
+    overwrite: bool = False,
+    logger: Optional[logging.Logger] = None,
+) -> zarr.group:
+    """
+    Set the stage for writing labels to a zarr group
+
+    This helper function is similar to `write_table`, in that it prepares the
+    appropriate zarr groups (`labels` and the new-label one) and performs
+    `overwrite`-dependent checks. At a difference with `write_table`, this
+    function does not actually write the label array to the new zarr group;
+    such writing operation must take place in the actual task function, since
+    in fractal-tasks-core it is done sequentially on different `region`s of the
+    zarr array.
+
+    What this function does is:
+
+    1. Create the `labels` group, if needed.
+    2. If `overwrite=False`, check that the new label does not exist (either in
+       zarr attributes or as a zarr sub-group).
+    3. Update the `labels` attribute of the image group.
+    4. If `label_attrs` is set, include this set of attributes in the
+       new-label zarr group.
+
+    Args:
+        image_group:
+            The group to write to.
+        label_name:
+            The name of the new label; this name also overrides the multiscale
+            name in NGFF-image Zarr attributes, if needed.
+        overwrite:
+            If `False`, check that the new label does not exist (either in zarr
+            attributes or as a zarr sub-group); if `True` propagate parameter
+            to `create_group` method, making it overwrite any existing
+            sub-group with the given name.
+        label_attrs:
+            Zarr attributes of the label-image group.
+        logger:
+            The logger to use (if unset, use `logging.getLogger(None)`).
+
+    Returns:
+        Zarr group of the new label.
+    """
+
+    # Set logger
+    if logger is None:
+        logger = logging.getLogger(None)
+
+    # Create labels group (if needed) and extract current_labels
+    if "labels" not in set(image_group.group_keys()):
+        labels_group = image_group.create_group("labels", overwrite=False)
+    else:
+        labels_group = image_group["labels"]
+    current_labels = labels_group.attrs.asdict().get("labels", [])
+
+    # If overwrite=False, check that the new label does not exist (either as a
+    # zarr sub-group or as part of the zarr-group attributes)
+    if not overwrite:
+        if label_name in set(labels_group.group_keys()):
+            error_msg = (
+                f"Sub-group '{label_name}' of group {image_group.store.path} "
+                f"already exists, but `{overwrite=}`.\n"
+                "Hint: try setting `overwrite=True`."
+            )
+            logger.error(error_msg)
+            raise OverwriteNotAllowedError(error_msg)
+        if label_name in current_labels:
+            error_msg = (
+                f"Item '{label_name}' already exists in `labels` attribute of "
+                f"group {image_group.store.path}, but `{overwrite=}`.\n"
+                "Hint: try setting `overwrite=True`."
+            )
+            logger.error(error_msg)
+            raise OverwriteNotAllowedError(error_msg)
+
+    # Update the `labels` metadata of the image group, if needed
+    if label_name not in current_labels:
+        new_labels = current_labels + [label_name]
+        labels_group.attrs["labels"] = new_labels
+
+    # Define new-label group
+    label_group = labels_group.create_group(label_name, overwrite=overwrite)
+
+    # Validate attrs against NGFF specs 0.4
+    try:
+        meta = NgffImageMeta(**label_attrs)
+    except ValidationError as e:
+        error_msg = (
+            "Label attributes do not comply with NGFF image "
+            "specifications, as encoded in fractal-tasks-core.\n"
+            f"Original error:\nValidationError: {str(e)}"
+        )
+        logger.error(error_msg)
+        raise ValueError(error_msg)
+    # Replace multiscale name with label_name, if needed
+    current_multiscale_name = meta.multiscale.name
+    if current_multiscale_name != label_name:
+        logger.warning(
+            f"Setting multiscale name to '{label_name}' (old value: "
+            f"'{current_multiscale_name}') in label-image NGFF "
+            "attributes."
+        )
+        label_attrs["multiscales"][0]["name"] = label_name
+    # Overwrite label_group attributes with label_attrs key/value pairs
+    label_group.attrs.put(label_attrs)
+
+    return label_group
diff --git a/fractal_tasks_core/lib_glob.py b/fractal_tasks_core/lib_glob.py
deleted file mode 100644
index 65d628ed6..000000000
--- a/fractal_tasks_core/lib_glob.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Auxiliary functions related to globbing (i.e. listing some items from a
-directory).
-"""
-import logging
-from glob import glob
-from typing import Sequence
-
-
-def glob_with_multiple_patterns(
-    *,
-    folder: str,
-    patterns: Sequence[str] = None,
-) -> set[str]:
-    """
-    List all the items (files and folders) in a given folder that
-    simultaneously match a series of glob patterns.
-
-    Args:
-        folder: Base folder where items will be searched.
-        patterns: If specified, the list of patterns (defined as in
-            https://docs.python.org/3/library/fnmatch.html) that item
-            names will match with.
-    """
-
-    # Sanitize base-folder path
-    if folder.endswith("/"):
-        actual_folder = folder[:-1]
-    else:
-        actual_folder = folder[:]
-
-    # If not pattern is specified, look for *all* items in the base folder
-    if not patterns:
-        patterns = ["*"]
-
-    # Combine multiple glob searches (via set intersection)
-    logging.info(f"[glob_with_multiple_patterns] {patterns=}")
-    items = None
-    for pattern in patterns:
-        new_matches = glob(f"{actual_folder}/{pattern}")
-        if items is None:
-            items = set(new_matches)
-        else:
-            items = items.intersection(new_matches)
-    items = items or set()
-    logging.info(f"[glob_with_multiple_patterns] Found {len(items)} items")
-
-    return items
diff --git a/fractal_tasks_core/lib_read_fractal_metadata.py b/fractal_tasks_core/lib_read_fractal_metadata.py
deleted file mode 100644
index 09adc2cdc..000000000
--- a/fractal_tasks_core/lib_read_fractal_metadata.py
+++ /dev/null
@@ -1,107 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Helper functions to extract parameters from metadata dictionary.
-"""
-from pathlib import Path
-from typing import Any
-from typing import Dict
-from typing import Sequence
-from typing import Union
-
-import zarr
-
-
-def find_omengff_acquisition(image_zarr_path: Path) -> Union[int, None]:
-    """
-    Discover the acquisition index based on OME-NGFF metadata.
-
-    Given the path to a zarr image folder (e.g. `/path/plate.zarr/B/03/0`),
-    extract the acquisition index from the `.zattrs` file of the parent
-    folder (i.e. at the well level), or return `None` if acquisition is not
-    specified.
-
-    Notes:
-
-    1. For non-multiplexing datasets, acquisition is not a required
-       information in the metadata. If it is not there, this function
-       returns `None`.
-    2. This function fails if we use an image that does not belong to
-       an OME-NGFF well.
-
-    Args:
-        image_zarr_path: Full path to an OME-NGFF image folder.
-    """
-
-    # Identify well path and attrs
-    well_zarr_path = image_zarr_path.parent
-    if not (well_zarr_path / ".zattrs").exists():
-        raise ValueError(
-            f"{str(well_zarr_path)} must be an OME-NGFF well "
-            "folder, but it does not include a .zattrs file."
-        )
-    well_group = zarr.open_group(str(well_zarr_path))
-    attrs_images = well_group.attrs["well"]["images"]
-
-    # Loook for the acquisition of the current image (if any)
-    acquisition = None
-    for img_dict in attrs_images:
-        if (
-            img_dict["path"] == image_zarr_path.name
-            and "acquisition" in img_dict.keys()
-        ):
-            acquisition = img_dict["acquisition"]
-            break
-
-    return acquisition
-
-
-def get_parameters_from_metadata(
-    *,
-    keys: Sequence[str],
-    metadata: Dict[str, Any],
-    image_zarr_path: Path,
-) -> Dict[str, Any]:
-    """
-    Flexibly extract parameters from metadata dictionary
-
-    This covers both parameters which are acquisition-specific (if the image
-    belongs to an OME-NGFF array and its acquisition is specified) or simply
-    available in the dictionary.
-    The two cases are handled as:
-    ```
-    metadata[acquisition]["some_parameter"]  # acquisition available
-    metadata["some_parameter"]               # acquisition not available
-    ```
-
-    Args:
-        keys: list of required parameters.
-        metadata: metadata dictionary.
-        image_zarr_path: full path to image, e.g. `/path/plate.zarr/B/03/0`.
-    """
-
-    parameters = {}
-    acquisition = find_omengff_acquisition(image_zarr_path)
-    if acquisition is not None:
-        parameters["acquisition"] = acquisition
-
-    for key in keys:
-        if acquisition is None:
-            parameter = metadata[key]
-        else:
-            try:
-                parameter = metadata[key][str(acquisition)]
-            except TypeError:
-                parameter = metadata[key]
-            except KeyError:
-                parameter = metadata[key]
-        parameters[key] = parameter
-    return parameters
diff --git a/fractal_tasks_core/lib_write.py b/fractal_tasks_core/lib_write.py
deleted file mode 100644
index ec19d2e1d..000000000
--- a/fractal_tasks_core/lib_write.py
+++ /dev/null
@@ -1,297 +0,0 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Module with custom wrappers of the Zarr API.
-"""
-import logging
-from collections.abc import MutableMapping
-from typing import Any
-from typing import Optional
-from typing import Union
-
-import zarr.hierarchy
-from anndata.experimental import write_elem
-from pydantic.error_wrappers import ValidationError
-from zarr.errors import ContainsGroupError
-from zarr.errors import GroupNotFoundError
-
-from .lib_ngff import NgffImageMeta
-
-
-class OverwriteNotAllowedError(RuntimeError):
-    pass
-
-
-def open_zarr_group_with_overwrite(
-    path: Union[str, MutableMapping],
-    *,
-    overwrite: bool,
-    logger: Optional[logging.Logger] = None,
-    **open_group_kwargs: Any,
-) -> zarr.hierarchy.Group:
-    """
-    Wrap `zarr.open_group` and add `overwrite` argument.
-
-    This wrapper sets `mode="w"` for `overwrite=True` and `mode="w-"` for
-    `overwrite=False`.
-
-    The expected behavior is
-
-
-    * if the group does not exist, create it (independently on `overwrite`);
-    * if the group already exists and `overwrite=True`, replace the group with
-      an empty one;
-    * if the group already exists and `overwrite=False`, fail.
-
-    From the [`zarr.open_group`
-    docs](https://zarr.readthedocs.io/en/stable/api/hierarchy.html#zarr.hierarchy.open_group):
-
-    * `mode="r"` means read only (must exist);
-    * `mode="r+"` means read/write (must exist);
-    * `mode="a"` means read/write (create if doesn’t exist);
-    * `mode="w"` means create (overwrite if exists);
-    * `mode="w-"` means create (fail if exists).
-
-
-    Args:
-        path:
-            Store or path to directory in file system or name of zip file
-            (`zarr.open_group` parameter).
-        overwrite:
-            Determines the `mode` parameter of `zarr.open_group`, which is
-            `"w"` (if `overwrite=True`) or `"w-"` (if `overwrite=False`).
-        logger:
-            The logger to use (if unset, use `logging.getLogger(None)`)
-        open_group_kwargs:
-            Keyword arguments of `zarr.open_group`.
-
-    Returns:
-        The zarr group.
-
-    Raises:
-        OverwriteNotAllowedError:
-            If `overwrite=False` and the group already exists.
-    """
-
-    # Set logger
-    if logger is None:
-        logger = logging.getLogger(None)
-
-    # Set mode for zarr.open_group
-    if overwrite:
-        new_mode = "w"
-    else:
-        new_mode = "w-"
-
-    # Write log about current status
-    logger.info(f"Start open_zarr_group_with_overwrite ({overwrite=}).")
-    try:
-        # Call `zarr.open_group` with `mode="r"`, which fails for missing group
-        current_group = zarr.open_group(path, mode="r")
-        keys = list(current_group.group_keys())
-        logger.info(f"Zarr group {path} already exists, with {keys=}")
-    except GroupNotFoundError:
-        logger.info(f"Zarr group {path} does not exist yet.")
-
-    # Raise warning if we are overriding an existing value of `mode`
-    if "mode" in open_group_kwargs.keys():
-        mode = open_group_kwargs.pop("mode")
-        logger.warning(
-            f"Overriding {mode=} with {new_mode=}, "
-            "in open_zarr_group_with_overwrite"
-        )
-
-    # Call zarr.open_group
-    try:
-        return zarr.open_group(path, mode=new_mode, **open_group_kwargs)
-    except ContainsGroupError:
-        # Re-raise error with custom message and type
-        error_msg = (
-            f"Cannot create zarr group at {path=} with `{overwrite=}` "
-            "(original error: `zarr.errors.ContainsGroupError`).\n"
-            "Hint: try setting `overwrite=True`."
-        )
-        logger.error(error_msg)
-        raise OverwriteNotAllowedError(error_msg)
-
-
-def _write_elem_with_overwrite(
-    group: zarr.hierarchy.Group,
-    key: str,
-    elem: Any,
-    *,
-    overwrite: bool,
-    logger: Optional[logging.Logger] = None,
-) -> None:
-    """
-    Wrap `anndata.experimental.write_elem`, to include `overwrite` parameter.
-
-    See docs for the original function
-    [here](https://anndata.readthedocs.io/en/stable/generated/anndata.experimental.write_elem.html).
-
-    This function writes `elem` to the sub-group `key` of `group`. The
-    `overwrite`-related expected behavior is:
-
-    * if the sub-group does not exist, create it (independently on
-      `overwrite`);
-    * if the sub-group already exists and `overwrite=True`, overwrite the
-      sub-group;
-    * if the sub-group already exists and `overwrite=False`, fail.
-
-    Note that this version of the wrapper does not include the original
-    `dataset_kwargs` parameter.
-
-    Args:
-        group:
-            The group to write to.
-        key:
-            The key to write to in the group. Note that absolute paths will be
-            written from the root.
-        elem:
-            The element to write. Typically an in-memory object, e.g. an
-            AnnData, pandas dataframe, scipy sparse matrix, etc.
-        overwrite:
-            If `True`, overwrite the `key` sub-group (if present); if `False`
-            and `key` sub-group exists, raise an error.
-        logger:
-            The logger to use (if unset, use `logging.getLogger(None)`)
-
-    Raises:
-        OverwriteNotAllowedError:
-            If `overwrite=False` and the sub-group already exists.
-    """
-
-    # Set logger
-    if logger is None:
-        logger = logging.getLogger(None)
-
-    if key in set(group.group_keys()):
-        if not overwrite:
-            error_msg = (
-                f"Sub-group '{key}' of group {group.store.path} "
-                f"already exists, but `{overwrite=}`.\n"
-                "Hint: try setting `overwrite=True`."
-            )
-            logger.error(error_msg)
-            raise OverwriteNotAllowedError(error_msg)
-    write_elem(group, key, elem)
-
-
-def prepare_label_group(
-    image_group: zarr.hierarchy.Group,
-    label_name: str,
-    label_attrs: dict[str, Any],
-    overwrite: bool = False,
-    logger: Optional[logging.Logger] = None,
-) -> zarr.group:
-    """
-    Set the stage for writing labels to a zarr group
-
-    This helper function is similar to `write_table`, in that it prepares the
-    appropriate zarr groups (`labels` and the new-label one) and performs
-    `overwrite`-dependent checks. At a difference with `write_table`, this
-    function does not actually write the label array to the new zarr group;
-    such writing operation must take place in the actual task function, since
-    in fractal-tasks-core it is done sequentially on different `region`s of the
-    zarr array.
-
-    What this function does is:
-
-    1. Create the `labels` group, if needed.
-    2. If `overwrite=False`, check that the new label does not exist (either in
-       zarr attributes or as a zarr sub-group).
-    3. Update the `labels` attribute of the image group.
-    4. If `label_attrs` is set, include this set of attributes in the
-       new-label zarr group.
-
-    Args:
-        image_group:
-            The group to write to.
-        label_name:
-            The name of the new label; this name also overrides the multiscale
-            name in NGFF-image Zarr attributes, if needed.
-        overwrite:
-            If `False`, check that the new label does not exist (either in zarr
-            attributes or as a zarr sub-group); if `True` propagate parameter
-            to `create_group` method, making it overwrite any existing
-            sub-group with the given name.
-        label_attrs:
-            Zarr attributes of the label-image group.
-        logger:
-            The logger to use (if unset, use `logging.getLogger(None)`).
-
-    Returns:
-        Zarr group of the new label.
-    """
-
-    # Set logger
-    if logger is None:
-        logger = logging.getLogger(None)
-
-    # Create labels group (if needed) and extract current_labels
-    if "labels" not in set(image_group.group_keys()):
-        labels_group = image_group.create_group("labels", overwrite=False)
-    else:
-        labels_group = image_group["labels"]
-    current_labels = labels_group.attrs.asdict().get("labels", [])
-
-    # If overwrite=False, check that the new label does not exist (either as a
-    # zarr sub-group or as part of the zarr-group attributes)
-    if not overwrite:
-        if label_name in set(labels_group.group_keys()):
-            error_msg = (
-                f"Sub-group '{label_name}' of group {image_group.store.path} "
-                f"already exists, but `{overwrite=}`.\n"
-                "Hint: try setting `overwrite=True`."
-            )
-            logger.error(error_msg)
-            raise OverwriteNotAllowedError(error_msg)
-        if label_name in current_labels:
-            error_msg = (
-                f"Item '{label_name}' already exists in `labels` attribute of "
-                f"group {image_group.store.path}, but `{overwrite=}`.\n"
-                "Hint: try setting `overwrite=True`."
-            )
-            logger.error(error_msg)
-            raise OverwriteNotAllowedError(error_msg)
-
-    # Update the `labels` metadata of the image group, if needed
-    if label_name not in current_labels:
-        new_labels = current_labels + [label_name]
-        labels_group.attrs["labels"] = new_labels
-
-    # Define new-label group
-    label_group = labels_group.create_group(label_name, overwrite=overwrite)
-
-    # Validate attrs against NGFF specs 0.4
-    try:
-        meta = NgffImageMeta(**label_attrs)
-    except ValidationError as e:
-        error_msg = (
-            "Label attributes do not comply with NGFF image "
-            "specifications, as encoded in fractal-tasks-core.\n"
-            f"Original error:\nValidationError: {str(e)}"
-        )
-        logger.error(error_msg)
-        raise ValueError(error_msg)
-    # Replace multiscale name with label_name, if needed
-    current_multiscale_name = meta.multiscale.name
-    if current_multiscale_name != label_name:
-        logger.warning(
-            f"Setting multiscale name to '{label_name}' (old value: "
-            f"'{current_multiscale_name}') in label-image NGFF "
-            "attributes."
-        )
-        label_attrs["multiscales"][0]["name"] = label_name
-    # Overwrite label_group attributes with label_attrs key/value pairs
-    label_group.attrs.put(label_attrs)
-
-    return label_group
diff --git a/fractal_tasks_core/lib_masked_loading.py b/fractal_tasks_core/masked_loading.py
similarity index 97%
rename from fractal_tasks_core/lib_masked_loading.py
rename to fractal_tasks_core/masked_loading.py
index 10d4367b5..d0ecf8c4f 100644
--- a/fractal_tasks_core/lib_masked_loading.py
+++ b/fractal_tasks_core/masked_loading.py
@@ -21,9 +21,9 @@
 import numpy as np
 import zarr
 
-from fractal_tasks_core.lib_tables.v1 import MaskingROITableAttrs
-from fractal_tasks_core.lib_upscale_array import convert_region_to_low_res
-from fractal_tasks_core.lib_upscale_array import upscale_array
+from fractal_tasks_core.tables.v1 import MaskingROITableAttrs
+from fractal_tasks_core.upscale_array import convert_region_to_low_res
+from fractal_tasks_core.upscale_array import upscale_array
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/ngff/__init__.py b/fractal_tasks_core/ngff/__init__.py
new file mode 100644
index 000000000..63b8a9411
--- /dev/null
+++ b/fractal_tasks_core/ngff/__init__.py
@@ -0,0 +1,13 @@
+"""
+Subpackage encoding OME-NGFF specifications 0.4 and providing Zarr-related
+tools.
+
+Note: this `__init__.py` file only exports the most relevant symbols, that is,
+the ones that are used outside this subpackage.
+"""
+from .specs import NgffImageMeta  # noqa
+from .specs import NgffWellMeta  # noqa
+from .zarr_utils import detect_ome_ngff_type  # noqa
+from .zarr_utils import load_NgffImageMeta  # noqa
+from .zarr_utils import load_NgffWellMeta  # noqa
+from .zarr_utils import ZarrGroupNotFoundError  # noqa
diff --git a/fractal_tasks_core/lib_ngff.py b/fractal_tasks_core/ngff/specs.py
similarity index 79%
rename from fractal_tasks_core/lib_ngff.py
rename to fractal_tasks_core/ngff/specs.py
index 910e0a8fd..8a9692530 100644
--- a/fractal_tasks_core/lib_ngff.py
+++ b/fractal_tasks_core/ngff/specs.py
@@ -1,26 +1,15 @@
 """
-Pydantic models related to OME-NGFF 0.4 specs.
+Pydantic models related to OME-NGFF 0.4 specs, as implemented in
+fractal-tasks-core.
 """
 import logging
 from typing import Literal
 from typing import Optional
 from typing import Union
 
-import zarr.hierarchy
 from pydantic import BaseModel
 from pydantic import Field
 from pydantic import validator
-from zarr.errors import GroupNotFoundError
-
-
-class ZarrGroupNotFoundError(ValueError):
-    """
-    Wrap zarr.errors.GroupNotFoundError
-
-    This is used to provide a user-friendly error message.
-    """
-
-    pass
 
 
 logger = logging.getLogger(__name__)
@@ -387,91 +376,3 @@ def get_acquisition_paths(self) -> dict[int, str]:
                 )
             acquisition_dict[image.acquisition] = image.path
         return acquisition_dict
-
-
-def load_NgffImageMeta(zarr_path: str) -> NgffImageMeta:
-    """
-    Load the attributes of a zarr group and cast them to `NgffImageMeta`.
-
-    Args:
-        zarr_path: Path to the zarr group.
-
-    Returns:
-        A new `NgffImageMeta` object.
-    """
-    try:
-        zarr_group = zarr.open_group(zarr_path, mode="r")
-    except GroupNotFoundError:
-        error_msg = (
-            "Could not load attributes for the requested image, "
-            f"because no Zarr image was found at {zarr_path}"
-        )
-        logging.error(error_msg)
-        raise ZarrGroupNotFoundError(error_msg)
-    zarr_attrs = zarr_group.attrs.asdict()
-    try:
-        return NgffImageMeta(**zarr_attrs)
-    except Exception as e:
-        logging.error(
-            f"Contents of {zarr_path} cannot be cast to NgffImageMeta.\n"
-            f"Original error:\n{str(e)}"
-        )
-        raise e
-
-
-def load_NgffWellMeta(zarr_path: str) -> NgffWellMeta:
-    """
-    Load the attributes of a zarr group and cast them to `NgffWellMeta`.
-
-    Args:
-        zarr_path: Path to the zarr group.
-
-    Returns:
-        A new `NgffWellMeta` object.
-    """
-    try:
-        zarr_group = zarr.open_group(zarr_path, mode="r")
-    except GroupNotFoundError:
-        error_msg = (
-            "Could not load attributes for the requested well, "
-            f"because no Zarr image was found at {zarr_path}"
-        )
-        logging.error(error_msg)
-        raise ZarrGroupNotFoundError(error_msg)
-    zarr_attrs = zarr_group.attrs.asdict()
-    try:
-        return NgffWellMeta(**zarr_attrs)
-    except Exception as e:
-        logging.error(
-            f"Contents of {zarr_path} cannot be cast to NgffWellMeta.\n"
-            f"Original error:\n{str(e)}"
-        )
-        raise e
-
-
-def detect_ome_ngff_type(group: zarr.hierarchy.Group) -> str:
-    """
-    Given a Zarr group, find whether it is an OME-NGFF plate, well or image.
-
-    Args:
-        group: Zarr group
-
-    Returns:
-        The detected OME-NGFF type (`plate`, `well` or `image`).
-    """
-    attrs = group.attrs.asdict()
-    if "plate" in attrs.keys():
-        ngff_type = "plate"
-    elif "well" in attrs.keys():
-        ngff_type = "well"
-    elif "multiscales" in attrs.keys():
-        ngff_type = "image"
-    else:
-        error_msg = (
-            "Zarr group at cannot be identified as one "
-            "of OME-NGFF plate/well/image groups."
-        )
-        logger.error(error_msg)
-        raise ValueError(error_msg)
-    logger.info(f"Zarr group identified as OME-NGFF {ngff_type}.")
-    return ngff_type
diff --git a/fractal_tasks_core/ngff/zarr_utils.py b/fractal_tasks_core/ngff/zarr_utils.py
new file mode 100644
index 000000000..ccfcb424b
--- /dev/null
+++ b/fractal_tasks_core/ngff/zarr_utils.py
@@ -0,0 +1,110 @@
+"""
+Utilities to work with the Pydantic models from `specs.py` for Zarr groups.
+"""
+import logging
+
+import zarr.hierarchy
+from zarr.errors import GroupNotFoundError
+
+from fractal_tasks_core.ngff.specs import NgffImageMeta
+from fractal_tasks_core.ngff.specs import NgffWellMeta
+
+logger = logging.getLogger(__name__)
+
+
+class ZarrGroupNotFoundError(ValueError):
+    """
+    Wrap zarr.errors.GroupNotFoundError
+
+    This is used to provide a user-friendly error message.
+    """
+
+    pass
+
+
+def load_NgffImageMeta(zarr_path: str) -> NgffImageMeta:
+    """
+    Load the attributes of a zarr group and cast them to `NgffImageMeta`.
+
+    Args:
+        zarr_path: Path to the zarr group.
+
+    Returns:
+        A new `NgffImageMeta` object.
+    """
+    try:
+        zarr_group = zarr.open_group(zarr_path, mode="r")
+    except GroupNotFoundError:
+        error_msg = (
+            "Could not load attributes for the requested image, "
+            f"because no Zarr image was found at {zarr_path}"
+        )
+        logging.error(error_msg)
+        raise ZarrGroupNotFoundError(error_msg)
+    zarr_attrs = zarr_group.attrs.asdict()
+    try:
+        return NgffImageMeta(**zarr_attrs)
+    except Exception as e:
+        logging.error(
+            f"Contents of {zarr_path} cannot be cast to NgffImageMeta.\n"
+            f"Original error:\n{str(e)}"
+        )
+        raise e
+
+
+def load_NgffWellMeta(zarr_path: str) -> NgffWellMeta:
+    """
+    Load the attributes of a zarr group and cast them to `NgffWellMeta`.
+
+    Args:
+        zarr_path: Path to the zarr group.
+
+    Returns:
+        A new `NgffWellMeta` object.
+    """
+    try:
+        zarr_group = zarr.open_group(zarr_path, mode="r")
+    except GroupNotFoundError:
+        error_msg = (
+            "Could not load attributes for the requested well, "
+            f"because no Zarr image was found at {zarr_path}"
+        )
+        logging.error(error_msg)
+        raise ZarrGroupNotFoundError(error_msg)
+    zarr_attrs = zarr_group.attrs.asdict()
+    try:
+        return NgffWellMeta(**zarr_attrs)
+    except Exception as e:
+        logging.error(
+            f"Contents of {zarr_path} cannot be cast to NgffWellMeta.\n"
+            f"Original error:\n{str(e)}"
+        )
+        raise e
+
+
+def detect_ome_ngff_type(group: zarr.hierarchy.Group) -> str:
+    """
+    Given a Zarr group, find whether it is an OME-NGFF plate, well or image.
+
+    Args:
+        group: Zarr group
+
+    Returns:
+        The detected OME-NGFF type (`plate`, `well` or `image`).
+    """
+    attrs = group.attrs.asdict()
+    if "plate" in attrs.keys():
+        ngff_type = "plate"
+    elif "well" in attrs.keys():
+        ngff_type = "well"
+    elif "multiscales" in attrs.keys():
+        ngff_type = "image"
+    else:
+        error_msg = (
+            "Zarr group at cannot be identified as one "
+            "of OME-NGFF plate/well/image groups."
+        )
+        logger.error(error_msg)
+        raise ValueError(error_msg)
+    logger.info(f"Zarr group identified as OME-NGFF {ngff_type}.")
+    return ngff_type
diff --git a/fractal_tasks_core/lib_pyramid_creation.py b/fractal_tasks_core/pyramids.py
similarity index 100%
rename from fractal_tasks_core/lib_pyramid_creation.py
rename to fractal_tasks_core/pyramids.py
diff --git a/fractal_tasks_core/lib_regions_of_interest/__init__.py b/fractal_tasks_core/roi/__init__.py
similarity index 100%
rename from fractal_tasks_core/lib_regions_of_interest/__init__.py
rename to fractal_tasks_core/roi/__init__.py
diff --git a/fractal_tasks_core/lib_regions_of_interest/_overlaps_common.py b/fractal_tasks_core/roi/_overlaps_common.py
similarity index 100%
rename from fractal_tasks_core/lib_regions_of_interest/_overlaps_common.py
rename to fractal_tasks_core/roi/_overlaps_common.py
diff --git a/fractal_tasks_core/lib_regions_of_interest/load_region.py b/fractal_tasks_core/roi/load_region.py
similarity index 100%
rename from fractal_tasks_core/lib_regions_of_interest/load_region.py
rename to fractal_tasks_core/roi/load_region.py
diff --git a/fractal_tasks_core/lib_regions_of_interest/v1.py b/fractal_tasks_core/roi/v1.py
similarity index 100%
rename from fractal_tasks_core/lib_regions_of_interest/v1.py
rename to fractal_tasks_core/roi/v1.py
diff --git a/fractal_tasks_core/lib_regions_of_interest/v1_checks.py b/fractal_tasks_core/roi/v1_checks.py
similarity index 98%
rename from fractal_tasks_core/lib_regions_of_interest/v1_checks.py
rename to fractal_tasks_core/roi/v1_checks.py
index a3dcdc0e3..aa46366b7 100644
--- a/fractal_tasks_core/lib_regions_of_interest/v1_checks.py
+++ b/fractal_tasks_core/roi/v1_checks.py
@@ -19,7 +19,7 @@
 import zarr
 from pydantic.error_wrappers import ValidationError
 
-from ..lib_tables.v1 import MaskingROITableAttrs
+from fractal_tasks_core.tables.v1 import MaskingROITableAttrs
 
 
 logger = logging.getLogger(__name__)
diff --git a/fractal_tasks_core/lib_regions_of_interest/v1_overlaps.py b/fractal_tasks_core/roi/v1_overlaps.py
similarity index 97%
rename from fractal_tasks_core/lib_regions_of_interest/v1_overlaps.py
rename to fractal_tasks_core/roi/v1_overlaps.py
index 92b6b4246..90dfec6d6 100644
--- a/fractal_tasks_core/lib_regions_of_interest/v1_overlaps.py
+++ b/fractal_tasks_core/roi/v1_overlaps.py
@@ -20,10 +20,10 @@
 
 import pandas as pd
 
-from ._overlaps_common import _is_overlapping_3D_int
-from ._overlaps_common import is_overlapping_1D
-from ._overlaps_common import is_overlapping_2D
-from ._overlaps_common import is_overlapping_3D
+from fractal_tasks_core.roi._overlaps_common import _is_overlapping_3D_int
+from fractal_tasks_core.roi._overlaps_common import is_overlapping_1D
+from fractal_tasks_core.roi._overlaps_common import is_overlapping_2D
+from fractal_tasks_core.roi._overlaps_common import is_overlapping_3D
 
 
 logger = logging.getLogger(__name__)
diff --git a/fractal_tasks_core/lib_tables/__init__.py b/fractal_tasks_core/tables/__init__.py
similarity index 92%
rename from fractal_tasks_core/lib_tables/__init__.py
rename to fractal_tasks_core/tables/__init__.py
index aaa1d4244..7df4ca1a1 100644
--- a/fractal_tasks_core/lib_tables/__init__.py
+++ b/fractal_tasks_core/tables/__init__.py
@@ -9,8 +9,9 @@
 # Institute for Biomedical Research and Pelkmans Lab from the University of
 # Zurich.
 """
-Functions and classes related to table specifications (see
-https://fractal-analytics-platform.github.io/fractal-tasks-core/tables)."""
+Subpackage with functions and classes related to table specifications (see
+https://fractal-analytics-platform.github.io/fractal-tasks-core/tables).
+"""
 from typing import Any
 from typing import Optional
 
@@ -35,6 +36,7 @@ def write_table(
     This is the general interface that should allow for a smooth coexistence of
     tables with different `fractal_table_version` values. Currently only V1 is
     defined and implemented. The assumption is that V2 should only change:
+
     1. The lower-level writing function (that is, `_write_table_v2`).
     2. The type of the table (which would also reflect into a more general type
         hint for `table`, in the current funciton);
@@ -54,9 +56,8 @@ def write_table(
         overwrite:
             If `False`, check that the new table does not exist (either as a
             zarr sub-group or as part of the zarr-group attributes). In all
-            cases, propagate parameter to `_write_elem_with_overwrite`, to
-            determine the behavior in case of an existing sub-group named as
-            `table_name`.
+            cases, propagate parameter to low-level functions, to determine the
+            behavior in case of an existing sub-group named as in `table_name`.
         table_type: `type` attribute for the table; in case `type` is also
             present in `table_attrs`, this function argument takes priority.
         table_attrs:
diff --git a/fractal_tasks_core/lib_tables/v1.py b/fractal_tasks_core/tables/v1.py
similarity index 77%
rename from fractal_tasks_core/lib_tables/v1.py
rename to fractal_tasks_core/tables/v1.py
index f99442ff6..9b77a5e1d 100644
--- a/fractal_tasks_core/lib_tables/v1.py
+++ b/fractal_tasks_core/tables/v1.py
@@ -10,12 +10,12 @@
 
 import anndata as ad
 import zarr.hierarchy
+from anndata.experimental import write_elem
 from pydantic import BaseModel
 from pydantic import validator
 from pydantic.error_wrappers import ValidationError
 
-from ..lib_write import _write_elem_with_overwrite
-from ..lib_write import OverwriteNotAllowedError
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 logger = logging.getLogger(__name__)
 
@@ -60,6 +60,68 @@ def warning_for_old_table_type(cls, v):
         return v
 
 
+def _write_elem_with_overwrite(
+    group: zarr.hierarchy.Group,
+    key: str,
+    elem: Any,
+    *,
+    overwrite: bool,
+    logger: Optional[logging.Logger] = None,
+) -> None:
+    """
+    Wrap `anndata.experimental.write_elem`, to include `overwrite` parameter.
+
+    See docs for the original function
+    [here](https://anndata.readthedocs.io/en/stable/generated/anndata.experimental.write_elem.html).
+
+    This function writes `elem` to the sub-group `key` of `group`. The
+    `overwrite`-related expected behavior is:
+
+    * if the sub-group does not exist, create it (independently on
+      `overwrite`);
+    * if the sub-group already exists and `overwrite=True`, overwrite the
+      sub-group;
+    * if the sub-group already exists and `overwrite=False`, fail.
+
+    Note that this version of the wrapper does not include the original
+    `dataset_kwargs` parameter.
+
+    Args:
+        group:
+            The group to write to.
+        key:
+            The key to write to in the group. Note that absolute paths will be
+            written from the root.
+        elem:
+            The element to write. Typically an in-memory object, e.g. an
+            AnnData, pandas dataframe, scipy sparse matrix, etc.
+        overwrite:
+            If `True`, overwrite the `key` sub-group (if present); if `False`
+            and `key` sub-group exists, raise an error.
+        logger:
+            The logger to use (if unset, use `logging.getLogger(None)`)
+
+    Raises:
+        OverwriteNotAllowedError:
+            If `overwrite=False` and the sub-group already exists.
+    """
+
+    # Set logger
+    if logger is None:
+        logger = logging.getLogger(None)
+
+    if key in set(group.group_keys()):
+        if not overwrite:
+            error_msg = (
+                f"Sub-group '{key}' of group {group.store.path} "
+                f"already exists, but `{overwrite=}`.\n"
+                "Hint: try setting `overwrite=True`."
+            )
+            logger.error(error_msg)
+            raise OverwriteNotAllowedError(error_msg)
+    write_elem(group, key, elem)
+
+
 def _write_table_v1(
     image_group: zarr.hierarchy.Group,
     table_name: str,
diff --git a/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py b/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py
index ed05e1d5f..b9268f802 100644
--- a/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py
+++ b/fractal_tasks_core/tasks/apply_registration_to_ROI_tables.py
@@ -24,11 +24,11 @@
 import zarr
 from pydantic.decorator import validate_arguments
 
-from fractal_tasks_core.lib_ngff import load_NgffWellMeta
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.ngff import load_NgffWellMeta
+from fractal_tasks_core.roi import (
     are_ROI_table_columns_valid,
 )
-from fractal_tasks_core.lib_tables import write_table
+from fractal_tasks_core.tables import write_table
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/apply_registration_to_image.py b/fractal_tasks_core/tasks/apply_registration_to_image.py
index 174f01e69..d158dd46b 100644
--- a/fractal_tasks_core/tasks/apply_registration_to_image.py
+++ b/fractal_tasks_core/tasks/apply_registration_to_image.py
@@ -26,18 +26,18 @@
 import zarr
 from pydantic.decorator import validate_arguments
 
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import (
     convert_indices_to_regions,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import is_standard_roi_table
-from fractal_tasks_core.lib_regions_of_interest import load_region
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_zattrs_utils import get_table_path_dict
+from fractal_tasks_core.roi import is_standard_roi_table
+from fractal_tasks_core.roi import load_region
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.utils import get_table_path_dict
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/calculate_registration_image_based.py b/fractal_tasks_core/tasks/calculate_registration_image_based.py
index 6d61919ed..015dcc4fd 100644
--- a/fractal_tasks_core/tasks/calculate_registration_image_based.py
+++ b/fractal_tasks_core/tasks/calculate_registration_image_based.py
@@ -25,18 +25,18 @@
 from pydantic.decorator import validate_arguments
 from skimage.registration import phase_cross_correlation
 
-from fractal_tasks_core.lib_channels import get_channel_from_image_zarr
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.channels import get_channel_from_image_zarr
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_indices_to_regions,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import load_region
-from fractal_tasks_core.lib_tables import write_table
+from fractal_tasks_core.roi import load_region
+from fractal_tasks_core.tables import write_table
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/cellpose_segmentation.py b/fractal_tasks_core/tasks/cellpose_segmentation.py
index 3e1f86651..562e1bc57 100644
--- a/fractal_tasks_core/tasks/cellpose_segmentation.py
+++ b/fractal_tasks_core/tasks/cellpose_segmentation.py
@@ -31,30 +31,30 @@
 from pydantic.decorator import validate_arguments
 
 import fractal_tasks_core
-from fractal_tasks_core.lib_channels import ChannelNotFoundError
-from fractal_tasks_core.lib_channels import get_channel_from_image_zarr
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_input_models import Channel
-from fractal_tasks_core.lib_masked_loading import masked_loading_wrapper
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.channels import ChannelInputModel
+from fractal_tasks_core.channels import ChannelNotFoundError
+from fractal_tasks_core.channels import get_channel_from_image_zarr
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.labels import prepare_label_group
+from fractal_tasks_core.masked_loading import masked_loading_wrapper
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import (
     array_to_bounding_box_table,
 )
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import empty_bounding_box_table
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import empty_bounding_box_table
+from fractal_tasks_core.roi import (
     find_overlaps_in_ROI_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import get_overlapping_pairs_3D
-from fractal_tasks_core.lib_regions_of_interest import is_ROI_table_valid
-from fractal_tasks_core.lib_regions_of_interest import load_region
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_write import prepare_label_group
-from fractal_tasks_core.lib_zattrs_utils import rescale_datasets
+from fractal_tasks_core.roi import get_overlapping_pairs_3D
+from fractal_tasks_core.roi import is_ROI_table_valid
+from fractal_tasks_core.roi import load_region
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.utils import rescale_datasets
 
 logger = logging.getLogger(__name__)
 
@@ -156,8 +156,8 @@ def cellpose_segmentation(
     metadata: dict[str, Any],
     # Task-specific arguments
     level: int,
-    channel: Channel,
-    channel2: Optional[Channel] = None,
+    channel: ChannelInputModel,
+    channel2: Optional[ChannelInputModel] = None,
     input_ROI_table: str = "FOV_ROI_table",
     output_ROI_table: Optional[str] = None,
     output_label_name: Optional[str] = None,
diff --git a/fractal_tasks_core/tasks/copy_ome_zarr.py b/fractal_tasks_core/tasks/copy_ome_zarr.py
index ff7cccbc5..bf05ffca1 100644
--- a/fractal_tasks_core/tasks/copy_ome_zarr.py
+++ b/fractal_tasks_core/tasks/copy_ome_zarr.py
@@ -22,12 +22,12 @@
 from pydantic.decorator import validate_arguments
 
 import fractal_tasks_core
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.roi import (
     convert_ROIs_from_3D_to_2D,
 )
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_write import open_zarr_group_with_overwrite
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/create_ome_zarr.py b/fractal_tasks_core/tasks/create_ome_zarr.py
index 5a7cab9d2..3cfe90287 100644
--- a/fractal_tasks_core/tasks/create_ome_zarr.py
+++ b/fractal_tasks_core/tasks/create_ome_zarr.py
@@ -21,18 +21,22 @@
 from pydantic.decorator import validate_arguments
 
 import fractal_tasks_core
-from fractal_tasks_core.lib_channels import check_unique_wavelength_ids
-from fractal_tasks_core.lib_channels import check_well_channel_labels
-from fractal_tasks_core.lib_channels import define_omero_channels
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_glob import glob_with_multiple_patterns
-from fractal_tasks_core.lib_metadata_parsing import parse_yokogawa_metadata
-from fractal_tasks_core.lib_parse_filename_metadata import parse_filename
-from fractal_tasks_core.lib_regions_of_interest import prepare_FOV_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import prepare_well_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import remove_FOV_overlaps
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_write import open_zarr_group_with_overwrite
+from fractal_tasks_core.cellvoyager.filenames import (
+    glob_with_multiple_patterns,
+)
+from fractal_tasks_core.cellvoyager.filenames import parse_filename
+from fractal_tasks_core.cellvoyager.metadata import (
+    parse_yokogawa_metadata,
+)
+from fractal_tasks_core.channels import check_unique_wavelength_ids
+from fractal_tasks_core.channels import check_well_channel_labels
+from fractal_tasks_core.channels import define_omero_channels
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.roi import prepare_FOV_ROI_table
+from fractal_tasks_core.roi import prepare_well_ROI_table
+from fractal_tasks_core.roi import remove_FOV_overlaps
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
 
 
 __OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
diff --git a/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py b/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py
index 91d0d3b6b..5dd37a9c8 100644
--- a/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py
+++ b/fractal_tasks_core/tasks/create_ome_zarr_multiplex.py
@@ -23,18 +23,22 @@
 from zarr.errors import ContainsGroupError
 
 import fractal_tasks_core
-from fractal_tasks_core.lib_channels import check_unique_wavelength_ids
-from fractal_tasks_core.lib_channels import check_well_channel_labels
-from fractal_tasks_core.lib_channels import define_omero_channels
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_glob import glob_with_multiple_patterns
-from fractal_tasks_core.lib_metadata_parsing import parse_yokogawa_metadata
-from fractal_tasks_core.lib_parse_filename_metadata import parse_filename
-from fractal_tasks_core.lib_regions_of_interest import prepare_FOV_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import prepare_well_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import remove_FOV_overlaps
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_write import open_zarr_group_with_overwrite
+from fractal_tasks_core.cellvoyager.filenames import (
+    glob_with_multiple_patterns,
+)
+from fractal_tasks_core.cellvoyager.filenames import parse_filename
+from fractal_tasks_core.cellvoyager.metadata import (
+    parse_yokogawa_metadata,
+)
+from fractal_tasks_core.channels import check_unique_wavelength_ids
+from fractal_tasks_core.channels import check_well_channel_labels
+from fractal_tasks_core.channels import define_omero_channels
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.roi import prepare_FOV_ROI_table
+from fractal_tasks_core.roi import prepare_well_ROI_table
+from fractal_tasks_core.roi import remove_FOV_overlaps
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
 
 
 __OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
diff --git a/fractal_tasks_core/tasks/illumination_correction.py b/fractal_tasks_core/tasks/illumination_correction.py
index 1bc708fb2..2b65e9752 100644
--- a/fractal_tasks_core/tasks/illumination_correction.py
+++ b/fractal_tasks_core/tasks/illumination_correction.py
@@ -27,12 +27,12 @@
 from pydantic.decorator import validate_arguments
 from skimage.io import imread
 
-from fractal_tasks_core.lib_channels import get_omero_channel_list
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.channels import get_omero_channel_list
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
 
diff --git a/fractal_tasks_core/tasks/import_ome_zarr.py b/fractal_tasks_core/tasks/import_ome_zarr.py
index 1e5093ac3..ca27ac1a8 100644
--- a/fractal_tasks_core/tasks/import_ome_zarr.py
+++ b/fractal_tasks_core/tasks/import_ome_zarr.py
@@ -21,12 +21,12 @@
 import zarr
 from pydantic.decorator import validate_arguments
 
-from fractal_tasks_core.lib_channels import update_omero_channels
-from fractal_tasks_core.lib_ngff import detect_ome_ngff_type
-from fractal_tasks_core.lib_ngff import NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import get_image_grid_ROIs
-from fractal_tasks_core.lib_regions_of_interest import get_single_image_ROI
-from fractal_tasks_core.lib_tables import write_table
+from fractal_tasks_core.channels import update_omero_channels
+from fractal_tasks_core.ngff import detect_ome_ngff_type
+from fractal_tasks_core.ngff import NgffImageMeta
+from fractal_tasks_core.roi import get_image_grid_ROIs
+from fractal_tasks_core.roi import get_single_image_ROI
+from fractal_tasks_core.tables import write_table
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/maximum_intensity_projection.py b/fractal_tasks_core/tasks/maximum_intensity_projection.py
index 30278fae6..ba75e8b4a 100644
--- a/fractal_tasks_core/tasks/maximum_intensity_projection.py
+++ b/fractal_tasks_core/tasks/maximum_intensity_projection.py
@@ -21,9 +21,9 @@
 from pydantic.decorator import validate_arguments
 from zarr.errors import ContainsArrayError
 
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 logger = logging.getLogger(__name__)
 
diff --git a/fractal_tasks_core/tasks/napari_workflows_wrapper.py b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
index eef47cbdf..9b4405485 100644
--- a/fractal_tasks_core/tasks/napari_workflows_wrapper.py
+++ b/fractal_tasks_core/tasks/napari_workflows_wrapper.py
@@ -27,20 +27,24 @@
 from pydantic.decorator import validate_arguments
 
 import fractal_tasks_core
-from fractal_tasks_core.lib_channels import get_channel_from_image_zarr
-from fractal_tasks_core.lib_input_models import NapariWorkflowsInput
-from fractal_tasks_core.lib_input_models import NapariWorkflowsOutput
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.channels import get_channel_from_image_zarr
+from fractal_tasks_core.labels import prepare_label_group
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import load_region
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_upscale_array import upscale_array
-from fractal_tasks_core.lib_write import prepare_label_group
-from fractal_tasks_core.lib_zattrs_utils import rescale_datasets
+from fractal_tasks_core.roi import load_region
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+    NapariWorkflowsInput,
+)
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+    NapariWorkflowsOutput,
+)
+from fractal_tasks_core.upscale_array import upscale_array
+from fractal_tasks_core.utils import rescale_datasets
 
 
 __OME_NGFF_VERSION__ = fractal_tasks_core.__OME_NGFF_VERSION__
diff --git a/fractal_tasks_core/lib_input_models.py b/fractal_tasks_core/tasks/napari_workflows_wrapper_models.py
similarity index 58%
rename from fractal_tasks_core/lib_input_models.py
rename to fractal_tasks_core/tasks/napari_workflows_wrapper_models.py
index e2cd89d32..7e74ef666 100644
--- a/fractal_tasks_core/lib_input_models.py
+++ b/fractal_tasks_core/tasks/napari_workflows_wrapper_models.py
@@ -1,50 +1,37 @@
-# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
-# University of Zurich
-#
-# Original authors:
-# Tommaso Comparin <tommaso.comparin@exact-lab.it>
-#
-# This file is part of Fractal and was originally developed by eXact lab S.r.l.
-# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
-# Institute for Biomedical Research and Pelkmans Lab from the University of
-# Zurich.
-"""
-Pydantic models for some task parameters.
-"""
 from typing import Literal
 from typing import Optional
 
 from pydantic import BaseModel
 from pydantic import validator
 
+from fractal_tasks_core.channels import ChannelInputModel
 
-class Channel(BaseModel):
+
+class NapariWorkflowsOutput(BaseModel):
     """
-    A channel which is specified by either `wavelength_id` or `label`.
+    A value of the `output_specs` argument in `napari_workflows_wrapper`.
 
     Attributes:
-        wavelength_id: Unique ID for the channel wavelength, e.g. `A01_C01`.
-        label: Name of the channel.
+        type: Output type (either `label` or `dataframe`).
+        label_name: Label name (for label outputs, it is used as the name of
+            the label; for dataframe outputs, it is used to fill the
+            `region["path"]` field).
+        table_name: Table name (for dataframe outputs only).
     """
 
-    wavelength_id: Optional[str] = None
-    label: Optional[str] = None
+    type: Literal["label", "dataframe"]
+    label_name: str
+    table_name: Optional[str] = None
 
-    @validator("label", always=True)
-    def mutually_exclusive_channel_attributes(cls, v, values):
+    @validator("table_name", always=True)
+    def table_name_only_for_dataframe_type(cls, v, values):
         """
-        Check that either `label` or `wavelength_id` is set.
+        Check that table_name is set only for dataframe outputs.
         """
-        wavelength_id = values.get("wavelength_id")
-        label = v
-        if wavelength_id and v:
-            raise ValueError(
-                "`wavelength_id` and `label` cannot be both set "
-                f"(given {wavelength_id=} and {label=})."
-            )
-        if wavelength_id is None and v is None:
+        _type = values.get("type")
+        if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v):
             raise ValueError(
-                "`wavelength_id` and `label` cannot be both `None`"
+                f"Output item has type={_type} but table_name={v}."
             )
         return v
 
@@ -56,12 +43,12 @@ class NapariWorkflowsInput(BaseModel):
     Attributes:
         type: Input type (either `image` or `label`).
         label_name: Label name (for label inputs only).
-        channel: Channel object (for image inputs only).
+        channel: `ChannelInputModel` object (for image inputs only).
     """
 
     type: Literal["image", "label"]
     label_name: Optional[str]
-    channel: Optional[Channel]
+    channel: Optional[ChannelInputModel]
 
     @validator("label_name", always=True)
     def label_name_is_present(cls, v, values):
@@ -84,32 +71,3 @@ def channel_is_present(cls, v, values):
         if _type == "image" and not v:
             raise ValueError(f"Input item has type={_type} but channel={v}.")
         return v
-
-
-class NapariWorkflowsOutput(BaseModel):
-    """
-    A value of the `output_specs` argument in `napari_workflows_wrapper`.
-
-    Attributes:
-        type: Output type (either `label` or `dataframe`).
-        label_name: Label name (for label outputs, it is used as the name of
-            the label; for dataframe outputs, it is used to fill the
-            `region["path"]` field).
-        table_name: Table name (for dataframe outputs only).
-    """
-
-    type: Literal["label", "dataframe"]
-    label_name: str
-    table_name: Optional[str] = None
-
-    @validator("table_name", always=True)
-    def table_name_only_for_dataframe_type(cls, v, values):
-        """
-        Check that table_name is set only for dataframe outputs.
-        """
-        _type = values.get("type")
-        if (_type == "dataframe" and (not v)) or (_type != "dataframe" and v):
-            raise ValueError(
-                f"Output item has type={_type} but table_name={v}."
-            )
-        return v
diff --git a/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py b/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py
index ad88465dd..4e3b4d623 100644
--- a/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py
+++ b/fractal_tasks_core/tasks/yokogawa_to_ome_zarr.py
@@ -24,20 +24,22 @@
 from pydantic.decorator import validate_arguments
 from zarr.errors import ContainsArrayError
 
-from fractal_tasks_core.lib_channels import get_omero_channel_list
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_glob import glob_with_multiple_patterns
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_parse_filename_metadata import parse_filename
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
-from fractal_tasks_core.lib_read_fractal_metadata import (
-    get_parameters_from_metadata,
+from fractal_tasks_core.cellvoyager.filenames import (
+    glob_with_multiple_patterns,
 )
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.cellvoyager.filenames import parse_filename
+from fractal_tasks_core.channels import get_omero_channel_list
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.ngff import load_NgffImageMeta
+from fractal_tasks_core.pyramids import build_pyramid
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
+from fractal_tasks_core.utils import (
+    get_parameters_from_metadata,
+)
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 logger = logging.getLogger(__name__)
diff --git a/fractal_tasks_core/lib_upscale_array.py b/fractal_tasks_core/upscale_array.py
similarity index 100%
rename from fractal_tasks_core/lib_upscale_array.py
rename to fractal_tasks_core/upscale_array.py
diff --git a/fractal_tasks_core/lib_zattrs_utils.py b/fractal_tasks_core/utils.py
similarity index 53%
rename from fractal_tasks_core/lib_zattrs_utils.py
rename to fractal_tasks_core/utils.py
index 28a5751a4..aa6cbb833 100644
--- a/fractal_tasks_core/lib_zattrs_utils.py
+++ b/fractal_tasks_core/utils.py
@@ -10,11 +10,13 @@
 # Institute for Biomedical Research and Pelkmans Lab from the University of
 # Zurich.
 """
-Helper functions for operations on OME-NGFF metadata.
+Helper functions for operations on Zarr attributes and OME-NGFF metadata.
 """
 import logging
 from pathlib import Path
 from typing import Any
+from typing import Sequence
+from typing import Union
 
 import zarr
 
@@ -103,3 +105,90 @@ def get_table_path_dict(input_path: Path, component: str) -> dict[str, str]:
         table_path_dict[table] = f"{input_path / component}/tables/{table}"
 
     return table_path_dict
+
+
+def _find_omengff_acquisition(image_zarr_path: Path) -> Union[int, None]:
+    """
+    Discover the acquisition index based on OME-NGFF metadata.
+
+    Given the path to a zarr image folder (e.g. `/path/plate.zarr/B/03/0`),
+    extract the acquisition index from the `.zattrs` file of the parent
+    folder (i.e. at the well level), or return `None` if acquisition is not
+    specified.
+
+    Notes:
+
+    1. For non-multiplexing datasets, acquisition is not a required
+       information in the metadata. If it is not there, this function
+       returns `None`.
+    2. This function fails if we use an image that does not belong to
+       an OME-NGFF well.
+
+    Args:
+        image_zarr_path: Full path to an OME-NGFF image folder.
+    """
+
+    # Identify well path and attrs
+    well_zarr_path = image_zarr_path.parent
+    if not (well_zarr_path / ".zattrs").exists():
+        raise ValueError(
+            f"{str(well_zarr_path)} must be an OME-NGFF well "
+            "folder, but it does not include a .zattrs file."
+        )
+    well_group = zarr.open_group(str(well_zarr_path))
+    attrs_images = well_group.attrs["well"]["images"]
+
+    # Loook for the acquisition of the current image (if any)
+    acquisition = None
+    for img_dict in attrs_images:
+        if (
+            img_dict["path"] == image_zarr_path.name
+            and "acquisition" in img_dict.keys()
+        ):
+            acquisition = img_dict["acquisition"]
+            break
+
+    return acquisition
+
+
+def get_parameters_from_metadata(
+    *,
+    keys: Sequence[str],
+    metadata: dict[str, Any],
+    image_zarr_path: Path,
+) -> dict[str, Any]:
+    """
+    Flexibly extract parameters from metadata dictionary
+
+    This covers both parameters which are acquisition-specific (if the image
+    belongs to an OME-NGFF array and its acquisition is specified) or simply
+    available in the dictionary.
+    The two cases are handled as:
+    ```
+    metadata[acquisition]["some_parameter"]  # acquisition available
+    metadata["some_parameter"]               # acquisition not available
+    ```
+
+    Args:
+        keys: list of required parameters.
+        metadata: metadata dictionary.
+        image_zarr_path: full path to image, e.g. `/path/plate.zarr/B/03/0`.
+    """
+
+    parameters = {}
+    acquisition = _find_omengff_acquisition(image_zarr_path)
+    if acquisition is not None:
+        parameters["acquisition"] = acquisition
+
+    for key in keys:
+        if acquisition is None:
+            parameter = metadata[key]
+        else:
+            try:
+                parameter = metadata[key][str(acquisition)]
+            except TypeError:
+                parameter = metadata[key]
+            except KeyError:
+                parameter = metadata[key]
+        parameters[key] = parameter
+    return parameters
diff --git a/fractal_tasks_core/zarr_utils.py b/fractal_tasks_core/zarr_utils.py
new file mode 100644
index 000000000..35ff8763f
--- /dev/null
+++ b/fractal_tasks_core/zarr_utils.py
@@ -0,0 +1,119 @@
+# Copyright 2022 (C) Friedrich Miescher Institute for Biomedical Research and
+# University of Zurich
+#
+# Original authors:
+# Tommaso Comparin <tommaso.comparin@exact-lab.it>
+#
+# This file is part of Fractal and was originally developed by eXact lab S.r.l.
+# <exact-lab.it> under contract with Liberali Lab from the Friedrich Miescher
+# Institute for Biomedical Research and Pelkmans Lab from the University of
+# Zurich.
+"""
+Module with custom wrappers of the Zarr API.
+"""
+import logging
+from collections.abc import MutableMapping
+from typing import Any
+from typing import Optional
+from typing import Union
+
+import zarr.hierarchy
+from zarr.errors import ContainsGroupError
+from zarr.errors import GroupNotFoundError
+
+
+class OverwriteNotAllowedError(RuntimeError):
+    pass
+
+
+def open_zarr_group_with_overwrite(
+    path: Union[str, MutableMapping],
+    *,
+    overwrite: bool,
+    logger: Optional[logging.Logger] = None,
+    **open_group_kwargs: Any,
+) -> zarr.hierarchy.Group:
+    """
+    Wrap `zarr.open_group` and add `overwrite` argument.
+
+    This wrapper sets `mode="w"` for `overwrite=True` and `mode="w-"` for
+    `overwrite=False`.
+
+    The expected behavior is
+
+
+    * if the group does not exist, create it (independently on `overwrite`);
+    * if the group already exists and `overwrite=True`, replace the group with
+      an empty one;
+    * if the group already exists and `overwrite=False`, fail.
+
+    From the [`zarr.open_group`
+    docs](https://zarr.readthedocs.io/en/stable/api/hierarchy.html#zarr.hierarchy.open_group):
+
+    * `mode="r"` means read only (must exist);
+    * `mode="r+"` means read/write (must exist);
+    * `mode="a"` means read/write (create if doesn’t exist);
+    * `mode="w"` means create (overwrite if exists);
+    * `mode="w-"` means create (fail if exists).
+
+
+    Args:
+        path:
+            Store or path to directory in file system or name of zip file
+            (`zarr.open_group` parameter).
+        overwrite:
+            Determines the `mode` parameter of `zarr.open_group`, which is
+            `"w"` (if `overwrite=True`) or `"w-"` (if `overwrite=False`).
+        logger:
+            The logger to use (if unset, use `logging.getLogger(None)`)
+        open_group_kwargs:
+            Keyword arguments of `zarr.open_group`.
+
+    Returns:
+        The zarr group.
+
+    Raises:
+        OverwriteNotAllowedError:
+            If `overwrite=False` and the group already exists.
+    """
+
+    # Set logger
+    if logger is None:
+        logger = logging.getLogger(None)
+
+    # Set mode for zarr.open_group
+    if overwrite:
+        new_mode = "w"
+    else:
+        new_mode = "w-"
+
+    # Write log about current status
+    logger.info(f"Start open_zarr_group_with_overwrite ({overwrite=}).")
+    try:
+        # Call `zarr.open_group` with `mode="r"`, which fails for missing group
+        current_group = zarr.open_group(path, mode="r")
+        keys = list(current_group.group_keys())
+        logger.info(f"Zarr group {path} already exists, with {keys=}")
+    except GroupNotFoundError:
+        logger.info(f"Zarr group {path} does not exist yet.")
+
+    # Raise warning if we are overriding an existing value of `mode`
+    if "mode" in open_group_kwargs.keys():
+        mode = open_group_kwargs.pop("mode")
+        logger.warning(
+            f"Overriding {mode=} with {new_mode=}, "
+            "in open_zarr_group_with_overwrite"
+        )
+
+    # Call zarr.open_group
+    try:
+        return zarr.open_group(path, mode=new_mode, **open_group_kwargs)
+    except ContainsGroupError:
+        # Re-raise error with custom message and type
+        error_msg = (
+            f"Cannot create zarr group at {path=} with `{overwrite=}` "
+            "(original error: `zarr.errors.ContainsGroupError`).\n"
+            "Hint: try setting `overwrite=True`."
+        )
+        logger.error(error_msg)
+        raise OverwriteNotAllowedError(error_msg)


Test patch:
diff --git a/tests/data/fake_package_for_schema_experiments/my_package/tasks/my_task.py b/tests/data/fake_package_for_schema_experiments/my_package/tasks/my_task.py
index 3aba45533..a3a49474b 100644
--- a/tests/data/fake_package_for_schema_experiments/my_package/tasks/my_task.py
+++ b/tests/data/fake_package_for_schema_experiments/my_package/tasks/my_task.py
@@ -1,5 +1,5 @@
 from ..lib_custom_models import CustomModel
-from fractal_tasks_core.lib_channels import OmeroChannel
+from fractal_tasks_core.channels import OmeroChannel
 
 
 def my_task(x: int, y: OmeroChannel, z: CustomModel):
diff --git a/tests/data/generate_zarr_ones.py b/tests/data/generate_zarr_ones.py
index 4d78a31c8..961135465 100644
--- a/tests/data/generate_zarr_ones.py
+++ b/tests/data/generate_zarr_ones.py
@@ -8,7 +8,7 @@
 import zarr
 from anndata._io.specs import write_elem
 
-from fractal_tasks_core.lib_regions_of_interest import prepare_FOV_ROI_table
+from fractal_tasks_core.roi import prepare_FOV_ROI_table
 
 
 num_C = 2
diff --git a/tests/dev/test_unit_lib_descriptions.py b/tests/dev/test_unit_lib_descriptions.py
index 969bff985..19214c347 100644
--- a/tests/dev/test_unit_lib_descriptions.py
+++ b/tests/dev/test_unit_lib_descriptions.py
@@ -22,7 +22,7 @@ def test_get_function_args_descriptions():
 
 def test_get_class_attrs_descriptions():
     attrs_descriptions = _get_class_attrs_descriptions(
-        "fractal_tasks_core", "lib_input_models.py", "Channel"
+        "fractal_tasks_core", "channels.py", "ChannelInputModel"
     )
     debug(attrs_descriptions)
     assert attrs_descriptions.keys() == set(("wavelength_id", "label"))
diff --git a/tests/tasks/_validation.py b/tests/tasks/_validation.py
index 8ed8b0840..cb86aff41 100644
--- a/tests/tasks/_validation.py
+++ b/tests/tasks/_validation.py
@@ -12,8 +12,8 @@
 from jsonschema import validate
 
 from fractal_tasks_core import __OME_NGFF_VERSION__
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.ngff.zarr_utils import load_NgffImageMeta
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
 
diff --git a/tests/tasks/test_import_ome_zarr.py b/tests/tasks/test_import_ome_zarr.py
index 06684e766..d408b737f 100644
--- a/tests/tasks/test_import_ome_zarr.py
+++ b/tests/tasks/test_import_ome_zarr.py
@@ -4,7 +4,7 @@
 
 import fractal_tasks_core.tasks  # noqa
 from .._zenodo_ome_zarrs import prepare_3D_zarr
-from fractal_tasks_core.lib_input_models import Channel
+from fractal_tasks_core.channels import ChannelInputModel
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.import_ome_zarr import import_ome_zarr
 from fractal_tasks_core.tasks.maximum_intensity_projection import (
@@ -292,7 +292,7 @@ def test_import_ome_zarr_image_BIA(tmp_path, monkeypatch):
             input_ROI_table="grid_ROI_table",
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="Channel 0"),
+            channel=ChannelInputModel(wavelength_id="Channel 0"),
             level=0,
             relabeling=True,
             diameter_level0=80.0,
diff --git a/tests/tasks/test_registration.py b/tests/tasks/test_registration.py
index 43f250c24..ea1c98749 100644
--- a/tests/tasks/test_registration.py
+++ b/tests/tasks/test_registration.py
@@ -13,15 +13,15 @@
 from PIL import Image
 from pytest import MonkeyPatch
 
-from fractal_tasks_core.lib_input_models import Channel
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.channels import ChannelInputModel
+from fractal_tasks_core.ngff.zarr_utils import load_NgffImageMeta
+from fractal_tasks_core.roi import (
     convert_indices_to_regions,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import load_region
+from fractal_tasks_core.roi import load_region
 from fractal_tasks_core.tasks.apply_registration_to_image import (
     apply_registration_to_image,
 )
@@ -258,7 +258,7 @@ def test_multiplexing_registration(
             metadata=metadata,
             component=component,
             level=1,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
         )
 
     # Calculate registration
diff --git a/tests/tasks/test_unit_illumination_correction.py b/tests/tasks/test_unit_illumination_correction.py
index 31486404f..a64e93045 100644
--- a/tests/tasks/test_unit_illumination_correction.py
+++ b/tests/tasks/test_unit_illumination_correction.py
@@ -10,8 +10,8 @@
 from pytest import LogCaptureFixture
 from pytest import MonkeyPatch
 
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.ngff.zarr_utils import load_NgffImageMeta
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
 from fractal_tasks_core.tasks.illumination_correction import correct
diff --git a/tests/tasks/test_workflows.py b/tests/tasks/test_workflows.py
index e983b0e63..86175dd1b 100644
--- a/tests/tasks/test_workflows.py
+++ b/tests/tasks/test_workflows.py
@@ -22,7 +22,6 @@
 
 from ._validation import check_file_number
 from ._validation import validate_schema
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
 from fractal_tasks_core.tasks.copy_ome_zarr import copy_ome_zarr
 from fractal_tasks_core.tasks.create_ome_zarr import create_ome_zarr
 from fractal_tasks_core.tasks.illumination_correction import (
@@ -32,6 +31,7 @@
     maximum_intensity_projection,
 )  # noqa
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 allowed_channels = [
diff --git a/tests/tasks/test_workflows_cellpose_segmentation.py b/tests/tasks/test_workflows_cellpose_segmentation.py
index f0e8c9e46..0cd4ea613 100644
--- a/tests/tasks/test_workflows_cellpose_segmentation.py
+++ b/tests/tasks/test_workflows_cellpose_segmentation.py
@@ -33,8 +33,7 @@
 from ._validation import validate_axes_and_coordinateTransformations
 from ._validation import validate_schema
 from .lib_empty_ROI_table import _add_empty_ROI_table
-from fractal_tasks_core.lib_input_models import Channel
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
+from fractal_tasks_core.channels import ChannelInputModel
 from fractal_tasks_core.tasks.cellpose_segmentation import (
     cellpose_segmentation,
 )
@@ -46,6 +45,7 @@
     maximum_intensity_projection,
 )  # noqa
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 allowed_channels = [
@@ -186,14 +186,14 @@ def test_failures(
         # Attempt 1
         cellpose_segmentation(
             **kwargs,
-            channel=Channel(wavelength_id="invalid_wavelength_id"),
+            channel=ChannelInputModel(wavelength_id="invalid_wavelength_id"),
         )
         assert "ChannelNotFoundError" in caplog.records[0].msg
 
         # Attempt 2
         cellpose_segmentation(
             **kwargs,
-            channel=Channel(label="invalid_channel_name"),
+            channel=ChannelInputModel(label="invalid_channel_name"),
         )
         assert "ChannelNotFoundError" in caplog.records[0].msg
         assert "ChannelNotFoundError" in caplog.records[1].msg
@@ -202,7 +202,7 @@ def test_failures(
         with pytest.raises(ValueError):
             cellpose_segmentation(
                 **kwargs,
-                channel=Channel(
+                channel=ChannelInputModel(
                     wavelength_id="A01_C01",
                     label="invalid_channel_name",
                 ),
@@ -247,7 +247,7 @@ def test_workflow_with_per_FOV_labeling(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -309,8 +309,8 @@ def test_workflow_with_multi_channel_input(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
-            channel2=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
+            channel2=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -366,7 +366,7 @@ def test_workflow_with_per_FOV_labeling_2D(
             output_path=str(zarr_path_mip),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=2,
             relabeling=True,
             diameter_level0=80.0,
@@ -458,7 +458,7 @@ def test_workflow_with_per_well_labeling_2D(
             output_path=str(zarr_path_mip),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=2,
             input_ROI_table="well_ROI_table",
             relabeling=True,
@@ -519,7 +519,7 @@ def test_workflow_bounding_box(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -533,7 +533,7 @@ def test_workflow_bounding_box(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -549,7 +549,7 @@ def test_workflow_bounding_box(
                 output_path=str(zarr_path),
                 metadata=metadata,
                 component=component,
-                channel=Channel(wavelength_id="A01_C01"),
+                channel=ChannelInputModel(wavelength_id="A01_C01"),
                 level=3,
                 relabeling=True,
                 diameter_level0=80.0,
@@ -615,7 +615,7 @@ def test_workflow_bounding_box_with_overlap(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -731,7 +731,7 @@ def test_workflow_with_per_FOV_labeling_with_empty_FOV_table(
             metadata=metadata,
             component=component,
             input_ROI_table=TABLE_NAME,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=3,
             relabeling=True,
             diameter_level0=80.0,
@@ -794,7 +794,7 @@ def test_CYX_input(
             output_path=str(zarr_path_mip),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
             diameter_level0=80.0,
@@ -848,7 +848,7 @@ def test_workflow_secondary_labeling(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
             input_ROI_table="FOV_ROI_table",
@@ -879,7 +879,7 @@ def test_workflow_secondary_labeling(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
             input_ROI_table="organoid_ROI_table",
@@ -934,7 +934,7 @@ def test_workflow_secondary_labeling_no_labels(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
             input_ROI_table="FOV_ROI_table",
@@ -962,7 +962,7 @@ def test_workflow_secondary_labeling_no_labels(
             output_path=str(zarr_path),
             metadata=metadata,
             component=component,
-            channel=Channel(wavelength_id="A01_C01"),
+            channel=ChannelInputModel(wavelength_id="A01_C01"),
             level=0,
             relabeling=True,
             input_ROI_table="organoid_ROI_table",
diff --git a/tests/tasks/test_workflows_multiplexing.py b/tests/tasks/test_workflows_multiplexing.py
index 3d7cd87ed..c139cbfc8 100644
--- a/tests/tasks/test_workflows_multiplexing.py
+++ b/tests/tasks/test_workflows_multiplexing.py
@@ -19,7 +19,6 @@
 
 from ._validation import check_file_number
 from ._validation import validate_schema
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
 from fractal_tasks_core.tasks.copy_ome_zarr import (
     copy_ome_zarr,
 )
@@ -30,6 +29,7 @@
     maximum_intensity_projection,
 )
 from fractal_tasks_core.tasks.yokogawa_to_ome_zarr import yokogawa_to_ome_zarr
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 single_cycle_allowed_channels_no_label = [
diff --git a/tests/tasks/test_workflows_napari_workflows.py b/tests/tasks/test_workflows_napari_workflows.py
index b9bb90eb5..e9f108d8a 100644
--- a/tests/tasks/test_workflows_napari_workflows.py
+++ b/tests/tasks/test_workflows_napari_workflows.py
@@ -25,12 +25,16 @@
 from ._validation import validate_labels_and_measurements
 from ._validation import validate_schema
 from .lib_empty_ROI_table import _add_empty_ROI_table
-from fractal_tasks_core.lib_input_models import NapariWorkflowsInput
-from fractal_tasks_core.lib_input_models import NapariWorkflowsOutput
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
 from fractal_tasks_core.tasks.napari_workflows_wrapper import (
     napari_workflows_wrapper,
 )
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+    NapariWorkflowsInput,
+)
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
+    NapariWorkflowsOutput,
+)
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 try:
     import napari_skimage_regionprops_mock
diff --git a/tests/test_unit_ROIs.py b/tests/test_unit_ROIs.py
index 4f117f290..acc5dba11 100644
--- a/tests/test_unit_ROIs.py
+++ b/tests/test_unit_ROIs.py
@@ -9,36 +9,36 @@
 from anndata._io.specs import write_elem
 from devtools import debug
 
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     are_ROI_table_columns_valid,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     array_to_bounding_box_table,
 )
-from fractal_tasks_core.lib_regions_of_interest import check_valid_ROI_indices
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import check_valid_ROI_indices
+from fractal_tasks_core.roi import (
     convert_indices_to_regions,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROI_table_to_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import (
     convert_ROIs_from_3D_to_2D,
 )
-from fractal_tasks_core.lib_regions_of_interest import empty_bounding_box_table
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import empty_bounding_box_table
+from fractal_tasks_core.roi import (
     find_overlaps_in_ROI_indices,
 )
-from fractal_tasks_core.lib_regions_of_interest import get_image_grid_ROIs
-from fractal_tasks_core.lib_regions_of_interest import get_single_image_ROI
-from fractal_tasks_core.lib_regions_of_interest import is_ROI_table_valid
-from fractal_tasks_core.lib_regions_of_interest import (
+from fractal_tasks_core.roi import get_image_grid_ROIs
+from fractal_tasks_core.roi import get_single_image_ROI
+from fractal_tasks_core.roi import is_ROI_table_valid
+from fractal_tasks_core.roi import (
     is_standard_roi_table,
 )
-from fractal_tasks_core.lib_regions_of_interest import load_region
-from fractal_tasks_core.lib_regions_of_interest import prepare_FOV_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import prepare_well_ROI_table
-from fractal_tasks_core.lib_regions_of_interest import reset_origin
+from fractal_tasks_core.roi import load_region
+from fractal_tasks_core.roi import prepare_FOV_ROI_table
+from fractal_tasks_core.roi import prepare_well_ROI_table
+from fractal_tasks_core.roi import reset_origin
 
 
 PIXEL_SIZE_X = 0.1625
diff --git a/tests/test_unit_channels.py b/tests/test_unit_channels.py
index 400d5f263..501b53f07 100644
--- a/tests/test_unit_channels.py
+++ b/tests/test_unit_channels.py
@@ -9,12 +9,12 @@
 from devtools import debug
 
 from fractal_tasks_core import __OME_NGFF_VERSION__
-from fractal_tasks_core.lib_channels import check_unique_wavelength_ids
-from fractal_tasks_core.lib_channels import check_well_channel_labels
-from fractal_tasks_core.lib_channels import define_omero_channels
-from fractal_tasks_core.lib_channels import get_channel_from_list
-from fractal_tasks_core.lib_channels import OmeroChannel
-from fractal_tasks_core.lib_channels import update_omero_channels
+from fractal_tasks_core.channels import check_unique_wavelength_ids
+from fractal_tasks_core.channels import check_well_channel_labels
+from fractal_tasks_core.channels import define_omero_channels
+from fractal_tasks_core.channels import get_channel_from_list
+from fractal_tasks_core.channels import OmeroChannel
+from fractal_tasks_core.channels import update_omero_channels
 
 
 def test_check_unique_wavelength_ids():
diff --git a/tests/test_unit_glob_function.py b/tests/test_unit_glob_function.py
index 85b29d238..a14654480 100644
--- a/tests/test_unit_glob_function.py
+++ b/tests/test_unit_glob_function.py
@@ -1,6 +1,8 @@
 from devtools import debug
 
-from fractal_tasks_core.lib_glob import glob_with_multiple_patterns
+from fractal_tasks_core.cellvoyager.filenames import (
+    glob_with_multiple_patterns,
+)
 
 
 def test_unit_glob_with_multiple_patterns(tmp_path):
diff --git a/tests/test_unit_input_models.py b/tests/test_unit_input_models.py
index b9d11d997..ae5a321d0 100644
--- a/tests/test_unit_input_models.py
+++ b/tests/test_unit_input_models.py
@@ -1,11 +1,11 @@
 import pytest
 from devtools import debug
 
-from fractal_tasks_core.lib_input_models import Channel
-from fractal_tasks_core.lib_input_models import (
+from fractal_tasks_core.channels import ChannelInputModel
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
     NapariWorkflowsInput,
 )
-from fractal_tasks_core.lib_input_models import (
+from fractal_tasks_core.tasks.napari_workflows_wrapper_models import (
     NapariWorkflowsOutput,
 )
 
@@ -14,12 +14,12 @@ def test_Channel():
 
     # Valid
 
-    c = Channel(wavelength_id="wavelength_id")
+    c = ChannelInputModel(wavelength_id="wavelength_id")
     debug(c)
     assert c.wavelength_id
     assert not c.label
 
-    c = Channel(label="label")
+    c = ChannelInputModel(label="label")
     debug(c)
     assert not c.wavelength_id
     assert c.label
@@ -27,11 +27,11 @@ def test_Channel():
     # Invalid
 
     with pytest.raises(ValueError) as e:
-        Channel()
+        ChannelInputModel()
     debug(e.value)
 
     with pytest.raises(ValueError) as e:
-        Channel(label="label", wavelength_id="wavelength_id")
+        ChannelInputModel(label="label", wavelength_id="wavelength_id")
     debug(e.value)
 
 
diff --git a/tests/test_unit_masked_loading.py b/tests/test_unit_masked_loading.py
index f0c45d633..ac06f6e48 100644
--- a/tests/test_unit_masked_loading.py
+++ b/tests/test_unit_masked_loading.py
@@ -5,7 +5,7 @@
 
 from ._zenodo_ome_zarrs import prepare_2D_zarr  # noqa
 from ._zenodo_ome_zarrs import prepare_3D_zarr  # noqa
-from fractal_tasks_core.lib_masked_loading import (
+from fractal_tasks_core.masked_loading import (
     masked_loading_wrapper,
 )  # noqa
 
diff --git a/tests/test_unit_ngff.py b/tests/test_unit_ngff.py
index 9306f2c14..00eedacd8 100644
--- a/tests/test_unit_ngff.py
+++ b/tests/test_unit_ngff.py
@@ -6,14 +6,14 @@
 import zarr
 from devtools import debug
 
-from fractal_tasks_core.lib_ngff import Dataset
-from fractal_tasks_core.lib_ngff import detect_ome_ngff_type
-from fractal_tasks_core.lib_ngff import load_NgffImageMeta
-from fractal_tasks_core.lib_ngff import load_NgffWellMeta
-from fractal_tasks_core.lib_ngff import Multiscale
-from fractal_tasks_core.lib_ngff import NgffImageMeta
-from fractal_tasks_core.lib_ngff import NgffWellMeta
-from fractal_tasks_core.lib_ngff import ZarrGroupNotFoundError
+from fractal_tasks_core.ngff.specs import Dataset
+from fractal_tasks_core.ngff.specs import Multiscale
+from fractal_tasks_core.ngff.specs import NgffImageMeta
+from fractal_tasks_core.ngff.specs import NgffWellMeta
+from fractal_tasks_core.ngff.zarr_utils import detect_ome_ngff_type
+from fractal_tasks_core.ngff.zarr_utils import load_NgffImageMeta
+from fractal_tasks_core.ngff.zarr_utils import load_NgffWellMeta
+from fractal_tasks_core.ngff.zarr_utils import ZarrGroupNotFoundError
 
 
 def test_load_NgffWellMeta(tmp_path):
diff --git a/tests/test_unit_parse_metadata_from_filename.py b/tests/test_unit_parse_metadata_from_filename.py
index 3fb12ff17..0035f7fcb 100644
--- a/tests/test_unit_parse_metadata_from_filename.py
+++ b/tests/test_unit_parse_metadata_from_filename.py
@@ -14,7 +14,7 @@
 import pytest
 from devtools import debug
 
-from fractal_tasks_core.lib_parse_filename_metadata import parse_filename
+from fractal_tasks_core.cellvoyager.filenames import parse_filename
 
 f1 = (
     "20200812-CardiomyocyteDifferentiation14-Cycle1"
diff --git a/tests/test_unit_parse_yokogawa_metadata.py b/tests/test_unit_parse_yokogawa_metadata.py
index 0fa0066b0..5957ed5ec 100644
--- a/tests/test_unit_parse_yokogawa_metadata.py
+++ b/tests/test_unit_parse_yokogawa_metadata.py
@@ -19,9 +19,11 @@
 from devtools import debug
 from pandas import Timestamp
 
-from fractal_tasks_core.lib_metadata_parsing import parse_yokogawa_metadata
-from fractal_tasks_core.lib_regions_of_interest import remove_FOV_overlaps
-from fractal_tasks_core.lib_regions_of_interest import run_overlap_check
+from fractal_tasks_core.cellvoyager.metadata import (
+    parse_yokogawa_metadata,
+)
+from fractal_tasks_core.roi import remove_FOV_overlaps
+from fractal_tasks_core.roi import run_overlap_check
 
 # General variables and paths (relative to the test folder)
 testdir = os.path.dirname(__file__)
diff --git a/tests/test_unit_pyramid_creation.py b/tests/test_unit_pyramid_creation.py
index 62153f7e9..50237b8d3 100644
--- a/tests/test_unit_pyramid_creation.py
+++ b/tests/test_unit_pyramid_creation.py
@@ -2,7 +2,7 @@
 import pytest
 from devtools import debug
 
-from fractal_tasks_core.lib_pyramid_creation import build_pyramid
+from fractal_tasks_core.pyramids import build_pyramid
 
 
 def test_build_pyramid(tmp_path):
diff --git a/tests/test_unit_read_fractal_metadata.py b/tests/test_unit_read_fractal_metadata.py
index 846fb70f8..5965fce50 100644
--- a/tests/test_unit_read_fractal_metadata.py
+++ b/tests/test_unit_read_fractal_metadata.py
@@ -2,10 +2,10 @@
 import zarr
 from devtools import debug
 
-from fractal_tasks_core.lib_read_fractal_metadata import (
-    find_omengff_acquisition,
+from fractal_tasks_core.utils import (
+    _find_omengff_acquisition,
 )
-from fractal_tasks_core.lib_read_fractal_metadata import (
+from fractal_tasks_core.utils import (
     get_parameters_from_metadata,
 )
 
@@ -22,7 +22,7 @@ def test_find_omengff_acquisition(tmp_path):
     debug(well_group)
     with pytest.raises(ValueError) as e:
         image_zarr_path = well_zarr_path / image_name
-        find_omengff_acquisition(image_zarr_path)
+        _find_omengff_acquisition(image_zarr_path)
     debug(e.value)
 
 
diff --git a/tests/test_unit_tables.py b/tests/test_unit_tables.py
index e2429836c..125285da3 100644
--- a/tests/test_unit_tables.py
+++ b/tests/test_unit_tables.py
@@ -5,10 +5,10 @@
 from devtools import debug
 
 from fractal_tasks_core import __FRACTAL_TABLE_VERSION__
-from fractal_tasks_core.lib_tables import write_table
-from fractal_tasks_core.lib_tables.v1 import FeatureTableAttrs
-from fractal_tasks_core.lib_tables.v1 import MaskingROITableAttrs
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
+from fractal_tasks_core.tables import write_table
+from fractal_tasks_core.tables.v1 import FeatureTableAttrs
+from fractal_tasks_core.tables.v1 import MaskingROITableAttrs
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 TYPE = "some-arbitrary-type"
diff --git a/tests/test_unit_upscale_array.py b/tests/test_unit_upscale_array.py
index 522fa114c..3be49cd78 100644
--- a/tests/test_unit_upscale_array.py
+++ b/tests/test_unit_upscale_array.py
@@ -5,8 +5,8 @@
 import pytest
 from devtools import debug
 
-from fractal_tasks_core.lib_upscale_array import convert_region_to_low_res
-from fractal_tasks_core.lib_upscale_array import upscale_array
+from fractal_tasks_core.upscale_array import convert_region_to_low_res
+from fractal_tasks_core.upscale_array import upscale_array
 
 
 list_success: list[tuple] = []
diff --git a/tests/test_unit_write.py b/tests/test_unit_write.py
index 7876cd6c8..e1c13b738 100644
--- a/tests/test_unit_write.py
+++ b/tests/test_unit_write.py
@@ -6,10 +6,10 @@
 import zarr
 from devtools import debug
 
-from fractal_tasks_core.lib_write import _write_elem_with_overwrite
-from fractal_tasks_core.lib_write import open_zarr_group_with_overwrite
-from fractal_tasks_core.lib_write import OverwriteNotAllowedError
-from fractal_tasks_core.lib_write import prepare_label_group
+from fractal_tasks_core.labels import prepare_label_group
+from fractal_tasks_core.tables.v1 import _write_elem_with_overwrite
+from fractal_tasks_core.zarr_utils import open_zarr_group_with_overwrite
+from fractal_tasks_core.zarr_utils import OverwriteNotAllowedError
 
 
 def test_open_zarr_group_with_overwrite(tmp_path, caplog):
diff --git a/tests/test_unit_zattrs_utils.py b/tests/test_unit_zattrs_utils.py
index da80e13d6..653dd8bc5 100644
--- a/tests/test_unit_zattrs_utils.py
+++ b/tests/test_unit_zattrs_utils.py
@@ -1,10 +1,10 @@
 import zarr
 from devtools import debug
 
-from fractal_tasks_core.lib_zattrs_utils import (
+from fractal_tasks_core.utils import (
     get_table_path_dict,
 )
-from fractal_tasks_core.lib_zattrs_utils import rescale_datasets
+from fractal_tasks_core.utils import rescale_datasets
 
 
 def test_rescale_datasets(tmp_path):