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,5 @@
// We need to define a UID for this extension as a device, and it should be the same for all saves:
const uid = '2.25.285241207697168520771311899641885187923';
export default uid;

View File

@@ -0,0 +1,186 @@
import areaOfPolygon from './areaOfPolygon';
import { PubSubService } from '@ohif/core';
const EVENTS = {
LABEL_UPDATED: 'labelUpdated',
GRAPHIC_UPDATED: 'graphicUpdated',
VIEW_UPDATED: 'viewUpdated',
REMOVED: 'removed',
};
/**
* Represents a single annotation for the Microscopy Viewer
*/
class RoiAnnotation extends PubSubService {
constructor(roiGraphic, studyInstanceUID, seriesInstanceUID, label = '', viewState = null) {
super(EVENTS);
this.uid = roiGraphic.uid;
this.roiGraphic = roiGraphic;
this.studyInstanceUID = studyInstanceUID;
this.seriesInstanceUID = seriesInstanceUID;
this.label = label;
this.viewState = viewState;
this.setMeasurements(roiGraphic);
}
getScoord3d() {
const roiGraphic = this.roiGraphic;
const roiGraphicSymbols = Object.getOwnPropertySymbols(roiGraphic);
const _scoord3d = roiGraphicSymbols.find(s => String(s) === 'Symbol(scoord3d)');
return roiGraphic[_scoord3d];
}
getCoordinates() {
const scoord3d = this.getScoord3d();
const scoord3dSymbols = Object.getOwnPropertySymbols(scoord3d);
const _coordinates = scoord3dSymbols.find(s => String(s) === 'Symbol(coordinates)');
const coordinates = scoord3d[_coordinates];
return coordinates;
}
/**
* When called will trigger the REMOVED event
*/
destroy() {
this._broadcastEvent(EVENTS.REMOVED, this);
}
/**
* Updates the ROI graphic for the annotation and triggers the GRAPHIC_UPDATED
* event
*
* @param {Object} roiGraphic
*/
setRoiGraphic(roiGraphic) {
this.roiGraphic = roiGraphic;
this.setMeasurements();
this._broadcastEvent(EVENTS.GRAPHIC_UPDATED, this);
}
/**
* Update ROI measurement values based on its scoord3d coordinates.
*
* @returns {void}
*/
setMeasurements() {
const type = this.roiGraphic.scoord3d.graphicType;
const coordinates = this.roiGraphic.scoord3d.graphicData;
switch (type) {
case 'ELLIPSE':
// This is a circle so only need one side
const point1 = coordinates[0];
const point2 = coordinates[1];
let xLength2 = point2[0] - point1[0];
let yLength2 = point2[1] - point1[1];
xLength2 *= xLength2;
yLength2 *= yLength2;
const length = Math.sqrt(xLength2 + yLength2);
const radius = length / 2;
const areaEllipse = Math.PI * radius * radius;
this._area = areaEllipse;
this._length = undefined;
break;
case 'POLYGON':
const areaPolygon = areaOfPolygon(coordinates);
this._area = areaPolygon;
this._length = undefined;
break;
case 'POINT':
this._area = undefined;
this._length = undefined;
break;
case 'POLYLINE':
let len = 0;
for (let i = 1; i < coordinates.length; i++) {
const p1 = coordinates[i - 1];
const p2 = coordinates[i];
let xLen = p2[0] - p1[0];
let yLen = p2[1] - p1[1];
xLen *= xLen;
yLen *= yLen;
len += Math.sqrt(xLen + yLen);
}
this._area = undefined;
this._length = len;
break;
}
}
/**
* Update the OpenLayer Map's view state for the annotation and triggers the
* VIEW_UPDATED event
*
* @param {Object} viewState The new view state for the annotation
*/
setViewState(viewState) {
this.viewState = viewState;
this._broadcastEvent(EVENTS.VIEW_UPDATED, this);
}
/**
* Update the label for the annotation and triggers the LABEL_UPDATED event
*
* @param {String} label New label for the annotation
*/
setLabel(label, finding) {
this.label = label || (finding && finding.CodeMeaning);
this.finding = finding || {
CodingSchemeDesignator: '@ohif/extension-dicom-microscopy',
CodeValue: label,
CodeMeaning: label,
};
this._broadcastEvent(EVENTS.LABEL_UPDATED, this);
}
/**
* Returns the geometry type of the annotation concatenated with the label
* defined for the annotation.
* Difference with getDetailedLabel() is that this will return empty string for empty
* label.
*
* @returns {String} Text with geometry type and label
*/
getLabel() {
const label = this.label ? `${this.label}` : '';
return label;
}
/**
* Returns the geometry type of the annotation concatenated with the label
* defined for the annotation
*
* @returns {String} Text with geometry type and label
*/
getDetailedLabel() {
const label = this.label ? `${this.label}` : '(empty)';
return label;
}
getLength() {
return this._length;
}
getArea() {
return this._area;
}
}
export { EVENTS };
export default RoiAnnotation;

