feat: add viewport overlays to microscopy mode (#4776)

This commit is contained in:
Pedro H. Köhler 2025-02-11 14:39:45 -03:00 committed by GitHub
parent 5043ea3260
commit 084a10f783
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 289 additions and 358 deletions

View File

@ -1,101 +1,50 @@
import React, { Component } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types';
import { cleanDenaturalizedDataset } from '@ohif/extension-default'; import { cleanDenaturalizedDataset } from '@ohif/extension-default';
import './DicomMicroscopyViewport.css'; import './DicomMicroscopyViewport.css';
import ViewportOverlay from './components/ViewportOverlay'; import ViewportOverlay from './components/ViewportOverlay';
import getDicomWebClient from './utils/dicomWebClient'; import getDicomWebClient from './utils/dicomWebClient';
import dcmjs from 'dcmjs'; import dcmjs from 'dcmjs';
import MicroscopyService from './services/MicroscopyService'; import { useSystem } from '@ohif/core';
import { CustomizationService } from '@ohif/core';
class DicomMicroscopyViewport extends Component { function DicomMicroscopyViewport({
state = { activeViewportId,
error: null as any, setViewportActive,
isLoaded: false, displaySets,
}; viewportId,
dataSource,
resizeRef,
}: {
activeViewportId: string;
setViewportActive: Function;
displaySets: any[];
viewportId: string;
dataSource: any;
resizeRef: any;
}) {
const { servicesManager, extensionManager } = useSystem();
const [isLoaded, setIsLoaded] = useState(false);
const [viewer, setViewer] = useState(null);
const [managedViewer, setManagedViewer] = useState(null);
const overlayElement = useRef();
const container = useRef();
const { microscopyService, customizationService } = servicesManager.services;
microscopyService: MicroscopyService; const overlayData = customizationService.getCustomization('microscopyViewport.overlay');
customizationService: CustomizationService;
viewer: any = null; // dicom-microscopy-viewer instance
managedViewer: any = null; // managed wrapper of microscopy-dicom extension
container = React.createRef();
overlayElement = React.createRef();
constructor(props: any) {
super(props);
const { microscopyService, customizationService } = this.props.servicesManager.services;
this.microscopyService = microscopyService;
this.customizationService = customizationService;
}
static propTypes = {
viewportData: PropTypes.object,
activeViewportId: PropTypes.string,
setViewportActive: PropTypes.func,
// props from OHIF Viewport Grid
displaySets: PropTypes.array,
viewportId: PropTypes.string,
viewportLabel: PropTypes.string,
dataSource: PropTypes.object,
viewportOptions: PropTypes.object,
displaySetOptions: PropTypes.array,
// other props from wrapping component
servicesManager: PropTypes.object,
extensionManager: PropTypes.object,
commandsManager: PropTypes.object,
resizeRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.any })]),
};
/**
* Get the nearest ROI from the mouse click point
*
* @param event
* @param autoselect
* @returns
*/
getNearbyROI(event: Event, autoselect = true) {
const symbols = Object.getOwnPropertySymbols(this.viewer);
const _drawingSource = symbols.find(p => p.description === 'drawingSource');
const _pyramid = symbols.find(p => p.description === 'pyramid');
const _map = symbols.find(p => p.description === 'map');
const _affine = symbols.find(p => p.description === 'affine');
const feature = this.viewer[_drawingSource].getClosestFeatureToCoordinate(
this.viewer[_map].getEventCoordinate(event)
);
if (!feature) {
return null;
}
const roiAnnotation = this.viewer._getROIFromFeature(
feature,
this.viewer[_pyramid].metadata,
this.viewer[_affine]
);
if (roiAnnotation && autoselect) {
this.microscopyService.selectAnnotation(roiAnnotation);
}
return roiAnnotation;
}
// install the microscopy renderer into the web page. // install the microscopy renderer into the web page.
// you should only do this once. // you should only do this once.
async installOpenLayersRenderer(container, displaySet) { const installOpenLayersRenderer = useCallback(
async (container, displaySet) => {
const loadViewer = async metadata => { const loadViewer = async metadata => {
const dicomMicroscopyModule = await this.microscopyService.importDicomMicroscopyViewer(); const dicomMicroscopyModule = await microscopyService.importDicomMicroscopyViewer();
const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule; const { viewer: DicomMicroscopyViewer, metadata: metadataUtils } = dicomMicroscopyModule;
const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer; const microscopyViewer = DicomMicroscopyViewer.VolumeImageViewer;
const client = getDicomWebClient({ const client = getDicomWebClient({
extensionManager: this.props.extensionManager, extensionManager,
servicesManager: this.props.servicesManager, servicesManager,
}); });
// Parse, format, and filter metadata // Parse, format, and filter metadata
@ -152,7 +101,7 @@ class DicomMicroscopyViewport extends Component {
{ {
StudyInstanceUID: m.StudyInstanceUID, StudyInstanceUID: m.StudyInstanceUID,
SeriesInstanceUID: m.SeriesInstanceUID, SeriesInstanceUID: m.SeriesInstanceUID,
dataSourceConfig: this.props.dataSource.getConfig(), dataSourceConfig: dataSource.getConfig(),
} }
); );
if (!inst['00480105']) { if (!inst['00480105']) {
@ -188,36 +137,39 @@ class DicomMicroscopyViewport extends Component {
controls: ['overview', 'position'], controls: ['overview', 'position'],
}; };
this.viewer = new microscopyViewer(options); const viewer = new microscopyViewer(options);
if (this.overlayElement && this.overlayElement.current && this.viewer.addViewportOverlay) { if (overlayElement && overlayElement.current && viewer.addViewportOverlay) {
this.viewer.addViewportOverlay({ viewer.addViewportOverlay({
element: this.overlayElement.current, element: overlayElement.current,
coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not. coordinates: [0, 0], // TODO: dicom-microscopy-viewer documentation says this can be false to be automatically, but it is not.
navigate: true, navigate: true,
className: 'OpenLayersOverlay', className: 'OpenLayersOverlay',
}); });
} }
this.viewer.render({ container }); viewer.render({ container });
const { StudyInstanceUID, SeriesInstanceUID } = displaySet; const { StudyInstanceUID, SeriesInstanceUID } = displaySet;
this.managedViewer = this.microscopyService.addViewer( const managedViewer = microscopyService.addViewer(
this.viewer, viewer,
this.props.viewportId, viewportId,
container, container,
StudyInstanceUID, StudyInstanceUID,
SeriesInstanceUID SeriesInstanceUID
); );
this.managedViewer.addContextMenuCallback((event: Event) => { managedViewer.addContextMenuCallback((event: Event) => {
// TODO: refactor this after Bill's changes on ContextMenu feature get merged // TODO: refactor this after Bill's changes on ContextMenu feature get merged
// const roiAnnotationNearBy = this.getNearbyROI(event); // const roiAnnotationNearBy = this.getNearbyROI(event);
}); });
setViewer(viewer);
setManagedViewer(managedViewer);
}; };
this.microscopyService.clearAnnotations(); microscopyService.clearAnnotations();
let smDisplaySet = displaySet; let smDisplaySet = displaySet;
if (displaySet.Modality === 'SR') { if (displaySet.Modality === 'SR') {
@ -231,49 +183,39 @@ class DicomMicroscopyViewport extends Component {
if (displaySet.Modality === 'SR') { if (displaySet.Modality === 'SR') {
displaySet.load(smDisplaySet); displaySet.load(smDisplaySet);
} }
} },
[dataSource, extensionManager, microscopyService, servicesManager, viewportId]
);
componentDidMount() { useEffect(() => {
const { displaySets, viewportOptions } = this.props;
// Todo-rename: this is always getting the 0
const displaySet = displaySets[0]; const displaySet = displaySets[0];
this.installOpenLayersRenderer(this.container.current, displaySet).then(() => { installOpenLayersRenderer(container.current, displaySet).then(() => {
this.setState({ isLoaded: true }); setIsLoaded(true);
}); });
}
componentDidUpdate(prevProps: Readonly<{}>, prevState: Readonly<{}>, snapshot?: any): void { return () => {
if (this.managedViewer && prevProps.displaySets !== this.props.displaySets) { if (viewer) {
const { displaySets } = this.props; microscopyService.removeViewer(viewer);
}
};
}, []);
useEffect(() => {
const displaySet = displaySets[0]; const displaySet = displaySets[0];
this.microscopyService.clearAnnotations(); microscopyService.clearAnnotations();
// loading SR // loading SR
if (displaySet.Modality === 'SR') { if (displaySet.Modality === 'SR') {
const referencedDisplaySet = displaySet.getSourceDisplaySet(); const referencedDisplaySet = displaySet.getSourceDisplaySet();
displaySet.load(referencedDisplaySet); displaySet.load(referencedDisplaySet);
} }
} }, [managedViewer, displaySets, microscopyService]);
}
componentWillUnmount() {
this.microscopyService.removeViewer(this.viewer);
}
setViewportActiveHandler = () => {
const { setViewportActive, viewportId, activeViewportId } = this.props;
if (viewportId !== activeViewportId) {
setViewportActive(viewportId);
}
};
render() {
const style = { width: '100%', height: '100%' }; const style = { width: '100%', height: '100%' };
const displaySet = this.props.displaySets[0]; const displaySet = displaySets[0];
const firstInstance = displaySet.firstInstance || displaySet.instance; const firstInstance = displaySet.firstInstance || displaySet.instance;
const LoadingIndicatorProgress = this.customizationService.getCustomization( const LoadingIndicatorProgress = customizationService.getCustomization(
'ui.loadingIndicatorProgress' 'ui.loadingIndicatorProgress'
); );
@ -281,13 +223,18 @@ class DicomMicroscopyViewport extends Component {
<div <div
className={'DicomMicroscopyViewer'} className={'DicomMicroscopyViewer'}
style={style} style={style}
onClick={this.setViewportActiveHandler} onClick={() => {
if (viewportId !== activeViewportId) {
setViewportActive(viewportId);
}
}}
> >
<div style={{ ...style, display: 'none' }}> <div style={{ ...style, display: 'none' }}>
<div style={{ ...style }} ref={this.overlayElement}> <div style={{ ...style }} ref={overlayElement}>
<div style={{ position: 'relative', height: '100%', width: '100%' }}> <div style={{ position: 'relative', height: '100%', width: '100%' }}>
{displaySet && firstInstance.imageId && ( {displaySet && firstInstance.imageId && (
<ViewportOverlay <ViewportOverlay
overlayData={overlayData}
displaySet={displaySet} displaySet={displaySet}
instance={displaySet.instance} instance={displaySet.instance}
metadata={displaySet.metadata} metadata={displaySet.metadata}
@ -296,23 +243,16 @@ class DicomMicroscopyViewport extends Component {
</div> </div>
</div> </div>
</div> </div>
{this.state.error ? (
<h2>{JSON.stringify(this.state.error)}</h2>
) : (
<div <div
style={style} style={style}
ref={(ref: any) => { ref={(ref: any) => {
this.container.current = ref; container.current = ref;
this.props.resizeRef.current = ref; resizeRef.current = ref;
}} }}
/> />
)} {isLoaded ? null : <LoadingIndicatorProgress className={'h-full w-full bg-black'} />}
{this.state.isLoaded ? null : (
<LoadingIndicatorProgress className={'h-full w-full bg-black'} />
)}
</div> </div>
); );
} }
}
export default DicomMicroscopyViewport; export default DicomMicroscopyViewport;

View File

@ -24,20 +24,18 @@ interface OverlayItem {
* @param {*} extensionManager is used to load the image data. * @param {*} extensionManager is used to load the image data.
* @returns * @returns
*/ */
export const generateFromConfig = ({ export const generateFromConfig = ({ config, overlayData, ...props }) => {
const {
topLeft = [], topLeft = [],
topRight = [], topRight = [],
bottomLeft = [], bottomLeft = [],
bottomRight = [], bottomRight = [],
itemGenerator = () => {},
}: { }: {
topLeft?: OverlayItem[]; topLeft?: OverlayItem[];
topRight?: OverlayItem[]; topRight?: OverlayItem[];
bottomLeft?: OverlayItem[]; bottomLeft?: OverlayItem[];
bottomRight?: OverlayItem[]; bottomRight?: OverlayItem[];
itemGenerator?: (props: any) => any; } = overlayData ?? {};
}) => {
return (props: any) => {
const topLeftClass = 'top-viewport left-viewport text-primary-light'; const topLeftClass = 'top-viewport left-viewport text-primary-light';
const topRightClass = 'top-viewport right-viewport-scrollbar text-primary-light'; const topRightClass = 'top-viewport right-viewport-scrollbar text-primary-light';
const bottomRightClass = 'bottom-viewport right-viewport-scrollbar text-primary-light'; const bottomRightClass = 'bottom-viewport right-viewport-scrollbar text-primary-light';
@ -52,10 +50,7 @@ export const generateFromConfig = ({
</div> </div>
)} )}
{topRight && topRight.length > 0 && ( {topRight && topRight.length > 0 && (
<div <div data-cy={'viewport-overlay-top-right'} className={classnames(overlay, topRightClass)}>
data-cy={'viewport-overlay-top-right'}
className={classnames(overlay, topRightClass)}
>
{listComponentGenerator({ {listComponentGenerator({
...props, ...props,
list: topRight, list: topRight,
@ -90,7 +85,6 @@ export const generateFromConfig = ({
</> </>
); );
}; };
};
const itemGenerator = (props: any) => { const itemGenerator = (props: any) => {
const { item } = props; const { item } = props;
@ -123,4 +117,4 @@ const itemGenerator = (props: any) => {
); );
}; };
export default generateFromConfig({}); export default generateFromConfig;

View File

@ -83,9 +83,6 @@ const extension: Types.Extensions.Extension = {
return ( return (
<MicroscopyViewport <MicroscopyViewport
key={displaySetsKey} key={displaySetsKey}
servicesManager={servicesManager}
extensionManager={extensionManager}
commandsManager={commandsManager}
activeViewportId={activeViewportId} activeViewportId={activeViewportId}
setViewportActive={(viewportId: string) => { setViewportActive={(viewportId: string) => {
viewportGridService.setActiveViewportId(viewportId); viewportGridService.setActiveViewportId(viewportId);

View File

@ -14,7 +14,7 @@ interface SystemContextProviderProps {
const systemContext = createContext(null); const systemContext = createContext(null);
const { Provider } = systemContext; const { Provider } = systemContext;
export const useSystem = () => useContext(systemContext); export const useSystem = () => useContext<SystemContextProviderProps>(systemContext);
export function SystemContextProvider({ export function SystemContextProvider({
children, children,