diff --git a/.gitignore b/.gitignore index 55e5c85..55bee24 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,10 @@ node_modules/ # VS Code *.code-workspace -*~ -*.swp + +# Root commands +*.cmd + +# vim files +**/*~ +**/*.swp diff --git a/packages/static-wado-creator/lib/StaticWado.js b/packages/static-wado-creator/lib/StaticWado.js index 836bb8c..47a4571 100644 --- a/packages/static-wado-creator/lib/StaticWado.js +++ b/packages/static-wado-creator/lib/StaticWado.js @@ -53,14 +53,14 @@ async function cs3dThumbnail( dataset, metadata, transferSyntaxUid, - doneCallback + doneCallback, ) { if (!originalImageFrame) { throw new Error(`No originalImageFrame data available`); } if (!dataset && !metadata) { throw new Error( - `Neither dataset ${!!dataset} nor metadata ${!!metadata} available.` + `Neither dataset ${!!dataset} nor metadata ${!!metadata} available.`, ); } if (isVideo(transferSyntaxUid)) { @@ -74,7 +74,7 @@ async function cs3dThumbnail( originalImageFrame, dataset, metadata, - transferSyntaxUid + transferSyntaxUid, ); } catch (error) { console.log("Error while decoding image:", error); @@ -90,13 +90,13 @@ async function cs3dThumbnail( const pixelData = dicomCodec.getPixelData( imageFrame, imageInfo, - transferSyntaxUid + transferSyntaxUid, ); return staticCS.getRenderedBuffer( transferSyntaxUid, pixelData, metadata, - doneCallback + doneCallback, ); } @@ -146,7 +146,7 @@ class StaticWado { setStudyData, rawDicomWriter: RawDicomWriter(this.options), notificationService: new NotificationService( - this.options.notificationDir + this.options.notificationDir, ), internalGenerateImage: cs3dThumbnail, }; @@ -184,13 +184,13 @@ class StaticWado { if (!this.showProgress) return; this.processedFiles++; const percentage = Math.round( - (this.processedFiles / this.totalFiles) * 100 + (this.processedFiles / this.totalFiles) * 100, ); const progressBar = "=".repeat(Math.floor(percentage / 4)) + "-".repeat(25 - Math.floor(percentage / 4)); process.stdout.write( - `\r[${progressBar}] ${percentage}% | ${this.processedFiles}/${this.totalFiles} files` + `\r[${progressBar}] ${percentage}% | ${this.processedFiles}/${this.totalFiles} files`, ); } @@ -202,7 +202,7 @@ class StaticWado { if (fs.statSync(file).isDirectory()) { const dirFiles = fs.readdirSync(file, { recursive: true }); this.totalFiles += dirFiles.filter( - (f) => !fs.statSync(path.join(file, f)).isDirectory() + (f) => !fs.statSync(path.join(file, f)).isDirectory(), ).length; } else { this.totalFiles++; @@ -215,7 +215,7 @@ class StaticWado { { FailureReason: 0xd000, TextValue: "File not DICOM" }, ], }, - file + file, ); } } @@ -297,13 +297,13 @@ class StaticWado { sopInstanceUid: dataSet.string("x00080018"), transferSyntaxUid: dataSet.string("x00020010"), }, - params.file + params.file, ); const targetId = transcodeId( id, this.options, - dataSet.uint16(Tags.RawSamplesPerPixel) + dataSet.uint16(Tags.RawSamplesPerPixel), ); let bulkDataIndex = 0; @@ -323,7 +323,7 @@ class StaticWado { { gzip: false, mkdir: true, - } + }, ); await writeStream.write(bulkData); await writeStream.close(); @@ -332,7 +332,7 @@ class StaticWado { targetId, _bulkDataIndex, bulkData, - options + options, ); }, imageFrame: async (originalImageFrame) => { @@ -345,7 +345,7 @@ class StaticWado { targetId, originalImageFrame, dataSet, - this.options + this.options, ); const lossyImage = await generateLossyImage(id, decoded, this.options); @@ -357,7 +357,7 @@ class StaticWado { await this.callback.imageFrame( lossyImage.id, currentImageFrameIndex, - lossyImage.imageFrame + lossyImage.imageFrame, ); } @@ -369,13 +369,13 @@ class StaticWado { id, frameIndex: currentImageFrameIndex, }, - this.options + this.options, ); return this.callback.imageFrame( transcodedId, currentImageFrameIndex, - transcodedImageFrame + transcodedImageFrame, ); }, videoWriter: async (_dataSet) => this.callback.videoWriter(id, _dataSet), @@ -394,14 +394,14 @@ class StaticWado { dataSet, transcodedMeta, this.callback, - this.options + this.options, ); await thumbnailService.generateRendered( id, dataSet, transcodedMeta, this.callback, - this.options + this.options, ); await this.callback.metadata(targetId, transcodedMeta); @@ -418,14 +418,14 @@ class StaticWado { dataSet, metadata, transferSyntaxUid, - doneCallback + doneCallback, ) { return cs3dThumbnail( originalImageFrame, dataSet, metadata, transferSyntaxUid, - doneCallback + doneCallback, ); } @@ -464,7 +464,7 @@ class StaticWado { const study = await JSONReader( `${studiesDir}/${dir}`, "index.json.gz", - null + null, ); if (study === null) { console.log("No study found in", dir); diff --git a/packages/static-wado-creator/lib/createPart10.js b/packages/static-wado-creator/lib/createPart10.js index 0626788..ff21172 100644 --- a/packages/static-wado-creator/lib/createPart10.js +++ b/packages/static-wado-creator/lib/createPart10.js @@ -18,7 +18,7 @@ const createFmi = (instance) => { UncompressedLEIExplicit; const MediaStorageSOPClassUID = Tags.getValue( instance, - Tags.MediaStorageSOPClassUID + Tags.MediaStorageSOPClassUID, ); const SOPInstanceUID = Tags.getValue(instance, Tags.SOPInstanceUID); const naturalFmi = { diff --git a/packages/static-wado-creator/lib/createThumbnail.js b/packages/static-wado-creator/lib/createThumbnail.mjs similarity index 56% rename from packages/static-wado-creator/lib/createThumbnail.js rename to packages/static-wado-creator/lib/createThumbnail.mjs index 8fdaa3d..c095e4e 100644 --- a/packages/static-wado-creator/lib/createThumbnail.js +++ b/packages/static-wado-creator/lib/createThumbnail.mjs @@ -1,13 +1,15 @@ -const { +import { Tags, readBulkData, handleHomeRelative, -} = require("@radicalimaging/static-wado-util"); -const dcmjs = require("dcmjs"); + readBulkDataValue, + readAllBulkData, +} from "@radicalimaging/static-wado-util"; +import dcmjs from "dcmjs"; -const StaticWado = require("./StaticWado"); -const adaptProgramOpts = require("./util/adaptProgramOpts"); -const WriteStream = require("./writer/WriteStream"); +import StaticWado from "./StaticWado"; +import adaptProgramOpts from "./util/adaptProgramOpts"; +import WriteStream from "./writer/WriteStream"; const UncompressedLEIExplicit = "1.2.840.10008.1.2.1"; const { DicomDict, DicomMetaDictionary } = dcmjs.data; @@ -15,69 +17,7 @@ const { DicomDict, DicomMetaDictionary } = dcmjs.data; const fileMetaInformationVersionArray = new Uint8Array(2); fileMetaInformationVersionArray[1] = 1; -const readBulkDataValue = async (studyDir, instance, value, options) => { - const { BulkDataURI } = value; - value.vr = "OB"; - const seriesUid = Tags.getValue(instance, Tags.SeriesInstanceUID); - const numberOfFrames = Tags.getValue(instance, Tags.NumberOfFrames) || 1; - if (BulkDataURI.indexOf("/frames") !== -1) { - const seriesDir = `${studyDir}/series/${seriesUid}`; - if (options?.frame === false) { - return; - } - // Really only support a limited number of frames based on memory size - value.Value = []; - if (typeof options?.frame === "number") { - console.noQuiet("Reading frame", options.frame); - const bulk = await readBulkData(seriesDir, BulkDataURI, options.frame); - if (!bulk) { - return; - } - value.Value[options.frame - 1] = bulk.binaryData; - value.transferSyntaxUid = bulk.transferSyntaxUid; - value.contentType = bulk.contentType; - return; - } - console.noQuiet("Reading frames", 1, "...", numberOfFrames); - for (let frame = 1; frame <= numberOfFrames; frame++) { - const bulk = await readBulkData(seriesDir, BulkDataURI, frame); - if (!bulk) break; - value.Value.push(bulk.binaryData); - value.transferSyntaxUid = bulk.transferSyntaxUid; - value.contentType = bulk.contentType; - } - } else { - const bulk = await readBulkData(studyDir, BulkDataURI); - value.Value = [new ArrayBuffer(bulk.binaryData)]; - value.contentType = bulk.contentType; - } -}; - -const readBinaryData = async (dir, instance, options = { frame: true }) => { - for (const tag of Object.keys(instance)) { - const v = instance[tag]; - if (v.BulkDataURI) { - await readBulkDataValue(dir, instance, v, options); - continue; - } - if (!v.vr) { - const value0 = v.Value?.[0]; - if (typeof value0 === "string") { - v.vr = "LT"; - } else { - console.log("Deleting", tag, v.Value, v); - delete instance[tag]; - } - continue; - } - if (v.vr === "SQ" && v.Values?.length) { - await Promise.all(v.Values.map(readBinaryData)); - continue; - } - } -}; - -module.exports = async function createThumbnail(options, program) { +export async function createThumbnail(options, program) { const finalOptions = adaptProgramOpts(options, { ...this, // Instance metadata is the instances//metadata.gz files @@ -141,7 +81,7 @@ module.exports = async function createThumbnail(options, program) { continue; } - await readBinaryData(dir, instance, codecOptions); + await readAllBulkData(dir, instance, codecOptions); const availableTransferSyntaxUID = Tags.getValue( instance, @@ -178,4 +118,6 @@ module.exports = async function createThumbnail(options, program) { } } await Promise.all(promises); -}; +} + +export default createThumbnail; \ No newline at end of file diff --git a/packages/static-wado-creator/lib/mkdicomwebConfig.js b/packages/static-wado-creator/lib/mkdicomwebConfig.js index 50bac77..fc349c5 100644 --- a/packages/static-wado-creator/lib/mkdicomwebConfig.js +++ b/packages/static-wado-creator/lib/mkdicomwebConfig.js @@ -2,7 +2,8 @@ const ConfigPoint = require("config-point"); const { staticWadoConfig } = require("@radicalimaging/static-wado-util"); const createMain = require("./createMain"); const createPart10 = require("./createPart10"); -const createThumbnail = require("./createThumbnail"); +const { createThumbnail } = require("./createThumbnail"); +const { transcodeImages } = require("./transcodeImages"); const deleteMain = require("./deleteMain"); const rejectMain = require("./rejectMain"); const serverMain = require("./serverMain"); @@ -305,6 +306,12 @@ const { mkdicomwebConfig } = ConfigPoint.register({ }, ], }, + { + command: "transcode", + main: transcodeImages, + helpDescription: + "Transcode images or create alternate representations for the specified study", + }, { command: "instance", arguments: ["input"], diff --git a/packages/static-wado-creator/lib/operation/InstanceDeduplicate.js b/packages/static-wado-creator/lib/operation/InstanceDeduplicate.js index 575f7ab..9d743b9 100644 --- a/packages/static-wado-creator/lib/operation/InstanceDeduplicate.js +++ b/packages/static-wado-creator/lib/operation/InstanceDeduplicate.js @@ -44,7 +44,7 @@ async function deduplicateSingleInstance(id, imageFrame, { force }) { deduplicated, key, this.extractors[key], - TagLists.RemoveExtract + TagLists.RemoveExtract, ); const hashKey = getValue(extracted, Tags.DeduppedHash); await studyData.addExtracted(this, hashKey, extracted); @@ -99,7 +99,7 @@ const InstanceDeduplicate = (options) => const deduppedInstance = await this.deduplicateSingleInstance( id, imageFrame, - options + options, ); if (deduppedInstance) { // this refers to callee diff --git a/packages/static-wado-creator/lib/operation/ScanStudy.js b/packages/static-wado-creator/lib/operation/ScanStudy.js index 51afcb4..1f1ef35 100644 --- a/packages/static-wado-creator/lib/operation/ScanStudy.js +++ b/packages/static-wado-creator/lib/operation/ScanStudy.js @@ -32,7 +32,7 @@ function ScanStudy(options) { const deduplicatedInstancesPath = path.join( deduplicatedInstancesRoot, // studyInstanceUid, - studySubDir + studySubDir, ); const deduplicatedPath = path.join(deduplicatedRoot, studySubDir); console.verbose( @@ -40,7 +40,7 @@ function ScanStudy(options) { studyInstanceUid, studyPath, deduplicatedInstancesPath, - deduplicatedPath + deduplicatedPath, ); return this.completeStudy.getCurrentStudyData(this, { studyPath, diff --git a/packages/static-wado-creator/lib/operation/StudyData.js b/packages/static-wado-creator/lib/operation/StudyData.js index 1872a86..f2793d9 100644 --- a/packages/static-wado-creator/lib/operation/StudyData.js +++ b/packages/static-wado-creator/lib/operation/StudyData.js @@ -29,7 +29,7 @@ class StudyData { deduplicatedPath, deduplicatedInstancesPath, }, - { isGroup, clean, hashStudyUidPath } + { isGroup, clean, hashStudyUidPath }, ) { this.studyInstanceUid = studyInstanceUid; this.studyPath = studyPath; @@ -71,25 +71,25 @@ class StudyData { console.verbose("deduplicatedPath", this.deduplicatedPath); console.verbose( "deduplicatedInstancesPath", - this.deduplicatedInstancesPath + this.deduplicatedInstancesPath, ); this.groupFiles = 0; const studyDeduplicated = await JSONReader( this.studyPath, "deduplicated/index.json.gz", - [] + [], ); const info = studyDeduplicated[0]; if (info) { const hash = getValue(info, Tags.DeduppedHash); if (this.hashStudyUidPath) { console.verbose( - "Reading studies////deduplicated/index.json.gz" + "Reading studies////deduplicated/index.json.gz", ); } else { console.verbose( - "Reading studies//deduplicated/index.json.gz" + "Reading studies//deduplicated/index.json.gz", ); } this.readDeduplicatedData("index.json.gz", studyDeduplicated, hash); @@ -97,7 +97,7 @@ class StudyData { console.log( "No deduplicated/index.json to read in", this.studyPath, - "/deduplicated/index.json.gz" + "/deduplicated/index.json.gz", ); } if (this.deduplicatedPath) { @@ -106,7 +106,7 @@ class StudyData { } if (this.deduplicatedInstancesPath) { this.instanceFiles = await this.readDeduplicated( - this.deduplicatedInstancesPath + this.deduplicatedInstancesPath, ); } } @@ -135,7 +135,7 @@ class StudyData { } if (this.groupFiles > 0) { console.verbose( - "dirtyMetadata::Study level deduplicated doesn't match group files" + "dirtyMetadata::Study level deduplicated doesn't match group files", ); } try { @@ -153,7 +153,7 @@ class StudyData { } catch (e) { console.verbose( "dirtyMetadata::Exception, assume study metadata is dirty", - e + e, ); return true; } @@ -215,7 +215,7 @@ class StudyData { const deduplicated = this.deduplicated[index]; if (index < 0 || index >= this.deduplicated.length) { throw new Error( - `Can't read index ${index}, out of bounds [0..${this.deduplicated.length})` + `Can't read index ${index}, out of bounds [0..${this.deduplicated.length})`, ); } const refs = getList(deduplicated, Tags.DeduppedRef); @@ -237,7 +237,7 @@ class StudyData { console.log( "Already have extracted", hashKey, - getValue(item, Tags.DeduppedType) + getValue(item, Tags.DeduppedType), ); return; } @@ -384,7 +384,7 @@ class StudyData { console.log( "Skipping deleted instance", type, - getValue(seriesInstance, Tags.SeriesInstanceUID) + getValue(seriesInstance, Tags.SeriesInstanceUID), ); continue; } @@ -393,7 +393,7 @@ class StudyData { console.log( "Cant get seriesUid from", Tags.SeriesInstanceUID, - seriesInstance + seriesInstance, ); continue; } @@ -401,12 +401,12 @@ class StudyData { const seriesQuery = TagLists.extract( seriesInstance, "series", - TagLists.SeriesQuery + TagLists.SeriesQuery, ); const seriesPath = path.join( this.studyPath, "series", - seriesInstanceUid + seriesInstanceUid, ); series[seriesInstanceUid] = { seriesPath, @@ -417,7 +417,7 @@ class StudyData { } series[seriesInstanceUid].instances.push(seriesInstance); series[seriesInstanceUid].instancesQuery.push( - TagLists.extract(seriesInstance, "instance", TagLists.InstanceQuery) + TagLists.extract(seriesInstance, "instance", TagLists.InstanceQuery), ); } @@ -454,7 +454,7 @@ class StudyData { "Wrote instances", seriesUid, "with", - instancesQuery.length + instancesQuery.length, ); } @@ -463,13 +463,13 @@ class StudyData { "Wrote series", this.studyInstanceUid, "with", - seriesList.length + seriesList.length, ); const studyQuery = TagLists.extract( anInstance, "study", - TagLists.PatientStudyQuery + TagLists.PatientStudyQuery, ); studyQuery[Tags.ModalitiesInStudy] = { Value: modalitiesInStudy, vr: "CS" }; studyQuery[Tags.NumberOfStudyRelatedInstances] = { @@ -492,7 +492,7 @@ class StudyData { Object.values(this.extractData).length, "extract items and", this.deduplicated.length, - "instance items" + "instance items", ); await JSONWriter(this.studyPath, "deduplicated", [ infoItem, @@ -525,7 +525,7 @@ class StudyData { console.log( "Deleting instances referenced in", this.studyInstanceUid, - this.deduplicatedInstancesPath + this.deduplicatedInstancesPath, ); const files = await this.listJsonFiles(deduplicatedDirectory); console.noQuiet("Deleting", files.length, "files"); @@ -558,14 +558,14 @@ class StudyData { Object.values(this.extractData).length, "extract items and", this.deduplicated.length, - "instance items" + "instance items", ); setList( data, Tags.DeduppedRef, Object.keys(this.readHashes).filter( - () => this.deduplicatedHashes[hashValue] == undefined - ) + () => this.deduplicatedHashes[hashValue] == undefined, + ), ); const deduplicatedList = [ data, diff --git a/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js b/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js index f752fc2..052873a 100644 --- a/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js +++ b/packages/static-wado-creator/lib/operation/adapter/getImageInfo.js @@ -3,7 +3,8 @@ const { Tags } = require("@radicalimaging/static-wado-util"); * Minimum image info data to be used on transcode process by dicom-codec api. */ function getImageInfo(dataSet, instance) { - if (instance) { + if (instance || !dataSet.uint16) { + instance ||= dataSet; const rows = Tags.getValue(instance, Tags.Rows); const columns = Tags.getValue(instance, Tags.Columns); const bitsAllocated = Tags.getValue(instance, Tags.BitsAllocated); diff --git a/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js b/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js index 1262172..e05a6cc 100644 --- a/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js +++ b/packages/static-wado-creator/lib/operation/adapter/transcodeImage.js @@ -135,7 +135,7 @@ function getDestinationTranscoder(id) { const destinationTranscoderEntry = transcodeDestinationMap[id] || Object.values(transcodeDestinationMap).find( - (value) => value.transferSyntaxUid === id + (value) => value.transferSyntaxUid === id, ); return destinationTranscoderEntry; } @@ -149,7 +149,7 @@ function getDestinationTranscoder(id) { function getTranscoder( transferSyntaxUid, { contentType: greyContentType, colorContentType }, - samplesPerPixel + samplesPerPixel, ) { const contentType = samplesPerPixel === 3 ? colorContentType : greyContentType; @@ -188,7 +188,7 @@ function shouldTranscodeImageFrame(id, options, samplesPerPixel) { console.verbose( "Not transcoding because no decoder found for", transferSyntaxUid, - samplesPerPixel + samplesPerPixel, ); return false; } @@ -199,7 +199,7 @@ function shouldTranscodeImageFrame(id, options, samplesPerPixel) { "Not transcoding because recompress", recompress, "does not include", - transcoder.alias + transcoder.alias, ); return false; } @@ -241,7 +241,7 @@ function transcodeLog(options, msg, error = "") { } } -function scale(imageFrame, imageInfo) { +function scale(imageFrame, imageInfo, factor = 4) { const { rows, columns, bitsPerPixel, pixelRepresentation, samplesPerPixel } = imageInfo; let ArrayConstructor = Float32Array; @@ -257,12 +257,12 @@ function scale(imageFrame, imageInfo) { samplesPerPixel, }; const dest = { - rows: Math.round(rows / 4), - columns: Math.round(columns / 4), + rows: Math.round(rows / factor), + columns: Math.round(columns / factor), samplesPerPixel, }; dest.pixelData = new ArrayConstructor( - dest.rows * dest.columns * samplesPerPixel + dest.rows * dest.columns * samplesPerPixel, ); replicate(src, dest); @@ -286,6 +286,12 @@ function scale(imageFrame, imageInfo) { * In either case, saves it to /fsiz/frameNo as multipart related */ async function generateLossyImage(id, decoded, options) { + console.warn( + "generateLossyImage", + options.alternate, + options.alternateThumbnail, + !!decoded?.imageFrame, + ); if (!options.alternate) return; if (!decoded?.imageFrame) return; console.verbose("Writing alternate thumbnail"); @@ -296,19 +302,19 @@ async function generateLossyImage(id, decoded, options) { ...id, imageFrameRootPath: id.imageFrameRootPath.replace( "frames", - options.alternateName + options.alternateName, ), transferSyntaxUid: transcodeDestinationMap.jhc.transferSyntaxUid, }; let lossy = true; - if (options.alternateThumbnail && imageInfo.rows >= 512) { - const scaled = scale(imageFrame, imageInfo); + if (options.alternateThumbnail && imageInfo.rows >= 128) { + const scaled = scale(imageFrame, imageInfo, imageInfo.rows > 512 ? 4 : 2); if (!scaled) { console.log("Couldn't scale"); return; } - imageFrame = Buffer.from(scaled.imageFrame.buffer); + imageFrame = scaled.imageFrame; imageInfo = scaled.imageInfo; } @@ -330,13 +336,13 @@ async function generateLossyImage(id, decoded, options) { imageFrame, imageInfo, lossyId.transferSyntaxUid, - encodeOptions + encodeOptions, ); console.verbose( "Encoded alternate", lossyId.transferSyntaxUid, "of size", - lossyEncoding.imageFrame.length + lossyEncoding.imageFrame.length, ); return { id: lossyId, imageFrame: lossyEncoding.imageFrame }; @@ -346,7 +352,10 @@ async function generateLossyImage(id, decoded, options) { } function isPalette(dataSet) { - return dataSet.string(Tags.RawPhotometricInterpretation) === "PALETTE COLOR"; + const pmi = + dataSet.string?.(Tags.RawPhotometricInterpretation) || + Tags.getValue(dataSet, Tags.PhotometricInterpretation); + return pmi === "PALETTE COLOR"; } /** @@ -364,13 +373,17 @@ async function transcodeImageFrame( targetIdSrc, imageFrame, dataSet, - options = {} + options = {}, ) { let targetId = targetIdSrc; let result = {}; - const samplesPerPixel = dataSet.uint16(Tags.RawSamplesPerPixel); - const planarConfiguration = dataSet.uint16("x00280006"); + const samplesPerPixel = + dataSet.uint16?.(Tags.RawSamplesPerPixel) || + Tags.getValue(dataSet, Tags.SamplesPerPixel); + const planarConfiguration = + dataSet.uint16?.("x00280006") || + Tags.getValue(dataSet, Tags.PlanarConfiguration); if ( !shouldTranscodeImageFrame(id, options, samplesPerPixel) || planarConfiguration === 1 || @@ -387,7 +400,7 @@ async function transcodeImageFrame( const transcoder = getTranscoder( id.transferSyntaxUid, options, - samplesPerPixel + samplesPerPixel, ); // Don't transcode if not required @@ -398,7 +411,7 @@ async function transcodeImageFrame( console.verbose( "Image is already in", targetId.transferSyntaxUid, - "not transcoding" + "not transcoding", ); return { id, @@ -422,54 +435,56 @@ async function transcodeImageFrame( case transcodeOp.transcode: transcodeLog( options, - `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m` + `Full transcoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`, ); decoded = await dicomCodec.decode( imageFrame, imageInfo, - id.transferSyntaxUid + id.transferSyntaxUid, ); result = await dicomCodec.encode( decoded.imageFrame, decoded.imageInfo, targetId.transferSyntaxUid, - encodeOptions + encodeOptions, ); console.verbose( "transcoded image to", targetId.transferSyntaxUid, "of size", - result.imageFrame.length + result.imageFrame.length, ); processResultMsg = `Transcoding finished`; break; case transcodeOp.encode: transcodeLog( options, - `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m` + `Encoding image to \x1b[43m${targetId.transferSyntaxUid}\x1b[0m`, ); result = await dicomCodec.encode( imageFrame, imageInfo, targetId.transferSyntaxUid, - encodeOptions + encodeOptions, ); + decoded = { imageFrame, imageInfo }; processResultMsg = `Encoding finished`; break; case transcodeOp.decode: transcodeLog( options, - `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m` + `Decoding image from \x1b[43m${id.transferSyntaxUid}\x1b[0m`, ); result = await dicomCodec.decode( imageFrame, imageInfo, - id.transferSyntaxUid + id.transferSyntaxUid, ); + decoded = { imageFrame: result, imageInfo }; processResultMsg = `Decoding finished`; break; @@ -479,7 +494,7 @@ async function transcodeImageFrame( done = !!result.imageFrame; } catch (e) { - transcodeLog(options, "Failed to transcode image", e); + console.noQuiet("Failed to transcode image", e); } // recover transfer syntax @@ -520,7 +535,7 @@ function transcodeId(id, options, samplesPerPixel) { const { transferSyntaxUid } = getTranscoder( id.transferSyntaxUid, options, - samplesPerPixel + samplesPerPixel, ); targetId.transferSyntaxUid = transferSyntaxUid; @@ -553,7 +568,7 @@ function transcodeMetadata(metadata, id, options) { Tags.setValue( result, Tags.AvailableTransferSyntaxUID, - transcodedId.transferSyntaxUid + transcodedId.transferSyntaxUid, ); console.verbose("Apply available tsuid", transcodeId.transferSyntaxUid); } diff --git a/packages/static-wado-creator/lib/operation/extractImageFrames.js b/packages/static-wado-creator/lib/operation/extractImageFrames.js index 60bdbf9..9c4a2ca 100644 --- a/packages/static-wado-creator/lib/operation/extractImageFrames.js +++ b/packages/static-wado-creator/lib/operation/extractImageFrames.js @@ -35,20 +35,20 @@ const extractImageFrames = async (dataSet, attr, vr, callback) => { dataSet, attr, frameIndex, - framesAreFragmented + framesAreFragmented, ); BulkDataURI = await callback.imageFrame(compressedFrame, { dataSet }); Stats.OverallStats.add( "Image Write", `Write image frame ${frameIndex + 1}`, - 5000 + 5000, ); } else { const uncompressedFrame = getUncompressedImageFrame( dataSet, attr, frameIndex, - uncompressedFrameSize + uncompressedFrameSize, ); BulkDataURI = await callback.imageFrame(uncompressedFrame, { dataSet }); } diff --git a/packages/static-wado-creator/lib/transcodeImages.mjs b/packages/static-wado-creator/lib/transcodeImages.mjs new file mode 100644 index 0000000..c2a4e5d --- /dev/null +++ b/packages/static-wado-creator/lib/transcodeImages.mjs @@ -0,0 +1,125 @@ +import { + Tags, + readBulkData, + handleHomeRelative, + readBulkDataValue, + readAllBulkData, +} from "@radicalimaging/static-wado-util"; +import dcmjs from "dcmjs"; + +import StaticWado from "./StaticWado"; +import { transcodeImageFrame } from "./operation/adapter/transcodeImage"; +import adaptProgramOpts from "./util/adaptProgramOpts"; +import WriteStream from "./writer/WriteStream"; + +const UncompressedLEIExplicit = "1.2.840.10008.1.2.1"; +const { DicomDict, DicomMetaDictionary } = dcmjs.data; + +const fileMetaInformationVersionArray = new Uint8Array(2); +fileMetaInformationVersionArray[1] = 1; + +export async function transcodeImages(options, program) { + const finalOptions = adaptProgramOpts(options, { + ...this, + // Instance metadata is the instances//metadata.gz files + isInstance: false, + // Deduplicated data is single instance deduplicated data + isDeduplicate: false, + // Group data is the group file directories + isGroup: false, + isStudyData: false, + isDeleteInstances: false, + }); + const importer = new StaticWado(finalOptions); + const studyInstanceUid = program.args[0]; + const dir = `${importer.options.rootDir}/studies/${studyInstanceUid}`; + console.noQuiet("Creating alternate for", studyInstanceUid, dir); + const study = await importer.callback.scanStudy(studyInstanceUid); + const outputPath = `./studies/${studyInstanceUid}`; + const { sopInstanceUid, seriesInstanceUid } = + options; + const sops = sopInstanceUid ? new Set() : null; + if (sopInstanceUid) { + for (const sop of sopInstanceUid.split(",").map((s) => s.trim())) { + sops.add(sop); + } + } + const promises = []; + + const writeAlternate = async (id, buffer) => { + console.verbose("Write alternate", id, buffer); + await importer.callback.thumbWriter( + id.sopInstanceRootPath, + "thumbnail", + buffer + ); + }; + + // TODO - choose middle frame of multiframe and choose middle image of + // series for series thumbnail, and first series for study thumbnail + const codecOptions = { frame: 1 }; + + for (const sop of study.getSopUids()) { + if (sops && !sops.has(sop)) { + continue; + } + const instance = await study.recombine(sop); + + if ( + seriesInstanceUid && + Tags.getValue(instance, Tags.SeriesInstanceUID) !== seriesInstanceUid + ) { + continue; + } + + await readAllBulkData(dir, instance, codecOptions); + + const availableTransferSyntaxUID = Tags.getValue( + instance, + Tags.AvailableTransferSyntaxUID + ); + const pixelData = Tags.getValue(instance, Tags.PixelData); + if (!pixelData) { + console.warn("No pixel data found in instance"); + continue; + } + const transferSyntaxUid = + instance[Tags.PixelData].transferSyntaxUid || availableTransferSyntaxUID; + const frame = Array.isArray(pixelData.Value) + ? pixelData.Value[0] + : pixelData; + console.verbose("Use transfer syntax uid:", transferSyntaxUid); + const id = importer.callback.uids({ + studyInstanceUid, + seriesInstanceUid: Tags.getValue(instance, Tags.SeriesInstanceUID), + sopInstanceUid: sop, + transferSyntaxUid, + }); + + const targetId = { + transferSyntaxUid: "1.2.840.10008.1.2.4.70", + }; + const transcodedFrame = await transcodeImageFrame( + id, + targetId, + frame, + instance, + { ...options, forceTranscode: true } + ); + console.warn("Got transcoded frame", transcodedFrame); + + // const promise = importer.callback.internalGenerateImage( + // frame, + // null, + // instance, + // id.transferSyntaxUid, + // writeAlternate.bind(null, id) + // ); + const promise = null; + console.warn("TODO - call alternate to generate alternate representation"); + promises.push(promise); + } + await Promise.all(promises); +} + +export default transcodeImages; \ No newline at end of file diff --git a/packages/static-wado-creator/lib/util/IdCreator.js b/packages/static-wado-creator/lib/util/IdCreator.js index e8ca1ae..780739e 100644 --- a/packages/static-wado-creator/lib/util/IdCreator.js +++ b/packages/static-wado-creator/lib/util/IdCreator.js @@ -25,14 +25,14 @@ function IdCreator({ const sopInstanceRootPath = path.join( seriesRootPath, "instances", - sopInstanceUid + sopInstanceUid, ); const imageFrameRootPath = path.join(sopInstanceRootPath, "frames"); const deduplicatedPath = path.join(deduplicatedRoot, studySubDir); const deduplicatedInstancesPath = path.join( deduplicatedInstancesRoot, - studySubDir + studySubDir, ); return { diff --git a/packages/static-wado-creator/lib/writer/CompleteStudyWriter.js b/packages/static-wado-creator/lib/writer/CompleteStudyWriter.js index cb3ed80..84c9b50 100644 --- a/packages/static-wado-creator/lib/writer/CompleteStudyWriter.js +++ b/packages/static-wado-creator/lib/writer/CompleteStudyWriter.js @@ -28,12 +28,12 @@ const CompleteStudyWriter = (options) => { await studyData.writeDeduplicatedGroup(); console.noQuiet( "Wrote updated deduplicated data for study", - studyData.studyInstanceUid + studyData.studyInstanceUid, ); } else { console.noQuiet( "Not writing new deduplicated data because it is clean:", - studyData.studyInstanceUid + studyData.studyInstanceUid, ); const message = { text: "Metadata clean, not updating", @@ -58,7 +58,7 @@ const CompleteStudyWriter = (options) => { console.log("Study metadata", studyData.studyInstanceUid, "is clean."); delete this.studyData; Stats.StudyStats.summarize( - `Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing` + `Study metadata ${studyData.studyInstanceUid} has clean metadata, not writing`, ); return; } @@ -69,7 +69,7 @@ const CompleteStudyWriter = (options) => { const allStudies = await JSONReader( options.directoryName, "studies/index.json.gz", - [] + [], ); const studyUID = Tags.getValue(studyQuery, Tags.StudyInstanceUID); if (!studyUID) { @@ -77,7 +77,7 @@ const CompleteStudyWriter = (options) => { throw new Error("Study query has null studyUID"); } const studyIndex = allStudies.findIndex( - (item) => Tags.getValue(item, Tags.StudyInstanceUID) == studyUID + (item) => Tags.getValue(item, Tags.StudyInstanceUID) == studyUID, ); if (studyIndex == -1) { allStudies.push(studyQuery); @@ -95,7 +95,7 @@ const CompleteStudyWriter = (options) => { this.success("completeStudyWriter", message); delete this.studyData; Stats.StudyStats.summarize( - `Wrote study metadata/query files for ${studyData.studyInstanceUid}` + `Wrote study metadata/query files for ${studyData.studyInstanceUid}`, ); } diff --git a/packages/static-wado-util/lib/dictionary/Tags.js b/packages/static-wado-util/lib/dictionary/Tags.js index 1f1f605..eaa2402 100644 --- a/packages/static-wado-util/lib/dictionary/Tags.js +++ b/packages/static-wado-util/lib/dictionary/Tags.js @@ -27,7 +27,7 @@ const findPrivate = (item, tagObject, create) => { if (create) { if (!assignPosition) throw new Error( - `Couldn't find any assign positions for ${creator} ${tag} in ${item}` + `Couldn't find any assign positions for ${creator} ${tag} in ${item}`, ); const creatorTag = `${start}00${assignPosition.toString(16)}`; item[creatorTag] = { Value: [creator], vr: "CS" }; @@ -137,3 +137,4 @@ Object.keys(dataDictionary).forEach((key) => { }); module.exports = Tags; +module.exports.Tags = Tags; diff --git a/packages/static-wado-util/lib/index.js b/packages/static-wado-util/lib/index.js index 49d8493..b1be81b 100644 --- a/packages/static-wado-util/lib/index.js +++ b/packages/static-wado-util/lib/index.js @@ -33,6 +33,7 @@ module.exports.program = program; module.exports.Stats = Stats; module.exports.getStudyUIDPathAndSubPath = require("./getStudyUIDPathAndSubPath"); module.exports.createStudyDirectories = require("./createStudyDirectories"); +module.exports.readBulkDataValue = require("./reader/readBulkDataValue"); module.exports.default = { ...module.exports, diff --git a/packages/static-wado-util/lib/index.ts b/packages/static-wado-util/lib/index.ts index efdd60d..f076631 100644 --- a/packages/static-wado-util/lib/index.ts +++ b/packages/static-wado-util/lib/index.ts @@ -25,6 +25,7 @@ import getStudyUIDPathAndSubPath from './getStudyUIDPathAndSubPath'; export { program, configureProgram, configureCommands } from "./program"; export * from "./dicomToXml"; import createStudyDirectories from "./createStudyDirectories"; +export * from "./reader/readBulkDataValue"; export { extractMultipart, diff --git a/packages/static-wado-util/lib/reader/JSONReader.js b/packages/static-wado-util/lib/reader/JSONReader.js index aac9413..c0e63d6 100644 --- a/packages/static-wado-util/lib/reader/JSONReader.js +++ b/packages/static-wado-util/lib/reader/JSONReader.js @@ -31,13 +31,13 @@ const JSONReader = async (dirSrc, name, def) => { JSONReader.readHashData = async ( studyDir, hashValue, - extension = ".json.gz" + extension = ".json.gz", ) => { const hashPath = path.join( studyDir, "bulkdata", hashValue.substring(0, 3), - hashValue.substring(3, 5) + hashValue.substring(3, 5), ); Stats.StudyStats.add("Read Hash Data", "Read hash data", 100); return JSONReader(hashPath, hashValue.substring(5) + extension); diff --git a/packages/static-wado-util/lib/reader/readBulkData.js b/packages/static-wado-util/lib/reader/readBulkData.js index f2db71b..8f95b63 100644 --- a/packages/static-wado-util/lib/reader/readBulkData.js +++ b/packages/static-wado-util/lib/reader/readBulkData.js @@ -111,7 +111,7 @@ const readBulkData = async (dirSrc, baseName, frame) => { console.noQuiet( "Bulkdata content type", `"${contentType}"`, - `"${transferSyntaxUid}"` + `"${transferSyntaxUid}"`, ); } } diff --git a/packages/static-wado-util/lib/reader/readBulkDataValue.mjs b/packages/static-wado-util/lib/reader/readBulkDataValue.mjs new file mode 100644 index 0000000..b757d2d --- /dev/null +++ b/packages/static-wado-util/lib/reader/readBulkDataValue.mjs @@ -0,0 +1,70 @@ +import {Tags} from "../dictionary/Tags"; +import readBulkData from "./readBulkData"; + +export const readBulkDataValue = async (studyDir, instance, value, options) => { + const { BulkDataURI } = value; + value.vr = "OB"; + const seriesUid = Tags.getValue(instance, Tags.SeriesInstanceUID); + const numberOfFrames = Tags.getValue(instance, Tags.NumberOfFrames) || 1; + if (BulkDataURI.indexOf("/frames") !== -1) { + const seriesDir = `${studyDir}/series/${seriesUid}`; + if (options?.frame === false) { + return; + } + // Really only support a limited number of frames based on memory size + value.Value = []; + if (typeof options?.frame === "number") { + console.noQuiet("Reading frame", options.frame); + const bulk = await readBulkData(seriesDir, BulkDataURI, options.frame); + if (!bulk) { + return; + } + value.Value[options.frame - 1] = bulk.binaryData; + value.transferSyntaxUid = bulk.transferSyntaxUid; + value.contentType = bulk.contentType; + return; + } + console.noQuiet("Reading frames", 1, "...", numberOfFrames); + for (let frame = 1; frame <= numberOfFrames; frame++) { + const bulk = await readBulkData(seriesDir, BulkDataURI, frame); + if (!bulk) break; + value.Value.push(bulk.binaryData); + value.transferSyntaxUid = bulk.transferSyntaxUid; + value.contentType = bulk.contentType; + } + } else { + const bulk = await readBulkData(studyDir, BulkDataURI); + value.Value = [new ArrayBuffer(bulk.binaryData)]; + value.contentType = bulk.contentType; + } +}; + +export const readAllBulkData = async ( + dir, + instance, + options = { frame: true } +) => { + for (const tag of Object.keys(instance)) { + const v = instance[tag]; + if (v.BulkDataURI) { + await readBulkDataValue(dir, instance, v, options); + continue; + } + if (!v.vr) { + const value0 = v.Value?.[0]; + if (typeof value0 === "string") { + v.vr = "LT"; + } else { + console.log("Deleting", tag, v.Value, v); + delete instance[tag]; + } + continue; + } + if (v.vr === "SQ" && v.Values?.length) { + await Promise.all(v.Values.map(readAllBulkData)); + continue; + } + } +}; + +export default readBulkDataValue; \ No newline at end of file diff --git a/packages/static-wado-util/lib/writer/JSONWriter.js b/packages/static-wado-util/lib/writer/JSONWriter.js index 415845a..f0652cf 100644 --- a/packages/static-wado-util/lib/writer/JSONWriter.js +++ b/packages/static-wado-util/lib/writer/JSONWriter.js @@ -9,7 +9,7 @@ const JSONWriter = async ( dirSrc, name, data, - options = { gzip: true, brotli: false, index: true, overwrite: true } + options = { gzip: true, brotli: false, index: true, overwrite: true }, ) => { const fileName = options.index ? "index.json.gz" @@ -19,13 +19,13 @@ const JSONWriter = async ( if (options.overwrite === false && fs.existsSync(`${dirName}/${fileName}`)) { console.verbose( - `File already exists. Skipping JSON file creation at "${dirName}" named "${fileName}"` + `File already exists. Skipping JSON file creation at "${dirName}" named "${fileName}"`, ); Stats.StudyStats.add( "JSON not written", `Did not write JSON file ${name}`, - 1000 + 1000, ); return;