View File

@@ -0,0 +1,15 @@
export default function areaOfPolygon(coordinates) {
// Shoelace algorithm.
const n = coordinates.length;
let area = 0.0;
let j = n - 1;
for (let i = 0; i < n; i++) {
area += (coordinates[j][0] + coordinates[i][0]) * (coordinates[j][1] - coordinates[i][1]);
j = i; // j is previous vertex to i
}
// Return absolute value of half the sum
// (The value is halved as we are summing up triangles, not rectangles).
return Math.abs(area / 2.0);
}

View File

@@ -0,0 +1,192 @@
import dcmjs from 'dcmjs';
import DEVICE_OBSERVER_UID from './DEVICE_OBSERVER_UID';
/**
*
* @param {*} metadata - Microscopy Image instance metadata
* @param {*} SeriesDescription - SR description
* @param {*} annotations - Annotations
*
* @return Comprehensive3DSR dataset
*/
export default function constructSR(metadata, { SeriesDescription, SeriesNumber }, annotations) {
// Handle malformed data
if (!metadata.SpecimenDescriptionSequence) {
metadata.SpecimenDescriptionSequence = {
SpecimenUID: metadata.SeriesInstanceUID,
SpecimenIdentifier: metadata.SeriesDescription,
};
}
const { SpecimenDescriptionSequence } = metadata;
// construct Comprehensive3DSR dataset
const observationContext = new dcmjs.sr.templates.ObservationContext({
observerPersonContext: new dcmjs.sr.templates.ObserverContext({
observerType: new dcmjs.sr.coding.CodedConcept({
value: '121006',
schemeDesignator: 'DCM',
meaning: 'Person',
}),
observerIdentifyingAttributes: new dcmjs.sr.templates.PersonObserverIdentifyingAttributes({
name: '@ohif/extension-dicom-microscopy',
}),
}),
observerDeviceContext: new dcmjs.sr.templates.ObserverContext({
observerType: new dcmjs.sr.coding.CodedConcept({
value: '121007',
schemeDesignator: 'DCM',
meaning: 'Device',
}),
observerIdentifyingAttributes: new dcmjs.sr.templates.DeviceObserverIdentifyingAttributes({
uid: DEVICE_OBSERVER_UID,
}),
}),
subjectContext: new dcmjs.sr.templates.SubjectContext({
subjectClass: new dcmjs.sr.coding.CodedConcept({
value: '121027',
schemeDesignator: 'DCM',
meaning: 'Specimen',
}),
subjectClassSpecificContext: new dcmjs.sr.templates.SubjectContextSpecimen({
uid: SpecimenDescriptionSequence.SpecimenUID,
identifier: SpecimenDescriptionSequence.SpecimenIdentifier || metadata.SeriesInstanceUID,
containerIdentifier: metadata.ContainerIdentifier || metadata.SeriesInstanceUID,
}),
}),
});
const imagingMeasurements = [];
for (let i = 0; i < annotations.length; i++) {
const { roiGraphic: roi, label } = annotations[i];
let { measurements, evaluations, marker, presentationState } = roi.properties;
console.log('[SR] storing marker...', marker);
console.log('[SR] storing measurements...', measurements);
console.log('[SR] storing evaluations...', evaluations);
console.log('[SR] storing presentation state...', presentationState);
if (presentationState) {
presentationState.marker = marker;
}
/** Avoid incompatibility with dcmjs */
measurements = measurements.map((measurement: any) => {
const ConceptName = Array.isArray(measurement.ConceptNameCodeSequence)
? measurement.ConceptNameCodeSequence[0]
: measurement.ConceptNameCodeSequence;
const MeasuredValue = Array.isArray(measurement.MeasuredValueSequence)
? measurement.MeasuredValueSequence[0]
: measurement.MeasuredValueSequence;
const MeasuredValueUnits = Array.isArray(MeasuredValue.MeasurementUnitsCodeSequence)
? MeasuredValue.MeasurementUnitsCodeSequence[0]
: MeasuredValue.MeasurementUnitsCodeSequence;
return new dcmjs.sr.valueTypes.NumContentItem({
name: new dcmjs.sr.coding.CodedConcept({
meaning: ConceptName.CodeMeaning,
value: ConceptName.CodeValue,
schemeDesignator: ConceptName.CodingSchemeDesignator,
}),
value: MeasuredValue.NumericValue,
unit: new dcmjs.sr.coding.CodedConcept({
value: MeasuredValueUnits.CodeValue,
meaning: MeasuredValueUnits.CodeMeaning,
schemeDesignator: MeasuredValueUnits.CodingSchemeDesignator,
}),
});
});
/** Avoid incompatibility with dcmjs */
evaluations = evaluations.map((evaluation: any) => {
const ConceptName = Array.isArray(evaluation.ConceptNameCodeSequence)
? evaluation.ConceptNameCodeSequence[0]
: evaluation.ConceptNameCodeSequence;
return new dcmjs.sr.valueTypes.TextContentItem({
name: new dcmjs.sr.coding.CodedConcept({
value: ConceptName.CodeValue,
meaning: ConceptName.CodeMeaning,
schemeDesignator: ConceptName.CodingSchemeDesignator,
}),
value: evaluation.TextValue,
relationshipType: evaluation.RelationshipType,
});
});
const identifier = `ROI #${i + 1}`;
const group = new dcmjs.sr.templates.PlanarROIMeasurementsAndQualitativeEvaluations({
trackingIdentifier: new dcmjs.sr.templates.TrackingIdentifier({
uid: roi.uid,
identifier: presentationState
? identifier.concat(`(${JSON.stringify(presentationState)})`)
: identifier,
}),
referencedRegion: new dcmjs.sr.contentItems.ImageRegion3D({
graphicType: roi.scoord3d.graphicType,
graphicData: roi.scoord3d.graphicData,
frameOfReferenceUID: roi.scoord3d.frameOfReferenceUID,
}),
findingType: new dcmjs.sr.coding.CodedConcept({
value: label,
schemeDesignator: '@ohif/extension-dicom-microscopy',
meaning: 'FREETEXT',
}),
/** Evaluations will conflict with current tracking identifier */
/** qualitativeEvaluations: evaluations, */
measurements,
});
imagingMeasurements.push(...group);
}
const measurementReport = new dcmjs.sr.templates.MeasurementReport({
languageOfContentItemAndDescendants: new dcmjs.sr.templates.LanguageOfContentItemAndDescendants(
{}
),
observationContext,
procedureReported: new dcmjs.sr.coding.CodedConcept({
value: '112703',
schemeDesignator: 'DCM',
meaning: 'Whole Slide Imaging',
}),
imagingMeasurements,
});
const dataset = new dcmjs.sr.documents.Comprehensive3DSR({
content: measurementReport[0],
evidence: [metadata],
seriesInstanceUID: dcmjs.data.DicomMetaDictionary.uid(),
seriesNumber: SeriesNumber,
seriesDescription: SeriesDescription || 'Whole slide imaging structured report',
sopInstanceUID: dcmjs.data.DicomMetaDictionary.uid(),
instanceNumber: 1,
manufacturer: 'dcmjs-org',
});
dataset.SpecificCharacterSet = 'ISO_IR 192';
const fileMetaInformationVersionArray = new Uint8Array(2);
fileMetaInformationVersionArray[1] = 1;
dataset._meta = {
FileMetaInformationVersion: {
Value: [fileMetaInformationVersionArray.buffer], // TODO
vr: 'OB',
},
MediaStorageSOPClassUID: dataset.sopClassUID,
MediaStorageSOPInstanceUID: dataset.sopInstanceUID,
TransferSyntaxUID: {
Value: ['1.2.840.10008.1.2.1'],
vr: 'UI',
},
ImplementationClassUID: {
Value: [dcmjs.data.DicomMetaDictionary.uid()],
vr: 'UI',
},
ImplementationVersionName: {
Value: ['@ohif/extension-dicom-microscopy'],
vr: 'SH',
},
};
return dataset;
}

