diff --git a/src/actions/importDicomChunks.ts b/src/actions/importDicomChunks.ts index 102c05c5..7472c4e7 100644 --- a/src/actions/importDicomChunks.ts +++ b/src/actions/importDicomChunks.ts @@ -13,8 +13,8 @@ export async function importDicomChunks(chunks: Chunk[]) { Object.entries(chunksByVolume).map(async ([id, groupedChunks]) => { const image = (chunkStore.chunkImageById[id] as DicomChunkImage) ?? - new DicomChunkImage(); - chunkStore.chunkImageById[id] = image; + new DicomChunkImage(id); + chunkStore.chunkImageById[image.dataId] = image; await image.addChunks(groupedChunks); diff --git a/src/components/PatientStudyVolumeBrowser.vue b/src/components/PatientStudyVolumeBrowser.vue index 027f25f3..5c4715bb 100644 --- a/src/components/PatientStudyVolumeBrowser.vue +++ b/src/components/PatientStudyVolumeBrowser.vue @@ -2,7 +2,7 @@ import { computed, defineComponent, reactive, toRefs, watch } from 'vue'; import type { PropType } from 'vue'; import GroupableItem from '@/src/components/GroupableItem.vue'; -import { DataSelection, isDicomImage } from '@/src/utils/dataSelection'; +import { isDicomImage } from '@/src/utils/dataSelection'; import { ThumbnailStrategy } from '@/src/core/streaming/chunkImage'; import useChunkStore from '@/src/store/chunks'; import { getDisplayName, useDICOMStore } from '../store/datasets-dicom'; @@ -49,7 +49,6 @@ export default defineComponent({ isDicomImage(primarySelection) && primarySelection; return volumeKeys.value.map((volumeKey) => { - const selectionKey = volumeKey as DataSelection; const isLayer = layerVolumeKeys.includes(volumeKey); const layerLoaded = loadedLayerVolumeKeys.includes(volumeKey); const layerLoading = isLayer && !layerLoaded; @@ -61,15 +60,14 @@ export default defineComponent({ info: volumeInfo[volumeKey], name: getDisplayName(volumeInfo[volumeKey]), // for UI selection - selectionKey, + selectionKey: volumeKey, isLayer, layerable, layerLoading, layerHandler: () => { if (!layerLoading && layerable) { - if (isLayer) - layersStore.deleteLayer(primarySelection, selectionKey); - else layersStore.addLayer(primarySelection, selectionKey); + if (isLayer) layersStore.deleteLayer(primarySelection, volumeKey); + else layersStore.addLayer(primarySelection, volumeKey); } }, }; @@ -92,17 +90,18 @@ export default defineComponent({ const chunkStore = useChunkStore(); try { - const chunk = chunkStore.chunkImageById[key]; - const thumb = await chunk.getThumbnail( + const chunkImage = chunkStore.chunkImageById[key]; + const thumb = await chunkImage.getThumbnail( ThumbnailStrategy.MiddleSlice ); thumbnailCache[cacheKey] = thumb; } catch (err) { if (err instanceof Error) { const messageStore = useMessageStore(); - messageStore.addError('Failed to generate thumbnails', { - details: `${err}. More details can be found in the developer's console.`, - }); + messageStore.addError( + 'Failed to generate thumbnails. Details in dev tools console.', + err + ); } } }); diff --git a/src/core/dicomTags.ts b/src/core/dicomTags.ts index 3a1ee6e0..d36cb550 100644 --- a/src/core/dicomTags.ts +++ b/src/core/dicomTags.ts @@ -1,40 +1,35 @@ -interface Tag { - name: string; - tag: string; -} +export const Tags = { + SOPInstanceUID: '0008|0018', + PatientName: '0010|0010', + PatientID: '0010|0020', + PatientBirthDate: '0010|0030', + PatientSex: '0010|0040', + StudyInstanceUID: '0020|000d', + StudyDate: '0008|0020', + StudyTime: '0008|0030', + StudyID: '0020|0010', + AccessionNumber: '0008|0050', + StudyDescription: '0008|1030', + Modality: '0008|0060', + SeriesInstanceUID: '0020|000e', + SeriesNumber: '0020|0011', + SeriesDescription: '0008|103e', + WindowLevel: '0028|1050', + WindowWidth: '0028|1051', + Rows: '0028|0010', + Columns: '0028|0011', + BitsAllocated: '0028|0100', + BitsStored: '0028|0101', + PixelRepresentation: '0028|0103', + ImagePositionPatient: '0020|0032', + ImageOrientationPatient: '0020|0037', + PixelSpacing: '0028|0030', + SamplesPerPixel: '0028|0002', + RescaleIntercept: '0028|1052', + RescaleSlope: '0028|1053', + NumberOfFrames: '0028|0008', +} as const; -const tags: Tag[] = [ - { name: 'SOPInstanceUID', tag: '0008|0018' }, - { name: 'PatientName', tag: '0010|0010' }, - { name: 'PatientID', tag: '0010|0020' }, - { name: 'PatientBirthDate', tag: '0010|0030' }, - { name: 'PatientSex', tag: '0010|0040' }, - { name: 'StudyInstanceUID', tag: '0020|000d' }, - { name: 'StudyDate', tag: '0008|0020' }, - { name: 'StudyTime', tag: '0008|0030' }, - { name: 'StudyID', tag: '0020|0010' }, - { name: 'AccessionNumber', tag: '0008|0050' }, - { name: 'StudyDescription', tag: '0008|1030' }, - { name: 'Modality', tag: '0008|0060' }, - { name: 'SeriesInstanceUID', tag: '0020|000e' }, - { name: 'SeriesNumber', tag: '0020|0011' }, - { name: 'SeriesDescription', tag: '0008|103e' }, - { name: 'WindowLevel', tag: '0028|1050' }, - { name: 'WindowWidth', tag: '0028|1051' }, - { name: 'Rows', tag: '0028|0010' }, - { name: 'Columns', tag: '0028|0011' }, - { name: 'BitsAllocated', tag: '0028|0100' }, - { name: 'BitsStored', tag: '0028|0101' }, - { name: 'PixelRepresentation', tag: '0028|0103' }, - { name: 'ImagePositionPatient', tag: '0020|0032' }, - { name: 'ImageOrientationPatient', tag: '0020|0037' }, - { name: 'PixelSpacing', tag: '0028|0030' }, - { name: 'SamplesPerPixel', tag: '0028|0002' }, - { name: 'RescaleIntercept', tag: '0028|1052' }, - { name: 'RescaleSlope', tag: '0028|1053' }, - { name: 'NumberOfFrames', tag: '0028|0008' }, -]; - -export const TAG_TO_NAME = new Map(tags.map((t) => [t.tag, t.name])); -export const NAME_TO_TAG = new Map(tags.map((t) => [t.name, t.tag])); -export const Tags = Object.fromEntries(tags.map((t) => [t.name, t.tag])); +export type NameToMeta = { + [key in keyof typeof Tags]: string; +}; diff --git a/src/core/streaming/ahiChunkImage.ts b/src/core/streaming/ahiChunkImage.ts index 18728194..4662c425 100644 --- a/src/core/streaming/ahiChunkImage.ts +++ b/src/core/streaming/ahiChunkImage.ts @@ -1,7 +1,5 @@ -import { readVolumeSlice } from '@/src/io/dicom'; import { Chunk, waitForChunkState } from '@/src/core/streaming/chunk'; -import { Image, readImage } from '@itk-wasm/image-io'; -import { getWorker } from '@/src/io/itk/worker'; +import { Image } from '@itk-wasm/image-io'; import { allocateImageFromChunks } from '@/src/utils/allocateImageFromChunks'; import { Maybe } from '@/src/types'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; @@ -13,7 +11,6 @@ import { VolumeInfo, useDICOMStore, } from '@/src/store/datasets-dicom'; -// import { Tags } from '@/src/core/dicomTags'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import { ChunkState } from '@/src/core/streaming/chunkStateMachine'; import { @@ -24,16 +21,12 @@ 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 = { +import { decode } from '@itk-wasm/htj2k'; +import { NameToMeta } from '../dicomTags'; + +const { fastComputeRange } = vtkDataArray; + +export const nameToMetaKey = { SOPInstanceUID: 'SOPInstanceUID', ImagePositionPatient: 'ImagePositionPatient', ImageOrientationPatient: 'ImageOrientationPatient', @@ -41,6 +34,7 @@ const nameToMetaKey = { Rows: 'Rows', Columns: 'Columns', BitsStored: 'BitsStored', + BitsAllocated: 'BitsAllocated', PixelRepresentation: 'PixelRepresentation', SamplesPerPixel: 'SamplesPerPixel', RescaleIntercept: 'RescaleIntercept', @@ -60,11 +54,9 @@ const nameToMetaKey = { SeriesInstanceUID: 'SeriesInstanceUID', SeriesNumber: 'SeriesNumber', SeriesDescription: 'SeriesDescription', - WindowLevel: 'WindowLevel', + WindowLevel: 'WindowCenter', WindowWidth: 'WindowWidth', -}; - -const { fastComputeRange } = vtkDataArray; +} as const satisfies NameToMeta; function getChunkId(chunk: Chunk) { const metadata = Object.fromEntries(chunk.metadata!); @@ -104,7 +96,6 @@ async function dicomSliceToImageUri(blob: Blob) { const array = await blob.arrayBuffer(); const uint8Array = new Uint8Array(array); const result = await decode(uint8Array); - console.log(result); return itkImageToURI(result.image); } @@ -114,16 +105,16 @@ export default class AhiChunkImage implements ChunkImage { private thumbnailCache: WeakMap>; private events: Emitter; public imageData: Maybe; - public dataId: Maybe; + public dataId: string; public chunkStatus: Ref; public isLoading: ComputedRef; public seriesMeta: Record; constructor(seriesMeta: Record) { this.seriesMeta = seriesMeta; + this.dataId = seriesMeta.SeriesInstanceUID; this.chunks = []; this.chunkListeners = []; - this.dataId = null; this.chunkStatus = ref([]); this.isLoading = computed(() => this.chunkStatus.value.some( @@ -154,7 +145,6 @@ export default class AhiChunkImage implements ChunkImage { this.events.all.clear(); this.chunks.length = 0; this.imageData = null; - this.dataId = null; this.chunkStatus.value = []; this.thumbnailCache = new WeakMap(); } @@ -180,18 +170,9 @@ export default class AhiChunkImage implements ChunkImage { this.chunks.push(chunk); }); - const chunksByVolume = { - [this.seriesMeta.ID]: this.chunks, - }; - - const volumes = Object.entries(chunksByVolume); - if (volumes.length !== 1) - throw new Error('Did not get just a single volume!'); - this.unregisterChunkListeners(); - // save the newly sorted chunk order - [this.dataId, this.chunks] = volumes[0]; + // this.chunks = sort(this.chunks); this.chunkStatus.value = this.chunks.map((chunk) => { switch (chunk.state) { @@ -339,7 +320,6 @@ 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]; @@ -366,13 +346,15 @@ export default class AhiChunkImage implements ChunkImage { const volumeInfo: VolumeInfo = { NumberOfSlices: this.chunks.length, - VolumeID: this.dataId ?? '', + VolumeID: this.dataId, Modality: metadata[nameToMetaKey.Modality], SeriesInstanceUID: metadata[nameToMetaKey.SeriesInstanceUID], SeriesNumber: metadata[nameToMetaKey.SeriesNumber], SeriesDescription: metadata[nameToMetaKey.SeriesDescription], - WindowLevel: metadata[nameToMetaKey.WindowLevel], - WindowWidth: metadata[nameToMetaKey.WindowWidth], + // @ts-expect-error + WindowLevel: metadata[nameToMetaKey.WindowLevel].join('\\'), + // @ts-expect-error + WindowWidth: metadata[nameToMetaKey.WindowWidth].join('\\'), }; store._updateDatabase(patientInfo, studyInfo, volumeInfo); diff --git a/src/core/streaming/dicomChunkImage.ts b/src/core/streaming/dicomChunkImage.ts index 271a3fbe..fd4f5d69 100644 --- a/src/core/streaming/dicomChunkImage.ts +++ b/src/core/streaming/dicomChunkImage.ts @@ -73,14 +73,14 @@ export default class DicomChunkImage implements ChunkImage { private thumbnailCache: WeakMap>; private events: Emitter; public imageData: Maybe; - public dataId: Maybe; + public dataId: string; public chunkStatus: Ref; public isLoading: ComputedRef; - constructor() { + constructor(id: string) { this.chunks = []; this.chunkListeners = []; - this.dataId = null; + this.dataId = id; this.chunkStatus = ref([]); this.isLoading = computed(() => this.chunkStatus.value.some( @@ -111,7 +111,6 @@ export default class DicomChunkImage implements ChunkImage { this.events.all.clear(); this.chunks.length = 0; this.imageData = null; - this.dataId = null; this.chunkStatus.value = []; this.thumbnailCache = new WeakMap(); } @@ -230,7 +229,7 @@ export default class DicomChunkImage implements ChunkImage { private reallocateImage() { this.imageData?.delete(); - this.imageData = allocateImageFromChunks(this.chunks); + this.imageData = allocateImageFromChunks(Tags, this.chunks); } private async onChunkHasData(chunkIndex: number) { diff --git a/src/io/import/awsAhi.ts b/src/io/import/awsAhi.ts index f13c289c..be576727 100644 --- a/src/io/import/awsAhi.ts +++ b/src/io/import/awsAhi.ts @@ -71,10 +71,12 @@ const importAhiImageSet = async (uri: string) => { const imageSetMetaUri = uri.replace('ahi:', 'http:'); 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 firstSeries = Object.entries(imageSetMeta.Study.Series)[0][1] as { + DICOM: Record; + Instances: Record; + }; const seriesTags = firstSeries.DICOM; const frames = Object.values(firstSeries.Instances).flatMap((instance: any) => instance.ImageFrames.map((frame: any) => ({ @@ -91,12 +93,12 @@ const importAhiImageSet = async (uri: string) => { ); const chunkStore = useChunkStore(); - const image = new AhiChunkImage(firstSeries); - chunkStore.chunkImageById[id] = image; + const image = new AhiChunkImage(seriesTags); + chunkStore.chunkImageById[image.dataId] = image; await image.addChunks(chunks); image.startLoad(); - return id; + return image.dataId; }; export const isAhiUri = (uri: string) => diff --git a/src/store/messages.ts b/src/store/messages.ts index a868e981..6b2f4b27 100644 --- a/src/store/messages.ts +++ b/src/store/messages.ts @@ -51,6 +51,7 @@ export const useMessageStore = defineStore('message', { */ addError(title: string, opts?: Error | string | MessageOptions) { if (opts instanceof Error) { + console.error(opts); return this._addMessage( { type: MessageType.Error, diff --git a/src/utils/allocateImageFromChunks.ts b/src/utils/allocateImageFromChunks.ts index 5f874df0..b123185e 100644 --- a/src/utils/allocateImageFromChunks.ts +++ b/src/utils/allocateImageFromChunks.ts @@ -1,55 +1,12 @@ import { Chunk } from '@/src/core/streaming/chunk'; +import { NameToMeta } from '@/src/core/dicomTags'; import { Maybe } from '@/src/types'; -import { NAME_TO_TAG } from '@/src/core/dicomTags'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import { Vector3 } from '@kitware/vtk.js/types'; import { mat3, vec3 } from 'gl-matrix'; import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray'; import { vtkWarningMacro } from '@kitware/vtk.js/macros'; -const ImagePositionPatientTag = NAME_TO_TAG.get('ImagePositionPatient')!; -const ImageOrientationPatientTag = NAME_TO_TAG.get('ImageOrientationPatient')!; -const PixelSpacingTag = NAME_TO_TAG.get('PixelSpacing')!; -const RowsTag = NAME_TO_TAG.get('Rows')!; -const ColumnsTag = NAME_TO_TAG.get('Columns')!; -const BitsStoredTag = NAME_TO_TAG.get('BitsStored')!; -const PixelRepresentationTag = NAME_TO_TAG.get('PixelRepresentation')!; -const SamplesPerPixelTag = NAME_TO_TAG.get('SamplesPerPixel')!; -const RescaleIntercept = NAME_TO_TAG.get('RescaleIntercept')!; -const RescaleSlope = NAME_TO_TAG.get('RescaleSlope')!; -const NumberOfFrames = NAME_TO_TAG.get('NumberOfFrames')!; - -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; const array = Array.isArray(s) ? s : s.split('\\'); @@ -102,7 +59,7 @@ function getTypedArrayConstructor( } export function allocateImageFromChunks( - nameToMeta: typeof nameToMetaKey, + nameToMeta: NameToMeta, sortedChunks: Chunk[] ) { if (sortedChunks.length === 0) {