Skip to content

Commit

Permalink
Unify Properties in Mag and BoundingBox, add Vec3Int (#421)
Browse files Browse the repository at this point in the history
* 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)
  • Loading branch information
fm3 authored Sep 23, 2021
1 parent 7c79afd commit 8d6a67a
Show file tree
Hide file tree
Showing 24 changed files with 777 additions and 426 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
55 changes: 55 additions & 0 deletions webknossos/Changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Change Log

All notable changes to the webknossos python library are documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/) `MAJOR.MINOR.PATCH`.
For upgrade instructions, please check the respective *Breaking Changes* sections.

## Unreleased
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.13...HEAD)

### Breaking Changes

- Breaking changes were introduced for geometry classes in [#421](https://github.com/scalableminds/webknossos-libs/pull/421):
- `BoundingBox`
- is now immutable, use convenience methods, e.g. `bb.with_topleft((0,0,0))`
- properties topleft and size are now Vec3Int instead of np.array, they are each immutable as well
- all `to_`-conversions return a copy, some were renamed:
- `to_array``to_list`
- `as_np``to_np`
- `as_wkw``to_wkw_dict`
- `from_wkw``from_wkw_dict`
- `as_config``to_config_dict`
- `as_checkpoint_name``to_checkpoint_name`
- `as_tuple6``to_tuple6`
- `as_csv``to_csv`
- `as_named_tuple``to_named_tuple`
- `as_slices``to_slices`
- `copy` → (gone, immutable)

- `Mag`
- is now immutable
- `mag.mag` is now `mag._mag` (considered private, use to_list instead if you really need it as list)
- all `to_`-conversions return a copy, some were renamed:
- `to_array``to_list`
- `scale_by` → (gone, immutable)
- `divide_by` → (gone, immutable)
- `as_np``to_np`

### Added

- 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)

### Changed

- `BoundingBox` and `Mag` are now immutable attr classes containing `Vec3Int` values. See breaking changes above.

### Fixed

-

## [0.8.13](https://github.com/scalableminds/webknossos-cuber/releases/tag/v0.8.13) - 2021-09-22
[Commits](https://github.com/scalableminds/webknossos-cuber/compare/v0.8.12...v0.8.13)

This is the latest release at the time of creating this changelog.
264 changes: 140 additions & 124 deletions webknossos/poetry.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion webknossos/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ networkx = "^2.6.2"
numpy = "^1.15.0" # see https://numpy.org/neps/nep-0029-deprecation_policy.html#support-table
python-dateutil = "^2.8.0"
python-dotenv = "^0.19.0"
rich = "^10.9.0"
scikit-image = "^0.16.0"
scipy = "^1.4.0"
wkw = "1.1.11"
rich = "^10.9.0"

[tool.poetry.dev-dependencies]
# autoflake
Expand Down
13 changes: 13 additions & 0 deletions webknossos/tests/test_bounding_box.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ def test_in_mag() -> None:
assert BoundingBox((2, 2, 2), (10, 10, 10)).in_mag(Mag(2)) == BoundingBox(
topleft=(1, 1, 1), size=(5, 5, 5)
)


def test_with_bounds() -> None:

assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_x(0, 10) == BoundingBox(
(0, 2, 3), (10, 5, 5)
)
assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_y(
new_topleft_y=0
) == BoundingBox((1, 0, 3), (5, 5, 5))
assert BoundingBox((1, 2, 3), (5, 5, 5)).with_bounds_z(
new_size_z=10
) == BoundingBox((1, 2, 3), (5, 5, 10))
12 changes: 7 additions & 5 deletions webknossos/tests/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def test_modify_existing_dataset() -> None:
)

ds2 = Dataset(TESTOUTPUT_DIR / "simple_wk_dataset")

