From 5862bb7905f9294e52d3f87189f424f7921c34b4 Mon Sep 17 00:00:00 2001 From: AlfandiMario Date: Thu, 24 Sep 2026 17:43:46 +0700 Subject: [PATCH] fix(dimse): forward multiframe attrs and return single frame on WADO-RS OHIF builds one imageId per frame from NumberOfFrames, but the proxy never returned it (QIDO instances + /metadata), so angio runs rendered a single frame with no cine. Also slice the requested frame in /instances/{uid}/frames/{frame} instead of returning the whole pixel data. --- src/dimse/parseMeta.ts | 8 ++++++++ src/dimse/tags.ts | 11 ++++++++++- src/dimse/wadoRs.ts | 29 +++++++++++++++++++++++------ src/routes/routes.ts | 6 +++--- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/dimse/parseMeta.ts b/src/dimse/parseMeta.ts index b6c6e69..46257a1 100644 --- a/src/dimse/parseMeta.ts +++ b/src/dimse/parseMeta.ts @@ -57,6 +57,10 @@ function parseFile(filename: string): Promise { const instanceNumber = dataset.string('x00200013'); const sliceThickness = dataset.string('x00180050'); const sliceLocation = dataset.string('x00201041'); + const numberOfFrames = dataset.string('x00280008'); + const recommendedDisplayFrameRate = dataset.string('x00082144'); + const cineRate = dataset.string('x00180040'); + const frameTime = dataset.string('x00181063'); // append to all results const result: ElementType = { @@ -86,6 +90,10 @@ function parseFile(filename: string): Promise { ...(iop && { '00200037': { Value: iop, vr: 'DS' } }), ...(ipp && { '00200032': { Value: ipp, vr: 'DS' } }), '00200013': { Value: [instanceNumber], vr: 'IS' }, + '00280008': { Value: [numberOfFrames], vr: 'IS' }, + '00082144': { Value: [recommendedDisplayFrameRate], vr: 'IS' }, + '00180040': { Value: [cineRate], vr: 'IS' }, + '00181063': { Value: [frameTime], vr: 'DS' }, '00180050': { Value: [sliceThickness], vr: 'DS' }, '00201041': { Value: [sliceLocation], vr: 'DS' }, }; diff --git a/src/dimse/tags.ts b/src/dimse/tags.ts index 9ae758b..4fda175 100644 --- a/src/dimse/tags.ts +++ b/src/dimse/tags.ts @@ -32,7 +32,16 @@ export const seriesLevelTags: Array = [ '00201209', ]; -export const imageLevelTags: Array = ['00080016', '00080018']; +export const imageLevelTags: Array = [ + '00080016', // SOPClassUID + '00080018', // SOPInstanceUID + '00200013', // InstanceNumber + '00280008', // NumberOfFrames + '00280010', // Rows + '00280011', // Columns + '00082144', // RecommendedDisplayFrameRate + '00180040', // CineRate +]; export function tagsForLevel(level: QUERY_LEVEL) { switch (level) { diff --git a/src/dimse/wadoRs.ts b/src/dimse/wadoRs.ts index 55d0668..50de066 100644 --- a/src/dimse/wadoRs.ts +++ b/src/dimse/wadoRs.ts @@ -22,7 +22,7 @@ type WadoRsArgs = { seriesInstanceUid?: string; sopInstanceUid?: string; dataFormat?: DataFormat; - frame?: number | number[]; + frame?: number; }; type WadoRsResponse = { contentType: string; @@ -91,9 +91,10 @@ interface AddFileToBuffer { filename: string, instanceInfo: InstanceInfo, dataFormat?: DataFormat, + frame?: number, } -async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo }: AddFileToBuffer): Promise { +async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo, frame }: AddFileToBuffer): Promise { const logger = LoggerSingleton.Instance; const filepath = path.join(pathname, filename); const buffArray: Buffer[] = []; @@ -128,8 +129,24 @@ async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo }: // Get the pixeldata from the DICOM and add it to the buffer. const dataset = dicomParser.parseDicom(data); const pixeldataElement = dataset.elements.x7fe00010; + let dataOffset = pixeldataElement.dataOffset; + let dataLength = pixeldataElement.length; + + // WADO-RS frame request: return only the requested frame's bytes. + // The file is recompressed to uncompressed (Implicit VR LE) above, so + // frames are contiguous and slicing by frame size is safe. + if (dataFormat === 'pixeldata' && typeof frame === 'number' && frame > 0) { + const rows = dataset.uint16('x00280010') ?? 0; + const cols = dataset.uint16('x00280011') ?? 0; + const samplesPerPixel = dataset.uint16('x00280002') ?? 1; + const bitsAllocated = dataset.uint16('x00280100') ?? 8; + const frameSize = rows * cols * samplesPerPixel * (bitsAllocated / 8); + dataOffset += (frame - 1) * frameSize; + dataLength = frameSize; + } + buffArray.push(Buffer.from(`Content-Type:application/octet-stream;${term}`)); - returnData = Buffer.from(dataset.byteArray.buffer, pixeldataElement.dataOffset, pixeldataElement.length); + returnData = Buffer.from(dataset.byteArray.buffer, dataOffset, dataLength); break; } case 'rendered': { @@ -157,7 +174,7 @@ type InstanceInfo = { instance?: string } -export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat }: WadoRsArgs): Promise { +export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat, frame }: WadoRsArgs): Promise { const logger = LoggerSingleton.Instance; // Set up all the paths and query levels. const storagePath = config.get(ConfParams.STORAGE_PATH) as string; @@ -237,14 +254,14 @@ export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanc buffers = await Promise.all(files.map(async (file) => { const instanceInfo = foundInstances.find((i) => i.instance === file) if (instanceInfo) { - return addFileToBuffer({ pathname, filename: file, dataFormat, instanceInfo }); + return addFileToBuffer({ pathname, filename: file, dataFormat, instanceInfo, frame }); } })) } else { // Attach the one file that we need to the return buffer const instanceInfo = { study: studyInstanceUid, series: seriesInstanceUid, instance: sopInstanceUid } - buffers = [await addFileToBuffer({ pathname: studyPath, filename, dataFormat, instanceInfo })]; + buffers = [await addFileToBuffer({ pathname: studyPath, filename, dataFormat, instanceInfo, frame })]; } // Set up the boundaries and join together all of the file buffers to form diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 62eb669..d06c30b 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -360,12 +360,12 @@ module.exports = function (server: FastifyInstance, opts: unknown, done: () => v //------------------------------------------------------------------ server.get<{ - Params: IParamsImage; + Params: IParamsImage & { frame: string }; }>('/rs/studies/:studyInstanceUid/series/:seriesInstanceUid/instances/:sopInstanceUid/frames/:frame', async (req, reply) => { - const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = req.params; + const { studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame } = req.params; try { - const rsp = await doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat: 'pixeldata' }); + const rsp = await doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat: 'pixeldata', frame: parseInt(frame, 10) }); reply.header('Content-Type', rsp.contentType); return reply.send(rsp.buffer); } catch (error) {