Skip to content

Commit

Permalink
Merge pull request #576 from PaulHax/auto-seg
Browse files Browse the repository at this point in the history
Automatically convert SEG DICOMs to Segment Groups
  • Loading branch information
PaulHax authored Apr 29, 2024
2 parents fccefa9 + e7fb0d3 commit 35fa47f
Show file tree
Hide file tree
Showing 27 changed files with 498 additions and 331 deletions.
10 changes: 9 additions & 1 deletion src/components/DataBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import PatientList from './dicom-web/PatientList.vue';
import { useDICOMStore } from '../store/datasets-dicom';
import { useImageStore } from '../store/datasets-images';
import { useDataBrowserStore } from '../store/data-browser';
import { useDatasetStore } from '../store/datasets';
import { removeFromArray } from '../utils';
const SAMPLE_DATA_KEY = 'sampleData';
Expand All @@ -27,6 +28,7 @@ export default defineComponent({
const imageStore = useImageStore();
const dicomWeb = useDicomWebStore();
const dataBrowserStore = useDataBrowserStore();
const datasetStore = useDatasetStore();
// TODO show patient ID in parens if there is a name conflict
const patients = computed(() =>
Expand Down Expand Up @@ -65,10 +67,16 @@ export default defineComponent({
const hideSampleData = computed(() => dataBrowserStore.hideSampleData);
const deletePatient = (key: string) => {
dicomStore.patientStudies[key]
.flatMap((study) => dicomStore.studyVolumes[study])
.forEach(datasetStore.remove);
};
return {
panels,
patients,
deletePatient: dicomStore.deletePatient,
deletePatient,
hasAnonymousImages,
dicomWeb,
SAMPLE_DATA_KEY,
Expand Down
15 changes: 7 additions & 8 deletions src/components/ImageDataBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import GroupableItem from '@/src/components/GroupableItem.vue';
import ImageListCard from '@/src/components/ImageListCard.vue';
import { createVTKImageThumbnailer } from '@/src/core/thumbnailers/vtk-image';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import { useImageStore } from '../store/datasets-images';
import { useDICOMStore } from '../store/datasets-dicom';
import {
DataSelection,
ImageSelection,
selectionEquals,
useDatasetStore,
} from '../store/datasets';
} from '@/src/utils/dataSelection';
import { useImageStore } from '../store/datasets-images';
import { useDICOMStore } from '../store/datasets-dicom';
import { useDatasetStore } from '../store/datasets';
import { useMultiSelection } from '../composables/useMultiSelection';
import { useLayersStore } from '../store/datasets-layers';
Expand Down Expand Up @@ -129,9 +130,7 @@ export default defineComponent({
useMultiSelection(nonDICOMImages);
function removeSelection() {
selected.value.forEach((id) => {
imageStore.deleteData(id);
});
selected.value.forEach(dataStore.remove);
selected.value = [];
}
Expand All @@ -145,7 +144,7 @@ export default defineComponent({
}
function removeData(id: string) {
imageStore.deleteData(id);
dataStore.remove(id);
}
return {
Expand Down
2 changes: 1 addition & 1 deletion src/components/LayerProperties.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
<script lang="ts">
import { computed, defineComponent, PropType, toRefs } from 'vue';
import { getImageID } from '@/src/utils/dataSelection';
import { InitViewSpecs } from '../config';
import { useImageStore } from '../store/datasets-images';
import { BlendConfig } from '../types/views';
import { Layer } from '../store/datasets-layers';
import useLayerColoringStore from '../store/view-configs/layers';
import { getImageID } from '../store/datasets';
const VIEWS_2D = Object.entries(InitViewSpecs)
.filter(([, { viewType }]) => viewType === '2D')
Expand Down
18 changes: 5 additions & 13 deletions src/components/PatientBrowser.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
<script lang="ts">
import { computed, defineComponent, ref, toRefs, watch } from 'vue';
import ItemGroup from '@/src/components/ItemGroup.vue';
import { DataSelection, selectionEquals } from '@/src/utils/dataSelection';
import { useDICOMStore } from '../store/datasets-dicom';
import {
DataSelection,
selectionEquals,
useDatasetStore,
} from '../store/datasets';
import { useDatasetStore } from '../store/datasets';
import { useMultiSelection } from '../composables/useMultiSelection';
import PatientStudyVolumeBrowser from './PatientStudyVolumeBrowser.vue';
Expand Down Expand Up @@ -62,15 +59,10 @@ export default defineComponent({
useMultiSelection(studyKeys);
const removeSelectedStudies = () => {
selected.value.forEach(async (study) => {
dicomStore.deleteStudy(study);
});
selected.value
.flatMap((study) => dicomStore.studyVolumes[study])
.forEach(dataStore.remove);
selected.value = [];
// Handle the case where we are deleting the selected study
// if (selected.value.indexOf(selectedStudy.value) !== -1) {
// dataStore.setPrimarySelection(null);
// }
};
return {
Expand Down
19 changes: 8 additions & 11 deletions src/components/PatientStudyVolumeBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,9 @@ import { computed, defineComponent, reactive, toRefs, watch } from 'vue';
import { Image } from 'itk-wasm';
import type { PropType } from 'vue';
import GroupableItem from '@/src/components/GroupableItem.vue';
import { DataSelection, DICOMSelection } from '@/src/utils/dataSelection';
import { getDisplayName, useDICOMStore } from '../store/datasets-dicom';
import {
DataSelection,
DICOMSelection,
useDatasetStore,
} from '../store/datasets';
import { useDatasetStore } from '../store/datasets';
import { useMultiSelection } from '../composables/useMultiSelection';
import { useMessageStore } from '../store/messages';
import { useLayersStore } from '../store/datasets-layers';
Expand Down Expand Up @@ -75,9 +72,10 @@ export default defineComponent({
const datasetStore = useDatasetStore();
const layersStore = useLayersStore();
const primarySelectionRef = computed(() => datasetStore.primarySelection);
const volumes = computed(() => {
const { volumeInfo } = dicomStore;
const { primarySelection } = datasetStore;
const volumeInfo = dicomStore.volumeInfo;
const primarySelection = primarySelectionRef.value;
const layerVolumes = layersStore
.getLayers(primarySelection)
.filter(({ selection }) => selection.type === 'dicom');
Expand Down Expand Up @@ -167,13 +165,12 @@ export default defineComponent({
useMultiSelection(volumeKeys);
const removeData = (key: string) => {
dicomStore.deleteVolume(key);
datasetStore.remove(key);
};
const removeSelectedDICOMVolumes = () => {
selected.value.forEach((volumeKey) => {
removeData(volumeKey);
});
// make copy of selected as removing selected will change the array
[...selected.value].forEach(removeData);
selected.value = [];
};
Expand Down
51 changes: 23 additions & 28 deletions src/components/SegmentGroupControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import SegmentList from '@/src/components/SegmentList.vue';
import CloseableDialog from '@/src/components/CloseableDialog.vue';
import SaveSegmentGroupDialog from '@/src/components/SaveSegmentGroupDialog.vue';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { selectionEquals, useDatasetStore } from '@/src/store/datasets';
import { useDICOMStore } from '@/src/store/datasets-dicom';
import { useImageStore } from '@/src/store/datasets-images';
import { useDatasetStore } from '@/src/store/datasets';
import {
getSelectionName,
selectionEquals,
DataSelection,
} from '@/src/utils/dataSelection';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import { usePaintToolStore } from '@/src/store/tools/paint';
import { Maybe } from '@/src/types';
Expand All @@ -15,8 +18,6 @@ const UNNAMED_GROUP_NAME = 'Unnamed Segment Group';
const segmentGroupStore = useSegmentGroupStore();
const { currentImageID } = useCurrentImage();
const imageStore = useImageStore();
const dicomStore = useDICOMStore();
const dataStore = useDatasetStore();
const currentSegmentGroups = computed(() => {
Expand Down Expand Up @@ -119,31 +120,25 @@ function createSegmentGroup() {
startEditing(id);
}
// Filter and collect all acceptable images, excluding
// the current background image, that can be converted into
// a SegmentGroup (labelmap) for the current background image.
const nonDICOMImages = computed(() => {
// Collect images that can be converted into
// a SegmentGroup for the current background image.
const segmentGroupConvertibles = computed(() => {
const primarySelection = dataStore.primarySelection;
const ids = imageStore.idList.filter(
(id) =>
!(id in dicomStore.imageIDToVolumeKey) &&
primarySelection &&
!selectionEquals({ type: 'image', dataID: id }, primarySelection)
);
return ids.map((id) => ({ id, name: imageStore.metadata[id].name }));
if (!primarySelection) return [];
return dataStore.idsAsSelections
.filter((selection) => !selectionEquals(selection, primarySelection))
.map((selection) => ({
selection,
name: getSelectionName(selection),
}));
});
function createSegmentGroupFromImage(selectedImageID: string) {
if (!selectedImageID) {
throw new Error('Cannot create a labelmap without a base image');
}
function createSegmentGroupFromImage(selection: DataSelection) {
const primarySelection = dataStore.primarySelection;
if (primarySelection) {
segmentGroupStore.convertImageToLabelmap(
{ type: 'image', dataID: selectedImageID },
primarySelection
);
if (!primarySelection) {
throw new Error('No primary selection');
}
segmentGroupStore.convertImageToLabelmap(selection, primarySelection);
}
const saveId = ref('');
Expand Down Expand Up @@ -178,11 +173,11 @@ function openSaveDialog(id: string) {
<v-icon class="mr-1">mdi-chevron-down</v-icon>From Image
</v-btn>
</template>
<v-list v-if="nonDICOMImages.length !== 0">
<v-list v-if="segmentGroupConvertibles.length !== 0">
<v-list-item
v-for="(item, index) in nonDICOMImages"
v-for="(item, index) in segmentGroupConvertibles"
:key="index"
@click="createSegmentGroupFromImage(item.id)"
@click="createSegmentGroupFromImage(item.selection)"
>
{{ item.name }}
<v-tooltip activator="parent" location="end" max-width="200px">
Expand Down
11 changes: 8 additions & 3 deletions src/components/vtk/VtkSegmentationSliceRepresentation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { LPSAxis } from '@/src/types/lps';
import { onVTKEvent } from '@/src/composables/onVTKEvent';
import { SlicingMode } from '@kitware/vtk.js/Rendering/Core/ImageMapper/Constants';
import { VtkViewContext } from '@/src/components/vtk/context';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import {
useSegmentGroupStore,
SegmentGroupMetadata,
} from '@/src/store/segmentGroups';
import { InterpolationType } from '@kitware/vtk.js/Rendering/Core/ImageProperty/Constants';
import vtkColorTransferFunction from '@kitware/vtk.js/Rendering/Core/ColorTransferFunction';
import vtkPiecewiseFunction from '@kitware/vtk.js/Common/DataModel/PiecewiseFunction';
Expand All @@ -29,7 +32,7 @@ const view = inject(VtkViewContext);
if (!view) throw new Error('No VtkView');
const segmentationStore = useSegmentGroupStore();
const metadata = computed(
const metadata = computed<SegmentGroupMetadata | undefined>(
() => segmentationStore.metadataByID[segmentationId.value]
);
const imageData = computed(
Expand Down Expand Up @@ -64,7 +67,7 @@ watchEffect(() => {
});
// set slicing mode
const parentImageId = computed(() => metadata.value.parentImage);
const parentImageId = computed(() => metadata.value?.parentImage);
const { metadata: parentMetadata } = useImage(parentImageId);
watchEffect(() => {
Expand All @@ -89,6 +92,8 @@ const applySegmentColoring = () => {
let maxValue = 0;
if (!metadata.value) return; // segment group just deleted
const { segments } = metadata.value;
segments.order.forEach((segId) => {
const segment = segments.byValue[segId];
Expand Down
7 changes: 2 additions & 5 deletions src/composables/useCurrentImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,8 @@ import {
} from '@/src/store/datasets-images';
import { useLayersStore } from '@/src/store/datasets-layers';
import { createLPSBounds, getAxisBounds } from '@/src/utils/lps';
import {
getDataSelection,
getImageID,
useDatasetStore,
} from '@/src/store/datasets';
import { useDatasetStore } from '@/src/store/datasets';
import { getDataSelection, getImageID } from '@/src/utils/dataSelection';
import { storeToRefs } from 'pinia';

export interface CurrentImageContext {
Expand Down
30 changes: 0 additions & 30 deletions src/io/dicom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,36 +219,6 @@ export class DICOMIO {
return result.outputs[0].data as Image;
}

async resample(fixed: SpatialParameters, moving: Image) {
await this.initialize();

const { size, spacing, origin, direction } = fixed;
const args = [
'--action',
'resample',
'0', // space for input image

'--size',
size.join(','),
'--spacing',
spacing.join(','),
'--origin',
origin.join(','),
'--direction',
direction.join(','),

'--memory-io',
'0',
];

const inputs = [{ type: InterfaceTypes.Image, data: moving }];
const outputs = [{ type: InterfaceTypes.Image }];

const result = await this.runTask('dicom', args, inputs, outputs);
const image = result.outputs[0].data as Image;
return image;
}

/**
* Builds a volume for a set of files.
* @async
Expand Down
13 changes: 13 additions & 0 deletions src/io/import/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface LoadableResult extends DataResult {
dataType: 'image' | 'dicom' | 'model';
}

export interface VolumeResult extends LoadableResult {
dataType: 'image' | 'dicom';
}

export interface ConfigResult extends DataResult {
config: Config;
}
Expand Down Expand Up @@ -46,6 +50,15 @@ export function isLoadableResult(
return 'dataID' in importResult && 'dataType' in importResult;
}

export function isVolumeResult(
importResult: ImportResult
): importResult is VolumeResult {
return (
isLoadableResult(importResult) &&
(importResult.dataType === 'image' || importResult.dataType === 'dicom')
);
}

export function isConfigResult(
importResult: ImportResult
): importResult is ConfigResult {
Expand Down
Loading

0 comments on commit 35fa47f

Please sign in to comment.