tk-rt-viewer 2.3.2__tar.gz → 2.3.3__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.2 → tk_rt_viewer-2.3.3}/CHANGELOG.md +12 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/PKG-INFO +6 -1
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/README.md +5 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/__init__.py +1 -1
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/io.py +2 -2
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rtstruct_io.py +36 -8
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_rtstruct_io.py +105 -2
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/.gitattributes +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/.github/workflows/ci.yml +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/.gitignore +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/LICENSE +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/pyproject.toml +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/__init__.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/bbox3d_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/bbox_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/brush_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/crosshair_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/rect_drag.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/viewer_events.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/events.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/geometry.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/isodose_levels.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/protocols.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/py.typed +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/reg_io.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/__init__.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/deformable.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/errors.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/params.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/rigid.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/session.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/registration/template.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/__init__.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/blit_compositor.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/contour_overlay.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/drawing_manager.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/dvh.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/image_layer.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/isodose.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/layout.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/render.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/rendering/view_axes.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/roi_operations.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/__init__.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/dose_manager.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/phase_manager.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/roi_editor.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/roi_manager.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/secondary_manager.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/structure_set.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/viewer_cache.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/state/viewer_state.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/viewer.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/window_level.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_bbox3d.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_bbox3d_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_brush_handler.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_event_controllers.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_geometry.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_io.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_isodose_levels.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_performance_opt.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_reg_io.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_registration.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_render.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_rendering.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_roi_operations.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_state_extensions.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_state_managers.py +0 -0
- {tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/tests/test_viewer_state.py +0 -0
|
@@ -4,6 +4,18 @@ 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.3]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `rtstruct_io.mask2rtstruct` and `rtstruct_io.save_structure_set` take a
|
|
12
|
+
keyword-only `add_roi_kwargs` mapping, forwarded to rt-utils'
|
|
13
|
+
`RTStruct.add_roi` for every ROI. Callers can now set
|
|
14
|
+
`approximate_contours`, `use_pin_hole`, `roi_generation_algorithm`,
|
|
15
|
+
`description`, etc. `mask`, `color` and `name` still come from the
|
|
16
|
+
structures and raise `ValueError` if given here. The default (`None`)
|
|
17
|
+
keeps the previous behaviour.
|
|
18
|
+
|
|
7
19
|
## [2.3.2]
|
|
8
20
|
|
|
9
21
|
### Fixed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tk-rt-viewer
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.3
|
|
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
|
|
@@ -374,6 +374,11 @@ written = save_structure_set(
|
|
|
374
374
|
)
|
|
375
375
|
```
|
|
376
376
|
|
|
377
|
+
Pass `add_roi_kwargs` to forward options to rt-utils' `RTStruct.add_roi`
|
|
378
|
+
for every ROI, e.g. `add_roi_kwargs={"approximate_contours": False}` to
|
|
379
|
+
keep every contour vertex instead of the simplified outline. `mask2rtstruct`
|
|
380
|
+
accepts the same keyword.
|
|
381
|
+
|
|
377
382
|
`rt_utils.RTStruct.save` silently appends `.dcm` to a path that does not
|
|
378
383
|
already end with it, with no way to opt out; `mask2rtstruct` resolves
|
|
379
384
|
against that internally so its `exists()` check, its log messages, and its
|
|
@@ -337,6 +337,11 @@ written = save_structure_set(
|
|
|
337
337
|
)
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
+
Pass `add_roi_kwargs` to forward options to rt-utils' `RTStruct.add_roi`
|
|
341
|
+
for every ROI, e.g. `add_roi_kwargs={"approximate_contours": False}` to
|
|
342
|
+
keep every contour vertex instead of the simplified outline. `mask2rtstruct`
|
|
343
|
+
accepts the same keyword.
|
|
344
|
+
|
|
340
345
|
`rt_utils.RTStruct.save` silently appends `.dcm` to a path that does not
|
|
341
346
|
already end with it, with no way to opt out; `mask2rtstruct` resolves
|
|
342
347
|
against that internally so its `exists()` check, its log messages, and its
|
|
@@ -55,7 +55,7 @@ import re
|
|
|
55
55
|
from collections.abc import Iterable, Iterator, Sequence
|
|
56
56
|
from concurrent.futures import ThreadPoolExecutor
|
|
57
57
|
from dataclasses import dataclass, field, replace
|
|
58
|
-
from typing import TypedDict
|
|
58
|
+
from typing import TypedDict, cast
|
|
59
59
|
|
|
60
60
|
import numpy as np
|
|
61
61
|
import pydicom
|
|
@@ -672,7 +672,7 @@ def _read_files(
|
|
|
672
672
|
"""Read *file_names* as one series, keeping per-slice metadata on *reader*."""
|
|
673
673
|
reader.SetFileNames(file_names)
|
|
674
674
|
reader.MetaDataDictionaryArrayUpdateOn()
|
|
675
|
-
image = reader.Execute()
|
|
675
|
+
image = cast(sitk.Image, reader.Execute())
|
|
676
676
|
logger.info(f"Series '{series_id}' loaded with {len(file_names)} files.")
|
|
677
677
|
return image
|
|
678
678
|
|
|
@@ -8,13 +8,15 @@ load_rt_struct(ct_dir, rtstruct_path, progress_callback=None, max_workers=1)
|
|
|
8
8
|
and display metadata. Raises RtStructLoadError if the file itself
|
|
9
9
|
cannot be parsed.
|
|
10
10
|
|
|
11
|
-
mask2rtstruct(ct_dir, rtss_path, structures
|
|
11
|
+
mask2rtstruct(ct_dir, rtss_path, structures, *, replace_existing=True,
|
|
12
|
+
add_roi_kwargs=None) -> pathlib.Path
|
|
12
13
|
Convert NumPy mask arrays to an RT-STRUCT DICOM file, creating or
|
|
13
14
|
updating as appropriate. Returns the path actually written to (rt-utils
|
|
14
|
-
appends ".dcm" to a path that lacks it).
|
|
15
|
+
appends ".dcm" to a path that lacks it). add_roi_kwargs is forwarded to
|
|
16
|
+
rt-utils' RTStruct.add_roi (e.g. approximate_contours).
|
|
15
17
|
|
|
16
18
|
save_structure_set(structure_set, ct_dir, rtss_path, lps_image,
|
|
17
|
-
original_image=None) -> int
|
|
19
|
+
original_image=None, *, add_roi_kwargs=None) -> int
|
|
18
20
|
Write every ROI of a StructureSet to an RT-STRUCT file, resampling
|
|
19
21
|
each mask back to the original DICOM geometry first.
|
|
20
22
|
|
|
@@ -29,7 +31,7 @@ random_hex_color() -> str
|
|
|
29
31
|
import logging
|
|
30
32
|
import pathlib
|
|
31
33
|
from collections import Counter
|
|
32
|
-
from collections.abc import Callable
|
|
34
|
+
from collections.abc import Callable, Mapping
|
|
33
35
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
34
36
|
from typing import TYPE_CHECKING, Any, TypedDict
|
|
35
37
|
|
|
@@ -63,6 +65,9 @@ _COLOR_RNG: np.random.Generator = np.random.default_rng()
|
|
|
63
65
|
#: Suffix rt-utils' ``RTStruct.save()`` silently appends to any path lacking it.
|
|
64
66
|
_RTSTRUCT_SUFFIX = ".dcm"
|
|
65
67
|
|
|
68
|
+
#: add_roi keywords mask2rtstruct fills in itself from *structures*
|
|
69
|
+
_RESERVED_ADD_ROI_KEYS = frozenset({"mask", "color", "name"})
|
|
70
|
+
|
|
66
71
|
|
|
67
72
|
def _resolved_rtss_path(rtss_path: pathlib.Path) -> pathlib.Path:
|
|
68
73
|
"""Return the path rt-utils will actually write *rtss_path* to.
|
|
@@ -97,6 +102,8 @@ def save_structure_set(
|
|
|
97
102
|
rtss_path: str | pathlib.Path,
|
|
98
103
|
lps_image: sitk.Image,
|
|
99
104
|
original_image: sitk.Image | None = None,
|
|
105
|
+
*,
|
|
106
|
+
add_roi_kwargs: Mapping[str, Any] | None = None,
|
|
100
107
|
) -> int:
|
|
101
108
|
"""Write every ROI of *structure_set* to an RT-STRUCT file.
|
|
102
109
|
|
|
@@ -115,13 +122,15 @@ def save_structure_set(
|
|
|
115
122
|
original_image: The CT before LPS alignment
|
|
116
123
|
(``SeriesInfo["original_sitk_image"]``). ``None`` when the series
|
|
117
124
|
needed no reorientation; the masks are then written as they are.
|
|
125
|
+
add_roi_kwargs: Forwarded to :func:`mask2rtstruct`.
|
|
118
126
|
|
|
119
127
|
Returns:
|
|
120
128
|
The number of ROIs written.
|
|
121
129
|
|
|
122
130
|
Raises:
|
|
123
131
|
ValueError: If *structure_set* contains no ROI with a usable mask,
|
|
124
|
-
since that would write an RT-STRUCT with no structures at all
|
|
132
|
+
since that would write an RT-STRUCT with no structures at all,
|
|
133
|
+
or if *add_roi_kwargs* contains a reserved key.
|
|
125
134
|
RuntimeError: If rt-utils rejects an ROI (propagated from
|
|
126
135
|
:func:`mask2rtstruct`).
|
|
127
136
|
"""
|
|
@@ -146,7 +155,9 @@ def save_structure_set(
|
|
|
146
155
|
if not structures:
|
|
147
156
|
raise ValueError("Structure set contains no ROI with a mask to save.")
|
|
148
157
|
|
|
149
|
-
written_path = mask2rtstruct(
|
|
158
|
+
written_path = mask2rtstruct(
|
|
159
|
+
ct_dir, rtss_path, structures, add_roi_kwargs=add_roi_kwargs
|
|
160
|
+
)
|
|
150
161
|
logger.info(f"Structure set saved to '{written_path}' ({len(structures)} ROIs).")
|
|
151
162
|
return len(structures)
|
|
152
163
|
|
|
@@ -333,6 +344,7 @@ def mask2rtstruct(
|
|
|
333
344
|
structures: dict[int, dict[str, Any]],
|
|
334
345
|
*,
|
|
335
346
|
replace_existing: bool = True,
|
|
347
|
+
add_roi_kwargs: Mapping[str, Any] | None = None,
|
|
336
348
|
) -> pathlib.Path:
|
|
337
349
|
"""Write ``(D, H, W)`` mask arrays to an RT-STRUCT DICOM file.
|
|
338
350
|
|
|
@@ -346,17 +358,32 @@ def mask2rtstruct(
|
|
|
346
358
|
rebuilt so it holds exactly *structures*. ``False`` appends to the
|
|
347
359
|
existing file instead; rt-utils cannot remove ROIs, so saving the
|
|
348
360
|
same ROIs twice that way duplicates them.
|
|
361
|
+
add_roi_kwargs: Extra keyword arguments forwarded to rt-utils'
|
|
362
|
+
``RTStruct.add_roi`` for every ROI, e.g.
|
|
363
|
+
``{"approximate_contours": False, "use_pin_hole": True}``.
|
|
364
|
+
``mask``, ``color`` and ``name`` are taken from *structures*
|
|
365
|
+
and must not be given here.
|
|
349
366
|
|
|
350
367
|
Returns:
|
|
351
368
|
The path actually written.
|
|
352
369
|
|
|
353
370
|
Raises:
|
|
354
|
-
ValueError: If *rtss_path* is ``None
|
|
355
|
-
|
|
371
|
+
ValueError: If *rtss_path* is ``None`` or *add_roi_kwargs* contains
|
|
372
|
+
a reserved key.
|
|
373
|
+
RuntimeError: If any ROI cannot be added to the RT-STRUCT builder
|
|
374
|
+
(including unknown keys in *add_roi_kwargs*).
|
|
356
375
|
"""
|
|
357
376
|
if rtss_path is None:
|
|
358
377
|
raise ValueError("rtss_path must not be None; provide a concrete output path.")
|
|
359
378
|
|
|
379
|
+
roi_options = dict(add_roi_kwargs or {})
|
|
380
|
+
reserved = _RESERVED_ADD_ROI_KEYS & roi_options.keys()
|
|
381
|
+
if reserved:
|
|
382
|
+
raise ValueError(
|
|
383
|
+
f"add_roi_kwargs must not contain {sorted(reserved)}; "
|
|
384
|
+
"they are taken from structures."
|
|
385
|
+
)
|
|
386
|
+
|
|
360
387
|
ct_dir = pathlib.Path(ct_dir)
|
|
361
388
|
rtss_path = _resolved_rtss_path(pathlib.Path(rtss_path))
|
|
362
389
|
|
|
@@ -382,6 +409,7 @@ def mask2rtstruct(
|
|
|
382
409
|
mask=np.transpose(roi_data["mask"], (1, 2, 0)).astype(bool, copy=False),
|
|
383
410
|
color=roi_data["color"],
|
|
384
411
|
name=roi_name,
|
|
412
|
+
**roi_options,
|
|
385
413
|
)
|
|
386
414
|
except Exception as exc:
|
|
387
415
|
logger.error(f"Failed to add ROI '{roi_name}': {exc}")
|
|
@@ -22,16 +22,17 @@ from tk_rt_viewer.state.viewer_state import StructureSet
|
|
|
22
22
|
|
|
23
23
|
@pytest.fixture()
|
|
24
24
|
def captured_structures(monkeypatch) -> dict:
|
|
25
|
-
"""Capture the
|
|
25
|
+
"""Capture the arguments passed to mask2rtstruct."""
|
|
26
26
|
captured: dict = {}
|
|
27
27
|
|
|
28
28
|
def fake_mask2rtstruct(
|
|
29
|
-
ct_dir, rtss_path, structures, *, replace_existing=True
|
|
29
|
+
ct_dir, rtss_path, structures, *, replace_existing=True, add_roi_kwargs=None
|
|
30
30
|
) -> pathlib.Path:
|
|
31
31
|
captured["ct_dir"] = ct_dir
|
|
32
32
|
captured["rtss_path"] = rtss_path
|
|
33
33
|
captured["structures"] = structures
|
|
34
34
|
captured["replace_existing"] = replace_existing
|
|
35
|
+
captured["add_roi_kwargs"] = add_roi_kwargs
|
|
35
36
|
return pathlib.Path(rtss_path)
|
|
36
37
|
|
|
37
38
|
monkeypatch.setattr(rtstruct_io, "mask2rtstruct", fake_mask2rtstruct)
|
|
@@ -142,6 +143,22 @@ class TestSaveStructureSet:
|
|
|
142
143
|
|
|
143
144
|
assert captured_structures["replace_existing"] is True
|
|
144
145
|
|
|
146
|
+
def test_add_roi_kwargs_is_forwarded(self, captured_structures) -> None:
|
|
147
|
+
lps = _image()
|
|
148
|
+
structure_set = StructureSet()
|
|
149
|
+
structure_set.add("PTV", _mask(lps), "#ff0000")
|
|
150
|
+
options = {"approximate_contours": False}
|
|
151
|
+
|
|
152
|
+
rtstruct_io.save_structure_set(
|
|
153
|
+
structure_set,
|
|
154
|
+
"/ct",
|
|
155
|
+
"/out/rs.dcm",
|
|
156
|
+
lps_image=lps,
|
|
157
|
+
add_roi_kwargs=options,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
assert captured_structures["add_roi_kwargs"] == options
|
|
161
|
+
|
|
145
162
|
|
|
146
163
|
class TestSaveStructureSetResampling:
|
|
147
164
|
"""Resampling must happen only when there is a different geometry to reach."""
|
|
@@ -344,6 +361,92 @@ class TestMask2RtStructImplicitDcmSuffix:
|
|
|
344
361
|
assert written == out
|
|
345
362
|
|
|
346
363
|
|
|
364
|
+
class TestMask2RtStructAddRoiKwargs:
|
|
365
|
+
"""``add_roi_kwargs`` reaches ``RTStruct.add_roi`` for every ROI."""
|
|
366
|
+
|
|
367
|
+
@staticmethod
|
|
368
|
+
def _structures() -> dict:
|
|
369
|
+
mask = np.zeros((4, 8, 8), dtype=bool)
|
|
370
|
+
mask[1:3, 2:6, 2:6] = True
|
|
371
|
+
return {
|
|
372
|
+
1: {"name": "PTV", "mask": mask, "color": "#ff0000"},
|
|
373
|
+
2: {"name": "CTV", "mask": mask.copy(), "color": "#00ff00"},
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _ct_dir(tmp_path) -> pathlib.Path:
|
|
378
|
+
ct_dir = tmp_path / "ct"
|
|
379
|
+
ct_dir.mkdir()
|
|
380
|
+
_write_minimal_ct_series(ct_dir)
|
|
381
|
+
return ct_dir
|
|
382
|
+
|
|
383
|
+
def test_options_are_passed_to_every_add_roi_call(
|
|
384
|
+
self, tmp_path, monkeypatch
|
|
385
|
+
) -> None:
|
|
386
|
+
from rt_utils import RTStruct
|
|
387
|
+
|
|
388
|
+
calls: list[dict] = []
|
|
389
|
+
real_add_roi = RTStruct.add_roi
|
|
390
|
+
|
|
391
|
+
def spy(self, **kwargs):
|
|
392
|
+
calls.append(kwargs)
|
|
393
|
+
return real_add_roi(self, **kwargs)
|
|
394
|
+
|
|
395
|
+
monkeypatch.setattr(RTStruct, "add_roi", spy)
|
|
396
|
+
|
|
397
|
+
rtstruct_io.mask2rtstruct(
|
|
398
|
+
self._ct_dir(tmp_path),
|
|
399
|
+
tmp_path / "rs.dcm",
|
|
400
|
+
self._structures(),
|
|
401
|
+
add_roi_kwargs={"approximate_contours": False, "description": "auto"},
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
assert len(calls) == 2
|
|
405
|
+
assert all(c["approximate_contours"] is False for c in calls)
|
|
406
|
+
assert all(c["description"] == "auto" for c in calls)
|
|
407
|
+
|
|
408
|
+
def test_none_keeps_the_rt_utils_defaults(self, tmp_path, monkeypatch) -> None:
|
|
409
|
+
from rt_utils import RTStruct
|
|
410
|
+
|
|
411
|
+
calls: list[dict] = []
|
|
412
|
+
real_add_roi = RTStruct.add_roi
|
|
413
|
+
|
|
414
|
+
def spy(self, **kwargs):
|
|
415
|
+
calls.append(kwargs)
|
|
416
|
+
return real_add_roi(self, **kwargs)
|
|
417
|
+
|
|
418
|
+
monkeypatch.setattr(RTStruct, "add_roi", spy)
|
|
419
|
+
|
|
420
|
+
rtstruct_io.mask2rtstruct(
|
|
421
|
+
self._ct_dir(tmp_path), tmp_path / "rs.dcm", self._structures()
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
assert all(set(c) == {"mask", "color", "name"} for c in calls)
|
|
425
|
+
|
|
426
|
+
@pytest.mark.parametrize("key", ["mask", "color", "name"])
|
|
427
|
+
def test_reserved_keys_are_rejected_before_writing(self, tmp_path, key) -> None:
|
|
428
|
+
out = tmp_path / "rs.dcm"
|
|
429
|
+
|
|
430
|
+
with pytest.raises(ValueError, match=key):
|
|
431
|
+
rtstruct_io.mask2rtstruct(
|
|
432
|
+
tmp_path / "missing_ct",
|
|
433
|
+
out,
|
|
434
|
+
self._structures(),
|
|
435
|
+
add_roi_kwargs={key: None},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
assert not out.exists()
|
|
439
|
+
|
|
440
|
+
def test_an_unknown_option_raises_runtime_error(self, tmp_path) -> None:
|
|
441
|
+
with pytest.raises(RuntimeError, match="PTV"):
|
|
442
|
+
rtstruct_io.mask2rtstruct(
|
|
443
|
+
self._ct_dir(tmp_path),
|
|
444
|
+
tmp_path / "rs.dcm",
|
|
445
|
+
self._structures(),
|
|
446
|
+
add_roi_kwargs={"no_such_option": True},
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
347
450
|
class TestLoadRtStructDuplicateNames:
|
|
348
451
|
"""Pins a 2.0.1 fix: two ROIs sharing a name must not collapse onto one mask.
|
|
349
452
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/bbox3d_handler.py
RENAMED
|
File without changes
|
{tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/bbox_handler.py
RENAMED
|
File without changes
|
{tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/brush_handler.py
RENAMED
|
File without changes
|
{tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/src/tk_rt_viewer/event_controllers/crosshair_handler.py
RENAMED
|
File without changes
|
|
File without changes
|
{tk_rt_viewer-2.3.2 → tk_rt_viewer-2.3.3}/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
|