Skip to content

Commit d1e3344

Browse files
committed
Improve handling of annotation group measurements
1 parent 59cf498 commit d1e3344

File tree

2 files changed

+97
-38
lines changed

2 files changed

+97
-38
lines changed

src/utils.js

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ function createWindow (lowerBound, upperBound) {
102102
}
103103

104104
/**
105-
* Computes the rotation of the image with respect to the frame of reference.
105+
* Compute the rotation of the image with respect to the frame of reference.
106106
*
107107
* @param {Object} options - Options
108108
* @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
@@ -644,13 +644,29 @@ async function _fetchBulkdata ({ client, reference }) {
644644
})
645645
}
646646

647+
/**
648+
* Convert RGB color triplet into hex code.
649+
*
650+
* @param {Number[]} values - RGB triplet
651+
* @returns {String} Hex code
652+
*
653+
* @private
654+
*/
655+
function rgb2hex (values) {
656+
const r = values[0]
657+
const g = values[1]
658+
const b = values[2]
659+
return '#' + (0x1000000 + (r << 16) + (g << 8) + b).toString(16).slice(1)
660+
}
661+
647662
export {
648663
_getUnitSuffix,
649664
applyInverseTransform,
650665
applyTransform,
651666
buildInverseTransform,
652667
buildTransform,
653668
computeRotation,
669+
createWindow,
654670
_fetchBulkdata,
655671
_generateUID,
656672
mapPixelCoordToSlideCoord,
@@ -661,6 +677,6 @@ export {
661677
doContentItemsMatch,
662678
areCodedConceptsEqual,
663679
getContentItemNameCodedConcept,
664-
rescale,
665-
createWindow
680+
rgb2hex,
681+
rescale
666682
}

src/viewer.js

Lines changed: 78 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import { ZoomSlider, Zoom } from 'ol/control'
3939
import { getCenter, getHeight, getWidth } from 'ol/extent'
4040
import { defaults as defaultInteractions } from 'ol/interaction'
4141
import dcmjs from 'dcmjs'
42-
import { quantileSeq } from 'mathjs'
4342

4443
import {
4544
AnnotationGroup,
@@ -65,14 +64,17 @@ import { ParameterMapping, _groupFramesPerMapping } from './mapping.js'
6564
import { ROI } from './roi.js'
6665
import { Segment } from './segment.js'
6766
import {
67+
areCodedConceptsEqual,
6868
applyTransform,
6969
buildInverseTransform,
7070
buildTransform,
7171
computeRotation,
72+
getContentItemNameCodedConcept,
7273
_generateUID,
7374
_getUnitSuffix,
7475
doContentItemsMatch,
75-
createWindow
76+
createWindow,
77+
rgb2hex
7678
} from './utils.js'
7779
import {
7880
_scoord3dCoordinates2geometryCoordinates,
@@ -630,7 +632,7 @@ function _getColorInterpolationStyleForTileLayer ({
630632
* @private
631633
*/
632634
function _getColorPaletteStyleForPointLayer ({
633-
name,
635+
key,
634636
minValue,
635637
maxValue,
636638
colormap
@@ -649,7 +651,7 @@ function _getColorPaletteStyleForPointLayer ({
649651
'*',
650652
[
651653
'-',
652-
['get', name],
654+
['get', key],
653655
minValue
654656
],
655657
[
@@ -674,7 +676,7 @@ function _getColorPaletteStyleForPointLayer ({
674676
const expression = [
675677
'palette',
676678
indexExpression,
677-
colormap
679+
colormap.map(c => rgb2hex(c))
678680
]
679681

680682
return { color: expression }
@@ -2935,7 +2937,7 @@ class VolumeImageViewer {
29352937

29362938
const defaultAnnotationGroupStyle = {
29372939
opacity: 1.0,
2938-
color: '#027ea3'
2940+
color: [2, 126, 163]
29392941
}
29402942

29412943
metadata.AnnotationGroupSequence.forEach((item, index) => {
@@ -3019,6 +3021,7 @@ class VolumeImageViewer {
30193021
const graphicIndex = retrievedBulkdata[1]
30203022
const measurements = retrievedBulkdata[2]
30213023

3024+
console.log('process annotations')
30223025
for (let i = 0; i < numberOfAnnotations; i++) {
30233026
const point = _getCentroid(
30243027
graphicType,
@@ -3038,13 +3041,11 @@ class VolumeImageViewer {
30383041
geometry: new PointGeometry(coordinates)
30393042
})
30403043
const properties = {}
3041-
measurements.forEach(item => {
3042-
const name = item.name
3043-
const key = `${name.CodingSchemeDesignator}${name.CodeValue}`
3044-
const value = item.values[i]
3045-
properties[key] = value
3044+
measurements.forEach((measurementItem, measurementIndex) => {
3045+
const value = measurementItem.values[i]
3046+
properties[measurementIndex] = value
30463047
})
3047-
feature.setProperties(properties)
3048+
feature.setProperties(properties, true)
30483049
feature.setId(i + 1)
30493050
features.push(feature)
30503051
}
@@ -3054,17 +3055,27 @@ class VolumeImageViewer {
30543055
`for annotation group "${annotationGroupUID}"`
30553056
)
30563057
this.addFeatures(features)
3058+
console.info(
3059+
'compute statistics for measurement values ' +
3060+
`of annotation group "${annotationGroupUID}"`
3061+
)
30573062
const properties = {}
3058-
measurements.forEach(item => {
3059-
const name = item.name
3060-
const key = `${name.CodingSchemeDesignator}${name.CodeValue}`
3061-
const value = quantileSeq(
3062-
[...item.values],
3063-
[0, 0.015, 0.25, 0.5, 0.75, 0.95, 1]
3063+
measurements.forEach((measurementItem, measurementIndex) => {
3064+
/*
3065+
* Ideally, we would compute quantiles, but that is an expensive
3066+
* operation. For now, just compute mininum and maximum.
3067+
*/
3068+
const min = measurementItem.values.reduce(
3069+
(a, b) => Math.min(a, b),
3070+
Infinity
3071+
)
3072+
const max = measurementItem.values.reduce(
3073+
(a, b) => Math.max(a, b),
3074+
-Infinity
30643075
)
3065-
properties[key] = value
3076+
properties[measurementIndex] = { min, max }
30663077
})
3067-
this.setProperties(properties)
3078+
this.setProperties(properties, true)
30683079
success(features)
30693080
}).catch(error => {
30703081
console.error(error)
@@ -3076,8 +3087,7 @@ class VolumeImageViewer {
30763087
loader,
30773088
wrapX: false,
30783089
rotateWithView: true,
3079-
overlaps: false,
3080-
features: new Collection([], { unique: true })
3090+
overlaps: false
30813091
})
30823092
source.on('featuresloadstart', (event) => {
30833093
const container = this[_map].getTargetElement()
@@ -3116,8 +3126,7 @@ class VolumeImageViewer {
31163126
}
31173127
annotationGroup.layer = new PointsLayer({
31183128
source,
3119-
style,
3120-
disableHitDetection: true
3129+
style
31213130
})
31223131
annotationGroup.layer.setVisible(false)
31233132

@@ -3159,7 +3168,9 @@ class VolumeImageViewer {
31593168
*
31603169
* @param {string} annotationGroupUID - Unique identifier of an annotation group
31613170
* @param {Object} styleOptions
3162-
* @param {number} styleOptions.measurement - Selected measurement for colorizing annotations
3171+
* @param {number} [styleOptions.opacity] - Opacity
3172+
* @param {number[]} [styleOptions.color] - RGB color triplet
3173+
* @param {Object} [styleOptions.measurement] - Selected measurement
31633174
*/
31643175
showAnnotationGroup (annotationGroupUID, styleOptions = {}) {
31653176
if (!(annotationGroupUID in this[_annotationGroups])) {
@@ -3212,8 +3223,10 @@ class VolumeImageViewer {
32123223
*
32133224
* @param {string} annotationGroupUID - Unique identifier of an annotation group
32143225
* @param {Object} styleOptions - Style options
3215-
* @param {number} styleOptions.opacity - Opacity
3216-
* @param {number} styleOptions.measurement - Selected measurement for colorizing annotations
3226+
* @param {number} [styleOptions.opacity] - Opacity
3227+
* @param {number[]} [styleOptions.color] - RGB color triplet
3228+
* @param {Object} [styleOptions.measurement] - Selected measurement for
3229+
* colorizing annotations
32173230
*/
32183231
setAnnotationGroupStyle (annotationGroupUID, styleOptions = {}) {
32193232
if (!(annotationGroupUID in this[_annotationGroups])) {
@@ -3227,13 +3240,40 @@ class VolumeImageViewer {
32273240
annotationGroup.style.opacity = styleOptions.opacity
32283241
annotationGroup.layer.setOpacity(styleOptions.opacity)
32293242
}
3243+
if (styleOptions.color != null) {
3244+
annotationGroup.style.color = styleOptions.color
3245+
}
3246+
console.info(
3247+
`set style for annotation group "${annotationGroupUID}"`,
3248+
styleOptions
3249+
)
32303250

3251+
const metadata = annotationGroup.metadata
32313252
const source = annotationGroup.layer.getSource()
3253+
const groupItem = metadata.AnnotationGroupSequence.find(item => {
3254+
return item.AnnotationGroupUID === annotationGroupUID
3255+
})
3256+
if (groupItem == null) {
3257+
throw new Error(
3258+
'Cannot set style of annotation group. ' +
3259+
`Could not find metadata of annotation group "${annotationGroupUID}".`
3260+
)
3261+
}
3262+
32323263
const name = styleOptions.measurement
32333264
if (name) {
3234-
const key = `${name.CodingSchemeDesignator}${name.CodeValue}`
3265+
const measurementIndex = groupItem.MeasurementsSequence.findIndex(item => {
3266+
return areCodedConceptsEqual(name, getContentItemNameCodedConcept(item))
3267+
})
3268+
if (measurementIndex == null) {
3269+
throw new Error(
3270+
'Cannot set style of annotation group. ' +
3271+
`Could not find measurement "${name.CodeMeaning}" ` +
3272+
`of annotation group "${annotationGroupUID}".`
3273+
)
3274+
}
32353275
const properties = source.getProperties()
3236-
if (properties[key]) {
3276+
if (properties[measurementIndex]) {
32373277
const colormap = createColormap({
32383278
name: ColormapNames.VIRIDIS,
32393279
bins: 50
@@ -3256,16 +3296,17 @@ class VolumeImageViewer {
32563296
Object.assign(
32573297
style,
32583298
_getColorPaletteStyleForPointLayer({
3259-
name: key,
3260-
minValue: properties[key][0],
3261-
maxValue: properties[key][properties[key].length - 2],
3299+
key: measurementIndex,
3300+
minValue: properties[measurementIndex].min,
3301+
maxValue: properties[measurementIndex].max,
32623302
colormap
32633303
})
32643304
)
32653305
const newLayer = new PointsLayer({
32663306
source,
32673307
style,
3268-
disableHitDetection: true
3308+
disableHitDetection: true,
3309+
visible: false
32693310
})
32703311
this[_map].addLayer(newLayer)
32713312
this[_map].removeLayer(annotationGroup.layer)
@@ -3285,20 +3326,22 @@ class VolumeImageViewer {
32853326
this[_pyramid].metadata.length,
32863327
15
32873328
],
3288-
color: annotationGroup.style.color,
3329+
color: rgb2hex(annotationGroup.style.color),
32893330
opacity: annotationGroup.style.opacity
32903331
}
32913332
}
32923333
const newLayer = new PointsLayer({
32933334
source,
32943335
style,
3295-
disableHitDetection: true
3336+
disableHitDetection: true,
3337+
visible: false
32963338
})
32973339
this[_map].addLayer(newLayer)
32983340
this[_map].removeLayer(annotationGroup.layer)
32993341
annotationGroup.layer.dispose()
33003342
annotationGroup.layer = newLayer
33013343
}
3344+
annotationGroup.layer.setVisible(true)
33023345
}
33033346

33043347
/**

0 commit comments

Comments
 (0)