medscope 0.1.0__py3-none-any.whl
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.
- medscope/__init__.py +7 -0
- medscope/main.py +800 -0
- medscope-0.1.0.dist-info/METADATA +108 -0
- medscope-0.1.0.dist-info/RECORD +6 -0
- medscope-0.1.0.dist-info/WHEEL +4 -0
- medscope-0.1.0.dist-info/licenses/LICENSE +21 -0
medscope/__init__.py
ADDED
medscope/main.py
ADDED
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from typing import Optional, List, Tuple, Dict, Any, Union, Callable
|
|
3
|
+
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QFrame
|
|
4
|
+
from PyQt5.QtCore import Qt, QTimer
|
|
5
|
+
from PyQt5.QtGui import QImage, QPixmap, QPainter, QPaintEvent
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
# VTK imports
|
|
9
|
+
import vtk
|
|
10
|
+
from vtkmodules.qt.QVTKRenderWindowInteractor import QVTKRenderWindowInteractor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class VTKModelManager:
|
|
14
|
+
"""Manages 3D models in VTK scene."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, renderer: vtk.vtkRenderer):
|
|
17
|
+
"""
|
|
18
|
+
Initialize the model manager.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
renderer: VTK renderer to add models to
|
|
22
|
+
"""
|
|
23
|
+
self.renderer: vtk.vtkRenderer = renderer
|
|
24
|
+
self.models: Dict[str, vtk.vtkActor] = {} # name -> actor
|
|
25
|
+
self.model_sources: Dict[str, Any] = {} # name -> source (for potential regeneration)
|
|
26
|
+
|
|
27
|
+
def add_model_from_file(self, name: str, file_path: str,
|
|
28
|
+
color: Optional[Tuple[float, float, float]] = None,
|
|
29
|
+
scale: float = 1.0,
|
|
30
|
+
position: Tuple[float, float, float] = (0, 0, 0)) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
Add a model from a file (supports .vtk, .stl, .obj, etc.)
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: Unique identifier for the model
|
|
36
|
+
file_path: Path to the model file
|
|
37
|
+
color: RGB tuple (0-1) or None for default
|
|
38
|
+
scale: Scale factor
|
|
39
|
+
position: (x, y, z) position
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool: True if successful, False otherwise
|
|
43
|
+
"""
|
|
44
|
+
if name in self.models:
|
|
45
|
+
print(f"Model '{name}' already exists. Remove it first or use a different name.")
|
|
46
|
+
return False
|
|
47
|
+
|
|
48
|
+
# Determine reader based on file extension
|
|
49
|
+
if file_path.lower().endswith('.stl'):
|
|
50
|
+
reader = vtk.vtkSTLReader()
|
|
51
|
+
elif file_path.lower().endswith('.obj'):
|
|
52
|
+
reader = vtk.vtkOBJReader()
|
|
53
|
+
elif file_path.lower().endswith('.vtk'):
|
|
54
|
+
reader = vtk.vtkDataSetReader()
|
|
55
|
+
elif file_path.lower().endswith('.ply'):
|
|
56
|
+
reader = vtk.vtkPLYReader()
|
|
57
|
+
else:
|
|
58
|
+
print(f"Unsupported file format: {file_path}")
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
if not os.path.isfile(file_path):
|
|
62
|
+
print(f"Model file {file_path} not found.")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
reader.SetFileName(file_path)
|
|
66
|
+
reader.Update()
|
|
67
|
+
|
|
68
|
+
return self._add_model_from_algorithm(name, reader, color, scale, position)
|
|
69
|
+
|
|
70
|
+
def _add_model_from_algorithm(self, name: str, algorithm: Any,
|
|
71
|
+
color: Optional[Tuple[float, float, float]],
|
|
72
|
+
scale: float,
|
|
73
|
+
position: Tuple[float, float, float]) -> bool:
|
|
74
|
+
"""Internal method to add model from a VTK algorithm."""
|
|
75
|
+
mapper = vtk.vtkPolyDataMapper()
|
|
76
|
+
mapper.SetInputConnection(algorithm.GetOutputPort())
|
|
77
|
+
return self._add_model_from_mapper(name, mapper, color, scale, position)
|
|
78
|
+
|
|
79
|
+
def _add_model_from_mapper(self, name: str, mapper: vtk.vtkPolyDataMapper,
|
|
80
|
+
color: Optional[Tuple[float, float, float]],
|
|
81
|
+
scale: float,
|
|
82
|
+
position: Tuple[float, float, float]) -> bool:
|
|
83
|
+
"""Internal method to add model from a mapper."""
|
|
84
|
+
actor = vtk.vtkActor()
|
|
85
|
+
actor.SetMapper(mapper)
|
|
86
|
+
|
|
87
|
+
# Apply color if provided
|
|
88
|
+
if color is not None:
|
|
89
|
+
actor.GetProperty().SetColor(color[0], color[1], color[2])
|
|
90
|
+
else:
|
|
91
|
+
# Random color
|
|
92
|
+
actor.GetProperty().SetColor(np.random.rand(), np.random.rand(), np.random.rand())
|
|
93
|
+
|
|
94
|
+
# Apply transformation
|
|
95
|
+
transform = vtk.vtkTransform()
|
|
96
|
+
transform.Scale(scale, scale, scale)
|
|
97
|
+
transform.Translate(position[0], position[1], position[2])
|
|
98
|
+
actor.SetUserTransform(transform)
|
|
99
|
+
|
|
100
|
+
# Add to renderer and store
|
|
101
|
+
self.renderer.AddActor(actor)
|
|
102
|
+
self.models[name] = actor
|
|
103
|
+
|
|
104
|
+
return True
|
|
105
|
+
|
|
106
|
+
def remove_model(self, name: str) -> bool:
|
|
107
|
+
"""
|
|
108
|
+
Remove a model from the scene.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
name: Name of the model to remove
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
bool: True if removed, False if not found
|
|
115
|
+
"""
|
|
116
|
+
if name in self.models:
|
|
117
|
+
self.renderer.RemoveActor(self.models[name])
|
|
118
|
+
del self.models[name]
|
|
119
|
+
if name in self.model_sources:
|
|
120
|
+
del self.model_sources[name]
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
123
|
+
|
|
124
|
+
def set_model_scale(self, name: str, scale: float) -> bool:
|
|
125
|
+
"""
|
|
126
|
+
Set model scale (uniform scaling).
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
name: Model name
|
|
130
|
+
scale: Scale factor
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
bool: True if successful, False if model not found
|
|
134
|
+
"""
|
|
135
|
+
if name not in self.models:
|
|
136
|
+
print(f"Model '{name}' not found.")
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
actor = self.models[name]
|
|
140
|
+
current_transform = actor.GetUserTransform()
|
|
141
|
+
if current_transform:
|
|
142
|
+
# Get current position
|
|
143
|
+
position = current_transform.GetPosition()
|
|
144
|
+
# Create new transform with new scale
|
|
145
|
+
new_transform = vtk.vtkTransform()
|
|
146
|
+
new_transform.Scale(scale, scale, scale)
|
|
147
|
+
new_transform.Translate(position[0], position[1], position[2])
|
|
148
|
+
actor.SetUserTransform(new_transform)
|
|
149
|
+
else:
|
|
150
|
+
transform = vtk.vtkTransform()
|
|
151
|
+
transform.Scale(scale, scale, scale)
|
|
152
|
+
actor.SetUserTransform(transform)
|
|
153
|
+
return True
|
|
154
|
+
|
|
155
|
+
def set_model_pose(self, name: str,
|
|
156
|
+
translation_matrix: Optional[Union[np.ndarray, Tuple[float, float, float]]] = None,
|
|
157
|
+
rotation_matrix: Optional[np.ndarray] = None) -> bool:
|
|
158
|
+
"""
|
|
159
|
+
Set model pose using translation and rotation matrices.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
name: Model name
|
|
163
|
+
translation_matrix: 3-element array for translation or 3x3/4x4 matrix
|
|
164
|
+
rotation_matrix: 3x3 or 4x4 numpy array for rotation
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
bool: True if successful, False if model not found
|
|
168
|
+
"""
|
|
169
|
+
if name not in self.models:
|
|
170
|
+
print(f"Model '{name}' not found.")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
actor = self.models[name]
|
|
174
|
+
transform = vtk.vtkTransform()
|
|
175
|
+
transform.Identity()
|
|
176
|
+
|
|
177
|
+
# Apply rotation
|
|
178
|
+
if rotation_matrix is not None:
|
|
179
|
+
# Convert numpy array to vtk matrix
|
|
180
|
+
if rotation_matrix.shape == (3, 3):
|
|
181
|
+
mat = vtk.vtkMatrix4x4()
|
|
182
|
+
mat.Identity()
|
|
183
|
+
for i in range(3):
|
|
184
|
+
for j in range(3):
|
|
185
|
+
mat.SetElement(i, j, rotation_matrix[i, j])
|
|
186
|
+
transform.Concatenate(mat)
|
|
187
|
+
elif rotation_matrix.shape == (4, 4):
|
|
188
|
+
mat = vtk.vtkMatrix4x4()
|
|
189
|
+
for i in range(4):
|
|
190
|
+
for j in range(4):
|
|
191
|
+
mat.SetElement(i, j, rotation_matrix[i, j])
|
|
192
|
+
transform.Concatenate(mat)
|
|
193
|
+
|
|
194
|
+
# Apply translation
|
|
195
|
+
if translation_matrix is not None:
|
|
196
|
+
if isinstance(translation_matrix, tuple) and len(translation_matrix) == 3:
|
|
197
|
+
transform.Translate(translation_matrix[0], translation_matrix[1], translation_matrix[2])
|
|
198
|
+
elif isinstance(translation_matrix, np.ndarray):
|
|
199
|
+
if translation_matrix.shape == (3,):
|
|
200
|
+
transform.Translate(translation_matrix[0], translation_matrix[1], translation_matrix[2])
|
|
201
|
+
elif translation_matrix.shape == (3, 1):
|
|
202
|
+
transform.Translate(translation_matrix[0, 0], translation_matrix[1, 0], translation_matrix[2, 0])
|
|
203
|
+
|
|
204
|
+
actor.SetUserTransform(transform)
|
|
205
|
+
return True
|
|
206
|
+
|
|
207
|
+
def set_model_position(self, name: str, x: float, y: float, z: float) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
Set model position.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
name: Model name
|
|
213
|
+
x: X coordinate
|
|
214
|
+
y: Y coordinate
|
|
215
|
+
z: Z coordinate
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
bool: True if successful, False if model not found
|
|
219
|
+
"""
|
|
220
|
+
if name not in self.models:
|
|
221
|
+
print(f"Model '{name}' not found.")
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
actor = self.models[name]
|
|
225
|
+
current_transform = actor.GetUserTransform()
|
|
226
|
+
if current_transform:
|
|
227
|
+
# Get current scale
|
|
228
|
+
scale = current_transform.GetScale()
|
|
229
|
+
new_transform = vtk.vtkTransform()
|
|
230
|
+
new_transform.Scale(scale[0], scale[1], scale[2])
|
|
231
|
+
new_transform.Translate(x, y, z)
|
|
232
|
+
actor.SetUserTransform(new_transform)
|
|
233
|
+
else:
|
|
234
|
+
transform = vtk.vtkTransform()
|
|
235
|
+
transform.Translate(x, y, z)
|
|
236
|
+
actor.SetUserTransform(transform)
|
|
237
|
+
return True
|
|
238
|
+
|
|
239
|
+
def get_model_list(self) -> List[str]:
|
|
240
|
+
"""Return list of all model names."""
|
|
241
|
+
return list(self.models.keys())
|
|
242
|
+
|
|
243
|
+
def clear_all_models(self) -> None:
|
|
244
|
+
"""Remove all models from the scene."""
|
|
245
|
+
for name in list(self.models.keys()):
|
|
246
|
+
self.remove_model(name)
|
|
247
|
+
|
|
248
|
+
class VTKWidget(QFrame):
|
|
249
|
+
"""VTK widget for 3D visualization with model management."""
|
|
250
|
+
|
|
251
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
252
|
+
super().__init__(parent)
|
|
253
|
+
self.setFrameStyle(QFrame.Box)
|
|
254
|
+
self.setStyleSheet("background-color: #2a2a2a;")
|
|
255
|
+
|
|
256
|
+
# Create VTK renderer and widget
|
|
257
|
+
self.vtk_widget: QVTKRenderWindowInteractor = QVTKRenderWindowInteractor(self)
|
|
258
|
+
self.renderer: vtk.vtkRenderer = vtk.vtkRenderer()
|
|
259
|
+
|
|
260
|
+
# IMPORTANT: Get the render window and configure it properly
|
|
261
|
+
render_window = self.vtk_widget.GetRenderWindow()
|
|
262
|
+
render_window.AddRenderer(self.renderer)
|
|
263
|
+
|
|
264
|
+
# CRITICAL: Set window size and position to match parent widget
|
|
265
|
+
# This prevents VTK from creating its own separate window
|
|
266
|
+
render_window.SetSize(self.width(), self.height())
|
|
267
|
+
render_window.SetPosition(0, 0)
|
|
268
|
+
|
|
269
|
+
# Optional: Disable off-screen rendering if not needed
|
|
270
|
+
# render_window.SetOffScreenRendering(False)
|
|
271
|
+
|
|
272
|
+
# Set background color (dark gray)
|
|
273
|
+
self.renderer.SetBackground(0.2, 0.2, 0.2)
|
|
274
|
+
|
|
275
|
+
# Create model manager
|
|
276
|
+
self.model_manager: VTKModelManager = VTKModelManager(self.renderer)
|
|
277
|
+
|
|
278
|
+
# Initialize camera with default settings
|
|
279
|
+
self.set_camera_default()
|
|
280
|
+
|
|
281
|
+
# Initialize interactor
|
|
282
|
+
render_window.Render()
|
|
283
|
+
self.vtk_widget.Initialize()
|
|
284
|
+
|
|
285
|
+
# Layout
|
|
286
|
+
layout = QVBoxLayout(self)
|
|
287
|
+
layout.setContentsMargins(0, 0, 0, 0)
|
|
288
|
+
layout.addWidget(self.vtk_widget)
|
|
289
|
+
|
|
290
|
+
# Mouse interaction enabled by default
|
|
291
|
+
self.mouse_interaction_enabled: bool = True
|
|
292
|
+
|
|
293
|
+
# Override resize event to update VTK window size
|
|
294
|
+
def resizeEvent(self, a0):
|
|
295
|
+
"""Update VTK render window size when widget is resized."""
|
|
296
|
+
super().resizeEvent(a0)
|
|
297
|
+
if hasattr(self, 'vtk_widget'):
|
|
298
|
+
render_window = self.vtk_widget.GetRenderWindow()
|
|
299
|
+
render_window.SetSize(self.width(), self.height())
|
|
300
|
+
self.vtk_render()
|
|
301
|
+
|
|
302
|
+
def set_camera_default(self) -> None:
|
|
303
|
+
"""Set default camera position."""
|
|
304
|
+
self.renderer.ResetCamera()
|
|
305
|
+
self.renderer.GetActiveCamera().SetClippingRange(0.01, 10 ** 7)
|
|
306
|
+
|
|
307
|
+
def set_camera_y_direction(self, up_vector: Tuple[float, float, float] = (0, 1, 0)) -> None:
|
|
308
|
+
"""
|
|
309
|
+
Set camera's Y-axis direction (up vector).
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
up_vector: Tuple (x, y, z) specifying the up direction
|
|
313
|
+
"""
|
|
314
|
+
self.renderer.GetActiveCamera().SetViewUp(up_vector[0], up_vector[1], up_vector[2])
|
|
315
|
+
self.vtk_render()
|
|
316
|
+
|
|
317
|
+
def set_camera_pose(self, position: Tuple[float, float, float],
|
|
318
|
+
focal_point: Tuple[float, float, float],
|
|
319
|
+
view_up: Tuple[float, float, float] = (0, 1, 0)) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Set camera position and orientation.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
position: Tuple (x, y, z) camera position
|
|
325
|
+
focal_point: Tuple (x, y, z) point the camera looks at
|
|
326
|
+
view_up: Tuple (x, y, z) up vector
|
|
327
|
+
"""
|
|
328
|
+
camera = self.renderer.GetActiveCamera()
|
|
329
|
+
camera.SetPosition(position[0], position[1], position[2])
|
|
330
|
+
camera.SetFocalPoint(focal_point[0], focal_point[1], focal_point[2])
|
|
331
|
+
camera.SetViewUp(view_up[0], view_up[1], view_up[2])
|
|
332
|
+
self.vtk_render()
|
|
333
|
+
|
|
334
|
+
def set_camera_clipping_range(self, near_plane: float = 0.01, far_plane: float = 1000) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Set camera clipping planes to maximum range.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
near_plane: Near clipping plane distance
|
|
340
|
+
far_plane: Far clipping plane distance
|
|
341
|
+
"""
|
|
342
|
+
self.renderer.GetActiveCamera().SetClippingRange(near_plane, far_plane)
|
|
343
|
+
self.vtk_render()
|
|
344
|
+
|
|
345
|
+
def set_mouse_interaction(self, enabled: bool) -> None:
|
|
346
|
+
"""
|
|
347
|
+
Enable or disable mouse interaction with the scene.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
enabled: Boolean, True to enable mouse interaction, False to disable
|
|
351
|
+
"""
|
|
352
|
+
self.mouse_interaction_enabled = enabled
|
|
353
|
+
interactor = self.vtk_widget.GetRenderWindow().GetInteractor()
|
|
354
|
+
|
|
355
|
+
if enabled:
|
|
356
|
+
if hasattr(self, '_original_style'):
|
|
357
|
+
interactor.SetInteractorStyle(self._original_style)
|
|
358
|
+
else:
|
|
359
|
+
style = vtk.vtkInteractorStyleTrackballCamera()
|
|
360
|
+
interactor.SetInteractorStyle(style)
|
|
361
|
+
else:
|
|
362
|
+
# 保存当前样式
|
|
363
|
+
self._original_style = interactor.GetInteractorStyle()
|
|
364
|
+
null_style = vtk.vtkInteractorStyle()
|
|
365
|
+
null_style.SetAutoAdjustCameraClippingRange(False)
|
|
366
|
+
interactor.SetInteractorStyle(null_style)
|
|
367
|
+
|
|
368
|
+
self.vtk_render()
|
|
369
|
+
|
|
370
|
+
def add_model_from_file(self, name: str, file_path: str,
|
|
371
|
+
color: Optional[Tuple[float, float, float]] = None,
|
|
372
|
+
scale: float = 1.0,
|
|
373
|
+
position: Tuple[float, float, float] = (0, 0, 0)) -> bool:
|
|
374
|
+
"""Add model from file."""
|
|
375
|
+
return self.model_manager.add_model_from_file(name, file_path, color, scale, position)
|
|
376
|
+
|
|
377
|
+
def remove_model(self, name: str) -> bool:
|
|
378
|
+
"""Remove a model by name."""
|
|
379
|
+
return self.model_manager.remove_model(name)
|
|
380
|
+
|
|
381
|
+
def set_model_scale(self, name: str, scale: float) -> bool:
|
|
382
|
+
"""Set model scale."""
|
|
383
|
+
return self.model_manager.set_model_scale(name, scale)
|
|
384
|
+
|
|
385
|
+
def set_model_pose(self, name: str,
|
|
386
|
+
translation_matrix: Optional[Union[np.ndarray, Tuple[float, float, float]]] = None,
|
|
387
|
+
rotation_matrix: Optional[np.ndarray] = None) -> bool:
|
|
388
|
+
"""Set model pose with matrices."""
|
|
389
|
+
return self.model_manager.set_model_pose(name, translation_matrix, rotation_matrix)
|
|
390
|
+
|
|
391
|
+
def set_model_position(self, name: str, x: float, y: float, z: float) -> bool:
|
|
392
|
+
"""Set model position."""
|
|
393
|
+
return self.model_manager.set_model_position(name, x, y, z)
|
|
394
|
+
|
|
395
|
+
def get_model_list(self) -> List[str]:
|
|
396
|
+
"""Get list of all model names."""
|
|
397
|
+
return self.model_manager.get_model_list()
|
|
398
|
+
|
|
399
|
+
def clear_all_models(self) -> None:
|
|
400
|
+
"""Remove all models."""
|
|
401
|
+
self.model_manager.clear_all_models()
|
|
402
|
+
|
|
403
|
+
def vtk_render(self) -> None:
|
|
404
|
+
"""Force a render."""
|
|
405
|
+
if self.vtk_widget:
|
|
406
|
+
self.vtk_widget.GetRenderWindow().Render()
|
|
407
|
+
|
|
408
|
+
def get_renderer(self) -> vtk.vtkRenderer:
|
|
409
|
+
"""Return the VTK renderer for external manipulation."""
|
|
410
|
+
return self.renderer
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class ImageDisplayWidget(QFrame):
|
|
414
|
+
"""Widget for displaying a 2D slice from a 3D volume."""
|
|
415
|
+
|
|
416
|
+
def __init__(self, parent: Optional[QWidget] = None):
|
|
417
|
+
super().__init__(parent)
|
|
418
|
+
self.setFrameStyle(QFrame.Box)
|
|
419
|
+
self.setStyleSheet("background-color: black;")
|
|
420
|
+
self.image_data: Optional[np.ndarray] = None
|
|
421
|
+
self.setMinimumSize(100, 100)
|
|
422
|
+
|
|
423
|
+
def update_slice(self, slice_2d: np.ndarray) -> None:
|
|
424
|
+
"""
|
|
425
|
+
Update displayed 2D slice.
|
|
426
|
+
|
|
427
|
+
Args:
|
|
428
|
+
slice_2d: 2D numpy array (grayscale, uint8)
|
|
429
|
+
"""
|
|
430
|
+
if slice_2d is None:
|
|
431
|
+
return
|
|
432
|
+
|
|
433
|
+
# Store a copy to avoid memory issues
|
|
434
|
+
self.image_data = slice_2d.copy()
|
|
435
|
+
self.update() # trigger repaint
|
|
436
|
+
|
|
437
|
+
def paintEvent(self, a0: QPaintEvent|None) -> None:
|
|
438
|
+
"""Paint the image scaled to fit the widget while preserving aspect ratio."""
|
|
439
|
+
if self.image_data is None or self.image_data.size == 0:
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
h, w = self.image_data.shape
|
|
443
|
+
# Create a copy to ensure data persists
|
|
444
|
+
img_copy = self.image_data.copy()
|
|
445
|
+
qimage = QImage(img_copy.data, w, h, w, QImage.Format_Grayscale8)
|
|
446
|
+
|
|
447
|
+
# Scale to widget size while preserving aspect ratio
|
|
448
|
+
pixmap = QPixmap.fromImage(qimage)
|
|
449
|
+
scaled_pixmap = pixmap.scaled(
|
|
450
|
+
self.size(),
|
|
451
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
452
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
453
|
+
|
|
454
|
+
# Center the image
|
|
455
|
+
painter = QPainter(self)
|
|
456
|
+
painter.fillRect(self.rect(), Qt.GlobalColor.black)
|
|
457
|
+
x = (self.width() - scaled_pixmap.width()) // 2
|
|
458
|
+
y = (self.height() - scaled_pixmap.height()) // 2
|
|
459
|
+
painter.drawPixmap(x, y, scaled_pixmap)
|
|
460
|
+
painter.end()
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
class VolumeSliceViewer:
|
|
464
|
+
"""Manages 3D volume and extracts slices for display."""
|
|
465
|
+
|
|
466
|
+
def __init__(self, image_widgets: List[ImageDisplayWidget]):
|
|
467
|
+
"""
|
|
468
|
+
Initialize the volume slice viewer.
|
|
469
|
+
|
|
470
|
+
Args:
|
|
471
|
+
image_widgets: List of 3 ImageDisplayWidgets for XY, XZ, YZ slices
|
|
472
|
+
"""
|
|
473
|
+
self.volume_data: Optional[np.ndarray] = None # 3D uint8 array
|
|
474
|
+
self.image_widgets = image_widgets
|
|
475
|
+
|
|
476
|
+
# Current slice indices
|
|
477
|
+
self.current_x: int = 0
|
|
478
|
+
self.current_y: int = 0
|
|
479
|
+
self.current_z: int = 0
|
|
480
|
+
|
|
481
|
+
def set_volume(self, volume: np.ndarray) -> None:
|
|
482
|
+
"""
|
|
483
|
+
Set the 3D volume data.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
volume: 3D numpy array (uint8) with shape (depth, height, width) or (z, y, x)
|
|
487
|
+
"""
|
|
488
|
+
if volume is None:
|
|
489
|
+
return
|
|
490
|
+
|
|
491
|
+
assert volume.ndim == 3, "Volume must be 3D"
|
|
492
|
+
assert volume.dtype == np.uint8, "Volume must be uint8"
|
|
493
|
+
|
|
494
|
+
self.volume_data = volume
|
|
495
|
+
|
|
496
|
+
# Update all slices with current indices
|
|
497
|
+
self.update_all_slices()
|
|
498
|
+
|
|
499
|
+
def set_slice_position(self, axis: str, position: int) -> None:
|
|
500
|
+
"""
|
|
501
|
+
Set the slice position for a specific axis.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
axis: 'x', 'y', or 'z'
|
|
505
|
+
position: Slice index
|
|
506
|
+
"""
|
|
507
|
+
if self.volume_data is None:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
old_position = None
|
|
511
|
+
if axis == 'x':
|
|
512
|
+
old_position = self.current_x
|
|
513
|
+
self.current_x = max(0, min(position, self.volume_data.shape[0] - 1))
|
|
514
|
+
if old_position == self.current_x:
|
|
515
|
+
return
|
|
516
|
+
elif axis == 'y':
|
|
517
|
+
old_position = self.current_y
|
|
518
|
+
self.current_y = max(0, min(position, self.volume_data.shape[1] - 1))
|
|
519
|
+
if old_position == self.current_y:
|
|
520
|
+
return
|
|
521
|
+
elif axis == 'z':
|
|
522
|
+
old_position = self.current_z
|
|
523
|
+
self.current_z = max(0, min(position, self.volume_data.shape[2] - 1))
|
|
524
|
+
if old_position == self.current_z:
|
|
525
|
+
return
|
|
526
|
+
else:
|
|
527
|
+
raise ValueError("Axis must be 'x', 'y', or 'z'")
|
|
528
|
+
|
|
529
|
+
# Update the corresponding slice
|
|
530
|
+
self._update_slice(axis)
|
|
531
|
+
|
|
532
|
+
def set_slice_positions(self, x: Optional[int] = None, y: Optional[int] = None, z: Optional[int] = None) -> None:
|
|
533
|
+
"""
|
|
534
|
+
Set multiple slice positions at once.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
x: X slice index (optional)
|
|
538
|
+
y: Y slice index (optional)
|
|
539
|
+
z: Z slice index (optional)
|
|
540
|
+
"""
|
|
541
|
+
if self.volume_data is None:
|
|
542
|
+
return
|
|
543
|
+
|
|
544
|
+
need_update = False
|
|
545
|
+
|
|
546
|
+
if x is not None:
|
|
547
|
+
new_x = max(0, min(x, self.volume_data.shape[0] - 1))
|
|
548
|
+
if new_x != self.current_x:
|
|
549
|
+
self.current_x = new_x
|
|
550
|
+
need_update = True
|
|
551
|
+
|
|
552
|
+
if y is not None:
|
|
553
|
+
new_y = max(0, min(y, self.volume_data.shape[1] - 1))
|
|
554
|
+
if new_y != self.current_y:
|
|
555
|
+
self.current_y = new_y
|
|
556
|
+
need_update = True
|
|
557
|
+
|
|
558
|
+
if z is not None:
|
|
559
|
+
new_z = max(0, min(z, self.volume_data.shape[2] - 1))
|
|
560
|
+
if new_z != self.current_z:
|
|
561
|
+
self.current_z = new_z
|
|
562
|
+
need_update = True
|
|
563
|
+
|
|
564
|
+
if need_update:
|
|
565
|
+
self.update_all_slices()
|
|
566
|
+
|
|
567
|
+
def update_all_slices(self) -> None:
|
|
568
|
+
"""Update all three slice views."""
|
|
569
|
+
if self.volume_data is None:
|
|
570
|
+
return
|
|
571
|
+
|
|
572
|
+
# Extract and display XY slice (constant Z)
|
|
573
|
+
xy_slice = self.volume_data[self.current_z, :, :]
|
|
574
|
+
self.image_widgets[0].update_slice(xy_slice)
|
|
575
|
+
|
|
576
|
+
# Extract and display XZ slice (constant Y)
|
|
577
|
+
xz_slice = self.volume_data[:, self.current_y, :]
|
|
578
|
+
self.image_widgets[1].update_slice(xz_slice)
|
|
579
|
+
|
|
580
|
+
# Extract and display YZ slice (constant X)
|
|
581
|
+
yz_slice = self.volume_data[:, :, self.current_x]
|
|
582
|
+
self.image_widgets[2].update_slice(yz_slice)
|
|
583
|
+
|
|
584
|
+
def _update_slice(self, axis: str) -> None:
|
|
585
|
+
"""Update a single slice based on axis."""
|
|
586
|
+
if self.volume_data is None:
|
|
587
|
+
return
|
|
588
|
+
|
|
589
|
+
if axis == 'z':
|
|
590
|
+
# XY slice
|
|
591
|
+
xy_slice = self.volume_data[self.current_z, :, :]
|
|
592
|
+
self.image_widgets[0].update_slice(xy_slice)
|
|
593
|
+
elif axis == 'y':
|
|
594
|
+
# XZ slice
|
|
595
|
+
xz_slice = self.volume_data[:, self.current_y, :]
|
|
596
|
+
self.image_widgets[1].update_slice(xz_slice)
|
|
597
|
+
elif axis == 'x':
|
|
598
|
+
# YZ slice
|
|
599
|
+
yz_slice = self.volume_data[:, :, self.current_x]
|
|
600
|
+
self.image_widgets[2].update_slice(yz_slice)
|
|
601
|
+
|
|
602
|
+
def get_current_positions(self) -> Tuple[int, int, int]:
|
|
603
|
+
"""Get current slice positions (x, y, z)."""
|
|
604
|
+
return (self.current_x, self.current_y, self.current_z)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class MedScopeWindow(QMainWindow):
|
|
608
|
+
"""Main window with four equal sub-windows."""
|
|
609
|
+
|
|
610
|
+
def set_window_title(self, new_title:str, force:bool=False):
|
|
611
|
+
if force or (new_title != self.window_title):
|
|
612
|
+
self.window_title = new_title
|
|
613
|
+
self.setWindowTitle(self.window_title)
|
|
614
|
+
|
|
615
|
+
def __init__(self):
|
|
616
|
+
super().__init__()
|
|
617
|
+
|
|
618
|
+
# Set window title
|
|
619
|
+
self.window_title = "MedScope"
|
|
620
|
+
self.set_window_title(self.window_title, True)
|
|
621
|
+
|
|
622
|
+
# Get screen resolution and set window to full screen
|
|
623
|
+
screen = QApplication.primaryScreen()
|
|
624
|
+
if screen is not None:
|
|
625
|
+
screen_geometry = screen.availableGeometry()
|
|
626
|
+
self.setGeometry(screen_geometry)
|
|
627
|
+
else:
|
|
628
|
+
self.setGeometry(0, 0, 1920, 1080) # default size
|
|
629
|
+
self.showMaximized()
|
|
630
|
+
|
|
631
|
+
# Create central widget and main layout
|
|
632
|
+
central_widget = QWidget()
|
|
633
|
+
self.setCentralWidget(central_widget)
|
|
634
|
+
main_layout = QVBoxLayout(central_widget)
|
|
635
|
+
main_layout.setContentsMargins(0, 0, 0, 0)
|
|
636
|
+
main_layout.setSpacing(2)
|
|
637
|
+
|
|
638
|
+
# Create a 2x2 grid layout
|
|
639
|
+
grid_layout = QHBoxLayout()
|
|
640
|
+
left_column = QVBoxLayout()
|
|
641
|
+
right_column = QVBoxLayout()
|
|
642
|
+
|
|
643
|
+
# Top-left: VTK scene
|
|
644
|
+
self.vtk_widget: VTKWidget = VTKWidget()
|
|
645
|
+
left_column.addWidget(self.vtk_widget)
|
|
646
|
+
|
|
647
|
+
# Bottom-left: XY slice (Z = constant)
|
|
648
|
+
self.image_widget_xy: ImageDisplayWidget = ImageDisplayWidget()
|
|
649
|
+
left_column.addWidget(self.image_widget_xy)
|
|
650
|
+
|
|
651
|
+
# Top-right: XZ slice (Y = constant)
|
|
652
|
+
self.image_widget_xz: ImageDisplayWidget = ImageDisplayWidget()
|
|
653
|
+
right_column.addWidget(self.image_widget_xz)
|
|
654
|
+
|
|
655
|
+
# Bottom-right: YZ slice (X = constant)
|
|
656
|
+
self.image_widget_yz: ImageDisplayWidget = ImageDisplayWidget()
|
|
657
|
+
right_column.addWidget(self.image_widget_yz)
|
|
658
|
+
|
|
659
|
+
# Set equal stretch factors
|
|
660
|
+
left_column.setStretch(0, 1)
|
|
661
|
+
left_column.setStretch(1, 1)
|
|
662
|
+
right_column.setStretch(0, 1)
|
|
663
|
+
right_column.setStretch(1, 1)
|
|
664
|
+
|
|
665
|
+
grid_layout.addLayout(left_column, 1)
|
|
666
|
+
grid_layout.addLayout(right_column, 1)
|
|
667
|
+
main_layout.addLayout(grid_layout)
|
|
668
|
+
|
|
669
|
+
# Create volume slice viewer
|
|
670
|
+
self.slice_viewer = VolumeSliceViewer([
|
|
671
|
+
self.image_widget_xy, # XY slice (Z constant)
|
|
672
|
+
self.image_widget_xz, # XZ slice (Y constant)
|
|
673
|
+
self.image_widget_yz # YZ slice (X constant)
|
|
674
|
+
])
|
|
675
|
+
|
|
676
|
+
# Store all timers
|
|
677
|
+
self.timer_pool: Dict[str, QTimer] = dict()
|
|
678
|
+
|
|
679
|
+
# Default: ban mouse interaction
|
|
680
|
+
self.set_mouse_interaction(False)
|
|
681
|
+
|
|
682
|
+
# Create a default test volume
|
|
683
|
+
self._create_default_volume()
|
|
684
|
+
|
|
685
|
+
# Show window
|
|
686
|
+
self.show()
|
|
687
|
+
|
|
688
|
+
def _create_default_volume(self) -> None:
|
|
689
|
+
"""Create a fast test 3D volume with simple patterns."""
|
|
690
|
+
self.set_volume(np.zeros((1, 1, 1), np.uint8))
|
|
691
|
+
|
|
692
|
+
def add_timer(self, timer_name: str, timer_ms:int, call_func:Optional[Callable]=None) -> None:
|
|
693
|
+
"""Add or erase a timer to the pool."""
|
|
694
|
+
assert timer_ms >= 0
|
|
695
|
+
|
|
696
|
+
if self.timer_pool.get(timer_name) is not None:
|
|
697
|
+
self.timer_pool[timer_name].stop()
|
|
698
|
+
del self.timer_pool[timer_name]
|
|
699
|
+
|
|
700
|
+
if call_func is not None:
|
|
701
|
+
qtimer_now = QTimer()
|
|
702
|
+
qtimer_now.timeout.connect(call_func)
|
|
703
|
+
qtimer_now.start()
|
|
704
|
+
self.timer_pool[timer_name] = qtimer_now
|
|
705
|
+
|
|
706
|
+
def __del__(self):
|
|
707
|
+
for timer_name in self.timer_pool:
|
|
708
|
+
self.timer_pool[timer_name].stop()
|
|
709
|
+
|
|
710
|
+
# Volume slice interface methods
|
|
711
|
+
def set_volume(self, volume: np.ndarray) -> None:
|
|
712
|
+
"""
|
|
713
|
+
Set the 3D volume data.
|
|
714
|
+
|
|
715
|
+
Args:
|
|
716
|
+
volume: 3D numpy array (uint8) with shape (z, y, x)
|
|
717
|
+
"""
|
|
718
|
+
self.slice_viewer.set_volume(volume)
|
|
719
|
+
|
|
720
|
+
def set_slice_x(self, x: int) -> None:
|
|
721
|
+
"""Set the X slice position (for YZ plane)."""
|
|
722
|
+
self.slice_viewer.set_slice_position('x', x)
|
|
723
|
+
|
|
724
|
+
def set_slice_y(self, y: int) -> None:
|
|
725
|
+
"""Set the Y slice position (for XZ plane)."""
|
|
726
|
+
self.slice_viewer.set_slice_position('y', y)
|
|
727
|
+
|
|
728
|
+
def set_slice_z(self, z: int) -> None:
|
|
729
|
+
"""Set the Z slice position (for XY plane)."""
|
|
730
|
+
self.slice_viewer.set_slice_position('z', z)
|
|
731
|
+
|
|
732
|
+
def set_slice_positions(self, x: Optional[int] = None, y: Optional[int] = None, z: Optional[int] = None) -> None:
|
|
733
|
+
"""Set multiple slice positions at once."""
|
|
734
|
+
self.slice_viewer.set_slice_positions(x, y, z)
|
|
735
|
+
|
|
736
|
+
def get_slice_positions(self) -> Tuple[int, int, int]:
|
|
737
|
+
"""Get current slice positions (x, y, z)."""
|
|
738
|
+
return self.slice_viewer.get_current_positions()
|
|
739
|
+
|
|
740
|
+
# VTK interface methods for external use
|
|
741
|
+
def get_vtk_renderer(self) -> vtk.vtkRenderer:
|
|
742
|
+
"""Return the VTK renderer."""
|
|
743
|
+
return self.vtk_widget.get_renderer()
|
|
744
|
+
|
|
745
|
+
def set_camera_y_direction(self, up_vector: Tuple[float, float, float] = (0, 1, 0)) -> None:
|
|
746
|
+
"""Set camera's up direction."""
|
|
747
|
+
self.vtk_widget.set_camera_y_direction(up_vector)
|
|
748
|
+
|
|
749
|
+
def set_camera_pose(self, position: Tuple[float, float, float],
|
|
750
|
+
focal_point: Tuple[float, float, float],
|
|
751
|
+
view_up: Tuple[float, float, float] = (0, 1, 0)) -> None:
|
|
752
|
+
"""Set camera position and orientation."""
|
|
753
|
+
self.vtk_widget.set_camera_pose(position, focal_point, view_up)
|
|
754
|
+
|
|
755
|
+
def set_camera_clipping_range(self, near_plane: float = 0.01, far_plane: float = 10**7) -> None:
|
|
756
|
+
"""Set camera clipping range."""
|
|
757
|
+
self.vtk_widget.set_camera_clipping_range(near_plane, far_plane)
|
|
758
|
+
|
|
759
|
+
def set_mouse_interaction(self, enabled: bool) -> None:
|
|
760
|
+
"""Enable/disable mouse interaction."""
|
|
761
|
+
self.vtk_widget.set_mouse_interaction(enabled)
|
|
762
|
+
|
|
763
|
+
def add_model_from_file(self, name: str, file_path: str,
|
|
764
|
+
color: Optional[Tuple[float, float, float]] = None,
|
|
765
|
+
scale: float = 1.0,
|
|
766
|
+
position: Tuple[float, float, float] = (0, 0, 0)) -> bool:
|
|
767
|
+
"""Add 3D model from file."""
|
|
768
|
+
return self.vtk_widget.add_model_from_file(name, file_path, color, scale, position)
|
|
769
|
+
|
|
770
|
+
def remove_model(self, name: str) -> bool:
|
|
771
|
+
"""Remove a model by name."""
|
|
772
|
+
return self.vtk_widget.remove_model(name)
|
|
773
|
+
|
|
774
|
+
def set_model_position(self, name: str, x: float, y: float, z: float) -> bool:
|
|
775
|
+
"""Set model position."""
|
|
776
|
+
ans = self.vtk_widget.set_model_position(name, x, y, z)
|
|
777
|
+
self.vtk_widget.vtk_render()
|
|
778
|
+
return ans
|
|
779
|
+
|
|
780
|
+
def set_model_scale(self, name: str, scale: float) -> bool:
|
|
781
|
+
"""Set model scale."""
|
|
782
|
+
ans = self.vtk_widget.set_model_scale(name, scale)
|
|
783
|
+
self.vtk_widget.vtk_render()
|
|
784
|
+
return ans
|
|
785
|
+
|
|
786
|
+
def set_model_pose(self, name: str,
|
|
787
|
+
translation_matrix: Optional[Union[np.ndarray, Tuple[float, float, float]]] = None,
|
|
788
|
+
rotation_matrix: Optional[np.ndarray] = None) -> bool:
|
|
789
|
+
"""Set model pose with matrices."""
|
|
790
|
+
ans = self.vtk_widget.set_model_pose(name, translation_matrix, rotation_matrix)
|
|
791
|
+
self.vtk_widget.vtk_render()
|
|
792
|
+
return ans
|
|
793
|
+
|
|
794
|
+
def get_model_list(self) -> List[str]:
|
|
795
|
+
"""Get list of all model names."""
|
|
796
|
+
return self.vtk_widget.get_model_list()
|
|
797
|
+
|
|
798
|
+
def clear_all_models(self) -> None:
|
|
799
|
+
"""Remove all models."""
|
|
800
|
+
self.vtk_widget.clear_all_models()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: medscope
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Medical Intraoperative Real-time Visualization System.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: GGN_2015
|
|
8
|
+
Author-email: neko@jlulug.org
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Requires-Dist: pyqt5
|
|
17
|
+
Requires-Dist: vtk
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# medscope
|
|
21
|
+
Medical Intraoperative Real-time Visualization System.
|
|
22
|
+
|
|
23
|
+
## Demo
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
> [!IMPORTANT]
|
|
30
|
+
> You should use python >=3.10, <3.13
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install medscope
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
> [!WARNING]
|
|
39
|
+
> Do not use multithreading, use `MedScopeWindow.add_timer`
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from medscope import MedScopeWindow, MedScopeSystem
|
|
43
|
+
import numpy as np
|
|
44
|
+
import sys
|
|
45
|
+
|
|
46
|
+
# Generate a random 3x3 rotation matrix uniformly distributed over SO(3).
|
|
47
|
+
def random_rotation_matrix() -> np.ndarray:
|
|
48
|
+
phi = np.random.uniform(0, 2 * np.pi)
|
|
49
|
+
theta = np.random.uniform(0, np.pi)
|
|
50
|
+
psi = np.random.uniform(0, 2 * np.pi)
|
|
51
|
+
cos_phi, sin_phi = np.cos(phi), np.sin(phi)
|
|
52
|
+
cos_theta, sin_theta = np.cos(theta), np.sin(theta)
|
|
53
|
+
cos_psi, sin_psi = np.cos(psi), np.sin(psi)
|
|
54
|
+
R = np.array([
|
|
55
|
+
[cos_phi*cos_theta*cos_psi - sin_phi*sin_psi,
|
|
56
|
+
-cos_phi*cos_theta*sin_psi - sin_phi*cos_psi,
|
|
57
|
+
cos_phi*sin_theta],
|
|
58
|
+
[sin_phi*cos_theta*cos_psi + cos_phi*sin_psi,
|
|
59
|
+
-sin_phi*cos_theta*sin_psi + cos_phi*cos_psi,
|
|
60
|
+
sin_phi*sin_theta],
|
|
61
|
+
[-sin_theta*cos_psi,
|
|
62
|
+
sin_theta*sin_psi,
|
|
63
|
+
cos_theta]
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
return R
|
|
67
|
+
|
|
68
|
+
# Initialize app and window
|
|
69
|
+
app = MedScopeSystem(sys.argv)
|
|
70
|
+
window = MedScopeWindow()
|
|
71
|
+
|
|
72
|
+
# Add a 3D model
|
|
73
|
+
window.add_model_from_file(
|
|
74
|
+
"bone_model",
|
|
75
|
+
"BONE-1.new.stl",
|
|
76
|
+
(1.0, 1.0, 1.0)) # white, random if not given
|
|
77
|
+
|
|
78
|
+
# Set the pose of camera
|
|
79
|
+
# you can change camera pos in callback function with add_timer
|
|
80
|
+
window.set_camera_pose(
|
|
81
|
+
(0, 0, -500),
|
|
82
|
+
(0, 0, 0),
|
|
83
|
+
(0, 1, 0)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# Volume data should be given in 3D np.ndarray
|
|
87
|
+
# and dtype should be np.uint8
|
|
88
|
+
window.set_volume(np.random.randint(0, 255, (256, 256, 256)).astype(np.uint8))
|
|
89
|
+
|
|
90
|
+
# Create a callback function to move your model
|
|
91
|
+
def move_model():
|
|
92
|
+
import random
|
|
93
|
+
x = random.randint(0, 256)
|
|
94
|
+
y = random.randint(0, 256)
|
|
95
|
+
z = random.randint(0, 256)
|
|
96
|
+
window.set_slice_positions(x, y, z)
|
|
97
|
+
window.set_model_pose(
|
|
98
|
+
"bone_model",
|
|
99
|
+
(x - 128, y - 128, z - 128),
|
|
100
|
+
random_rotation_matrix()
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Call move_model every 1ms (as quickly as the processor can)
|
|
104
|
+
window.add_timer("move_model", 1, move_model)
|
|
105
|
+
sys.exit(app.exec_())
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
medscope/__init__.py,sha256=DQWwgwJkZroJP_RRn9fhanSMwZnrfqSdvlP-wM_Gslk,147
|
|
2
|
+
medscope/main.py,sha256=sZ_10dNhOG_cBdUV4GcDmndgY1y3u0TgT5p8x9CsTZ8,31328
|
|
3
|
+
medscope-0.1.0.dist-info/licenses/LICENSE,sha256=gmkEFqkF3KJjRnhXGoLSfX4xXtfPHT1ghq3YgCF7B3w,1086
|
|
4
|
+
medscope-0.1.0.dist-info/METADATA,sha256=LBHDSs16qu_IdAf267NMcEDcELTaXtT_naO5b3slR9I,2786
|
|
5
|
+
medscope-0.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
6
|
+
medscope-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GGN_2015
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|