View File

@@ -0,0 +1,109 @@
import { inv, multiply } from 'mathjs';
// TODO -> This is pulled out of some internal logic from Dicom Microscopy Viewer,
// We should likely just expose this there.
export default function coordinateFormatScoord3d2Geometry(coordinates, pyramid) {
let transform = false;
if (!Array.isArray(coordinates[0])) {
coordinates = [coordinates];
transform = true;
}
const metadata = pyramid[pyramid.length - 1];
const orientation = metadata.ImageOrientationSlide;
const spacing = _getPixelSpacing(metadata);
const origin = metadata.TotalPixelMatrixOriginSequence[0];
const offset = [
Number(origin.XOffsetInSlideCoordinateSystem),
Number(origin.YOffsetInSlideCoordinateSystem),
];
coordinates = coordinates.map(c => {
const slideCoord = [c[0], c[1]];
const pixelCoord = mapSlideCoord2PixelCoord({
offset,
orientation,
spacing,
point: slideCoord,
});
return [pixelCoord[0], -(pixelCoord[1] + 1), 0];
});
if (transform) {
return coordinates[0];
}
return coordinates;
}
function _getPixelSpacing(metadata) {
if (metadata.PixelSpacing) {
return metadata.PixelSpacing;
}
const functionalGroup = metadata.SharedFunctionalGroupsSequence[0];
const pixelMeasures = functionalGroup.PixelMeasuresSequence[0];
return pixelMeasures.PixelSpacing;
}
function mapSlideCoord2PixelCoord(options) {
// X and Y Offset in Slide Coordinate System
if (!('offset' in options)) {
throw new Error('Option "offset" is required.');
}
if (!Array.isArray(options.offset)) {
throw new Error('Option "offset" must be an array.');
}
if (options.offset.length !== 2) {
throw new Error('Option "offset" must be an array with 2 elements.');
}
const offset = options.offset;
// Image Orientation Slide with direction cosines for Row and Column direction
if (!('orientation' in options)) {
throw new Error('Option "orientation" is required.');
}
if (!Array.isArray(options.orientation)) {
throw new Error('Option "orientation" must be an array.');
}
if (options.orientation.length !== 6) {
throw new Error('Option "orientation" must be an array with 6 elements.');
}
const orientation = options.orientation;
// Pixel Spacing along the Row and Column direction
if (!('spacing' in options)) {
throw new Error('Option "spacing" is required.');
}
if (!Array.isArray(options.spacing)) {
throw new Error('Option "spacing" must be an array.');
}
if (options.spacing.length !== 2) {
throw new Error('Option "spacing" must be an array with 2 elements.');
}
const spacing = options.spacing;
// X and Y coordinate in the Slide Coordinate System
if (!('point' in options)) {
throw new Error('Option "point" is required.');
}
if (!Array.isArray(options.point)) {
throw new Error('Option "point" must be an array.');
}
if (options.point.length !== 2) {
throw new Error('Option "point" must be an array with 2 elements.');
}
const point = options.point;
const m = [
[orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]],
[orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]],
[0, 0, 1],
];
const mInverted = inv(m);
const vSlide = [[point[0]], [point[1]], [1]];
const vImage = multiply(mInverted, vSlide);
const row = Number(vImage[1][0].toFixed(4));
const col = Number(vImage[0][0].toFixed(4));
return [col, row];
}

