Skip to content

Commit d24cc0a

Browse files
Add clusteringPixelSizeThreshold option (#222)
* Add clusteringPixelSizeThreshold * Update open layers * Update comments * Add progress indicator to bulk ann retrieve and processing * Add toggle for clusters * Fix regression * Revert "Fix regression" This reverts commit ead7d8c. * Lock dicomweb-client verison * Revert "Add progress indicator to bulk ann retrieve and processing" This reverts commit 4ef28c0. * Remove media types
1 parent 89fd3a6 commit d24cc0a

4 files changed

Lines changed: 150 additions & 62 deletions

File tree

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,13 @@
7171
"@cornerstonejs/codec-libjpeg-turbo-8bit": "^1.2.2",
7272
"@cornerstonejs/codec-openjpeg": "^1.2.4",
7373
"@cornerstonejs/codec-openjph": "^2.4.5",
74-
"dicomweb-client": "^0.10.3",
74+
"dicomweb-client": "0.10.3",
7575
"colormap": "^2.3",
7676
"dcmjs": "^0.41.0",
7777
"dicomicc": "^0.1",
7878
"image-type": "^4.1",
7979
"mathjs": "^11.2",
80-
"ol": "^10.6.0",
80+
"ol": "^10.7.0",
8181
"uuid": "^9.0"
8282
},
8383
"resolutions": {

src/pyramid.js

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -432,17 +432,6 @@ function _createTileLoadFunction ({
432432
)
433433
}
434434

435-
const jpegMediaType = 'image/jpeg'
436-
const jpegTransferSyntaxUID = '1.2.840.10008.1.2.4.50'
437-
const jlsMediaType = 'image/jls'
438-
const jlsTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.80'
439-
const jlsTransferSyntaxUID = '1.2.840.10008.1.2.4.81'
440-
const jp2MediaType = 'image/jp2'
441-
const jp2TransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.90'
442-
const jp2TransferSyntaxUID = '1.2.840.10008.1.2.4.91'
443-
const jpxMediaType = 'image/jpx'
444-
const jpxTransferSyntaxUIDlossless = '1.2.840.10008.1.2.4.92'
445-
const jpxTransferSyntaxUID = '1.2.840.10008.1.2.4.93'
446435
const octetStreamMediaType = 'application/octet-stream'
447436
/*
448437
* Use of the "*" transfer syntax is a hack to work around standard
@@ -456,41 +445,11 @@ function _createTileLoadFunction ({
456445

457446
const mediaTypes = []
458447
mediaTypes.push(...[
459-
{
460-
mediaType: jlsMediaType,
461-
transferSyntaxUID: jlsTransferSyntaxUIDlossless
462-
},
463-
{
464-
mediaType: jlsMediaType,
465-
transferSyntaxUID: jlsTransferSyntaxUID
466-
},
467-
{
468-
mediaType: jp2MediaType,
469-
transferSyntaxUID: jp2TransferSyntaxUIDlossless
470-
},
471-
{
472-
mediaType: jp2MediaType,
473-
transferSyntaxUID: jp2TransferSyntaxUID
474-
},
475-
{
476-
mediaType: jpxMediaType,
477-
transferSyntaxUID: jpxTransferSyntaxUIDlossless
478-
},
479-
{
480-
mediaType: jpxMediaType,
481-
transferSyntaxUID: jpxTransferSyntaxUID
482-
},
483448
{
484449
mediaType: octetStreamMediaType,
485450
transferSyntaxUID: octetStreamTransferSyntaxUID
486451
}
487452
])
488-
if (bitsAllocated <= 8) {
489-
mediaTypes.push({
490-
mediaType: jpegMediaType,
491-
transferSyntaxUID: jpegTransferSyntaxUID
492-
})
493-
}
494453

495454
const frameInfo = {
496455
studyInstanceUID,

src/viewer.js

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,10 @@ class VolumeImageViewer {
826826
* @param {number[]} [options.highlightColor=[140, 184, 198]] - Color that
827827
* should be used to highlight things that get selected by the user
828828
* @param {object} [options.annotationOptions] - Annotation options
829+
* @param {number} [options.annotationOptions.clusteringPixelSizeThreshold] -
830+
* Pixel size threshold in millimeters. When the current pixel size is smaller
831+
* than or equal to this threshold, clustering is disabled (high resolution mode).
832+
* Defaults to undefined, which falls back to zoom-based detection.
829833
* @param {errorInterceptor} [options.errorInterceptor] - Callback for
830834
* intercepting errors
831835
* @param {number[]} [options.mapViewResolutions] Map's view list of
@@ -901,6 +905,10 @@ class VolumeImageViewer {
901905
this[_annotationOptions] = this[_options].annotationOptions
902906
}
903907

908+
if (this[_annotationOptions].clusteringPixelSizeThreshold === undefined) {
909+
this[_annotationOptions].clusteringPixelSizeThreshold = 0.001
910+
}
911+
904912
if (this[_options].errorInterceptor == null) {
905913
this[_options].errorInterceptor = error => error
906914
}
@@ -3823,18 +3831,50 @@ class VolumeImageViewer {
38233831
const affine = this[_affine]
38243832
const map = this[_map]
38253833
const view = map.getView()
3826-
const maxZoom = view.getMaxZoom()
38273834
const isHighResolution = () => {
3828-
const isZoomUnlimited = this[_mapViewResolutions] === undefined
3829-
const highestResolution = this[_tileGrid].getResolutions()[0]
3830-
const updatedMaxZoom = isZoomUnlimited ? highestResolution : (this[_annotationOptions].maxZoom || maxZoom)
3831-
const zoom = isZoomUnlimited ? (view.getZoom() * this[_tileGrid].getResolutions().length) : view.getZoom()
3832-
console.debug('Zoom:', zoom)
3833-
console.debug('Max Zoom:', updatedMaxZoom)
3834-
console.debug('Original Max Zoom:', maxZoom)
3835-
console.debug('Highest Resolution:', highestResolution)
3836-
console.debug('Resolutions:', this[_tileGrid].getResolutions().length)
3837-
return zoom >= updatedMaxZoom
3835+
const clusteringPixelSizeThreshold = this[_annotationOptions]?.clusteringPixelSizeThreshold
3836+
if (clusteringPixelSizeThreshold !== undefined) {
3837+
const currentResolution = view.getResolution()
3838+
const resolutions = this[_tileGrid].getResolutions()
3839+
3840+
/** Find the closest pyramid level based on current resolution */
3841+
let closestLevelIndex = 0
3842+
let minDiff = Math.abs(resolutions[0] - currentResolution)
3843+
for (let i = 1; i < resolutions.length; i++) {
3844+
const diff = Math.abs(resolutions[i] - currentResolution)
3845+
if (diff < minDiff) {
3846+
minDiff = diff
3847+
closestLevelIndex = i
3848+
}
3849+
}
3850+
3851+
/** Get pixel spacing for the current pyramid level */
3852+
const currentPixelSpacing = this[_pyramid].pixelSpacings[closestLevelIndex]
3853+
/** Use the smaller of the two pixel spacing values (typically they're similar) */
3854+
const currentPixelSize = Math.min(currentPixelSpacing[0], currentPixelSpacing[1])
3855+
3856+
console.debug('Current Resolution:', currentResolution)
3857+
console.debug('Closest Level Index:', closestLevelIndex)
3858+
console.debug('Current Pixel Size (mm):', currentPixelSize)
3859+
console.debug('Clustering Threshold (mm):', clusteringPixelSizeThreshold)
3860+
3861+
/** Return true (high resolution) when pixel size is <= threshold (smaller pixels = higher resolution) */
3862+
return currentPixelSize <= clusteringPixelSizeThreshold
3863+
}
3864+
3865+
/**
3866+
* When clusteringPixelSizeThreshold is undefined, it means clustering is disabled.
3867+
* In this case, always use high-res layer (return true).
3868+
*
3869+
* Note: This handles both cases:
3870+
* 1. Clustering was explicitly disabled (threshold set to undefined)
3871+
* 2. Clustering was never configured (threshold never set)
3872+
*
3873+
* In both cases, we want to use high-res layer, so return true.
3874+
* The zoom-based fallback in setAnnotationOptions is for backward compatibility
3875+
* but here we simplify to always use high-res when threshold is undefined.
3876+
*/
3877+
return true
38383878
}
38393879

