Skip to content

Commit

Permalink
feat(SegmentGroupControls): add save button
Browse files Browse the repository at this point in the history
  • Loading branch information
PaulHax committed Dec 15, 2023
1 parent 55cb8a3 commit c1f847c
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 41 deletions.
115 changes: 115 additions & 0 deletions src/components/SaveSegmentGroupDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<template>
<v-card>
<v-card-title class="d-flex flex-row align-center">
Save Segment Group
</v-card-title>
<v-card-text>
<v-form v-model="valid" @submit.prevent="saveSegmentGroup">
<v-text-field
v-model="fileName"
hint="Filename that will appear in downloads."
label="Filename"
:rules="[validFileName]"
required
id="filename"
/>

<v-radio-group v-model="fileFormat" label="Format">
<v-radio v-for="ext in EXTENSIONS" :key="ext" :value="ext">
<template #label>
<div class="d-flex flex-row align-center w-100" :title="ext">
<span class="group-name">{{ ext }}</span>
</div>
</template>
</v-radio>
</v-radio-group>
</v-form>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn
:loading="saving"
color="secondary"
@click="saveSegmentGroup"
:disabled="!valid"
>
<v-icon class="mr-2">mdi-content-save</v-icon>
<span data-testid="save-confirm-button">Save</span>
</v-btn>
</v-card-actions>
</v-card>
</template>

