feat: Add CS3D WSI and Video Viewports and add annotation navigation for MPR (#4182)
This commit is contained in:
1 parent
b12e0211d4
commit
7599ec9421
65 files changed
+781
-513
No files matched your search
@@ -1,121 +0,0 @@
|
||||
import OHIF from '@ohif/core';
|
||||
|
||||
const { utils } = OHIF;
|
||||
|
||||
const SOP_CLASS_UIDS = {
|
||||
VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE: '1.2.840.10008.5.1.4.1.1.77.1.6',
|
||||
};
|
||||
|
||||
const SOPClassHandlerId =
|
||||
'@ohif/extension-dicom-microscopy.sopClassHandlerModule.DicomMicroscopySopClassHandler';
|
||||
|
||||
function _getDisplaySetsFromSeries(instances, servicesManager, extensionManager) {
|
||||
// If the series has no instances, stop here
|
||||
if (!instances || !instances.length) {
|
||||
throw new Error('No instances were provided');
|
||||
}
|
||||
|
||||
const instance = instances[0];
|
||||
|
||||
let singleFrameInstance = instance;
|
||||
let currentFrames = +singleFrameInstance.NumberOfFrames || 1;
|
||||
for (const instanceI of instances) {
|
||||
const framesI = +instanceI.NumberOfFrames || 1;
|
||||
if (framesI < currentFrames) {
|
||||
singleFrameInstance = instanceI;
|
||||
currentFrames = framesI;
|
||||
}
|
||||
}
|
||||
let imageIdForThumbnail = null;
|
||||
if (singleFrameInstance) {
|
||||
if (currentFrames == 1) {
|
||||
// Not all DICOM server implementations support thumbnail service,
|
||||
// So if we have a single-frame image, we will prefer it.
|
||||
imageIdForThumbnail = singleFrameInstance.imageId;
|
||||
}
|
||||
if (!imageIdForThumbnail) {
|
||||
// use the thumbnail service provided by DICOM server
|
||||
const dataSource = extensionManager.getActiveDataSource()[0];
|
||||
imageIdForThumbnail = dataSource.getImageIdsForInstance({
|
||||
instance: singleFrameInstance,
|
||||
thumbnail: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const {
|
||||
FrameOfReferenceUID,
|
||||
SeriesDescription,
|
||||
ContentDate,
|
||||
ContentTime,
|
||||
SeriesNumber,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
SOPClassUID,
|
||||
} = instance;
|
||||
|
||||
instances = instances.map(inst => {
|
||||
// NOTE: According to DICOM standard a series should have a FrameOfReferenceUID
|
||||
// When the Microscopy file was built by certain tool from multiple image files,
|
||||
// each instance's FrameOfReferenceUID is sometimes different.
|
||||
// Even though this means the file was not well formatted DICOM VL Whole Slide Microscopy Image,
|
||||
// the case is so often, so let's override this value manually here.
|
||||
//
|
||||
// https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.4.html#sect_C.7.4.1.1.1
|
||||
|
||||
inst.FrameOfReferenceUID = instance.FrameOfReferenceUID;
|
||||
|
||||
return inst;
|
||||
});
|
||||
|
||||
const othersFrameOfReferenceUID = instances
|
||||
.filter(v => v)
|
||||
.map(inst => inst.FrameOfReferenceUID)
|
||||
.filter((value, index, array) => array.indexOf(value) === index);
|
||||
if (othersFrameOfReferenceUID.length > 1) {
|
||||
console.warn(
|
||||
'Expected FrameOfReferenceUID of difference instances within a series to be the same, found multiple different values',
|
||||
othersFrameOfReferenceUID
|
||||
);
|
||||
}
|
||||
|
||||
const displaySet = {
|
||||
plugin: 'microscopy',
|
||||
Modality: 'SM',
|
||||
altImageText: 'Microscopy',
|
||||
displaySetInstanceUID: utils.guid(),
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
StudyInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
SOPClassHandlerId,
|
||||
SOPClassUID,
|
||||
SeriesDescription: SeriesDescription || 'Microscopy Data',
|
||||
// Map ContentDate/Time to SeriesTime for series list sorting.
|
||||
SeriesDate: ContentDate,
|
||||
SeriesTime: ContentTime,
|
||||
SeriesNumber,
|
||||
firstInstance: singleFrameInstance, // top level instance in the image Pyramid
|
||||
instance,
|
||||
numImageFrames: 0,
|
||||
numInstances: 1,
|
||||
imageIdForThumbnail, // thumbnail image
|
||||
others: instances, // all other level instances in the image Pyramid
|
||||
othersFrameOfReferenceUID,
|
||||
};
|
||||
|
||||
return [displaySet];
|
||||
}
|
||||
|
||||
export default function getDicomMicroscopySopClassHandler({ servicesManager, extensionManager }) {
|
||||
const getDisplaySetsFromSeries = instances => {
|
||||
return _getDisplaySetsFromSeries(instances, servicesManager, extensionManager);
|
||||
};
|
||||
|
||||
return {
|
||||
name: 'DicomMicroscopySopClassHandler',
|
||||
sopClassUids: [SOP_CLASS_UIDS.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE_STORAGE],
|
||||
getDisplaySetsFromSeries,
|
||||
};
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { LoadingIndicatorProgress } from '@ohif/ui';
|
||||
import { cleanDenaturalizedDataset } from '@ohif/extension-default';
|
||||
|
||||
import './DicomMicroscopyViewport.css';
|
||||
import ViewportOverlay from './components/ViewportOverlay';
|
||||
import getDicomWebClient from './utils/dicomWebClient';
|
||||
import dcmjs from 'dcmjs';
|
||||
import cleanDenaturalizedDataset from './utils/cleanDenaturalizedDataset';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
|
||||
class DicomMicroscopyViewport extends Component {
|
||||
|
||||
@@ -5,7 +5,6 @@ import getCommandsModule from './getCommandsModule';
|
||||
import { Types } from '@ohif/core';
|
||||
|
||||
import { useViewportGrid } from '@ohif/ui';
|
||||
import getDicomMicroscopySopClassHandler from './DicomMicroscopySopClassHandler';
|
||||
import getDicomMicroscopySRSopClassHandler from './DicomMicroscopySRSopClassHandler';
|
||||
import MicroscopyService from './services/MicroscopyService';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
@@ -153,17 +152,8 @@ const extension: Types.Extensions.Extension = {
|
||||
* Each sop class handler is defined by a { name, sopClassUids, getDisplaySetsFromSeries}.
|
||||
* Examples include the default sop class handler provided by the default extension
|
||||
*/
|
||||
getSopClassHandlerModule({ servicesManager, commandsManager, extensionManager }) {
|
||||
return [
|
||||
getDicomMicroscopySopClassHandler({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
getDicomMicroscopySRSopClassHandler({
|
||||
servicesManager,
|
||||
extensionManager,
|
||||
}),
|
||||
];
|
||||
getSopClassHandlerModule(params) {
|
||||
return [getDicomMicroscopySRSopClassHandler(params)];
|
||||
},
|
||||
|
||||
getPanelModule,
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import { dicomWebUtils } from '@ohif/extension-default';
|
||||
|
||||
function isPrimitive(v: any) {
|
||||
return !(typeof v == 'object' || Array.isArray(v));
|
||||
}
|
||||
|
||||
const vrNumerics = [
|
||||
'DS',
|
||||
'FL',
|
||||
'FD',
|
||||
'IS',
|
||||
'OD',
|
||||
'OF',
|
||||
'OL',
|
||||
'OV',
|
||||
'SL',
|
||||
'SS',
|
||||
'SV',
|
||||
'UL',
|
||||
'US',
|
||||
'UV',
|
||||
];
|
||||
|
||||
/**
|
||||
* Specialized for DICOM JSON format dataset cleaning.
|
||||
* @param obj
|
||||
* @returns
|
||||
*/
|
||||
export default function cleanDenaturalizedDataset(
|
||||
obj: any,
|
||||
options: {
|
||||
StudyInstanceUID: string;
|
||||
SeriesInstanceUID: string;
|
||||
dataSourceConfig: unknown;
|
||||
}
|
||||
): any {
|
||||
if (Array.isArray(obj)) {
|
||||
const newAry = obj.map(o => (isPrimitive(o) ? o : cleanDenaturalizedDataset(o, options)));
|
||||
return newAry;
|
||||
} else if (isPrimitive(obj)) {
|
||||
return obj;
|
||||
} else {
|
||||
Object.keys(obj).forEach(key => {
|
||||
if (obj[key].Value === null && obj[key].vr) {
|
||||
delete obj[key].Value;
|
||||
} else if (Array.isArray(obj[key].Value) && obj[key].vr) {
|
||||
if (obj[key].Value.length === 1 && obj[key].Value[0].BulkDataURI) {
|
||||
dicomWebUtils.fixBulkDataURI(obj[key].Value[0], options, options.dataSourceConfig);
|
||||
|
||||
obj[key].BulkDataURI = obj[key].Value[0].BulkDataURI;
|
||||
|
||||
// prevent mixed-content blockage
|
||||
if (window.location.protocol === 'https:' && obj[key].BulkDataURI.startsWith('http:')) {
|
||||
obj[key].BulkDataURI = obj[key].BulkDataURI.replace('http:', 'https:');
|
||||
}
|
||||
delete obj[key].Value;
|
||||
} else if (vrNumerics.includes(obj[key].vr)) {
|
||||
obj[key].Value = obj[key].Value.map(v => +v);
|
||||
} else {
|
||||
obj[key].Value = obj[key].Value.map(entry => cleanDenaturalizedDataset(entry, options));
|
||||
}
|
||||
}
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user