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:
668 files changed
+41669
-13410
No files matched your search
@@ -1,136 +0,0 @@
|
||||
let stackMap = {};
|
||||
let configuration = {};
|
||||
const stackUpdatedCallbacks = [];
|
||||
|
||||
/**
|
||||
* Loop through the current series and add metadata to the
|
||||
* Cornerstone meta data provider. This will be used to fill information
|
||||
* into the viewport overlays, and to calculate reference lines and orientation markers
|
||||
* @param {Object} stackMap stackMap object
|
||||
* @param {Object} displaySet The set of images to make the stack from
|
||||
* @return {Array} Array with image IDs
|
||||
*/
|
||||
function createAndAddStack(
|
||||
stackMap,
|
||||
displaySet,
|
||||
dataSource,
|
||||
stackUpdatedCallbacks
|
||||
) {
|
||||
const {
|
||||
images,
|
||||
displaySetInstanceUID,
|
||||
StudyInstanceUID,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
} = displaySet;
|
||||
if (!images) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageIds = dataSource.getImageIdsForDisplaySet(displaySet);
|
||||
|
||||
const stack = {
|
||||
StudyInstanceUID,
|
||||
displaySetInstanceUID,
|
||||
imageIds,
|
||||
frameRate,
|
||||
isClip,
|
||||
initialImageIdIndex,
|
||||
};
|
||||
|
||||
stackMap[displaySetInstanceUID] = stack;
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
configuration = {
|
||||
createAndAddStack,
|
||||
};
|
||||
|
||||
/**
|
||||
* This object contains all the functions needed for interacting with the stack manager.
|
||||
* Generally, findStack is the only function used. If you want to know when new stacks
|
||||
* come in, you can register a callback with addStackUpdatedCallback.
|
||||
*/
|
||||
const StackManager = {
|
||||
/**
|
||||
* Removes all current stacks
|
||||
*/
|
||||
clearStacks() {
|
||||
stackMap = {};
|
||||
},
|
||||
/**
|
||||
* Create a stack from an image set, as well as add in the metadata on a per image bases.
|
||||
* @param displaySet The set of images to make the stack from
|
||||
* @return {Array} Array with image IDs
|
||||
*/
|
||||
makeAndAddStack(displaySet, dataSource) {
|
||||
return configuration.createAndAddStack(
|
||||
stackMap,
|
||||
displaySet,
|
||||
dataSource,
|
||||
stackUpdatedCallbacks
|
||||
);
|
||||
},
|
||||
/**
|
||||
* Find a stack from the currently created stacks.
|
||||
* @param displaySetInstanceUID The UID of the stack to find.
|
||||
* @returns {*} undefined if not found, otherwise the stack object is returned.
|
||||
*/
|
||||
findStack(displaySetInstanceUID) {
|
||||
return stackMap[displaySetInstanceUID];
|
||||
},
|
||||
/**
|
||||
* Find a stack or create one if it has not been created yet
|
||||
* @param displaySet The set of images to make the stack from
|
||||
* @return {Array} Array with image IDs
|
||||
*/
|
||||
findOrCreateStack(displaySet, dataSource) {
|
||||
let stack = this.findStack(displaySet.displaySetInstanceUID);
|
||||
|
||||
if (!stack || !stack.imageIds) {
|
||||
stack = this.makeAndAddStack(displaySet, dataSource);
|
||||
}
|
||||
|
||||
return stack;
|
||||
},
|
||||
/**
|
||||
* Gets the underlying map of displaySetInstanceUID to stack object.
|
||||
* WARNING: Do not change this object. It directly affects the manager.
|
||||
* @returns {{}} map of displaySetInstanceUID -> stack.
|
||||
*/
|
||||
getAllStacks() {
|
||||
return stackMap;
|
||||
},
|
||||
/**
|
||||
* Adds in a callback to be called on a stack being added / updated.
|
||||
* @param callback must accept at minimum one argument,
|
||||
* which is the stack that was added / updated.
|
||||
*/
|
||||
addStackUpdatedCallback(callback) {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('callback must be provided as a function');
|
||||
}
|
||||
stackUpdatedCallbacks.push(callback);
|
||||
},
|
||||
/**
|
||||
* Return configuration
|
||||
*/
|
||||
getConfiguration() {
|
||||
return configuration;
|
||||
},
|
||||
/**
|
||||
* Set configuration, in order to provide compatibility
|
||||
* with other systems by overriding this functions
|
||||
* @param {Object} config object with functions to be overrided
|
||||
*
|
||||
* For now, only makeAndAddStack can be overrided
|
||||
*/
|
||||
setConfiguration(config) {
|
||||
configuration = config;
|
||||
},
|
||||
};
|
||||
|
||||
export { StackManager };
|
||||
export default StackManager;
|
||||
@@ -0,0 +1,21 @@
|
||||
// Returns a function, that, as long as it continues to be invoked, will not
|
||||
// be triggered. The function will be called after it stops being called for
|
||||
// N milliseconds. If `immediate` is passed, trigger the function on the
|
||||
// leading edge, instead of the trailing.
|
||||
function debounce(func, wait, immediate) {
|
||||
var timeout;
|
||||
return function() {
|
||||
var context = this,
|
||||
args = arguments;
|
||||
var later = function() {
|
||||
timeout = null;
|
||||
if (!immediate) func.apply(context, args);
|
||||
};
|
||||
var callNow = immediate && !timeout;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
if (callNow) func.apply(context, args);
|
||||
};
|
||||
}
|
||||
|
||||
export default debounce;
|
||||
@@ -1,205 +0,0 @@
|
||||
import cornerstone from 'cornerstone-core';
|
||||
import cornerstoneWADOImageLoader from 'cornerstone-wado-image-loader';
|
||||
import { api } from 'dicomweb-client';
|
||||
import DICOMWeb from '../DICOMWeb';
|
||||
|
||||
import errorHandler from '../errorHandler';
|
||||
|
||||
const getImageId = imageObj => {
|
||||
if (!imageObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
return typeof imageObj.getImageId === 'function'
|
||||
? imageObj.getImageId()
|
||||
: imageObj.url;
|
||||
};
|
||||
|
||||
const findImageIdOnStudies = (studies, displaySetInstanceUID) => {
|
||||
const study = studies.find(study => {
|
||||
const displaySet = study.displaySets.some(
|
||||
displaySet => displaySet.displaySetInstanceUID === displaySetInstanceUID
|
||||
);
|
||||
return displaySet;
|
||||
});
|
||||
const { series = [] } = study;
|
||||
const { instances = [] } = series[0] || {};
|
||||
const instance = instances[0];
|
||||
|
||||
return getImageId(instance);
|
||||
};
|
||||
|
||||
const someInvalidStrings = strings => {
|
||||
const stringsArray = Array.isArray(strings) ? strings : [strings];
|
||||
const emptyString = string => !string;
|
||||
let invalid = stringsArray.some(emptyString);
|
||||
return invalid;
|
||||
};
|
||||
|
||||
const getImageInstance = dataset => {
|
||||
return dataset && dataset.images && dataset.images[0];
|
||||
};
|
||||
|
||||
const getImageInstanceId = imageInstance => {
|
||||
return getImageId(imageInstance);
|
||||
};
|
||||
|
||||
const fetchIt = (url, headers = DICOMWeb.getAuthorizationHeader()) => {
|
||||
return fetch(url, headers).then(response => response.arrayBuffer());
|
||||
};
|
||||
|
||||
const cornerstoneRetriever = imageId => {
|
||||
return cornerstone.loadAndCacheImage(imageId).then(image => {
|
||||
return image && image.data && image.data.byteArray.buffer;
|
||||
});
|
||||
};
|
||||
|
||||
const wadorsRetriever = (
|
||||
url,
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
headers = DICOMWeb.getAuthorizationHeader(),
|
||||
errorInterceptor = errorHandler.getHTTPErrorHandler()
|
||||
) => {
|
||||
const config = {
|
||||
url,
|
||||
headers,
|
||||
errorInterceptor,
|
||||
};
|
||||
const dicomWeb = new api.DICOMwebClient(config);
|
||||
|
||||
return dicomWeb.retrieveInstance({
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
});
|
||||
};
|
||||
|
||||
const getImageLoaderType = imageId => {
|
||||
const loaderRegExp = /^\w+\:/;
|
||||
const loaderType = loaderRegExp.exec(imageId);
|
||||
|
||||
return (
|
||||
(loaderRegExp.lastIndex === 0 &&
|
||||
loaderType &&
|
||||
loaderType[0] &&
|
||||
loaderType[0].replace(':', '')) ||
|
||||
''
|
||||
);
|
||||
};
|
||||
|
||||
class DicomLoaderService {
|
||||
getLocalData(dataset, studies) {
|
||||
if (dataset && dataset.localFile) {
|
||||
// Use referenced imageInstance
|
||||
const imageInstance = getImageInstance(dataset);
|
||||
let imageId = getImageInstanceId(imageInstance);
|
||||
|
||||
// or Try to get it from studies
|
||||
if (someInvalidStrings(imageId)) {
|
||||
imageId = findImageIdOnStudies(studies, dataset.displaySetInstanceUID);
|
||||
}
|
||||
|
||||
if (!someInvalidStrings(imageId)) {
|
||||
return cornerstoneWADOImageLoader.wadouri.loadFileRequest(imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getDataByImageType(dataset) {
|
||||
const imageInstance = getImageInstance(dataset);
|
||||
|
||||
if (imageInstance) {
|
||||
const imageId = getImageInstanceId(imageInstance);
|
||||
let getDicomDataMethod = fetchIt;
|
||||
const loaderType = getImageLoaderType(imageId);
|
||||
|
||||
switch (loaderType) {
|
||||
case 'dicomfile':
|
||||
getDicomDataMethod = cornerstoneRetriever.bind(this, imageId);
|
||||
break;
|
||||
case 'wadors':
|
||||
const url = imageInstance.getData().wadoRoot;
|
||||
const studyInstanceUID = imageInstance.getStudyInstanceUID();
|
||||
const seriesInstanceUID = imageInstance.getSeriesInstanceUID();
|
||||
const sopInstanceUID = imageInstance.getSOPInstanceUID();
|
||||
const invalidParams = someInvalidStrings([
|
||||
url,
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID,
|
||||
]);
|
||||
if (invalidParams) {
|
||||
return;
|
||||
}
|
||||
|
||||
getDicomDataMethod = wadorsRetriever.bind(
|
||||
this,
|
||||
url,
|
||||
studyInstanceUID,
|
||||
seriesInstanceUID,
|
||||
sopInstanceUID
|
||||
);
|
||||
break;
|
||||
case 'wadouri':
|
||||
// Strip out the image loader specifier
|
||||
imageId = imageId.substring(imageId.indexOf(':') + 1);
|
||||
|
||||
if (someInvalidStrings(imageId)) {
|
||||
return;
|
||||
}
|
||||
getDicomDataMethod = fetchIt.bind(this, imageId);
|
||||
break;
|
||||
}
|
||||
|
||||
return getDicomDataMethod();
|
||||
}
|
||||
}
|
||||
|
||||
getDataByDatasetType(dataset) {
|
||||
const {
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
authorizationHeaders,
|
||||
wadoRoot,
|
||||
wadoUri,
|
||||
} = dataset;
|
||||
// Retrieve wadors or just try to fetch wadouri
|
||||
if (!someInvalidStrings(wadoRoot)) {
|
||||
return wadorsRetriever(
|
||||
wadoRoot,
|
||||
StudyInstanceUID,
|
||||
SeriesInstanceUID,
|
||||
SOPInstanceUID,
|
||||
authorizationHeaders
|
||||
);
|
||||
} else if (!someInvalidStrings(wadoUri)) {
|
||||
return fetchIt(wadoUri, { headers: authorizationHeaders });
|
||||
}
|
||||
}
|
||||
|
||||
*getLoaderIterator(dataset, studies) {
|
||||
yield this.getLocalData(dataset, studies);
|
||||
yield this.getDataByImageType(dataset);
|
||||
yield this.getDataByDatasetType(dataset);
|
||||
}
|
||||
|
||||
findDicomDataPromise(dataset, studies) {
|
||||
const loaderIterator = this.getLoaderIterator(dataset, studies);
|
||||
// it returns first valid retriever method.
|
||||
for (const loader of loaderIterator) {
|
||||
if (loader) {
|
||||
return loader;
|
||||
}
|
||||
}
|
||||
|
||||
// in case of no valid loader
|
||||
throw new Error('Invalid dicom data loader');
|
||||
}
|
||||
}
|
||||
|
||||
const dicomLoaderService = new DicomLoaderService();
|
||||
|
||||
export default dicomLoaderService;
|
||||
@@ -0,0 +1,115 @@
|
||||
import { DicomMetadataStore } from '../services/DicomMetadataStore/DicomMetadataStore';
|
||||
|
||||
export default function downloadCSVReport(measurementData) {
|
||||
if (measurementData.length === 0) {
|
||||
// Prevent download of report with no measurements.
|
||||
return;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
'Patient ID',
|
||||
'Patient Name',
|
||||
'StudyInstanceUID',
|
||||
'SeriesInstanceUID',
|
||||
'SOPInstanceUID',
|
||||
'Label',
|
||||
];
|
||||
|
||||
const reportMap = {};
|
||||
measurementData.forEach(measurement => {
|
||||
const {
|
||||
referenceStudyUID,
|
||||
referenceSeriesUID,
|
||||
getReport,
|
||||
uid,
|
||||
} = measurement;
|
||||
|
||||
if (!getReport) {
|
||||
console.warn('Measurement does not have a getReport function');
|
||||
return;
|
||||
}
|
||||
|
||||
const seriesMetadata = DicomMetadataStore.getSeries(
|
||||
referenceStudyUID,
|
||||
referenceSeriesUID
|
||||
);
|
||||
|
||||
const commonRowItems = _getCommonRowItems(measurement, seriesMetadata);
|
||||
const report = getReport(measurement);
|
||||
|
||||
reportMap[uid] = {
|
||||
report,
|
||||
commonRowItems,
|
||||
};
|
||||
});
|
||||
|
||||
// get columns names inside the report from each measurement and
|
||||
// add them to the rows array (this way we can add columns for any custom
|
||||
// measurements that may be added in the future)
|
||||
Object.keys(reportMap).forEach(id => {
|
||||
const { report } = reportMap[id];
|
||||
report.columns.forEach(column => {
|
||||
if (!columns.includes(column)) {
|
||||
columns.push(column);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const results = _mapReportsToRowArray(reportMap, columns);
|
||||
|
||||
let csvContent =
|
||||
'data:text/csv;charset=utf-8,' +
|
||||
results.map(res => res.join(',')).join('\n');
|
||||
|
||||
_createAndDownloadFile(csvContent);
|
||||
}
|
||||
|
||||
function _mapReportsToRowArray(reportMap, columns) {
|
||||
const results = [columns];
|
||||
Object.keys(reportMap).forEach(id => {
|
||||
const { report, commonRowItems } = reportMap[id];
|
||||
const row = [];
|
||||
// For commonRowItems, find the correct index and add the value to the
|
||||
// correct row in the results array
|
||||
Object.keys(commonRowItems).forEach(key => {
|
||||
const index = columns.indexOf(key);
|
||||
const value = commonRowItems[key];
|
||||
row[index] = value;
|
||||
});
|
||||
|
||||
// For each annotation data, find the correct index and add the value to the
|
||||
// correct row in the results array
|
||||
report.columns.forEach((column, index) => {
|
||||
const colIndex = columns.indexOf(column);
|
||||
const value = report.values[index];
|
||||
row[colIndex] = value;
|
||||
});
|
||||
|
||||
results.push(row);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function _getCommonRowItems(measurement, seriesMetadata) {
|
||||
const firstInstance = seriesMetadata.instances[0];
|
||||
|
||||
return {
|
||||
'Patient ID': firstInstance.PatientID, // Patient ID
|
||||
'Patient Name': firstInstance.PatientName.Alphabetic, // PatientName
|
||||
StudyInstanceUID: measurement.referenceStudyUID, // StudyInstanceUID
|
||||
SeriesInstanceUID: measurement.referenceSeriesUID, // SeriesInstanceUID
|
||||
SOPInstanceUID: measurement.SOPInstanceUID, // SOPInstanceUID
|
||||
Label: measurement.label || '', // Label
|
||||
};
|
||||
}
|
||||
|
||||
function _createAndDownloadFile(csvContent) {
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.setAttribute('href', encodedUri);
|
||||
link.setAttribute('download', 'MeasurementReport.csv');
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* Removes the data loader scheme from the imageId
|
||||
*
|
||||
* @param {string} imageId Image ID
|
||||
* @returns {string} imageId without the data loader scheme
|
||||
* @memberof Cache
|
||||
*/
|
||||
export default function imageIdToURI(imageId) {
|
||||
const colonIndex = imageId.indexOf(':');
|
||||
|
||||
return imageId.substring(colonIndex + 1);
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import ObjectPath from './objectPath';
|
||||
import StackManager from './StackManager.js';
|
||||
import absoluteUrl from './absoluteUrl';
|
||||
import guid from './guid';
|
||||
import sortBy from './sortBy.js';
|
||||
import sortBySeriesDate from './sortBySeriesDate.js';
|
||||
import writeScript from './writeScript.js';
|
||||
import DicomLoaderService from './dicomLoaderService.js';
|
||||
import b64toBlob from './b64toBlob.js';
|
||||
//import loadAndCacheDerivedDisplaySets from './loadAndCacheDerivedDisplaySets.js';
|
||||
import urlUtil from './urlUtil';
|
||||
@@ -22,9 +20,13 @@ import progressTrackingUtils from './progressTrackingUtils';
|
||||
import isLowPriorityModality from './isLowPriorityModality';
|
||||
import { isImage } from './isImage';
|
||||
import isDisplaySetReconstructable from './isDisplaySetReconstructable';
|
||||
import imageIdToURI from './imageIdToURI';
|
||||
import debounce from './debounce';
|
||||
import roundNumber from './roundNumber';
|
||||
import downloadCSVReport from './downloadCSVReport';
|
||||
|
||||
// Commented out unused functionality.
|
||||
// Need to implement new mechanism for dervived displaySets using the displaySetManager.
|
||||
// Need to implement new mechanism for derived displaySets using the displaySetManager.
|
||||
|
||||
const utils = {
|
||||
guid,
|
||||
@@ -36,9 +38,8 @@ const utils = {
|
||||
formatDate,
|
||||
formatPN,
|
||||
b64toBlob,
|
||||
StackManager,
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
imageIdToURI,
|
||||
//loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
makeCancelable,
|
||||
@@ -51,6 +52,9 @@ const utils = {
|
||||
isLowPriorityModality,
|
||||
isImage,
|
||||
isDisplaySetReconstructable,
|
||||
debounce,
|
||||
roundNumber,
|
||||
downloadCSVReport,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -61,8 +65,6 @@ export {
|
||||
formatDate,
|
||||
writeScript,
|
||||
b64toBlob,
|
||||
StackManager,
|
||||
DicomLoaderService,
|
||||
urlUtil,
|
||||
//loadAndCacheDerivedDisplaySets,
|
||||
makeDeferred,
|
||||
@@ -76,6 +78,10 @@ export {
|
||||
isLowPriorityModality,
|
||||
isImage,
|
||||
isDisplaySetReconstructable,
|
||||
imageIdToURI,
|
||||
debounce,
|
||||
roundNumber,
|
||||
downloadCSVReport,
|
||||
};
|
||||
|
||||
export default utils;
|
||||
@@ -10,14 +10,16 @@ describe('Top level exports', () => {
|
||||
'sortBySeriesDate',
|
||||
'isLowPriorityModality',
|
||||
'writeScript',
|
||||
'debounce',
|
||||
'downloadCSVReport',
|
||||
'imageIdToURI',
|
||||
'roundNumber',
|
||||
'b64toBlob',
|
||||
'StackManager',
|
||||
'formatDate',
|
||||
'formatPN',
|
||||
//'loadAndCacheDerivedDisplaySets',
|
||||
'isDisplaySetReconstructable',
|
||||
'isImage',
|
||||
'DicomLoaderService',
|
||||
'urlUtil',
|
||||
'makeDeferred',
|
||||
'makeCancelable',
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import toNumber from './toNumber';
|
||||
|
||||
// TODO: Is 10% a reasonable spacingTolerance for spacing?
|
||||
const spacingTolerance = 0.2;
|
||||
const iopTolerance = 0.01;
|
||||
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
@@ -8,7 +14,7 @@ export default function isDisplaySetReconstructable(instances) {
|
||||
return { value: false };
|
||||
}
|
||||
|
||||
const firstInstance = instances[0].getData().metadata;
|
||||
const firstInstance = instances[0];
|
||||
|
||||
const Modality = firstInstance.Modality;
|
||||
const isMultiframe = firstInstance.NumberOfFrames > 1;
|
||||
@@ -35,19 +41,21 @@ function processMultiframe(instance) {
|
||||
}
|
||||
|
||||
function processSingleframe(instances) {
|
||||
const firstImage = instances[0].getData().metadata;
|
||||
const firstImageRows = firstImage.Rows;
|
||||
const firstImageColumns = firstImage.Columns;
|
||||
const firstImageSamplesPerPixel = firstImage.SamplesPerPixel;
|
||||
const firstImageOrientationPatient = firstImage.ImageOrientationPatient;
|
||||
const firstImagePositionPatient = firstImage.ImagePositionPatient;
|
||||
const firstImage = instances[0];
|
||||
const firstImageRows = toNumber(firstImage.Rows);
|
||||
const firstImageColumns = toNumber(firstImage.Columns);
|
||||
const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel);
|
||||
const firstImageOrientationPatient = toNumber(
|
||||
firstImage.ImageOrientationPatient
|
||||
);
|
||||
const firstImagePositionPatient = toNumber(firstImage.ImagePositionPatient);
|
||||
|
||||
// Can't reconstruct if we:
|
||||
// -- Have a different dimensions within a displaySet.
|
||||
// -- Have a different number of components within a displaySet.
|
||||
// -- Have different orientations within a displaySet.
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
const instance = instances[i];
|
||||
const {
|
||||
Rows,
|
||||
Columns,
|
||||
@@ -55,11 +63,13 @@ function processSingleframe(instances) {
|
||||
ImageOrientationPatient,
|
||||
} = instance;
|
||||
|
||||
const imageOrientationPatient = toNumber(ImageOrientationPatient);
|
||||
|
||||
if (
|
||||
Rows !== firstImageRows ||
|
||||
Columns !== firstImageColumns ||
|
||||
SamplesPerPixel !== firstImageSamplesPerPixel ||
|
||||
!_isSameOrientation(ImageOrientationPatient, firstImageOrientationPatient)
|
||||
!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
return { value: false };
|
||||
}
|
||||
@@ -71,8 +81,9 @@ function processSingleframe(instances) {
|
||||
// If spacing is on a uniform grid but we are missing frames,
|
||||
// Allow reconstruction, but pass back the number of missing frames.
|
||||
if (instances.length > 2) {
|
||||
const lastIpp = instances[instances.length - 1].getData().metadata
|
||||
.ImagePositionPatient;
|
||||
const lastIpp = toNumber(
|
||||
instances[instances.length - 1].ImagePositionPatient
|
||||
);
|
||||
|
||||
// We can't reconstruct if we are missing ImagePositionPatient values
|
||||
if (!firstImagePositionPatient || !lastIpp) {
|
||||
@@ -86,11 +97,12 @@ function processSingleframe(instances) {
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i].getData().metadata;
|
||||
const { ImagePositionPatient } = instance;
|
||||
const instance = instances[i];
|
||||
// Todo: get metadata from OHIF.MetadataProvider
|
||||
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
|
||||
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
ImagePositionPatient,
|
||||
imagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
@@ -108,7 +120,7 @@ function processSingleframe(instances) {
|
||||
}
|
||||
}
|
||||
|
||||
previousImagePositionPatient = ImagePositionPatient;
|
||||
previousImagePositionPatient = imagePositionPatient;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +139,6 @@ function _isSameOrientation(iop1, iop2) {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Is 10% a reasonable spacingTolerance for spacing?
|
||||
const spacingTolerance = 0.1;
|
||||
const iopTolerance = 0.01;
|
||||
|
||||
/**
|
||||
* Checks for spacing issues.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
function _round(value, decimals) {
|
||||
return Number(value.toFixed(decimals));
|
||||
}
|
||||
|
||||
export default _round;
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Returns the values as an array of javascript numbers
|
||||
*
|
||||
* @param val - The javascript object for the specified element in the metadata
|
||||
* @returns {*}
|
||||
*/
|
||||
export default function toNumber(val) {
|
||||
if (Array.isArray(val)) {
|
||||
return val.map(v => (v !== undefined ? Number(v) : v));
|
||||
} else {
|
||||
return val !== undefined ? Number(val) : val;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user