Skip to content

Commit

Permalink
Merge pull request #669 from PaulHax/segment-opacity
Browse files Browse the repository at this point in the history
Segments:  opacity slider and visibile toggle
  • Loading branch information
PaulHax authored Oct 23, 2024
2 parents d4b0cc1 + 1bc37fe commit 80021a2
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 34 deletions.
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;
}

0 comments on commit 80021a2

Please sign in to comment.