Initial commit from prod-batam
This commit is contained in:
@@ -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;
|
||||
186
extensions/dicom-microscopy/src/utils/RoiAnnotation.js
Normal file
186
extensions/dicom-microscopy/src/utils/RoiAnnotation.js
Normal 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;
|
||||
15
extensions/dicom-microscopy/src/utils/areaOfPolygon.js
Normal file
15
extensions/dicom-microscopy/src/utils/areaOfPolygon.js
Normal 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);
|
||||
}
|
||||
192
extensions/dicom-microscopy/src/utils/constructSR.ts
Normal file
192
extensions/dicom-microscopy/src/utils/constructSR.ts
Normal 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;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
14
extensions/dicom-microscopy/src/utils/dcmCodeValues.js
Normal file
14
extensions/dicom-microscopy/src/utils/dcmCodeValues.js
Normal 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;
|
||||
81
extensions/dicom-microscopy/src/utils/dicomWebClient.ts
Normal file
81
extensions/dicom-microscopy/src/utils/dicomWebClient.ts
Normal 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;
|
||||
}
|
||||
32
extensions/dicom-microscopy/src/utils/getSourceDisplaySet.js
Normal file
32
extensions/dicom-microscopy/src/utils/getSourceDisplaySet.js
Normal 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;
|
||||
}
|
||||
184
extensions/dicom-microscopy/src/utils/loadSR.ts
Normal file
184
extensions/dicom-microscopy/src/utils/loadSR.ts
Normal 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;
|
||||
}
|
||||
12
extensions/dicom-microscopy/src/utils/saveByteArray.ts
Normal file
12
extensions/dicom-microscopy/src/utils/saveByteArray.ts
Normal 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();
|
||||
}
|
||||
48
extensions/dicom-microscopy/src/utils/styles.js
Normal file
48
extensions/dicom-microscopy/src/utils/styles.js
Normal 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;
|
||||
3
extensions/dicom-microscopy/src/utils/toArray.js
Normal file
3
extensions/dicom-microscopy/src/utils/toArray.js
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function toArray(item) {
|
||||
return Array.isArray(item) ? item : [item];
|
||||
}
|
||||
Reference in New Issue
Block a user