Skip to content

Commit 8ea1a45

Browse files
committed
Allow viewer construction with formatted metadata
1 parent 1607859 commit 8ea1a45

File tree

5 files changed

+248
-176
lines changed

5 files changed

+248
-176
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dicom-microscopy-viewer",
3-
"version": "0.31.0",
3+
"version": "0.32.0",
44
"description": "Interactive web-based viewer for DICOM Microscopy Images",
55
"main": "./src/dicom-microscopy-viewer.js",
66
"standard": {

src/channel.js

Lines changed: 103 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { VLWholeSlideMicroscopyImage, getFrameMapping } from './metadata.js'
1+
import { getFrameMapping } from './metadata.js'
22
import *
33
as DICOMwebClient from 'dicomweb-client'
44
import {
@@ -119,68 +119,75 @@ class _Channel {
119119
* determine the image pyramid structure, i.e. the size and resolution
120120
* images at the different pyramid levels.
121121
*/
122-
123122
const geometryArrays = _Channel.deriveImageGeometry(this)
124-
123+
const opticalPathIdentifier = this.blendingInformation.opticalPathIdentifier
125124
// Check frame of reference
126125
if (referenceFrameOfReferenceUID !== this.FrameOfReferenceUID) {
127126
throw new Error(
128-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
129-
' image has different FrameOfReferenceUID respect to the reference optical path ' +
130-
referenceOpticalPathIdentifier
127+
`Image with optical path "${opticalPathIdentifier}"` +
128+
'has different FrameOfReferenceUID with respect to the reference ' +
129+
'image with optical path ' +
130+
`"${referenceOpticalPathIdentifier}".`
131131
)
132132
}
133133

134134
// Check container identifier
135135
if (referenceContainerIdentifier !== this.ContainerIdentifier) {
136136
throw new Error(
137-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
138-
' image has different ContainerIdentifier respect to the reference optical path ' +
139-
referenceOpticalPathIdentifier
137+
`Image with optical path "${opticalPathIdentifier}"` +
138+
'has different ContainerIdentifier with respect to the reference ' +
139+
'image with optical path ' +
140+
`"${referenceOpticalPathIdentifier}".`
140141
)
141142
}
142143

143144
// Check that all the channels have the same pyramid parameters
144145
if (!are2DArraysAlmostEqual(geometryArrays[0], referenceExtent)) {
145146
throw new Error(
146-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
147-
' image has incompatible extent respect to the reference optical path ' +
148-
referenceOpticalPathIdentifier
147+
`Image with optical path "${opticalPathIdentifier}"` +
148+
'has an incompatible extent with respect to the reference ' +
149+
'image with optical path ' +
150+
`"${referenceOpticalPathIdentifier}".`
149151
)
150152
}
151153
if (!are2DArraysAlmostEqual(geometryArrays[1], referenceOrigins)) {
152154
throw new Error(
153-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
154-
' image has incompatible origins respect to the reference optical path ' +
155-
referenceOpticalPathIdentifier
155+
`Image with optical path "${opticalPathIdentifier}"` +
156+
'has incompatible origins with respect to the reference ' +
157+
'image with optical path ' +
158+
`"${referenceOpticalPathIdentifier}".`
156159
)
157160
}
158161
if (!are2DArraysAlmostEqual(geometryArrays[2], referenceResolutions)) {
159162
throw new Error(
160-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
161-
' image has incompatible resolutions respect to the reference optical path ' +
162-
referenceOpticalPathIdentifier
163+
`Image with optical path "${opticalPathIdentifier}"` +
164+
'has incompatible resolutions with respect to the reference ' +
165+
'image with optical path ' +
166+
`"${referenceOpticalPathIdentifier}".`
163167
)
164168
}
165169
if (!are2DArraysAlmostEqual(geometryArrays[3], referenceGridSizes)) {
166170
throw new Error(
167-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
168-
' image has incompatible grid sizes respect to the reference optical path ' +
169-
referenceOpticalPathIdentifier
171+
`Image with optical path "${opticalPathIdentifier}"` +
172+
'has incompatible grid sizes with respect to the reference ' +
173+
'image with optical path ' +
174+
`"${referenceOpticalPathIdentifier}".`
170175
)
171176
}
172177
if (!are2DArraysAlmostEqual(geometryArrays[4], referenceTileSizes)) {
173178
throw new Error(
174-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
175-
' image has incompatible tile sizes respect to the reference optical path ' +
176-
referenceOpticalPathIdentifier
179+
`Image with optical path "${opticalPathIdentifier}"` +
180+
'has incompatible tile sizes with respect to the reference ' +
181+
'image with optical path ' +
182+
`"${referenceOpticalPathIdentifier}".`
177183
)
178184
}
179185
if (!are2DArraysAlmostEqual(geometryArrays[5], referencePixelSpacings)) {
180186
throw new Error(
181-
'Optical path ' + this.blendingInformation.opticalPathIdentifier +
182-
' image has incompatible pixel spacings respect to the reference optical path ' +
183-
referenceOpticalPathIdentifier
187+
`Image with optical path "${opticalPathIdentifier}"` +
188+
'has incompatible pixel spacings with respect to the reference ' +
189+
'image with optical path ' +
190+
`"${referenceOpticalPathIdentifier}".`
184191
)
185192
}
186193

@@ -197,7 +204,6 @@ class _Channel {
197204
* viewport. Note that this is in contrast to the nomenclature used
198205
* by Openlayers.
199206
*/
200-
201207
const z = tileCoord[0]
202208
const y = tileCoord[1] + 1
203209
const x = tileCoord[2] + 1
@@ -226,9 +232,9 @@ class _Channel {
226232
const z = tile.tileCoord[0]
227233
const columns = this.pyramidMetadata[z].Columns
228234
const rows = this.pyramidMetadata[z].Rows
229-
const samplesPerPixel = this.pyramidMetadata[z].SamplesPerPixel // number of colors for pixel
230-
const bitsAllocated = this.pyramidMetadata[z].BitsAllocated // memory for pixel
231-
const pixelRepresentation = this.pyramidMetadata[z].PixelRepresentation // 0 unsigned, 1 signed
235+
const samplesPerPixel = this.pyramidMetadata[z].SamplesPerPixel
236+
const bitsAllocated = this.pyramidMetadata[z].BitsAllocated
237+
const pixelRepresentation = this.pyramidMetadata[z].PixelRepresentation
232238

233239
if (src !== null && samplesPerPixel === 1) {
234240
const studyInstanceUID = DICOMwebClient.utils.getStudyInstanceUIDFromUri(src)
@@ -241,12 +247,13 @@ class _Channel {
241247
tile.isLoading = true
242248

243249
if (options.retrieveRendered) {
244-
// allowed mediaTypes: http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.4.html
245-
// we use in order: jp2, jpeg.
246-
// we could add png, but at the moment we don't have a png decoder library and we would have to draw
247-
// to a canvas, retieve the imageData and then recompat the array from a RGBA to a 1 component array
248-
// for the offscreen rendering engine, which provides poor perfomances.
249-
250+
/*
251+
* We could use PNG, but at the moment we don't have a PNG decoder
252+
* library and thus would have to draw to a canvas, retrieve the
253+
* imageData and then recompat the array from a RGBA to a 1 component
254+
* array for the offscreen rendering engine, which would result in
255+
* poor perfomance.
256+
*/
250257
const jp2MediaType = 'image/jp2' // decoded with OpenJPEG
251258
const jpegMediaType = 'image/jpeg' // decoded with libJPEG-turbo
252259
const transferSyntaxUID = ''
@@ -288,15 +295,14 @@ class _Channel {
288295
rows
289296
}
290297

291-
const rendered = renderingEngine.colorMonochromeImageFrame(frameData)
298+
const rendered = renderingEngine.colorMonochromeImageFrame(
299+
frameData
300+
)
292301
tile.needToRerender = !rendered
293302
tile.isLoading = false
294303
}
295304
)
296305
} else {
297-
// allowed mediaTypes: http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_8.7.3.3.2.html
298-
// we use in order: jls, jp2, jpx, jpeg. Finally octet-stream if the first retrieve will fail.
299-
300306
const jlsMediaType = 'image/jls' // decoded with CharLS
301307
const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80'
302308
const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81'
@@ -318,13 +324,34 @@ class _Channel {
318324
sopInstanceUID,
319325
frameNumbers,
320326
mediaTypes: [
321-
{ mediaType: jlsMediaType, transferSyntaxUID: jlsTransferSyntaxUIDlossless },
322-
{ mediaType: jlsMediaType, transferSyntaxUID: jlsTransferSyntaxUID },
323-
{ mediaType: jp2MediaType, transferSyntaxUID: jp2TransferSyntaxUIDlossless },
324-
{ mediaType: jp2MediaType, transferSyntaxUID: jp2TransferSyntaxUID },
325-
{ mediaType: jpxMediaType, transferSyntaxUID: jpxTransferSyntaxUIDlossless },
326-
{ mediaType: jpxMediaType, transferSyntaxUID: jpxTransferSyntaxUID },
327-
{ mediaType: jpegMediaType, transferSyntaxUID: jpegTransferSyntaxUID }
327+
{
328+
mediaType: jlsMediaType,
329+
transferSyntaxUID: jlsTransferSyntaxUIDlossless
330+
},
331+
{
332+
mediaType: jlsMediaType,
333+
transferSyntaxUID: jlsTransferSyntaxUID
334+
},
335+
{
336+
mediaType: jp2MediaType,
337+
transferSyntaxUID: jp2TransferSyntaxUIDlossless
338+
},
339+
{
340+
mediaType: jp2MediaType,
341+
transferSyntaxUID: jp2TransferSyntaxUID
342+
},
343+
{
344+
mediaType: jpxMediaType,
345+
transferSyntaxUID: jpxTransferSyntaxUIDlossless
346+
},
347+
{
348+
mediaType: jpxMediaType,
349+
transferSyntaxUID: jpxTransferSyntaxUID
350+
},
351+
{
352+
mediaType: jpegMediaType,
353+
transferSyntaxUID: jpegTransferSyntaxUID
354+
}
328355
]
329356
}
330357

@@ -350,7 +377,9 @@ class _Channel {
350377
rows
351378
}
352379

353-
const rendered = renderingEngine.colorMonochromeImageFrame(frameData)
380+
const rendered = renderingEngine.colorMonochromeImageFrame(
381+
frameData
382+
)
354383
tile.needToRerender = !rendered
355384
tile.isLoading = false
356385
}
@@ -392,7 +421,9 @@ class _Channel {
392421
rows
393422
}
394423

395-
const rendered = renderingEngine.colorMonochromeImageFrame(frameData)
424+
const rendered = renderingEngine.colorMonochromeImageFrame(
425+
frameData
426+
)
396427
tile.needToRerender = !rendered
397428
tile.isLoading = false
398429
}
@@ -452,39 +483,31 @@ class _Channel {
452483
* @static
453484
*/
454485
static deriveImageGeometry (image) {
455-
image.microscopyImages = []
456-
image.metadata.forEach(m => {
457-
const microscopyImage = new VLWholeSlideMicroscopyImage({ metadata: m })
458-
if (microscopyImage.ImageType[2] === 'VOLUME') {
459-
image.microscopyImages.push(microscopyImage)
460-
}
461-
})
462-
463-
if (image.microscopyImages.length === 0) {
486+
if (image.metadata.length === 0) {
464487
throw new Error('No VOLUME image provided for Optioncal Path ID: ' +
465488
image.blendingInformation.opticalPathIdentifier)
466489
}
467490

468-
image.FrameOfReferenceUID = image.microscopyImages[0].FrameOfReferenceUID
469-
for (let i = 0; i < image.microscopyImages.length; ++i) {
470-
if (image.FrameOfReferenceUID !== image.microscopyImages[i].FrameOfReferenceUID) {
491+
image.FrameOfReferenceUID = image.metadata[0].FrameOfReferenceUID
492+
for (let i = 0; i < image.metadata.length; ++i) {
493+
if (image.FrameOfReferenceUID !== image.metadata[i].FrameOfReferenceUID) {
471494
throw new Error('Optioncal Path ID ' +
472495
image.blendingInformation.opticalPathIdentifier +
473496
' has volume microscopy images with different FrameOfReferenceUID')
474497
}
475498
}
476499

477-
image.ContainerIdentifier = image.microscopyImages[0].ContainerIdentifier
478-
for (let i = 0; i < image.microscopyImages.length; ++i) {
479-
if (image.ContainerIdentifier !== image.microscopyImages[i].ContainerIdentifier) {
500+
image.ContainerIdentifier = image.metadata[0].ContainerIdentifier
501+
for (let i = 0; i < image.metadata.length; ++i) {
502+
if (image.ContainerIdentifier !== image.metadata[i].ContainerIdentifier) {
480503
throw new Error('Optioncal Path ID ' +
481504
image.blendingInformation.opticalPathIdentifier +
482505
' has volume microscopy images with different ContainerIdentifier')
483506
}
484507
}
485508

486509
// Sort instances and optionally concatenation parts if present.
487-
image.microscopyImages.sort((a, b) => {
510+
image.metadata.sort((a, b) => {
488511
const sizeDiff = a.TotalPixelMatrixColumns - b.TotalPixelMatrixColumns
489512
if (sizeDiff === 0) {
490513
if (a.ConcatenationFrameOffsetNumber !== undefined) {
@@ -497,11 +520,11 @@ class _Channel {
497520

498521
image.pyramidMetadata = []
499522
image.pyramidFrameMappings = []
500-
const frameMappings = image.microscopyImages.map(m => getFrameMapping(m))
501-
for (let i = 0; i < image.microscopyImages.length; i++) {
502-
const cols = image.microscopyImages[i].TotalPixelMatrixColumns
503-
const rows = image.microscopyImages[i].TotalPixelMatrixRows
504-
const numberOfFrames = image.microscopyImages[i].NumberOfFrames
523+
const frameMappings = image.metadata.map(m => getFrameMapping(m))
524+
for (let i = 0; i < image.metadata.length; i++) {
525+
const cols = image.metadata[i].TotalPixelMatrixColumns
526+
const rows = image.metadata[i].TotalPixelMatrixRows
527+
const numberOfFrames = image.metadata[i].NumberOfFrames
505528
/*
506529
* Instances may be broken down into multiple concatentation parts.
507530
* Therefore, we have to re-assemble instance metadata.
@@ -521,25 +544,25 @@ class _Channel {
521544
// Update with information obtained from current concatentation part.
522545
Object.assign(image.pyramidFrameMappings[index], frameMappings[i])
523546
image.pyramidMetadata[index].NumberOfFrames += numberOfFrames
524-
if ('PerFrameFunctionalGroupsSequence' in image.microscopyImages[index]) {
547+
if ('PerFrameFunctionalGroupsSequence' in image.metadata[index]) {
525548
image.pyramidMetadata[index].PerFrameFunctionalGroupsSequence.push(
526-
...image.microscopyImages[i].PerFrameFunctionalGroupsSequence
549+
...image.metadata[i].PerFrameFunctionalGroupsSequence
527550
)
528551
}
529-
if (!('SOPInstanceUIDOfConcatenationSource' in image.microscopyImages[i])) {
552+
if (!('SOPInstanceUIDOfConcatenationSource' in image.metadata[i])) {
530553
throw new Error(
531554
'Attribute "SOPInstanceUIDOfConcatenationSource" is required ' +
532555
'for concatenation parts.'
533556
)
534557
}
535-
const sopInstanceUID = image.microscopyImages[i].SOPInstanceUIDOfConcatenationSource
558+
const sopInstanceUID = image.metadata[i].SOPInstanceUIDOfConcatenationSource
536559
image.pyramidMetadata[index].SOPInstanceUID = sopInstanceUID
537560
delete image.pyramidMetadata[index].SOPInstanceUIDOfConcatenationSource
538561
delete image.pyramidMetadata[index].ConcatenationUID
539562
delete image.pyramidMetadata[index].InConcatenationNumber
540563
delete image.pyramidMetadata[index].ConcatenationFrameOffsetNumber
541564
} else {
542-
image.pyramidMetadata.push(image.microscopyImages[i])
565+
image.pyramidMetadata.push(image.metadata[i])
543566
image.pyramidFrameMappings.push(frameMappings[i])
544567
}
545568
}
@@ -552,7 +575,7 @@ class _Channel {
552575
/*
553576
* Collect relevant information from DICOM metadata for each pyramid
554577
* level to construct the Openlayers map.
555-
*/
578+
*/
556579
const imageTileSizes = []
557580
const imageGridSizes = []
558581
const imageResolutions = []
@@ -627,7 +650,9 @@ class _Channel {
627650
* @param {object} metadata
628651
*/
629652
addMetadata (metadata) {
630-
this.metadata.push(metadata)
653+
if (metadata.ImageType[2] === 'VOLUME') {
654+
this.metadata.push(metadata)
655+
}
631656
}
632657

633658
/** Gets the channel visualization/presentation parameters

0 commit comments

Comments
 (0)