Repository: scalableminds/webknossos-libs

Issue:
Add layer_name to Dataset.from_images
## Detailed Description
Please add an optional `layer_name` argument to `Dataset.from_images`. When multiple layers are created, the layer name shall be the common prefix.



Discussion:

Resolving patch:
diff --git a/webknossos/webknossos/cli/convert.py b/webknossos/webknossos/cli/convert.py
--- a/webknossos/webknossos/cli/convert.py
+++ b/webknossos/webknossos/cli/convert.py
@@ -45,6 +45,12 @@ def main(
             metavar="VOXEL_SIZE",
         ),
     ],
+    layer_name: Annotated[
+        Optional[str],
+        typer.Option(
+            help="This name is used if only one layer is created. Otherwise this name is used as a common prefix for all layers.",
+        ),
+    ] = None,
     category: Annotated[
         Optional[LayerCategory],
         typer.Option(
@@ -107,5 +113,6 @@ def main(
             data_format=data_format,
             executor=executor,
             compress=compress,
+            layer_name=layer_name,
             layer_category=category.value if category else None,
         )
diff --git a/webknossos/webknossos/dataset/dataset.py b/webknossos/webknossos/dataset/dataset.py
--- a/webknossos/webknossos/dataset/dataset.py
+++ b/webknossos/webknossos/dataset/dataset.py
@@ -566,6 +566,7 @@ def from_images(
             ConversionLayerMapping, Callable[[Path], str]
         ] = ConversionLayerMapping.INSPECT_SINGLE_FILE,
         z_slices_sort_key: Callable[[Path], Any] = natsort_keygen(),
+        layer_name: Optional[str] = None,
         layer_category: Optional[LayerCategoryType] = None,
         data_format: Union[str, DataFormat] = DEFAULT_DATA_FORMAT,
         chunk_shape: Optional[Union[Vec3IntLike, int]] = None,
@@ -593,6 +594,9 @@ def from_images(
         The order of the z-slices can be customized by setting
         `z_slices_sort_key`.
 
+        If a layer_name is set, this name is used if a single layer is created.
+        Otherwise the layer_name is used as a common prefix for all layers.
+
         The category of layers (`color` vs `segmentation`) is determined
         automatically by checking if `segmentation` is part of the path.
         Alternatively, a category can be enforced by passing `layer_category`.
@@ -634,20 +638,32 @@ def from_images(
 
         filepaths_per_layer: Dict[str, List[Path]] = {}
         for input_file in input_files:
-            layer_name = map_filepath_to_layer_name(input_file)
+            layer_name_from_mapping = map_filepath_to_layer_name(input_file)
             # Remove characters from layer name that are not allowed
-            layer_name = _UNALLOWED_LAYER_NAME_CHARS.sub("", layer_name)
+            layer_name_from_mapping = _UNALLOWED_LAYER_NAME_CHARS.sub(
+                "", layer_name_from_mapping
+            )
             # Ensure layer name does not start with a dot
-            layer_name = layer_name.lstrip(".")
+            layer_name_from_mapping = layer_name_from_mapping.lstrip(".")
 
             assert (
-                layer_name != ""
+                layer_name_from_mapping != ""
             ), f"Could not determine a layer name for {input_file}."
 
-            filepaths_per_layer.setdefault(layer_name, []).append(
+            filepaths_per_layer.setdefault(layer_name_from_mapping, []).append(
                 input_path / input_file
             )
 
+        if layer_name is not None:
+            if len(filepaths_per_layer) == 1:
+                filepaths_per_layer[layer_name] = filepaths_per_layer.pop(
+                    layer_name_from_mapping
+                )
+            else:
+                filepaths_per_layer = {
+                    f"{layer_name}_{k}": v for k, v in filepaths_per_layer.items()
+                }
+
         for layer_name, filepaths in filepaths_per_layer.items():
             filepaths.sort(key=z_slices_sort_key)
             category: LayerCategoryType


Test patch:
diff --git a/webknossos/tests/dataset/test_from_images.py b/webknossos/tests/dataset/test_from_images.py
--- a/webknossos/tests/dataset/test_from_images.py
+++ b/webknossos/tests/dataset/test_from_images.py
@@ -25,13 +25,14 @@ def test_compare_tifffile(tmp_path: Path) -> None:
         tmp_path,
         (1, 1, 1),
         compress=True,
+        layer_name="tiff_stack",
         layer_category="segmentation",
         chunks_per_shard=(8, 8, 8),
         map_filepath_to_layer_name=wk.Dataset.ConversionLayerMapping.ENFORCE_SINGLE_LAYER,
     )
     assert len(ds.layers) == 1
-    assert "tiff" in ds.layers
-    data = ds.layers["tiff"].get_finest_mag().read()[0, :, :]
+    assert "tiff_stack" in ds.layers
+    data = ds.layers["tiff_stack"].get_finest_mag().read()[0, :, :]
     for z_index in range(0, data.shape[-1]):
         with TiffFile(TESTDATA_DIR / "tiff" / "test.0000.tiff") as tif_file:
             comparison_slice = tif_file.asarray().T
@@ -43,14 +44,15 @@ def test_multiple_multitiffs(tmp_path: Path) -> None:
         TESTDATA_DIR / "various_tiff_formats",
         tmp_path,
         (1, 1, 1),
+        layer_name="tiffs",
     )
     assert len(ds.layers) == 4
 
     expected_dtype_channels_size_per_layer = {
-        "test_CS.tif": ("uint8", 3, (128, 128, 320)),
-        "test_C.tif": ("uint8", 1, (128, 128, 320)),
-        "test_I.tif": ("uint32", 1, (64, 128, 64)),
-        "test_S.tif": ("uint16", 3, (128, 128, 64)),
+        "tiffs_test_CS.tif": ("uint8", 3, (128, 128, 320)),
+        "tiffs_test_C.tif": ("uint8", 1, (128, 128, 320)),
+        "tiffs_test_I.tif": ("uint32", 1, (64, 128, 64)),
+        "tiffs_test_S.tif": ("uint16", 3, (128, 128, 64)),
     }
 
     for layer_name, layer in ds.layers.items():