Skip to content

Commit 1bda6a0

Browse files
authored
Integrate view configurations into dataset API (#344)
* add methods to get and set the view configuration of a layer * fix test * update changelog * add functions to set the view configuration of a dataset * implement PR feedback
1 parent 5181fb5 commit 1bda6a0

File tree

5 files changed

+281
-6
lines changed

5 files changed

+281
-6
lines changed

Changelog.md

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ and this project adheres to [Calendar Versioning](http://calver.org/) `0Y.0M.MIC
77
For upgrade instructions, please check the respective *Breaking Changes* sections.
88

99
## Unreleased
10-
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.0...HEAD)
10+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.4...HEAD)
11+
12+
### Breaking Changes in Config & CLI
13+
14+
### Added
15+
- 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)
16+
17+
### Changed
18+
19+
### Fixed
20+
21+
## [0.8.4](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.4) - 2021-07-26
22+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.3...v0.8.4)
1123

1224
### Breaking Changes in Config & CLI
1325

@@ -18,8 +30,21 @@ For upgrade instructions, please check the respective *Breaking Changes* section
1830

1931
### Fixed
2032

33+
## [0.8.3](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.3) - 2021-07-26
34+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.2...v0.8.3)
35+
36+
### Breaking Changes in Config & CLI
37+
38+
### Added
39+
40+
### Changed
41+
- Updated `cluster-tools` to `1.58` [#361](https://github.com/scalableminds/webknossos-cuber/pull/361)
42+
43+
### Fixed
44+
45+
2146
## [0.8.2](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.2) - 2021-07-26
22-
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.1...HEAD)
47+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.1...v0.8.2)
2348

2449
### Breaking Changes in Config & CLI
2550

tests/test_dataset.py

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,24 @@
1414
from wkw import wkw
1515
from wkw.wkw import WKWException
1616

17+
from wkcuber.api.dataset import Dataset, DatasetViewConfiguration
1718
from wkcuber.api.bounding_box import BoundingBox
18-
from wkcuber.api.dataset import Dataset
1919
from os import makedirs
2020

21-
from wkcuber.api.layer import Layer, LayerCategories, SegmentationLayer
21+
from wkcuber.api.layer import (
22+
Layer,
23+
LayerCategories,
24+
SegmentationLayer,
25+
LayerViewConfiguration,
26+
)
2227
from wkcuber.api.mag_view import MagView
2328
from wkcuber.api.properties.dataset_properties import Properties
2429
from wkcuber.api.properties.layer_properties import SegmentationLayerProperties
2530
from wkcuber.api.properties.resolution_properties import Resolution
2631
from wkcuber.api.view import View
2732
from wkcuber.compress import compress_mag_inplace
2833
from wkcuber.mag import Mag
29-
from wkcuber.utils import get_executor_for_args, named_partial
34+
from wkcuber.utils import get_executor_for_args, named_partial, _snake_to_camel_case
3035

3136
TESTDATA_DIR = Path("testdata")
3237
TESTOUTPUT_DIR = Path("testoutput")
@@ -1398,6 +1403,123 @@ def test_compression(tmp_path: Path) -> None:
13981403
)
13991404

14001405

