Skip to content
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

feat: add stack synchronization within or across studies #291

Merged
merged 2 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 16 additions & 7 deletions common/reviews/api/core.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

```ts

import type { mat4 } from 'gl-matrix';
import { mat4 } from 'gl-matrix';
import { vec3 } from 'gl-matrix';
import type vtkActor from '@kitware/vtk.js/Rendering/Core/Actor';
import type { vtkCamera } from '@kitware/vtk.js/Rendering/Core/Camera';
Expand Down Expand Up @@ -35,9 +35,7 @@ type ActorSliceRange = {
};

// @public (undocumented)
function addProvider(provider: (type: string, imageId: string) => {
any: any;
}, priority?: number): void;
function addProvider(provider: (type: string, query: any) => any, priority?: number): void;

// @public (undocumented)
export function addVolumesToViewports(renderingEngine: IRenderingEngine, volumeInputs: Array<IVolumeInput>, viewportIds: Array<string>, immediateRender?: boolean, suppressEvents?: boolean): Promise<void>;
Expand All @@ -57,6 +55,9 @@ enum BlendModes {
// @public (undocumented)
export const cache: Cache_2;

// @public (undocumented)
function calculateViewportsSpatialRegistration(viewport1: IStackViewport, viewport2: IStackViewport): void;

// @public (undocumented)
type CameraModifiedEvent = CustomEvent_2<CameraModifiedEventDetail>;

Expand Down Expand Up @@ -551,7 +552,7 @@ export function getEnabledElements(): IEnabledElement[];
function getImageSliceDataForVolumeViewport(viewport: IVolumeViewport): ImageSliceData;

// @public (undocumented)
function getMetaData(type: string, imageId: string): any;
function getMetaData(type: string, query: string): any;

// @public (undocumented)
function getMinMax(storedPixelData: number[]): {
Expand Down Expand Up @@ -1566,7 +1567,7 @@ function registerVolumeLoader(scheme: string, volumeLoader: Types.VolumeLoaderFn
function removeAllProviders(): void;

// @public (undocumented)
function removeProvider(provider: (type: string, imageId: string) => {
function removeProvider(provider: (type: string, query: any) => {
any: any;
}): void;

Expand Down Expand Up @@ -1696,6 +1697,12 @@ function snapFocalPointToSlice(focalPoint: Point3, position: Point3, sliceRange:
newPosition: Point3;
};

// @public (undocumented)
const spatialRegistrationMetadataProvider: {
add: (query: string[], payload: mat4) => void;
get: (type: string, query: string[]) => mat4;
};

// @public (undocumented)
type StackNewImageEvent = CustomEvent_2<StackNewImageEventDetail>;

Expand Down Expand Up @@ -1943,7 +1950,9 @@ declare namespace utilities {
snapFocalPointToSlice,
getImageSliceDataForVolumeViewport,
isImageActor,
getViewportsWithImageURI
getViewportsWithImageURI,
calculateViewportsSpatialRegistration,
spatialRegistrationMetadataProvider
}
}
export { utilities }
Expand Down
6 changes: 5 additions & 1 deletion common/reviews/api/tools.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,9 @@ function createLabelmapVolumeForViewport(input: {
// @public (undocumented)
function createMergedLabelmapForIndex(labelmaps: Array<Types_2.IImageVolume>, segmentIndex?: number, volumeId?: string): Types_2.IImageVolume;

// @public (undocumented)
function createStackImageSynchronizer(synchronizerName: string): Synchronizer;

// @public (undocumented)
function createSynchronizer(synchronizerId: string, eventName: string, eventHandler: ISynchronizerEventHandler): Synchronizer;

Expand Down Expand Up @@ -4101,7 +4104,8 @@ declare namespace synchronizers {
export {
createCameraPositionSynchronizer,
createVOISynchronizer,
createZoomPanSynchronizer
createZoomPanSynchronizer,
createStackImageSynchronizer
}
}
export { synchronizers }
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/RenderingEngine/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import RenderingEngine from './RenderingEngine';
import getRenderingEngine from './getRenderingEngine';
import VolumeViewport from './VolumeViewport';
import StackViewport from './StackViewport';
import {
createVolumeActor,
createVolumeMapper,
Expand All @@ -14,6 +15,7 @@ export {
createVolumeActor,
createVolumeMapper,
getOrCreateCanvas,
StackViewport,
};

export default RenderingEngine;
10 changes: 5 additions & 5 deletions packages/core/src/metaData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const providers = [];
* @category MetaData
*/
export function addProvider(
provider: (type: string, imageId: string) => { any },
provider: (type: string, query: any) => any,
priority = 0
): void {
let i;
Expand All @@ -38,7 +38,7 @@ export function addProvider(
* @category MetaData
*/
export function removeProvider(
provider: (type: string, imageId: string) => { any }
provider: (type: string, query: any) => { any }
): void {
for (let i = 0; i < providers.length; i++) {
if (providers[i].provider === provider) {
Expand All @@ -65,15 +65,15 @@ export function removeAllProviders(): void {
* until one responds
*
* @param type - The type of metadata requested from the metadata store
* @param imageId - The Cornerstone Image Object's imageId
* @param query - The query for the metadata store, often imageId
*
* @returns The metadata retrieved from the metadata store
* @category MetaData
*/
function getMetaData(type: string, imageId: string): any {
function getMetaData(type: string, query: string): any {
// Invoke each provider in priority order until one returns something
for (let i = 0; i < providers.length; i++) {
const result = providers[i].provider(type, imageId);
const result = providers[i].provider(type, query);

if (result !== undefined) {
return result;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { vec3, mat4 } from 'gl-matrix';
import { IStackViewport } from '../types';
import { StackViewport } from '../RenderingEngine';
import spatialRegistrationMetadataProvider from './spatialRegistrationMetadataProvider';
import { metaData } from '..';
import isEqual from './isEqual';

/**
* It calculates the registration matrix between two viewports (currently only
* translation is supported)
* If the viewports are in the same frame of reference, it will return early,
* but otherwise it will use the current image's metadata to calculate the
* translation between the two viewports and adds it to the spatialRegistrationModule
* metadata provider
*
*
* @param viewport1 - The first stack viewport
* @param viewport2 - The second stack viewport
*/
function calculateViewportsSpatialRegistration(
sedghi marked this conversation as resolved.
Show resolved Hide resolved
viewport1: IStackViewport,
viewport2: IStackViewport
): void {
if (
!(viewport1 instanceof StackViewport) ||
!(viewport2 instanceof StackViewport)
) {
throw new Error(
'calculateViewportsSpatialRegistration: Both viewports must be StackViewports, volume viewports are not supported yet'
);
}

const isSameFrameOfReference =
viewport1.getFrameOfReferenceUID() === viewport2.getFrameOfReferenceUID();

if (isSameFrameOfReference) {
return;
}

const imageId1 = viewport1.getCurrentImageId();
const imageId2 = viewport2.getCurrentImageId();

const imagePlaneModule1 = metaData.get('imagePlaneModule', imageId1);
const imagePlaneModule2 = metaData.get('imagePlaneModule', imageId2);

const isSameImagePlane =
imagePlaneModule1 &&
imagePlaneModule2 &&
isEqual(
imagePlaneModule1.imageOrientationPatient,
imagePlaneModule2.imageOrientationPatient
);

if (!isSameImagePlane) {
throw new Error(
'Viewport spatial registration only supported for same orientation (hence translation only) for now'
);
}

const imagePositionPatient1 = imagePlaneModule1.imagePositionPatient;
const imagePositionPatient2 = imagePlaneModule2.imagePositionPatient;

const translation = vec3.subtract(
vec3.create(),
imagePositionPatient1,
imagePositionPatient2
);

const mat = mat4.fromTranslation(mat4.create(), translation);

spatialRegistrationMetadataProvider.add([viewport1.id, viewport2.id], mat);
}

export default calculateViewportsSpatialRegistration;
4 changes: 4 additions & 0 deletions packages/core/src/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import snapFocalPointToSlice from './snapFocalPointToSlice';
import getImageSliceDataForVolumeViewport from './getImageSliceDataForVolumeViewport';
import isImageActor from './isImageActor';
import getViewportsWithImageURI from './getViewportsWithImageURI';
import calculateViewportsSpatialRegistration from './calculateViewportsSpatialRegistration';
import spatialRegistrationMetadataProvider from './spatialRegistrationMetadataProvider';

// name spaces
import * as planar from './planar';
Expand Down Expand Up @@ -64,4 +66,6 @@ export {
getImageSliceDataForVolumeViewport,
isImageActor,
getViewportsWithImageURI,
calculateViewportsSpatialRegistration,
spatialRegistrationMetadataProvider,
};
50 changes: 50 additions & 0 deletions packages/core/src/utilities/spatialRegistrationMetadataProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { mat4 } from 'gl-matrix';
import { addProvider } from '../metaData';

const state = {};

/**
* Simple metadataProvider object to store metadata for spatial registration module.
*/
const spatialRegistrationMetadataProvider = {
/* Adding a new entry to the state object. */
add: (query: string[], payload: mat4): void => {
const [viewportId1, viewportId2] = query;
const entryId = `${viewportId1}_${viewportId2}`;

if (!state[entryId]) {
state[entryId] = {};
}

state[entryId] = payload;
},

get: (type: string, query: string[]): mat4 => {
if (type !== 'spatialRegistrationModule') {
return;
}

const [viewportId1, viewportId2] = query;

// check both ways
const entryId = `${viewportId1}_${viewportId2}`;

if (state[entryId]) {
return state[entryId];
}

const entryIdReverse = `${viewportId2}_${viewportId1}`;

if (state[entryIdReverse]) {
return mat4.invert(mat4.create(), state[entryIdReverse]);
}
},
};

addProvider(
spatialRegistrationMetadataProvider.get.bind(
spatialRegistrationMetadataProvider
)
);

export default spatialRegistrationMetadataProvider;
24 changes: 22 additions & 2 deletions packages/tools/src/store/SynchronizerManager/Synchronizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,16 @@ class Synchronizer {
});
}

private fireEvent(sourceViewport: Types.IViewportId, sourceEvent: any): void {
private async fireEvent(
sourceViewport: Types.IViewportId,
sourceEvent: any
): Promise<void> {
if (this.isDisabled() || this._ignoreFiredEvents) {
return;
}

this._ignoreFiredEvents = true;

try {
for (let i = 0; i < this._targetViewports.length; i++) {
const targetViewport = this._targetViewports[i];
Expand All @@ -212,7 +216,23 @@ class Synchronizer {
continue;
}

this._eventHandler(this, sourceViewport, targetViewport, sourceEvent);
// Note: since the eventHandlers for synchronizers can be async (e.g. scroll,
// or new image set), we need to make sure that each handler has completed
// before we move to the next one. If the callback is async and we don't await
// here, then we will just finish all the handlers (while the async ones are
// still running), and later we set the ignoreFireEvents flag to false, which
// lets the targetViewports fire events again. This means that the async handlers
// will fire again, and we will have multiple handlers running at the same time.
// so just await here to make sure we don't have this unnecessary
// eventHandler running.
// Note: this might be very slow if the eventHandlers are very slow, so
// we might need to find a better way to do this.
await this._eventHandler(
this,
sourceViewport,
targetViewport,
sourceEvent
);
}
} catch (ex) {
console.warn(`Synchronizer, for: ${this._eventName}`, ex);
Expand Down
Loading