Skip to content

Commit 8d6a67a

Browse files
authored
Unify Properties in Mag and BoundingBox, add Vec3Int (#421)
* unify mag class * fix usage in test * make topleft + size private for bbox * Adapt BoundingBox to Vec3Int (todo: serialize) * have vec3 extend tuple, freeze bounding box * Fix segmentation layer initialization * freeze Mag * use attr.s instead of attr.frozen * start using Vec3Int in dataset api, add tests * add more tests * more tests, add neg * use attr.frozen again * relative path in toml * changelog * implement pr feedback * add more with_* convenience methods to bbox and vecint * fix typo in comment * implement pr feedback (part 2)
1 parent 7c79afd commit 8d6a67a

24 files changed

+777
-426
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.idea

webknossos/Changelog.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Change Log
2+
3+
All notable changes to the webknossos python library are documented in this file.
4+
5+
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
6+
and this project adheres to [Semantic Versioning](http://semver.org/) `MAJOR.MINOR.PATCH`.
7+
For upgrade instructions, please check the respective *Breaking Changes* sections.
8+
9+
## Unreleased
10+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.13...HEAD)
11+
12+
### Breaking Changes
13+
14+
- Breaking changes were introduced for geometry classes in [#421](https://github.com/scalableminds/webknossos-libs/pull/421):
15+
- `BoundingBox`
16+
- is now immutable, use convenience methods, e.g. `bb.with_topleft((0,0,0))`
17+
- properties topleft and size are now Vec3Int instead of np.array, they are each immutable as well
18+
- all `to_`-conversions return a copy, some were renamed:
19+
- `to_array``to_list`
20+
- `as_np``to_np`
21+
- `as_wkw``to_wkw_dict`
22+
- `from_wkw``from_wkw_dict`
23+
- `as_config``to_config_dict`
24+
- `as_checkpoint_name``to_checkpoint_name`
25+
- `as_tuple6``to_tuple6`
26+
- `as_csv``to_csv`
27+
- `as_named_tuple``to_named_tuple`
28+
- `as_slices``to_slices`
29+
- `copy` → (gone, immutable)
30+
31+
- `Mag`
32+
- is now immutable
33+
- `mag.mag` is now `mag._mag` (considered private, use to_list instead if you really need it as list)
34+
- all `to_`-conversions return a copy, some were renamed:
35+
- `to_array``to_list`
36+
- `scale_by` → (gone, immutable)
37+
- `divide_by` → (gone, immutable)
38+
- `as_np``to_np`
39+
40+
### Added
41+
42+
- An immutable Vec3Int class was introduced that holds three integers and provides a number of convenience methods and accessors. [#421](https://github.com/scalableminds/webknossos-libs/pull/421)
43+
44+
### Changed
45+
46+
- `BoundingBox` and `Mag` are now immutable attr classes containing `Vec3Int` values. See breaking changes above.
47+
48+
### Fixed
49+
50+
-
51+
52+
## [0.8.13](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.13) - 2021-09-22
53+
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.12...v0.8.13)
54+
55+
This is the latest release at the time of creating this changelog.

webknossos/poetry.lock

Lines changed: 140 additions & 124 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webknossos/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ networkx = "^2.6.2"
1818
numpy = "^1.15.0" # see https://numpy.org/neps/nep-0029-deprecation_policy.html#support-table
1919
python-dateutil = "^2.8.0"
2020
python-dotenv = "^0.19.0"
21-
rich = "^10.9.0"
2221
scikit-image = "^0.16.0"
2322
scipy = "^1.4.0"
2423
wkw = "1.1.11"
24+
rich = "^10.9.0"
2525

2626
[tool.poetry.dev-dependencies]
2727
# autoflake

webknossos/tests/test_bounding_box.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,16 @@ def test_in_mag() -> None:
4646
assert BoundingBox((2, 2, 2), (10, 10, 10)).in_mag(Mag(2)) == BoundingBox(
4747
topleft=(1, 1, 1), size=(5, 5, 5)
4848
)
49+
50+
51+
def test_with_bounds() -> None:
52+
53+
assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_x(0, 10) == BoundingBox(
54+
(0, 2, 3), (10, 5, 5)
55+
)
56+
assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_y(
57+
new_topleft_y=0
58+
) == BoundingBox((1, 0, 3), (5, 5, 5))
59+
assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_z(
60+
new_size_z=10
61+
) == BoundingBox((1, 2, 3), (5, 5, 10))

webknossos/tests/test_dataset.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def test_modify_existing_dataset() -> None:
209209
)
210210

211211
ds2 = Dataset(TESTOUTPUT_DIR / "simple_wk_dataset")
212+
212213
ds2.add_layer(
213214
"segmentation",
214215
LayerCategories.SEGMENTATION_TYPE,
@@ -761,18 +762,19 @@ def test_changing_layer_bounding_box() -> None:
761762
original_data = mag.read(size=bbox_size)
762763
assert original_data.shape == (3, 24, 24, 24)
763764

764-
old_bbox = layer.bounding_box
765-
old_bbox.size = np.array([12, 12, 10])
766-
layer.bounding_box = old_bbox # decrease bounding box
765+
layer.bounding_box = layer.bounding_box.with_size(
766+
[12, 12, 10]
767+
) # decrease bounding box
767768

768769
bbox_size = ds.get_layer("color").bounding_box.size
769770
assert tuple(bbox_size) == (12, 12, 10)
770771
less_data = mag.read(size=bbox_size)
771772
assert less_data.shape == (3, 12, 12, 10)
772773
assert np.array_equal(original_data[:, :12, :12, :10], less_data)
773774

774-
old_bbox.size = np.array([36, 48, 60])
775-
layer.bounding_box = old_bbox # increase the bounding box
775+
layer.bounding_box = layer.bounding_box.with_size(
776+
[36, 48, 60]
777+
) # increase the bounding box
776778

777779
bbox_size = ds.get_layer("color").bounding_box.size
778780
assert tuple(bbox_size) == (36, 48, 60)

webknossos/tests/test_mag.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55

66
def test_mag_constructor() -> None:
77
mag = Mag(16)
8-
assert mag.to_array() == [16, 16, 16]
8+
assert mag.to_list() == [16, 16, 16]
99

1010
mag = Mag("256")
11-
assert mag.to_array() == [256, 256, 256]
11+
assert mag.to_list() == [256, 256, 256]
1212

1313
mag = Mag("16-2-4")
1414

15-
assert mag.to_array() == [16, 2, 4]
15+
assert mag.to_list() == [16, 2, 4]
1616

1717
mag1 = Mag("16-2-4")
1818
mag2 = Mag("8-2-4")
1919

2020
assert mag1 > mag2
2121
assert mag1.to_layer_name() == "16-2-4"
2222

23-
assert np.all(mag1.as_np() == np.array([16, 2, 4]))
23+
assert np.all(mag1.to_np() == np.array([16, 2, 4]))
2424
assert mag1 == Mag(mag1)
25-
assert mag1 == Mag(mag1.as_np())
25+
assert mag1 == Mag(mag1.to_np())

webknossos/tests/test_vec3_int.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import numpy as np
2+
3+
from webknossos.geometry import Mag, Vec3Int
4+
5+
6+
def test_with() -> None:
7+
8+
assert Vec3Int(1, 2, 3).with_x(5) == Vec3Int(5, 2, 3)
9+
assert Vec3Int(1, 2, 3).with_y(5) == Vec3Int(1, 5, 3)
10+
assert Vec3Int(1, 2, 3).with_z(5) == Vec3Int(1, 2, 5)
11+
12+
13+
def test_import() -> None:
14+
15+
assert Vec3Int(1, 2, 3) == Vec3Int(1, 2, 3)
16+
assert Vec3Int((1, 2, 3)) == Vec3Int(1, 2, 3)
17+
assert Vec3Int([1, 2, 3]) == Vec3Int(1, 2, 3)
18+
assert Vec3Int(i for i in [1, 2, 3]) == Vec3Int(1, 2, 3)
19+
assert Vec3Int(np.array([1, 2, 3])) == Vec3Int(1, 2, 3)
20+
assert Vec3Int(Mag(4)) == Vec3Int(4, 4, 4)
21+
22+
23+
def test_export() -> None:
24+
25+
assert Vec3Int(1, 2, 3).x == 1
26+
assert Vec3Int(1, 2, 3).y == 2
27+
assert Vec3Int(1, 2, 3).z == 3
28+
assert Vec3Int(1, 2, 3)[0] == 1
29+
assert Vec3Int(1, 2, 3)[1] == 2
30+
assert Vec3Int(1, 2, 3)[2] == 3
31+
assert np.array_equal(Vec3Int(1, 2, 3).to_np(), np.array([1, 2, 3]))
32+
assert Vec3Int(1, 2, 3).to_list() == [1, 2, 3]
33+
assert Vec3Int(1, 2, 3).to_tuple() == (1, 2, 3)
34+
35+
36+
def test_operator_arithmetic() -> None:
37+
38+
# other is Vec3Int
39+
assert Vec3Int(1, 2, 3) + Vec3Int(4, 5, 6) == Vec3Int(5, 7, 9)
40+
assert Vec3Int(1, 2, 3) + Vec3Int(0, 0, 0) == Vec3Int(1, 2, 3)
41+
assert Vec3Int(1, 2, 3) - Vec3Int(4, 5, 6) == Vec3Int(-3, -3, -3)
42+
assert Vec3Int(1, 2, 3) * Vec3Int(4, 5, 6) == Vec3Int(4, 10, 18)
43+
assert Vec3Int(4, 5, 6) // Vec3Int(1, 2, 3) == Vec3Int(4, 2, 2)
44+
assert Vec3Int(4, 5, 6) % Vec3Int(1, 2, 3) == Vec3Int(0, 1, 0)
45+
46+
# other is scalar int
47+
assert Vec3Int(1, 2, 3) * 3 == Vec3Int(3, 6, 9)
48+
assert Vec3Int(1, 2, 3) + 3 == Vec3Int(4, 5, 6)
49+
assert Vec3Int(1, 2, 3) - 3 == Vec3Int(-2, -1, 0)
50+
assert Vec3Int(4, 5, 6) // 2 == Vec3Int(2, 2, 3)
51+
assert Vec3Int(4, 5, 6) % 3 == Vec3Int(1, 2, 0)
52+
53+
# other is Vec3IntLike (e.g. tuple)
54+
assert Vec3Int(1, 2, 3) + (4, 5, 6) == Vec3Int(5, 7, 9)
55+
56+
# be wary of the tuple “+” operation:
57+
assert (1, 2, 3) + Vec3Int(4, 5, 6) == (1, 2, 3, 4, 5, 6)
58+
59+
assert -Vec3Int(1, 2, 3) == Vec3Int(-1, -2, -3)
60+
61+
62+
def test_method_arithmetic() -> None:
63+
64+
assert Vec3Int(4, 5, 6).ceildiv(Vec3Int(1, 2, 3)) == Vec3Int(4, 3, 2)
65+
assert Vec3Int(4, 5, 6).ceildiv((1, 2, 3)) == Vec3Int(4, 3, 2)
66+
assert Vec3Int(4, 5, 6).ceildiv(2) == Vec3Int(2, 3, 3)
67+
68+
assert Vec3Int(1, 2, 6).pairmax(Vec3Int(4, 5, 3)) == Vec3Int(4, 5, 6)
69+
assert Vec3Int(1, 2, 6).pairmin(Vec3Int(4, 5, 3)) == Vec3Int(1, 2, 3)
70+
71+
72+
def test_repr() -> None:
73+
74+
assert str(Vec3Int(1, 2, 3)) == "Vec3Int(1,2,3)"
75+
76+
77+
def test_prod() -> None:
78+
79+
assert Vec3Int(1, 2, 3).prod() == 6
80+
81+
82+
def test_contains() -> None:
83+
84+
assert Vec3Int(1, 2, 3).contains(1)
85+
assert not Vec3Int(1, 2, 3).contains(4)
86+
87+
88+
def test_custom_initialization() -> None:
89+
90+
assert Vec3Int.zeros() == Vec3Int(0, 0, 0)
91+
assert Vec3Int.ones() == Vec3Int(1, 1, 1)
92+
assert Vec3Int.full(4) == Vec3Int(4, 4, 4)
93+
94+
assert Vec3Int.ones() - Vec3Int.ones() == Vec3Int.zeros()
95+
assert Vec3Int.full(4) == Vec3Int.ones() * 4

webknossos/webknossos/dataset/dataset.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@
99
from shutil import rmtree
1010
from typing import Any, Dict, Optional, Tuple, Union, cast
1111

12+
import attr
1213
import numpy as np
1314
import wkw
1415

15-
from webknossos.geometry import BoundingBox
16+
from webknossos.geometry import BoundingBox, Vec3Int
1617
from webknossos.utils import get_executor_for_args
1718

1819
from .layer import (
@@ -32,7 +33,6 @@
3233
_extract_num_channels,
3334
_properties_floating_type_to_python_type,
3435
dataset_converter,
35-
layer_properties_converter,
3636
)
3737
from .view import View
3838

@@ -238,8 +238,8 @@ def add_layer(
238238

239239
segmentation_layer_properties: SegmentationLayerProperties = (
240240
SegmentationLayerProperties(
241-
**layer_properties_converter.unstructure(
242-
layer_properties
241+
**(
242+
attr.asdict(layer_properties, recurse=False)
243243
), # use all attributes from LayerProperties
244244
largest_segment_id=kwargs["largest_segment_id"],
245245
)
@@ -517,12 +517,9 @@ def copy_dataset(
517517

518518
# The bounding box needs to be updated manually because chunked views do not have a reference to the dataset itself
519519
# The base view of a MagDataset always starts at (0, 0, 0)
520-
target_mag._global_offset = (0, 0, 0)
521-
target_mag._size = cast(
522-
Tuple[int, int, int],
523-
tuple(
524-
bbox.align_with_mag(mag, ceil=True).in_mag(mag).bottomright
525-
),
520+
target_mag._global_offset = Vec3Int(0, 0, 0)
521+
target_mag._size = (
522+
bbox.align_with_mag(mag, ceil=True).in_mag(mag).bottomright
526523
)
527524
target_mag.layer.bounding_box = bbox
528525

webknossos/webknossos/dataset/downsampling_utils.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import math
33
from enum import Enum
44
from itertools import product
5-
from typing import Callable, List, Optional, Tuple, Union, cast
5+
from typing import Callable, List, Optional, Tuple, cast
66

77
import numpy as np
88
from scipy.ndimage import zoom
99
from wkw import wkw
1010

11-
from webknossos.geometry import Mag
11+
from webknossos.geometry import Mag, Vec3Int, Vec3IntLike
1212
from webknossos.utils import time_start, time_stop
1313

1414
from .view import View
@@ -33,26 +33,23 @@ class InterpolationModes(Enum):
3333
DEFAULT_EDGE_LEN = 256
3434

3535

36-
Vec3 = Union[Tuple[int, int, int], np.ndarray]
37-
38-
3936
def determine_buffer_edge_len(dataset: wkw.Dataset) -> int:
4037
return min(DEFAULT_EDGE_LEN, dataset.header.file_len * dataset.header.block_len)
4138

4239

4340
def calculate_mags_to_downsample(
4441
from_mag: Mag, max_mag: Mag, scale: Optional[Tuple[float, float, float]]
4542
) -> List[Mag]:
46-
assert np.all(from_mag.as_np() <= max_mag.as_np())
43+
assert np.all(from_mag.to_np() <= max_mag.to_np())
4744
mags = []
4845
current_mag = from_mag
4946
while current_mag < max_mag:
5047
if scale is None:
5148
# In case the sampling mode is CONSTANT_Z or ISOTROPIC:
52-
current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np()))
49+
current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np()))
5350
else:
5451
# In case the sampling mode is ANISOTROPIC:
55-
current_size = current_mag.as_np() * np.array(scale)
52+
current_size = current_mag.to_np() * np.array(scale)
5653
min_value = np.min(current_size)
5754
min_value_bitmask = np.array(current_size == min_value)
5855
factor = min_value_bitmask + 1
@@ -68,11 +65,11 @@ def calculate_mags_to_downsample(
6865
# The smaller the ratio between the smallest dimension and the largest dimension, the better.
6966
if all_scaled_ratio < min_scaled_ratio:
7067
# Multiply all dimensions with "2"
71-
current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np()))
68+
current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np()))
7269
else:
7370
# Multiply only the minimal dimension by "2".
7471
current_mag = Mag(
75-
np.minimum(current_mag.as_np() * factor, max_mag.as_np())
72+
np.minimum(current_mag.to_np() * factor, max_mag.to_np())
7673
)
7774

