Skip to content

Rotatable crosshairs #115

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Oct 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c229272
Initial commit
JamesAPetts Sep 23, 2020
375c291
Set up svg widget with colors + corner, add in apis.
JamesAPetts Sep 23, 2020
7927489
Line direction derived from other views.
JamesAPetts Sep 23, 2020
9700a5c
Correct crosshairs for obliques.
JamesAPetts Sep 23, 2020
19ffd53
Lines update on rotate
JamesAPetts Sep 23, 2020
6320047
Make logic for switching interaction within the tool.
JamesAPetts Sep 24, 2020
8b9b1a9
Refactor so we can move the line calculation out of the render functi…
JamesAPetts Sep 24, 2020
6209a9f
Move all calculation logic out of render loop and remove its dependen…
JamesAPetts Sep 24, 2020
885954c
WIP
JamesAPetts Sep 24, 2020
f0bf950
Add gap, prep for adding line interaction.
JamesAPetts Sep 25, 2020
c3758b1
Add calculation to grab line. When drag line, work out in-plane angle…
JamesAPetts Sep 25, 2020
084de26
WIP rotatable crosshairs, something wrong with rotation.
JamesAPetts Sep 25, 2020
be706da
Fix rotation, need to fix UI.
JamesAPetts Sep 28, 2020
c2e60c2
Fix UI grabbing.
JamesAPetts Sep 28, 2020
5d5a599
WIP
JamesAPetts Sep 28, 2020
1d5a3bc
complete MVP
JamesAPetts Sep 28, 2020
496bbb1
Rotation markets
JamesAPetts Sep 30, 2020
408af28
Remove unused code.
JamesAPetts Sep 30, 2020
a5eeb3d
Move rotate to handle and update UI to deal with this.
JamesAPetts Sep 30, 2020
5d3e98c
Drag lines.
JamesAPetts Sep 30, 2020
605fcd5
enable rotation to be optional.
JamesAPetts Oct 5, 2020
ef7cc53
feat: 🎸 Rotatable Crosshairs
JamesAPetts Oct 5, 2020
01f97bd
Merge branch 'master' into RotatableCrosshairs
JamesAPetts Oct 5, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions examples/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import VTKMPRPaintingExample from './VTKMPRPaintingExample.js';
import VTKCornerstonePaintingSyncExample from './VTKCornerstonePaintingSyncExample.js';
import VTKLoadImageDataExample from './VTKLoadImageDataExample.js';
import VTKCrosshairsExample from './VTKCrosshairsExample.js';
import VTKRotatableCrosshairsExample from './VTKRotatableCrosshairsExample.js';
import VTKMPRRotateExample from './VTKMPRRotateExample.js';
import VTKVolumeRenderingExample from './VTKVolumeRenderingExample.js';