View File

@@ -0,0 +1,14 @@
const DCM_CODE_VALUES = {
IMAGING_MEASUREMENTS: '126010',
MEASUREMENT_GROUP: '125007',
IMAGE_REGION: '111030',
FINDING: '121071',
TRACKING_UNIQUE_IDENTIFIER: '112039',
LENGTH: '410668003',
AREA: '42798000',
SHORT_AXIS: 'G-A186',
LONG_AXIS: 'G-A185',
ELLIPSE_AREA: 'G-D7FE', // TODO: Remove this
};
export default DCM_CODE_VALUES;

View File

@@ -0,0 +1,81 @@
import { errorHandler, DicomMetadataStore } from '@ohif/core';
import { StaticWadoClient } from '@ohif/extension-default';
/**
* create a DICOMwebClient object to be used by Dicom Microscopy Viewer
*
* Referenced the code from `/extensions/default/src/DicomWebDataSource/index.js`
*
* @param param0
* @returns
*/
export default function getDicomWebClient({ extensionManager, servicesManager }: withAppTypes) {
const dataSourceConfig = window.config.dataSources.find(
ds => ds.sourceName === extensionManager.activeDataSource
);
const { userAuthenticationService } = servicesManager.services;
const { wadoRoot, staticWado, singlepart } = dataSourceConfig.configuration;
const wadoConfig = {
url: wadoRoot || '/dicomlocal',
staticWado,
singlepart,
headers: userAuthenticationService.getAuthorizationHeader(),
errorInterceptor: errorHandler.getHTTPErrorHandler(),
};
const client = new StaticWadoClient(wadoConfig);
client.wadoURL = wadoConfig.url;
if (extensionManager.activeDataSource === 'dicomlocal') {
/**
* For local data source, override the retrieveInstanceFrames() method of the
* dicomweb-client to retrieve image data from memory cached metadata.
* Other methods of the client doesn't matter, as we are feeding the DMV
* with the series metadata already.
*
* @param {Object} options
* @param {String} options.studyInstanceUID - Study Instance UID
* @param {String} options.seriesInstanceUID - Series Instance UID
* @param {String} options.sopInstanceUID - SOP Instance UID
* @param {String} options.frameNumbers - One-based indices of Frame Items
* @param {Object} [options.queryParams] - HTTP query parameters
* @returns {ArrayBuffer[]} Rendered Frame Items as byte arrays
*/
//
client.retrieveInstanceFrames = async options => {
if (!('studyInstanceUID' in options)) {
throw new Error('Study Instance UID is required for retrieval of instance frames');
}
if (!('seriesInstanceUID' in options)) {
throw new Error('Series Instance UID is required for retrieval of instance frames');
}
if (!('sopInstanceUID' in options)) {
throw new Error('SOP Instance UID is required for retrieval of instance frames');
}
if (!('frameNumbers' in options)) {
throw new Error('frame numbers are required for retrieval of instance frames');
}
console.log(
`retrieve frames ${options.frameNumbers.toString()} of instance ${options.sopInstanceUID}`
);
const instance = DicomMetadataStore.getInstance(
options.studyInstanceUID,
options.seriesInstanceUID,
options.sopInstanceUID
);
const frameNumbers = Array.isArray(options.frameNumbers)
? options.frameNumbers
: options.frameNumbers.split(',');
return frameNumbers.map(fr =>
Array.isArray(instance.PixelData) ? instance.PixelData[+fr - 1] : instance.PixelData
);
};
}
return client;
}

