diff --git a/package-lock.json b/package-lock.json index 22978b6c..38534a10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.435.0", "@itk-wasm/dicom": "6.0.1", + "@itk-wasm/htj2k": "^2.3.1", "@itk-wasm/image-io": "1.1.1", "@kitware/vtk.js": "30.9.0", "@netlify/edge-functions": "^2.0.0", @@ -3397,6 +3398,114 @@ "itk-wasm": "^1.0.0-b.170" } }, + "node_modules/@itk-wasm/htj2k": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@itk-wasm/htj2k/-/htj2k-2.3.1.tgz", + "integrity": "sha512-A99SicVtpkrlvGNldEg8A/UmuIXxtwZsu8R8Ppzbf7peljPAzHCfgCbYe61eAorRZ6sxPy7a5o6crt9ry39//w==", + "license": "Apache-2.0", + "dependencies": { + "itk-wasm": "^1.0.0-b.177" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/itk-wasm": { + "version": "1.0.0-b.178", + "resolved": "https://registry.npmjs.org/itk-wasm/-/itk-wasm-1.0.0-b.178.tgz", + "integrity": "sha512-kW8sP2+6CwLL7VUC7jjRBnDjqsDLl/ZjRSwdcIj1XJ3I/JvFLIkLfPjsdaOlcG4VpKsi+LBQWpvpEb99iR9aWw==", + "license": "Apache-2.0", + "dependencies": { + "@itk-wasm/dam": "^1.1.1", + "@thewtex/zstddec": "^0.2.1", + "@types/emscripten": "^1.39.10", + "axios": "^1.6.2", + "chalk": "^5.3.0", + "comlink": "^4.4.1", + "commander": "^11.1.0", + "fs-extra": "^11.2.0", + "glob": "^8.1.0", + "markdown-table": "^3.0.3", + "mime-types": "^2.1.35", + "wasm-feature-detect": "^1.6.1" + }, + "bin": { + "itk-wasm": "src/itk-wasm-cli.js" + } + }, + "node_modules/@itk-wasm/htj2k/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/@itk-wasm/image-io": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@itk-wasm/image-io/-/image-io-1.1.1.tgz", @@ -26039,6 +26148,83 @@ "itk-wasm": "^1.0.0-b.170" } }, + "@itk-wasm/htj2k": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@itk-wasm/htj2k/-/htj2k-2.3.1.tgz", + "integrity": "sha512-A99SicVtpkrlvGNldEg8A/UmuIXxtwZsu8R8Ppzbf7peljPAzHCfgCbYe61eAorRZ6sxPy7a5o6crt9ry39//w==", + "requires": { + "itk-wasm": "^1.0.0-b.177" + }, + "dependencies": { + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "requires": { + "balanced-match": "^1.0.0" + } + }, + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==" + }, + "commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==" + }, + "fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + } + }, + "itk-wasm": { + "version": "1.0.0-b.178", + "resolved": "https://registry.npmjs.org/itk-wasm/-/itk-wasm-1.0.0-b.178.tgz", + "integrity": "sha512-kW8sP2+6CwLL7VUC7jjRBnDjqsDLl/ZjRSwdcIj1XJ3I/JvFLIkLfPjsdaOlcG4VpKsi+LBQWpvpEb99iR9aWw==", + "requires": { + "@itk-wasm/dam": "^1.1.1", + "@thewtex/zstddec": "^0.2.1", + "@types/emscripten": "^1.39.10", + "axios": "^1.6.2", + "chalk": "^5.3.0", + "comlink": "^4.4.1", + "commander": "^11.1.0", + "fs-extra": "^11.2.0", + "glob": "^8.1.0", + "markdown-table": "^3.0.3", + "mime-types": "^2.1.35", + "wasm-feature-detect": "^1.6.1" + } + }, + "minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "requires": { + "brace-expansion": "^2.0.1" + } + } + } + }, "@itk-wasm/image-io": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@itk-wasm/image-io/-/image-io-1.1.1.tgz", diff --git a/package.json b/package.json index b58b4938..72deeb99 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "dependencies": { "@aws-sdk/client-s3": "^3.435.0", "@itk-wasm/dicom": "6.0.1", + "@itk-wasm/htj2k": "^2.3.1", "@itk-wasm/image-io": "1.1.1", "@kitware/vtk.js": "30.9.0", "@netlify/edge-functions": "^2.0.0", diff --git a/src/core/streaming/ahiChunkImage.ts b/src/core/streaming/ahiChunkImage.ts index 4e0ae41f..18728194 100644 --- a/src/core/streaming/ahiChunkImage.ts +++ b/src/core/streaming/ahiChunkImage.ts @@ -24,9 +24,44 @@ import { } from '@/src/core/streaming/chunkImage'; import { ComputedRef, Ref, computed, ref } from 'vue'; import mitt, { Emitter } from 'mitt'; +import { + decode, + encode, + setPipelinesBaseUrl, + getPipelinesBaseUrl, + setPipelineWorkerUrl, + getPipelineWorkerUrl, +} from '@itk-wasm/htj2k'; const nameToMetaKey = { - SOPInstanceUID: 'ID', + SOPInstanceUID: 'SOPInstanceUID', + ImagePositionPatient: 'ImagePositionPatient', + ImageOrientationPatient: 'ImageOrientationPatient', + PixelSpacing: 'PixelSpacing', + Rows: 'Rows', + Columns: 'Columns', + BitsStored: 'BitsStored', + PixelRepresentation: 'PixelRepresentation', + SamplesPerPixel: 'SamplesPerPixel', + RescaleIntercept: 'RescaleIntercept', + RescaleSlope: 'RescaleSlope', + NumberOfFrames: 'NumberOfFrames', + PatientID: 'PatientID', + PatientName: 'PatientName', + PatientBirthDate: 'PatientBirthDate', + PatientSex: 'PatientSex', + StudyID: 'StudyID', + StudyInstanceUID: 'StudyInstanceUID', + StudyDate: 'StudyDate', + StudyTime: 'StudyTime', + AccessionNumber: 'AccessionNumber', + StudyDescription: 'StudyDescription', + Modality: 'Modality', + SeriesInstanceUID: 'SeriesInstanceUID', + SeriesNumber: 'SeriesNumber', + SeriesDescription: 'SeriesDescription', + WindowLevel: 'WindowLevel', + WindowWidth: 'WindowWidth', }; const { fastComputeRange } = vtkDataArray; @@ -66,9 +101,11 @@ function itkImageToURI(itkImage: Image) { } async function dicomSliceToImageUri(blob: Blob) { - const file = new File([blob], 'file.dcm'); - const itkImage = await readVolumeSlice(file, true); - return itkImageToURI(itkImage); + const array = await blob.arrayBuffer(); + const uint8Array = new Uint8Array(array); + const result = await decode(uint8Array); + console.log(result); + return itkImageToURI(result.image); } export default class AhiChunkImage implements ChunkImage { @@ -244,12 +281,14 @@ export default class AhiChunkImage implements ChunkImage { const chunk = this.chunks[chunkIndex]; if (!chunk.dataBlob) throw new Error('Chunk does not have data'); - const result = await readImage( - new File([chunk.dataBlob], `file-${chunkIndex}.dcm`), - { - webWorker: getWorker(), - } - ); + + // await chunk.dataBlob.arrayBuffer() + const array = await chunk.dataBlob.arrayBuffer(); + const uint8Array = new Uint8Array(array); + // const result = await decode(uint8Array, { + // webWorker: getWorker(), + // }); + const result = await decode(uint8Array); if (!result.image.data) throw new Error('No data read from chunk'); @@ -300,6 +339,7 @@ export default class AhiChunkImage implements ChunkImage { } private updateDicomStore() { + console.log('updateDicomStore', this.chunks.length); if (this.chunks.length === 0) return; const firstChunk = this.chunks[0]; diff --git a/src/io/import/awsAhi.ts b/src/io/import/awsAhi.ts index d9d6818d..f13c289c 100644 --- a/src/io/import/awsAhi.ts +++ b/src/io/import/awsAhi.ts @@ -72,17 +72,28 @@ const importAhiImageSet = async (uri: string) => { const setResponse = await fetch(imageSetMetaUri); const imageSetMeta = await setResponse.json(); console.log(imageSetMeta); + const patentTags = imageSetMeta.Patient.DICOM; + const studyTags = imageSetMeta.Study.DICOM; const [id, firstSeries] = Object.entries(imageSetMeta.Study.Series)[0] as any; + const seriesTags = firstSeries.DICOM; const frames = Object.values(firstSeries.Instances).flatMap((instance: any) => - instance.ImageFrames.map((frame: any) => ({ ...instance, ...frame })) + instance.ImageFrames.map((frame: any) => ({ + ...patentTags, + ...studyTags, + ...seriesTags, + ...instance.DICOM, + ...frame, + })) ); - const chunks = frames.map((frame: any) => makeAhiChunk(uri, frame)); + const chunks = frames.map((frame: any) => + makeAhiChunk(imageSetMetaUri, frame) + ); const chunkStore = useChunkStore(); const image = new AhiChunkImage(firstSeries); chunkStore.chunkImageById[id] = image; - image.addChunks(chunks); + await image.addChunks(chunks); image.startLoad(); return id; diff --git a/src/utils/allocateImageFromChunks.ts b/src/utils/allocateImageFromChunks.ts index c7e31cd0..5f874df0 100644 --- a/src/utils/allocateImageFromChunks.ts +++ b/src/utils/allocateImageFromChunks.ts @@ -19,9 +19,41 @@ const RescaleIntercept = NAME_TO_TAG.get('RescaleIntercept')!; const RescaleSlope = NAME_TO_TAG.get('RescaleSlope')!; const NumberOfFrames = NAME_TO_TAG.get('NumberOfFrames')!; -function toVec(s: Maybe): number[] | null { +const nameToMetaKey = { + SOPInstanceUID: 'SOPInstanceUID', + ImagePositionPatient: 'ImagePositionPatient', + ImageOrientationPatient: 'ImageOrientationPatient', + PixelSpacing: 'PixelSpacing', + Rows: 'Rows', + Columns: 'Columns', + BitsStored: 'BitsStored', + PixelRepresentation: 'PixelRepresentation', + SamplesPerPixel: 'SamplesPerPixel', + RescaleIntercept: 'RescaleIntercept', + RescaleSlope: 'RescaleSlope', + NumberOfFrames: 'NumberOfFrames', + PatientID: 'PatientID', + PatientName: 'PatientName', + PatientBirthDate: 'PatientBirthDate', + PatientSex: 'PatientSex', + StudyID: 'StudyID', + StudyInstanceUID: 'StudyInstanceUID', + StudyDate: 'StudyDate', + StudyTime: 'StudyTime', + AccessionNumber: 'AccessionNumber', + StudyDescription: 'StudyDescription', + Modality: 'Modality', + SeriesInstanceUID: 'SeriesInstanceUID', + SeriesNumber: 'SeriesNumber', + SeriesDescription: 'SeriesDescription', + WindowLevel: 'WindowLevel', + WindowWidth: 'WindowWidth', +}; + +function toVec(s: Maybe): number[] | null { if (!s?.length) return null; - return s.split('\\').map((a) => Number(a)) as number[]; + const array = Array.isArray(s) ? s : s.split('\\'); + return array.map((a) => Number(a)) as number[]; } function getBitStorageSize(num: number, signed: boolean) { @@ -69,25 +101,30 @@ function getTypedArrayConstructor( return getTypedArrayForDataRange(outputMin, outputMax); } -export function allocateImageFromChunks(sortedChunks: Chunk[]) { +export function allocateImageFromChunks( + nameToMeta: typeof nameToMetaKey, + sortedChunks: Chunk[] +) { if (sortedChunks.length === 0) { throw new Error('Cannot allocate an image from zero chunks'); } // use the first chunk as the source of metadata const meta = new Map(sortedChunks[0].metadata!); - const imagePositionPatient = toVec(meta.get(ImagePositionPatientTag)); - const imageOrientationPatient = toVec(meta.get(ImageOrientationPatientTag)); - const pixelSpacing = toVec(meta.get(PixelSpacingTag)); - const rows = Number(meta.get(RowsTag) ?? 0); - const columns = Number(meta.get(ColumnsTag) ?? 0); - const bitsStored = Number(meta.get(BitsStoredTag) ?? 0); - const pixelRepresentation = Number(meta.get(PixelRepresentationTag)); - const samplesPerPixel = Number(meta.get(SamplesPerPixelTag) ?? 1); - const rescaleIntercept = Number(meta.get(RescaleIntercept) ?? 0); - const rescaleSlope = Number(meta.get(RescaleSlope) ?? 1); - const numberOfFrames = meta.has(NumberOfFrames) - ? Number(meta.get(NumberOfFrames)) + const imagePositionPatient = toVec(meta.get(nameToMeta.ImagePositionPatient)); + const imageOrientationPatient = toVec( + meta.get(nameToMeta.ImageOrientationPatient) + ); + const pixelSpacing = toVec(meta.get(nameToMeta.PixelSpacing)); + const rows = Number(meta.get(nameToMeta.Rows) ?? 0); + const columns = Number(meta.get(nameToMeta.Columns) ?? 0); + const bitsStored = Number(meta.get(nameToMeta.BitsStored) ?? 0); + const pixelRepresentation = Number(meta.get(nameToMeta.PixelRepresentation)); + const samplesPerPixel = Number(meta.get(nameToMeta.SamplesPerPixel) ?? 1); + const rescaleIntercept = Number(meta.get(nameToMeta.RescaleIntercept) ?? 0); + const rescaleSlope = Number(meta.get(nameToMeta.RescaleSlope) ?? 1); + const numberOfFrames = meta.has(nameToMeta.NumberOfFrames) + ? Number(meta.get(nameToMeta.NumberOfFrames)) : null; // If we have NumberOfFrames, chances are it's a multi-frame DICOM.