Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac162cd2a3 | ||
|
|
bfb55055a2 | ||
|
|
af9ec01c19 | ||
|
|
ae53d7faa3 |
@@ -2,7 +2,7 @@ version: '3'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
dicomweb-proxy:
|
dicomweb-proxy:
|
||||||
image: dicomweb-proxy:2.0
|
image: dicomweb-proxy:3.0.1
|
||||||
container_name: dicomweb-proxy
|
container_name: dicomweb-proxy
|
||||||
ports:
|
ports:
|
||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
|
|||||||
@@ -57,10 +57,34 @@ function parseFile(filename: string): Promise<ElementType> {
|
|||||||
const instanceNumber = dataset.string('x00200013');
|
const instanceNumber = dataset.string('x00200013');
|
||||||
const sliceThickness = dataset.string('x00180050');
|
const sliceThickness = dataset.string('x00180050');
|
||||||
const sliceLocation = dataset.string('x00201041');
|
const sliceLocation = dataset.string('x00201041');
|
||||||
const numberOfFrames = dataset.string('x00280008');
|
|
||||||
const recommendedDisplayFrameRate = dataset.string('x00082144');
|
// TODO: buat ini dynamic berdasarkan includefields atau modality
|
||||||
const cineRate = dataset.string('x00180040');
|
// TODO: hapus tambahan di bawah ini yang tidak diperlukan
|
||||||
const frameTime = dataset.string('x00181063');
|
|
||||||
|
// MR-specific tags for overlay
|
||||||
|
const spacingBetweenSlices = dataset.string('x00180088');
|
||||||
|
const percentPhaseFieldOfView = dataset.string('x00180094');
|
||||||
|
const fovDimensionElement = dataset.elements.x00181149;
|
||||||
|
const fovDimension = fovDimensionElement ? [dataset.uint16('x00181149', 0), dataset.uint16('x00181149', 1)] : null;
|
||||||
|
const acquisitionMatrixElement = dataset.elements.x00181310;
|
||||||
|
const acquisitionMatrix = acquisitionMatrixElement
|
||||||
|
? [dataset.uint16('x00181310', 0), dataset.uint16('x00181310', 1), dataset.uint16('x00181310', 2), dataset.uint16('x00181310', 3)]
|
||||||
|
: null;
|
||||||
|
const scanningSequence = dataset.string('x00180020');
|
||||||
|
const repetitionTime = dataset.string('x00180080');
|
||||||
|
const echoTime = dataset.string('x00180081');
|
||||||
|
const inversionTime = dataset.string('x00180082');
|
||||||
|
const receiveCoilName = dataset.string('x00181250');
|
||||||
|
const mrAcquisitionType = dataset.string('x00180023');
|
||||||
|
const phaseEncodingDirection = dataset.string('x00181312');
|
||||||
|
const numOfAverages = dataset.string('x00180083');
|
||||||
|
const echoTrainLength = dataset.string('x00180091');
|
||||||
|
const flipAngle = dataset.string('x00181314');
|
||||||
|
const pixelBandwidth = dataset.string('x00180095');
|
||||||
|
const acquisitionTime = dataset.string('x00080032');
|
||||||
|
const acquistionDurationTotal = dataset.string('x00189073'); // in seconds
|
||||||
|
const acquistionDurationPerFrame = dataset.string('x00189220'); // in ms
|
||||||
|
const parallelAcquisitionTechnique = dataset.string('x00189078');
|
||||||
|
|
||||||
// append to all results
|
// append to all results
|
||||||
const result: ElementType = {
|
const result: ElementType = {
|
||||||
@@ -90,13 +114,29 @@ function parseFile(filename: string): Promise<ElementType> {
|
|||||||
...(iop && { '00200037': { Value: iop, vr: 'DS' } }),
|
...(iop && { '00200037': { Value: iop, vr: 'DS' } }),
|
||||||
...(ipp && { '00200032': { Value: ipp, vr: 'DS' } }),
|
...(ipp && { '00200032': { Value: ipp, vr: 'DS' } }),
|
||||||
'00200013': { Value: [instanceNumber], vr: 'IS' },
|
'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' },
|
'00180050': { Value: [sliceThickness], vr: 'DS' },
|
||||||
'00201041': { Value: [sliceLocation], vr: 'DS' },
|
'00201041': { Value: [sliceLocation], vr: 'DS' },
|
||||||
|
'00180088': { Value: [spacingBetweenSlices], vr: 'DS' },
|
||||||
|
'00180094': { Value: [percentPhaseFieldOfView], vr: 'DS' },
|
||||||
|
...(fovDimension && { '00181149': { Value: fovDimension, vr: 'IS' } }),
|
||||||
|
...(acquisitionMatrix && { '00181310': { Value: acquisitionMatrix, vr: 'US' } }),
|
||||||
|
'00180020': { Value: [scanningSequence], vr: 'CS' },
|
||||||
|
'00180080': { Value: [repetitionTime], vr: 'DS' },
|
||||||
|
'00180081': { Value: [echoTime], vr: 'DS' },
|
||||||
|
'00180082': { Value: [inversionTime], vr: 'DS' },
|
||||||
|
'00181250': { Value: [receiveCoilName], vr: 'SH' },
|
||||||
|
'00180023': { Value: [mrAcquisitionType], vr: 'CS' },
|
||||||
|
'00181312': { Value: [phaseEncodingDirection], vr: 'CS' },
|
||||||
|
'00180083': { Value: [numOfAverages], vr: 'DS' },
|
||||||
|
'00180091': { Value: [echoTrainLength], vr: 'IS' },
|
||||||
|
'00181314': { Value: [flipAngle], vr: 'DS' },
|
||||||
|
'00180095': { Value: [pixelBandwidth], vr: 'DS' },
|
||||||
|
'00080032': { Value: [acquisitionTime], vr: 'TM' },
|
||||||
|
'00189073': { Value: [acquistionDurationTotal], vr: 'DS' },
|
||||||
|
'00189220': { Value: [acquistionDurationPerFrame], vr: 'FD' },
|
||||||
|
'00189078': { Value: [parallelAcquisitionTechnique], vr: 'CS' },
|
||||||
};
|
};
|
||||||
|
|
||||||
resolve(result);
|
resolve(result);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -113,7 +153,6 @@ export function parseMeta(json: object, studyInstanceUID: string, seriesInstance
|
|||||||
const sopInstanceUid = json[key]['00080018'].Value[0];
|
const sopInstanceUid = json[key]['00080018'].Value[0];
|
||||||
const pathname = path.join(storagePath, studyInstanceUID, sopInstanceUid);
|
const pathname = path.join(storagePath, studyInstanceUID, sopInstanceUid);
|
||||||
parsing.push(parseFile(pathname));
|
parsing.push(parseFile(pathname));
|
||||||
|
|
||||||
}
|
}
|
||||||
return Promise.all(parsing);
|
return Promise.all(parsing);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,7 @@ export const seriesLevelTags: Array<string> = [
|
|||||||
'00201209',
|
'00201209',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const imageLevelTags: Array<string> = [
|
export const imageLevelTags: Array<string> = ['00080016', '00080018'];
|
||||||
'00080016', // SOPClassUID
|
|
||||||
'00080018', // SOPInstanceUID
|
|
||||||
'00200013', // InstanceNumber
|
|
||||||
'00280008', // NumberOfFrames
|
|
||||||
'00280010', // Rows
|
|
||||||
'00280011', // Columns
|
|
||||||
'00082144', // RecommendedDisplayFrameRate
|
|
||||||
'00180040', // CineRate
|
|
||||||
];
|
|
||||||
|
|
||||||
export function tagsForLevel(level: QUERY_LEVEL) {
|
export function tagsForLevel(level: QUERY_LEVEL) {
|
||||||
switch (level) {
|
switch (level) {
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ type WadoRsArgs = {
|
|||||||
seriesInstanceUid?: string;
|
seriesInstanceUid?: string;
|
||||||
sopInstanceUid?: string;
|
sopInstanceUid?: string;
|
||||||
dataFormat?: DataFormat;
|
dataFormat?: DataFormat;
|
||||||
frame?: number;
|
frame?: number | number[];
|
||||||
};
|
};
|
||||||
type WadoRsResponse = {
|
type WadoRsResponse = {
|
||||||
contentType: string;
|
contentType: string;
|
||||||
@@ -91,10 +91,9 @@ interface AddFileToBuffer {
|
|||||||
filename: string,
|
filename: string,
|
||||||
instanceInfo: InstanceInfo,
|
instanceInfo: InstanceInfo,
|
||||||
dataFormat?: DataFormat,
|
dataFormat?: DataFormat,
|
||||||
frame?: number,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo, frame }: AddFileToBuffer): Promise<Buffer> {
|
async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo }: AddFileToBuffer): Promise<Buffer> {
|
||||||
const logger = LoggerSingleton.Instance;
|
const logger = LoggerSingleton.Instance;
|
||||||
const filepath = path.join(pathname, filename);
|
const filepath = path.join(pathname, filename);
|
||||||
const buffArray: Buffer[] = [];
|
const buffArray: Buffer[] = [];
|
||||||
@@ -129,24 +128,8 @@ async function addFileToBuffer({ pathname, filename, dataFormat, instanceInfo, f
|
|||||||
// Get the pixeldata from the DICOM and add it to the buffer.
|
// Get the pixeldata from the DICOM and add it to the buffer.
|
||||||
const dataset = dicomParser.parseDicom(data);
|
const dataset = dicomParser.parseDicom(data);
|
||||||
const pixeldataElement = dataset.elements.x7fe00010;
|
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}`));
|
buffArray.push(Buffer.from(`Content-Type:application/octet-stream;${term}`));
|
||||||
returnData = Buffer.from(dataset.byteArray.buffer, dataOffset, dataLength);
|
returnData = Buffer.from(dataset.byteArray.buffer, pixeldataElement.dataOffset, pixeldataElement.length);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'rendered': {
|
case 'rendered': {
|
||||||
@@ -174,7 +157,7 @@ type InstanceInfo = {
|
|||||||
instance?: string
|
instance?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat, frame }: WadoRsArgs): Promise<WadoRsResponse> {
|
export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat }: WadoRsArgs): Promise<WadoRsResponse> {
|
||||||
const logger = LoggerSingleton.Instance;
|
const logger = LoggerSingleton.Instance;
|
||||||
// Set up all the paths and query levels.
|
// Set up all the paths and query levels.
|
||||||
const storagePath = config.get(ConfParams.STORAGE_PATH) as string;
|
const storagePath = config.get(ConfParams.STORAGE_PATH) as string;
|
||||||
@@ -254,14 +237,14 @@ export async function doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanc
|
|||||||
buffers = await Promise.all(files.map(async (file) => {
|
buffers = await Promise.all(files.map(async (file) => {
|
||||||
const instanceInfo = foundInstances.find((i) => i.instance === file)
|
const instanceInfo = foundInstances.find((i) => i.instance === file)
|
||||||
if (instanceInfo) {
|
if (instanceInfo) {
|
||||||
return addFileToBuffer({ pathname, filename: file, dataFormat, instanceInfo, frame });
|
return addFileToBuffer({ pathname, filename: file, dataFormat, instanceInfo });
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Attach the one file that we need to the return buffer
|
// Attach the one file that we need to the return buffer
|
||||||
const instanceInfo = { study: studyInstanceUid, series: seriesInstanceUid, instance: sopInstanceUid }
|
const instanceInfo = { study: studyInstanceUid, series: seriesInstanceUid, instance: sopInstanceUid }
|
||||||
buffers = [await addFileToBuffer({ pathname: studyPath, filename, dataFormat, instanceInfo, frame })];
|
buffers = [await addFileToBuffer({ pathname: studyPath, filename, dataFormat, instanceInfo })];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the boundaries and join together all of the file buffers to form
|
// Set up the boundaries and join together all of the file buffers to form
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ module.exports = function (server: FastifyInstance, opts: unknown, done: () => v
|
|||||||
const hasStudyDate = query.StudyDate !== undefined;
|
const hasStudyDate = query.StudyDate !== undefined;
|
||||||
|
|
||||||
if (!hasMedicalRecord && !hasAccessionNumber && !hasPatientName && !hasStudyInstanceUID && !hasStudyDate) {
|
if (!hasMedicalRecord && !hasAccessionNumber && !hasPatientName && !hasStudyInstanceUID && !hasStudyDate) {
|
||||||
const startDate = moment().format('YYYYMMDD');
|
const startDate = moment().subtract(7, 'days').format('YYYYMMDD');
|
||||||
const endDate = moment().format('YYYYMMDD');
|
const endDate = moment().format('YYYYMMDD');
|
||||||
query.StudyDate = `${startDate}-${endDate}`;
|
query.StudyDate = `${startDate}-${endDate}`;
|
||||||
|
|
||||||
@@ -360,12 +360,12 @@ module.exports = function (server: FastifyInstance, opts: unknown, done: () => v
|
|||||||
//------------------------------------------------------------------
|
//------------------------------------------------------------------
|
||||||
|
|
||||||
server.get<{
|
server.get<{
|
||||||
Params: IParamsImage & { frame: string };
|
Params: IParamsImage;
|
||||||
}>('/rs/studies/:studyInstanceUid/series/:seriesInstanceUid/instances/:sopInstanceUid/frames/:frame', async (req, reply) => {
|
}>('/rs/studies/:studyInstanceUid/series/:seriesInstanceUid/instances/:sopInstanceUid/frames/:frame', async (req, reply) => {
|
||||||
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid, frame } = req.params;
|
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = req.params;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rsp = await doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat: 'pixeldata', frame: parseInt(frame, 10) });
|
const rsp = await doWadoRs({ studyInstanceUid, seriesInstanceUid, sopInstanceUid, dataFormat: 'pixeldata' });
|
||||||
reply.header('Content-Type', rsp.contentType);
|
reply.header('Content-Type', rsp.contentType);
|
||||||
return reply.send(rsp.buffer);
|
return reply.send(rsp.buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -410,6 +410,25 @@ module.exports = function (server: FastifyInstance, opts: unknown, done: () => v
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.get<{
|
||||||
|
Params: IParamsImage;
|
||||||
|
Querystring: QueryParams;
|
||||||
|
}>('/rs/studies/:studyInstanceUid/series/:seriesInstanceUid/instances/:sopInstanceUid/metadata', async (req, reply) => {
|
||||||
|
const { studyInstanceUid, seriesInstanceUid, sopInstanceUid } = req.params;
|
||||||
|
const { query } = req;
|
||||||
|
query.StudyInstanceUID = studyInstanceUid;
|
||||||
|
query.SeriesInstanceUID = seriesInstanceUid;
|
||||||
|
query.SOPInstanceUID = sopInstanceUid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rsp = await fetchMeta(query, studyInstanceUid, seriesInstanceUid);
|
||||||
|
return reply.send(rsp);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return reply.send(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user