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:
rodrigobasilio2022andIgor Octaviano authored and GitHub committed 2023-08-11 19:16:25 -03:00
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);
}
}