ohif-viewer/platform/core/src/classes/MetadataProvider.js

413 lines
13 KiB
JavaScript
Raw Normal View History

Chore/kill yalc with fire (#629) * init * more shifting * Move @babel dependencies up to workspace root * Begin updating dependent libraries to use sync'd webpack builds w/ watches * Commit changes before a long weekend * Split packages into platform and extensions * Consolidate how/where we specify file/module type entrypoints * Make it possible to pass in extensions as App props * Resolve viewer's modules * Target for UMD bundle * Add Webpack Stylus loader * Provide WebPack build options for microscopy, vtk, and ui * Better entrypoint for extensions * Push changes up to switch PCs * Fix ESM symlink build for Viewers * Shift docs to root * Specify additional lerna config props * Begin providing basic readme info * Rename example extension folder * Shift links to bottom of doc * First attempt at a netlify deploy preview * Attempt to fix deploy preview build * push workspace enabled to initial command; remove second yarn install; use npx to call gitbook cli commands * Try to use global gitbook-cli * Export node_modules path * More agressive with modifying PATH * Follow deploy log output advice * try again to set bin path * Try swapping bash with a new shell * Try bash -l instead of exec bash * Annd let's try again * and again * We may have figured it out johnny, boy * We need to pull cornerstone-wado-image-loader files from hoisted node_modules * Attempt to fix ticks/escapes * Change copy syntax; try running tests on viewers from root for circleci * Lower version to match circleci image * Try alternative jest-canvas-mock location and version file syntax * Run version command instead of calling node directly * Try alternative jest transform * Set path and enable workspaces * Add codecov flags * Use cpx so our copying finishes? * Get all platform unit tests to run * Run and report individually and in parallel * Try once relying on codecov yaml to split w/ flags * Add projects to split by flags * Generate separate reports * Trigger codecov after everything has finished running; these may not support flags * Use recommended flags from issue comments for codecov * Fix typo * Remove clear flag * Shift back to calling codecov from root * Generate example for codecov issue * Try fixing paths * Simplify * Try using codecov orb * Bump circleci version * Upload core and viewer * Use full string paths * Remove individual codecov calls * Fix dir * Combine files before upload * Escape anchors * tryfix syntax * Escape string literal * Combine lines to reduce path * long paths * Use home alias * Fix filename * Try to see the contents of our cat'd file * Also upload `core` * Try running with aliased folder * Fix path; split PR and Merge unit tests into separate jobs * Set default threshold * Fix typo * Changing to scoped package names * Adding note * Catch more updates * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.0 - @ohif/extension-dicom-html@0.0.4-alpha.0 - @ohif/extension-dicom-microscopy@0.0.9-alpha.0 - @ohif/extension-dicom-pdf@0.0.8-alpha.0 - @ohif/extension-vtk@0.1.4-alpha.0 - @ohif/core@0.11.1-alpha.0 - @ohif/i18n@0.2.3-alpha.0 - @ohif/ui@0.2.18-alpha.0 - @ohif/viewer@0.0.22-alpha.0 * Clean up primary readme * Support for dev and dev:* commands * Shift build command; satisfy default PWA build * Remove old scripts * Clean up UI to set webpack scripts * Tidy up project links * Clean duplicate code in UI project * Update UI project's docs * Clean up ui and i18n config * Reduce duplicate code in `@ohif/core` * Fix @ohif/ui versions * bump @ohif/core versions * Redux testkit dep * Remove unused/duplicate config files for projects/packages * ohif-core --> @ohif/core * Safer publish command * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.1 - @ohif/extension-dicom-html@0.0.4-alpha.1 - @ohif/extension-dicom-microscopy@0.0.9-alpha.1 - @ohif/extension-dicom-pdf@0.0.8-alpha.1 - @ohif/extension-vtk@0.1.4-alpha.1 - @ohif/core@0.11.1-alpha.1 - @ohif/i18n@0.2.3-alpha.1 - @ohif/ui@0.2.18-alpha.1 - @ohif/viewer@0.0.22-alpha.1 * test version bump * Add global prettier config * chore(release): publish %s [skip ci] - @ohif/viewer@0.0.22-alpha.2 * test lerna publish * Update package.jsons * Fix build scripts * catch changes * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.2 - @ohif/extension-dicom-html@0.0.4-alpha.2 - @ohif/extension-dicom-microscopy@0.0.9-alpha.2 - @ohif/extension-dicom-pdf@0.0.8-alpha.2 - @ohif/extension-vtk@0.1.4-alpha.2 - @ohif/core@0.11.1-alpha.2 - @ohif/i18n@0.2.3-alpha.2 - @ohif/ui@0.2.18-alpha.2 - @ohif/viewer@0.0.22-alpha.3 * fix eslint issues caused by eslint 6 and our react-app config * Publish with canary tag * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.3 - @ohif/extension-dicom-html@0.0.4-alpha.3 - @ohif/extension-dicom-microscopy@0.0.9-alpha.3 - @ohif/extension-dicom-pdf@0.0.8-alpha.3 - @ohif/extension-vtk@0.1.4-alpha.3 - @ohif/core@0.11.1-alpha.3 - @ohif/i18n@0.2.3-alpha.3 - @ohif/ui@0.2.18-alpha.3 - @ohif/viewer@0.0.22-alpha.4 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.4 - @ohif/extension-dicom-html@0.0.4-alpha.4 - @ohif/extension-dicom-microscopy@0.0.9-alpha.4 - @ohif/extension-dicom-pdf@0.0.8-alpha.4 - @ohif/extension-vtk@0.1.4-alpha.4 - @ohif/core@0.11.1-alpha.4 - @ohif/i18n@0.2.3-alpha.4 - @ohif/ui@0.2.18-alpha.4 - @ohif/viewer@0.0.22-alpha.5 * Simplify build * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.5 - @ohif/extension-dicom-html@0.0.4-alpha.5 - @ohif/extension-dicom-microscopy@0.0.9-alpha.5 - @ohif/extension-dicom-pdf@0.0.8-alpha.5 - @ohif/extension-vtk@0.1.4-alpha.5 - @ohif/core@0.11.1-alpha.5 - @ohif/i18n@0.2.3-alpha.5 - @ohif/ui@0.2.18-alpha.5 - @ohif/viewer@0.0.22-alpha.6 * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.6 - @ohif/extension-dicom-html@0.0.4-alpha.6 - @ohif/extension-dicom-microscopy@0.0.9-alpha.6 - @ohif/extension-dicom-pdf@0.0.8-alpha.6 - @ohif/extension-vtk@0.1.4-alpha.6 - @ohif/core@0.11.1-alpha.6 - @ohif/i18n@0.2.3-alpha.6 - @ohif/ui@0.2.18-alpha.6 - @ohif/viewer@0.0.22-alpha.7 * Split version and publish * chore(release): publish %s [skip ci] - @ohif/extension-cornerstone@0.0.39-alpha.7 - @ohif/extension-dicom-html@0.0.4-alpha.7 - @ohif/extension-dicom-microscopy@0.0.9-alpha.7 - @ohif/extension-dicom-pdf@0.0.8-alpha.7 - @ohif/extension-vtk@0.1.4-alpha.7 - @ohif/core@0.11.1-alpha.7 - @ohif/i18n@0.2.3-alpha.7 - @ohif/ui@0.2.18-alpha.7 - @ohif/viewer@0.0.22-alpha.8 * Try simplify netlify build * react-viewerbase --> @ohif/ui * add test for stylus loader * Try new way to specify publish path * Use an output folder that won't be ignored * Try move command * fiddle with paths * Dig for some information * Make sure we're in root for our commands * Remove discovery * Setup script-tag example * Fix typo * Move config folder * Update cornerstone extension to use shared/root webpack configs * capture changes * Update extension webpack configs * Don't check in docz output * Fix babel runtime resolution issue * Simplify ui build steps * Push in progress * Shift directories * Get viewer dev path to work * Fix viewer commonjs build * Fix build pwa config for viewers * Make sure our package specific command can be called from root * Update notes * Fix imports and misc. merge notes * Bump wado-image-loader version * Switch up commands to remove webworkers copy * Fix CI config * Fix more broken package references * Add react-dropzone * Persist so e2e test can use * Fix lockfile * Use cypress orb to simplify CI * Fix config * Try again * Install yarn latest for orb * Install yarn a different way * Wait on correct port * Run tests in viewer project * Fix cypress config * Make Cypress happier - use default location * Simplify e2e testing * Capture UI changes since fork * Catch core up * Grab latest deps after merge * Fix broken import * Make sure cornerstone-wado-image-loader resolves to 3+ * Note regarding orbs * Fix refresh on 404 routes * Fix odd error for cornerstone-wado-image-loader * Different target for UMD * Simplify dev script * Try to get fancy with netlify output * Let's do it again * Try to fix measurementTools issue * See if we can get our lerna magic to work on merge to monorepo branch
2019-08-13 21:36:46 +02:00
import { parsingUtils } from '../lib/parsingUtils';
const FUNCTION = 'function';
class MetadataProvider {
constructor() {
// Define the main "metadataLookup" private property as an immutable property.
Object.defineProperty(this, 'metadataLookup', {
configurable: false,
enumerable: false,
writable: false,
value: new Map(),
});
// Local reference to provider function bound to current instance.
Object.defineProperty(this, '_provider', {
configurable: false,
enumerable: false,
writable: true,
value: null,
});
}
/**
* Cornerstone Metadata provider to store image meta data
* Data from instances, series, and studies are associated with
* imageIds to facilitate usage of this information by Cornerstone's Tools
*
* e.g. the imagePlane metadata object contains instance information about
* row/column pixel spacing, patient position, and patient orientation. It
* is used in CornerstoneTools to position reference lines and orientation markers.
*
* @param {String} imageId The Cornerstone ImageId
* @param {Object} data An object containing instance, series, and study metadata
*/
addMetadata(imageId, data) {
const instanceMetadata = data.instance;
const seriesMetadata = data.series;
const studyMetadata = data.study;
const numImages = data.numImages;
const metadata = {};
metadata.frameNumber = data.frameNumber;
metadata.study = {
accessionNumber: studyMetadata.accessionNumber,
patientId: studyMetadata.patientId,
studyInstanceUid: studyMetadata.studyInstanceUid,
studyDate: studyMetadata.studyDate,
studyTime: studyMetadata.studyTime,
studyDescription: studyMetadata.studyDescription,
institutionName: studyMetadata.institutionName,
patientHistory: studyMetadata.patientHistory,
};
metadata.series = {
seriesDescription: seriesMetadata.seriesDescription,
seriesNumber: seriesMetadata.seriesNumber,
seriesDate: seriesMetadata.seriesDate,
seriesTime: seriesMetadata.seriesTime,
modality: seriesMetadata.modality,
seriesInstanceUid: seriesMetadata.seriesInstanceUid,
numImages: numImages,
};
metadata.instance = instanceMetadata;
metadata.patient = {
name: studyMetadata.patientName,
id: studyMetadata.patientId,
birthDate: studyMetadata.patientBirthDate,
sex: studyMetadata.patientSex,
age: studyMetadata.patientAge,
};
// If there is sufficient information, populate
// the imagePlane object for easier use in the Viewer
metadata.imagePlane = this.getImagePlane(instanceMetadata);
// Add the metadata to the imageId lookup object
this.metadataLookup.set(imageId, metadata);
}
/**
* Return the metadata for the given imageId
* @param {String} imageId The Cornerstone ImageId
* @returns image metadata
*/
getMetadata(imageId) {
return this.metadataLookup.get(imageId);
}
/**
* Adds a set of metadata to the Cornerstone metadata provider given a specific
* imageId, type, and dataset
*
* @param imageId
* @param type (e.g. series, instance, tagDisplay)
* @param data
*/
addSpecificMetadata(imageId, type, data) {
const metadata = {};
metadata[type] = data;
const oldMetadata = this.metadataLookup.get(imageId);
this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
}
getFromImage(image, type, tag, attrName, defaultValue) {
let value;
if (image.data) {
value = this.getFromDataSet(image.data, type, tag);
} else {
value = image.instance[attrName];
}
return value === null ? defaultValue : value;
}
getFromDataSet(dataSet, type, tag) {
if (!dataSet) {
return;
}
const fn = dataSet[type];
if (!fn) {
return;
}
return fn.call(dataSet, tag);
}
getFrameIncrementPointer(image) {
const dataSet = image.data;
let frameInstancePointer = '';
if (parsingUtils.isValidDataSet(dataSet)) {
const frameInstancePointerNames = {
x00181063: 'frameTime',
x00181065: 'frameTimeVector',
};
// (0028,0009) = Frame Increment Pointer
const frameInstancePointerTag = parsingUtils.attributeTag(
dataSet,
'x00280009'
);
frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
} else {
frameInstancePointer = image.instance.frameIncrementPointer;
}
return frameInstancePointer || '';
}
getFrameTimeVector(image) {
const dataSet = image.data;
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
return parsingUtils.floatArray(dataSet, 'x00181065');
}
return image.instance.frameTimeVector;
}
getFrameTime(image) {
const dataSet = image.data;
if (parsingUtils.isValidDataSet(dataSet)) {
// Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
// Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
// but that case is currently not supported.
return dataSet.floatString('x00181063', -1);
}
return image.instance.frameTime;
}
/**
* Updates the related metadata for missing fields given a specified image
*
* @param image
*/
updateMetadata(image) {
const imageMetadata = this.metadataLookup.get(image.imageId);
if (!imageMetadata) {
return;
}
imageMetadata.patient.age =
imageMetadata.patient.age ||
this.getFromDataSet(image.data, 'string', 'x00101010');
imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
imageMetadata.instance.columns =
imageMetadata.instance.columns || image.columns;
imageMetadata.instance.sopClassUid =
imageMetadata.instance.sopClassUid ||
this.getFromDataSet(image.data, 'string', 'x00080016');
imageMetadata.instance.sopInstanceUid =
imageMetadata.instance.sopInstanceUid ||
this.getFromDataSet(image.data, 'string', 'x00080018');
imageMetadata.instance.pixelSpacing =
imageMetadata.instance.pixelSpacing ||
this.getFromDataSet(image.data, 'string', 'x00280030');
imageMetadata.instance.frameOfReferenceUID =
imageMetadata.instance.frameOfReferenceUID ||
this.getFromDataSet(image.data, 'string', 'x00200052');
imageMetadata.instance.imageOrientationPatient =
imageMetadata.instance.imageOrientationPatient ||
this.getFromDataSet(image.data, 'string', 'x00200037');
imageMetadata.instance.imagePositionPatient =
imageMetadata.instance.imagePositionPatient ||
this.getFromDataSet(image.data, 'string', 'x00200032');
imageMetadata.instance.sliceThickness =
imageMetadata.instance.sliceThickness ||
this.getFromDataSet(image.data, 'string', 'x00180050');
imageMetadata.instance.sliceLocation =
imageMetadata.instance.sliceLocation ||
this.getFromDataSet(image.data, 'string', 'x00201041');
imageMetadata.instance.tablePosition =
imageMetadata.instance.tablePosition ||
this.getFromDataSet(image.data, 'string', 'x00189327');
imageMetadata.instance.spacingBetweenSlices =
imageMetadata.instance.spacingBetweenSlices ||
this.getFromDataSet(image.data, 'string', 'x00180088');
imageMetadata.instance.lossyImageCompression =
imageMetadata.instance.lossyImageCompression ||
this.getFromDataSet(image.data, 'string', 'x00282110');
imageMetadata.instance.lossyImageCompressionRatio =
imageMetadata.instance.lossyImageCompressionRatio ||
this.getFromDataSet(image.data, 'string', 'x00282112');
imageMetadata.instance.frameIncrementPointer =
imageMetadata.instance.frameIncrementPointer ||
this.getFromDataSet(image.data, 'string', 'x00280009');
imageMetadata.instance.frameTime =
imageMetadata.instance.frameTime ||
this.getFromDataSet(image.data, 'string', 'x00181063');
imageMetadata.instance.frameTimeVector =
imageMetadata.instance.frameTimeVector ||
this.getFromDataSet(image.data, 'string', 'x00181065');
if (
(image.data || image.instance) &&
!imageMetadata.instance.multiframeMetadata
) {
imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
image
);
}
imageMetadata.imagePlane =
imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
}
/**
* Constructs and returns the imagePlane given the metadata instance
*
* @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
* @returns imagePlane The constructed imagePlane to be used in viewer easily
*/
getImagePlane(instance) {
if (
!instance.rows ||
!instance.columns ||
!instance.pixelSpacing ||
!instance.frameOfReferenceUID ||
!instance.imageOrientationPatient ||
!instance.imagePositionPatient
) {
return;
}
const imageOrientation = instance.imageOrientationPatient.split('\\');
const imagePosition = instance.imagePositionPatient.split('\\');
let columnPixelSpacing = 1.0;
let rowPixelSpacing = 1.0;
if (instance.pixelSpacing) {
const split = instance.pixelSpacing.split('\\');
rowPixelSpacing = parseFloat(split[0]);
columnPixelSpacing = parseFloat(split[1]);
}
return {
frameOfReferenceUID: instance.frameOfReferenceUID,
rows: instance.rows,
columns: instance.columns,
rowCosines: [
parseFloat(imageOrientation[0]),
parseFloat(imageOrientation[1]),
parseFloat(imageOrientation[2]),
],
columnCosines: [
parseFloat(imageOrientation[3]),
parseFloat(imageOrientation[4]),
parseFloat(imageOrientation[5]),
],
imagePositionPatient: [
parseFloat(imagePosition[0]),
parseFloat(imagePosition[1]),
parseFloat(imagePosition[2]),
],
rowPixelSpacing,
columnPixelSpacing,
};
}
/**
* This function extracts miltiframe information from a dicomParser.DataSet object.
*
* @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
* @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
*/
getMultiframeModuleMetadata(image) {
const imageInfo = {
isMultiframeImage: false,
frameIncrementPointer: null,
numberOfFrames: 0,
frameTime: 0,
frameTimeVector: null,
averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
};
let frameTime;
const numberOfFrames = this.getFromImage(
image,
'intString',
'x00280008',
'numberOfFrames',
-1
);
if (numberOfFrames > 0) {
// set multi-frame image indicator
imageInfo.isMultiframeImage = true;
imageInfo.numberOfFrames = numberOfFrames;
// (0028,0009) = Frame Increment Pointer
const frameIncrementPointer = this.getFrameIncrementPointer(image);
if (frameIncrementPointer === 'frameTimeVector') {
// Frame Increment Pointer points to Frame Time Vector (0018,1065) field
const frameTimeVector = this.getFrameTimeVector(image);
if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTimeVector = frameTimeVector;
frameTime =
frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
imageInfo.averageFrameRate = 1000 / frameTime;
}
} else if (
frameIncrementPointer === 'frameTime' ||
frameIncrementPointer === ''
) {
frameTime = this.getFrameTime(image);
if (frameTime > 0) {
imageInfo.frameIncrementPointer = frameIncrementPointer;
imageInfo.frameTime = frameTime;
imageInfo.averageFrameRate = 1000 / frameTime;
}
}
}
return imageInfo;
}
/**
* Get a bound reference to the provider function.
*/
getProvider() {
let provider = this._provider;
if (typeof this._provider !== FUNCTION) {
provider = this.provider.bind(this);
this._provider = provider;
}
return provider;
}
/**
* Looks up metadata for Cornerstone Tools given a specified type and imageId
* A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
* are keys in the stored metadata objects.
*
* @param type
* @param imageId
* @returns {Object} Relevant metadata of the specified type
*/
provider(type, imageId) {
const imageMetadata = this.metadataLookup.get(imageId);
if (!imageMetadata) {
return;
}
if (imageMetadata.hasOwnProperty(type)) {
return imageMetadata[type];
}
}
}
export default MetadataProvider;