Skip to content

Commit 1c86f0e

Browse files
author
hackermd
committed
Use keywords to access dicom json metadata
1 parent f204c32 commit 1c86f0e

File tree

3 files changed

+8711
-125
lines changed

3 files changed

+8711
-125
lines changed

src/api.js

Lines changed: 114 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { default as MapEventType } from "ol/MapEventType";
2727
import { getCenter } from 'ol/extent';
2828
import { toStringXY } from 'ol/coordinate';
2929

30-
import { formatImageMetadata } from './metadata.js';
30+
import { formatImageMetadata, getFrameMapping } from './metadata.js';
3131
import { ROI } from './roi.js';
3232
import { generateUID } from './utils.js';
3333
import {
@@ -42,8 +42,14 @@ import {
4242
import DICOMwebClient from 'dicomweb-client/build/dicomweb-client.js'
4343

4444

45+
function _getPixelSpacing(metadata) {
46+
const functionalGroup = metadata.SharedFunctionalGroupsSequence[0];
47+
const pixelMeasures = functionalGroup.PixelMeasuresSequence[0];
48+
return pixelMeasures.PixelSpacing;
49+
}
50+
4551
function _geometry2Scoord3d(geometry, pyramid) {
46-
const frameOfReferenceUID = pyramid[pyramid.length-1].frameOfReferenceUID;
52+
const frameOfReferenceUID = pyramid[pyramid.length-1].FrameOfReferenceUID;
4753
const type = geometry.getType();
4854
if (type === 'Point') {
4955
let coordinates = geometry.getCoordinates();
@@ -161,9 +167,10 @@ function _coordinateFormatGeometry2Scoord3d(coordinates, pyramid) {
161167
coordinates = [coordinates];
162168
}
163169
coordinates.map(coord =>{
164-
let x = (coord[0] * pyramid[pyramid.length-1].pixelSpacing[0]).toFixed(4);
165-
let y = (-(coord[1] - 1) * pyramid[pyramid.length-1].pixelSpacing[1]).toFixed(4);
166-
let z = (1).toFixed(4);
170+
const pixelSpacing = _getPixelSpacing(pyramid[pyramid.length-1]);
171+
const x = (coord[0] * pixelSpacing[0]).toFixed(4);
172+
const y = (-(coord[1] - 1) * pixelSpacing[1]).toFixed(4);
173+
const z = (1).toFixed(4);
167174
coordinates = [Number(x), Number(y), Number(z)];
168175
})
169176
return(coordinates);
@@ -178,9 +185,10 @@ function _coordinateFormatScoord3d2Geometry(coordinates, pyramid) {
178185
coordinates = [coordinates];
179186
}
180187
coordinates.map(coord =>{
181-
let x = (coord[0] / pyramid[pyramid.length-1].pixelSpacing[0] - 1);
182-
let y = -(coord[1] / pyramid[pyramid.length-1].pixelSpacing[1] - 1);
183-
let z = coord[2];
188+
const pixelSpacing = _getPixelSpacing(pyramid[pyramid.length-1]);
189+
const x = (coord[0] / pixelSpacing[0] - 1);
190+
const y = -(coord[1] /pixelSpacing[1] - 1);
191+
const z = coord[2];
184192
coordinates = [x, y, z];
185193
});
186194
return(coordinates);
@@ -207,11 +215,12 @@ const _features = Symbol('features');
207215
const _drawingSource = Symbol('drawingSource');
208216
const _drawingLayer = Symbol('drawingLayer');
209217
const _segmentations = Symbol('segmentations');
210-
const _pyramid = Symbol('pyramid');
211218
const _client = Symbol('client');
212219
const _controls = Symbol('controls');
213220
const _interactions = Symbol('interactions');
214-
const _pyramidBase = Symbol('pyramidBaseLayer');
221+
const _pyramidMetadata = Symbol('pyramidMetadata');
222+
const _pyramidFrameMappings = Symbol('pyramidFrameMappings');
223+
const _pyramidBaseMetadata = Symbol('pyramidMetadataBase');
215224
const _metadata = Symbol('metadata');
216225

217226

@@ -262,44 +271,69 @@ class VLWholeSlideMicroscopyImageViewer {
262271
* images at the different pyramid levels.
263272
*/
264273
this[_metadata] = options.metadata.map(m => formatImageMetadata(m));
265-
this._pyramid = [];
274+
// Sort instances and optionally concatenation parts if present.
275+
this[_metadata].sort((a, b) => {
276+
const sizeDiff = a.TotalPixelMatrixColumns - b.TotalPixelMatrixColumns;
277+
if (sizeDiff !== 0) {
278+
return sizeDiff;
279+
}
280+
if (a.ConcatenationFrameOffsetNumber !== undefined) {
281+
return a.ConcatenationFrameOffsetNumber - b.ConcatenationFrameOffsetNumber;
282+
}
283+
return sizeDiff;
284+
});
285+
this[_pyramidMetadata] = [];
286+
this[_pyramidFrameMappings] = [];
287+
let frameMappings = options.metadata.map(m => getFrameMapping(m));
266288
for (let i = 0; i < this[_metadata].length; i++) {
267-
const cols = this[_metadata][i].totalPixelMatrixColumns;
268-
const rows = this[_metadata][i].totalPixelMatrixRows;
269-
const mapping = this[_metadata][i].frameMapping;
289+
const cols = this[_metadata][i].TotalPixelMatrixColumns;
290+
const rows = this[_metadata][i].TotalPixelMatrixRows;
291+
const numberOfFrames = this[_metadata][i].NumberOfFrames;
292+
const perFrameFunctionalGroups = this[_metadata][i].PerFrameFunctionalGroupsSequence;
270293
/*
271294
* Instances may be broken down into multiple concatentation parts.
272295
* Therefore, we have to re-assemble instance metadata.
273296
*/
274297
let alreadyExists = false;
275298
let index = null;
276-
for (let j = 0; j < this._pyramid.length; j++) {
299+
for (let j = 0; j < this[_pyramidMetadata].length; j++) {
277300
if (
278-
(this._pyramid[j].totalPixelMatrixColumns === cols) &&
279-
(this._pyramid[j].totalPixelMatrixRows === rows)
301+
(this[_pyramidMetadata][j].TotalPixelMatrixColumns === cols) &&
302+
(this[_pyramidMetadata][j].TotalPixelMatrixRows === rows)
280303
) {
281304
alreadyExists = true;
282305
index = j;
283306
}
284307
}
285308
if (alreadyExists) {
286309
// Update with information obtained from current concatentation part.
287-
Object.assign(this._pyramid[index].frameMapping, mapping);
310+
Object.assign(this[_pyramidFrameMappings][index], frameMappings[i]);
311+
this[_pyramidMetadata][index].NumberOfFrames += numberOfFrames;
312+
this[_pyramidMetadata][index].PerFrameFunctionalGroupsSequence.push(
313+
...perFrameFunctionalGroups
314+
);
315+
if (!"SOPInstanceUIDOfConcatenationSource" in this[_metadata][i]) {
316+
throw new Error(
317+
'Attribute "SOPInstanceUIDOfConcatenationSource" is required ' +
318+
'for concatenation parts.'
319+
);
320+
}
321+
const sopInstanceUID = this[_metadata][i].SOPInstanceUIDOfConcatenationSource;
322+
this[_pyramidMetadata][index].SOPInstanceUID = sopInstanceUID;
323+
delete this[_pyramidMetadata][index].SOPInstanceUIDOfConcatenationSource;
324+
delete this[_pyramidMetadata][index].ConcatenationUID;
325+
delete this[_pyramidMetadata][index].InConcatenationNumber;
326+
delete this[_pyramidMetadata][index].ConcatenationFrameOffsetNumber;
288327
} else {
289-
this._pyramid.push(this[_metadata][i]);
328+
this[_pyramidMetadata].push(this[_metadata][i]);
329+
this[_pyramidFrameMappings].push(frameMappings[i]);
290330
}
291331
}
292-
// Sort levels in ascending order
293-
this._pyramid.sort(function(a, b) {
294-
if(a.totalPixelMatrixColumns < b.totalPixelMatrixColumns) {
295-
return -1;
296-
} else if(a.totalPixelMatrixColumns > b.totalPixelMatrixColumns) {
297-
return 1;
298-
} else {
299-
return 0;
300-
}
301-
});
302-
this[_pyramidBase] = this._pyramid[this._pyramid.length-1];
332+
const nLevels = this[_pyramidMetadata].length;
333+
if (nLevels === 0) {
334+
console.error('empty pyramid - no levels found')
335+
}
336+
this[_pyramidBaseMetadata] = this[_pyramidMetadata][nLevels - 1];
303337
/*
304338
* Collect relevant information from DICOM metadata for each pyramid
305339
* level to construct the Openlayers map.
@@ -309,31 +343,33 @@ class VLWholeSlideMicroscopyImageViewer {
309343
const resolutions = [];
310344
const origins = [];
311345
const offset = [0, -1];
312-
const nLevels = this._pyramid.length;
313-
if (nLevels === 0) {
314-
console.error('empty pyramid - no levels found')
315-
}
316-
const basePixelSpacing = this._pyramid[nLevels-1].pixelSpacing;
317-
const baseColumns = this._pyramid[nLevels-1].columns;
318-
const baseRows = this._pyramid[nLevels-1].rows;
319-
const baseTotalPixelMatrixColumns = this._pyramid[nLevels-1].totalPixelMatrixColumns;
320-
const baseTotalPixelMatrixRows = this._pyramid[nLevels-1].totalPixelMatrixRows;
346+
const basePixelSpacing = _getPixelSpacing(this[_pyramidBaseMetadata]);
347+
const baseColumns = this[_pyramidBaseMetadata].Columns;
348+
const baseRows = this[_pyramidBaseMetadata].Rows;
349+
const baseTotalPixelMatrixColumns = this[_pyramidBaseMetadata].TotalPixelMatrixColumns;
350+
const baseTotalPixelMatrixRows = this[_pyramidBaseMetadata].TotalPixelMatrixRows;
321351
const baseColFactor = Math.ceil(baseTotalPixelMatrixColumns / baseColumns);
322352
const baseRowFactor = Math.ceil(baseTotalPixelMatrixRows / baseRows);
323353
const baseAdjustedTotalPixelMatrixColumns = baseColumns * baseColFactor;
324354
const baseAdjustedTotalPixelMatrixRows = baseRows * baseRowFactor;
325355
for (let j = (nLevels - 1); j >= 0; j--) {
326-
let columns = this._pyramid[j].columns;
327-
let rows = this._pyramid[j].rows;
328-
let totalPixelMatrixColumns = this._pyramid[j].totalPixelMatrixColumns;
329-
let totalPixelMatrixRows = this._pyramid[j].totalPixelMatrixRows;
330-
let pixelSpacing = this._pyramid[j].pixelSpacing;
331-
let colFactor = Math.ceil(totalPixelMatrixColumns / columns);
332-
let rowFactor = Math.ceil(totalPixelMatrixRows / rows);
333-
let adjustedTotalPixelMatrixColumns = columns * colFactor;
334-
let adjustedTotalPixelMatrixRows = rows * rowFactor;
335-
tileSizes.push([columns, rows]);
336-
totalSizes.push([adjustedTotalPixelMatrixColumns, adjustedTotalPixelMatrixRows]);
356+
const columns = this[_pyramidMetadata][j].Columns;
357+
const rows = this[_pyramidMetadata][j].Rows;
358+
const totalPixelMatrixColumns = this[_pyramidMetadata][j].TotalPixelMatrixColumns;
359+
const totalPixelMatrixRows = this[_pyramidMetadata][j].TotalPixelMatrixRows;
360+
const pixelSpacing = _getPixelSpacing(this[_pyramidMetadata][j]);
361+
const colFactor = Math.ceil(totalPixelMatrixColumns / columns);
362+
const rowFactor = Math.ceil(totalPixelMatrixRows / rows);
363+
const adjustedTotalPixelMatrixColumns = columns * colFactor;
364+
const adjustedTotalPixelMatrixRows = rows * rowFactor;
365+
tileSizes.push([
366+
columns,
367+
rows
368+
]);
369+
totalSizes.push([
370+
adjustedTotalPixelMatrixColumns,
371+
adjustedTotalPixelMatrixRows
372+
]);
337373

338374
/*
339375
* Compute the resolution at each pyramid level, since the zoom
@@ -353,13 +389,15 @@ class VLWholeSlideMicroscopyImageViewer {
353389
tileSizes.reverse();
354390
origins.reverse();
355391

356-
const pyramid = this._pyramid;
392+
// Functions won't be able to access "this"
393+
const pyramid = this[_pyramidMetadata];
394+
const pyramidFrameMappings = this[_pyramidFrameMappings];
357395

358396
/*
359397
* Define custom tile URL function to retrive frames via DICOMweb
360398
* WADO-RS.
361399
*/
362-
function tileUrlFunction(tileCoord, pixelRatio, projection) {
400+
const tileUrlFunction = (tileCoord, pixelRatio, projection) => {
363401
/*
364402
* Variables x and y correspond to the X and Y axes of the slide
365403
* coordinate system. Since we want to view the slide horizontally
@@ -378,14 +416,14 @@ class VLWholeSlideMicroscopyImageViewer {
378416
*/
379417
let x = -(tileCoord[2] + 1) + 1;
380418
let index = x + "-" + y;
381-
let path = pyramid[z].frameMapping[index];
419+
let path = pyramidFrameMappings[z][index];
382420
if (path === undefined) {
383421
console.warn("tile " + index + " not found at level " + z);
384422
return(null);
385423
}
386424
let url = options.client.wadoURL +
387-
"/studies/" + pyramid[z].studyInstanceUID +
388-
"/series/" + pyramid[z].seriesInstanceUID +
425+
"/studies/" + pyramid[z].StudyInstanceUID +
426+
"/series/" + pyramid[z].SeriesInstanceUID +
389427
'/instances/' + path;
390428
if (options.retrieveRendered) {
391429
url = url + '/rendered';
@@ -397,20 +435,20 @@ class VLWholeSlideMicroscopyImageViewer {
397435
* Define custonm tile loader function, which is required because the
398436
* WADO-RS response message has content type "multipart/related".
399437
*/
400-
function base64Encode(data){
438+
const base64Encode = (data) => {
401439
const uint8Array = new Uint8Array(data);
402440
const chunkSize = 0x8000;
403441
const strArray = [];
404442
for (let i=0; i < uint8Array.length; i+=chunkSize) {
405-
let str = String.fromCharCode.apply(
443+
const str = String.fromCharCode.apply(
406444
null, uint8Array.subarray(i, i + chunkSize)
407445
);
408446
strArray.push(str);
409447
}
410448
return btoa(strArray.join(''));
411449
}
412450

413-
function tileLoadFunction(tile, src) {
451+
const tileLoadFunction = (tile, src) => {
414452
if (src !== null) {
415453
const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri(src);
416454
const seriesInstanceUID = DICOMwebClient.utils.getSeriesInstanceUIDFromUri(src);
@@ -478,8 +516,8 @@ class VLWholeSlideMicroscopyImageViewer {
478516
*/
479517
var degrees = 0;
480518
if (
481-
(this[_pyramidBase].imageOrientationSlide[1] === -1) &&
482-
(this[_pyramidBase].imageOrientationSlide[3] === -1)
519+
(this[_pyramidBaseMetadata].ImageOrientationSlide[1] === -1) &&
520+
(this[_pyramidBaseMetadata].ImageOrientationSlide[3] === -1)
483521
) {
484522
/*
485523
* The row direction (left to right) of the total pixel matrix
@@ -512,7 +550,7 @@ class VLWholeSlideMicroscopyImageViewer {
512550
* DICOM pixel spacing has millimeter unit while the projection has
513551
* has meter unit.
514552
*/
515-
let spacing = pyramid[nLevels-1].pixelSpacing[0] / 10**3;
553+
let spacing = _getPixelSpacing(pyramid[nLevels-1])[0] / 10**3;
516554
let res = pixelRes * spacing;
517555
return(res);
518556
}
@@ -677,23 +715,23 @@ class VLWholeSlideMicroscopyImageViewer {
677715
const container = this[_map].getTargetElement();
678716

679717
this[_drawingSource].on(VectorEventType.ADDFEATURE, (e) => {
680-
publish(container, EVENT.ROI_ADDED, _getROIFromFeature(e.feature, this._pyramid));
718+
publish(container, EVENT.ROI_ADDED, _getROIFromFeature(e.feature, this[_pyramidMetadata]));
681719
});
682720

683721
this[_drawingSource].on(VectorEventType.CHANGEFEATURE, (e) => {
684-
publish(container, EVENT.ROI_MODIFIED, _getROIFromFeature(e.feature, this._pyramid));
722+
publish(container, EVENT.ROI_MODIFIED, _getROIFromFeature(e.feature, this[_pyramidMetadata]));
685723
});
686724

687725
this[_drawingSource].on(VectorEventType.REMOVEFEATURE, (e) => {
688-
publish(container, EVENT.ROI_REMOVED, _getROIFromFeature(e.feature, this._pyramid));
726+
publish(container, EVENT.ROI_REMOVED, _getROIFromFeature(e.feature, this[_pyramidMetadata]));
689727
});
690728

691729
this[_map].on(MapEventType.MOVESTART, (e) => {
692-
publish(container, EVENT.DICOM_MOVE_STARTED, this.getAllROIs());
730+
publish(container, EVENT.MOVE_STARTED, this.getAllROIs());
693731
});
694732

695733
this[_map].on(MapEventType.MOVEEND, (e) => {
696-
publish(container, EVENT.DICOM_MOVE_ENDED, this.getAllROIs());
734+
publish(container, EVENT.MOVE_ENDED, this.getAllROIs());
697735
});
698736

699737
}
@@ -758,7 +796,7 @@ class VLWholeSlideMicroscopyImageViewer {
758796
//attaching openlayers events handling
759797
this[_interactions].draw.on('drawend', (e) => {
760798
e.feature.setId(generateUID());
761-
publish(container, EVENT.ROI_DRAWN, _getROIFromFeature(e.feature, this._pyramid));
799+
publish(container, EVENT.ROI_DRAWN, _getROIFromFeature(e.feature, this[_pyramidMetadata]));
762800
});
763801

764802
this[_map].addInteraction(this[_interactions].draw);
@@ -789,7 +827,7 @@ class VLWholeSlideMicroscopyImageViewer {
789827
const container = this[_map].getTargetElement();
790828

791829
this[_interactions].select.on('select', (e) => {
792-
publish(container, EVENT.ROI_SELECTED, _getROIFromFeature(e.selected[0], this._pyramid));
830+
publish(container, EVENT.ROI_SELECTED, _getROIFromFeature(e.selected[0], this[_pyramidMetadata]));
793831
});
794832

795833
this[_map].addInteraction(this[_interactions].select);
@@ -845,16 +883,16 @@ class VLWholeSlideMicroscopyImageViewer {
845883

846884
getROI(uid) {
847885
const feature = this[_drawingSource].getFeatureById(uid);
848-
return _getROIFromFeature(feature, this._pyramid);
886+
return _getROIFromFeature(feature, this[_pyramidMetadata]);
849887
}
850888

851889
popROI() {
852890
const feature = this[_features].pop();
853-
return _getROIFromFeature(feature, this._pyramid);
891+
return _getROIFromFeature(feature, this[_pyramidMetadata]);
854892
}
855893

856894
addROI(item) {
857-
const geometry = _scoord3d2Geometry(item.scoord3d, this._pyramid);
895+
const geometry = _scoord3d2Geometry(item.scoord3d, this[_pyramidMetadata]);
858896
const feature = new Feature(geometry);
859897
feature.setProperties(item.properties, true);
860898
feature.setId(item.uid);
@@ -881,6 +919,11 @@ class VLWholeSlideMicroscopyImageViewer {
881919
get areROIsVisible() {
882920
return this[_drawingLayer].getVisible();
883921
}
922+
923+
get imageMetadata() {
924+
return this[_pyramidMetadata].reverse();
925+
}
926+
884927
}
885928

886929
export { VLWholeSlideMicroscopyImageViewer };

0 commit comments

Comments
 (0)