<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { saveAs } from 'file-saver';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import { writeImage } from '@/src/io/readWriteImage';
const EXTENSIONS = [
'dcm',
'nrrd',
'hdf5',
'nii',
'nii.gz',
'tif',
'mha',
'vtk',
'iwi.cbor',
];
export default defineComponent({
props: {
close: {
type: Function,
required: true,
},
id: {
type: String,
required: true,
},
},
setup(props) {
const fileName = ref('');
const valid = ref(true);
const saving = ref(false);
const fileFormat = ref('dcm');
const segmentGroupStore = useSegmentGroupStore();
async function saveSegmentGroup() {
if (fileName.value.trim().length === 0) {
return;
}
saving.value = true;
try {
const image = segmentGroupStore.dataIndex[props.id];
const serialized = await writeImage(fileFormat.value, image);
saveAs(new Blob([serialized]), `${fileName.value}.${fileFormat.value}`);
props.close();
} finally {
saving.value = false;
}
}
onMounted(() => {
// trigger form validation check so can immediately save with default value
fileName.value = segmentGroupStore.metadataByID[props.id].name;
});
function validFileName(name: string) {
return name.trim().length > 0 || 'Required';
}
return {
saving,
saveSegmentGroup,
fileName,
validFileName,
valid,
fileFormat,
EXTENSIONS,
};
},
});
</script>
1 change: 1 addition & 0 deletions src/components/SaveSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default defineComponent({
}
onMounted(() => {
// triggers form validation check so can immediately save with default value
fileName.value = DEFAULT_FILENAME;
});
Expand Down
25 changes: 23 additions & 2 deletions src/components/SegmentGroupControls.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<script setup lang="ts">
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 { useSegmentGroupStore } from '@/src/store/segmentGroups';
import { usePaintToolStore } from '@/src/store/tools/paint';
Expand Down Expand Up @@ -110,6 +112,13 @@ function createSegmentGroup() {
startEditing(id);
}
const saveId = ref('');
const saveDialog = ref(false);
function openSaveDialog(id: string) {
saveId.value = id;
saveDialog.value = true;
}
</script>

<template>
Expand Down Expand Up @@ -143,15 +152,21 @@ function createSegmentGroup() {
<div class="d-flex flex-row align-center w-100" :title="group.name">
<span class="group-name">{{ group.name }}</span>
<v-spacer />
<v-btn
icon="mdi-content-save"
size="small"
variant="flat"
@click.stop="openSaveDialog(group.id)"
></v-btn>
<v-btn
icon="mdi-pencil"
size="x-small"
size="small"
variant="flat"
@click.stop="startEditing(group.id)"
></v-btn>
<v-btn
icon="mdi-delete"
size="x-small"
size="small"
variant="flat"
@click.stop="deleteGroup(group.id)"
></v-btn>
Expand Down Expand Up @@ -186,6 +201,12 @@ function createSegmentGroup() {
</v-card-actions>
</v-card>
</v-dialog>

<closeable-dialog v-model="saveDialog" max-width="30%">
<template v-slot="{ close }">
<save-segment-group-dialog :close="close" :id="saveId" />
</template>
</closeable-dialog>
</template>

<style>
Expand Down
39 changes: 39 additions & 0 deletions src/io/readWriteImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import { copyImage } from 'itk-wasm';
import {
readImage as readImageItk,
writeImage as writeImageItk,
} from '@itk-wasm/image-io';
import { vtiReader, vtiWriter } from '@/src/io/vtk/async';

export const readImage = async (file: File) => {
if (file.name.endsWith('.vti'))
return (await vtiReader(file)) as vtkImageData;

const { image } = await readImageItk(null, file);
return vtkITKHelper.convertItkToVtkImage(image);
};

export const writeImage = async (format: string, image: vtkImageData) => {
if (format === 'vti') {
return vtiWriter(image);
}
// copyImage so writeImage does not detach live data when passing to worker
const itkImage = copyImage(vtkITKHelper.convertVtkToItkImage(image));

// Transpose the direction matrix to fix bug in @itk-wasm/image-io.writeImage
// Remove when @itk-wasm/image-io version is above 0.5.0 https://github.com/InsightSoftwareConsortium/itk-wasm/commit/ad9ca85eedc47c9d3444cf36859569c529886bde
const oldDirection = [...itkImage.direction];
const { dimension } = itkImage.imageType;
for (let idx = 0; idx < dimension; ++idx) {
for (let idy = 0; idy < dimension; ++idy) {
itkImage.direction[idx + idy * dimension] =
oldDirection[idy + idx * dimension];
}
}

const result = await writeImageItk(null, itkImage, `image.${format}`);
result.webWorker?.terminate();
return result.serializedImage.data;
};
41 changes: 2 additions & 39 deletions src/store/segmentGroups.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import { computed, reactive, ref, toRaw, watch } from 'vue';
import vtkDataArray from '@kitware/vtk.js/Common/Core/DataArray';
import vtkImageData from '@kitware/vtk.js/Common/DataModel/ImageData';
import vtkITKHelper from '@kitware/vtk.js/Common/DataModel/ITKHelper';
import { copyImage } from 'itk-wasm';
import {
readImage as readImageItk,
writeImage as writeImageItk,
} from '@itk-wasm/image-io';
import { RGBAColor } from '@kitware/vtk.js/types';
import { defineStore } from 'pinia';
import { useImageStore } from '@/src/store/datasets-images';
import { join, normalize } from '@/src/utils/path';
Expand All @@ -16,14 +11,13 @@ import { normalizeForStore, removeFromArray } from '@/src/utils';
import { compareImageSpaces } from '@/src/utils/imageSpace';
import { SegmentMask } from '@/src/types/segment';
import { DEFAULT_SEGMENT_MASKS } from '@/src/config';
import { RGBAColor } from '@kitware/vtk.js/types';
import { readImage, writeImage } from '@/src/io/readWriteImage';
import vtkLabelMap from '../vtk/LabelMap';
import {
StateFile,
Manifest,
SegmentGroupMetadata,
} from '../io/state-file/schema';
import { vtiReader, vtiWriter } from '../io/vtk/async';
import { FileEntry } from '../io/types';
import {
DataSelection,
Expand All @@ -33,37 +27,6 @@ import {
selectionEquals,
} from './datasets';

const readImage = async (file: File) => {
if (file.name.endsWith('.vti'))
return (await vtiReader(file)) as vtkImageData;

const { image } = await readImageItk(null, file);
return vtkITKHelper.convertItkToVtkImage(image);
};

const writeImage = async (format: string, image: vtkImageData) => {
if (format === 'vti') {
return vtiWriter(image);
}
// copyImage so writeImage does not detach live data when passing to worker
const itkImage = copyImage(vtkITKHelper.convertVtkToItkImage(image));

// Transpose the direction matrix to fix bug in @itk-wasm/image-io.writeImage
// Remove when @itk-wasm/image-io version is above 0.5.0 https://github.com/InsightSoftwareConsortium/itk-wasm/commit/ad9ca85eedc47c9d3444cf36859569c529886bde
const oldDirection = [...itkImage.direction];
const { dimension } = itkImage.imageType;
for (let idx = 0; idx < dimension; ++idx) {
for (let idy = 0; idy < dimension; ++idy) {
itkImage.direction[idx + idy * dimension] =
oldDirection[idy + idx * dimension];
}
}

const result = await writeImageItk(null, itkImage, `image.${format}`);
result.webWorker?.terminate();
return result.serializedImage.data;
};

const LabelmapArrayType = Uint8Array;
export type LabelmapArrayType = Uint8Array;

Expand Down

0 comments on commit c1f847c

Please sign in to comment.