Skip to content

Commit

Permalink
Using dicom-web infra for AHI
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Aug 19, 2024
1 parent b04e762 commit 5f17586
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 69 deletions.
126 changes: 126 additions & 0 deletions src/core/ahi-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { NameToMeta } from './dicomTags';
import { dicomSliceToImageUri, nameToMetaKey } from './streaming/ahiChunkImage';

export interface FetchImageSetOptions {
imageSet: string;
}

export interface FetchSeriesOptions extends FetchImageSetOptions {
seriesInstanceUID: string;
}

export interface FetchInstanceOptions extends FetchSeriesOptions {
sopInstanceUID: string;
}

export type Instance = NameToMeta & { imageSet: string };

function parseInstance(instance: any) {
return Object.fromEntries(
Object.entries(nameToMetaKey).map(([key, value]) => {
return [key, instance[value]];
})
);
}

export async function searchForStudies(dicomWebRoot: string) {
const setResponse = await fetch(`${dicomWebRoot}/list-image-sets`);
const imageSetMeta = await setResponse.json();
return imageSetMeta.map((set: any) => ({
...parseInstance(set),
imageSet: set.imageSetId,
}));
}

export async function retrieveStudyMetadata(
dicomWebRoot: string,
options: FetchImageSetOptions
) {
const url = `${dicomWebRoot}/image-set/${options.imageSet}`;
const setResponse = await fetch(url);
const imageSetMeta = await setResponse.json();
const patentTags = imageSetMeta.Patient.DICOM;
const studyTags = imageSetMeta.Study.DICOM;
const series = (
Object.values(imageSetMeta.Study.Series) as {
DICOM: Record<string, string>;
Instances: Record<string, any>;
}[]
).map((s) => s.DICOM);
const instances = series.map((s) => ({ ...patentTags, ...studyTags, ...s }));
return instances.map(parseInstance);
}

export async function retrieveSeriesMetadata(
dicomWebRoot: string,
options: FetchSeriesOptions
) {
const url = `${dicomWebRoot}/image-set/${options.imageSet}`;
const setResponse = await fetch(url);
const imageSetMeta = await setResponse.json();
const patentTags = imageSetMeta.Patient.DICOM;
const studyTags = imageSetMeta.Study.DICOM;
const series = Object.values(imageSetMeta.Study.Series) as {
DICOM: Record<string, string>;
Instances: Record<string, any>;
}[];
const instances = series.flatMap((s) => {
return Object.values(s.Instances).map((i) => ({
...patentTags,
...studyTags,
...s.DICOM,
...i.DICOM,
}));
});
return instances.map(parseInstance);
}

export async function fetchInstanceThumbnail(
dicomWebRoot: string,
apiParams: FetchInstanceOptions
) {
const url = `${dicomWebRoot}/image-set/${apiParams.imageSet}`;
const setResponse = await fetch(url);
const imageSetMeta = await setResponse.json();
const series = Object.values(imageSetMeta.Study.Series) as {
DICOM: Record<string, string>;
Instances: Record<string, any>;
}[];
const theSeries = series.find(
(s) => s.DICOM.SeriesInstanceUID === apiParams.seriesInstanceUID
);
if (!theSeries) {
throw new Error('Series not found');
}
const instanceRemote = theSeries.Instances[apiParams.sopInstanceUID];
const id = instanceRemote.ImageFrames[0].ID;

const request = await fetch(`${url}/${id}/pixel-data`);
const blob = await request.blob();
return dicomSliceToImageUri(blob);
}

const LEVELS = ['image-set'] as const;