1406+
def test_dataset_view_configuration(tmp_path: Path) -> None:
1407+
ds1 = Dataset.create(tmp_path, scale=(2, 2, 1))
1408+
default_view_configuration = ds1.get_view_configuration()
1409+
assert default_view_configuration is None
1410+
1411+
ds1.set_view_configuration(DatasetViewConfiguration(four_bit=True))
1412+
default_view_configuration = ds1.get_view_configuration()
1413+
assert default_view_configuration is not None
1414+
assert default_view_configuration.four_bit == True
1415+
assert default_view_configuration.interpolation == None
1416+
assert default_view_configuration.render_missing_data_black == None
1417+
assert default_view_configuration.loading_strategy == None
1418+
assert default_view_configuration.segmentation_pattern_opacity == None
1419+
assert default_view_configuration.zoom == None
1420+
assert default_view_configuration.position == None
1421+
assert default_view_configuration.rotation == None
1422+
1423+
# Test if only the set parameters are stored in the properties
1424+
assert ds1.properties.default_view_configuration == {"fourBit": True}
1425+
1426+
ds1.set_view_configuration(
1427+
DatasetViewConfiguration(
1428+
four_bit=True,
1429+
interpolation=False,
1430+
render_missing_data_black=True,
1431+
loading_strategy="PROGRESSIVE_QUALITY",
1432+
segmentation_pattern_opacity=40,
1433+
zoom=0.1,
1434+
position=(12, 12, 12),
1435+
rotation=(1, 2, 3),
1436+
)
1437+
)
1438+
default_view_configuration = ds1.get_view_configuration()
1439+
assert default_view_configuration is not None
1440+
assert default_view_configuration.four_bit == True
1441+
assert default_view_configuration.interpolation == False
1442+
assert default_view_configuration.render_missing_data_black == True
1443+
assert default_view_configuration.loading_strategy == "PROGRESSIVE_QUALITY"
1444+
assert default_view_configuration.segmentation_pattern_opacity == 40
1445+
assert default_view_configuration.zoom == 0.1
1446+
assert default_view_configuration.position == (12, 12, 12)
1447+
assert default_view_configuration.rotation == (1, 2, 3)
1448+
1449+
# Test if the data is persisted to disk
1450+
ds2 = Dataset(tmp_path)
1451+
default_view_configuration = ds2.get_view_configuration()
1452+
assert default_view_configuration is not None
1453+
assert default_view_configuration.four_bit == True
1454+
assert default_view_configuration.interpolation == False
1455+
assert default_view_configuration.render_missing_data_black == True
1456+
assert default_view_configuration.loading_strategy == "PROGRESSIVE_QUALITY"
1457+
assert default_view_configuration.segmentation_pattern_opacity == 40
1458+
assert default_view_configuration.zoom == 0.1
1459+
assert default_view_configuration.position == (12, 12, 12)
1460+
assert default_view_configuration.rotation == (1, 2, 3)
1461+
1462+
# Test camel case
1463+
view_configuration_dict = ds2.properties.default_view_configuration
1464+
assert view_configuration_dict is not None
1465+
for k in view_configuration_dict.keys():
1466+
assert _snake_to_camel_case(k) == k
1467+
1468+
1469+
def test_layer_view_configuration(tmp_path: Path) -> None:
1470+
ds1 = Dataset.create(tmp_path, scale=(2, 2, 1))
1471+
layer1 = ds1.add_layer("color", LayerCategories.COLOR_TYPE)
1472+
default_view_configuration = layer1.get_view_configuration()
1473+
assert default_view_configuration is None
1474+
1475+
layer1.set_view_configuration(LayerViewConfiguration(color=(255, 0, 0)))
1476+
default_view_configuration = layer1.get_view_configuration()
1477+
assert default_view_configuration is not None
1478+
assert default_view_configuration.color == (255, 0, 0)
1479+
assert default_view_configuration.alpha is None
1480+
assert default_view_configuration.intensity_range is None
1481+
assert default_view_configuration.is_inverted is None
1482+
# Test if only the set parameters are stored in the properties
1483+
assert ds1.properties.data_layers["color"].default_view_configuration == {
1484+
"color": (255, 0, 0)
1485+
}
1486+
1487+
layer1.set_view_configuration(
1488+
LayerViewConfiguration(
1489+
color=(255, 0, 0),
1490+
alpha=1.0,
1491+
min=55.0,
1492+
intensity_range=(-12.3e1, 123),
1493+
is_inverted=True,
1494+
)
1495+
)
1496+
default_view_configuration = layer1.get_view_configuration()
1497+
assert default_view_configuration is not None
1498+
assert default_view_configuration.color == (255, 0, 0)
1499+
assert default_view_configuration.alpha == 1.0
1500+
assert default_view_configuration.intensity_range == (-12.3e1, 123)
1501+
assert default_view_configuration.is_inverted == True
1502+
assert default_view_configuration.min == 55.0
1503+
1504+
# Test if the data is persisted to disk
1505+
ds2 = Dataset(tmp_path)
1506+
default_view_configuration = ds2.get_layer("color").get_view_configuration()
1507+
assert default_view_configuration is not None
1508+
assert default_view_configuration.color == (255, 0, 0)
1509+
assert default_view_configuration.alpha == 1.0
1510+
assert default_view_configuration.intensity_range == (-12.3e1, 123)
1511+
assert default_view_configuration.is_inverted == True
1512+
assert default_view_configuration.min == 55.0
1513+
1514+
# Test camel case
1515+
view_configuration_dict = ds2.properties.data_layers[
1516+
"color"
1517+
].default_view_configuration
1518+
assert view_configuration_dict is not None
1519+
for k in view_configuration_dict.keys():
1520+
assert _snake_to_camel_case(k) == k
1521+
1522+
14011523
def test_get_largest_segment_id(tmp_path: Path) -> None:
14021524
ds = Dataset.create(tmp_path, scale=(1, 1, 1))
14031525