View File

@@ -0,0 +1,32 @@
/**
* Get referenced SM displaySet from SR displaySet
*
* @param {*} allDisplaySets
* @param {*} microscopySRDisplaySet
* @returns
*/
export default function getSourceDisplaySet(allDisplaySets, microscopySRDisplaySet) {
const { ReferencedFrameOfReferenceUID } = microscopySRDisplaySet;
const otherDisplaySets = allDisplaySets.filter(
ds => ds.displaySetInstanceUID !== microscopySRDisplaySet.displaySetInstanceUID
);
const referencedDisplaySet = otherDisplaySets.find(
displaySet =>
displaySet.Modality === 'SM' &&
(displaySet.FrameOfReferenceUID === ReferencedFrameOfReferenceUID ||
// sometimes each depth instance has the different FrameOfReferenceID
displaySet.othersFrameOfReferenceUID.includes(ReferencedFrameOfReferenceUID))
);
if (!referencedDisplaySet && otherDisplaySets.length >= 1) {
console.warn(
'No display set with FrameOfReferenceUID',
ReferencedFrameOfReferenceUID,
'single series, assuming data error, defaulting to only series.'
);
return otherDisplaySets.find(displaySet => displaySet.Modality === 'SM');
}
return referencedDisplaySet;
}

View File

