1 Commits

Author SHA1 Message Date
AlfandiMario
5862bb7905 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.
2026-09-24 17:43:46 +07:00
4 changed files with 44 additions and 10 deletions

View File

@@ -57,6 +57,10 @@ function parseFile(filename: string): Promise<ElementType> {
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<ElementType> {
...(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' },
};

View File

@@ -32,7 +32,16 @@ export const seriesLevelTags: Array<string> = [
'00201209',
];
export const imageLevelTags: Array<string> = ['00080016', '00080018'];
export const imageLevelTags: Array<string> = [
'00080016', // SOPClassUID
'00080018', // SOPInstanceUID
'00200013', // InstanceNumber
'00280008', // NumberOfFrames
'00280010', // Rows
'00280011', // Columns
'00082144', // RecommendedDisplayFrameRate
'00180040', // CineRate
];
export function tagsForLevel(level: QUERY_LEVEL) {
switch (level) {

View File

@@ -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<Buffer> {
async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo, frame }: AddFileToBuffer): Promise<Buffer> {
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<WadoRsResponse> {
export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat, frame }: WadoRsArgs): Promise<WadoRsResponse> {
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

View File

@@ -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) {