Repository navigation
fix(cs3d): render thumbnails for images with a VOI LUT Sequence - #140
Conversation
createImage handed the cornerstone renderer the naturalized Modality/VOI LUT
Sequence item as-is. The renderer reads a LUT as { firstValueMapped, lut } and
spreads `lut`, so every image carrying a VOI LUT Sequence threw inside
getRenderedBuffer, no thumbnail was written, and the server answered
/thumbnail with 404 (seen on a JPEG Lossless MONOCHROME1 CR).
Convert the sequence item into the renderer's LUT shape:
- Read the LUT Descriptor and LUT Data from InlineBinary or decoded values.
LUT Data that is only a BulkDataURI cannot be read here, so that LUT is
dropped and the window center/width (or rescale) applies instead.
- Keep LUT entries unsigned (US/OW). The first value mapped is signed when
the values indexing the LUT are: pixel representation for a Modality LUT,
and for a VOI LUT the modality output, by the same rule as
dicom-image-loader's getModalityLUTOutputPixelRepresentation.
- A descriptor entry count of 0 means 65536; 8 bit entries are read one per
byte or one per word, told apart by the data length.
Two renderer behaviours need working around:
- The renderer clamps a Modality LUT at firstValueMapped but reads
lut[storedValue] without subtracting it, so a Modality LUT starting above 0
is padded at the front with its first entry and moved to start at 0.
- The renderer looks the VOI LUT up by the modality output without rounding,
so with no Modality LUT and a fractional rescale the VOI LUT is skipped and
the window renders the image.
wayfarer3130
left a comment
There was a problem hiding this comment.
I merged origin/master (v1.7.9) into this branch. The merge had no conflicts, and the 46 tests of @radicalimaging/cs3d pass after the merge.
The conversion is correct. I compared it with getVOILut and getModalityLut of the CPU renderer in @cornerstonejs/core 5.11.5. The padding and the rule for the signed first value mapped agree with the two renderer functions.
Please change: the length of the comments
An inline comment must stay short, with one or two lines. Put a longer explanation in a JSDoc comment of the function. A JSDoc comment is shared with the editor tools and with the generated documentation.
In packages/cs3d/src/image/createImage.ts:
- Lines 7-9, 31-37, 64-66, 78-80: change each
//block above a function to a JSDoc comment (/** ... */). Add@paramand@returns. The block abovetoCornerstoneLUThas 7 lines and holds three different facts. Give each fact one short sentence in the JSDoc. - Lines 89-90, in
isModalityOutputSigned: make the comment one line, for example// dcmjs gives null or [] for an empty element; treat it as absent. Move the reason to the JSDoc of the function. - Lines 268-270, in
createImage: make the comment one line, for example// Skip the VOI LUT for a fractional rescale; see modalityOutputIsInteger. Move the reason to a JSDoc comment. One way is a small functionisModalityOutputInteger(image, slope, intercept)with that JSDoc.
Minor: a Modality LUT that the code does not apply
isModalityOutputSigned returns false when ModalityLUTSequence has an item. createImage does not always apply that item:
imageFrameUtils.is.modalityLutreturnsfalsefor XA and XRF.toCornerstoneLUTreturnsundefinedfor an item that it cannot read, for example a short descriptor or a BulkDataURI.
In these cases, the renderer uses slope 1 and intercept 0. With signed pixels, the modality output is signed, but the VOI LUT reads its first value mapped as unsigned. A fix: give isModalityOutputSigned the result of the conversion (image.modalityLUT), and not the raw sequence. This case is rare, so a follow-up PR is also acceptable.
Accepted as is
- The VOI LUT has priority over the window center and width. This behaviour is correct for a thumbnail.
- A Modality LUT that starts below 0 still renders incorrectly. The PR description states this limit.
isModalityOutputSigned read the raw Modality LUT Sequence, but createImage does not apply every item: XA and XRF skip it, and an item that cannot be read gives no LUT. The renderer then uses the rescale, so with signed pixels the VOI LUT was indexed by signed values while its first value mapped was read as unsigned. The function now takes the Modality LUT that the image applies, and checks it first, because the renderer uses it in place of the rescale and its entries are unsigned. Also turn the long // blocks above the LUT helpers into JSDoc comments, and move the fractional-rescale check into isModalityOutputInteger.
|
Thanks for the review. Both points are done in this PR, in f1ae03d. Comment length
A Modality LUT that the code does not apply
Checks
I will also update the PR description for the new rule order and test count. |
Summary
The server makes no thumbnail for an image that carries a VOI LUT Sequence (0028,3010), and
/thumbnailanswers 404.createImagehanded the naturalized sequence item to the cornerstone CPU renderer as-is. The renderer reads a LUT as{ firstValueMapped, lut }and spreadslut, sogetRenderedBufferthrew, logged the error, and wrote nothing. A Modality LUT Sequence failed the same way.This PR converts the sequence items into the renderer's LUT shape, and works around two renderer behaviours that the conversion makes reachable.
Changes
packages/cs3d/src/image/createImage.ts:getModalityLUTOutputPixelRepresentation. A Modality LUT Sequence that the code does not apply (XA, XRF, or an item it cannot read) does not count.firstValueMapped, but then readslut[storedValue]without subtractingfirstValueMapped. A Modality LUT that starts above 0 is padded at the front with its first entry, and starts at 0. A Modality LUT that starts below 0 is left as is and still renders wrong; such LUTs are rare.Where this differs from dicom-image-loader's
getLUTs, it follows PS3.3 C.11.1/C.11.2:getLUTsreads LUT Data as signed for signed pixel data, and reads a count of 0 as 65535 entries.Behaviour for users
Tests
packages/cs3d/test/voiLut.jest.js(43 tests):getRenderedBufferthat check the colour of a rendered pixel: one through a VOI LUT, one through a Modality LUT that starts above 0.Each test fails against the code before this change. A mutation check of 30 single-line changes to the conversion code fails at least one test each.
Manual check: the 1.7.6 server image with the patched
createImage.ts, and the two instances of the CR study stored by STOW, answers the series and instance thumbnails with 200. The series thumbnail shows the wrist with the correct MONOCHROME1 polarity. Without the patch, the same setup answers 404.