feat(Thumbnail): Display set messages support & displaying of series inconsistencies in the thumbnail (#3499)
Co-authored-by: Igor Octaviano <igoroctaviano@gmail.com>
This commit is contained in:
1 parent
aedb9d1576
commit
5302e5b62b
31 files changed
+1126
-12
No files matched your search
@@ -317,6 +317,7 @@ function _mapDisplaySets(displaySets, thumbnailImageSrcMap) {
|
||||
numInstances: ds.numImageFrames,
|
||||
countIcon: ds.countIcon,
|
||||
StudyInstanceUID: ds.StudyInstanceUID,
|
||||
messages: ds.messages,
|
||||
componentType,
|
||||
imageSrc,
|
||||
dragData: {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import sortInstancesByPosition from '@ohif/core/src/utils/sortInstancesByPosition';
|
||||
import { constructableModalities } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||
import checkMultiFrame from './utils/validations/checkMultiframe';
|
||||
import checkSingleFrames from './utils/validations/checkSingleFrames';
|
||||
/**
|
||||
* Checks if a series is reconstructable to a 3D volume.
|
||||
*
|
||||
* @param {Object[]} instances An array of `OHIFInstanceMetadata` objects.
|
||||
*/
|
||||
export default function getDisplaySetMessages(
|
||||
instances: Array<any>,
|
||||
isReconstructable: boolean
|
||||
): DisplaySetMessageList {
|
||||
const messages = new DisplaySetMessageList();
|
||||
if (!instances.length) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.NO_VALID_INSTANCES);
|
||||
}
|
||||
|
||||
const firstInstance = instances[0];
|
||||
// Due to current requirements, LOCALIZER series doesn't have any messages
|
||||
if (firstInstance.ImageType.includes('LOCALIZER')) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const Modality = firstInstance.Modality;
|
||||
if (!constructableModalities.includes(Modality)) {
|
||||
return messages;
|
||||
}
|
||||
|
||||
const isMultiframe = firstInstance.NumberOfFrames > 1;
|
||||
// Can't reconstruct if all instances don't have the ImagePositionPatient.
|
||||
if (
|
||||
!isMultiframe &&
|
||||
!instances.every(instance => instance.ImagePositionPatient)
|
||||
) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.NO_POSITION_INFORMATION);
|
||||
}
|
||||
|
||||
const sortedInstances = sortInstancesByPosition(instances);
|
||||
|
||||
isMultiframe
|
||||
? checkMultiFrame(sortedInstances[0], messages)
|
||||
: checkSingleFrames(sortedInstances, messages);
|
||||
|
||||
if (!isReconstructable) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.NOT_RECONSTRUCTABLE);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import sopClassDictionary from '@ohif/core/src/utils/sopClassDictionary';
|
||||
import ImageSet from '@ohif/core/src/classes/ImageSet';
|
||||
import isDisplaySetReconstructable from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import { id } from './id';
|
||||
import getDisplaySetMessages from './getDisplaySetMessages';
|
||||
|
||||
const sopClassHandlerName = 'stack';
|
||||
|
||||
@@ -18,8 +19,9 @@ const makeDisplaySet = instances => {
|
||||
value: isReconstructable,
|
||||
averageSpacingBetweenFrames,
|
||||
} = isDisplaySetReconstructable(instances);
|
||||
|
||||
// set appropriate attributes to image set...
|
||||
const messages = getDisplaySetMessages(instances, isReconstructable);
|
||||
|
||||
imageSet.setAttributes({
|
||||
displaySetInstanceUID: imageSet.uid, // create a local alias for the imageSet UID
|
||||
SeriesDate: instance.SeriesDate,
|
||||
@@ -36,6 +38,7 @@ const makeDisplaySet = instances => {
|
||||
numImageFrames: instances.length,
|
||||
SOPClassHandlerId: `${id}.sopClassHandlerModule.${sopClassHandlerName}`,
|
||||
isReconstructable,
|
||||
messages,
|
||||
averageSpacingBetweenFrames: averageSpacingBetweenFrames || null,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
|
||||
/**
|
||||
* Calculates the scanAxisNormal based on a image orientation vector extract from a frame
|
||||
* @param {*} imageOrientation
|
||||
* @returns
|
||||
*/
|
||||
export default function calculateScanAxisNormal(imageOrientation) {
|
||||
const rowCosineVec = vec3.fromValues(
|
||||
imageOrientation[0],
|
||||
imageOrientation[1],
|
||||
imageOrientation[2]
|
||||
);
|
||||
const colCosineVec = vec3.fromValues(
|
||||
imageOrientation[3],
|
||||
imageOrientation[4],
|
||||
imageOrientation[5]
|
||||
);
|
||||
return vec3.cross(vec3.create(), rowCosineVec, colCosineVec);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||
|
||||
/**
|
||||
* Check if all voxels in series images has same number of components (samplesPerPixel)
|
||||
* @param {*} instances
|
||||
* @returns
|
||||
*/
|
||||
export default function areAllImageComponentsEqual(
|
||||
instances: Array<any>
|
||||
): boolean {
|
||||
if (!instances?.length) {
|
||||
return false;
|
||||
}
|
||||
const firstImage = instances[0];
|
||||
const firstImageSamplesPerPixel = toNumber(firstImage.SamplesPerPixel);
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const { SamplesPerPixel } = instance;
|
||||
|
||||
if (SamplesPerPixel !== firstImageSamplesPerPixel) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||
|
||||
/**
|
||||
* Check if the frames in a series has different dimensions
|
||||
* @param {*} instances
|
||||
* @returns
|
||||
*/
|
||||
export default function areAllImageDimensionsEqual(
|
||||
instances: Array<any>
|
||||
): boolean {
|
||||
if (!instances?.length) {
|
||||
return false;
|
||||
}
|
||||
const firstImage = instances[0];
|
||||
const firstImageRows = toNumber(firstImage.Rows);
|
||||
const firstImageColumns = toNumber(firstImage.Columns);
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const { Rows, Columns } = instance;
|
||||
|
||||
if (Rows !== firstImageRows || Columns !== firstImageColumns) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||
import { _isSameOrientation } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
|
||||
/**
|
||||
* Check is the series has frames with different orientations
|
||||
* @param {*} instances
|
||||
* @returns
|
||||
*/
|
||||
export default function areAllImageOrientationsEqual(
|
||||
instances: Array<any>
|
||||
): boolean {
|
||||
if (!instances?.length) {
|
||||
return false;
|
||||
}
|
||||
const firstImage = instances[0];
|
||||
const firstImageOrientationPatient = toNumber(
|
||||
firstImage.ImageOrientationPatient
|
||||
);
|
||||
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const imageOrientationPatient = toNumber(instance.ImageOrientationPatient);
|
||||
|
||||
if (
|
||||
!_isSameOrientation(imageOrientationPatient, firstImageOrientationPatient)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||
import { _getPerpendicularDistance } from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import calculateScanAxisNormal from '../calculateScanAxisNormal';
|
||||
|
||||
/**
|
||||
* Checks if there is a position shift between consecutive frames
|
||||
* @param {*} previousPosition
|
||||
* @param {*} actualPosition
|
||||
* @param {*} scanAxisNormal
|
||||
* @param {*} averageSpacingBetweenFrames
|
||||
* @returns
|
||||
*/
|
||||
function _checkSeriesPositionShift(
|
||||
previousPosition,
|
||||
actualPosition,
|
||||
scanAxisNormal,
|
||||
averageSpacingBetweenFrames
|
||||
) {
|
||||
// predicted position should be the previous position added by the multiplication
|
||||
// of the scanAxisNormal and the average spacing between frames
|
||||
const predictedPosition = vec3.scaleAndAdd(
|
||||
vec3.create(),
|
||||
previousPosition,
|
||||
scanAxisNormal,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
return (
|
||||
vec3.distance(actualPosition, predictedPosition) >
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a series has position shifts between consecutive frames
|
||||
* @param {*} instances
|
||||
* @returns
|
||||
*/
|
||||
export default function areAllImagePositionsEqual(
|
||||
instances: Array<any>
|
||||
): boolean {
|
||||
if (!instances?.length) {
|
||||
return false;
|
||||
}
|
||||
const firstImageOrientationPatient = toNumber(
|
||||
instances[0].ImageOrientationPatient
|
||||
);
|
||||
const scanAxisNormal = calculateScanAxisNormal(firstImageOrientationPatient);
|
||||
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
|
||||
const lastIpp = toNumber(
|
||||
instances[instances.length - 1].ImagePositionPatient
|
||||
);
|
||||
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(instances.length - 1);
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
|
||||
|
||||
if (
|
||||
_checkSeriesPositionShift(
|
||||
previousImagePositionPatient,
|
||||
imagePositionPatient,
|
||||
scanAxisNormal,
|
||||
averageSpacingBetweenFrames
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
previousImagePositionPatient = imagePositionPatient;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
_getPerpendicularDistance,
|
||||
_getSpacingIssue,
|
||||
reconstructionIssues,
|
||||
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import { DisplaySetMessage } from '@ohif/core';
|
||||
import toNumber from '@ohif/core/src/utils/toNumber';
|
||||
import { DisplaySetMessageList } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Checks if series has spacing issues
|
||||
* @param {*} instances
|
||||
* @param {*} warnings
|
||||
*/
|
||||
export default function areAllImageSpacingEqual(
|
||||
instances: Array<any>,
|
||||
messages: DisplaySetMessageList
|
||||
): void {
|
||||
if (!instances?.length) {
|
||||
return;
|
||||
}
|
||||
const firstImagePositionPatient = toNumber(instances[0].ImagePositionPatient);
|
||||
const lastIpp = toNumber(
|
||||
instances[instances.length - 1].ImagePositionPatient
|
||||
);
|
||||
|
||||
const averageSpacingBetweenFrames =
|
||||
_getPerpendicularDistance(firstImagePositionPatient, lastIpp) /
|
||||
(instances.length - 1);
|
||||
|
||||
let previousImagePositionPatient = firstImagePositionPatient;
|
||||
|
||||
const issuesFound = [];
|
||||
for (let i = 1; i < instances.length; i++) {
|
||||
const instance = instances[i];
|
||||
const imagePositionPatient = toNumber(instance.ImagePositionPatient);
|
||||
|
||||
const spacingBetweenFrames = _getPerpendicularDistance(
|
||||
imagePositionPatient,
|
||||
previousImagePositionPatient
|
||||
);
|
||||
|
||||
const spacingIssue = _getSpacingIssue(
|
||||
spacingBetweenFrames,
|
||||
averageSpacingBetweenFrames
|
||||
);
|
||||
|
||||
if (spacingIssue) {
|
||||
const issue = spacingIssue.issue;
|
||||
|
||||
// avoid multiple warning of the same thing
|
||||
if (!issuesFound.includes(issue)) {
|
||||
issuesFound.push(issue);
|
||||
if (issue === reconstructionIssues.MISSING_FRAMES) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.MISSING_FRAMES);
|
||||
} else if (issue === reconstructionIssues.IRREGULAR_SPACING) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.IRREGULAR_SPACING);
|
||||
}
|
||||
}
|
||||
// we just want to find issues not how many
|
||||
if (issuesFound.length > 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
previousImagePositionPatient = imagePositionPatient;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
hasPixelMeasurements,
|
||||
hasOrientation,
|
||||
hasPosition,
|
||||
} from '@ohif/core/src/utils/isDisplaySetReconstructable';
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Check various multi frame issues. It calls OHIF core functions
|
||||
* @param {*} multiFrameInstance
|
||||
* @param {*} warnings
|
||||
*/
|
||||
export default function checkMultiFrame(
|
||||
multiFrameInstance,
|
||||
messages: DisplaySetMessageList
|
||||
): void {
|
||||
if (!hasPixelMeasurements(multiFrameInstance)) {
|
||||
messages.addMessage(
|
||||
DisplaySetMessage.CODES.MULTIFRAME_NO_PIXEL_MEASUREMENTS
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasOrientation(multiFrameInstance)) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.MULTIFRAME_NO_ORIENTATION);
|
||||
}
|
||||
|
||||
if (!hasPosition(multiFrameInstance)) {
|
||||
messages.addMessage(
|
||||
DisplaySetMessage.CODES.MULTIFRAME_NO_POSITION_INFORMATION
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import areAllImageDimensionsEqual from './areAllImageDimensionsEqual';
|
||||
import areAllImageComponentsEqual from './areAllImageComponentsEqual';
|
||||
import areAllImageOrientationsEqual from './areAllImageOrientationsEqual';
|
||||
import areAllImagePositionsEqual from './areAllImagePositionsEqual';
|
||||
import areAllImageSpacingEqual from './areAllImageSpacingEqual';
|
||||
import { DisplaySetMessage, DisplaySetMessageList } from '@ohif/core';
|
||||
|
||||
/**
|
||||
* Runs various checks in a single frame series
|
||||
* @param {*} instances
|
||||
* @param {*} warnings
|
||||
*/
|
||||
export default function checkSingleFrames(
|
||||
instances: Array<any>,
|
||||
messages: DisplaySetMessageList
|
||||
): void {
|
||||
if (instances.length > 2) {
|
||||
if (!areAllImageDimensionsEqual(instances)) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_DIMENSIONS);
|
||||
}
|
||||
|
||||
if (!areAllImageComponentsEqual(instances)) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_COMPONENTS);
|
||||
}
|
||||
|
||||
if (!areAllImageOrientationsEqual(instances)) {
|
||||
messages.addMessage(DisplaySetMessage.CODES.INCONSISTENT_ORIENTATIONS);
|
||||
}
|
||||
|
||||
if (!areAllImagePositionsEqual(instances)) {
|
||||
messages.addMessage(
|
||||
DisplaySetMessage.CODES.INCONSISTENT_POSITION_INFORMATION
|
||||
);
|
||||
}
|
||||
areAllImageSpacingEqual(instances, messages);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user