Initial commit from prod-batam
This commit is contained in:
209
extensions/tmtv/src/utils/calculateSUVPeakWorker.js
Normal file
209
extensions/tmtv/src/utils/calculateSUVPeakWorker.js
Normal file
@@ -0,0 +1,209 @@
|
||||
import { utilities } from '@cornerstonejs/core';
|
||||
import { utilities as cstUtils } from '@cornerstonejs/tools';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
|
||||
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
|
||||
import { expose } from 'comlink';
|
||||
|
||||
const createVolume = ({ dimensions, origin, direction, spacing, metadata, scalarData }) => {
|
||||
const imageData = vtkImageData.newInstance();
|
||||
imageData.setDimensions(dimensions);
|
||||
imageData.setOrigin(origin);
|
||||
imageData.setDirection(direction);
|
||||
imageData.setSpacing(spacing);
|
||||
|
||||
const scalarArray = vtkDataArray.newInstance({
|
||||
name: 'Pixels',
|
||||
numberOfComponents: 1,
|
||||
values: scalarData,
|
||||
});
|
||||
|
||||
imageData.getPointData().setScalars(scalarArray);
|
||||
|
||||
imageData.modified();
|
||||
|
||||
const voxelManager = utilities.VoxelManager.createScalarVolumeVoxelManager({
|
||||
scalarData,
|
||||
dimensions,
|
||||
numberOfComponents: 1,
|
||||
});
|
||||
return {
|
||||
imageData,
|
||||
spacing,
|
||||
origin,
|
||||
direction,
|
||||
metadata,
|
||||
voxelManager,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* This method calculates the SUV peak on a segmented ROI from a reference PET
|
||||
* volume. If a rectangle annotation is provided, the peak is calculated within that
|
||||
* rectangle. Otherwise, the calculation is performed on the entire volume which
|
||||
* will be slower but same result.
|
||||
* @param viewport Viewport to use for the calculation
|
||||
* @param labelmap Labelmap from which the mask is taken
|
||||
* @param referenceVolume PET volume to use for SUV calculation
|
||||
* @param toolData [Optional] list of toolData to use for SUV calculation
|
||||
* @param segmentIndex The index of the segment to use for masking
|
||||
* @returns
|
||||
*/
|
||||
function calculateSuvPeak({ labelmapProps, referenceVolumeProps, annotations, segmentIndex = 1 }) {
|
||||
const labelmapInfo = createVolume(labelmapProps);
|
||||
const referenceInfo = createVolume(referenceVolumeProps);
|
||||
|
||||
if (referenceInfo.metadata.Modality !== 'PT') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { dimensions, imageData: labelmapImageData } = labelmapInfo;
|
||||
const { imageData: referenceVolumeImageData } = referenceInfo;
|
||||
|
||||
let boundsIJK;
|
||||
// Todo: using the first annotation for now
|
||||
if (annotations?.length && annotations[0].data?.cachedStats) {
|
||||
const { projectionPoints } = annotations[0].data.cachedStats;
|
||||
const pointsToUse = [].concat(...projectionPoints); // cannot use flat() because of typescript compiler right now
|
||||
|
||||
const rectangleCornersIJK = pointsToUse.map(world => {
|
||||
const ijk = vec3.fromValues(0, 0, 0);
|
||||
referenceVolumeImageData.worldToIndex(world, ijk);
|
||||
return ijk;
|
||||
});
|
||||
|
||||
boundsIJK = cstUtils.boundingBox.getBoundingBoxAroundShape(rectangleCornersIJK, dimensions);
|
||||
}
|
||||
|
||||
let max = 0;
|
||||
let maxIJK = [0, 0, 0];
|
||||
let maxLPS = [0, 0, 0];
|
||||
|
||||
const callback = ({ pointIJK, pointLPS }) => {
|
||||
const value = labelmapInfo.voxelManager.getAtIJKPoint(pointIJK);
|
||||
|
||||
if (value !== segmentIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
const referenceValue = referenceInfo.voxelManager.getAtIJKPoint(pointIJK);
|
||||
|
||||
if (referenceValue > max) {
|
||||
max = referenceValue;
|
||||
maxIJK = pointIJK;
|
||||
maxLPS = pointLPS;
|
||||
}
|
||||
};
|
||||
|
||||
labelmapInfo.voxelManager.forEach(callback, {
|
||||
boundsIJK,
|
||||
imageData: labelmapImageData,
|
||||
isInObject: () => true,
|
||||
returnPoints: true,
|
||||
});
|
||||
|
||||
const direction = labelmapImageData.getDirection().slice(0, 3);
|
||||
|
||||
/**
|
||||
* 2. Find the bottom and top of the great circle for the second sphere (1cc sphere)
|
||||
* V = (4/3)πr3
|
||||
*/
|
||||
const radius = Math.pow(1 / ((4 / 3) * Math.PI), 1 / 3) * 10;
|
||||
const diameter = radius * 2;
|
||||
|
||||
const secondaryCircleWorld = vec3.create();
|
||||
const bottomWorld = vec3.create();
|
||||
const topWorld = vec3.create();
|
||||
referenceVolumeImageData.indexToWorld(maxIJK, secondaryCircleWorld);
|
||||
vec3.scaleAndAdd(bottomWorld, secondaryCircleWorld, direction, -diameter / 2);
|
||||
vec3.scaleAndAdd(topWorld, secondaryCircleWorld, direction, diameter / 2);
|
||||
const suvPeakCirclePoints = [bottomWorld, topWorld];
|
||||
|
||||
/**
|
||||
* 3. Find the Mean and Max of the 1cc sphere centered on the suv Max of the previous
|
||||
* sphere
|
||||
*/
|
||||
let count = 0;
|
||||
let acc = 0;
|
||||
const suvPeakMeanCallback = ({ value }) => {
|
||||
acc += value;
|
||||
count += 1;
|
||||
};
|
||||
|
||||
cstUtils.pointInSurroundingSphereCallback(
|
||||
referenceVolumeImageData,
|
||||
suvPeakCirclePoints,
|
||||
suvPeakMeanCallback
|
||||
);
|
||||
|
||||
const mean = acc / count;
|
||||
|
||||
return {
|
||||
max,
|
||||
maxIJK,
|
||||
maxLPS,
|
||||
mean,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateTMTV(labelmapProps, segmentIndex = 1) {
|
||||
const labelmaps = labelmapProps.map(props => createVolume(props));
|
||||
|
||||
const mergedLabelmap =
|
||||
labelmaps.length === 1
|
||||
? labelmaps[0]
|
||||
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
|
||||
|
||||
const { imageData, spacing } = mergedLabelmap;
|
||||
const values = imageData.getPointData().getScalars().getData();
|
||||
|
||||
// count non-zero values inside the outputData, this would
|
||||
// consider the overlapping regions to be only counted once
|
||||
const numVoxels = values.reduce((acc, curr) => {
|
||||
if (curr > 0) {
|
||||
return acc + 1;
|
||||
}
|
||||
return acc;
|
||||
}, 0);
|
||||
|
||||
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
|
||||
}
|
||||
|
||||
function getTotalLesionGlycolysis({ labelmapProps, referenceVolumeProps }) {
|
||||
const labelmaps = labelmapProps.map(props => createVolume(props));
|
||||
|
||||
const mergedLabelmap =
|
||||
labelmaps.length === 1
|
||||
? labelmaps[0]
|
||||
: cstUtils.segmentation.createMergedLabelmapForIndex(labelmaps);
|
||||
|
||||
// grabbing the first labelmap referenceVolume since it will be the same for all
|
||||
const { spacing } = labelmaps[0];
|
||||
|
||||
const ptVolume = createVolume(referenceVolumeProps);
|
||||
|
||||
let suv = 0;
|
||||
let totalLesionVoxelCount = 0;
|
||||
const scalarDataLength = mergedLabelmap.voxelManager.getScalarDataLength();
|
||||
for (let i = 0; i < scalarDataLength; i++) {
|
||||
// if not background
|
||||
if (mergedLabelmap.voxelManager.getAtIndex(i) !== 0) {
|
||||
suv += ptVolume.voxelManager.getAtIndex(i);
|
||||
totalLesionVoxelCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Average SUV for the merged labelmap
|
||||
const averageSuv = suv / totalLesionVoxelCount;
|
||||
|
||||
// total Lesion Glycolysis [suv * ml]
|
||||
return averageSuv * totalLesionVoxelCount * spacing[0] * spacing[1] * spacing[2] * 1e-3;
|
||||
}
|
||||
|
||||
const obj = {
|
||||
calculateSuvPeak,
|
||||
calculateTMTV,
|
||||
getTotalLesionGlycolysis,
|
||||
};
|
||||
|
||||
expose(obj);
|
||||
42
extensions/tmtv/src/utils/calculateTMTV.ts
Normal file
42
extensions/tmtv/src/utils/calculateTMTV.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Types } from '@cornerstonejs/core';
|
||||
import { utilities } from '@cornerstonejs/tools';
|
||||
|
||||
/**
|
||||
* Given a list of labelmaps (with the possibility of overlapping regions),
|
||||
* and a referenceVolume, it calculates the total metabolic tumor volume (TMTV)
|
||||
* by flattening and rasterizing each segment into a single labelmap and summing
|
||||
* the total number of volume voxels. It should be noted that for this calculation
|
||||
* we do not double count voxels that are part of multiple labelmaps.
|
||||
* @param {} labelmaps
|
||||
* @param {number} segmentIndex
|
||||
* @returns {number} TMTV in ml
|
||||
*/
|
||||
function calculateTMTV(labelmaps: Array<Types.IImageVolume>, segmentIndex = 1): number {
|
||||
const volumeId = 'mergedLabelmap';
|
||||
|
||||
const mergedLabelmap = utilities.segmentation.createMergedLabelmapForIndex(
|
||||
labelmaps,
|
||||
segmentIndex,
|
||||
volumeId
|
||||
);
|
||||
|
||||
const { imageData, spacing, voxelManager } = mergedLabelmap;
|
||||
|
||||
// count non-zero values inside the outputData, this would
|
||||
// consider the overlapping regions to be only counted once
|
||||
let numVoxels = 0;
|
||||
const callback = ({ value }) => {
|
||||
if (value > 0) {
|
||||
numVoxels += 1;
|
||||
}
|
||||
};
|
||||
|
||||
voxelManager.forEach(callback, {
|
||||
imageData,
|
||||
isInObject: () => true,
|
||||
});
|
||||
|
||||
return 1e-3 * numVoxels * spacing[0] * spacing[1] * spacing[2];
|
||||
}
|
||||
|
||||
export default calculateTMTV;
|
||||
1554
extensions/tmtv/src/utils/colormaps/index.js
Normal file
1554
extensions/tmtv/src/utils/colormaps/index.js
Normal file
File diff suppressed because it is too large
Load Diff
45
extensions/tmtv/src/utils/createAndDownloadTMTVReport.js
Normal file
45
extensions/tmtv/src/utils/createAndDownloadTMTVReport.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export default function createAndDownloadTMTVReport(segReport, additionalReportRows, options = {}) {
|
||||
const firstReport = segReport[Object.keys(segReport)[0]];
|
||||
const columns = Object.keys(firstReport);
|
||||
const csv = [columns.join(',')];
|
||||
|
||||
Object.values(segReport).forEach(segmentation => {
|
||||
const row = [];
|
||||
columns.forEach(column => {
|
||||
// if it is array then we need to replace , with space to avoid csv parsing error
|
||||
row.push(
|
||||
Array.isArray(segmentation[column]) ? segmentation[column].join(' ') : segmentation[column]
|
||||
);
|
||||
});
|
||||
csv.push(row.join(','));
|
||||
});
|
||||
|
||||
csv.push('');
|
||||
csv.push('');
|
||||
csv.push('');
|
||||
|
||||
csv.push(`Patient ID,${firstReport.PatientID}`);
|
||||
csv.push(`Study Date,${firstReport.StudyDate}`);
|
||||
csv.push('');
|
||||
additionalReportRows.forEach(({ key, value: values }) => {
|
||||
const temp = [];
|
||||
temp.push(`${key}`);
|
||||
Object.keys(values).forEach(k => {
|
||||
temp.push(`${k}`);
|
||||
temp.push(`${values[k]}`);
|
||||
});
|
||||
|
||||
csv.push(temp.join(','));
|
||||
});
|
||||
|
||||
const blob = new Blob([csv.join('\n')], {
|
||||
type: 'text/csv;charset=utf-8',
|
||||
});
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = options.filename ?? `${firstReport.PatientID}_tmtv.csv`;
|
||||
a.click();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import dcmjs from 'dcmjs';
|
||||
import { classes, DicomMetadataStore } from '@ohif/core';
|
||||
import { adaptersRT } from '@cornerstonejs/adapters';
|
||||
|
||||
const { datasetToBlob } = dcmjs.data;
|
||||
const metadataProvider = classes.MetadataProvider;
|
||||
|
||||
export default function dicomRTAnnotationExport(annotations) {
|
||||
const dataset = adaptersRT.Cornerstone3D.RTSS.generateRTSSFromAnnotations(
|
||||
annotations,
|
||||
metadataProvider,
|
||||
DicomMetadataStore
|
||||
);
|
||||
const reportBlob = datasetToBlob(dataset);
|
||||
|
||||
//Create a URL for the binary.
|
||||
var objectUrl = URL.createObjectURL(reportBlob);
|
||||
window.location.assign(objectUrl);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import dicomRTAnnotationExport from './dicomRTAnnotationExport';
|
||||
|
||||
export default dicomRTAnnotationExport;
|
||||
73
extensions/tmtv/src/utils/getThresholdValue.ts
Normal file
73
extensions/tmtv/src/utils/getThresholdValue.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as csTools from '@cornerstonejs/tools';
|
||||
|
||||
function getRoiStats(referencedVolume, annotations) {
|
||||
// roiStats
|
||||
const { imageData } = referencedVolume;
|
||||
const values = imageData.getPointData().getScalars().getData();
|
||||
|
||||
// Todo: add support for other strategies
|
||||
const { fn, baseValue } = _getStrategyFn('max');
|
||||
let value = baseValue;
|
||||
|
||||
const boundsIJK = csTools.utilities.rectangleROITool.getBoundsIJKFromRectangleAnnotations(
|
||||
annotations,
|
||||
referencedVolume
|
||||
);
|
||||
|
||||
const [[iMin, iMax], [jMin, jMax], [kMin, kMax]] = boundsIJK;
|
||||
|
||||
for (let i = iMin; i <= iMax; i++) {
|
||||
for (let j = jMin; j <= jMax; j++) {
|
||||
for (let k = kMin; k <= kMax; k++) {
|
||||
const offset = imageData.computeOffsetIndex([i, j, k]);
|
||||
value = fn(values[offset], value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function getThresholdValues(
|
||||
annotationUIDs,
|
||||
referencedVolumes,
|
||||
config
|
||||
): { ptLower: number; ptUpper: number; ctLower: number; ctUpper: number } {
|
||||
if (config.strategy === 'range') {
|
||||
return {
|
||||
ptLower: Number(config.ptLower),
|
||||
ptUpper: Number(config.ptUpper),
|
||||
ctLower: Number(config.ctLower),
|
||||
ctUpper: Number(config.ctUpper),
|
||||
};
|
||||
}
|
||||
|
||||
const { weight } = config;
|
||||
const annotations = annotationUIDs.map(annotationUID =>
|
||||
csTools.annotation.state.getAnnotation(annotationUID)
|
||||
);
|
||||
|
||||
const ptValue = getRoiStats(referencedVolumes[0], annotations);
|
||||
|
||||
return {
|
||||
ctLower: -Infinity,
|
||||
ctUpper: +Infinity,
|
||||
ptLower: weight * ptValue,
|
||||
ptUpper: +Infinity,
|
||||
};
|
||||
}
|
||||
|
||||
function _getStrategyFn(statistic): {
|
||||
fn: (a: number, b: number) => number;
|
||||
baseValue: number;
|
||||
} {
|
||||
const baseValue = -Infinity;
|
||||
const fn = (number, maxValue) => {
|
||||
if (number > maxValue) {
|
||||
maxValue = number;
|
||||
}
|
||||
return maxValue;
|
||||
};
|
||||
return { fn, baseValue };
|
||||
}
|
||||
|
||||
export default getThresholdValues;
|
||||
97
extensions/tmtv/src/utils/handleROIThresholding.ts
Normal file
97
extensions/tmtv/src/utils/handleROIThresholding.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Segment, Segmentation } from '@cornerstonejs/tools/types';
|
||||
import { triggerEvent, eventTarget, Enums } from '@cornerstonejs/core';
|
||||
|
||||
export const handleROIThresholding = async ({
|
||||
segmentationId,
|
||||
commandsManager,
|
||||
segmentationService,
|
||||
}: withAppTypes<{
|
||||
segmentationId: string;
|
||||
}>) => {
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
|
||||
progress: 0,
|
||||
type: 'Calculate Lesion Stats',
|
||||
id: segmentationId,
|
||||
});
|
||||
|
||||
// re-calculating the cached stats for the active segmentation
|
||||
const updatedPerSegmentCachedStats = {};
|
||||
for (const [segmentIndex, segment] of Object.entries(segmentation.segments)) {
|
||||
if (!segment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const numericSegmentIndex = Number(segmentIndex);
|
||||
|
||||
const lesionStats = await commandsManager.run('getLesionStats', {
|
||||
segmentationId,
|
||||
segmentIndex: numericSegmentIndex,
|
||||
});
|
||||
|
||||
const suvPeak = await commandsManager.run('calculateSuvPeak', {
|
||||
segmentationId,
|
||||
segmentIndex: numericSegmentIndex,
|
||||
});
|
||||
|
||||
const lesionGlyoclysisStats = lesionStats.volume * lesionStats.meanValue;
|
||||
|
||||
// update segDetails with the suv peak for the active segmentation
|
||||
const cachedStats = {
|
||||
lesionStats,
|
||||
suvPeak,
|
||||
lesionGlyoclysisStats,
|
||||
};
|
||||
|
||||
const updatedSegment: Segment = {
|
||||
...segment,
|
||||
cachedStats: {
|
||||
...segment.cachedStats,
|
||||
...cachedStats,
|
||||
},
|
||||
};
|
||||
|
||||
updatedPerSegmentCachedStats[numericSegmentIndex] = cachedStats;
|
||||
|
||||
segmentation.segments[segmentIndex] = updatedSegment;
|
||||
}
|
||||
|
||||
// all available segmentations
|
||||
const segmentations = segmentationService.getSegmentations();
|
||||
const tmtv = await commandsManager.run('calculateTMTV', { segmentations });
|
||||
|
||||
triggerEvent(eventTarget, Enums.Events.WEB_WORKER_PROGRESS, {
|
||||
progress: 100,
|
||||
type: 'Calculate Lesion Stats',
|
||||
id: segmentationId,
|
||||
});
|
||||
|
||||
// add the tmtv to all the segment cachedStats, although it is a global
|
||||
// value but we don't have any other way to display it for now
|
||||
// Update all segmentations with the calculated TMTV
|
||||
segmentations.forEach(segmentation => {
|
||||
segmentation.cachedStats = {
|
||||
...segmentation.cachedStats,
|
||||
tmtv,
|
||||
};
|
||||
|
||||
// Update each segment within the segmentation
|
||||
Object.keys(segmentation.segments).forEach(segmentIndex => {
|
||||
segmentation.segments[segmentIndex].cachedStats = {
|
||||
...segmentation.segments[segmentIndex].cachedStats,
|
||||
tmtv,
|
||||
};
|
||||
});
|
||||
|
||||
// Update the segmentation object
|
||||
const updatedSegmentation: Segmentation = {
|
||||
...segmentation,
|
||||
segments: {
|
||||
...segmentation.segments,
|
||||
},
|
||||
};
|
||||
|
||||
segmentationService.addOrUpdateSegmentation(updatedSegmentation);
|
||||
});
|
||||
};
|
||||
494
extensions/tmtv/src/utils/hpViewports.ts
Normal file
494
extensions/tmtv/src/utils/hpViewports.ts
Normal file
@@ -0,0 +1,494 @@
|
||||
// Common sync group configurations
|
||||
const cameraPositionSync = (id: string) => ({
|
||||
type: 'cameraPosition',
|
||||
id,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
|
||||
const hydrateSegSync = {
|
||||
type: 'hydrateseg',
|
||||
id: 'sameFORId',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
matchingRules: ['sameFOR'],
|
||||
},
|
||||
};
|
||||
|
||||
const ctAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctAXIAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
initialImageOptions: {
|
||||
// index: 5,
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ctSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctSAGITTAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
syncGroups: [
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ctCORONAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ctCORONAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: 'ctToolGroup',
|
||||
syncGroups: [
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ptAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptAXIAL',
|
||||
viewportType: 'volume',
|
||||
background: [1, 1, 1],
|
||||
orientation: 'axial',
|
||||
toolGroupId: 'ptToolGroup',
|
||||
initialImageOptions: {
|
||||
// index: 5,
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: true,
|
||||
target: false,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
voiInverted: true,
|
||||
},
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ptSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptSAGITTAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
background: [1, 1, 1],
|
||||
toolGroupId: 'ptToolGroup',
|
||||
syncGroups: [
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: true,
|
||||
target: false,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
voiInverted: true,
|
||||
},
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ptCORONAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'ptCORONAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
background: [1, 1, 1],
|
||||
toolGroupId: 'ptToolGroup',
|
||||
syncGroups: [
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: true,
|
||||
target: false,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: {
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
voiInverted: true,
|
||||
},
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fusionAXIAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionAXIAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
toolGroupId: 'fusionToolGroup',
|
||||
initialImageOptions: {
|
||||
// index: 5,
|
||||
preset: 'first', // 'first', 'last', 'middle'
|
||||
},
|
||||
syncGroups: [
|
||||
cameraPositionSync('axialSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: {
|
||||
colormap: {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fusionSAGITTAL = {
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionSAGITTAL',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
toolGroupId: 'fusionToolGroup',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
syncGroups: [
|
||||
cameraPositionSync('sagittalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: {
|
||||
colormap: {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const fusionCORONAL = {
|
||||
viewportOptions: {
|
||||
viewportId: 'fusionCoronal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
toolGroupId: 'fusionToolGroup',
|
||||
// initialImageOptions: {
|
||||
// index: 180,
|
||||
// preset: 'middle', // 'first', 'last', 'middle'
|
||||
// },
|
||||
syncGroups: [
|
||||
cameraPositionSync('coronalSync'),
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ctWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'fusionWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: false,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
id: 'ctDisplaySet',
|
||||
},
|
||||
{
|
||||
id: 'ptDisplaySet',
|
||||
options: {
|
||||
colormap: {
|
||||
name: 'hsv',
|
||||
opacity: [
|
||||
{ value: 0, opacity: 0 },
|
||||
{ value: 0.1, opacity: 0.8 },
|
||||
{ value: 1, opacity: 0.9 },
|
||||
],
|
||||
},
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mipSAGITTAL: AppTypes.HangingProtocol.Viewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'mipSagittal',
|
||||
viewportType: 'volume',
|
||||
orientation: 'sagittal',
|
||||
background: [1, 1, 1],
|
||||
toolGroupId: 'mipToolGroup',
|
||||
syncGroups: [
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptWLSync',
|
||||
source: true,
|
||||
target: true,
|
||||
options: {
|
||||
syncColormap: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'ptFusionWLSync',
|
||||
source: true,
|
||||
target: false,
|
||||
options: {
|
||||
syncColormap: false,
|
||||
syncInvertState: false,
|
||||
},
|
||||
},
|
||||
hydrateSegSync,
|
||||
],
|
||||
|
||||
// Custom props can be used to set custom properties which extensions
|
||||
// can react on.
|
||||
customViewportProps: {
|
||||
// We use viewportDisplay to filter the viewports which are displayed
|
||||
// in mip and we set the scrollbar according to their rotation index
|
||||
// in the cornerstone extension.
|
||||
hideOverlays: true,
|
||||
},
|
||||
},
|
||||
displaySets: [
|
||||
{
|
||||
options: {
|
||||
blendMode: 'MIP',
|
||||
slabThickness: 'fullVolume',
|
||||
voi: {
|
||||
custom: 'getPTVOIRange',
|
||||
},
|
||||
voiInverted: true,
|
||||
},
|
||||
id: 'ptDisplaySet',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export {
|
||||
ctAXIAL,
|
||||
ctSAGITTAL,
|
||||
ctCORONAL,
|
||||
ptAXIAL,
|
||||
ptSAGITTAL,
|
||||
ptCORONAL,
|
||||
fusionAXIAL,
|
||||
fusionSAGITTAL,
|
||||
fusionCORONAL,
|
||||
mipSAGITTAL,
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone';
|
||||
|
||||
const CircleROIStartEndThreshold = {
|
||||
toAnnotation: (measurement, definition) => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Length tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
cornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
const { cachedStats } = data;
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
// points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: metadata.label,
|
||||
// displayText: displayText,
|
||||
data: data.cachedStats,
|
||||
type: 'CircleROIStartEndThreshold',
|
||||
// getReport,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default CircleROIStartEndThreshold;
|
||||
@@ -0,0 +1,63 @@
|
||||
import SUPPORTED_TOOLS from './constants/supportedTools';
|
||||
import { getSOPInstanceAttributes } from '@ohif/extension-cornerstone';
|
||||
|
||||
const RectangleROIStartEndThreshold = {
|
||||
toAnnotation: (measurement, definition) => {},
|
||||
|
||||
/**
|
||||
* Maps cornerstone annotation event data to measurement service format.
|
||||
*
|
||||
* @param {Object} cornerstone Cornerstone event data
|
||||
* @return {Measurement} Measurement instance
|
||||
*/
|
||||
toMeasurement: (csToolsEventDetail, displaySetService, cornerstoneViewportService) => {
|
||||
const { annotation, viewportId } = csToolsEventDetail;
|
||||
const { metadata, data, annotationUID } = annotation;
|
||||
|
||||
if (!metadata || !data) {
|
||||
console.warn('Length tool: Missing metadata or data');
|
||||
return null;
|
||||
}
|
||||
|
||||
const { toolName, referencedImageId, FrameOfReferenceUID } = metadata;
|
||||
const validToolType = SUPPORTED_TOOLS.includes(toolName);
|
||||
|
||||
if (!validToolType) {
|
||||
throw new Error('Tool not supported');
|
||||
}
|
||||
|
||||
const { SOPInstanceUID, SeriesInstanceUID, StudyInstanceUID } = getSOPInstanceAttributes(
|
||||
referencedImageId,
|
||||
cornerstoneViewportService,
|
||||
viewportId
|
||||
);
|
||||
|
||||
let displaySet;
|
||||
|
||||
if (SOPInstanceUID) {
|
||||
displaySet = displaySetService.getDisplaySetForSOPInstanceUID(
|
||||
SOPInstanceUID,
|
||||
SeriesInstanceUID
|
||||
);
|
||||
} else {
|
||||
displaySet = displaySetService.getDisplaySetsForSeries(SeriesInstanceUID);
|
||||
}
|
||||
|
||||
return {
|
||||
uid: annotationUID,
|
||||
SOPInstanceUID,
|
||||
FrameOfReferenceUID,
|
||||
// points,
|
||||
metadata,
|
||||
referenceSeriesUID: SeriesInstanceUID,
|
||||
referenceStudyUID: StudyInstanceUID,
|
||||
toolName: metadata.toolName,
|
||||
displaySetInstanceUID: displaySet.displaySetInstanceUID,
|
||||
label: metadata.label,
|
||||
data: data.cachedStats,
|
||||
type: 'RectangleROIStartEndThreshold',
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
export default RectangleROIStartEndThreshold;
|
||||
@@ -0,0 +1 @@
|
||||
export default ['RectangleROIStartEndThreshold'];
|
||||
@@ -0,0 +1,41 @@
|
||||
import RectangleROIStartEndThreshold from './RectangleROIStartEndThreshold';
|
||||
import CircleROIStartEndThreshold from './CircleROIStartEndThreshold';
|
||||
|
||||
const measurementServiceMappingsFactory = (
|
||||
measurementService,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
) => {
|
||||
return {
|
||||
RectangleROIStartEndThreshold: {
|
||||
toAnnotation: RectangleROIStartEndThreshold.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
RectangleROIStartEndThreshold.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
CircleROIStartEndThreshold: {
|
||||
toAnnotation: CircleROIStartEndThreshold.toAnnotation,
|
||||
toMeasurement: csToolsAnnotation =>
|
||||
CircleROIStartEndThreshold.toMeasurement(
|
||||
csToolsAnnotation,
|
||||
displaySetService,
|
||||
cornerstoneViewportService
|
||||
),
|
||||
matchingCriteria: [
|
||||
{
|
||||
valueType: measurementService.VALUE_TYPES.ROI_THRESHOLD_MANUAL,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default measurementServiceMappingsFactory;
|
||||
Reference in New Issue
Block a user