7875
mags += [current_mag]
@@ -88,7 +85,8 @@ def calculate_mags_to_upsample(
8885
] + [min_mag]
8986

9087

91-
def calculate_default_max_mag(dataset_size: Vec3) -> Mag:
88+
def calculate_default_max_mag(dataset_size: Vec3IntLike) -> Mag:
89+
dataset_size = Vec3Int(dataset_size)
9290
# The lowest mag should have a size of ~ 100vx**2 per slice
9391
max_x_y = max(dataset_size[0], dataset_size[1])
9492
# highest power of 2 larger (or equal) than max_x_y divided by 100
@@ -234,7 +232,7 @@ def downsample_unpadded_data(
234232
logging.info(
235233
f"Downsampling buffer of size {buffer.shape} to mag {target_mag.to_layer_name()}"
236234
)
237-
target_mag_np = np.array(target_mag.to_array())
235+
target_mag_np = np.array(target_mag.to_list())
238236
current_dimension_size = np.array(buffer.shape[1:])
239237
padding_size_for_downsampling = (
240238
target_mag_np - (current_dimension_size % target_mag_np) % target_mag_np
@@ -243,12 +241,12 @@ def downsample_unpadded_data(
243241
buffer = np.pad(
244242
buffer, pad_width=[(0, 0)] + padding_size_for_downsampling, mode="constant"
245243
)
246-
dimension_decrease = np.array([1] + target_mag.to_array())
244+
dimension_decrease = np.array([1] + target_mag.to_list())
247245
downsampled_buffer_shape = np.array(buffer.shape) // dimension_decrease
248246
downsampled_buffer = np.empty(dtype=buffer.dtype, shape=downsampled_buffer_shape)
249247
for channel in range(buffer.shape[0]):
250248
downsampled_buffer[channel] = downsample_cube(
251-
buffer[channel], target_mag.to_array(), interpolation_mode
249+
buffer[channel], target_mag.to_list(), interpolation_mode
252250
)
253251
return downsampled_buffer
254252

0 commit comments

Comments
 (0)