Skip to content

Commit e5d496d

Browse files
authored
Fix add layer from images for unaligned topleft (#1036)
* fix issue with from_images when called with unaligned topleft. * Adapt batch size for Zarr files to avoid corrupted data. * Update Changelog.md * Adapt copy to view to use relative bbox to access original image data. * Replace conversion from nd to 3d bounding box with raising a runtime error. * Change error message.
1 parent 91fde94 commit e5d496d

File tree

4 files changed

+32
-46
lines changed

4 files changed

+32
-46
lines changed

webknossos/Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ For upgrade instructions, please check the respective _Breaking Changes_ section
1919
### Changed
2020

2121
### Fixed
22+
- Fixed a bug, where using an unaligned topleft value for `add_layer_from_images` leads to corrupted data. [#1036](https://github.com/scalableminds/webknossos-libs/pull/1036)
2223

2324

2425
## [0.14.17](https://github.com/scalableminds/webknossos-libs/releases/tag/v0.14.17) - 2024-04-10

webknossos/tests/dataset/test_add_layer_from_images.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ def test_compare_nd_tifffile(tmp_path: Path) -> None:
4949
"testdata/4D/4D_series/4D-series.ome.tif",
5050
layer_name="color",
5151
category="color",
52-
topleft=(100, 100, 55),
52+
topleft=(2, 55, 100, 100),
5353
use_bioformats=True,
5454
data_format="zarr3",
5555
chunk_shape=(8, 8, 8),
5656
chunks_per_shard=(8, 8, 8),
5757
)
5858
assert layer.bounding_box.topleft == wk.VecInt(
59-
0, 55, 100, 100, axes=("t", "z", "y", "x")
59+
2, 55, 100, 100, axes=("t", "z", "y", "x")
6060
)
6161
assert layer.bounding_box.size == wk.VecInt(
6262
7, 5, 167, 439, axes=("t", "z", "y", "x")

webknossos/webknossos/dataset/_utils/pims_images.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -504,14 +504,17 @@ def copy_to_view(
504504
copy_to_view returns an iterable of image shapes and largest segment ids. When using this
505505
method a manual update of the bounding box and the largest segment id might be necessary.
506506
"""
507-
relative_bbox = args
507+
absolute_bbox = args
508+
relative_bbox = absolute_bbox.offset(-mag_view.bounding_box.topleft)
508509

509510
assert all(
510511
size == 1
511-
for size, axis in zip(relative_bbox.size, relative_bbox.axes)
512+
for size, axis in zip(absolute_bbox.size, absolute_bbox.axes)
512513
if axis not in ("x", "y", "z")
513514
), "The delivered BoundingBox has to be flat except for x,y and z dimension."
514515

516+
# z_start and z_end are relative to the bounding box of the mag_view
517+
# to access the correct data from the images
515518
z_start, z_end = relative_bbox.get_bounds("z")
516519
shapes = []
517520
max_id: Optional[int]
@@ -522,22 +525,22 @@ def copy_to_view(
522525

523526
with self._open_images() as images:
524527
if self._iter_axes is not None and self._iter_loop_size is not None:
525-
# select the range of images that represents one xyz combination
528+
# select the range of images that represents one xyz combination in the mag_view
526529
lower_bounds = sum(
527530
self._iter_loop_size[axis_name]
528531
* relative_bbox.get_bounds(axis_name)[0]
529532
for axis_name in self._iter_axes[:-1]
530533
)
531-
upper_bounds = lower_bounds + relative_bbox.get_shape("z")
534+
upper_bounds = lower_bounds + mag_view.bounding_box.get_shape("z")
532535
images = images[lower_bounds:upper_bounds]
533536
if self._flip_z:
534537
images = images[::-1] # pylint: disable=unsubscriptable-object
535538

536539
with mag_view.get_buffered_slice_writer(
537540
# Previously only z_start and its end were important, now the slice writer needs to know
538541
# which axis is currently written.
539-
relative_bounding_box=relative_bbox,
540-
buffer_size=mag_view.info.chunk_shape.z,
542+
absolute_bounding_box=absolute_bbox,
543+
buffer_size=absolute_bbox.get_shape("z"),
541544
# copy_to_view is typically used in a multiprocessing-context. Therefore the
542545
# buffered slice writer should not update the json file to avoid race conditions.
543546
json_update_allowed=False,

webknossos/webknossos/dataset/dataset.py

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from numpy.typing import DTypeLike
3636
from upath import UPath
3737

38-
from webknossos.geometry.vec_int import VecInt, VecIntLike
38+
from webknossos.geometry.vec_int import VecIntLike
3939

4040
from ..client.api_client.models import ApiDataset
4141
from ..geometry.vec3_int import Vec3Int, Vec3IntLike
@@ -1243,11 +1243,17 @@ def add_layer_from_images(
12431243
)
12441244

12451245
if batch_size is None:
1246-
if compress:
1246+
if compress or (
1247+
layer.data_format in (DataFormat.Zarr3, DataFormat.Zarr)
1248+
):
1249+
# if data is compressed or dataformat is zarr, parallel write access
1250+
# to a shard leads to corrupted data, the batch size must be aligned
1251+
# with the shard size
12471252
batch_size = mag_view.info.shard_shape.z
12481253
else:
1254+
# in uncompressed wkw only writing to the same chunk is problematic
12491255
batch_size = mag_view.info.chunk_shape.z
1250-
elif compress:
1256+
elif compress or (layer.data_format in (DataFormat.Zarr3, DataFormat.Zarr)):
12511257
assert (
12521258
batch_size % mag_view.info.shard_shape.z == 0
12531259
), f"batch_size {batch_size} must be divisible by z shard-size {mag_view.info.shard_shape.z} when creating compressed layers"
@@ -1263,44 +1269,20 @@ def add_layer_from_images(
12631269
dtype=current_dtype,
12641270
)
12651271

1266-
args = []
1267-
bbox = layer.bounding_box
1268-
additional_axes = [
1269-
axis_name for axis_name in bbox.axes if axis_name not in ("x", "y", "z")
1270-
]
1271-
additional_axes_shapes = tuple(
1272-
product(
1273-
*[range(bbox.get_shape(axis_name)) for axis_name in additional_axes]
1272+
if (
1273+
set(layer.bounding_box.axes).difference("x", "y", "z")
1274+
) and layer.data_format != DataFormat.Zarr3:
1275+
raise RuntimeError(
1276+
"The data stores additional axes other than x, y and z."
12741277
)
1275-
)
1276-
if additional_axes and layer.data_format != DataFormat.Zarr3:
1277-
assert (
1278-
len(additional_axes_shapes) == 1
1279-
), "The data stores additional axes with shape bigger than 1. These are only supported by data format Zarr3."
12801278

1281-
# Convert NDBoundingBox to 3D BoundingBox
1282-
bbox = BoundingBox(
1283-
bbox.topleft_xyz,
1284-
bbox.size_xyz,
1279+
buffered_slice_writer_shape = layer.bounding_box.size_xyz.with_z(batch_size)
1280+
args = list(
1281+
layer.bounding_box.chunk(
1282+
buffered_slice_writer_shape,
1283+
Vec3Int(1, 1, batch_size),
12851284
)
1286-
expected_bbox = bbox
1287-
additional_axes = []
1288-
1289-
z_shape = bbox.get_shape("z")
1290-
bbox = bbox.with_topleft(VecInt.zeros(bbox.axes))
1291-
for z_start in range(0, z_shape, batch_size):
1292-
z_size = min(batch_size, z_shape - z_start)
1293-
z_bbox = bbox.with_bounds("z", z_start, z_size)
1294-
if not additional_axes:
1295-
args.append(z_bbox)
1296-
else:
1297-
for shape in additional_axes_shapes:
1298-
reduced_bbox = z_bbox
1299-
for index, axis in enumerate(additional_axes):
1300-
reduced_bbox = reduced_bbox.with_bounds(
1301-
axis, shape[index], 1
1302-
)
1303-
args.append(reduced_bbox)
1285+
)
13041286

13051287
with warnings.catch_warnings():
13061288
# Block alignmnent within the dataset should not be a problem, since shard-wise chunking is enforced.

0 commit comments

Comments
 (0)