38403880
/**
@@ -4533,6 +4573,95 @@ class VolumeImageViewer {
45334573
return annotationGroup.activeLayer().getVisible()
45344574
}
45354575

4576+
/**
4577+
* Update annotation options.
4578+
*
4579+
* @param {Object} options - Annotation options
4580+
* @param {number} [options.clusteringPixelSizeThreshold] - Pixel size threshold
4581+
* in millimeters. When the current pixel size is smaller than or equal to this
4582+
* threshold, clustering is disabled (high resolution mode). Set to undefined
4583+
* to use zoom-based detection.
4584+
* @returns {void}
4585+
*/
4586+
setAnnotationOptions (options = {}) {
4587+
if ('clusteringPixelSizeThreshold' in options) {
4588+
if (options.clusteringPixelSizeThreshold !== undefined) {
4589+
this[_annotationOptions].clusteringPixelSizeThreshold = options.clusteringPixelSizeThreshold
4590+
} else {
4591+
if (this[_annotationOptions].clusteringPixelSizeThreshold !== undefined) {
4592+
delete this[_annotationOptions].clusteringPixelSizeThreshold
4593+
}
4594+
}
4595+
4596+
const view = this[_map].getView()
4597+
const isHighResolution = () => {
4598+
const clusteringPixelSizeThreshold = this[_annotationOptions]?.clusteringPixelSizeThreshold
4599+
if (clusteringPixelSizeThreshold !== undefined) {
4600+
const currentResolution = view.getResolution()
4601+
const resolutions = this[_tileGrid].getResolutions()
4602+
4603+
let closestLevelIndex = 0
4604+
let minDiff = Math.abs(resolutions[0] - currentResolution)
4605+
for (let i = 1; i < resolutions.length; i++) {
4606+
const diff = Math.abs(resolutions[i] - currentResolution)
4607+
if (diff < minDiff) {
4608+
minDiff = diff
4609+
closestLevelIndex = i
4610+
}
4611+
}
4612+
4613+
const currentPixelSpacing = this[_pyramid].pixelSpacings[closestLevelIndex]
4614+
const currentPixelSize = Math.min(currentPixelSpacing[0], currentPixelSpacing[1])
4615+
return currentPixelSize <= clusteringPixelSizeThreshold
4616+
}
4617+
4618+
/**
4619+
* Fallback to zoom-based detection
4620+
*/
4621+
const isZoomUnlimited = this[_mapViewResolutions] === undefined
4622+
const highestResolution = this[_tileGrid].getResolutions()[0]
4623+
const updatedMaxZoom = isZoomUnlimited ? highestResolution : (this[_annotationOptions].maxZoom || view.getMaxZoom())
4624+
const zoom = isZoomUnlimited ? (view.getZoom() * this[_tileGrid].getResolutions().length) : view.getZoom()
4625+
return zoom >= updatedMaxZoom
4626+
}
4627+
4628+
/**
4629+
* Update visibility for all annotation groups
4630+
* Only update if the annotation group is currently visible to avoid triggering unnecessary loads
4631+
*/
4632+
Object.values(this[_annotationGroups]).forEach((annotationGroup) => {
4633+
if (annotationGroup.layers && annotationGroup.layers.length >= 2) {
4634+
/** Check if annotation group is currently visible (at least one layer is visible) */
4635+
const isCurrentlyVisible = annotationGroup.layers.some(layer => layer.getVisible() === true)
4636+
4637+
/**
4638+
* Only update visibility if the annotation group is already visible
4639+
* If it's not visible, just update the config and let moveend handler take care of it
4640+
*/
4641+
if (isCurrentlyVisible) {
4642+
/** When clustering is disabled (undefined), always use high-res layer */
4643+
const clusteringPixelSizeThreshold = this[_annotationOptions]?.clusteringPixelSizeThreshold
4644+
let shouldShowHighRes
4645+
if (clusteringPixelSizeThreshold === undefined) {
4646+
shouldShowHighRes = true
4647+
} else {
4648+
shouldShowHighRes = isHighResolution()
4649+
}
4650+
4651+
/** Only update visibility if it's actually changing to avoid triggering unnecessary loads */
4652+
const currentlyHighResVisible = annotationGroup.layers[0].getVisible()
4653+
if (currentlyHighResVisible !== shouldShowHighRes) {
4654+
annotationGroup.layers[0].setVisible(shouldShowHighRes)
4655+
annotationGroup.layers[1].setVisible(!shouldShowHighRes)
4656+
}
4657+
}
4658+
// If annotation group is not visible, don't touch layers - just let the config update
4659+
// The moveend handler will apply the correct layer when the group becomes visible
4660+
}
4661+
})
4662+
}
4663+
}
4664+
45364665
/**
45374666
* Set style of an annotation group.
45384667
*

yarn.lock

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2919,10 +2919,10 @@ dicomicc@^0.1:
29192919
resolved "https://registry.yarnpkg.com/dicomicc/-/dicomicc-0.1.0.tgz#c73acc60a8e2d73a20f462c8c7d0e1e0d977c486"
29202920
integrity sha512-kZejPGjLQ9NsgovSyVsiAuCpq6LofNR9Erc8Tt/vQAYGYCoQnTyWDlg5D0TJJQATKul7cSr9k/q0TF8G9qdDkQ==
29212921

2922-
dicomweb-client@^0.10.3:
2923-
version "0.10.4"
2924-
resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.4.tgz#f32f8659e51fbaf3f924c81fe8b7ad80297f2bc5"
2925-
integrity sha512-TEt26c0JI37IGmSqoj1k1/Y/ZIyq33/ysVaUwE0/Haosn2IBM55NEIPkT+AnhFss2nFAMVtKKWKWLox4luthVw==
2922+
dicomweb-client@0.10.3:
2923+
version "0.10.3"
2924+
resolved "https://registry.yarnpkg.com/dicomweb-client/-/dicomweb-client-0.10.3.tgz#b4fe550037166dff5d8afd88db3800eb579c91f4"
2925+
integrity sha512-/fHNEAYiz8j+9TNOrNJ0k+hYqirbOT85B7vM7I4VkY8DeDQb4BDUeL3RX6huDVtn6ZQlR91dI+2tejLc5c99wA==
29262926

29272927
diff-sequences@^27.5.1:
29282928
version "27.5.1"
@@ -5734,10 +5734,10 @@ obuf@^1.0.0, obuf@^1.1.2:
57345734
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
57355735
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
57365736

5737-
ol@^10.6.0:
5738-
version "10.6.1"
5739-
resolved "https://registry.yarnpkg.com/ol/-/ol-10.6.1.tgz#950f3914b4eec978f087b36aa74ce1e18c41ab09"
5740-
integrity sha512-xp174YOwPeLj7c7/8TCIEHQ4d41tgTDDhdv6SqNdySsql5/MaFJEJkjlsYcvOPt7xA6vrum/QG4UdJ0iCGT1cg==
5737+
ol@^10.7.0:
5738+
version "10.7.0"
5739+
resolved "https://registry.yarnpkg.com/ol/-/ol-10.7.0.tgz#6a072a602cab3a5d9b35356de8b837221d78379b"
5740+
integrity sha512-122U5gamPqNgLpLOkogFJhgpywvd/5en2kETIDW+Ubfi9lPnZ0G9HWRdG+CX0oP8od2d6u6ky3eewIYYlrVczw==
57415741
dependencies:
57425742
"@types/rbush" "4.0.0"
57435743
earcut "^3.0.0"

0 commit comments

Comments
 (0)