wkcuber/api/dataset.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
LayerProperties,
2020
)
2121
from wkcuber.api.bounding_box import BoundingBox
22-
from wkcuber.utils import get_executor_for_args
22+
from wkcuber.utils import get_executor_for_args, _snake_to_camel_case
2323

2424
from wkcuber.api.properties.dataset_properties import Properties
2525
from wkcuber.api.layer import Layer, LayerCategories, SegmentationLayer
@@ -655,3 +655,67 @@ def _create_layer(
655655
Layer if category == LayerCategories.COLOR_TYPE else SegmentationLayer
656656
)
657657
return layer_type(layer_name, self, dtype_per_channel, num_channels)
658+
659+
def set_view_configuration(
660+
self, view_configuration: "DatasetViewConfiguration"
661+
) -> None:
662+
self.properties._default_view_configuration = {
663+
_snake_to_camel_case(k): v
664+
for k, v in vars(view_configuration).items()
665+
if v is not None
666+
}
667+
self.properties._export_as_json() # update properties on disk
668+
669+
def get_view_configuration(self) -> Optional["DatasetViewConfiguration"]:
670+
view_configuration_dict = self.properties.default_view_configuration
671+
if view_configuration_dict is None:
672+
return None
673+
674+
return DatasetViewConfiguration(
675+
four_bit=view_configuration_dict.get("fourBit"),
676+
interpolation=view_configuration_dict.get("interpolation"),
677+
render_missing_data_black=view_configuration_dict.get(
678+
"renderMissingDataBlack"
679+
),
680+
loading_strategy=view_configuration_dict.get("loadingStrategy"),
681+
segmentation_pattern_opacity=view_configuration_dict.get(
682+
"segmentationPatternOpacity"
683+
),
684+
zoom=view_configuration_dict.get("zoom"),
685+
position=cast(
686+
Tuple[int, int, int], tuple(view_configuration_dict["position"])
687+
)
688+
if "position" in view_configuration_dict
689+
else None,
690+
rotation=cast(
691+
Tuple[int, int, int], tuple(view_configuration_dict["rotation"])
692+
)
693+
if "rotation" in view_configuration_dict
694+
else None,
695+
)
696+
697+
698+
class DatasetViewConfiguration:
699+
"""
700+
Stores information on how the dataset is shown in webknossos by default.
701+
"""
702+
703+
def __init__(
704+
self,
705+
four_bit: Optional[bool] = None,
706+
interpolation: Optional[bool] = None,
707+
render_missing_data_black: Optional[bool] = None,
708+
loading_strategy: Optional[str] = None,
709+
segmentation_pattern_opacity: Optional[int] = None,
710+
zoom: Optional[float] = None,
711+
position: Optional[Tuple[int, int, int]] = None,
712+
rotation: Optional[Tuple[int, int, int]] = None,
713+
):
714+
self.four_bit = four_bit
715+
self.interpolation = interpolation
716+
self.render_missing_data_black = render_missing_data_black
717+
self.loading_strategy = loading_strategy
718+
self.segmentation_pattern_opacity = segmentation_pattern_opacity
719+
self.zoom = zoom
720+
self.position = position
721+
self.rotation = rotation

