Skip to content

Commit f00aa0f

Browse files
Merge pull request #62 from arcadelab/dev
Dev
2 parents a7de915 + 4a88600 commit f00aa0f

File tree

11 files changed

+319
-97
lines changed

11 files changed

+319
-97
lines changed

README.md

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ in the base directory. Then do `cd docs` and `make html` to build the static sit
8282
The following minimal example loads a CT volume from a NifTi `.nii.gz` file and simulates an X-ray projection:
8383

8484
```python
85-
from deepdrr import geo, Volume, MobileCArm, Projector
85+
from deepdrr import geo, Volume, MobileCArm
86+
from deepdrr.projector import Projector # separate import for CUDA init
8687
import matplotlib.pyplot as plt
8788

8889
volume = Volume.from_nifti('/path/to/ct_image.nii.gz')
@@ -147,6 +148,41 @@ This capability has not been tested in version 1.0. For tool insertion, we recom
147148
2. The density of the tool needs to be provided via hard coding in the file 'load_dicom_tool.py' (line 127). The pose of the tool/implant with respect to the CT volume requires manual setup. We provide one example origin setting at line 23-24.
148149
3. The tool/implant will supersede the anatomy defined by the CT volume intensities. To this end, we sample the CT materials and densities at the location of the tool in the tool volume, and subtract them from the anatomy forward projections in detector domain (to enable different resolutions of CT and tool volume). Further information can be found in the IJCARS article.
149150

151+
### Using DeepDRR Simultaneously with PyTorch
152+
153+
Some issues may arise when using DeepDRR at the same time as PyTorch due to conflicts between pycuda's CUDA initialization and PyTorch CUDA initialization. The best workaround we know of is to first initialize the PyCUDA context (by importing `deepdrr.projector`) and then run your model on a dummy batch before creating a `Projector` object. For mysterious reasons (likely involving overlapping GPU resources and the retrograde of Mercury), this seems to work.
154+
155+
```Python
156+
import torch
157+
from torch import nn
158+
from torchvision import models
159+
160+
import deepdrr
161+
from deepdrr.projector import Projector # initializes PyCUDA
162+
163+
# Before creating a Projector, run backprop to initialize PyTorch
164+
criterion = nn.CrossEntropyLoss()
165+
model = models.resnet50() # Your model here
166+
model.cuda()
167+
optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
168+
optimizer.zero_grad()
169+
x = torch.ones((32, 3, 224, 224), dtype=torch.float32).cuda() # Your image size
170+
y = torch.ones(32, dtype=torch.int64).cuda()
171+
y_pred = model(x)
172+
loss = criterion(y_pred, y)
173+
loss.backward()
174+
optimizer.step()
175+
log.info(f"Ran dummy batch to initialize torch.")
176+
177+
volume = ...
178+
carm = ...
179+
with Projector(volume, carm=carm):
180+
image = projector()
181+
image = image.unsqueeze(0) # add batch dim
182+
y_pred = model(image)
183+
...
184+
```
185+
150186
## Reference
151187

152188
We hope this proves useful for medical imaging research. If you use our work, we would kindly ask you to reference our work.

deepdrr/annotations/line_annotation.py

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from __future__ import annotations
22

33
import logging
4-
from typing import Optional
4+
from typing import List, Optional
55
from pathlib import Path
66
import numpy as np
77
import json
88
import pyvista as pv
99

10-
from .. import geo
10+
from .. import geo, utils
1111
from ..vol import Volume, AnyVolume
1212

1313
logger = logging.getLogger(__name__)
@@ -65,6 +65,110 @@ def from_markup(cls, path: str, volume: AnyVolume) -> LineAnnotation:
6565

6666
return cls(*points, volume)
6767

68+
def save(self, path: str, color: List[float] = [0.5, 0.5, 0.5]):
69+
"""Save the Line annotation to a mrk.json file, which can be opened by 3D Slicer.
70+
71+
Args:
72+
path (str): Output path to the file.
73+
color (List[int], optional): The color of the saved annotation.
74+
"""
75+
path = Path(path).expanduser()
76+
77+
markup = {
78+
"@schema": "https://raw.githubusercontent.com/slicer/slicer/master/Modules/Loadable/Markups/Resources/Schema/markups-schema-v1.0.0.json#",
79+
"markups": [
80+
{
81+
"type": "Line",
82+
"coordinateSystem": self.volume.anatomical_coordinate_system,
83+
"locked": True,
84+
"labelFormat": "%N-%d",
85+
"controlPoints": [
86+
{
87+
"id": "1",
88+
"label": "entry",
89+
"description": "",
90+
"associatedNodeID": "",
91+
"position": utils.jsonable(self.startpoint),
92+
"orientation": [
93+
-1.0,
94+
-0.0,
95+
-0.0,
96+
-0.0,
97+
-1.0,
98+
-0.0,
99+
0.0,
100+
0.0,
101+
1.0,
102+
],
103+
"selected": True,
104+
"locked": False,
105+
"visibility": True,
106+
"positionStatus": "defined",
107+
},
108+
{
109+
"id": "2",
110+
"label": "exit",
111+
"description": "",
112+
"associatedNodeID": "",
113+
"position": utils.jsonable(self.endpoint),
114+
"orientation": [
115+
-1.0,
116+
-0.0,
117+
-0.0,
118+
-0.0,
119+
-1.0,
120+
-0.0,
121+
0.0,
122+
0.0,
123+
1.0,
124+
],
125+
"selected": True,
126+
"locked": False,
127+
"visibility": True,
128+
"positionStatus": "defined",
129+
},
130+
],
131+
"measurements": [
132+
{
133+
"name": "length",
134+
"enabled": True,
135+
"value": 124.90054351814699,
136+
"printFormat": "%-#4.4gmm",
137+
}
138+
],
139+
"display": {
140+
"visibility": True,
141+
"opacity": 1.0,
142+
"color": color,
143+
"selectedColor": [1.0, 0.5000076295109484, 0.5000076295109484],
144+
"activeColor": [0.4, 1.0, 0.0],
145+
"propertiesLabelVisibility": True,
146+
"pointLabelsVisibility": True,
147+
"textScale": 3.0,
148+
"glyphType": "Sphere3D",
149+
"glyphScale": 5.800000000000001,
150+
"glyphSize": 5.0,
151+
"useGlyphScale": True,
152+
"sliceProjection": False,
153+
"sliceProjectionUseFiducialColor": True,
154+
"sliceProjectionOutlinedBehindSlicePlane": False,
155+
"sliceProjectionColor": [1.0, 1.0, 1.0],
156+
"sliceProjectionOpacity": 0.6,
157+
"lineThickness": 0.2,
158+
"lineColorFadingStart": 1.0,
159+
"lineColorFadingEnd": 10.0,
160+
"lineColorFadingSaturation": 1.0,
161+
"lineColorFadingHueOffset": 0.0,
162+
"handlesInteractive": False,
163+
"snapMode": "toVisibleSurface",
164+
},
165+
}
166+
],
167+
}
168+
169+
with open(path, "w") as file:
170+
json.dump(markup, file)
171+
68172
@property
69173
def startpoint_in_world(self) -> geo.Point:
70174
return self.volume.world_from_anatomical @ self.startpoint

deepdrr/device.py

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional, Tuple, Union, List
1+
from typing import Any, Dict, Optional, Tuple, Union, List
22

33
import logging
44
import numpy as np
@@ -96,14 +96,14 @@ def __init__(
9696
isocenter: geo.Point3D = [0, 0, 0],
9797
alpha: float = 0,
9898
beta: float = 0,
99+
degrees: bool = True,
99100
horizontal_movement: float = 200, # width of window in X and Y planes.
100101
vertical_travel: float = 430, # width of window in Z plane.
101102
min_alpha: float = -40,
102103
max_alpha: float = 110,
103104
# note that this would collide with the patient. Suggested to limit to +/- 45
104105
min_beta: float = -225,
105106
max_beta: float = 225,
106-
degrees: bool = True,
107107
source_to_detector_distance: float = 1020,
108108
# vertical component of the source point offset from the isocenter of rotation, in -Z. Previously called `isocenter_distance`
109109
source_to_isocenter_vertical_distance: float = 530,
@@ -117,7 +117,7 @@ def __init__(
117117
sensor_width: int = 1536,
118118
pixel_size: float = 0.194,
119119
rotate_camera_left: bool = True, # make it so that down in the image corresponds to -x, so that patient images appear as expected.
120-
enforce_isocenter_bounds: bool = True,
120+
enforce_isocenter_bounds: bool = False, # Allow the isocenter to travel arbitrarily far from the device origin
121121
) -> None:
122122
"""A simulated C-arm imaging device with orbital movement (alpha), angulation (beta) and 3D translation.
123123
@@ -201,6 +201,34 @@ def __str__(self):
201201
f"alpha={np.degrees(self.alpha)}, beta={np.degrees(self.beta)}, degrees=True)"
202202
)
203203

204+
def to_config(self) -> Dict[str, Any]:
205+
"""Get a json-safe dictionary that can be used to initialize the C-arm in its current pose."""
206+
return utils.jsonable(
207+
dict(
208+
world_from_device=self.world_from_device,
209+
isocenter=self.isocenter,
210+
alpha=self.alpha,
211+
beta=self.beta,
212+
degrees=False,
213+
horizontal_movement=self.horizontal_movement,
214+
vertical_travel=self.vertical_travel,
215+
min_alpha=self.min_alpha,
216+
max_alpha=self.max_alpha,
217+
min_beta=self.min_beta,
218+
max_beta=self.max_beta,
219+
source_to_detector_distance=self.source_to_detector_distance,
220+
source_to_isocenter_vertical_distance=self.source_to_isocenter_vertical_distance,
221+
source_to_isocenter_horizontal_offset=self.source_to_isocenter_horizontal_offset,
222+
immersion_depth=self.immersion_depth,
223+
free_space=self.free_space,
224+
sensor_height=self.sensor_height,
225+
sensor_width=self.sensor_width,
226+
pixel_size=self.pixel_size,
227+
rotate_camera_left=self.rotate_camera_left,
228+
enforce_isocenter_bounds=self.enforce_isocenter_bounds,
229+
)
230+
)
231+
204232
@property
205233
def max_isocenter(self) -> np.ndarray:
206234
return (
@@ -257,10 +285,10 @@ def camera3d_from_device(self) -> geo.FrameTransform:
257285
@property
258286
def camera3d_from_world(self) -> geo.FrameTransform:
259287
"""Rigid transformation of the C-arm camera pose."""
260-
return self.camera3d_from_device @ self.device_from_world
288+
return self.get_camera3d_from_world()
261289

262290
def get_camera3d_from_world(self) -> geo.FrameTransform:
263-
return self.camera3d_from_world
291+
return self.camera3d_from_device @ self.device_from_world
264292

265293
def get_camera_projection(self) -> geo.CameraProjection:
266294
return geo.CameraProjection(

deepdrr/geo/camera_projection.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from typing import Union, Optional, Any, TYPE_CHECKING
22
import numpy as np
33

4-
from .core import Transform, FrameTransform, point, Point3D
4+
from .core import Transform, FrameTransform, point, Point3D, get_data
55
from .camera_intrinsic_transform import CameraIntrinsicTransform
66
from ..vol import AnyVolume
77

@@ -66,17 +66,25 @@ def extrinsic(self) -> FrameTransform:
6666
return self.camera3d_from_world
6767

6868
@property
69-
def index_from_world(self) -> FrameTransform:
69+
def index_from_world(self) -> Transform:
7070
proj = np.concatenate([np.eye(3), np.zeros((3, 1))], axis=1)
7171
camera2d_from_camera3d = Transform(proj, _inv=proj.T)
7272
return (
7373
self.index_from_camera2d @ camera2d_from_camera3d @ self.camera3d_from_world
7474
)
7575

7676
@property
77-
def world_from_index(self) -> FrameTransform:
77+
def world_from_index(self) -> Transform:
7878
return self.index_from_world.inv
7979

80+
@property
81+
def world_from_index_on_image_plane(self) -> FrameTransform:
82+
"""Get the transform to points in world on the image (detector) plane from image indices."""
83+
proj = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 0], [0, 0, 1]])
84+
proj = Transform(proj, _inv=proj.T)
85+
index_from_world_3d = proj @ self.index_from_world
86+
return FrameTransform(data=get_data(index_from_world_3d.inv))
87+
8088
@property
8189
def sensor_width(self) -> int:
8290
return self.intrinsic.sensor_width

deepdrr/geo/core.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ def to_array(self, is_point):
9292
"""
9393
pass
9494

95+
def tolist(self) -> List:
96+
"""Get a json-save list with the data in this object."""
97+
return self.data.tolist()
98+
9599
def __array__(self, dtype=None):
96100
return self.to_array()
97101

0 commit comments

Comments
 (0)