Expand Down Expand Up @@ -51,8 +52,7 @@ function Index() {
{
title: 'Volume Rendering',
url: '/volume-rendering',
text:
'Demonstrates how to perform volume rendering for a CT volume.',
text: 'Demonstrates how to perform volume rendering for a CT volume.',
},
{
title: 'Image Segmentation via Paint Widget',
Expand All @@ -72,6 +72,12 @@ function Index() {
text:
'Demonstrates how to set up the Crosshairs interactor style and SVG Widget',
},
{
title: 'MPR Rotatable Crosshairs Example',
url: '/rotatable-crosshairs',
text:
'Demonstrates how to set up the Rotatable Crosshairs interactor style and SVG Widget',
},
{
title: 'MPR Rotate Example',
url: '/rotate',
Expand Down Expand Up @@ -143,8 +149,11 @@ function AppRouter() {
const synced = () =>
Example({ children: <VTKCornerstonePaintingSyncExample /> });
const crosshairs = () => Example({ children: <VTKCrosshairsExample /> });
const rotatableCrosshairs = () =>
Example({ children: <VTKRotatableCrosshairsExample /> });
const rotateMPR = () => Example({ children: <VTKMPRRotateExample /> });
const volumeRendering = () => Example({ children: <VTKVolumeRenderingExample /> });
const volumeRendering = () =>
Example({ children: <VTKVolumeRenderingExample /> });

return (
<Router>
Expand All @@ -155,6 +164,11 @@ function AppRouter() {
<Route exact path="/painting" render={painting} />
<Route exact path="/cornerstone-sync-painting" render={synced} />
<Route exact path="/crosshairs" render={crosshairs} />
<Route
exact
path="/rotatable-crosshairs"
render={rotatableCrosshairs}
/>
<Route exact path="/rotate" render={rotateMPR} />
<Route exact path="/volume-rendering" render={volumeRendering} />
<Route exact path="/cornerstone-load-image-data" render={loadImage} />
Expand Down
277 changes: 277 additions & 0 deletions examples/VTKRotatableCrosshairsExample.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
import React from 'react';
import { Component } from 'react';
import {
View2D,
getImageData,
loadImageData,
vtkSVGRotatableCrosshairsWidget,
vtkInteractorStyleRotatableMPRCrosshairs,
vtkInteractorStyleMPRWindowLevel,
} from '@vtk-viewport';
import { api as dicomwebClientApi } from 'dicomweb-client';
import vtkVolume from 'vtk.js/Sources/Rendering/Core/Volume';
import vtkVolumeMapper from 'vtk.js/Sources/Rendering/Core/VolumeMapper';

const url = 'https://server.dcmjs.org/dcm4chee-arc/aets/DCM4CHEE/rs';
const studyInstanceUID =
'1.3.6.1.4.1.14519.5.2.1.2744.7002.373729467545468642229382466905';
const ctSeriesInstanceUID =
'1.3.6.1.4.1.14519.5.2.1.2744.7002.182837959725425690842769990419';
const searchInstanceOptions = {
studyInstanceUID,
};

function loadDataset(imageIds, displaySetInstanceUid) {
const imageDataObject = getImageData(imageIds, displaySetInstanceUid);

loadImageData(imageDataObject);
return imageDataObject;
}

function createStudyImageIds(baseUrl, studySearchOptions) {
const SOP_INSTANCE_UID = '00080018';
const SERIES_INSTANCE_UID = '0020000E';

const client = new dicomwebClientApi.DICOMwebClient({ url });

return new Promise((resolve, reject) => {
client.retrieveStudyMetadata(studySearchOptions).then(instances => {
const imageIds = instances.map(metaData => {
const imageId =
`wadors:` +
baseUrl +
'/studies/' +
studyInstanceUID +
'/series/' +
metaData[SERIES_INSTANCE_UID].Value[0] +
'/instances/' +
metaData[SOP_INSTANCE_UID].Value[0] +
'/frames/1';

cornerstoneWADOImageLoader.wadors.metaDataManager.add(
imageId,
metaData
);

return imageId;
});

resolve(imageIds);
}, reject);
});
}

class VTKRotatableCrosshairsExample extends Component {
state = {
volumes: [],
displayCrosshairs: true,
crosshairsTool: true,
};

async componentDidMount() {
this.apis = [];

const imageIds = await createStudyImageIds(url, searchInstanceOptions);

let ctImageIds = imageIds.filter(imageId =>
imageId.includes(ctSeriesInstanceUID)
);

const ctImageDataObject = loadDataset(ctImageIds, 'ctDisplaySet');

const onAllPixelDataInsertedCallback = () => {
const ctImageData = ctImageDataObject.vtkImageData;

const range = ctImageData
.getPointData()
.getScalars()
.getRange();

const mapper = vtkVolumeMapper.newInstance();
const ctVol = vtkVolume.newInstance();
const rgbTransferFunction = ctVol.getProperty().getRGBTransferFunction(0);

mapper.setInputData(ctImageData);
mapper.setMaximumSamplesPerRay(2000);
rgbTransferFunction.setRange(range[0], range[1]);
ctVol.setMapper(mapper);

this.setState({
volumes: [ctVol],
});
};

ctImageDataObject.onAllPixelDataInserted(onAllPixelDataInsertedCallback);
}

storeApi = viewportIndex => {
return api => {
this.apis[viewportIndex] = api;

window.apis = this.apis;

const apis = this.apis;
const renderWindow = api.genericRenderWindow.getRenderWindow();

// Add rotatable svg widget
api.addSVGWidget(
vtkSVGRotatableCrosshairsWidget.newInstance(),
'rotatableCrosshairsWidget'
);

const istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();

// // add istyle
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex: viewportIndex },
});

//api.setInteractorStyle({ istyle });

// set blend mode to MIP.
const mapper = api.volumes[0].getMapper();
if (mapper.setBlendModeToMaximumIntensity) {
mapper.setBlendModeToMaximumIntensity();
}

api.setSlabThickness(0.1);

renderWindow.render();

// Its up to the layout manager of an app to know how many viewports are being created.
if (apis[0] && apis[1] && apis[2]) {
const api = apis[0];

apis.forEach((api, index) => {
api.svgWidgets.rotatableCrosshairsWidget.setApiIndex(index);
api.svgWidgets.rotatableCrosshairsWidget.setApis(apis);
});

api.svgWidgets.rotatableCrosshairsWidget.resetCrosshairs(apis, 0);
}
};
};

handleSlabThicknessChange(evt) {
const value = evt.target.value;
const valueInMM = value / 10;
const apis = this.apis;

apis.forEach(api => {
const renderWindow = api.genericRenderWindow.getRenderWindow();

api.setSlabThickness(valueInMM);
renderWindow.render();
});
}

toggleTool = () => {
let { crosshairsTool } = this.state;
const apis = this.apis;

crosshairsTool = !crosshairsTool;

apis.forEach((api, apiIndex) => {
let istyle;

if (crosshairsTool) {
istyle = vtkInteractorStyleRotatableMPRCrosshairs.newInstance();
} else {
istyle = vtkInteractorStyleMPRWindowLevel.newInstance();
}

// // add istyle
api.setInteractorStyle({
istyle,
configuration: { apis, apiIndex },
});
});

this.setState({ crosshairsTool });
};

toggleCrosshairs = () => {
const { displayCrosshairs } = this.state;
const apis = this.apis;

const shouldDisplayCrosshairs = !displayCrosshairs;

apis.forEach(api => {
const { svgWidgetManager, svgWidgets } = api;
svgWidgets.rotatableCrosshairsWidget.setDisplay(shouldDisplayCrosshairs);

svgWidgetManager.render();
});

this.setState({ displayCrosshairs: shouldDisplayCrosshairs });
};

render() {
if (!this.state.volumes || !this.state.volumes.length) {
return <h4>Loading...</h4>;
}

return (
<>
<div className="row">
<div className="col-xs-4">
<p>
This example demonstrates how to use the Crosshairs manipulator.
</p>
<label htmlFor="set-slab-thickness">SlabThickness: </label>
<input
id="set-slab-thickness"
type="range"
name="points"
min="1"
max="5000"
onChange={this.handleSlabThicknessChange.bind(this)}
/>
</div>
<div className="col-xs-4">
<p>Click bellow to toggle crosshairs on/off.</p>
<button onClick={this.toggleCrosshairs}>
{this.state.displayCrosshairs
? 'Hide Crosshairs'
: 'Show Crosshairs'}
</button>
<button onClick={this.toggleTool}>
{this.state.crosshairsTool
? 'Switch To WL/Zoom/Pan/Scroll'
: 'Switch To Crosshairs'}
</button>
</div>
</div>
<div className="row">
<div className="col-sm-4">
<View2D
volumes={this.state.volumes}
onCreated={this.storeApi(0)}
orientation={{ sliceNormal: [0, 1, 0], viewUp: [0, 0, 1] }}
showRotation={true}
/>
</div>
<div className="col-sm-4">
<View2D
volumes={this.state.volumes}
onCreated={this.storeApi(1)}
orientation={{ sliceNormal: [1, 0, 0], viewUp: [0, 0, 1] }}
showRotation={true}
/>
</div>
<div className="col-sm-4">
<View2D
volumes={this.state.volumes}
onCreated={this.storeApi(2)}
orientation={{ sliceNormal: [0, 0, 1], viewUp: [0, -1, 0] }}
showRotation={true}
/>
</div>
</div>
</>
);
}
}

export default VTKRotatableCrosshairsExample;
Loading