Skip to content

Commit

Permalink
Merge pull request #652 from lchauvin/synchronize-cameras-images
Browse files Browse the repository at this point in the history
Synchronize cameras images
  • Loading branch information
floryst authored Oct 9, 2024
2 parents e7e1b78 + 4f0ebf5 commit a77aad8
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 4 deletions.
40 changes: 40 additions & 0 deletions src/components/ImageDataBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { useDatasetStore } from '../store/datasets';
import { useMultiSelection } from '../composables/useMultiSelection';
import { useLayersStore } from '../store/datasets-layers';
import { useViewSliceStore } from '../store/view-configs/slicing';
import { useViewCameraStore } from '../store/view-configs/camera';
function imageCacheKey(dataID: string) {
return `image-${dataID}`;
Expand All @@ -31,6 +33,8 @@ export default defineComponent({
const dataStore = useDatasetStore();
const layersStore = useLayersStore();
const segmentGroupStore = useSegmentGroupStore();
const viewSliceStore = useViewSliceStore();
const viewCameraStore = useViewCameraStore();
const primarySelection = computed(() => dataStore.primarySelection);
Expand Down Expand Up @@ -116,6 +120,23 @@ export default defineComponent({
{ immediate: true, deep: true }
);
// --- sync --- //
const sameSpaceImages = computed(() => {
return imageStore.checkAllImagesSameSpace();
});
const isSync = computed(() => {
return viewSliceStore.isSync() && viewCameraStore.isSync();
});
function toggleSyncImages() {
viewSliceStore.toggleSyncImages();
viewCameraStore.toggleSyncCameras();
viewCameraStore.disableCameraAutoReset = isSync.value;
}
watch(isSync, () => {
viewSliceStore.updateSyncConfigs();
viewCameraStore.updateSyncConfigs();
});
// --- selection --- //
const { selected, selectedAll, selectedSome, toggleSelectAll } =
Expand Down Expand Up @@ -151,6 +172,9 @@ export default defineComponent({
setPrimarySelection: (sel: DataSelection) => {
dataStore.setPrimarySelection(sel);
},
sameSpaceImages,
toggleSyncImages,
isSync,
};
},
});
Expand All @@ -173,6 +197,22 @@ export default defineComponent({
/>
</v-col>
<v-col cols="6" align-self="center" class="d-flex justify-end">
<v-btn
icon
variant="text"
:disabled="!sameSpaceImages"
@click.stop="toggleSyncImages"
>
<v-icon v-if="isSync">mdi-lock</v-icon>
<v-icon flip="vertical" v-else>mdi-lock-open-variant</v-icon>
<v-tooltip
:disabled="!sameSpaceImages"
location="left"
activator="parent"
>
Sync Images
</v-tooltip>
</v-btn>
<v-btn
icon
variant="text"
Expand Down
10 changes: 6 additions & 4 deletions src/composables/usePersistCameraConfig.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { MaybeRef, Ref, computed, unref } from 'vue';
import { Maybe } from '@/src/types';
import { CameraConfig } from '@/src/store/view-configs/types';
import useViewCameraStore from '@/src/store/view-configs/camera';
import vtkCamera from '@kitware/vtk.js/Rendering/Core/Camera';
import { vtkFieldRef } from '@/src/core/vtk/vtkFieldRef';
Expand All @@ -14,14 +13,14 @@ export function usePersistCameraConfig(
) {
const viewCameraStore = useViewCameraStore();

type KeyType = keyof CameraConfig;
const keys: KeyType[] = [
const keys = [
'position',
'focalPoint',
'viewUp',
'parallelScale',
'directionOfProjection',
];
] as const;
type KeyType = (typeof keys)[number];

const cameraRefs = keys.reduce(
(refs, key) => ({
Expand Down Expand Up @@ -50,6 +49,9 @@ export function usePersistCameraConfig(
viewCameraStore.updateConfig(viewIDVal, dataIDVal, {
[key]: v,
});
if (viewCameraStore.isSync()) {
viewCameraStore.updateSyncConfigs();
}
},
}),
}),
Expand Down
5 changes: 5 additions & 0 deletions src/composables/useSliceConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export function useSliceConfig(
const imageIdVal = unref(imageID);
if (!imageIdVal || val == null) return;
store.updateConfig(unref(viewID), imageIdVal, { slice: val });

// Update other synchronized views if any
if (config.value?.syncState) {
store.updateSyncConfigs();
}
},
});
const range = computed((): Vector2 => {
Expand Down
20 changes: 20 additions & 0 deletions src/io/import/processors/importSingleFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { ImportHandler } from '@/src/io/import/common';
import { DataSourceWithFile } from '@/src/io/import/dataSource';
import { useDatasetStore } from '@/src/store/datasets';
import { useMessageStore } from '@/src/store/messages';
import { useViewStore } from '@/src/store/views';
import { useViewSliceStore } from '@/src/store/view-configs/slicing';
import { getLPSAxisFromDir } from '@/src/utils/lps';
import { InitViewSpecs } from '@/src/config';

/**
* Reads and imports a file DataSource.
Expand Down Expand Up @@ -36,6 +40,22 @@ const importSingleFile: ImportHandler = async (dataSource, { done }) => {
);
fileStore.add(dataID, [dataSource as DataSourceWithFile]);

// Create a default view for each viewID
useViewStore().viewIDs.forEach((viewID: string) => {
const { lpsOrientation, dimensions } = useImageStore().metadata[dataID];
const axisDir = InitViewSpecs[viewID].props.viewDirection;
const lpsFromDir = getLPSAxisFromDir(axisDir);
const lpsOrient = lpsOrientation[lpsFromDir];

const dimMax = dimensions[lpsOrient];

useViewSliceStore().updateConfig(viewID, dataID, {
axisDirection: axisDir,
max: dimMax - 1,
});
useViewSliceStore().resetSlice(viewID, dataID);
});

return done({
dataID,
dataSource,
Expand Down
2 changes: 2 additions & 0 deletions src/io/state-file/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ const SliceConfig = z.object({
min: z.number(),
max: z.number(),
axisDirection: LPSAxisDir,
syncState: z.boolean(),
}) satisfies z.ZodType<SliceConfig>;

const CameraConfig = z.object({
Expand All @@ -121,6 +122,7 @@ const CameraConfig = z.object({
focalPoint: Vector3.optional(),
directionOfProjection: Vector3.optional(),
viewUp: Vector3.optional(),
syncState: z.boolean().optional(),
}) satisfies z.ZodType<CameraConfig>;

const ColorBy = z.object({
Expand Down
12 changes: 12 additions & 0 deletions src/store/datasets-images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { StateFile, DatasetType } from '../io/state-file/schema';
import { serializeData } from '../io/state-file/utils';
import { useFileStore } from './datasets-files';
import { ImageMetadata } from '../types/image';
import { compareImageSpaces } from '../utils/imageSpace';

export const defaultImageMetadata = () => ({
name: '(none)',
Expand Down Expand Up @@ -79,6 +80,17 @@ export const useImageStore = defineStore('images', {
}
},

checkAllImagesSameSpace() {
if (this.idList.length < 2) return false;

const dataFirst = this.dataIndex[this.idList[0]];
const allEqual = this.idList.slice(1).every((id) => {
return compareImageSpaces(this.dataIndex[id], dataFirst);
});

return allEqual;
},

async serialize(stateFile: StateFile) {
const fileStore = useFileStore();
// We want to filter out volume images (which are generated and don't have
Expand Down
46 changes: 46 additions & 0 deletions src/store/view-configs/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import {
} from '@/src/utils/doubleKeyRecord';
import { reactive, ref } from 'vue';
import { Maybe } from '@/src/types';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { createViewConfigSerializer } from './common';
import { ViewConfig } from '../../io/state-file/schema';
import { CameraConfig } from './types';
import { useImageStore } from '../datasets-images';

export const useViewCameraStore = defineStore('viewCamera', () => {
const imageStore = useImageStore();
const configs = reactive<DoubleKeyRecord<CameraConfig>>({});

const getConfig = (viewID: Maybe<string>, dataID: Maybe<string>) =>
Expand Down Expand Up @@ -48,6 +51,46 @@ export const useViewCameraStore = defineStore('viewCamera', () => {
}
};

const toggleSyncCameras = () => {
// Synchronize all cameras when toggled
Object.keys(configs).forEach((viewID) => {
imageStore.idList.forEach((imageID) => {
const { syncState } = {
...getConfig(viewID, imageID),
};
updateConfig(viewID, imageID, { syncState: !syncState });
});
});
};

const isSync = () => {
const allSync = Object.keys(configs).every((sc) =>
Object.keys(configs[sc]).every((c) => configs[sc][c].syncState)
);

return allSync;
};

const updateSyncConfigs = () => {
Object.keys(configs).forEach((viewID) => {
const { currentImageID } = useCurrentImage();
const config = getConfig(viewID, currentImageID.value);
imageStore.idList.forEach((imageID) => {
const { syncState } = {
...getConfig(viewID, imageID),
};

if (syncState) {
updateConfig(viewID, imageID, {
position: config?.position,
focalPoint: config?.focalPoint,
parallelScale: config?.parallelScale,
});
}
});
});
};

const serialize = createViewConfigSerializer(configs, 'camera');

const deserialize = (viewID: string, config: Record<string, ViewConfig>) => {
Expand All @@ -66,6 +109,9 @@ export const useViewCameraStore = defineStore('viewCamera', () => {
toggleCameraAutoReset,
removeView,
removeData,
toggleSyncCameras,
updateSyncConfigs,
isSync,
serialize,
deserialize,
};
Expand Down
45 changes: 45 additions & 0 deletions src/store/view-configs/slicing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ import {
patchDoubleKeyRecord,
} from '@/src/utils/doubleKeyRecord';
import { Maybe } from '@/src/types';
import { useCurrentImage } from '@/src/composables/useCurrentImage';
import { createViewConfigSerializer } from './common';
import { ViewConfig } from '../../io/state-file/schema';
import { SliceConfig } from './types';
import { useImageStore } from '../datasets-images';

export const defaultSliceConfig = (): SliceConfig => ({
slice: 0,
min: 0,
max: 1,
axisDirection: 'Inferior',
syncState: false,
});

export const useViewSliceStore = defineStore('viewSlice', () => {
const imageStore = useImageStore();
const configs = reactive<DoubleKeyRecord<SliceConfig>>({});

const getConfig = (viewID: Maybe<string>, dataID: Maybe<string>) =>
Expand Down Expand Up @@ -64,6 +68,44 @@ export const useViewSliceStore = defineStore('viewSlice', () => {
}
};

const toggleSyncImages = () => {
// Synchronize all images when toggled
Object.keys(configs).forEach((viewID) => {
imageStore.idList.forEach((imageID) => {
const { syncState } = {
...defaultSliceConfig(),
...getConfig(viewID, imageID),
};
updateConfig(viewID, imageID, { syncState: !syncState });
});
});
};

const isSync = () => {
const allSync = Object.keys(configs).every((sc) =>
Object.keys(configs[sc]).every((c) => configs[sc][c].syncState)
);

return allSync;
};

const updateSyncConfigs = () => {
Object.keys(configs).forEach((viewID) => {
const { currentImageID } = useCurrentImage();
const config = getConfig(viewID, currentImageID.value);
imageStore.idList.forEach((imageID) => {
const { syncState } = {
...defaultSliceConfig(),
...getConfig(viewID, imageID),
};

if (syncState) {
updateConfig(viewID, imageID, { slice: config?.slice });
}
});
});
};

const serialize = createViewConfigSerializer(configs, 'slice');

const deserialize = (viewID: string, config: Record<string, ViewConfig>) => {
Expand All @@ -81,6 +123,9 @@ export const useViewSliceStore = defineStore('viewSlice', () => {
resetSlice,
removeView,
removeData,
toggleSyncImages,
updateSyncConfigs,
isSync,
serialize,
deserialize,
};
Expand Down
2 changes: 2 additions & 0 deletions src/store/view-configs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ export interface CameraConfig {
focalPoint?: Vector3;
directionOfProjection?: Vector3;
viewUp?: Vector3;
syncState?: boolean;
}

export interface SliceConfig {
slice: number;
min: number;
max: number;
axisDirection: LPSAxisDir;
syncState: boolean;
}

export interface VolumeColorConfig {
Expand Down

0 comments on commit a77aad8

Please sign in to comment.