diff --git a/documentation/content/doc/configuration_file.md b/documentation/content/doc/configuration_file.md index 850d8390..73debcc2 100644 --- a/documentation/content/doc/configuration_file.md +++ b/documentation/content/doc/configuration_file.md @@ -56,15 +56,17 @@ hdf5, iwi.cbor, mha, nii, nii.gz, nrrd, vtk ## Automatic Segment Groups by File Name When loading files, VolView can automatically convert images to segment groups -if they follow this naming convention: An image with name like `foo.segmentation.bar` +if they follow a naming convention. For example, an image with name like `foo.segmentation.bar` will be converted to a segment group for a base image named like `foo.baz`. -The key is the `segmentation` extension to identify the segment group image. -Also the `matchNames` key must be set to `true`. The default is false. +The `segmentation` extension is defined by the `io.segmentGroupExtension` key, which takes a +string. Files `foo.[segmentGroupExtension].bar` will be automatilly converted to segment groups for a base image named `foo.baz`. The default is `''` and will disable the feature. + +This will define `myFile.seg.nrrd` as a segment group for a `myFile.nii` base file. ```json { "io": { - "matchNames": true + "segmentGroupExtension": "seg" } } ``` diff --git a/src/actions/loadUserFiles.ts b/src/actions/loadUserFiles.ts index dddebb15..351b1447 100644 --- a/src/actions/loadUserFiles.ts +++ b/src/actions/loadUserFiles.ts @@ -36,8 +36,6 @@ const BASE_MODALITY_TYPES = { DX: { priority: 1 }, } as const; -const SEGMENTATION_EXTENSION = 'segmentation'; - function findBaseDicom(loadableDataSources: Array) { // find dicom dataset for primary selection if available const dicoms = loadableDataSources.filter( @@ -76,18 +74,22 @@ function findBaseDicom(loadableDataSources: Array) { } function isSegmentation(extension: string, name: string) { + if (!extension) return false; // avoid 'foo..bar' if extension is '' const extensions = name.split('.').slice(1); return extensions.includes(extension); } // does not pick segmentation images -function findBaseImage(loadableDataSources: Array) { +function findBaseImage( + loadableDataSources: Array, + segmentGroupExtension: string +) { const baseImages = loadableDataSources .filter(({ dataType }) => dataType === 'image') .filter((importResult) => { const name = getDataSourceName(importResult.dataSource); if (!name) return false; - return !isSegmentation(SEGMENTATION_EXTENSION, name); + return !isSegmentation(segmentGroupExtension, name); }); if (baseImages.length) return baseImages[0]; @@ -135,12 +137,14 @@ function getStudyUID(volumeID: string) { } function findBaseDataSource( - succeeded: Array> + succeeded: Array>, + segmentGroupExtension: string ) { const loadableDataSources = filterLoadableDataSources(succeeded); const baseDicom = findBaseDicom(loadableDataSources); if (baseDicom) return baseDicom; - const baseImage = findBaseImage(loadableDataSources); + + const baseImage = findBaseImage(loadableDataSources, segmentGroupExtension); if (baseImage) return baseImage; return loadableDataSources[0]; } @@ -186,23 +190,21 @@ function loadLayers( layersStore.addLayer(primarySelection, layerSelection); } -// Loads other DataSources Segment Groups: +// Loads other DataSources as Segment Groups: // - DICOM SEG modalities with matching StudyUIDs. // - DataSources that have a name like foo.segmentation.bar and the primary DataSource is named foo.baz function loadSegmentations( primaryDataSource: VolumeResult, succeeded: Array>, - matchNames: boolean + segmentGroupExtension: string ) { - const matchingNames = matchNames - ? filterMatchingNames( - primaryDataSource, - succeeded, - SEGMENTATION_EXTENSION - ).filter( - isVolumeResult // filter out models - ) - : []; + const matchingNames = filterMatchingNames( + primaryDataSource, + succeeded, + segmentGroupExtension + ).filter( + isVolumeResult // filter out models + ); const dicomStore = useDICOMStore(); const otherSegVolumesInStudy = filterOtherVolumesInStudy( @@ -240,7 +242,11 @@ function loadDataSources(sources: DataSource[]) { const [succeeded, errored] = partitionResults(results); if (!dataStore.primarySelection && succeeded.length) { - const primaryDataSource = findBaseDataSource(succeeded); + const primaryDataSource = findBaseDataSource( + succeeded, + loadDataStore.segmentGroupExtension + ); + if (isVolumeResult(primaryDataSource)) { const selection = toDataSelection(primaryDataSource); dataStore.setPrimarySelection(selection); @@ -248,7 +254,7 @@ function loadDataSources(sources: DataSource[]) { loadSegmentations( primaryDataSource, succeeded, - loadDataStore.matchNames + loadDataStore.segmentGroupExtension ); } // then must be primaryDataSource.type === 'model' } diff --git a/src/io/import/configJson.ts b/src/io/import/configJson.ts index 293c6b4a..9ac10ee9 100644 --- a/src/io/import/configJson.ts +++ b/src/io/import/configJson.ts @@ -14,6 +14,9 @@ import { useSegmentGroupStore } from '@/src/store/segmentGroups'; import { AnnotationToolStore } from '@/src/store/tools/useAnnotationTool'; import useLoadDataStore from '@/src/store/load-data'; +// -------------------------------------------------------------------------- +// Interface + const layout = z .object({ activeLayout: zodEnumFromObjKeys(Layouts).optional(), @@ -57,10 +60,13 @@ const labels = z }) .optional(); +// -------------------------------------------------------------------------- +// IO + const io = z .object({ segmentGroupSaveFormat: z.string().optional(), - matchNames: z.boolean().optional(), + segmentGroupExtension: z.string().default(''), }) .optional(); @@ -131,7 +137,7 @@ const applyIo = (manifest: Config) => { if (manifest.io.segmentGroupSaveFormat) useSegmentGroupStore().saveFormat = manifest.io.segmentGroupSaveFormat; - useLoadDataStore().matchNames = manifest.io.matchNames ?? false; + useLoadDataStore().segmentGroupExtension = manifest.io.segmentGroupExtension; }; export const applyConfig = (manifest: Config) => { diff --git a/src/store/datasets-layers.ts b/src/store/datasets-layers.ts index e8eeba47..6293cb54 100644 --- a/src/store/datasets-layers.ts +++ b/src/store/datasets-layers.ts @@ -1,9 +1,7 @@ import { ref } from 'vue'; -import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper'; import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData'; import vtkBoundingBox from '@kitware/vtk.js/Common/DataModel/BoundingBox'; import { defineStore } from 'pinia'; -import { compareImageSpaces } from '@/src/utils/imageSpace'; import { DataSelection, getImage, @@ -11,7 +9,7 @@ import { makeImageSelection, selectionEquals, } from '@/src/utils/dataSelection'; -import { resample } from '../io/resample/resample'; +import { ensureSameSpace } from '@/src/io/resample/resample'; import { useErrorMessage } from '../composables/useErrorMessage'; import { Manifest, StateFile } from '../io/state-file/schema'; @@ -89,16 +87,7 @@ export const useLayersStore = defineStore('layer', () => { ); } - let image: vtkImageData; - if (compareImageSpaces(parentImage, sourceImage)) { - image = sourceImage; - } else { - const itkImage = await resample( - vtkITKHelper.convertVtkToItkImage(parentImage), - vtkITKHelper.convertVtkToItkImage(sourceImage) - ); - image = vtkITKHelper.convertItkToVtkImage(itkImage); - } + const image = await ensureSameSpace(parentImage, sourceImage); this.layerImages[id] = image; } diff --git a/src/store/load-data.ts b/src/store/load-data.ts index bf924ed1..946627bb 100644 --- a/src/store/load-data.ts +++ b/src/store/load-data.ts @@ -99,10 +99,10 @@ const useLoadDataStore = defineStore('loadData', () => { const { startLoading, stopLoading, setError, isLoading } = useLoadingNotifications(); - const matchNames = ref(false); + const segmentGroupExtension = ref(''); return { - matchNames, + segmentGroupExtension, isLoading, startLoading, stopLoading,