scikit-surgeryvtk 2.3.0__tar.gz → 2.4.0__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.
- {scikit_surgeryvtk-2.3.0/scikit_surgeryvtk.egg-info → scikit_surgeryvtk-2.4.0}/PKG-INFO +2 -2
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0/scikit_surgeryvtk.egg-info}/PKG-INFO +2 -2
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/scikit_surgeryvtk.egg-info/SOURCES.txt +3 -1
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/scikit_surgeryvtk.egg-info/requires.txt +1 -1
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/setup.py +1 -1
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/_version.py +3 -3
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/utils/projection_utils.py +2 -4
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_base_calibrated_window.py +13 -7
- scikit_surgeryvtk-2.4.0/sksurgeryvtk/widgets/vtk_base_stereo_window.py +123 -0
- scikit_surgeryvtk-2.4.0/sksurgeryvtk/widgets/vtk_interlaced_stereo_window.py +181 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_overlay_window.py +11 -2
- scikit_surgeryvtk-2.4.0/sksurgeryvtk/widgets/vtk_stacked_stereo_window.py +113 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_zbuffer_window.py +3 -1
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/test_requirements.py +1 -1
- scikit_surgeryvtk-2.3.0/tests/widgets/test_interlaced_stereo_window.py → scikit_surgeryvtk-2.4.0/tests/widgets/test_stereo_window.py +54 -29
- scikit_surgeryvtk-2.3.0/sksurgeryvtk/widgets/vtk_interlaced_stereo_window.py +0 -287
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/LICENSE +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/MANIFEST.in +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/README.rst +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/scikit_surgeryvtk.egg-info/dependency_links.txt +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/scikit_surgeryvtk.egg-info/top_level.txt +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/setup.cfg +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/camera/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/camera/vtk_camera_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/outline_render.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/surface_model_loader.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/voxelise.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_base_actor.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_base_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_cylinder_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_grid_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_image_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_point_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_sphere_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_surface_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_surface_model_directory_loader.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/models/vtk_tube_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/text/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/text/text_overlay.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/utils/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/utils/matrix_utils.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/utils/platform_utils.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/utils/polydata_utils.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_lus_simulator.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_rendering_generator.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_reslice_widget.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/camera/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/camera/test_liver_overlay.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/camera/test_vtk_camera_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_surface_model_loader.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_voxelise.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_cylinder_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_image_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_point_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_sphere_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_surface_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_surface_model_directory_loader.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_tube_model.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/models/test_vtk_unstructured_grid.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/utils/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/utils/test_matrix_utills.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/utils/test_polydata_utils.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/utils/test_projection_utils.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/__init__.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_lus_simulator.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_rendering_generator.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_vtk_overlay_window.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_vtk_overlay_window_5_layers.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_vtk_overlay_window_with_outlines.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_vtk_reslice.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/tests/widgets/test_vtkzbuffer_window.py +0 -0
- {scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scikit-surgeryvtk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: scikit-surgeryvtk uses VTK for Image Guided Surgery applications
|
|
5
5
|
Home-page: https://github.com/SciKit-Surgery/scikit-surgeryvtk
|
|
6
6
|
Author: Thomas Dowrick
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/x-rst
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: numpy>=1.11
|
|
23
23
|
Requires-Dist: vtk>=9.5.0
|
|
24
|
-
Requires-Dist: PySide6
|
|
24
|
+
Requires-Dist: PySide6<6.10.0,>=6.5.1.1
|
|
25
25
|
Requires-Dist: opencv-contrib-python-headless>=4.2.0.32
|
|
26
26
|
Requires-Dist: scikit-surgerycore>=0.1.7
|
|
27
27
|
Requires-Dist: scikit-surgeryimage>=0.10.1
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: scikit-surgeryvtk
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: scikit-surgeryvtk uses VTK for Image Guided Surgery applications
|
|
5
5
|
Home-page: https://github.com/SciKit-Surgery/scikit-surgeryvtk
|
|
6
6
|
Author: Thomas Dowrick
|
|
@@ -21,7 +21,7 @@ Description-Content-Type: text/x-rst
|
|
|
21
21
|
License-File: LICENSE
|
|
22
22
|
Requires-Dist: numpy>=1.11
|
|
23
23
|
Requires-Dist: vtk>=9.5.0
|
|
24
|
-
Requires-Dist: PySide6
|
|
24
|
+
Requires-Dist: PySide6<6.10.0,>=6.5.1.1
|
|
25
25
|
Requires-Dist: opencv-contrib-python-headless>=4.2.0.32
|
|
26
26
|
Requires-Dist: scikit-surgerycore>=0.1.7
|
|
27
27
|
Requires-Dist: scikit-surgeryimage>=0.10.1
|
|
@@ -36,11 +36,13 @@ sksurgeryvtk/utils/polydata_utils.py
|
|
|
36
36
|
sksurgeryvtk/utils/projection_utils.py
|
|
37
37
|
sksurgeryvtk/widgets/__init__.py
|
|
38
38
|
sksurgeryvtk/widgets/vtk_base_calibrated_window.py
|
|
39
|
+
sksurgeryvtk/widgets/vtk_base_stereo_window.py
|
|
39
40
|
sksurgeryvtk/widgets/vtk_interlaced_stereo_window.py
|
|
40
41
|
sksurgeryvtk/widgets/vtk_lus_simulator.py
|
|
41
42
|
sksurgeryvtk/widgets/vtk_overlay_window.py
|
|
42
43
|
sksurgeryvtk/widgets/vtk_rendering_generator.py
|
|
43
44
|
sksurgeryvtk/widgets/vtk_reslice_widget.py
|
|
45
|
+
sksurgeryvtk/widgets/vtk_stacked_stereo_window.py
|
|
44
46
|
sksurgeryvtk/widgets/vtk_zbuffer_window.py
|
|
45
47
|
tests/test_requirements.py
|
|
46
48
|
tests/camera/__init__.py
|
|
@@ -62,9 +64,9 @@ tests/utils/test_matrix_utills.py
|
|
|
62
64
|
tests/utils/test_polydata_utils.py
|
|
63
65
|
tests/utils/test_projection_utils.py
|
|
64
66
|
tests/widgets/__init__.py
|
|
65
|
-
tests/widgets/test_interlaced_stereo_window.py
|
|
66
67
|
tests/widgets/test_lus_simulator.py
|
|
67
68
|
tests/widgets/test_rendering_generator.py
|
|
69
|
+
tests/widgets/test_stereo_window.py
|
|
68
70
|
tests/widgets/test_vtk_overlay_window.py
|
|
69
71
|
tests/widgets/test_vtk_overlay_window_5_layers.py
|
|
70
72
|
tests/widgets/test_vtk_overlay_window_with_outlines.py
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "
|
|
11
|
+
"date": "2026-04-20T06:13:00+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "2.
|
|
14
|
+
"full-revisionid": "0422a88b01c027061aef20a450a63f3ba6100e3f",
|
|
15
|
+
"version": "2.4.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -36,12 +36,11 @@ def _validate_input_for_projection(points,
|
|
|
36
36
|
|
|
37
37
|
if camera_to_world is None:
|
|
38
38
|
raise ValueError('camera_to_world is NULL')
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
if not isinstance(camera_to_world, np.ndarray):
|
|
40
|
+
raise TypeError('camera_to_world is not an np.ndarray')
|
|
41
41
|
|
|
42
42
|
if camera_matrix is None:
|
|
43
43
|
raise ValueError('camera_matrix is NULL')
|
|
44
|
-
|
|
45
44
|
vm.validate_camera_matrix(camera_matrix)
|
|
46
45
|
|
|
47
46
|
if distortion is not None:
|
|
@@ -63,7 +62,6 @@ def project_points(points,
|
|
|
63
62
|
:raises: ValueError, TypeError:
|
|
64
63
|
:return: nx2 ndarray representing 2D points, typically in pixels
|
|
65
64
|
"""
|
|
66
|
-
|
|
67
65
|
_validate_input_for_projection(points,
|
|
68
66
|
camera_to_world,
|
|
69
67
|
camera_matrix,
|
|
@@ -32,6 +32,9 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
32
32
|
:param clipping_range: Near/Far clipping range.
|
|
33
33
|
:param opencv_style: If True, adopts OpenCV camera convention, otherwise OpenGL.
|
|
34
34
|
:param reset_camera: If True, resets camera when a new model is added.
|
|
35
|
+
:param aspect_ratio: to adjust for slight differences in pixel size, expressed as x/y.
|
|
36
|
+
:param xscale: horizontal scale factor, for horizontal stacking
|
|
37
|
+
:param yscale: vertical scale factor, for vertical stacking
|
|
35
38
|
"""
|
|
36
39
|
def __init__(
|
|
37
40
|
self,
|
|
@@ -39,7 +42,10 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
39
42
|
camera_matrix=None,
|
|
40
43
|
clipping_range=(1, 1000),
|
|
41
44
|
opencv_style=True,
|
|
42
|
-
reset_camera=True
|
|
45
|
+
reset_camera=True,
|
|
46
|
+
aspect_ratio=1,
|
|
47
|
+
xscale=1,
|
|
48
|
+
yscale=1
|
|
43
49
|
):
|
|
44
50
|
"""
|
|
45
51
|
Constructs a new VTKBaseCalibratedWindow.
|
|
@@ -58,7 +64,9 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
58
64
|
self.reset_camera = reset_camera
|
|
59
65
|
|
|
60
66
|
# Member variables.
|
|
61
|
-
self.aspect_ratio =
|
|
67
|
+
self.aspect_ratio = aspect_ratio
|
|
68
|
+
self.xscale = xscale
|
|
69
|
+
self.yscale = yscale
|
|
62
70
|
self.camera_to_world = np.eye(4)
|
|
63
71
|
self.screen = None
|
|
64
72
|
|
|
@@ -212,7 +220,6 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
212
220
|
# Issue #236: Take size from vtkRenderWindow, not Qt widget.
|
|
213
221
|
# Issue #236: On Mac Retina displays, size given by Qt is halved.
|
|
214
222
|
window_size = self.GetRenderWindow().GetSize()
|
|
215
|
-
|
|
216
223
|
if window_size[0] == 0:
|
|
217
224
|
LOGGER.warning("VTK Render Window appears to have zero width, so abandoning _update_projection_matrix.")
|
|
218
225
|
return opengl_mat, vtk_mat
|
|
@@ -227,8 +234,8 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
227
234
|
opengl_mat, vtk_mat = cm.set_camera_intrinsics(
|
|
228
235
|
renderer,
|
|
229
236
|
camera,
|
|
230
|
-
input_image.shape[1],
|
|
231
|
-
input_image.shape[0],
|
|
237
|
+
input_image.shape[1] * self.xscale,
|
|
238
|
+
input_image.shape[0] * self.yscale,
|
|
232
239
|
self.camera_matrix[0][0],
|
|
233
240
|
self.camera_matrix[1][1],
|
|
234
241
|
self.camera_matrix[0][2],
|
|
@@ -242,7 +249,7 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
242
249
|
window_size[1],
|
|
243
250
|
input_image.shape[1],
|
|
244
251
|
input_image.shape[0],
|
|
245
|
-
self.aspect_ratio,
|
|
252
|
+
aspect_ratio=self.aspect_ratio,
|
|
246
253
|
)
|
|
247
254
|
|
|
248
255
|
x_min, y_min, x_max, y_max = cm.compute_viewport(
|
|
@@ -332,7 +339,6 @@ class VTKBaseCalibratedWindow(QVTKRenderWindowInteractor):
|
|
|
332
339
|
Sets the camera position and orientation, from a numpy 4x4 array.
|
|
333
340
|
:param camera_to_world: camera_to_world transform.
|
|
334
341
|
"""
|
|
335
|
-
vm.validate_rigid_matrix(camera_to_world)
|
|
336
342
|
self.camera_to_world = camera_to_world
|
|
337
343
|
vtk_mat = mu.create_vtk_matrix_from_numpy(camera_to_world)
|
|
338
344
|
self._update_pose_matrices(vtk_mat)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Base class for vtk_interlaced_stereo_window.py and vtk_stacked_stereo_window.py
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import abc
|
|
8
|
+
from PySide6 import QtWidgets
|
|
9
|
+
from PySide6.QtWidgets import QSizePolicy
|
|
10
|
+
import sksurgeryvtk.widgets.vtk_overlay_window as ow
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VTKBaseStereoWindow(QtWidgets.QWidget):
|
|
14
|
+
"""
|
|
15
|
+
Class to contain a pair of VTKOverlayWindows, left, right,
|
|
16
|
+
and common methods.
|
|
17
|
+
|
|
18
|
+
:param init_widget: If True we will call self.Initialize and self.Start
|
|
19
|
+
as part of the init function. Set to false if you're on Linux.
|
|
20
|
+
"""
|
|
21
|
+
# pylint: disable=too-many-positional-arguments
|
|
22
|
+
def __init__(self,
|
|
23
|
+
offscreen=False,
|
|
24
|
+
left_camera_matrix=None,
|
|
25
|
+
right_camera_matrix=None,
|
|
26
|
+
clipping_range=(1, 10000),
|
|
27
|
+
init_widget=True,
|
|
28
|
+
left_is_top=True,
|
|
29
|
+
aspect_ratio=1,
|
|
30
|
+
xscale=1,
|
|
31
|
+
yscale=1
|
|
32
|
+
):
|
|
33
|
+
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.left_is_top = left_is_top
|
|
36
|
+
self.xscale = xscale
|
|
37
|
+
self.yscale = yscale
|
|
38
|
+
self.aspect_ratio = aspect_ratio
|
|
39
|
+
|
|
40
|
+
self.left_widget = ow.VTKOverlayWindow(
|
|
41
|
+
offscreen=offscreen,
|
|
42
|
+
camera_matrix=left_camera_matrix,
|
|
43
|
+
clipping_range=clipping_range,
|
|
44
|
+
init_widget=init_widget,
|
|
45
|
+
aspect_ratio=aspect_ratio,
|
|
46
|
+
xscale=xscale,
|
|
47
|
+
yscale=yscale
|
|
48
|
+
)
|
|
49
|
+
self.left_widget.setContentsMargins(0, 0, 0, 0)
|
|
50
|
+
|
|
51
|
+
self.right_widget = ow.VTKOverlayWindow(
|
|
52
|
+
offscreen=offscreen,
|
|
53
|
+
camera_matrix=right_camera_matrix,
|
|
54
|
+
clipping_range=clipping_range,
|
|
55
|
+
init_widget=init_widget,
|
|
56
|
+
aspect_ratio=aspect_ratio,
|
|
57
|
+
xscale=xscale,
|
|
58
|
+
yscale=yscale
|
|
59
|
+
)
|
|
60
|
+
self.right_widget.setContentsMargins(0, 0, 0, 0)
|
|
61
|
+
|
|
62
|
+
self.size_policy = \
|
|
63
|
+
QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
|
64
|
+
self.setSizePolicy(self.size_policy)
|
|
65
|
+
self.setContentsMargins(0, 0, 0, 0)
|
|
66
|
+
|
|
67
|
+
def set_camera_matrices(self, left_camera_matrix, right_camera_matrix):
|
|
68
|
+
"""
|
|
69
|
+
Sets both the left and right camera matrices.
|
|
70
|
+
|
|
71
|
+
:param left_camera_matrix: numpy 3x3 ndarray containing fx, fy, cx, cy
|
|
72
|
+
:param right_camera_matrix: numpy 3x3 ndarray containing fx, fy, cx, cy
|
|
73
|
+
"""
|
|
74
|
+
self.left_widget.set_camera_matrix(left_camera_matrix)
|
|
75
|
+
self.right_widget.set_camera_matrix(right_camera_matrix)
|
|
76
|
+
|
|
77
|
+
def set_camera_poses(self, left_camera_to_world, right_camera_to_world):
|
|
78
|
+
"""
|
|
79
|
+
Sets the pose of both the left and right camera.
|
|
80
|
+
|
|
81
|
+
:param left_camera_to_world: 4x4 numpy ndarray, rigid transform
|
|
82
|
+
:param right_camera_to_world: 4x4 numpy ndarray, rigid transform
|
|
83
|
+
"""
|
|
84
|
+
self.left_widget.set_camera_pose(left_camera_to_world)
|
|
85
|
+
self.right_widget.set_camera_pose(right_camera_to_world)
|
|
86
|
+
|
|
87
|
+
def add_vtk_models(self, models):
|
|
88
|
+
"""
|
|
89
|
+
Add models to both left and right widgets.
|
|
90
|
+
Here a model is anything with an attribute called actor that
|
|
91
|
+
is a vtkActor.
|
|
92
|
+
|
|
93
|
+
:param models: vtk_base_model
|
|
94
|
+
"""
|
|
95
|
+
self.left_widget.add_vtk_models(models)
|
|
96
|
+
self.right_widget.add_vtk_models(models)
|
|
97
|
+
|
|
98
|
+
def add_vtk_actor(self, actor):
|
|
99
|
+
"""
|
|
100
|
+
Adds a vtkActor to both left and right widgets.
|
|
101
|
+
|
|
102
|
+
:param actor: vtkActor
|
|
103
|
+
"""
|
|
104
|
+
self.left_widget.add_vtk_actor(actor)
|
|
105
|
+
self.right_widget.add_vtk_actor(actor)
|
|
106
|
+
|
|
107
|
+
@abc.abstractmethod
|
|
108
|
+
def set_video_images(self, left_image, right_image):
|
|
109
|
+
"""
|
|
110
|
+
Derived classes must implement this method.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
@abc.abstractmethod
|
|
114
|
+
def render(self):
|
|
115
|
+
"""
|
|
116
|
+
Derived classes must implement this method.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
@abc.abstractmethod
|
|
120
|
+
def save_scene_to_file(self, file_name):
|
|
121
|
+
"""
|
|
122
|
+
Derived classes must implement this method.
|
|
123
|
+
"""
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Module to provide an interlaced stereo window, designed for
|
|
5
|
+
driving things like the Storz 3D laparoscope monitor.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# pylint: disable=c-extension-no-member, no-name-in-module, too-many-instance-attributes
|
|
9
|
+
import cv2
|
|
10
|
+
import numpy as np
|
|
11
|
+
from PySide6 import QtWidgets
|
|
12
|
+
import sksurgeryimage.processing.interlace as i
|
|
13
|
+
import sksurgeryvtk.widgets.vtk_base_stereo_window as bw
|
|
14
|
+
import sksurgeryvtk.widgets.vtk_overlay_window as ow
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VTKInterlacedStereoWindow(bw.VTKBaseStereoWindow):
|
|
18
|
+
"""
|
|
19
|
+
Class to contain a pair of VTKOverlayWindows, left, right, that we render,
|
|
20
|
+
grab, interlace, and then display as a background image
|
|
21
|
+
on a separate VTKOverlayWindow.
|
|
22
|
+
|
|
23
|
+
:param init_widget: If True we will call self.Initialize and self.Start
|
|
24
|
+
as part of the init function. Set to false if you're on Linux.
|
|
25
|
+
"""
|
|
26
|
+
# pylint: disable=too-many-positional-arguments
|
|
27
|
+
def __init__(self,
|
|
28
|
+
offscreen=False,
|
|
29
|
+
left_camera_matrix=None,
|
|
30
|
+
right_camera_matrix=None,
|
|
31
|
+
clipping_range=(1, 10000),
|
|
32
|
+
init_widget=True,
|
|
33
|
+
left_is_top=True,
|
|
34
|
+
aspect_ratio=1
|
|
35
|
+
):
|
|
36
|
+
|
|
37
|
+
# Superclass creates left/right viewer.
|
|
38
|
+
super().__init__(offscreen=offscreen,
|
|
39
|
+
left_camera_matrix=left_camera_matrix,
|
|
40
|
+
right_camera_matrix=right_camera_matrix,
|
|
41
|
+
clipping_range=clipping_range,
|
|
42
|
+
init_widget=init_widget,
|
|
43
|
+
left_is_top=left_is_top,
|
|
44
|
+
aspect_ratio=aspect_ratio,
|
|
45
|
+
xscale=1,
|
|
46
|
+
yscale=1
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# This class adds an interlaced widget and a layout.
|
|
50
|
+
self.interlaced_widget = ow.VTKOverlayWindow(
|
|
51
|
+
offscreen=offscreen,
|
|
52
|
+
init_widget=init_widget,
|
|
53
|
+
aspect_ratio=aspect_ratio,
|
|
54
|
+
xscale=1,
|
|
55
|
+
yscale=1
|
|
56
|
+
)
|
|
57
|
+
self.interlaced_widget.setContentsMargins(0, 0, 0, 0)
|
|
58
|
+
|
|
59
|
+
self.stacked = QtWidgets.QStackedWidget()
|
|
60
|
+
self.stacked.addWidget(self.left_widget)
|
|
61
|
+
self.stacked.addWidget(self.right_widget)
|
|
62
|
+
self.stacked.addWidget(self.interlaced_widget)
|
|
63
|
+
self.stacked.setContentsMargins(0, 0, 0, 0)
|
|
64
|
+
self.stacked.setSizePolicy(self.size_policy)
|
|
65
|
+
|
|
66
|
+
self.layout = QtWidgets.QVBoxLayout(self)
|
|
67
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
68
|
+
self.layout.setSpacing(0)
|
|
69
|
+
self.layout.addWidget(self.stacked)
|
|
70
|
+
|
|
71
|
+
# Default the view to show the interlaced window.
|
|
72
|
+
self.stacked.setCurrentIndex(2)
|
|
73
|
+
|
|
74
|
+
# pylint: disable=invalid-name
|
|
75
|
+
def paintEvent(self, ev):
|
|
76
|
+
"""
|
|
77
|
+
Ensure that the interlaced image is recomputed.
|
|
78
|
+
"""
|
|
79
|
+
self.render()
|
|
80
|
+
super().paintEvent(ev)
|
|
81
|
+
|
|
82
|
+
# pylint: disable=invalid-name
|
|
83
|
+
def resizeEvent(self, ev):
|
|
84
|
+
"""
|
|
85
|
+
Ensure that the interlaced image is recomputed.
|
|
86
|
+
"""
|
|
87
|
+
self.set_current_viewer_index(0)
|
|
88
|
+
self.left_widget.resizeEvent(ev)
|
|
89
|
+
self.left_widget.Render()
|
|
90
|
+
self.left_widget.update()
|
|
91
|
+
self.set_current_viewer_index(1)
|
|
92
|
+
self.right_widget.resizeEvent(ev)
|
|
93
|
+
self.right_widget.Render()
|
|
94
|
+
self.right_widget.update()
|
|
95
|
+
self.set_current_viewer_index(2)
|
|
96
|
+
self.interlaced_widget.resizeEvent(ev)
|
|
97
|
+
self.interlaced_widget.Render()
|
|
98
|
+
self.interlaced_widget.update()
|
|
99
|
+
super().resizeEvent(ev)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def closeEvent(self, QCloseEvent):
|
|
103
|
+
self.left_widget.Finalize()
|
|
104
|
+
self.right_widget.Finalize()
|
|
105
|
+
self.interlaced_widget.Finalize()
|
|
106
|
+
super().closeEvent(QCloseEvent)
|
|
107
|
+
|
|
108
|
+
def set_current_viewer_index(self, viewer_index):
|
|
109
|
+
"""
|
|
110
|
+
Sets the current viewer selection.
|
|
111
|
+
Defaults to 2
|
|
112
|
+
|
|
113
|
+
0 = left
|
|
114
|
+
1 = right
|
|
115
|
+
2 = interlaced
|
|
116
|
+
|
|
117
|
+
:param viewer_index: index of viewer, as above.
|
|
118
|
+
"""
|
|
119
|
+
if viewer_index < 0:
|
|
120
|
+
raise ValueError('viewer_index must be >= 0')
|
|
121
|
+
if viewer_index > 2:
|
|
122
|
+
raise ValueError('viewer_index must be <= 2')
|
|
123
|
+
self.stacked.setCurrentIndex(viewer_index)
|
|
124
|
+
|
|
125
|
+
def set_video_images(self, left_image, right_image):
|
|
126
|
+
"""
|
|
127
|
+
Sets both left and right video images. Images
|
|
128
|
+
must be the same shape, and have an even number of rows.
|
|
129
|
+
|
|
130
|
+
:param left_image: left numpy image
|
|
131
|
+
:param right_image: right numpy image
|
|
132
|
+
:raises: ValueError, TypeError
|
|
133
|
+
"""
|
|
134
|
+
if not isinstance(left_image, np.ndarray):
|
|
135
|
+
raise TypeError('left image is not an np.ndarray')
|
|
136
|
+
if not isinstance(right_image, np.ndarray):
|
|
137
|
+
raise TypeError('right image is not an np.ndarray')
|
|
138
|
+
if left_image.shape != right_image.shape:
|
|
139
|
+
raise ValueError('left and right images differ in shape')
|
|
140
|
+
if left_image.shape[0] % 2 != 0:
|
|
141
|
+
raise ValueError('left image does not have an even number of rows')
|
|
142
|
+
if right_image.shape[0] % 2 != 0:
|
|
143
|
+
raise ValueError('right image does not have an even number of rows')
|
|
144
|
+
|
|
145
|
+
self.left_widget.set_video_image(left_image)
|
|
146
|
+
self.right_widget.set_video_image(right_image)
|
|
147
|
+
|
|
148
|
+
def render(self):
|
|
149
|
+
"""
|
|
150
|
+
Calls Render on all 3 contained vtk_overlay_windows.
|
|
151
|
+
"""
|
|
152
|
+
self.left_widget.Render()
|
|
153
|
+
self.left_widget.update()
|
|
154
|
+
self.right_widget.Render()
|
|
155
|
+
self.right_widget.update()
|
|
156
|
+
|
|
157
|
+
left = self.left_widget.convert_scene_to_numpy_array()
|
|
158
|
+
right = self.right_widget.convert_scene_to_numpy_array()
|
|
159
|
+
|
|
160
|
+
left_rescaled = cv2.resize(left, (0, 0), fx=1.0, fy=0.5)
|
|
161
|
+
right_rescaled = cv2.resize(right, (0, 0), fx=1.0, fy=0.5)
|
|
162
|
+
|
|
163
|
+
if self.left_is_top:
|
|
164
|
+
interlaced = i.interlace_to_new(left_rescaled,
|
|
165
|
+
right_rescaled)
|
|
166
|
+
else:
|
|
167
|
+
interlaced = i.interlace_to_new(right_rescaled,
|
|
168
|
+
left_rescaled)
|
|
169
|
+
|
|
170
|
+
self.interlaced_widget.set_video_image(interlaced)
|
|
171
|
+
self.interlaced_widget.Render()
|
|
172
|
+
self.interlaced_widget.update()
|
|
173
|
+
|
|
174
|
+
def save_scene_to_file(self, file_name):
|
|
175
|
+
"""
|
|
176
|
+
Writes the interlaced widget contents to file.
|
|
177
|
+
|
|
178
|
+
:param file_name: file name compatible with cv2.imwrite()
|
|
179
|
+
"""
|
|
180
|
+
self.render()
|
|
181
|
+
self.interlaced_widget.save_scene_to_file(file_name)
|
{scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_overlay_window.py
RENAMED
|
@@ -73,6 +73,9 @@ class VTKOverlayWindow(bcw.VTKBaseCalibratedWindow):
|
|
|
73
73
|
:param layer_2_video_mask: Mask image for layer 2.
|
|
74
74
|
:param layer_1_interactive: True if you want the VTK interactor to pickup events in this layer.
|
|
75
75
|
:param layer_3_interactive: True if you want the VTK interactor to pickup events in this layer.
|
|
76
|
+
:param aspect_ratio: to adjust for slight differences in pixel size, expressed as x/y.
|
|
77
|
+
:param xscale: horizontal scale factor, for horizontal stacking
|
|
78
|
+
:param yscale: vertical scale factor, for vertical stacking
|
|
76
79
|
layer_1_interactive and layer_3_interactive are mutually exclusive.
|
|
77
80
|
"""
|
|
78
81
|
# pylint: disable=too-many-positional-arguments
|
|
@@ -90,7 +93,10 @@ class VTKOverlayWindow(bcw.VTKBaseCalibratedWindow):
|
|
|
90
93
|
layer_2_video_mask=None, # For masking in Layer 3
|
|
91
94
|
use_depth_peeling=True,
|
|
92
95
|
layer_1_interactive=True, # For backwards compatibility, prior to 3rd Feb 2024.
|
|
93
|
-
layer_3_interactive=False # For backwards compatibility, prior to 3rd Feb 2024.
|
|
96
|
+
layer_3_interactive=False, # For backwards compatibility, prior to 3rd Feb 2024.
|
|
97
|
+
aspect_ratio=1,
|
|
98
|
+
xscale=1,
|
|
99
|
+
yscale=1
|
|
94
100
|
):
|
|
95
101
|
"""
|
|
96
102
|
Constructs a new VTKOverlayWindow.
|
|
@@ -99,7 +105,10 @@ class VTKOverlayWindow(bcw.VTKBaseCalibratedWindow):
|
|
|
99
105
|
camera_matrix,
|
|
100
106
|
clipping_range,
|
|
101
107
|
opencv_style,
|
|
102
|
-
reset_camera
|
|
108
|
+
reset_camera,
|
|
109
|
+
aspect_ratio=aspect_ratio,
|
|
110
|
+
xscale=xscale,
|
|
111
|
+
yscale=yscale
|
|
103
112
|
)
|
|
104
113
|
LOGGER.info("Creating VTKOverlayWindow")
|
|
105
114
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Module to provide an stacked stereo window, designed for
|
|
5
|
+
driving things like a 3D monitor that accepts left=top, right=bottom
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# pylint: disable=c-extension-no-member, no-name-in-module, too-many-instance-attributes
|
|
9
|
+
import cv2
|
|
10
|
+
import numpy as np
|
|
11
|
+
from PySide6 import QtWidgets
|
|
12
|
+
import sksurgeryvtk.widgets.vtk_base_stereo_window as bw
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class VTKStackedStereoWindow(bw.VTKBaseStereoWindow):
|
|
16
|
+
"""
|
|
17
|
+
Class to contain a pair of VTKOverlayWindows, left, right, that are
|
|
18
|
+
stacked on top of each other. Left=top. Right=bottom.
|
|
19
|
+
|
|
20
|
+
:param init_widget: If True we will call self.Initialize and self.Start
|
|
21
|
+
as part of the init function. Set to false if you're on Linux.
|
|
22
|
+
: param left_is_top: If True left is top. Else left is bottom.
|
|
23
|
+
"""
|
|
24
|
+
# pylint: disable=too-many-positional-arguments
|
|
25
|
+
def __init__(self,
|
|
26
|
+
offscreen=False,
|
|
27
|
+
left_camera_matrix=None,
|
|
28
|
+
right_camera_matrix=None,
|
|
29
|
+
clipping_range=(1, 10000),
|
|
30
|
+
init_widget=True,
|
|
31
|
+
left_is_top=True,
|
|
32
|
+
aspect_ratio=1
|
|
33
|
+
):
|
|
34
|
+
|
|
35
|
+
# Superclass creates left/right viewer.
|
|
36
|
+
super().__init__(offscreen=offscreen,
|
|
37
|
+
left_camera_matrix=left_camera_matrix,
|
|
38
|
+
right_camera_matrix=right_camera_matrix,
|
|
39
|
+
clipping_range=clipping_range,
|
|
40
|
+
init_widget=init_widget,
|
|
41
|
+
left_is_top=left_is_top,
|
|
42
|
+
aspect_ratio=aspect_ratio,
|
|
43
|
+
xscale=1,
|
|
44
|
+
yscale=2
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
self.layout = QtWidgets.QVBoxLayout(self)
|
|
48
|
+
self.layout.setContentsMargins(0, 0, 0, 0)
|
|
49
|
+
self.layout.setSpacing(0)
|
|
50
|
+
if self.left_is_top:
|
|
51
|
+
self.layout.addWidget(self.left_widget)
|
|
52
|
+
self.layout.addWidget(self.right_widget)
|
|
53
|
+
else:
|
|
54
|
+
self.layout.addWidget(self.right_widget)
|
|
55
|
+
self.layout.addWidget(self.left_widget)
|
|
56
|
+
|
|
57
|
+
def resizeEvent(self, ev):
|
|
58
|
+
"""
|
|
59
|
+
Ensure that the interlaced image is recomputed.
|
|
60
|
+
"""
|
|
61
|
+
self.left_widget.resizeEvent(ev)
|
|
62
|
+
self.left_widget.Render()
|
|
63
|
+
self.left_widget.update()
|
|
64
|
+
self.right_widget.resizeEvent(ev)
|
|
65
|
+
self.right_widget.Render()
|
|
66
|
+
self.right_widget.update()
|
|
67
|
+
super().resizeEvent(ev)
|
|
68
|
+
|
|
69
|
+
def set_video_images(self, left_image, right_image):
|
|
70
|
+
"""
|
|
71
|
+
Sets both left and right video images. Images
|
|
72
|
+
must be the same shape, and have an even number of rows.
|
|
73
|
+
|
|
74
|
+
:param left_image: left numpy image
|
|
75
|
+
:param right_image: right numpy image
|
|
76
|
+
:raises: ValueError, TypeError
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(left_image, np.ndarray):
|
|
79
|
+
raise TypeError('left image is not an np.ndarray')
|
|
80
|
+
if not isinstance(right_image, np.ndarray):
|
|
81
|
+
raise TypeError('right image is not an np.ndarray')
|
|
82
|
+
if left_image.shape != right_image.shape:
|
|
83
|
+
raise ValueError('left and right images differ in shape')
|
|
84
|
+
|
|
85
|
+
left_rescaled = cv2.resize(left_image, (0, 0), fx=1.0, fy=0.5)
|
|
86
|
+
right_rescaled = cv2.resize(right_image, (0, 0), fx=1.0, fy=0.5)
|
|
87
|
+
|
|
88
|
+
self.left_widget.set_video_image(left_rescaled)
|
|
89
|
+
self.right_widget.set_video_image(right_rescaled)
|
|
90
|
+
|
|
91
|
+
def render(self):
|
|
92
|
+
"""
|
|
93
|
+
Simply triggers rendering on both widgets.
|
|
94
|
+
"""
|
|
95
|
+
self.left_widget.Render()
|
|
96
|
+
self.left_widget.update()
|
|
97
|
+
self.right_widget.Render()
|
|
98
|
+
self.right_widget.update()
|
|
99
|
+
|
|
100
|
+
def save_scene_to_file(self, file_name):
|
|
101
|
+
"""
|
|
102
|
+
Writes the interlaced widget contents to file.
|
|
103
|
+
|
|
104
|
+
:param file_name: file name compatible with cv2.imwrite()
|
|
105
|
+
"""
|
|
106
|
+
self.render()
|
|
107
|
+
left_image = self.left_widget.convert_scene_to_numpy_array()
|
|
108
|
+
right_image = self.right_widget.convert_scene_to_numpy_array()
|
|
109
|
+
if self.left_is_top:
|
|
110
|
+
stacked = np.vstack((left_image, right_image))
|
|
111
|
+
else:
|
|
112
|
+
stacked = np.vstack((right_image, left_image))
|
|
113
|
+
cv2.imwrite(file_name, stacked)
|
{scikit_surgeryvtk-2.3.0 → scikit_surgeryvtk-2.4.0}/sksurgeryvtk/widgets/vtk_zbuffer_window.py
RENAMED
|
@@ -70,6 +70,7 @@ class VTKZBufferWindow(bcw.VTKBaseCalibratedWindow):
|
|
|
70
70
|
reset_camera=True,
|
|
71
71
|
init_widget=True,
|
|
72
72
|
use_depth_peeling=True,
|
|
73
|
+
aspect_ratio=1
|
|
73
74
|
):
|
|
74
75
|
"""
|
|
75
76
|
Constructs a new VTKZBufferWindow.
|
|
@@ -78,7 +79,8 @@ class VTKZBufferWindow(bcw.VTKBaseCalibratedWindow):
|
|
|
78
79
|
camera_matrix,
|
|
79
80
|
clipping_range,
|
|
80
81
|
opencv_style,
|
|
81
|
-
reset_camera
|
|
82
|
+
reset_camera,
|
|
83
|
+
aspect_ratio=aspect_ratio
|
|
82
84
|
)
|
|
83
85
|
LOGGER.info("Creating VTKZBufferWindow")
|
|
84
86
|
|
|
@@ -31,7 +31,7 @@ def test_requirements_vs_setup():
|
|
|
31
31
|
setup_reqs = []
|
|
32
32
|
|
|
33
33
|
for line in searchlines[install_line + 1: end_line]:
|
|
34
|
-
req = line.replace(',
|
|
34
|
+
req = line.replace("',", "'").replace("'", "")
|
|
35
35
|
req = req.replace(' ', '').replace('\n', '')
|
|
36
36
|
setup_reqs.append(req)
|
|
37
37
|
|