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
Show file tree
Hide file tree
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
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
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
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