Initial commit from prod-batam

This commit is contained in:
mario
2025-05-27 10:51:12 +07:00
commit e0befad0b8
3361 changed files with 304290 additions and 0 deletions

View 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);

View 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;

File diff suppressed because it is too large Load Diff

View 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();
}

View File

@@ -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);
}

View File

@@ -0,0 +1,3 @@
import dicomRTAnnotationExport from './dicomRTAnnotationExport';
export default dicomRTAnnotationExport;

View 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;

View 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);
});
};

View 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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1 @@
export default ['RectangleROIStartEndThreshold'];

View File

@@ -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;