Skip to content

Commit d9f15a7

Browse files
authored
Add mags of foreign datasets (#367)
* add function to add a symlink to a foreign mag * implement add_copy_mag * update changelog * adjust error messages
1 parent dcf6b4a commit d9f15a7

File tree

4 files changed

+161
-1
lines changed

4 files changed

+161
-1
lines changed

Changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ For upgrade instructions, please check the respective *Breaking Changes* section
3939

4040
### Added
4141
- Added functions to `wkcuber.api.dataset.Dataset` and `wkcuber.api.layer.Layer` to set and get the view configuration. [#344](https://github.com/scalableminds/webknossos-cuber/pull/344)
42+
- Added functions to add mags of a foreign dataset (`Layer.add_symlink_mag` and `Layer.add_copy_mag`) [#367](https://github.com/scalableminds/webknossos-cuber/pull/367)
4243

4344
### Changed
4445

tests/test_dataset.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1150,6 +1150,81 @@ def test_add_symlink_layer() -> None:
11501150
assert np.array_equal(original_mag.read(size=(10, 10, 10)), write_data)
11511151

11521152

1153+
def test_add_symlink_mag(tmp_path: Path) -> None:
1154+
original_ds = Dataset.create(tmp_path / "original", scale=(1, 1, 1))
1155+
original_layer = original_ds.add_layer(
1156+
"color", LayerCategories.COLOR_TYPE, dtype_per_channel="uint8"
1157+
)
1158+
original_layer.add_mag(1).write(
1159+
data=(np.random.rand(10, 20, 30) * 255).astype(np.uint8)
1160+
)
1161+
original_layer.add_mag(2).write(
1162+
data=(np.random.rand(5, 10, 15) * 255).astype(np.uint8)
1163+
)
1164+
1165+
ds = Dataset.create(tmp_path / "link", scale=(1, 1, 1))
1166+
layer = ds.add_layer("color", LayerCategories.COLOR_TYPE, dtype_per_channel="uint8")
1167+
layer.add_mag(1).write(
1168+
offset=(6, 6, 6), data=(np.random.rand(10, 20, 30) * 255).astype(np.uint8)
1169+
)
1170+
1171+
assert tuple(layer.get_bounding_box().topleft) == (6, 6, 6)
1172+
assert tuple(layer.get_bounding_box().size) == (10, 20, 30)
1173+
1174+
symlink_mag = layer.add_symlink_mag(tmp_path / "original" / "color" / "2")
1175+
1176+
assert (tmp_path / "link" / "color" / "1").exists()
1177+
assert len(ds.properties.data_layers["color"].wkw_magnifications) == 2
1178+
1179+
assert tuple(layer.get_bounding_box().topleft) == (0, 0, 0)
1180+
assert tuple(layer.get_bounding_box().size) == (16, 26, 36)
1181+
1182+
# Write data in symlink layer
1183+
# Note: The written data is fully inside the bounding box of the original data.
1184+
# This is important because the bounding box of the foreign layer would not be updated if we use the linked dataset to write outside of its original bounds.
1185+
write_data = (np.random.rand(5, 5, 5) * 255).astype(np.uint8)
1186+
symlink_mag.write(offset=(0, 0, 0), data=write_data)
1187+
1188+
assert np.array_equal(symlink_mag.read(size=(5, 5, 5))[0], write_data)
1189+
assert np.array_equal(original_layer.get_mag(2).read(size=(5, 5, 5))[0], write_data)
1190+
1191+
1192+
def test_add_copy_mag(tmp_path: Path) -> None:
1193+
original_ds = Dataset.create(tmp_path / "original", scale=(1, 1, 1))
1194+
original_layer = original_ds.add_layer(
1195+
"color", LayerCategories.COLOR_TYPE, dtype_per_channel="uint8"
1196+
)
1197+
original_layer.add_mag(1).write(
1198+
data=(np.random.rand(10, 20, 30) * 255).astype(np.uint8)
1199+
)
1200+
original_data = (np.random.rand(5, 10, 15) * 255).astype(np.uint8)
1201+
original_layer.add_mag(2).write(data=original_data)
1202+
1203+
ds = Dataset.create(tmp_path / "link", scale=(1, 1, 1))
1204+
layer = ds.add_layer("color", LayerCategories.COLOR_TYPE, dtype_per_channel="uint8")
1205+
layer.add_mag(1).write(
1206+
offset=(6, 6, 6), data=(np.random.rand(10, 20, 30) * 255).astype(np.uint8)
1207+
)
1208+
1209+
assert tuple(layer.get_bounding_box().topleft) == (6, 6, 6)
1210+
assert tuple(layer.get_bounding_box().size) == (10, 20, 30)
1211+
1212+
copy_mag = layer.add_copy_mag(tmp_path / "original" / "color" / "2")
1213+
1214+
assert (tmp_path / "link" / "color" / "1").exists()
1215+
assert len(ds.properties.data_layers["color"].wkw_magnifications) == 2
1216+
1217+
assert tuple(layer.get_bounding_box().topleft) == (0, 0, 0)
1218+
assert tuple(layer.get_bounding_box().size) == (16, 26, 36)
1219+
1220+
# Write data in copied layer
1221+
write_data = (np.random.rand(5, 5, 5) * 255).astype(np.uint8)
1222+
copy_mag.write(offset=(0, 0, 0), data=write_data)
1223+
1224+
assert np.array_equal(copy_mag.read(size=(5, 5, 5))[0], write_data)
1225+
assert np.array_equal(original_layer.get_mag(2).read()[0], original_data)
1226+
1227+
11531228
def test_search_dataset_also_for_long_layer_name() -> None:
11541229
delete_dir(TESTOUTPUT_DIR / "long_layer_name")
11551230

wkcuber/api/layer.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import logging
22
import math
33
import os
4+
import shutil
45
from argparse import Namespace
6+
from pathlib import Path
57
from shutil import rmtree
68
from os.path import join
79
from os import makedirs
@@ -200,6 +202,88 @@ def delete_mag(self, mag: Union[int, str, list, tuple, np.ndarray, Mag]) -> None
200202
)
201203
rmtree(full_path)
202204

205+
def _add_foreign_mag(
206+
self, foreign_mag_path: Path, symlink: bool, make_relative: bool
207+
) -> MagView:
208+
mag_name = foreign_mag_path.name
209+
mag = Mag(mag_name)
210+
operation = "symlink" if symlink else "copy"
211+
if mag in self.mags.keys():
212+
raise IndexError(
213+
f"Cannot {operation} {foreign_mag_path}. This dataset already has a mag called {mag_name}."
214+
)
215+
216+
foreign_normalized_mag_path = (
217+
Path(os.path.relpath(foreign_mag_path, self.dataset.path))
218+
if make_relative
219+
else foreign_mag_path
220+
)
221+
222+
if symlink:
223+
os.symlink(
224+
foreign_normalized_mag_path,
225+
join(self.dataset.path, self.name, mag_name),
226+
)
227+
else:
228+
shutil.copytree(
229+
foreign_normalized_mag_path,
230+
join(self.dataset.path, self.name, mag_name),
231+
)
232+
233+
# copy the properties of the layer into the properties of this dataset
234+
mag_properties = None
235+
from wkcuber.api.properties.dataset_properties import (
236+
Properties,
237+
) # using a relative import prevents a circular dependency
238+
239+
foreign_layer_properties = Properties._from_json(
240+
foreign_mag_path.parent.parent / Properties.FILE_NAME
241+
).data_layers[self.name]
242+
for resolution in foreign_layer_properties.wkw_magnifications:
243+
if resolution.mag == mag:
244+
mag_properties = resolution
245+
break
246+
247+
assert (
248+
mag_properties is not None
249+
), f"Failed to {operation} existing mag at {foreign_mag_path}: The properties on the foreign dataset do not contain an entry for the specified mag."
250+
new_bbox = self.get_bounding_box().extended_by(
251+
foreign_layer_properties.get_bounding_box()
252+
)
253+
self.set_bounding_box(offset=new_bbox.topleft, size=new_bbox.size)
254+
self.dataset.properties.data_layers[self.name]._wkw_magnifications += [
255+
mag_properties
256+
]
257+
self.dataset.properties._export_as_json()
258+
259+
self._setup_mag(mag)
260+
return self.mags[mag]
261+
262+
def add_symlink_mag(
263+
self, foreign_mag_path: Union[str, Path], make_relative: bool = False
264+
) -> MagView:
265+
"""
266+
Creates a symlink to the data at `foreign_mag_path` which belongs to another dataset.
267+
The relevant information from the `datasource-properties.json` of the other dataset is copied to this dataset.
268+
Note: If the other dataset modifies its bounding box afterwards, the change does not affect this properties
269+
(or vice versa).
270+
If make_relative is True, the symlink is made relative to the current dataset path.
271+
"""
272+
foreign_mag_path = Path(os.path.abspath(foreign_mag_path))
273+
return self._add_foreign_mag(
274+
foreign_mag_path, symlink=True, make_relative=make_relative
275+
)
276+
277+
def add_copy_mag(self, foreign_mag_path: Union[str, Path]) -> MagView:
278+
"""
279+
Copies the data at `foreign_mag_path` which belongs to another dataset to the current dataset.
280+
Additionally, the relevant information from the `datasource-properties.json` of the other dataset are copied too.
281+
"""
282+
foreign_mag_path = Path(os.path.abspath(foreign_mag_path))
283+
return self._add_foreign_mag(
284+
foreign_mag_path, symlink=False, make_relative=False
285+
)
286+
203287
def _create_dir_for_mag(
204288
self, mag: Union[int, str, list, tuple, np.ndarray, Mag]
205289
) -> None:

wkcuber/api/properties/dataset_properties.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from pathlib import Path
33
from typing import Tuple, Optional, Dict, Any, cast
44

5-
from wkcuber.api.layer import Layer, LayerCategories
5+
from wkcuber.api.layer import LayerCategories
66
from wkcuber.api.properties.layer_properties import (
77
SegmentationLayerProperties,
88
LayerProperties,

0 commit comments

Comments
 (0)