// takes a url like http://localhost:3000/dicom-web/studies/someid/series/anotherid
// returns { host: 'http://localhost:3000/dicom-web', studies: 'someid', series: 'anotherid' }
export function parseUrl(deepDicomWebUrl: string) {
// remove trailing slash
const sansSlash = deepDicomWebUrl.replace(/\/$/, '');

let paths = sansSlash.split('/');
const parentIDs = LEVELS.reduce((idObj, dicomLevel) => {
const [urlLevel, dicomID] = paths.slice(-2);
if (urlLevel === dicomLevel) {
paths = paths.slice(0, -2);
return { [dicomLevel]: dicomID, ...idObj };
}
return idObj;
}, {});

const pathsToSlice = Object.keys(parentIDs).length * 2;
const allPaths = sansSlash.split('/');
const host = allPaths.slice(0, allPaths.length - pathsToSlice).join('/');

return { host, ...parentIDs };
}
1 change: 1 addition & 0 deletions src/core/dicomTags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const Tags = {
RescaleIntercept: '0028|1052',
RescaleSlope: '0028|1053',
NumberOfFrames: '0028|0008',
InstanceNumber: '0020|0013',
} as const;

export type NameToMeta = {
Expand Down
26 changes: 16 additions & 10 deletions src/core/streaming/ahiChunkImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const nameToMetaKey = {
SOPInstanceUID: 'SOPInstanceUID',
ImagePositionPatient: 'ImagePositionPatient',
ImageOrientationPatient: 'ImageOrientationPatient',
InstanceNumber: 'InstanceNumber',
PixelSpacing: 'PixelSpacing',
Rows: 'Rows',
Columns: 'Columns',
Expand All @@ -40,11 +41,11 @@ export const nameToMetaKey = {
RescaleIntercept: 'RescaleIntercept',
RescaleSlope: 'RescaleSlope',
NumberOfFrames: 'NumberOfFrames',
PatientID: 'PatientID',
PatientID: 'PatientId',
PatientName: 'PatientName',
PatientBirthDate: 'PatientBirthDate',
PatientSex: 'PatientSex',
StudyID: 'StudyID',
StudyID: 'StudyInstanceUID',
StudyInstanceUID: 'StudyInstanceUID',
StudyDate: 'StudyDate',
StudyTime: 'StudyTime',
Expand Down Expand Up @@ -92,7 +93,7 @@ function itkImageToURI(itkImage: Image) {
return '';
}

async function dicomSliceToImageUri(blob: Blob) {
export async function dicomSliceToImageUri(blob: Blob) {
const array = await blob.arrayBuffer();
const uint8Array = new Uint8Array(array);
const result = await decode(uint8Array);
Expand Down Expand Up @@ -263,16 +264,21 @@ export default class AhiChunkImage implements ChunkImage {
const chunk = this.chunks[chunkIndex];
if (!chunk.dataBlob) throw new Error('Chunk does not have data');

// 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');

const meta = new Map(chunk.metadata);
const rescaleInterceptMeta = meta.get(nameToMetaKey.RescaleIntercept);
const rescaleIntercept = rescaleInterceptMeta
? Number(rescaleInterceptMeta)
: 0;
const pixels = result.image.data as unknown as number[];
for (let i = 0; i < pixels.length; i++) {
pixels[i] += rescaleIntercept;
}

const scalars = this.imageData.getPointData().getScalars();
const pixelData = scalars.getData() as TypedArray;

Expand Down Expand Up @@ -352,9 +358,9 @@ export default class AhiChunkImage implements ChunkImage {
SeriesNumber: metadata[nameToMetaKey.SeriesNumber],
SeriesDescription: metadata[nameToMetaKey.SeriesDescription],
// @ts-expect-error
WindowLevel: metadata[nameToMetaKey.WindowLevel].join('\\'),
WindowLevel: metadata[nameToMetaKey.WindowLevel]?.join('\\'),
// @ts-expect-error
WindowWidth: metadata[nameToMetaKey.WindowWidth].join('\\'),
WindowWidth: metadata[nameToMetaKey.WindowWidth]?.join('\\'),
};

store._updateDatabase(patientInfo, studyInfo, volumeInfo);
Expand Down
17 changes: 9 additions & 8 deletions src/io/import/awsAhi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ class AhiDataLoader implements DataLoader {
}
}

const makeAhiChunk = (uri: string, frame: any) => {
const pixelDataUri = `${uri}/${frame.ID}/pixel-data`;
const makeAhiChunk = (imageSetUrl: string, frame: any) => {
const pixelDataUri = `${imageSetUrl}/${frame.ID}/pixel-data`;

const metaLoader = new AhiMetaLoader(frame);
const fetcher = new CachedStreamFetcher(pixelDataUri, {
Expand All @@ -68,12 +68,15 @@ const makeAhiChunk = (uri: string, frame: any) => {
};

const importAhiImageSet = async (uri: string) => {
const imageSetMetaUri = uri.replace('ahi:', 'http:');
const setResponse = await fetch(imageSetMetaUri);
const withProto = uri.replace('ahi:', 'http:');
const lastSlash = withProto.lastIndexOf('/');
const seriesId = withProto.substring(lastSlash + 1);
const imageSetUrl = withProto.substring(0, lastSlash);
const setResponse = await fetch(imageSetUrl);
const imageSetMeta = await setResponse.json();
const patentTags = imageSetMeta.Patient.DICOM;
const studyTags = imageSetMeta.Study.DICOM;
const firstSeries = Object.entries(imageSetMeta.Study.Series)[0][1] as {
const firstSeries = imageSetMeta.Study.Series[seriesId] as {
DICOM: Record<string, string>;
Instances: Record<string, any>;
};
Expand All @@ -88,9 +91,7 @@ const importAhiImageSet = async (uri: string) => {
}))
);

const chunks = frames.map((frame: any) =>
makeAhiChunk(imageSetMetaUri, frame)
);
const chunks = frames.map((frame: any) => makeAhiChunk(imageSetUrl, frame));

const chunkStore = useChunkStore();
const image = new AhiChunkImage(seriesTags);
Expand Down
19 changes: 11 additions & 8 deletions src/store/dicom-web/dicom-meta-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ import {
ANONYMOUS_PATIENT,
ANONYMOUS_PATIENT_ID,
PatientInfo,
StudyInfo,
StudyInfo as StudyInfoDicom,
VolumeInfo,
} from '../datasets-dicom';
import { pick, removeFromArray } from '../../utils';
import { Instance } from '../../core/dicom-web-api';
import { Instance } from '../../core/ahi-api';

type StudyInfo = StudyInfoDicom & { imageSet: string };

interface InstanceInfo {
SopInstanceUID: string;
SOPInstanceUID: string;
InstanceNumber: string;
Rows: number;
Columns: number;
Expand Down Expand Up @@ -79,7 +81,8 @@ export const useDicomMetaStore = defineStore('dicom-meta', {
'StudyDate',
'StudyTime',
'AccessionNumber',
'StudyDescription'
'StudyDescription',
'imageSet'
);

const volumeInfo = {
Expand All @@ -97,9 +100,9 @@ export const useDicomMetaStore = defineStore('dicom-meta', {
};

const instanceInfo = {
...pick(info, 'SopInstanceUID', 'InstanceNumber'),
Rows: Number.parseInt(info.Rows, 10),
Columns: Number.parseInt(info.Columns, 10),
...pick(info, 'SOPInstanceUID', 'InstanceNumber'),
Rows: Number.parseInt(info.Rows ?? '0', 10),
Columns: Number.parseInt(info.Columns ?? '0', 10),
};

this._updateDatabase(patient, study, volumeInfo, instanceInfo);
Expand Down Expand Up @@ -172,7 +175,7 @@ export const useDicomMetaStore = defineStore('dicom-meta', {
this.studyVolumes[studyKey].push(volumeKey);
}

const instanceKey = instance.SopInstanceUID;
const instanceKey = instance.SOPInstanceUID;
if (instanceKey && !(instanceKey in this.instanceInfo)) {
this.instanceInfo[instanceKey] = instance;
this.instanceVolume[instanceKey] = volumeKey;
Expand Down
Loading

0 comments on commit 5f17586

Please sign in to comment.