feat: cornerstone3D stack and volume viewports (#2787)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: cs3d tools and toolGroups (#20)

* add more tools to work with cs3d mode

* fix hotkeys

* add stack manager usage for stach viewports

* add image scrollbar

* wip viewport overlay

* fix toAnnotation schema for tools

* fix the unnecessary size change that triggered resize

* hanging protocol improvement to allow unmatched errors

* study description matching for hanging protocol

* fix the displaysetOptions to work

* fix handle the active tool when a new viewport is added

* fix separate toolGroups for mode

* apply review comments

* apply review comments

* yarn lock

* feat: overlay component (#21)

* fix default displayset options

* add viewport overlay

* apply review comments

* feat: loading and orientation indicators (#22)

* add loading indicator

* add orientation marker initial work

* apply review comments

* fix: orientation markers (#23)

* finished the orientation markers

* fix various broken cypress tests

* apply review comments

* update yarn lock

* feat: re-working measurement tracking mode with cornerstone3d (#2805)

* feat: cs3d working stack viewport and tools (#19)

* Squashed everything

* fix weird react issue

* fix eslint / prettier stuff

* thumbnails work now with cpu rendering

* feat: use new loadImageToCanvas in cs3d

* make jump to measurement work

* fix active thumbnail

* fix measurement delete

* fix window level presets

* remove segmentation and sync groups for now

* fix the dicom pdf and dicom video

* fix cornerstone window assignment for cypress

* apply review comments

Co-authored-by: Erik <erik.sweed@gmail.com>

* feat: Add Measurement tracking mode with cs3D (#2789)

* feat: first render for cornerstone3d tracked viewport

* make tool active work

* wip for SR extension

* renamed dicom sr to cornerstone dicom sr

* remove cornerstone from dicom pdf and video

* move dicom sr logic to sr extension

* feat: Add hydration for the length tool

* fix SR display tool for length using cs3d

* fix default config

* fix: various bugs with sr viewport and tracking

* fix promptying to continue tracking for when SR is created

* feat: add keep trackign of unique identifiers

* fix hydration for same imageIds

* feat: Add SR toolGroup creation on modeEnter

* feat: remove the need for separate mapper for SR hydration

* add SR display for ellipse

* handle hydration of elliptical ROI tool

* remove cornerstone extension

* add arrow mapping

* feat: Add ArrowAnnotate SR display and hydration

* apply review comments

* apply review comments

* move viewport labels to the viewportData

* apply review comments

* fix: integration cypress tests with cornerstone3D and add CINE tool (#2795)

* fix: integration cypress tests with cornerstone3D

* revert to addOrUpdate as it makes more sense

* fix local drag and drop

* fix tests

* move dicomLoaderService to cornerstone extension

* fix various import bugs

* fix bug for local PT series

* fix various unit tests

* bump cs3d versions

* add angle and magnify tool

* bump dependencies to avoid broken peerDeps

* feat: add initial work for capture using cs3d

* feat: show annotations on the image capture

* feat: add svg layer export

* feat: Add CINE Tool

* feat: remove unnecessary viewport rendering for cine state changes

Co-authored-by: Erik Ziegler <erik.sweed@gmail.com>

* docs: modify and improve documentation (#2800)

* cleanup docs versionings

* feat: Add all version explanations

* version docs for 3.0

* wip for changing docs

* wip for updated docs

* add utility module documentation

* fix demo with nohoisting of history

* add slides and video to resources

* apply review comments

* fix: drag and drop SR into SR viewport (#2803)

* update yarn lock

Co-authored-by: Erik <erik.sweed@gmail.com>

* update pathnames to match v3-stable

* update the e2e pathname

* fix: various bugs with regard to tracking workflow (#2811)

* fix: various issues with measurement panel

* fix: update default tool style for annotations

* fix: annotatoin label getting removed

* feat: Add backward compatibility for SR hydration with legacy cornerstone

* fix: cursors and ellipse ROI max style

* fix: ArrowAnnotate SRDisplay

* apply review comments

* bump package versions

* fix: bugin rehydration of SR

* fix: e2e tests

* fix active viewport thickness and arrowTool ui

* add readme for measurement tracking

* use uploaded image for readme

* add back images

* try to fix e2e test

* fix: window level presets hotkeys

* Update README.md

* update yarn lock

* feat: volume api and TMTV mode (#2817)

* feat: volumeAPI and TMTV mode

* feat: use cs3d cache service to obtain viewportData

* wip for volume api

* wip: fusion viewport

* feat: add blend mode option

* wip for image scrollbar

* fix drag and drop thumbnail

* wip for image scrollbar for voluems

* fix: element mismatch bug for scroll

* feat: Add image scrollbar to volumes

* feat: add syncGroups to volume api

* feat: add tmtv mode initial setup

* feat: add initial image options for the stack viewports

* feat: Add custom load strategy for volume viewports via HP

* apply review comments

fix: Jump presets cs3d (#2812)

* feat: Add JumpPreset to OHIF for Cornerstone3D

* fix: accessing viewport service from servicesManager

feat: volume API and TMTV mode (#2814)

* fix: do not display overlays on mip viewports

* feat: add optional disableCommands for toggle buttons

* fix: toggleCrossharis

* feat: config the crosshairs

* feat: add PetSUV Panel for changing metadata

* feat: initial work for the rectangleROIThreshold panel

* wip: measurement service

* roi threshold working

* feat: Add displayText to segmentations

* feat: add remove segmentation

* feat: add csv export

* feat: add RT export for annotations in tmtv mode

* fix: fusion to use pt in tmtv mode and measuremet mappings

* fix: various bugs

* apply review comments

* apply review comments

* feat: add fusion color maps

* add readme to tmtv mode

* Update README.md

* fix: try to fix build

* fix unit tests

* Update README.md

* feat: add about to the panel

* fix: changing strategy in roi panel

* apply review comments

* update package versions

* wip for stackPrefetch

* fix: cornerstone3d hydration and renaming (#2818)

* update readme

* renamed cornerstone extension

* wip for renaming cornerstone3D variables

* wip for renaming cornerstone3D variables

* wip for fixing bugs for SR viewport

* fix: jumpToMeasurement and initial label after hydration

* fix: fileName capitalization

* feat: use the new prefetch stack in the cs3d (#2820)

* feat: use the new prefetch stack in the cs3d

* use viewport scroll api for stack viewport

* fix cine stop when scrollbar changes

* feat: use new prefetch events

* fix loading state to not show repeatedly

* fix: various bugs for tmtv mode thresholding and new icons (#2823)

* feat: make tmtv mode available in worklist

* feat: add new icons for tmtv mode

* feat: add fusion color icon

* fix: parallel scale calculation

* fix: bump Cornerstone to get large image support working

* fix: Fix issues with CPU viewport flipping, including config files

* fix: bump cornerstone to fix magnify tool

* fix: Bump Cornerstone version to fix resetCamera issue in StackViewport

* ci: Add _headers file to enable CORS headers for Netlify Drag/Drop deploys

* fix: WADO-URI was not working. PET Metadata was coming from the wrong place. Fixed some minor React errors

* feat(OHIF):Allow modes and extensions to be added after commpile time. (#2838)

Also works with the compile time add that the existing cli uses, so that both build types work.
Eric and I agreed this doesn't change existing functionality, but is almost entirely build issues/fixes.

* bump: dependency versions to fix hydration bugs (#2848)

* bump: dcmjs version to fix hydration bugs

* try to fix tests

* bump dependency versions

Co-authored-by: Alireza <ar.sedghi@gmail.com>
Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
This commit is contained in:
authored and GitHub committed 2022-07-27 12:39:04 -04:00
1 parent 86dcaf6293
commit bd1c38c00c
668 files changed
+41669 -13410

No files matched your search

@@ -1,4 +1,12 @@
import { api } from 'dicomweb-client';
import {
DicomMetadataStore,
IWebApiDataSource,
utils,
errorHandler,
classes,
} from '@ohif/core';
import {
mapParams,
search as qidoSearch,
@@ -7,12 +15,6 @@ import {
processSeriesResults,
} from './qido.js';
import dcm4cheeReject from './dcm4cheeReject';
import {
DicomMetadataStore,
IWebApiDataSource,
utils,
errorHandler,
} from '@ohif/core';
import getImageId from './utils/getImageId';
import dcmjs from 'dcmjs';
@@ -31,6 +33,8 @@ const ImplementationClassUID =
const ImplementationVersionName = 'OHIF-VIEWER-2.0.0';
const EXPLICIT_VR_LITTLE_ENDIAN = '1.2.840.10008.1.2.1';
const metadataProvider = classes.MetadataProvider;
/**
*
* @param {string} name - Data source name
@@ -94,7 +98,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
query: {
studies: {
mapParams: mapParams.bind(),
search: async function (origParams) {
search: async function(origParams) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
@@ -119,7 +123,7 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
},
series: {
// mapParams: mapParams.bind(),
search: async function (studyInstanceUid) {
search: async function(studyInstanceUid) {
const headers = UserAuthenticationService.getAuthorizationHeader();
if (headers) {
qidoDicomWebClient.headers = headers;
@@ -163,13 +167,13 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
* @returns an absolute URL to the resource, if the absolute URL can be retrieved as singlepart,
* or is already retrieved, or a promise to a URL for such use if a BulkDataURI
*/
directURL: (params) => {
directURL: params => {
const {
instance,
tag = "PixelData",
defaultPath = "/pixeldata",
defaultType = "video/mp4",
singlepart: fetchPart = "video",
tag = 'PixelData',
defaultPath = '/pixeldata',
defaultType = 'video/mp4',
singlepart: fetchPart = 'video',
} = params;
const value = instance[tag];
if (!value) return undefined;
@@ -180,10 +184,15 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
value.DirectRetrieveURL = URL.createObjectURL(blob);
return value.DirectRetrieveURL;
}
if (!singlepart || singlepart !== true && singlepart.indexOf(fetchPart) === -1) {
if (
!singlepart ||
(singlepart !== true && singlepart.indexOf(fetchPart) === -1)
) {
if (value.retrieveBulkData) {
return value.retrieveBulkData().then(arr => {
value.DirectRetrieveURL = URL.createObjectURL(new Blob([arr], { type: defaultType }));
value.DirectRetrieveURL = URL.createObjectURL(
new Blob([arr], { type: defaultType })
);
return value.DirectRetrieveURL;
});
}
@@ -191,7 +200,11 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
return undefined;
}
const { StudyInstanceUID, SeriesInstanceUID, SOPInstanceUID } = instance;
const {
StudyInstanceUID,
SeriesInstanceUID,
SOPInstanceUID,
} = instance;
const BulkDataURI =
(value && value.BulkDataURI) ||
`series/${SeriesInstanceUID}/instances/${SOPInstanceUID}${defaultPath}`;
@@ -234,94 +247,27 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
);
}
// Get Series
const {
seriesSummaryMetadata,
seriesPromises,
} = await retrieveStudyMetadata(
wadoDicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad,
filters,
sortCriteria,
sortFunction
);
/**
* naturalizes the dataset, and adds a retrieve bulkdata method
* to any values containing BulkDataURI.
* @param {*} instance
* @returns naturalized dataset, with retrieveBulkData methods
*/
const addRetrieveBulkData = instance => {
const naturalized = naturalizeDataset(instance);
Object.keys(naturalized).forEach(key => {
const value = naturalized[key];
// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// Provide a method to fetch bulkdata
value.retrieveBulkData = () => {
const options = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server
// decide which type to respond with.
multipart: false,
BulkDataURI: value.BulkDataURI,
// The study instance UID is required if the bulkdata uri
// is relative - that isn't disallowed by DICOMweb, but
// isn't well specified in the standard, but is needed in
// any implementation that stores static copies of the metadata
StudyInstanceUID: naturalized.StudyInstanceUID,
};
return qidoDicomWebClient
.retrieveBulkData(options)
.then(val => {
const ret = (val && val[0]) || undefined;
value.Value = ret;
return ret;
});
};
}
});
return naturalized;
};
// Async load series, store as retrieved
function storeInstances(instances) {
const naturalizedInstances = instances.map(addRetrieveBulkData);
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
}
function setSuccessFlag() {
const study = DicomMetadataStore.getStudy(
if (enableStudyLazyLoad) {
return implementation._retrieveSeriesMetadataAsync(
StudyInstanceUID,
filters,
sortCriteria,
sortFunction,
madeInClient
);
study.isLoaded = true;
}
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
// it manually here
seriesSummaryMetadata.forEach(aSeries => {
aSeries.StudyInstanceUID = StudyInstanceUID;
});
DicomMetadataStore.addSeriesMetadata(
seriesSummaryMetadata,
return implementation._retrieveSeriesMetadataSync(
StudyInstanceUID,
filters,
sortCriteria,
sortFunction,
madeInClient
);
const numberOfSeries = seriesPromises.length;
seriesPromises.forEach(async (seriesPromise, index) => {
const instances = await seriesPromise;
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
});
},
},
},
store: {
dicom: async dataset => {
const headers = UserAuthenticationService.getAuthorizationHeader();
@@ -353,6 +299,187 @@ function createDicomWebApi(dicomWebConfig, UserAuthenticationService) {
await wadoDicomWebClient.storeInstances(options);
},
},
_retrieveSeriesMetadataSync: async (
StudyInstanceUID,
filters,
sortCriteria,
sortFunction,
madeInClient
) => {
const enableStudyLazyLoad = false;
// data is all SOPInstanceUIDs
const data = await retrieveStudyMetadata(
wadoDicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad,
filters,
sortCriteria,
sortFunction
);
// first naturalize the data
const naturalizedInstancesMetadata = data.map(naturalizeDataset);
const seriesSummaryMetadata = {};
const instancesPerSeries = {};
naturalizedInstancesMetadata.forEach(instance => {
if (!seriesSummaryMetadata[instance.SeriesInstanceUID]) {
seriesSummaryMetadata[instance.SeriesInstanceUID] = {
StudyInstanceUID: instance.StudyInstanceUID,
StudyDescription: instance.StudyDescription,
SeriesInstanceUID: instance.SeriesInstanceUID,
SeriesDescription: instance.SeriesDescription,
SeriesNumber: instance.SeriesNumber,
SeriesTime: instance.SeriesTime,
SOPClassUID: instance.SOPClassUID,
ProtocolName: instance.ProtocolName,
Modality: instance.Modality,
};
}
if (!instancesPerSeries[instance.SeriesInstanceUID]) {
instancesPerSeries[instance.SeriesInstanceUID] = [];
}
const imageId = implementation.getImageIdsForInstance({
instance,
});
instance.imageId = imageId;
metadataProvider.addImageIdToUIDs(imageId, {
StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
SOPInstanceUID: instance.SOPInstanceUID,
});
instancesPerSeries[instance.SeriesInstanceUID].push(instance);
});
// grab all the series metadata
const seriesMetadata = Object.values(seriesSummaryMetadata);
DicomMetadataStore.addSeriesMetadata(seriesMetadata, madeInClient);
Object.keys(instancesPerSeries).forEach(seriesInstanceUID =>
DicomMetadataStore.addInstances(
instancesPerSeries[seriesInstanceUID],
madeInClient
)
);
},
_retrieveSeriesMetadataAsync: async (
StudyInstanceUID,
filters,
sortCriteria,
sortFunction,
madeInClient = false
) => {
const enableStudyLazyLoad = true;
// Get Series
const {
preLoadData: seriesSummaryMetadata,
promises: seriesPromises,
} = await retrieveStudyMetadata(
wadoDicomWebClient,
StudyInstanceUID,
enableStudyLazyLoad,
filters,
sortCriteria,
sortFunction
);
/**
* naturalizes the dataset, and adds a retrieve bulkdata method
* to any values containing BulkDataURI.
* @param {*} instance
* @returns naturalized dataset, with retrieveBulkData methods
*/
const addRetrieveBulkData = instance => {
const naturalized = naturalizeDataset(instance);
Object.keys(naturalized).forEach(key => {
const value = naturalized[key];
// The value.Value will be set with the bulkdata read value
// in which case it isn't necessary to re-read this.
if (value && value.BulkDataURI && !value.Value) {
// Provide a method to fetch bulkdata
value.retrieveBulkData = () => {
const options = {
// The bulkdata fetches work with either multipart or
// singlepart, so set multipart to false to let the server
// decide which type to respond with.
multipart: false,
BulkDataURI: value.BulkDataURI,
// The study instance UID is required if the bulkdata uri
// is relative - that isn't disallowed by DICOMweb, but
// isn't well specified in the standard, but is needed in
// any implementation that stores static copies of the metadata
StudyInstanceUID: naturalized.StudyInstanceUID,
};
return qidoDicomWebClient.retrieveBulkData(options).then(val => {
const ret = (val && val[0]) || undefined;
value.Value = ret;
return ret;
});
};
}
});
return naturalized;
};
// Async load series, store as retrieved
function storeInstances(instances) {
const naturalizedInstances = instances.map(addRetrieveBulkData);
// Adding instanceMetadata to OHIF MetadataProvider
naturalizedInstances.forEach((instance, index) => {
const imageId = implementation.getImageIdsForInstance({
instance,
});
// Adding imageId to each instance
// Todo: This is not the best way I can think of to let external
// metadata handlers know about the imageId that is stored in the store
instance.imageId = imageId;
// Adding UIDs to metadataProvider
// Note: storing imageURI in metadataProvider since stack viewports
// will use the same imageURI
metadataProvider.addImageIdToUIDs(imageId, {
StudyInstanceUID,
SeriesInstanceUID: instance.SeriesInstanceUID,
SOPInstanceUID: instance.SOPInstanceUID,
});
});
DicomMetadataStore.addInstances(naturalizedInstances, madeInClient);
}
function setSuccessFlag() {
const study = DicomMetadataStore.getStudy(
StudyInstanceUID,
madeInClient
);
study.isLoaded = true;
}
// Google Cloud Healthcare doesn't return StudyInstanceUID, so we need to add
// it manually here
seriesSummaryMetadata.forEach(aSeries => {
aSeries.StudyInstanceUID = StudyInstanceUID;
});
DicomMetadataStore.addSeriesMetadata(seriesSummaryMetadata, madeInClient);
const numberOfSeries = seriesPromises.length;
seriesPromises.forEach(async (seriesPromise, index) => {
const instances = await seriesPromise;
storeInstances(instances);
if (index === numberOfSeries - 1) setSuccessFlag();
});
},
deleteStudyMetadataPromise,
getImageIdsForDisplaySet(displaySet) {
const images = displaySet.images;
@@ -25,7 +25,7 @@ const seriesSortCriteria = {
};
const instancesSortCriteria = {
default: (a, b) => a.InstanceNumber - b.InstanceNumber,
default: (a, b) => parseInt(a.InstanceNumber) - parseInt(b.InstanceNumber),
};
const sortingCriteria = {
@@ -1,4 +1,4 @@
//import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
import RetrieveMetadataLoaderSync from './retrieveMetadataLoaderSync';
import RetrieveMetadataLoaderAsync from './retrieveMetadataLoaderAsync';
/**
@@ -19,12 +19,10 @@ async function RetrieveMetadata(
sortCriteria,
sortFunction
) {
// const RetrieveMetadataLoader =
// enableStudyLazyLoad !== false
// ? RetrieveMetadataLoaderAsync
// : RetrieveMetadataLoaderSync;
const RetrieveMetadataLoader = RetrieveMetadataLoaderAsync;
const RetrieveMetadataLoader =
enableStudyLazyLoad !== false
? RetrieveMetadataLoaderAsync
: RetrieveMetadataLoaderSync;
const retrieveMetadataLoader = new RetrieveMetadataLoader(
dicomWebClient,
@@ -33,12 +31,9 @@ async function RetrieveMetadata(
sortCriteria,
sortFunction
);
const { preLoadData, promises } = await retrieveMetadataLoader.execLoad();
const data = await retrieveMetadataLoader.execLoad();
return {
seriesSummaryMetadata: preLoadData,
seriesPromises: promises,
};
return data;
}
export default RetrieveMetadata;
@@ -2,7 +2,6 @@ import dcmjs from 'dcmjs';
import { sortStudySeries, sortingCriteria } from '../utils/sortStudy';
import RetrieveMetadataLoader from './retrieveMetadataLoader';
/**
* Creates an immutable series loader object which loads each series sequentially using the iterator interface
* @param {DICOMWebClient} dicomWebClient The DICOMWebClient instance to be used for series load
@@ -60,7 +60,6 @@ export default class RetrieveMetadataLoaderSync extends RetrieveMetadataLoader {
}
async posLoad(loadData) {
const { server } = this;
return createStudyFromSOPInstanceList(server, loadData);
return loadData;
}
}