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

Segments: opacity slider and visibile toggle #669

Merged
merged 7 commits into from
Oct 23, 2024
Merged
8 changes: 6 additions & 2 deletions src/components/EditableChipList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const props = withDefaults(
const itemsToRender = computed(() =>
props.items.map((item) => ({
key: item[props.itemKey] as string | number | symbol,
title: item[props.itemTitle],
title: item[props.itemTitle] as string | undefined,
}))
);
</script>
Expand All @@ -47,7 +47,11 @@ const itemsToRender = computed(() =>
@click="toggle"
>
<slot name="item-prepend" :key="key" :item="items[idx]"></slot>
<span class="overflow-hidden">{{ title }}</span>
<v-tooltip :text="title" location="end">
<template #activator="{ props }">
<span v-bind="props" class="text-truncate">{{ title }}</span>
</template>
</v-tooltip>
<v-spacer />
<slot name="item-append" :key="key" :item="items[idx]"></slot>
</v-chip>
Expand Down
11 changes: 6 additions & 5 deletions src/components/SegmentGroupControls.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const currentSegmentGroups = computed(() => {
const paintStore = usePaintToolStore();
const currentSegmentGroupID = computed({
get: () => paintStore.activeSegmentGroupID,
set: (id) => paintStore.setActiveLabelmap(id),
set: (id) => paintStore.setActiveSegmentGroup(id),
});

// clear selection if we delete the active segment group
Expand Down Expand Up @@ -204,6 +204,11 @@ function openSaveDialog(id: string) {
</v-menu>
</div>
<v-divider />

<segment-group-opacity
v-if="currentSegmentGroupID"
:group-id="currentSegmentGroupID"
/>
<v-radio-group
v-model="currentSegmentGroupID"
hide-details
Expand Down Expand Up @@ -258,10 +263,6 @@ function openSaveDialog(id: string) {
<v-divider />
</div>
<div v-else class="text-center text-caption">No selected image</div>
<segment-group-opacity
v-if="currentSegmentGroupID"
:group-id="currentSegmentGroupID"
/>
<segment-list
v-if="currentSegmentGroupID"
:group-id="currentSegmentGroupID"
Expand Down
2 changes: 1 addition & 1 deletion src/components/SegmentGroupOpacity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const setOpacity = (opacity: number) => {

<template>
<v-slider
class="pa-4"
class="ma-4"
label="Segment Group Opacity"
min="0"
max="1"
Expand Down
97 changes: 95 additions & 2 deletions src/components/SegmentList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { hexaToRGBA, rgbaToHexa } from '@/src/utils/color';
import { reactive, ref, toRefs, computed, watch } from 'vue';
import { SegmentMask } from '@/src/types/segment';
import { usePaintToolStore } from '@/src/store/tools/paint';
import { RGBAColor } from '@kitware/vtk.js/types';

const props = defineProps({
groupId: {
Expand Down Expand Up @@ -57,6 +58,58 @@ watch(
{ immediate: true }
);

// --- segment opacity --- //

const selectedSegmentMask = computed(() => {
if (!selectedSegment.value) return null;
return segmentGroupStore.getSegment(groupId.value, selectedSegment.value);
});

const segmentOpacity = computed(() => {
if (!selectedSegmentMask.value) return 1;
return selectedSegmentMask.value.color[3] / 255;
});

const setSegmentOpacity = (opacity: number) => {
if (!selectedSegmentMask.value) {
return;
}

const color = selectedSegmentMask.value.color;
segmentGroupStore.updateSegment(
groupId.value,
selectedSegmentMask.value.value,
{
color: [
...(color.slice(0, 3) as [number, number, number]),
Math.round(opacity * 255),
],
}
);
};

const toggleVisible = (value: number) => {
const segment = segmentGroupStore.getSegment(groupId.value, value);
if (!segment) return;
segmentGroupStore.updateSegment(groupId.value, value, {
visible: !segment.visible,
});
};

const allVisible = computed(() => {
return segments.value.every((seg) => seg.visible);
});

function toggleGlobalVisible() {
const visible = !allVisible.value;

segments.value.forEach((seg) => {
segmentGroupStore.updateSegment(groupId.value, seg.value, {
visible,
});
});
}

// --- editing state --- //

const editingSegmentValue = ref<Maybe<number>>(null);
Expand Down Expand Up @@ -106,6 +159,30 @@ function deleteEditingSegment() {
</script>

<template>
<v-btn @click.stop="toggleGlobalVisible">
Toggle Segments
<slot name="append">
<v-icon v-if="allVisible" class="pl-2">mdi-eye</v-icon>
<v-icon v-else class="pl-2">mdi-eye-off</v-icon>
<v-tooltip location="top" activator="parent">{{
allVisible ? 'Hide' : 'Show'
}}</v-tooltip>
</slot>
</v-btn>

<v-slider
class="ma-4"
label="Segment Opacity"
min="0"
max="1"
step="0.01"
density="compact"
hide-details
thumb-label
:model-value="segmentOpacity"
@update:model-value="setSegmentOpacity($event)"
/>

<editable-chip-list
v-model="selectedSegment"
:items="segments"
Expand All @@ -119,11 +196,27 @@ function deleteEditingSegment() {
<div class="dot-container mr-3">
<div
class="color-dot"
:style="{ background: rgbaToHexa(item.color) }"
:style="{ background: rgbaToHexa([...item.color.slice(0,3), 255] as RGBAColor) }"
/>
</div>
</template>
<template #item-append="{ key }">
<template #item-append="{ key, item }">
<v-btn
icon
size="small"
density="compact"
class="ml-auto mr-1"
variant="plain"
@click.stop="toggleVisible(key as number)"
>
<v-icon v-if="item.visible" style="pointer-events: none"
>mdi-eye</v-icon
>
<v-icon v-else style="pointer-events: none">mdi-eye-off</v-icon>
<v-tooltip location="left" activator="parent">{{
item.visible ? 'Hide' : 'Show'
}}</v-tooltip>
</v-btn>
<v-btn
icon="mdi-pencil"
size="small"
Expand Down
2 changes: 1 addition & 1 deletion src/components/vtk/VtkSegmentationSliceRepresentation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ const applySegmentColoring = () => {
const r = segment.color[0] || 0;
const g = segment.color[1] || 0;
const b = segment.color[2] || 0;
const a = segment.color[3] || 0;
const a = (segment.visible && segment.color[3]) || 0;
cfun.addRGBPoint(segment.value, r / 255, g / 255, b / 255);
ofun.addPoint(segment.value, a / 255);

Expand Down
3 changes: 3 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,16 +184,19 @@ export const DEFAULT_SEGMENT_MASKS: SegmentMask[] = [
value: 1,
name: 'Tissue',
color: [255, 0, 0, 255],
visible: true,
},
{
value: 2,
name: 'Liver',
color: [0, 255, 0, 255],
visible: true,
},
{
value: 3,
name: 'Heart',
color: [0, 0, 255, 255],
visible: true,
},
];

Expand Down
1 change: 1 addition & 0 deletions src/io/state-file/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ const SegmentMask = z.object({
value: z.number(),
name: z.string(),
color: RGBAColor,
visible: z.boolean().default(true),
});

export const SegmentGroupMetadata = z.object({
Expand Down
3 changes: 3 additions & 0 deletions src/store/segmentGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => {
value: segment.labelID,
name: segment.SegmentLabel,
color: [...segment.recommendedDisplayRGBValue, 255],
visible: true,
}));
}
}
Expand All @@ -237,6 +238,7 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => {
value,
name: makeDefaultSegmentName(value),
color: getNextColor(),
visible: true,
}));
}

Expand Down Expand Up @@ -327,6 +329,7 @@ export const useSegmentGroupStore = defineStore('segmentGroup', () => {
name: makeDefaultSegmentName(value),
value,
color: DEFAULT_SEGMENT_COLOR,
visible: true,
};
}

Expand Down
62 changes: 39 additions & 23 deletions src/store/tools/paint.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Vector2 } from '@kitware/vtk.js/types';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { Manifest, StateFile } from '@/src/io/state-file/schema';
import { computed, ref, watch } from 'vue';
import { computed, ref, watchEffect } from 'vue';
import { vec3 } from 'gl-matrix';
import { defineStore } from 'pinia';
import { Maybe } from '@/src/types';
Expand All @@ -27,9 +27,10 @@ export const usePaintToolStore = defineStore('paint', () => {
return this.$paint.factory;
}

const segmentGroupStore = useSegmentGroupStore();

const activeLabelmap = computed(() => {
if (!activeSegmentGroupID.value) return null;
const segmentGroupStore = useSegmentGroupStore();
return segmentGroupStore.dataIndex[activeSegmentGroupID.value] ?? null;
});

Expand All @@ -47,25 +48,37 @@ export const usePaintToolStore = defineStore('paint', () => {
/**
* Sets the active labelmap.
*/
function setActiveLabelmap(segmentGroupID: Maybe<string>) {
function setActiveSegmentGroup(segmentGroupID: Maybe<string>) {
activeSegmentGroupID.value = segmentGroupID;
}

/**
* Gets the first segment group ID for a given image.
* @param imageID
*/
function getFirstSegmentGroupID(imageID: Maybe<string>): Maybe<string> {
if (!imageID) return null;
const segmentGroups = segmentGroupStore.orderByParent[imageID];
if (segmentGroups && segmentGroups.length > 0) {
return segmentGroups[0];
}
return null;
}

/**
* Sets the active labelmap from a given image.
*
* If a labelmap exists, pick the first one. If no labelmap exists, create one.
*/
function setActiveLabelmapFromImage(imageID: Maybe<string>) {
function ensureActiveSegmentGroupForImage(imageID: Maybe<string>) {
if (!imageID) {
setActiveLabelmap(null);
setActiveSegmentGroup(null);
return;
}

const segmentGroupStore = useSegmentGroupStore();
const labelmaps = segmentGroupStore.orderByParent[imageID];
if (labelmaps?.length) {
activeSegmentGroupID.value = labelmaps[0];
const segmentGroupID = getFirstSegmentGroupID(imageID);
if (segmentGroupID) {
setActiveSegmentGroup(segmentGroupID);
} else {
activeSegmentGroupID.value =
segmentGroupStore.newLabelmapFromImage(imageID);
Expand All @@ -83,7 +96,6 @@ export const usePaintToolStore = defineStore('paint', () => {
if (!activeSegmentGroupID.value)
throw new Error('Cannot set active segment without a labelmap');

const segmentGroupStore = useSegmentGroupStore();
const { segments } =
segmentGroupStore.metadataByID[activeSegmentGroupID.value];

Expand Down Expand Up @@ -158,7 +170,7 @@ export const usePaintToolStore = defineStore('paint', () => {
if (!imageID) {
return false;
}
setActiveLabelmapFromImage(imageID);
ensureActiveSegmentGroupForImage(imageID);
this.$paint.setBrushSize(this.brushSize);

isActive.value = true;
Expand Down Expand Up @@ -190,22 +202,26 @@ export const usePaintToolStore = defineStore('paint', () => {
if (paint.activeSegmentGroupID !== null) {
activeSegmentGroupID.value =
segmentGroupIDMap[paint.activeSegmentGroupID];
setActiveLabelmap(activeSegmentGroupID.value);
setActiveSegmentGroup(activeSegmentGroupID.value);
setActiveSegment.call(this, paint.activeSegment);
}
}

// --- change labelmap if paint is active --- //
// Create segment group if paint is active and none exist.
// If paint is not active, but there is a segment group for the current image, set it as active.
watchEffect(() => {
const imageID = currentImageID.value;
if (!imageID) return;

watch(
currentImageID,
(imageID) => {
if (isActive.value) {
setActiveLabelmapFromImage(imageID);
if (isActive.value) {
ensureActiveSegmentGroupForImage(imageID);
} else {
const segmentGroupID = getFirstSegmentGroupID(imageID);
if (segmentGroupID) {
setActiveSegmentGroup(segmentGroupID);
}
},
{ immediate: true }
);
}
});

return {
// state
Expand All @@ -222,8 +238,8 @@ export const usePaintToolStore = defineStore('paint', () => {
deactivateTool,

setMode,
setActiveLabelmap,
setActiveLabelmapFromImage,
setActiveSegmentGroup,
ensureActiveSegmentGroupForImage,
setActiveSegment,
setBrushSize,
setSliceAxis,
Expand Down
1 change: 1 addition & 0 deletions src/types/segment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export interface SegmentMask {
value: number;
name: string;
color: RGBAColor;
visible: boolean;
}
Loading