Skip to content

Commit

Permalink
Merge pull request #539 from PaulHax/export-gui
Browse files Browse the repository at this point in the history
Export segment group GUI
  • Loading branch information
floryst authored Mar 1, 2024
2 parents 135cbe6 + 6c0f098 commit 7420340
Show file tree
Hide file tree
Showing 9 changed files with 2,077 additions and 3,081 deletions.
4,915 changes: 1,888 additions & 3,027 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,18 @@
"@vue/eslint-config-airbnb": "^6.0.0",
"@vue/eslint-config-typescript": "^9.1.0",
"@vue/test-utils": "^2.3.2",
"@wdio/cli": "^8.11.2",
"@wdio/local-runner": "^8.11.2",
"@wdio/mocha-framework": "^8.11.0",
"@wdio/spec-reporter": "^8.11.2",
"@wdio/cli": "^8.32.3",
"@wdio/local-runner": "^8.32.3",
"@wdio/mocha-framework": "^8.32.3",
"@wdio/spec-reporter": "^8.32.2",
"@wdio/static-server-service": "^8.11.0",
"babel-eslint": "10.1.0",
"canvas": "^2.11.2",
"chai": "4.3.6",
"chai-almost": "^1.0.1",
"chai-as-promised": "7.1.1",
"chai-subset": "^1.6.0",
"chromedriver": "^119.0.0",
"chromedriver": "^121.0.2",
"concurrently": "^8.2.2",
"cross-env": "^7.0.3",
"eslint": "^7.32.0",
Expand Down Expand Up @@ -115,7 +115,7 @@
"wdio-image-comparison-service": "^5.0.3",
"wdio-safaridriver-service": "^2.1.0",
"wdio-wait-for": "^3.0.4",
"webdriverio": "^8.20.0",
"webdriverio": "^8.32.3",
"yorkie": "^2.0.0"
},
"gitHooks": {
Expand Down
99 changes: 99 additions & 0 deletions src/components/SaveSegmentGroupDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<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-select
label="Format"
v-model="fileFormat"
:items="EXTENSIONS"
></v-select>
</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 setup lang="ts">
import { onMounted, ref } from 'vue';
import { onKeyDown } from '@vueuse/core';
import { saveAs } from 'file-saver';
import { useSegmentGroupStore } from '@/src/store/segmentGroups';
import { writeImage } from '@/src/io/readWriteImage';
import { useErrorMessage } from '@/src/composables/useErrorMessage';
const EXTENSIONS = [
'nrrd',
'nii',
'nii.gz',
'dcm',
'hdf5',
'tif',
'mha',
'vtk',
'iwi.cbor',
];
const props = defineProps<{
id: string;
}>();
const emit = defineEmits(['done']);
const fileName = ref('');
const valid = ref(true);
const saving = ref(false);
const fileFormat = ref(EXTENSIONS[0]);
const segmentGroupStore = useSegmentGroupStore();
async function saveSegmentGroup() {
if (fileName.value.trim().length === 0) {
return;
}
saving.value = true;
await useErrorMessage('Failed to save segment group', async () => {
const image = segmentGroupStore.dataIndex[props.id];
const serialized = await writeImage(fileFormat.value, image);
saveAs(new Blob([serialized]), `${fileName.value}.${fileFormat.value}`);
});
saving.value = false;
emit('done');
}
onMounted(() => {
// trigger form validation check so can immediately save with default value
fileName.value = segmentGroupStore.metadataByID[props.id].name;
});
onKeyDown('Enter', () => {
saveSegmentGroup();
});
function validFileName(name: string) {
return name.trim().length > 0 || 'Required';
}
</script>
6 changes: 6 additions & 0 deletions src/components/SaveSession.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue';
import { saveAs } from 'file-saver';
import { onKeyDown } from '@vueuse/core';
import { serialize } from '../io/state-file';
Expand Down Expand Up @@ -64,9 +65,14 @@ export default defineComponent({
}
onMounted(() => {
// triggers form validation check so can immediately save with default value
fileName.value = DEFAULT_FILENAME;
});
onKeyDown('Enter', () => {
saveSession();
});
function validFileName(name: string) {
return name.trim().length > 0 || 'Required';
}
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 { selectionEquals, useDatasetStore } from '@/src/store/datasets';
import { useDICOMStore } from '@/src/store/datasets-dicom';
Expand Down Expand Up @@ -143,6 +145,13 @@ function createSegmentGroupFromImage(selectedImageID: string) {
);
}
}
const saveId = ref('');
const saveDialog = ref(false);
function openSaveDialog(id: string) {
saveId.value = id;
saveDialog.value = true;
}
</script>

<template>
Expand Down Expand Up @@ -202,15 +211,21 @@ function createSegmentGroupFromImage(selectedImageID: string) {
<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 @@ -245,6 +260,12 @@ function createSegmentGroupFromImage(selectedImageID: string) {
</v-card-actions>
</v-card>
</v-dialog>

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

<style>
Expand Down
40 changes: 40 additions & 0 deletions src/io/readWriteImage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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, webWorker } = await readImageItk(null, file);
webWorker.terminate();
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;
};
18 changes: 12 additions & 6 deletions src/io/vtk/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,18 @@ interface FailResult {
type ReadResult = SuccessReadResult | FailResult;

export const runAsyncVTKReader = (readerName: string) => async (file: File) => {
const worker = new PromiseWorker(
new Worker(new URL('./async.reader.worker.ts', import.meta.url), {
const asyncWorker = new Worker(
new URL('./async.reader.worker.ts', import.meta.url),
{
type: 'module',
})
}
);
const worker = new PromiseWorker(asyncWorker);
const data = (await worker.postMessage({
file,
readerName,
})) as ReadResult;
asyncWorker.terminate();
if (data.status === 'success') {
return vtk(data.obj) as vtkObject;
}
Expand All @@ -41,15 +44,18 @@ type WriteResult = SuccessWriteResult | FailResult;

export const runAsyncVTKWriter =
(writerName: string) => async (dataSet: vtkDataSet) => {
const worker = new PromiseWorker(
new Worker(new URL('./async.writer.worker.ts', import.meta.url), {
const asyncWorker = new Worker(
new URL('./async.writer.worker.ts', import.meta.url),
{
type: 'module',
})
}
);
const worker = new PromiseWorker(asyncWorker);
const result = (await worker.postMessage({
obj: dataSet.getState(),
writerName,
})) as WriteResult;
asyncWorker.terminate();
if (result.status === 'success') {
return result.data;
}
Expand Down
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
2 changes: 1 addition & 1 deletion tests/pageobjects/volview.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class VolViewPage extends Page {
const views = await this_.views;
if (views.length === 0) return false;
const inView = await Promise.all(
views.map((v) => v.isDisplayedInViewport())
Array.from(views).map((v) => v.isDisplayedInViewport())
);
return inView.every(Boolean);
},
Expand Down

0 comments on commit 7420340

Please sign in to comment.