wkcuber/api/layer.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
DEFAULT_WKW_FILE_LEN,
4646
get_executor_for_args,
4747
named_partial,
48+
_snake_to_camel_case,
4849
)
4950

5051

@@ -607,6 +608,38 @@ def _initialize_mag_from_other_mag(
607608
compress=compress,
608609
)
609610

611+
def set_view_configuration(
612+
self, view_configuration: "LayerViewConfiguration"
613+
) -> None:
614+
self.dataset.properties._data_layers[self.name]._default_view_configuration = {
615+
_snake_to_camel_case(k): v
616+
for k, v in vars(view_configuration).items()
617+
if v is not None
618+
}
619+
self.dataset.properties._export_as_json() # update properties on disk
620+
621+
def get_view_configuration(self) -> Optional["LayerViewConfiguration"]:
622+
view_configuration_dict = self.dataset.properties.data_layers[
623+
self.name
624+
].default_view_configuration
625+
if view_configuration_dict is None:
626+
return None
627+
628+
return LayerViewConfiguration(
629+
color=cast(Tuple[int, int, int], tuple(view_configuration_dict["color"])),
630+
alpha=view_configuration_dict.get("alpha"),
631+
intensity_range=cast(
632+
Tuple[float, float], tuple(view_configuration_dict["intensityRange"])
633+
)
634+
if "intensityRange" in view_configuration_dict.keys()
635+
else None,
636+
min=view_configuration_dict.get("min"),
637+
max=view_configuration_dict.get("max"),
638+
is_disabled=view_configuration_dict.get("isDisabled"),
639+
is_inverted=view_configuration_dict.get("isInverted"),
640+
is_in_edit_mode=view_configuration_dict.get("isInEditMode"),
641+
)
642+
610643

611644
class SegmentationLayer(Layer):
612645
@property
@@ -638,3 +671,29 @@ class LayerCategories:
638671

639672
COLOR_TYPE = "color"
640673
SEGMENTATION_TYPE = "segmentation"
674+
675+
676+
class LayerViewConfiguration:
677+
"""
678+
Stores information on how the dataset is shown in webknossos by default.
679+
"""
680+
681+
def __init__(
682+
self,
683+
color: Optional[Tuple[int, int, int]] = None,
684+
alpha: Optional[float] = None,
685+
intensity_range: Optional[Tuple[float, float]] = None,
686+
min: Optional[float] = None, # pylint: disable=redefined-builtin
687+
max: Optional[float] = None, # pylint: disable=redefined-builtin
688+
is_disabled: Optional[bool] = None,
689+
is_inverted: Optional[bool] = None,
690+
is_in_edit_mode: Optional[bool] = None,
691+
):
692+
self.color = color
693+
self.alpha = alpha
694+
self.intensity_range = intensity_range
695+
self.min = min
696+
self.max = max
697+
self.is_disabled = is_disabled
698+
self.is_inverted = is_inverted
699+
self.is_in_edit_mode = is_in_edit_mode

wkcuber/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,3 +514,8 @@ def get_executor_args(global_args: argparse.Namespace) -> argparse.Namespace:
514514
executor_args.distribution_strategy = global_args.distribution_strategy
515515
executor_args.job_resources = global_args.job_resources
516516
return executor_args
517+
518+
519+
def _snake_to_camel_case(snake_case_name: str) -> str:
520+
parts = snake_case_name.split("_")
521+
return parts[0] + "".join(part.title() for part in parts[1:])

0 commit comments

Comments
 (0)