tk-rt-viewer 2.3.0__tar.gz → 2.3.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/CHANGELOG.md +23 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/PKG-INFO +6 -3
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/README.md +5 -2
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/__init__.py +1 -1
- tk_rt_viewer-2.3.1/src/tk_rt_viewer/event_controllers/bbox3d_handler.py +60 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/geometry.py +19 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_bbox3d_handler.py +56 -9
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_geometry.py +17 -0
- tk_rt_viewer-2.3.0/src/tk_rt_viewer/event_controllers/bbox3d_handler.py +0 -59
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/.gitattributes +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/.github/workflows/ci.yml +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/.gitignore +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/LICENSE +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/pyproject.toml +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/__init__.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/bbox_handler.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/brush_handler.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/crosshair_handler.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/rect_drag.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/viewer_events.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/events.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/isodose_levels.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/protocols.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/py.typed +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/reg_io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/__init__.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/deformable.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/errors.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/params.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/rigid.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/session.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/registration/template.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/__init__.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/blit_compositor.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/contour_overlay.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/drawing_manager.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/dvh.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/image_layer.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/isodose.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/layout.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/render.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rendering/view_axes.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/roi_operations.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/rtstruct_io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/__init__.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/dose_manager.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/phase_manager.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/roi_editor.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/roi_manager.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/secondary_manager.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/structure_set.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/viewer_cache.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/state/viewer_state.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/viewer.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/window_level.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_bbox3d.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_brush_handler.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_event_controllers.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_isodose_levels.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_performance_opt.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_reg_io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_registration.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_render.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_rendering.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_roi_operations.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_rtstruct_io.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_state_extensions.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_state_managers.py +0 -0
- {tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/tests/test_viewer_state.py +0 -0
|
@@ -4,6 +4,29 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.3.1]
|
|
8
|
+
|
|
9
|
+
A newly drawn 3-D box no longer spans the whole image along the axis that
|
|
10
|
+
cannot be seen while drawing.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`geometry.fit_box_length(image, dim, length, min_extent)`**: returns
|
|
15
|
+
*length*, or half of the image's extent along *dim* when that extent is
|
|
16
|
+
shorter than *min_extent*.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- A 3-D box newly drawn on a view is now
|
|
21
|
+
**`Bbox3dEventHandler.NEW_BOX_DEPTH_MM`** (50 mm) deep along that view's
|
|
22
|
+
normal, centred on the displayed slice. It used to keep the depth of the box
|
|
23
|
+
it replaced, or span the whole image when none was left (e.g. after a
|
|
24
|
+
clearing click), which hid how deep the box was while drawing.
|
|
25
|
+
- Where the image along that normal is shorter than
|
|
26
|
+
**`Bbox3dEventHandler.NARROW_EXTENT_MM`** (60 mm), e.g. a short
|
|
27
|
+
craniocaudal scan range, the new box is half the image deep instead, so it
|
|
28
|
+
does not fill the image end to end.
|
|
29
|
+
|
|
7
30
|
## [2.3.0]
|
|
8
31
|
|
|
9
32
|
Zoom per view, a reversible wheel, and views that fill the figure.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tk-rt-viewer
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.1
|
|
4
4
|
Summary: SimpleITK-based DICOM-RT MPR viewer widget for Tkinter (CT, RT-DOSE, RT-STRUCT, DVH)
|
|
5
5
|
Project-URL: Homepage, https://github.com/oki1002/DICOM_RT_Viewer
|
|
6
6
|
Project-URL: Repository, https://github.com/oki1002/DICOM_RT_Viewer
|
|
@@ -544,7 +544,7 @@ visible the 3-D box takes the mouse.
|
|
|
544
544
|
```python
|
|
545
545
|
from tk_rt_viewer import Box3D
|
|
546
546
|
|
|
547
|
-
# Turn the tool on; the user draws on any view.
|
|
547
|
+
# Turn the tool on; the user draws on any view. Resizing on a second view
|
|
548
548
|
# trims the depth of the box drawn on the first.
|
|
549
549
|
state.set_bbox_3d_visible(True)
|
|
550
550
|
|
|
@@ -560,7 +560,10 @@ state.set_bounding_box_3d(None)
|
|
|
560
560
|
```
|
|
561
561
|
|
|
562
562
|
Clicking outside the box clears it, as with the per-view box; a drag that
|
|
563
|
-
follows
|
|
563
|
+
follows draws a new box in that plane, 50 mm deep along the view's normal and
|
|
564
|
+
centred on the displayed slice (`Bbox3dEventHandler.NEW_BOX_DEPTH_MM`), or half
|
|
565
|
+
the image deep where the image is narrower than 60 mm along that normal
|
|
566
|
+
(`NARROW_EXTENT_MM`). Resize or move it on another view to change its depth.
|
|
564
567
|
|
|
565
568
|
Each view draws the box solid while the displayed slice cuts through it and
|
|
566
569
|
dashed while it does not, so its depth is visible without leaving the slice.
|
|
@@ -507,7 +507,7 @@ visible the 3-D box takes the mouse.
|
|
|
507
507
|
```python
|
|
508
508
|
from tk_rt_viewer import Box3D
|
|
509
509
|
|
|
510
|
-
# Turn the tool on; the user draws on any view.
|
|
510
|
+
# Turn the tool on; the user draws on any view. Resizing on a second view
|
|
511
511
|
# trims the depth of the box drawn on the first.
|
|
512
512
|
state.set_bbox_3d_visible(True)
|
|
513
513
|
|
|
@@ -523,7 +523,10 @@ state.set_bounding_box_3d(None)
|
|
|
523
523
|
```
|
|
524
524
|
|
|
525
525
|
Clicking outside the box clears it, as with the per-view box; a drag that
|
|
526
|
-
follows
|
|
526
|
+
follows draws a new box in that plane, 50 mm deep along the view's normal and
|
|
527
|
+
centred on the displayed slice (`Bbox3dEventHandler.NEW_BOX_DEPTH_MM`), or half
|
|
528
|
+
the image deep where the image is narrower than 60 mm along that normal
|
|
529
|
+
(`NARROW_EXTENT_MM`). Resize or move it on another view to change its depth.
|
|
527
530
|
|
|
528
531
|
Each view draws the box solid while the displayed slice cuts through it and
|
|
529
532
|
dashed while it does not, so its depth is visible without leaving the slice.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""bbox3d_handler.py — Volumetric (3-D) bounding box drag handler.
|
|
2
|
+
|
|
3
|
+
The box is a single :class:`~tk_rt_viewer.geometry.Box3D` in
|
|
4
|
+
:class:`~tk_rt_viewer.state.viewer_state.SliceViewerState`; every view shows
|
|
5
|
+
its projection and the viewer renders it through the
|
|
6
|
+
``"bounding_box_3d_changed"`` listener.
|
|
7
|
+
|
|
8
|
+
A gesture on any view edits that one box and only the two dimensions the view
|
|
9
|
+
displays; the perpendicular dimension is set from a view that shows it. A new
|
|
10
|
+
box drawn on a view is :attr:`Bbox3dEventHandler.NEW_BOX_DEPTH_MM` deep,
|
|
11
|
+
centred on the displayed slice, so it never spans the whole image along the
|
|
12
|
+
axis the user cannot see while drawing. Along an image narrower than
|
|
13
|
+
:attr:`Bbox3dEventHandler.NARROW_EXTENT_MM` it is half the image instead.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ..geometry import AXIS_TO_XYZ_DIM, Box3D, fit_box_length
|
|
17
|
+
from .rect_drag import Rect, RectDragHandler
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Bbox3dEventHandler(RectDragHandler):
|
|
21
|
+
"""Create / move / resize the 3-D bounding box from any view."""
|
|
22
|
+
|
|
23
|
+
#: Depth (mm) of a newly drawn box along the normal of the view it is
|
|
24
|
+
#: drawn on, centred on the displayed slice. Hosts may override it.
|
|
25
|
+
NEW_BOX_DEPTH_MM: float = 50.0
|
|
26
|
+
|
|
27
|
+
#: Image extent (mm) along that normal below which the new box is half the
|
|
28
|
+
#: extent instead, so it does not fill a narrow scan range.
|
|
29
|
+
NARROW_EXTENT_MM: float = 60.0
|
|
30
|
+
|
|
31
|
+
def _is_enabled(self) -> bool:
|
|
32
|
+
return self.state.bbox_3d_visible and self.state.primary_image is not None
|
|
33
|
+
|
|
34
|
+
def _current_rect(self, axis: str) -> Rect | None:
|
|
35
|
+
box = self.state.bounding_box_3d
|
|
36
|
+
return None if box is None else box.project(axis)
|
|
37
|
+
|
|
38
|
+
def _clear(self, axis: str) -> None:
|
|
39
|
+
self.state.set_bounding_box_3d(None)
|
|
40
|
+
|
|
41
|
+
def _commit(self, axis: str, rect: Rect, mode: str) -> None:
|
|
42
|
+
if mode == "create" and self.state.primary_image is not None:
|
|
43
|
+
self.state.set_bounding_box_3d(self._new_box(axis, rect))
|
|
44
|
+
else:
|
|
45
|
+
self.state.set_bbox_3d_from_view(axis, rect)
|
|
46
|
+
|
|
47
|
+
def _new_box(self, axis: str, rect: Rect) -> Box3D:
|
|
48
|
+
"""Return a box drawn as *rect* on *axis*, centred on the displayed slice."""
|
|
49
|
+
dim = AXIS_TO_XYZ_DIM[axis]
|
|
50
|
+
center = self.state.index_to_physical(axis, self.state.indices[axis])
|
|
51
|
+
depth = fit_box_length(
|
|
52
|
+
self.state.primary_image,
|
|
53
|
+
dim,
|
|
54
|
+
self.NEW_BOX_DEPTH_MM,
|
|
55
|
+
self.NARROW_EXTENT_MM,
|
|
56
|
+
)
|
|
57
|
+
base = Box3D(lower=(0.0, 0.0, 0.0), upper=(0.0, 0.0, 0.0))
|
|
58
|
+
return base.with_range(
|
|
59
|
+
dim, center - depth / 2.0, center + depth / 2.0
|
|
60
|
+
).with_view_rect(axis, rect)
|
|
@@ -197,6 +197,25 @@ def as_point(values) -> tuple[float, float, float]:
|
|
|
197
197
|
return (float(values[0]), float(values[1]), float(values[2]))
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def fit_box_length(
|
|
201
|
+
image: sitk.Image, dim: int, length: float, min_extent: float
|
|
202
|
+
) -> float:
|
|
203
|
+
"""Return *length*, or half of *image*'s extent along *dim* if it is narrow.
|
|
204
|
+
|
|
205
|
+
A default-sized box would cover a narrow image (e.g. a short craniocaudal
|
|
206
|
+
scan range) end to end, so when the extent along physical dimension *dim*
|
|
207
|
+
(0=x, 1=y, 2=z) is shorter than *min_extent* the box takes half of it.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
image: The image the box is drawn on.
|
|
211
|
+
dim: Physical dimension (0=x, 1=y, 2=z).
|
|
212
|
+
length: Preferred box length in mm.
|
|
213
|
+
min_extent: Extent (mm) below which half the extent is used instead.
|
|
214
|
+
"""
|
|
215
|
+
extent = Box3D.from_image_extent(image).size[dim]
|
|
216
|
+
return extent / 2.0 if extent < min_extent else length
|
|
217
|
+
|
|
218
|
+
|
|
200
219
|
def view_dims(axis: str) -> tuple[int, int]:
|
|
201
220
|
"""Return the physical dimensions plotted on *axis*' x and y axes.
|
|
202
221
|
|
|
@@ -19,7 +19,7 @@ from matplotlib.figure import Figure
|
|
|
19
19
|
|
|
20
20
|
from tk_rt_viewer.event_controllers import rect_drag
|
|
21
21
|
from tk_rt_viewer.event_controllers.bbox3d_handler import Bbox3dEventHandler
|
|
22
|
-
from tk_rt_viewer.geometry import AXES
|
|
22
|
+
from tk_rt_viewer.geometry import AXES
|
|
23
23
|
from tk_rt_viewer.state.viewer_state import SliceViewerState
|
|
24
24
|
|
|
25
25
|
|
|
@@ -134,33 +134,80 @@ class TestRectDrag:
|
|
|
134
134
|
|
|
135
135
|
|
|
136
136
|
class TestBbox3dHandler:
|
|
137
|
-
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _slice_coordinate(state: SliceViewerState, axis: str) -> float:
|
|
139
|
+
return state.index_to_physical(axis, state.indices[axis])
|
|
140
|
+
|
|
141
|
+
def test_drag_creates_a_fixed_depth_box_on_the_displayed_slice(self, setup) -> None:
|
|
138
142
|
state, handler, _ = setup
|
|
139
143
|
drag(handler, (-10.0, -10.0), (10.0, 20.0))
|
|
140
144
|
|
|
141
145
|
box = state.bounding_box_3d
|
|
142
146
|
assert box is not None
|
|
143
147
|
assert box.project("axial") == pytest.approx((-10.0, -10.0, 20.0, 30.0))
|
|
144
|
-
|
|
145
|
-
|
|
148
|
+
center = self._slice_coordinate(state, "axial")
|
|
149
|
+
half = Bbox3dEventHandler.NEW_BOX_DEPTH_MM / 2.0
|
|
150
|
+
assert (box.lower[2], box.upper[2]) == pytest.approx(
|
|
151
|
+
(center - half, center + half)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def test_new_box_on_a_narrow_image_is_half_the_image_deep(self) -> None:
|
|
155
|
+
state = SliceViewerState()
|
|
156
|
+
# 10 axial slices x 3 mm = 30 mm craniocaudally (narrower than 60 mm)
|
|
157
|
+
image = sitk.GetImageFromArray(np.zeros((10, 40, 40), dtype=np.int16))
|
|
158
|
+
image.SetSpacing((2.0, 2.0, 3.0))
|
|
159
|
+
state.set_primary_image_data(image)
|
|
160
|
+
state.set_bbox_3d_visible(True)
|
|
161
|
+
handler = Bbox3dEventHandler(state, FakeViewer(), FakeHover())
|
|
162
|
+
|
|
163
|
+
drag(handler, (10.0, 10.0), (30.0, 30.0))
|
|
146
164
|
|
|
147
|
-
|
|
165
|
+
assert state.bounding_box_3d.size[2] == pytest.approx(15.0)
|
|
166
|
+
|
|
167
|
+
def test_new_box_after_a_clearing_click_keeps_the_fixed_depth(self, setup) -> None:
|
|
168
|
+
state, handler, _ = setup
|
|
169
|
+
drag(handler, (-10.0, -10.0), (10.0, 20.0))
|
|
170
|
+
drag(handler, (-30.0, -30.0), (-30.0, -30.0))
|
|
171
|
+
assert state.bounding_box_3d is None
|
|
172
|
+
|
|
173
|
+
drag(handler, (-20.0, -20.0), (0.0, 0.0))
|
|
174
|
+
|
|
175
|
+
box = state.bounding_box_3d
|
|
176
|
+
assert box.size[2] == pytest.approx(Bbox3dEventHandler.NEW_BOX_DEPTH_MM)
|
|
177
|
+
|
|
178
|
+
def test_new_box_on_another_view_uses_that_view_slice_for_depth(
|
|
148
179
|
self, setup
|
|
149
180
|
) -> None:
|
|
150
181
|
state, handler, hover = setup
|
|
151
182
|
drag(handler, (-10.0, -10.0), (10.0, 20.0))
|
|
152
|
-
y_range = (state.bounding_box_3d.lower[1], state.bounding_box_3d.upper[1])
|
|
153
183
|
|
|
154
|
-
# Start outside the current projection:
|
|
155
|
-
# plane,
|
|
184
|
+
# Start outside the current projection: a new box is drawn in this
|
|
185
|
+
# plane, its depth (y) centred on the displayed coronal slice
|
|
156
186
|
hover.current_axis = "coronal" # shows x and z
|
|
157
187
|
drag(handler, (-30.0, -6.0), (-20.0, 6.0))
|
|
158
188
|
|
|
159
189
|
box = state.bounding_box_3d
|
|
160
|
-
|
|
190
|
+
center = self._slice_coordinate(state, "coronal")
|
|
191
|
+
half = Bbox3dEventHandler.NEW_BOX_DEPTH_MM / 2.0
|
|
192
|
+
assert (box.lower[1], box.upper[1]) == pytest.approx(
|
|
193
|
+
(center - half, center + half)
|
|
194
|
+
)
|
|
161
195
|
assert (box.lower[2], box.upper[2]) == pytest.approx((-6.0, 6.0))
|
|
162
196
|
assert (box.lower[0], box.upper[0]) == pytest.approx((-30.0, -20.0))
|
|
163
197
|
|
|
198
|
+
def test_resize_on_another_view_keeps_the_depth(self, setup) -> None:
|
|
199
|
+
state, handler, hover = setup
|
|
200
|
+
drag(handler, (-10.0, -10.0), (10.0, 20.0))
|
|
201
|
+
y_range = (state.bounding_box_3d.lower[1], state.bounding_box_3d.upper[1])
|
|
202
|
+
|
|
203
|
+
hover.current_axis = "coronal"
|
|
204
|
+
z_low = state.bounding_box_3d.lower[2]
|
|
205
|
+
drag(handler, (0.0, z_low), (0.0, z_low - 5.0)) # bottom edge
|
|
206
|
+
|
|
207
|
+
box = state.bounding_box_3d
|
|
208
|
+
assert (box.lower[1], box.upper[1]) == y_range
|
|
209
|
+
assert box.lower[2] == pytest.approx(z_low - 5.0)
|
|
210
|
+
|
|
164
211
|
def test_click_outside_clears_the_box(self, setup) -> None:
|
|
165
212
|
state, handler, _ = setup
|
|
166
213
|
drag(handler, (-10.0, -10.0), (10.0, 20.0))
|
|
@@ -14,6 +14,7 @@ import SimpleITK as sitk
|
|
|
14
14
|
from tk_rt_viewer.geometry import (
|
|
15
15
|
AXES,
|
|
16
16
|
compute_extent,
|
|
17
|
+
fit_box_length,
|
|
17
18
|
mask_slice_to_paths,
|
|
18
19
|
slice_along_axis,
|
|
19
20
|
)
|
|
@@ -123,3 +124,19 @@ class TestSliceAlongAxis:
|
|
|
123
124
|
np.testing.assert_array_equal(
|
|
124
125
|
slice_along_axis(arr, "sagittal", 3), arr[:, :, 3]
|
|
125
126
|
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestFitBoxLength:
|
|
130
|
+
@staticmethod
|
|
131
|
+
def _image(slices: int) -> sitk.Image:
|
|
132
|
+
image = sitk.GetImageFromArray(np.zeros((slices, 4, 4), dtype=np.int16))
|
|
133
|
+
image.SetSpacing((1.0, 1.0, 2.0))
|
|
134
|
+
return image
|
|
135
|
+
|
|
136
|
+
def test_keeps_the_length_on_a_wide_image(self) -> None:
|
|
137
|
+
# 30 slices x 2 mm = 60 mm: not narrower than the threshold
|
|
138
|
+
assert fit_box_length(self._image(30), 2, 50.0, 60.0) == 50.0
|
|
139
|
+
|
|
140
|
+
def test_uses_half_the_extent_on_a_narrow_image(self) -> None:
|
|
141
|
+
# 20 slices x 2 mm = 40 mm
|
|
142
|
+
assert fit_box_length(self._image(20), 2, 50.0, 60.0) == 40.0 / 2
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
"""bbox3d_handler.py — Volumetric (3-D) bounding box drag handler.
|
|
2
|
-
|
|
3
|
-
The box is a single :class:`~tk_rt_viewer.geometry.Box3D` in
|
|
4
|
-
:class:`~tk_rt_viewer.state.viewer_state.SliceViewerState`; every view shows
|
|
5
|
-
its projection and the viewer renders it through the
|
|
6
|
-
``"bounding_box_3d_changed"`` listener.
|
|
7
|
-
|
|
8
|
-
A gesture on any view edits that one box and only the two dimensions the view
|
|
9
|
-
displays; the perpendicular dimension is set from a view that shows it. A new
|
|
10
|
-
box drawn on a view keeps the depth of the box it replaces, or spans the whole
|
|
11
|
-
image when there was none.
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
from typing import TYPE_CHECKING
|
|
15
|
-
|
|
16
|
-
from ..geometry import Box3D
|
|
17
|
-
from ..protocols import ViewerHost
|
|
18
|
-
from .rect_drag import Rect, RectDragHandler
|
|
19
|
-
|
|
20
|
-
if TYPE_CHECKING:
|
|
21
|
-
from ..state.viewer_state import SliceViewerState
|
|
22
|
-
from .viewer_events import ViewerEventHandler
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class Bbox3dEventHandler(RectDragHandler):
|
|
26
|
-
"""Create / move / resize the 3-D bounding box from any view."""
|
|
27
|
-
|
|
28
|
-
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
state: "SliceViewerState",
|
|
31
|
-
viewer: ViewerHost,
|
|
32
|
-
hover: "ViewerEventHandler",
|
|
33
|
-
) -> None:
|
|
34
|
-
super().__init__(state, viewer, hover)
|
|
35
|
-
# The box cleared by a create press; its depth is reused for the new one
|
|
36
|
-
self._previous_box: Box3D | None = None
|
|
37
|
-
|
|
38
|
-
def cancel(self) -> None:
|
|
39
|
-
super().cancel()
|
|
40
|
-
self._previous_box = None
|
|
41
|
-
|
|
42
|
-
def _is_enabled(self) -> bool:
|
|
43
|
-
return self.state.bbox_3d_visible and self.state.primary_image is not None
|
|
44
|
-
|
|
45
|
-
def _current_rect(self, axis: str) -> Rect | None:
|
|
46
|
-
box = self.state.bounding_box_3d
|
|
47
|
-
return None if box is None else box.project(axis)
|
|
48
|
-
|
|
49
|
-
def _clear(self, axis: str) -> None:
|
|
50
|
-
self._previous_box = self.state.bounding_box_3d
|
|
51
|
-
self.state.set_bounding_box_3d(None)
|
|
52
|
-
|
|
53
|
-
def _commit(self, axis: str, rect: Rect, mode: str) -> None:
|
|
54
|
-
image = self.state.primary_image
|
|
55
|
-
if mode == "create" and image is not None:
|
|
56
|
-
base = self._previous_box or Box3D.from_image_extent(image)
|
|
57
|
-
self.state.set_bounding_box_3d(base.with_view_rect(axis, rect))
|
|
58
|
-
else:
|
|
59
|
-
self.state.set_bbox_3d_from_view(axis, rect)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/bbox_handler.py
RENAMED
|
File without changes
|
{tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/brush_handler.py
RENAMED
|
File without changes
|
{tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/crosshair_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{tk_rt_viewer-2.3.0 → tk_rt_viewer-2.3.1}/src/tk_rt_viewer/event_controllers/viewer_events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|