Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AHI POC #642

Closed
wants to merge 6 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 48 additions & 27 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -26,8 +26,9 @@
},
"dependencies": {
"@aws-sdk/client-s3": "^3.435.0",
"@itk-wasm/dicom": "6.0.1",
"@itk-wasm/image-io": "1.1.1",
"@itk-wasm/dicom": "7.1.0",
"@itk-wasm/htj2k": "^2.3.1",
"@itk-wasm/image-io": "1.3.0",
"@kitware/vtk.js": "30.9.0",
"@netlify/edge-functions": "^2.0.0",
"@sentry/vue": "^7.54.0",
@@ -40,7 +41,7 @@
"fast-deep-equal": "^3.1.3",
"file-saver": "^2.0.5",
"gl-matrix": "3.4.3",
"itk-wasm": "1.0.0-b.171",
"itk-wasm": "1.0.0-b.178",
"jszip": "3.10.0",
"mitt": "^3.0.0",
"nanoid": "^4.0.1",
4 changes: 2 additions & 2 deletions src/actions/importDicomChunks.ts
Original file line number Diff line number Diff line change
@@ -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);

21 changes: 10 additions & 11 deletions src/components/PatientStudyVolumeBrowser.vue
Original file line number Diff line number Diff line change
@@ -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
);
}
}
});
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 };
}
Loading
Loading