ds2.add_layer(
"segmentation",
LayerCategories.SEGMENTATION_TYPE,
Expand Down Expand Up @@ -761,18 +762,19 @@ def test_changing_layer_bounding_box() -> None:
original_data = mag.read(size=bbox_size)
assert original_data.shape == (3, 24, 24, 24)

old_bbox = layer.bounding_box
old_bbox.size = np.array([12, 12, 10])
layer.bounding_box = old_bbox # decrease bounding box
layer.bounding_box = layer.bounding_box.with_size(
[12, 12, 10]
) # decrease bounding box

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

old_bbox.size = np.array([36, 48, 60])
layer.bounding_box = old_bbox # increase the bounding box
layer.bounding_box = layer.bounding_box.with_size(
[36, 48, 60]
) # increase the bounding box

bbox_size = ds.get_layer("color").bounding_box.size
assert tuple(bbox_size) == (36, 48, 60)
Expand Down
10 changes: 5 additions & 5 deletions webknossos/tests/test_mag.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@

def test_mag_constructor() -> None:
mag = Mag(16)
assert mag.to_array() == [16, 16, 16]
assert mag.to_list() == [16, 16, 16]

mag = Mag("256")
assert mag.to_array() == [256, 256, 256]
assert mag.to_list() == [256, 256, 256]

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

assert mag.to_array() == [16, 2, 4]
assert mag.to_list() == [16, 2, 4]

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

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

assert np.all(mag1.as_np() == np.array([16, 2, 4]))
assert np.all(mag1.to_np() == np.array([16, 2, 4]))
assert mag1 == Mag(mag1)
assert mag1 == Mag(mag1.as_np())
assert mag1 == Mag(mag1.to_np())
95 changes: 95 additions & 0 deletions webknossos/tests/test_vec3_int.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import numpy as np

from webknossos.geometry import Mag, Vec3Int


def test_with() -> None:

assert Vec3Int(1, 2, 3).with_x(5) == Vec3Int(5, 2, 3)
assert Vec3Int(1, 2, 3).with_y(5) == Vec3Int(1, 5, 3)
assert Vec3Int(1, 2, 3).with_z(5) == Vec3Int(1, 2, 5)


def test_import() -> None:

assert Vec3Int(1, 2, 3) == Vec3Int(1, 2, 3)
assert Vec3Int((1, 2, 3)) == Vec3Int(1, 2, 3)
assert Vec3Int([1, 2, 3]) == Vec3Int(1, 2, 3)
assert Vec3Int(i for i in [1, 2, 3]) == Vec3Int(1, 2, 3)
assert Vec3Int(np.array([1, 2, 3])) == Vec3Int(1, 2, 3)
assert Vec3Int(Mag(4)) == Vec3Int(4, 4, 4)


def test_export() -> None:

assert Vec3Int(1, 2, 3).x == 1
assert Vec3Int(1, 2, 3).y == 2
assert Vec3Int(1, 2, 3).z == 3
assert Vec3Int(1, 2, 3)[0] == 1
assert Vec3Int(1, 2, 3)[1] == 2
assert Vec3Int(1, 2, 3)[2] == 3
assert np.array_equal(Vec3Int(1, 2, 3).to_np(), np.array([1, 2, 3]))
assert Vec3Int(1, 2, 3).to_list() == [1, 2, 3]
assert Vec3Int(1, 2, 3).to_tuple() == (1, 2, 3)


def test_operator_arithmetic() -> None:

# other is Vec3Int
assert Vec3Int(1, 2, 3) + Vec3Int(4, 5, 6) == Vec3Int(5, 7, 9)
assert Vec3Int(1, 2, 3) + Vec3Int(0, 0, 0) == Vec3Int(1, 2, 3)
assert Vec3Int(1, 2, 3) - Vec3Int(4, 5, 6) == Vec3Int(-3, -3, -3)
assert Vec3Int(1, 2, 3) * Vec3Int(4, 5, 6) == Vec3Int(4, 10, 18)
assert Vec3Int(4, 5, 6) // Vec3Int(1, 2, 3) == Vec3Int(4, 2, 2)
assert Vec3Int(4, 5, 6) % Vec3Int(1, 2, 3) == Vec3Int(0, 1, 0)

# other is scalar int
assert Vec3Int(1, 2, 3) * 3 == Vec3Int(3, 6, 9)
assert Vec3Int(1, 2, 3) + 3 == Vec3Int(4, 5, 6)
assert Vec3Int(1, 2, 3) - 3 == Vec3Int(-2, -1, 0)
assert Vec3Int(4, 5, 6) // 2 == Vec3Int(2, 2, 3)
assert Vec3Int(4, 5, 6) % 3 == Vec3Int(1, 2, 0)

# other is Vec3IntLike (e.g. tuple)
assert Vec3Int(1, 2, 3) + (4, 5, 6) == Vec3Int(5, 7, 9)

# be wary of the tuple “+” operation:
assert (1, 2, 3) + Vec3Int(4, 5, 6) == (1, 2, 3, 4, 5, 6)

assert -Vec3Int(1, 2, 3) == Vec3Int(-1, -2, -3)


def test_method_arithmetic() -> None:

assert Vec3Int(4, 5, 6).ceildiv(Vec3Int(1, 2, 3)) == Vec3Int(4, 3, 2)
assert Vec3Int(4, 5, 6).ceildiv((1, 2, 3)) == Vec3Int(4, 3, 2)
assert Vec3Int(4, 5, 6).ceildiv(2) == Vec3Int(2, 3, 3)

assert Vec3Int(1, 2, 6).pairmax(Vec3Int(4, 5, 3)) == Vec3Int(4, 5, 6)
assert Vec3Int(1, 2, 6).pairmin(Vec3Int(4, 5, 3)) == Vec3Int(1, 2, 3)


def test_repr() -> None:

assert str(Vec3Int(1, 2, 3)) == "Vec3Int(1,2,3)"


def test_prod() -> None:

assert Vec3Int(1, 2, 3).prod() == 6


def test_contains() -> None:

assert Vec3Int(1, 2, 3).contains(1)
assert not Vec3Int(1, 2, 3).contains(4)


def test_custom_initialization() -> None:

assert Vec3Int.zeros() == Vec3Int(0, 0, 0)
assert Vec3Int.ones() == Vec3Int(1, 1, 1)
assert Vec3Int.full(4) == Vec3Int(4, 4, 4)

assert Vec3Int.ones() - Vec3Int.ones() == Vec3Int.zeros()
assert Vec3Int.full(4) == Vec3Int.ones() * 4
17 changes: 7 additions & 10 deletions webknossos/webknossos/dataset/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@
from shutil import rmtree
from typing import Any, Dict, Optional, Tuple, Union, cast

import attr
import numpy as np
import wkw

from webknossos.geometry import BoundingBox
from webknossos.geometry import BoundingBox, Vec3Int
from webknossos.utils import get_executor_for_args

from .layer import (
Expand All @@ -32,7 +33,6 @@
_extract_num_channels,
_properties_floating_type_to_python_type,
dataset_converter,
layer_properties_converter,
)
from .view import View

Expand Down Expand Up @@ -238,8 +238,8 @@ def add_layer(

segmentation_layer_properties: SegmentationLayerProperties = (
SegmentationLayerProperties(
**layer_properties_converter.unstructure(
layer_properties
**(
attr.asdict(layer_properties, recurse=False)
), # use all attributes from LayerProperties
largest_segment_id=kwargs["largest_segment_id"],
)
Expand Down Expand Up @@ -517,12 +517,9 @@ def copy_dataset(

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

Expand Down
26 changes: 12 additions & 14 deletions webknossos/webknossos/dataset/downsampling_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
import math
from enum import Enum
from itertools import product
from typing import Callable, List, Optional, Tuple, Union, cast
from typing import Callable, List, Optional, Tuple, cast

import numpy as np
from scipy.ndimage import zoom
from wkw import wkw

from webknossos.geometry import Mag
from webknossos.geometry import Mag, Vec3Int, Vec3IntLike
from webknossos.utils import time_start, time_stop

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


Vec3 = Union[Tuple[int, int, int], np.ndarray]


def determine_buffer_edge_len(dataset: wkw.Dataset) -> int:
return min(DEFAULT_EDGE_LEN, dataset.header.file_len * dataset.header.block_len)


def calculate_mags_to_downsample(
from_mag: Mag, max_mag: Mag, scale: Optional[Tuple[float, float, float]]
) -> List[Mag]:
assert np.all(from_mag.as_np() <= max_mag.as_np())
assert np.all(from_mag.to_np() <= max_mag.to_np())
mags = []
current_mag = from_mag
while current_mag < max_mag:
if scale is None:
# In case the sampling mode is CONSTANT_Z or ISOTROPIC:
current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np()))
current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np()))
else:
# In case the sampling mode is ANISOTROPIC:
current_size = current_mag.as_np() * np.array(scale)
current_size = current_mag.to_np() * np.array(scale)
min_value = np.min(current_size)
min_value_bitmask = np.array(current_size == min_value)
factor = min_value_bitmask + 1
Expand All @@ -68,11 +65,11 @@ def calculate_mags_to_downsample(
# The smaller the ratio between the smallest dimension and the largest dimension, the better.
if all_scaled_ratio < min_scaled_ratio:
# Multiply all dimensions with "2"
current_mag = Mag(np.minimum(current_mag.as_np() * 2, max_mag.as_np()))
current_mag = Mag(np.minimum(current_mag.to_np() * 2, max_mag.to_np()))
else:
# Multiply only the minimal dimension by "2".
current_mag = Mag(
np.minimum(current_mag.as_np() * factor, max_mag.as_np())
np.minimum(current_mag.to_np() * factor, max_mag.to_np())
)

mags += [current_mag]
Expand All @@ -88,7 +85,8 @@ def calculate_mags_to_upsample(
] + [min_mag]


def calculate_default_max_mag(dataset_size: Vec3) -> Mag:
def calculate_default_max_mag(dataset_size: Vec3IntLike) -> Mag:
dataset_size = Vec3Int(dataset_size)
# The lowest mag should have a size of ~ 100vx**2 per slice
max_x_y = max(dataset_size[0], dataset_size[1])
# highest power of 2 larger (or equal) than max_x_y divided by 100
Expand Down Expand Up @@ -234,7 +232,7 @@ def downsample_unpadded_data(
logging.info(
f"Downsampling buffer of size {buffer.shape} to mag {target_mag.to_layer_name()}"
)
target_mag_np = np.array(target_mag.to_array())
target_mag_np = np.array(target_mag.to_list())
current_dimension_size = np.array(buffer.shape[1:])
padding_size_for_downsampling = (
target_mag_np - (current_dimension_size % target_mag_np) % target_mag_np
Expand All @@ -243,12 +241,12 @@ def downsample_unpadded_data(
buffer = np.pad(
buffer, pad_width=[(0, 0)] + padding_size_for_downsampling, mode="constant"
)
dimension_decrease = np.array([1] + target_mag.to_array())
dimension_decrease = np.array([1] + target_mag.to_list())
downsampled_buffer_shape = np.array(buffer.shape) // dimension_decrease
downsampled_buffer = np.empty(dtype=buffer.dtype, shape=downsampled_buffer_shape)
for channel in range(buffer.shape[0]):
downsampled_buffer[channel] = downsample_cube(
buffer[channel], target_mag.to_array(), interpolation_mode
buffer[channel], target_mag.to_list(), interpolation_mode
)
return downsampled_buffer

Expand Down
Loading

0 comments on commit 8d6a67a

Please sign in to comment.