Skip to content

Commit 2072830

Browse files
authored
[wk-libs] Allow neg bbox topleft (#589)
* upgrade pdoc to fix some linkify behavior * warn when layers are not uploaded due to linking * allow negative bbox.topleft, always convert size to positive pendant * add explaining comment * add changelog entry * don't cancel ci builds on master * ci syntax fix
1 parent 335fd86 commit 2072830

File tree

9 files changed

+60
-20
lines changed

9 files changed

+60
-20
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ on: push
44

55
concurrency:
66
group: ${{ github.workflow }}-${{ github.ref }}
7-
cancel-in-progress: true
7+
cancel-in-progress: ${{ github.ref != 'refs/heads/master' }}
88

99
jobs:
1010
changes:

docs/poetry.lock

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

docs/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ LinkChecker = "^10.0.1"
1313
mkdocs = "1.2.2"
1414
mkdocs-material = "^7.3.0"
1515
mkdocs-redirects = "^1.0.3"
16-
pdoc = "^9.0.1"
16+
pdoc = { git = "https://github.com/mitmproxy/pdoc.git", rev = "1574222ab0568e072dc04e3569027d39aa124256"}
1717
webknossos = { path = "../webknossos/", develop = true }
1818
wkcuber = { path = "../wkcuber/", develop = true }
1919

webknossos/Changelog.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ For upgrade instructions, please check the respective *Breaking Changes* section
1515
- Added AnnotationInfo, Project and Task classes for handling annotation information and annotation project administration. [#574](https://github.com/scalableminds/webknossos-libs/pull/574):
1616

1717
### Changed
18+
- Lifted the restriction that `BoundingBox` cannot have a negative topleft (introduced in v0.9.0). Also, negative size dimensions are flipped, so that the topleft <= bottomright,
19+
e.g. `BoundingBox((10, 10, 10), (-5, 5, 5))` -> `BoundingBox((5, 10, 10), (5, 5, 5))`. [#589](https://github.com/scalableminds/webknossos-libs/pull/589)
1820

1921
### Fixed
2022

@@ -65,6 +67,7 @@ For upgrade instructions, please check the respective *Breaking Changes* section
6567
the default behaviour changes from starting at absolute (0, 0, 0) to the layer's bounding box
6668
* `(Mag)View.get_view`: read_only is a keyword-only argument now
6769
* `MagView.get_bounding_boxes_on_disk()` now returns an iterator yielding bounding boxes in Mag(1)
70+
* `BoundingBox` cannot have negative topleft or size entries anymore (lifted in v0.9.4).
6871
- **Deprecations**
6972
The following usages are marked as deprecated with warnings and will be removed in future releases:
7073
* Using the `offset` parameter for `read`/`write`/`get_view` in MagView and View is deprecated.

webknossos/tests/test_bounding_box.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,20 @@ def test_align_with_mag_against_numpy_implementation(
9292
# The slower numpy implementation is wrong for very large numbers:
9393
if all(i < 12e15 for i in bb.bottomright):
9494
assert bb.align_with_mag(mag, ceil) == slow_np_result
95+
96+
97+
def test_negative_size() -> None:
98+
assert BoundingBox((10, 10, 10), (-5, 5, 5)) == BoundingBox((5, 10, 10), (5, 5, 5))
99+
assert BoundingBox((10, 10, 10), (-5, 5, -5)) == BoundingBox((5, 10, 5), (5, 5, 5))
100+
assert BoundingBox((10, 10, 10), (-5, 5, -50)) == BoundingBox(
101+
(5, 10, -40), (5, 5, 50)
102+
)
103+
104+
105+
@given(bbox=infer)
106+
def test_negative_inversion(
107+
bbox: BoundingBox,
108+
) -> None:
109+
"""Flipping the topleft and bottomright (by padding both with the negative size)
110+
results in the original bbox, as negative sizes are converted to positive ones."""
111+
assert bbox == bbox.padded_with_margins(-bbox.size, -bbox.size)

webknossos/webknossos/client/_upload_dataset.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
import warnings
23
from functools import lru_cache
34
from pathlib import Path
45
from tempfile import TemporaryDirectory
@@ -85,6 +86,10 @@ def upload_dataset(
8586
context = _get_context()
8687
layer_names_to_link = set(i.new_layer_name or i.layer_name for i in layers_to_link)
8788
if len(layer_names_to_link.intersection(dataset.layers.keys())) > 0:
89+
warnings.warn(
90+
"Excluding the following layers from upload, since they will be linked: "
91+
+ f"{layer_names_to_link.intersection(dataset.layers.keys())}"
92+
)
8893
with TemporaryDirectory() as tmpdir:
8994
tmp_ds = dataset.shallow_copy_dataset(
9095
tmpdir, name=dataset.name, layers_to_ignore=layer_names_to_link

webknossos/webknossos/dataset/layer.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,9 @@ def bounding_box(self, bbox: BoundingBox) -> None:
499499
"""
500500
Updates the offset and size of the bounding box of this layer in the properties.
501501
"""
502+
assert (
503+
bbox.topleft.is_positive()
504+
), f"Updating the bounding box of layer {self} to {bbox} failed, topleft must not contain negative dimensions."
502505
self._properties.bounding_box = bbox
503506
self.dataset._export_as_json()
504507

webknossos/webknossos/dataset/view.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,9 @@ def read(
371371
f"The size ({mag1_bbox.size} in mag1) contains a zero. "
372372
+ "All dimensions must be strictly larger than '0'."
373373
)
374+
assert (
375+
mag1_bbox.topleft.is_positive()
376+
), f"The offset ({mag1_bbox.topleft} in mag1) must not contain negative dimensions."
374377

375378
return self._read_without_checks(mag1_bbox.in_mag(self._mag))
376379

@@ -525,6 +528,9 @@ def get_view(
525528
f"The size ({mag1_bbox.size} in mag1) contains a zero. "
526529
+ "All dimensions must be strictly larger than '0'."
527530
)
531+
assert (
532+
mag1_bbox.topleft.is_positive()
533+
), f"The offset ({mag1_bbox.topleft} in mag1) must not contain negative dimensions."
528534

529535
if not read_only:
530536
assert self.bounding_box.contains_bbox(mag1_bbox), (

webknossos/webknossos/geometry/bounding_box.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import json
22
import re
33
from collections import defaultdict
4-
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
4+
from typing import Dict, Generator, Iterable, List, Optional, Tuple, Union, cast
55

66
import attr
77
import numpy as np
@@ -16,15 +16,16 @@ class BoundingBox:
1616
size: Vec3Int = attr.field(converter=Vec3Int)
1717
bottomright = attr.field(init=False)
1818

19-
@topleft.validator
20-
def validate_topleft(self, _attribute: Any, value: Vec3Int) -> None:
21-
assert value.is_positive(), "topleft of a BoundingBox must not be negative"
22-
23-
@topleft.validator
24-
def validate_size(self, _attribute: Any, value: Vec3Int) -> None:
25-
assert value.is_positive(), "size of a BoundingBox must not be negative"
26-
2719
def __attrs_post_init__(self) -> None:
20+
if not self.size.is_positive():
21+
# Flip the size in negative dimensions, so that the topleft is smaller than bottomright.
22+
# E.g. BoundingBox((10, 10, 10), (-5, 5, 5)) -> BoundingBox((5, 10, 10), (5, 5, 5)).
23+
negative_size = self.size.pairmin(Vec3Int.zeros())
24+
new_topleft = self.topleft + negative_size
25+
new_size = self.size.pairmax(-self.size)
26+
object.__setattr__(self, "topleft", new_topleft)
27+
object.__setattr__(self, "size", new_size)
28+
2829
# Compute bottomright to avoid that it's recomputed every time
2930
# it is needed.
3031
object.__setattr__(self, "bottomright", self.topleft + self.size)
@@ -238,9 +239,9 @@ def intersected_with(
238239
) -> "BoundingBox":
239240
"""If dont_assert is set to False, this method may return empty bounding boxes (size == (0, 0, 0))"""
240241

241-
topleft = np.maximum(self.topleft.to_np(), other.topleft.to_np())
242-
bottomright = np.minimum(self.bottomright.to_np(), other.bottomright.to_np())
243-
size = np.maximum(bottomright - topleft, (0, 0, 0))
242+
topleft = self.topleft.pairmax(other.topleft)
243+
bottomright = self.bottomright.pairmin(other.bottomright)
244+
size = (bottomright - topleft).pairmax(Vec3Int.zeros())
244245

245246
intersection = BoundingBox(topleft, size)
246247

@@ -257,8 +258,8 @@ def extended_by(self, other: "BoundingBox") -> "BoundingBox":
257258
if other.is_empty():
258259
return self
259260

260-
topleft = np.minimum(self.topleft, other.topleft)
261-
bottomright = np.maximum(self.bottomright, other.bottomright)
261+
topleft = self.topleft.pairmin(other.topleft)
262+
bottomright = self.bottomright.pairmax(other.bottomright)
262263
size = bottomright - topleft
263264

264265
return BoundingBox(topleft, size)

0 commit comments

Comments
 (0)