ResliceCursor Implementation inside glance
mayukhdeep12 opened this issue · 20 comments
I implemented the ResliceCursorWidget script and the html file inside glance, but I can’t figure out what the problem is with the code. I checked the console (browser) and the terminal, and both show no errors.
I believe the issue is with the ResliceWidget functions I’m using; they may be incorrect; I’ve checked the functions on vtk.js github, the Reslice class reference, and the documentation.
Could you please check the code and let me know what is wrong with it?
Here’s the code for the html and script files.
Template.html
<div>
<v-container :class="$style.container" class="black">
<v-layout wrap align-center>
<v-flex xs12>
<source-select
label="Target volume"
:filterFunc="resliceFilterImages"
bindToActiveSource
hideIfOneDataset
@input="setTargetVolume"
/>
</v-flex>
</v-layout>
</v-container>
<v-container :class="$style.container" class="black">
<v-layout wrap align-center>
<template v-if="enabled">
<v-btn text @click="disable">
<v-icon left>mdi-cursor</v-icon>
Hide
</v-btn>
</template>
<template v-else>
<v-btn text @click="enable" :disabled="!canReslice" class="white--text">
<v-icon left color="white">mdi-plus</v-icon>
Reslice
</v-btn>
</template>
<v-spacer />
<v-btn @click="reset" text class="white--text">
<v-icon left color="white">mdi-replay</v-icon>
Reset
</v-btn>
</v-layout>
</v-container>
</div>
Script.js
watch: {
enabled(enabled) {
console.log("Reslice Emabled working")
if (enabled) {
const resliceFilter = this.getCropFilter(this.targetVolume);
let resliceProxy = this.resliceProxy;
if (!resliceProxy) {
resliceProxy = this.$proxyManager
.getProxyInGroup('Widgets')
.find((w) => w.getProxyName() === 'ResliceCursor');
if (!resliceProxy) {
resliceProxy = this.$proxyManager.createProxy('Widgets', 'ResliceCursor');
}
this.widgetId = resliceProxy.getProxyId();
}
const widget = resliceProxy.getWidget();
const widgetState = resliceProxy.getWidgetState();
// widget.updateCameraPoints(false);
const imageData = this.targetVolume.getDataset();
widget.setImage(imageData);
widget.placeWidget(imageData.getBounds());
if(resliceFilter.isResetAvailable()) {
const state = widgetState.updateReslice();
state.setPlanes(resliceFilter.updateReslice());
}
const resliceState = widgetState.updateReslice();
this.stateSub.sub(
resliceState.onModified(() => {
const reslicer = resliceState.getPlanes();
resliceFilter.updateReslice(reslicer);
this.canReset = resliceFilter.isResetAvailable();
this.storeResliceState(this.targetVolumeId, reslicer);
})
);
resliceProxy.addToViews();
} else {
this.resliceProxy.removeFromViews();
this.$proxyManager.deleteProxy(this.resliceProxy);
this.widgetId = -1;
this.stateSub.unsub();
}
},
Do you see the reslice widget UI? Or is it that nothing shows up?
Nothing appears, the code i've shown in that enabled function isn't working, and it's not showing any errors
Any update?
Do you have the reslice widget added to the EditTools component as well? The enable/disable flow is as such:
- clicking on the enable button (or whatever button enables your widget) will cause your component to emit an "enable" event.
- The EditTools parent component sets the enabled tool based on the "enable" event.
- The "enabled" prop is set to true for the enabled widget.
What's your full script.js
? If you would rather not post it, do you have an enable()
method in the methods section?
My Reslice script.js is like this
import SourceSelect from 'glance/src/components/widgets/SourceSelect';
import { getCropFilter, makeSubManager } from 'glance/src/utils';
export default {
name: 'ResliceCursor',
components: {
SourceSelect,
},
props: ['enabled'],
data() {
return {
targetVolumeId: -1,
widgetId: -1,
canReset: false,
};
},
computed: {
targetVolume() {
return this.$proxyManager.getProxyById(this.targetVolumeId);
},
resliceProxy() {
return this.$proxyManager.getProxyById(this.widgetId);
},
canReslice() {
return this.targetVolumeId > -1;
},
...mapState('widgets', {
allResliceStates: 'resliceStates',
}),
},
watch: {
enabled(enabled) {
console.log("Reslice Emabled working");
// if (enabled) {
// const resliceFilter = this.getCropFilter(this.targetVolume);
// let resliceProxy = this.resliceProxy;
// if (!resliceProxy) {
// resliceProxy = this.$proxyManager
// .getProxyInGroup('Widgets')
// .find((w) => w.getProxyName() === 'ResliceCursor');
// if (!resliceProxy) {
// resliceProxy = this.$proxyManager.createProxy('Widgets', 'ResliceCursor');
// }
// this.widgetId = resliceProxy.getProxyId();
// }
// const widget = resliceProxy.getWidget();
// const widgetState = resliceProxy.getWidgetState();
// // widget.updateCameraPoints(false);
// const imageData = this.targetVolume.getDataset();
// widget.setImage(imageData);
// widget.placeWidget(imageData.getBounds());
// if(resliceFilter.isResetAvailable()) {
// const state = widgetState.updateReslice();
// state.setPlanes(resliceFilter.updateReslice());
// }
// resliceProxy.addToViews();
// } else {
// this.resliceProxy.removeFromViews();
// this.$proxyManager.deleteProxy(this.resliceProxy);
// this.widgetId = -1;
// this.stateSub.unsub();
// }
},
targetVolumeId(id) {
if (this.enabled) {
console.log("TargetVolume Called");
this.disable();
}
this.canReset = false;
if (id !== -1) {
const resliceFilter = this.getCropFilter(this.targetVolume);
this.canReset = resliceFilter.isResetAvailable();
}
},
},
mounted() {
this.stateSub = makeSubManager();
},
beforeDestroy() {
this.stateSub.unsub();
},
methods: {
resliceFilterImages(source) {
return (
source.getProxyName() === 'TrivialProducer' &&
source.getType() === 'vtkImageData'
);
},
getCropFilter(volProxy) {
return getCropFilter(this.$proxyManager, volProxy);
},
setTargetVolume(sourceId) {
this.targetVolumeId = sourceId;
},
enable() {
console.log("Reslice enable called");
this.$emit('enable', true);
},
disable() {
this.$emit('enable', false);
console.log("Reslice disable called");
},
reset() {
console.log("Reset Working");
if (this.targetVolume) {
const Rfilter = this.getCropFilter(this.targetVolume);
Rfilter.reset();
this.canReset = false;
if (this.resliceProxy) {
const state = this.resliceProxy.getWidgetState().updateReslice();
state.setPlanes(Rfilter.updateReslice());
}
}
},
storeResliceState(datasetId, data) {
this.setResliceState({ datasetId, data});
},
...mapActions('widgets', ['setResliceState']),
},
}
template.html
<div>
<v-container :class="$style.container" class="black">
<v-layout wrap align-center>
<v-flex xs12>
<source-select
label="Target volume"
:filterFunc="filterImages"
bindToActiveSource
hideIfOneDataset
@input="setTargetVolume"
/>
</v-flex>
</v-layout>
</v-container>
<v-container :class="$style.container" class="black">
<v-layout wrap align-center>
<template v-if="enabled">
<v-btn text @click="disable">
<v-icon left>mdi-crop-free</v-icon>
Hide
</v-btn>
</template>
<template v-else>
<v-btn text @click="enable" :disabled="!canCrop" class="white--text">
<v-icon left color="white">mdi-crop</v-icon>
Crop
</v-btn>
</template>
<v-spacer />
<v-btn @click="reset" text class="white--text">
<v-icon left color="white">mdi-replay</v-icon>
Reset
</v-btn>
</v-layout>
</v-container>
</div>
I've shared the Edit tools script in the previous reply.
So here is what I'm expecting:
- Clicking on the crop icon (since you left it as the crop icon) should call
enable()
, which should emit an "enable" event with "true". - EditTools listens for the enable event, and sets the current tool to be "resliceCursor"
- your component's watch on
enabled
should then trigger with "true".
Does clicking on the crop icon output anything to the console for you?
Clicking the crop icon is not showing console output
I've resolved the console issue with the enabled function, and now I'm attempting to put the reslice widget inside of the enabled function. However, I'm having trouble determining the proper function to add from reslice to the enabled function.
The enabled function should perform initialization steps for the reslice widget, in a similar manner that the ResliceCursorWidget example does. The goal is to configure the views (which can be done via this.$proxyManager.getViews()
) and add the reslice widget into them.
Okay, let me check, and if there are any problems, I'll let you know.
I initialized the reslice steps inside enabled functions, is that correct? Because there are no errors or output visible in the terminal or on output window only enalbed function console is working.
code for the enabled functions
watch: {
enabled(enabled) {
console.log("CropTwo Enabled Working");
if (enabled) {
const viewColors = [
[1, 0, 0], // sagittal
[0, 1, 0], // coronal
[0, 0, 1], // axial
[0.5, 0.5, 0.5], // 3D
];
const cropFilter = this.getCropFilter(this.targetVolume);
let cropProxy = this.cropProxy;
if (!cropProxy) {
cropProxy = this.$proxyManager
.getProxyInGroup('Widgets')
.find((w) => w.getProxyName() === 'CropTwo');
if (!cropProxy) {
cropProxy = this.$proxyManager.createProxy('Widgets', 'CropTwo');
}
this.widgetId = cropProxy.getProxyId();
}
const viewAttributes = [];
const widget = cropProxy.getWidget();
const widgetState = cropProxy.getWidgetState();
widgetState.setKeepOrthogonality(true);
widgetState.setOpacity(1);
widgetState.setSphereRadius(5);
widgetState.setLineThickness(2);
const imageData = this.targetVolume.getDataset();
widget.setImage(imageData);
widget.placeWidget(imageData.getBounds());
viewAttributes.forEach((obj, i) => {
obj.reslice.setInputData(image);
obj.renderer.addActor(obj.resliceActor);
view3D.renderer.addActor(obj.resliceActor);
obj.sphereActors.forEach((actor) => {
obj.renderer.addActor(actor);
view3D.renderer.addActor(actor);
});
const reslice = obj.reslice;
const viewType = xyzToViewType[i];
viewAttributes
.forEach((v) => {
v.widgetInstance.onInteractionEvent(
({ computeFocalPointOffset, canUpdateFocalPoint }) => {
const activeViewType = widget
.getWidgetState()
.getActiveViewType();
const keepFocalPointPosition =
activeViewType !== viewType && canUpdateFocalPoint;
updateReslice({
viewType,
reslice,
resetFocalPoint: false,
keepFocalPointPosition,
computeFocalPointOffset,
sphereSources: obj.sphereSources,
});
}
);
});
updateReslice({
viewType,
reslice,
resetFocalPoint: true,
keepFocalPointPosition: false,
computeFocalPointOffset: true,
sphereSources: obj.sphereSources,
});
});
const initialPlanesState = { ...widgetState.getPlanes() };
cropProxy.addToViews();
} else {
this.cropProxy.removeFromViews();
this.$proxyManager.deleteProxy(this.cropProxy);
this.widgetId = -1;
this.stateSub.unsub();
}
},
updateReslice(
interactionContext = {
viewType: '', // sagittal, coronal, axial
reslice: null, // reslice filter
resetFocalPoint: false, // Reset the focal point to the center of the display image
keepFocalPointPosition: false, // Defines if the focal point position is kepts (same display distance from reslice cursor center)
computeFocalPointOffset: false, // Defines if the display offset between reslice center and focal point has to be
// computed. If so, then this offset will be used to keep the focal point position during rotation.
spheres: null,
}
) {
console.log("CropTwo Update Reslice Called");
const obj = widget.updateReslicePlane( // update the reslice plane
interactionContext.reslice, // reslice filter
interactionContext.viewType // sagittal, coronal, axial
);
if (obj.modified) {
// Get returned modified from setter to know if we have to render
interactionContext.actor.setUserMatrix( // set the actor matrix
interactionContext.reslice.getResliceAxes() // get the reslice axes
);
interactionContext.sphereSources[0].setCenter(...obj.origin); // set the sphere center
interactionContext.sphereSources[1].setCenter(...obj.point1);
interactionContext.sphereSources[2].setCenter(...obj.point2);
}
widget.updateCameraPoints(
interactionContext.viewType, // sagittal, coronal, axial
interactionContext.resetFocalPoint, // Reset the focal point to the center of the display image
interactionContext.keepFocalPointPosition, // Defines if the focal point position is kepts (same display distance from reslice cursor center)
interactionContext.computeFocalPointOffset // Defines if the display offset between reslice center and focal point has to be
);
view3D.renderWindow.render(); // render
return obj.modified; // return the modified flag
},
targetVolumeId(id) {
if (this.enabled) {
console.log("TargetVolumeId")
this.disable();
}
// handle canReset flag
this.canReset = false;
if (id !== -1) {
const cropFilter = this.getCropFilter(this.targetVolume);
this.canReset = cropFilter.isResetAvailable();
}
},
},
And I can access the views you mentioned using the get crop filter. Is that correct? If not, what other options do I have?
utils.js
export function getCropFilter(pxm, proxy) {
// find 3d view
const view3d = pxm.getViews().find((v) => v.getProxyName() === 'View3D');
if (!view3d) {
throw new Error('Cannot find 3D view!');
}
// find volume rep
const volRep = pxm.getRepresentation(proxy, view3d);
if (!volRep || !volRep.getCropFilter) {
throw new Error('Cannot find the volume rep with a crop filter!');
}
return volRep.getCropFilter();
}
The last thing you should do at the end of enabled
is to call render. You can do this via this.$proxyManager.renderAllViews()
. If you want to get an individual view, you will have to go through this.$proxyManager.getViews()
, and then pick the view you are looking for based on view.getName()
and view.getClassName()
.
I'm attempting to create a new function called getcropfilterTwo for the crop widget, although the original function name is getcropfilter. With getcropfilter, I'm having no problems, but with getcropfilterTwo, I'm having a volRep error. Could you please help me with that?
Utils.js
export function getCropFilterTwo(pxm, proxy) {
// 3D View
const view3d = pxm.getViews().find((v) => v.getProxyName() === 'View3D');
if (!view3d) {
throw new Error('Cannot find 3D view!');
}
// find volume rep
const volRep = pxm.getRepresentation(proxy, view3d);
if (!volRep || !volRep.getCropFilterTwo) {
throw new Error('Cannot find the volume rep with a crop filter!');
}
return volRep.getCropFilter();
}
...
export default {
makeSubManager,
wrapSub,
wrapMutationAsAction,
remapIdKeys,
remapIdList,
createRepresentationInAllViews,
getCropFilter,
getCropFilterTwo,
};
vtkVolumeRepresentationProxy has a cropFilter defined, which is why getCropFilter
exists and works. getCropFilterTwo
doesn't exist on that representation, unless you added it to vtkCustomVolumeRepresentationProxy
.
Can you explain how we can write a widget volume representation in this?
You will need to look at vtkCustomVolumeRepresentationProxy
, as well as the upstream vtkVolumeRepresentationProxy
.
The idea is to add the reslice filter after the crop filter. Something like the following:
// model.sourceDependencies[0] should be a vtkCropFilter
resliceFilter.setInputConnection(model.sourceDependencies[0].getOutputPort())
// replace the crop filter with the reslice filter
model.sourceDependencies[0] = resliceFilter
I'll explain what I'm attempting to do and now the views is gone
- There is a crop filter in Glance that I'm trying to duplicate in a new Tools folder with a different function name and variables under the name CropToolTwo.
- I'm attempting to understand how the interaction between widgets in Glance works.
- I tried creating a custom proxy for the cropToolTwo inside the CustomVolumeReprestation Folder as you mentioned, but I am getting the following error instead.
Here is the code for CustomVolumeRepresentation
import macro from '@kitware/vtk.js/macro';
import vtkVolumeRepresentationProxy from '@kitware/vtk.js/Proxy/Representations/VolumeRepresentationProxy';
// ----------------------------------------------------------------------------
// vtkCustomVolumeRepresentationProxy methods
// ----------------------------------------------------------------------------
function vtkCustomVolumeRepresentationProxy(publicAPI, model) {
// Set our className
model.classHierarchy.push('vtkCustomVolumeRepresentationProxy');
//model.vtkImageCropFilter //should be a vtkCropFilter
cropFilter.setInputConnection(model.vtkImageCropFilter[0].getOutputPort())
// replace the crop filter with the reslice filter
model.vtkImageCropFilter[0] = cropFilter
// model.cropFilter = vtkImageCropFilter.newInstance();
// model.mapper.setInputConnection(model.cropFilter.getOutputPort());
// model.sourceDependencies.push(model.cropFilter);
const superClass = { ...publicAPI };
...
const DEFAULT_VALUES = {
sliceOpacity: 1,
sliceUseColorByForColor: false,
sliceUseColorByForOpacity: false,
};
// ----------------------------------------------------------------------------
export function extend(publicAPI, model, initialValues = {}) {
Object.assign(model, DEFAULT_VALUES, initialValues);
// Object methods
vtkVolumeRepresentationProxy.extend(publicAPI, model);
macro.setGet(publicAPI, model, [
'sliceOpacity',
'sliceUseColorByForColor',
'sliceUseColorByForOpacity',
'cropFilter',
]);
// Object specific methods
vtkCustomVolumeRepresentationProxy(publicAPI, model);
}
// ----------------------------------------------------------------------------
export const newInstance = macro.newInstance(
extend,
'vtkCustomVolumeRepresentationProxy'
);
// ----------------------------------------------------------------------------
export default { newInstance, extend };
Right now the VolumeRepresentationProxy has the following pipeline:
vtkImageData -> vtkCropFilter -> vtkMapper
^
vtkResliceFilter (or whatever image filter)
If you want to insert a new filter (e.g. vtkResliceFilter as shown above) after the crop filter, it should look like the following:
model.myFilter = vtkImageResliceFilter.newInstance();
model.myFilter.setInputConnection(model.cropFilter.getOutputPort())
model.mapper.setInputConnection(model.myFilter.getOutputPort())