Kitware/glance

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:

  1. clicking on the enable button (or whatever button enables your widget) will cause your component to emit an "enable" event.
  2. The EditTools parent component sets the enabled tool based on the "enable" event.
  3. The "enabled" prop is set to true for the enabled widget.

Yes, ResliceWidget has been added to the EditTools.
Screenshot 2022-06-21 140956

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:

  1. Clicking on the crop icon (since you left it as the crop icon) should call enable(), which should emit an "enable" event with "true".
  2. EditTools listens for the enable event, and sets the current tool to be "resliceCursor"
  3. 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,
};

Screenshot 2022-07-08 142434

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

  1. 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.
  2. I'm attempting to understand how the interaction between widgets in Glance works.
  3. 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 };

Screenshot 2022-07-21 171307

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())