Skip to content

Commit

Permalink
Interpret ID and window/level metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Aug 18, 2024
1 parent 0c42192 commit b04e762
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 143 deletions.
4 changes: 2 additions & 2 deletions src/actions/importDicomChunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
21 changes: 10 additions & 11 deletions src/components/PatientStudyVolumeBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}
},
};
Expand All @@ -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
);
}
}
});
Expand Down
73 changes: 34 additions & 39 deletions src/core/dicomTags.ts
Original file line number Diff line number Diff line change
@@ -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;
};
54 changes: 18 additions & 36 deletions src/core/streaming/ahiChunkImage.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand All @@ -24,23 +21,20 @@ 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',
PixelSpacing: 'PixelSpacing',
Rows: 'Rows',
Columns: 'Columns',
BitsStored: 'BitsStored',
BitsAllocated: 'BitsAllocated',
PixelRepresentation: 'PixelRepresentation',
SamplesPerPixel: 'SamplesPerPixel',
RescaleIntercept: 'RescaleIntercept',
Expand All @@ -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!);
Expand Down Expand Up @@ -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);
}

Expand All @@ -114,16 +105,16 @@ export default class AhiChunkImage implements ChunkImage {
private thumbnailCache: WeakMap<Chunk, Promise<unknown>>;
private events: Emitter<ChunkImageEvents>;
public imageData: Maybe<vtkImageData>;
public dataId: Maybe<string>;
public dataId: string;
public chunkStatus: Ref<ChunkStatus[]>;
public isLoading: ComputedRef<boolean>;
public seriesMeta: Record<string, string>;

constructor(seriesMeta: Record<string, string>) {
this.seriesMeta = seriesMeta;
this.dataId = seriesMeta.SeriesInstanceUID;
this.chunks = [];
this.chunkListeners = [];
this.dataId = null;
this.chunkStatus = ref([]);
this.isLoading = computed(() =>
this.chunkStatus.value.some(
Expand Down Expand Up @@ -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();
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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];
Expand All @@ -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);
Expand Down
9 changes: 4 additions & 5 deletions src/core/streaming/dicomChunkImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,14 @@ export default class DicomChunkImage implements ChunkImage {
private thumbnailCache: WeakMap<Chunk, Promise<unknown>>;
private events: Emitter<ChunkImageEvents>;
public imageData: Maybe<vtkImageData>;
public dataId: Maybe<string>;
public dataId: string;
public chunkStatus: Ref<ChunkStatus[]>;
public isLoading: ComputedRef<boolean>;

constructor() {
constructor(id: string) {
this.chunks = [];
this.chunkListeners = [];
this.dataId = null;
this.dataId = id;
this.chunkStatus = ref([]);
this.isLoading = computed(() =>
this.chunkStatus.value.some(
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand Down
12 changes: 7 additions & 5 deletions src/io/import/awsAhi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>;
Instances: Record<string, any>;
};
const seriesTags = firstSeries.DICOM;
const frames = Object.values(firstSeries.Instances).flatMap((instance: any) =>
instance.ImageFrames.map((frame: any) => ({
Expand All @@ -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) =>
Expand Down
1 change: 1 addition & 0 deletions src/store/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit b04e762

Please sign in to comment.