@@ -0,0 +1,184 @@
import dcmjs from 'dcmjs';
import DCM_CODE_VALUES from './dcmCodeValues';
import toArray from './toArray';
const MeasurementReport = dcmjs.adapters.DICOMMicroscopyViewer.MeasurementReport;
// Define as async so that it returns a promise, expected by the ViewportGrid
export default async function loadSR(
microscopyService,
microscopySRDisplaySet,
referencedDisplaySet
) {
const naturalizedDataset = microscopySRDisplaySet.metadata;
const { StudyInstanceUID, FrameOfReferenceUID } = referencedDisplaySet;
const managedViewers = microscopyService.getManagedViewersForStudy(StudyInstanceUID);
if (!managedViewers || !managedViewers.length) {
return;
}
microscopySRDisplaySet.isLoaded = true;
const { rois, labels } = await _getROIsFromToolState(microscopyService, naturalizedDataset, FrameOfReferenceUID);
const managedViewer = managedViewers[0];
for (let i = 0; i < rois.length; i++) {
// NOTE: When saving Microscopy SR, we are attaching identifier property
// to each ROI, and when read for display, it is coming in as "TEXT"
// evaluation.
// As the Dicom Microscopy Viewer will override styles for "Text" evaluations
// to hide all other geometries, we are going to manually remove that
// evaluation item.
const roi = rois[i];
const roiSymbols = Object.getOwnPropertySymbols(roi);
const _properties = roiSymbols.find(s => s.description === 'properties');
const properties = roi[_properties];
properties['evaluations'] = [];
managedViewer.addRoiGraphicWithLabel(roi, labels[i]);
}
}
async function _getROIsFromToolState(microscopyService, naturalizedDataset, FrameOfReferenceUID) {
const toolState = MeasurementReport.generateToolState(naturalizedDataset);
const tools = Object.getOwnPropertyNames(toolState);
// Does a dynamic import to prevent webpack from rebuilding the library
const DICOMMicroscopyViewer = await microscopyService.importDicomMicroscopyViewer();
const measurementGroupContentItems = _getMeasurementGroups(naturalizedDataset);
const rois = [];
const labels = [];
tools.forEach(t => {
const toolSpecificToolState = toolState[t];
let scoord3d;
const capsToolType = t.toUpperCase();
const measurementGroupContentItemsForTool = measurementGroupContentItems.filter(mg => {
const imageRegionContentItem = toArray(mg.ContentSequence).find(
ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGE_REGION
);
return imageRegionContentItem.GraphicType === capsToolType;
});
toolSpecificToolState.forEach((coordinates, index) => {
const properties = {};
const options = {
coordinates,
frameOfReferenceUID: FrameOfReferenceUID,
};
if (t === 'Polygon') {
scoord3d = new DICOMMicroscopyViewer.scoord3d.Polygon(options);
} else if (t === 'Polyline') {
scoord3d = new DICOMMicroscopyViewer.scoord3d.Polyline(options);
} else if (t === 'Point') {
scoord3d = new DICOMMicroscopyViewer.scoord3d.Point(options);
} else if (t === 'Ellipse') {
scoord3d = new DICOMMicroscopyViewer.scoord3d.Ellipse(options);
} else {
throw new Error('Unsupported tool type');
}
const measurementGroup = measurementGroupContentItemsForTool[index];
const findingGroup = toArray(measurementGroup.ContentSequence).find(
ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.FINDING
);
const trackingGroup = toArray(measurementGroup.ContentSequence).find(
ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.TRACKING_UNIQUE_IDENTIFIER
);
/**
* Extract presentation state from tracking identifier.
* Currently is stored in SR but should be stored in its tags.
*/
if (trackingGroup) {
const regExp = /\(([^)]+)\)/;
const matches = regExp.exec(trackingGroup.TextValue);
if (matches && matches[1]) {
properties.presentationState = JSON.parse(matches[1]);
properties.marker = properties.presentationState.marker;
}
}
let measurements = toArray(measurementGroup.ContentSequence).filter(ci =>
[
DCM_CODE_VALUES.LENGTH,
DCM_CODE_VALUES.AREA,
DCM_CODE_VALUES.SHORT_AXIS,
DCM_CODE_VALUES.LONG_AXIS,
DCM_CODE_VALUES.ELLIPSE_AREA,
].includes(ci.ConceptNameCodeSequence.CodeValue)
);
let evaluations = toArray(measurementGroup.ContentSequence).filter(ci =>
[DCM_CODE_VALUES.TRACKING_UNIQUE_IDENTIFIER].includes(ci.ConceptNameCodeSequence.CodeValue)
);
/**
* TODO: Resolve bug in DCMJS.
* ConceptNameCodeSequence should be a sequence with only one item.
*/
evaluations = evaluations.map(evaluation => {
const e = { ...evaluation };
e.ConceptNameCodeSequence = toArray(e.ConceptNameCodeSequence);
return e;
});
/**
* TODO: Resolve bug in DCMJS.
* ConceptNameCodeSequence should be a sequence with only one item.
*/
measurements = measurements.map(measurement => {
const m = { ...measurement };
m.ConceptNameCodeSequence = toArray(m.ConceptNameCodeSequence);
return m;
});
if (measurements && measurements.length) {
properties.measurements = measurements;
console.log('[SR] retrieving measurements...', measurements);
}
if (evaluations && evaluations.length) {
properties.evaluations = evaluations;
console.log('[SR] retrieving evaluations...', evaluations);
}
const roi = new DICOMMicroscopyViewer.roi.ROI({ scoord3d, properties });
rois.push(roi);
if (findingGroup) {
labels.push(findingGroup.ConceptCodeSequence.CodeValue);
} else {
labels.push('');
}
});
});
return { rois, labels };
}
function _getMeasurementGroups(naturalizedDataset) {
const { ContentSequence } = naturalizedDataset;
const imagingMeasurementsContentItem = ContentSequence.find(
ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.IMAGING_MEASUREMENTS
);
const measurementGroupContentItems = toArray(
imagingMeasurementsContentItem.ContentSequence
).filter(ci => ci.ConceptNameCodeSequence.CodeValue === DCM_CODE_VALUES.MEASUREMENT_GROUP);
return measurementGroupContentItems;
}

View File

@@ -0,0 +1,12 @@
/**
* Trigger file download from an array buffer
* @param buffer
* @param filename
*/
export function saveByteArray(buffer: ArrayBuffer, filename: string) {
const blob = new Blob([buffer], { type: 'application/dicom' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
link.click();
}

View File

@@ -0,0 +1,48 @@
const defaultFill = {
color: 'rgba(255,255,255,0.4)',
};
const emptyFill = {
color: 'rgba(255,255,255,0.0)',
};
const defaultStroke = {
color: 'rgb(0,255,0)',
width: 1.5,
};
const activeStroke = {
color: 'rgb(255,255,0)',
width: 1.5,
};
const defaultStyle = {
image: {
circle: {
fill: defaultFill,
stroke: activeStroke,
radius: 5,
},
},
fill: defaultFill,
stroke: activeStroke,
};
const emptyStyle = {
image: {
circle: {
fill: emptyFill,
stroke: defaultStroke,
radius: 5,
},
},
fill: emptyFill,
stroke: defaultStroke,
};
const styles = {
active: defaultStyle,
default: emptyStyle,
};
export default styles;

View File

@@ -0,0 +1,3 @@
export default function toArray(item) {
return Array.isArray(item) ? item : [item];
}