Skip to content

Commit 9480282

Browse files
authored
feat: 🎸 Rotatable Crosshairs (#115)
feat: 🎸 Rotatable Crosshairs Adds an alternative implementation for rotatable crosshairs.
1 parent 92b7782 commit 9480282

11 files changed

+1676
-21
lines changed

‎examples/App.js‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import VTKMPRPaintingExample from './VTKMPRPaintingExample.js';
77
import VTKCornerstonePaintingSyncExample from './VTKCornerstonePaintingSyncExample.js';
88
import VTKLoadImageDataExample from './VTKLoadImageDataExample.js';
99
import VTKCrosshairsExample from './VTKCrosshairsExample.js';
10+
import VTKRotatableCrosshairsExample from './VTKRotatableCrosshairsExample.js';
1011
import VTKMPRRotateExample from './VTKMPRRotateExample.js';
1112
import VTKVolumeRenderingExample from './VTKVolumeRenderingExample.js';
1213

@@ -51,8 +52,7 @@ function Index() {
5152
{
5253
title: 'Volume Rendering',
5354
url: '/volume-rendering',
54-
text:
55-
'Demonstrates how to perform volume rendering for a CT volume.',
55+
text: 'Demonstrates how to perform volume rendering for a CT volume.',
5656
},
5757
{
5858
title: 'Image Segmentation via Paint Widget',
@@ -72,6 +72,12 @@ function Index() {
7272
text:
7373
'Demonstrates how to set up the Crosshairs interactor style and SVG Widget',
7474
},
75+
{
76+
title: 'MPR Rotatable Crosshairs Example',
77+
url: '/rotatable-crosshairs',
78+
text:
79+
'Demonstrates how to set up the Rotatable Crosshairs interactor style and SVG Widget',
80+
},
7581
{
7682
title: 'MPR Rotate Example',
7783
url: '/rotate',
@@ -143,8 +149,11 @@ function AppRouter() {
143149
const synced = () =>
144150
Example({ children: <VTKCornerstonePaintingSyncExample /> });
145151
const crosshairs = () => Example({ children: <VTKCrosshairsExample /> });
152+
const rotatableCrosshairs = () =>
153+
Example({ children: <VTKRotatableCrosshairsExample /> });
146154
const rotateMPR = () => Example({ children: <VTKMPRRotateExample /> });
147-
const volumeRendering = () => Example({ children: <VTKVolumeRenderingExample /> });
155+
const volumeRendering = () =>
156+
Example({ children: <VTKVolumeRenderingExample /> });
148157

149158
return (
150159
<Router>
@@ -155,6 +164,11 @@ function AppRouter() {
155164
<Route exact path="/painting" render={painting} />
156165
<Route exact path="/cornerstone-sync-painting" render={synced} />
157166
<Route exact path="/crosshairs" render={crosshairs} />
167+
<Route
168+
exact
169+
path="/rotatable-crosshairs"
170+
render={rotatableCrosshairs}
171+
/>
158172
<Route exact path="/rotate" render={rotateMPR} />
159173
<Route exact path="/volume-rendering" render={volumeRendering} />
160174
<Route exact path="/cornerstone-load-image-data" render={loadImage} />
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import React from 'react';
2+
import { Component } from 'react';
3+
import {
4+
View2D,
5+
getImageData,
6+
loadImageData,
7+
vtkSVGRotatableCrosshairsWidget,
8+
vtkInteractorStyleRotatableMPRCrosshairs,
9+
vtkInteractorStyleMPRWindowLevel,
10+
} from '@vtk-viewport';
11+
import { api as dicomwebClientApi } from 'dicomweb-client';
12+
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
13+
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';
14+
15+
const url = 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs';
16+
const studyInstanceUID =
17+
'1.3.6.1.4.1.14519.5.2.1.2744.7002.373729467545468642229382466905';
18+
const ctSeriesInstanceUID =
19+
'1.3.6.1.4.1.14519.5.2.1.2744.7002.182837959725425690842769990419';
20+
const searchInstanceOptions = {
21+
studyInstanceUID,
22+
};
23+
24+
function loadDataset(imageIds, displaySetInstanceUid) {
25+
const imageDataObject = getImageData(imageIds, displaySetInstanceUid);
26+
27+
loadImageData(imageDataObject);
28+
return imageDataObject;
29+
}
30+
31+
function createStudyImageIds(baseUrl, studySearchOptions) {
32+
const SOP_INSTANCE_UID = '00080018';
33+
const SERIES_INSTANCE_UID = '0020000E';
34+
35+
const client = new dicomwebClientApi.DICOMwebClient({ url });
36+
37+
return new Promise((resolve, reject) => {
38+
client.retrieveStudyMetadata(studySearchOptions).then(instances => {
39+
const imageIds = instances.map(metaData => {
40+
const imageId =
41+
`wadors:` +
42+
baseUrl +
43+
'/studies/' +
44+
studyInstanceUID +
45+
'/series/' +
46+
metaData[SERIES_INSTANCE_UID].Value[0] +
47+
'/instances/' +
48+
metaData[SOP_INSTANCE_UID].Value[0] +
49+
'/frames/1';
50+
51+
cornerstoneWADOImageLoader.wadors.metaDataManager.add(
52+
imageId,
53+
metaData
54+
);
55+
56+
return imageId;
57+
});
58+
59+
resolve(imageIds);
60+
}, reject);
61+
});
62+
}
63+
64+
class VTKRotatableCrosshairsExample extends Component {
65+
state = {
66+
volumes: [],
67+
displayCrosshairs: true,
68+
crosshairsTool: true,
69+
};
70+
71+
async componentDidMount() {
72+
this.apis = [];
73+
74+
const imageIds = await createStudyImageIds(url, searchInstanceOptions);
75+
76+
let ctImageIds = imageIds.filter(imageId =>
77+
imageId.includes(ctSeriesInstanceUID)
78+
);
79+
80+
const ctImageDataObject = loadDataset(ctImageIds, 'ctDisplaySet');
81+
82+
const onAllPixelDataInsertedCallback = () => {
83+
const ctImageData = ctImageDataObject.vtkImageData;
84+
85+
const range = ctImageData
86+
.getPointData()
87+
.getScalars()
88+
.getRange();
89+
90+
const mapper = vtkVolumeMapper.newInstance();
91+
const ctVol = vtkVolume.newInstance();
92+
const rgbTransferFunction = ctVol.getProperty().getRGBTransferFunction(0);
93+
94+
mapper.setInputData(ctImageData);
95+
mapper.setMaximumSamplesPerRay(2000);
96+
rgbTransferFunction.setRange(range[0], range[1]);
97+
ctVol.setMapper(mapper);
98+
99+
this.setState({
100+
volumes: [ctVol],
101+
});
102+
};
103+
104+
ctImageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback);
105+
}
106+
107+
storeApi = viewportIndex => {
108+
return api => {
109+
this.apis[viewportIndex] = api;
110+
111+
window.apis = this.apis;
112+
113+
const apis = this.apis;
114+
const renderWindow = api.genericRenderWindow.getRenderWindow();
115+
116+
// Add rotatable svg widget
117+
api.addSVGWidget(
118+
vtkSVGRotatableCrosshairsWidget.newInstance(),
119+
'rotatableCrosshairsWidget'
120+
);
121+
122+
const istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();
123+
124+
// // add istyle
125+
api.setInteractorStyle({
126+
istyle,
127+
configuration: { apis, apiIndex: viewportIndex },
128+
});
129+
130+
//api.setInteractorStyle({ istyle });
131+
132+
// set blend mode to MIP.
133+
const mapper = api.volumes[0].getMapper();
134+
if (mapper.setBlendModeToMaximumIntensity) {
135+
mapper.setBlendModeToMaximumIntensity();
136+
}
137+
138+
api.setSlabThickness(0.1);
139+
140+
renderWindow.render();
141+
142+
// Its up to the layout manager of an app to know how many viewports are being created.
143+
if (apis[0] && apis[1] && apis[2]) {
144+
const api = apis[0];
145+
146+
apis.forEach((api, index) => {
147+
api.svgWidgets.rotatableCrosshairsWidget.setApiIndex(index);
148+
api.svgWidgets.rotatableCrosshairsWidget.setApis(apis);
149+
});
150+
151+
api.svgWidgets.rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
152+
}
153+
};
154+
};
155+
156+
handleSlabThicknessChange(evt) {
157+
const value = evt.target.value;
158+
const valueInMM = value / 10;
159+
const apis = this.apis;
160+
161+
apis.forEach(api => {
162+
const renderWindow = api.genericRenderWindow.getRenderWindow();
163+
164+
api.setSlabThickness(valueInMM);
165+
renderWindow.render();
166+
});
167+
}
168+
169+
toggleTool = () => {
170+
let { crosshairsTool } = this.state;
171+
const apis = this.apis;
172+
173+
crosshairsTool = !crosshairsTool;
174+
175+
apis.forEach((api, apiIndex) => {
176+
let istyle;
177+
178+
if (crosshairsTool) {
179+
istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();
180+
} else {
181+
istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
182+
}
183+
184+
// // add istyle
185+
api.setInteractorStyle({
186+
istyle,
187+
configuration: { apis, apiIndex },
188+
});
189+
});
190+
191+
this.setState({ crosshairsTool });
192+
};
193+
194+
toggleCrosshairs = () => {
195+
const { displayCrosshairs } = this.state;
196+
const apis = this.apis;
197+
198+
const shouldDisplayCrosshairs = !displayCrosshairs;
199+
200+
apis.forEach(api => {
201+
const { svgWidgetManager, svgWidgets } = api;
202+
svgWidgets.rotatableCrosshairsWidget.setDisplay(shouldDisplayCrosshairs);
203+
204+
svgWidgetManager.render();
205+
});
206+
207+
this.setState({ displayCrosshairs: shouldDisplayCrosshairs });
208+
};
209+
210+
render() {
211+
if (!this.state.volumes || !this.state.volumes.length) {
212+
return <h4>Loading...</h4>;
213+
}
214+
215+
return (
216+
<>
217+
<div className="row">
218+
<div className="col-xs-4">
219+
<p>
220+
This example demonstrates how to use the Crosshairs manipulator.
221+
</p>
222+
<label htmlFor="set-slab-thickness">SlabThickness: </label>
223+
<input
224+
id="set-slab-thickness"
225+
type="range"
226+
name="points"
227+
min="1"
228+
max="5000"
229+
onChange={this.handleSlabThicknessChange.bind(this)}
230+
/>
231+
</div>
232+
<div className="col-xs-4">
233+
<p>Click bellow to toggle crosshairs on/off.</p>
234+
<button onClick={this.toggleCrosshairs}>
235+
{this.state.displayCrosshairs
236+
? 'Hide Crosshairs'
237+
: 'Show Crosshairs'}
238+
</button>
239+
<button onClick={this.toggleTool}>
240+
{this.state.crosshairsTool
241+
? 'Switch To WL/Zoom/Pan/Scroll'
242+
: 'Switch To Crosshairs'}
243+
</button>
244+
</div>
245+
</div>
246+
<div className="row">
247+
<div className="col-sm-4">
248+
<View2D
249+
volumes={this.state.volumes}
250+
onCreated={this.storeApi(0)}
251+
orientation={{ sliceNormal: [0, 1, 0], viewUp: [0, 0, 1] }}
252+
showRotation={true}
253+
/>
254+
</div>
255+
<div className="col-sm-4">
256+
<View2D
257+
volumes={this.state.volumes}
258+
onCreated={this.storeApi(1)}
259+
orientation={{ sliceNormal: [1, 0, 0], viewUp: [0, 0, 1] }}
260+
showRotation={true}
261+
/>
262+
</div>
263+
<div className="col-sm-4">
264+
<View2D
265+
volumes={this.state.volumes}
266+
onCreated={this.storeApi(2)}
267+
orientation={{ sliceNormal: [0, 0, 1], viewUp: [0, -1, 0] }}
268+
showRotation={true}
269+
/>
270+
</div>
271+
</div>
272+
</>
273+
);
274+
}
275+
}
276+
277+
export default VTKRotatableCrosshairsExample;

0 commit comments

Comments
 (0)