From 8517a6eeef6387b57ccd00b88f2299f188018f51 Mon Sep 17 00:00:00 2001 From: Marc-Lorenz <169900493+Marc-Lorenz@users.noreply.github.com> Date: Wed, 13 Nov 2024 12:20:44 +0100 Subject: [PATCH 01/20] intermediate commit in adding new image loading --- src/App.vue | 2 + src/Editors/BackgroundDrawer.ts | 10 +- src/Editors/Editor.ts | 26 ++-- src/Editors/FaceMeshEditor.ts | 2 - src/components/ImageLoadModal.vue | 153 ++++++++++++++++++++++ src/components/Main/CentralCanvas.vue | 41 +++--- src/components/Navbar/LoadSaveActions.vue | 17 +-- src/components/OrientationScroller.vue | 92 +++++++++++++ src/editor2d.ts | 0 src/enums/orientation.ts | 6 + src/enums/threeDimView.ts | 5 + src/imageFile.ts | 21 ++- src/stores/imageLoadStore.ts | 11 ++ src/util/orientationGuesser.ts | 147 +++++++++++++++++++++ static/css/bootstrap.scss | 5 +- 15 files changed, 485 insertions(+), 53 deletions(-) create mode 100644 src/components/ImageLoadModal.vue create mode 100644 src/components/OrientationScroller.vue delete mode 100644 src/editor2d.ts create mode 100644 src/enums/orientation.ts create mode 100644 src/enums/threeDimView.ts create mode 100644 src/stores/imageLoadStore.ts create mode 100644 src/util/orientationGuesser.ts diff --git a/src/App.vue b/src/App.vue index 875d314..44c7813 100644 --- a/src/App.vue +++ b/src/App.vue @@ -4,6 +4,7 @@ import Sidebar from '@/components/Main/SidebarContainer.vue'; import CentralCanvas from '@/components/Main/CentralCanvas.vue'; import ThumbnailGallery from '@/components/Main/ThumbnailGallery.vue'; import TopNavbar from '@/components/Main/TopNavbar.vue'; +import ImageLoadModal from '@/components/ImageLoadModal.vue'; onMounted(() => { const elements = document.querySelectorAll('[aria-keyshortcuts]'); @@ -42,5 +43,6 @@ onMounted(() => { + diff --git a/src/Editors/BackgroundDrawer.ts b/src/Editors/BackgroundDrawer.ts index c0dac57..0b33f59 100644 --- a/src/Editors/BackgroundDrawer.ts +++ b/src/Editors/BackgroundDrawer.ts @@ -4,12 +4,20 @@ import { Editor } from '@/Editors/Editor'; import { AnnotationTool } from '@/enums/annotationTool'; +import { useAnnotationHistoryStore } from '@/stores/annotationHistoryStore'; export class BackgroundDrawer extends Editor { + private readonly annotationHistoryStore = useAnnotationHistoryStore(); + constructor() { super(); - Editor.add(this); + this.annotationHistoryStore.$subscribe(() => { + if (!this.annotationHistoryStore.selectedHistory) { + return; + } + Editor.image.src = this.annotationHistoryStore.selectedHistory.file.html; + }); } draw() { diff --git a/src/Editors/Editor.ts b/src/Editors/Editor.ts index 8461c0a..439740a 100644 --- a/src/Editors/Editor.ts +++ b/src/Editors/Editor.ts @@ -18,6 +18,10 @@ export abstract class Editor { public static image: HTMLImageElement = new Image(); private static allEditors: Editor[] = []; + protected constructor() { + Editor.add(this); + } + protected static add(editor: Editor) { Editor.allEditors.push(editor); } @@ -46,7 +50,7 @@ export abstract class Editor { } public static async setBackgroundSource(source: ImageFile): Promise { - const imageLoadPromise = new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { Editor.image.src = source.html; Editor.image.onload = () => { if (Editor.image.width === 0) { @@ -55,6 +59,11 @@ export abstract class Editor { if (Editor.image.height === 0) { reject(new Error('Image loaded with height 0.')); } + // on success reset global zoom and pan + Editor.zoomScale = 1; + Editor.offsetX = 0; + Editor.offsetY = 0; + resolve(); }; Editor.image.onerror = (e) => { @@ -62,21 +71,6 @@ export abstract class Editor { reject(new Error('Failed to load image.')); }; }); - - // Wait for the image to load - await imageLoadPromise; - - if (Editor.image.width === 0) { - throw new Error('image parsed with 0 width'); - } - if (Editor.image.height === 0) { - throw new Error('image parsed with 0 height'); - } - - // on success reset global zoom and pan - Editor.zoomScale = 1; - Editor.offsetX = 0; - Editor.offsetY = 0; } public static pan(deltaX: number, deltaY: number): void { diff --git a/src/Editors/FaceMeshEditor.ts b/src/Editors/FaceMeshEditor.ts index 3896b83..2bb4253 100644 --- a/src/Editors/FaceMeshEditor.ts +++ b/src/Editors/FaceMeshEditor.ts @@ -58,8 +58,6 @@ export class FaceMeshEditor extends Editor { this.graph = this.annotationHistoryStore.selectedHistory?.get(); Editor.draw(); }); - - Editor.add(this); } private _graph: Graph = new Graph([]); diff --git a/src/components/ImageLoadModal.vue b/src/components/ImageLoadModal.vue new file mode 100644 index 0000000..1f07b71 --- /dev/null +++ b/src/components/ImageLoadModal.vue @@ -0,0 +1,153 @@ + + + diff --git a/src/components/Main/CentralCanvas.vue b/src/components/Main/CentralCanvas.vue index 213c7b6..7710a09 100644 --- a/src/components/Main/CentralCanvas.vue +++ b/src/components/Main/CentralCanvas.vue @@ -6,6 +6,7 @@ import { useAnnotationToolStore } from '@/stores/annotationToolStore'; import { AnnotationTool } from '@/enums/annotationTool'; import { FaceMeshEditor } from '@/Editors/FaceMeshEditor'; import { BackgroundDrawer } from '@/Editors/BackgroundDrawer'; +import ThreeDimViewContainer from '@/components/ImageLoadModal.vue'; const annotationHistoryStore = useAnnotationHistoryStore(); const annotationToolStore = useAnnotationToolStore(); @@ -54,12 +55,15 @@ watch( () => annotationHistoryStore.selectedHistory, async (value) => { if (!value) return; - await Editor.setBackgroundSource(value.file); - Editor.center(); - Editor.draw(); - editors.value.forEach((editor) => { - editor.onBackgroundLoaded(); - }); + Editor.setBackgroundSource(value.file) + .then(() => { + Editor.center(); + Editor.draw(); + editors.value.forEach((editor) => { + editor.onBackgroundLoaded(); + }); + }) + .catch((reason) => console.error(reason)); } ); @@ -138,17 +142,20 @@ const onResize = () => { diff --git a/src/components/Navbar/LoadSaveActions.vue b/src/components/Navbar/LoadSaveActions.vue index baf2855..8bd3f60 100644 --- a/src/components/Navbar/LoadSaveActions.vue +++ b/src/components/Navbar/LoadSaveActions.vue @@ -7,9 +7,11 @@ import { ModelType } from '@/enums/modelType'; import { useModelStore } from '@/stores/modelStore'; import { useAnnotationHistoryStore } from '@/stores/annotationHistoryStore'; import ButtonWithIcon from '@/components/MenuItems/ButtonWithIcon.vue'; +import { useImageLoadStore } from '@/stores/imageLoadStore'; const modelStore = useModelStore(); const annotationHistoryStore = useAnnotationHistoryStore(); +const imageLoadStore = useImageLoadStore(); const showSendAnno = computed( () => @@ -21,20 +23,7 @@ function handleSendAnno(): void { } function openImage(): void { - const input: HTMLInputElement = document.createElement('input'); - input.id = 'image-input'; - input.type = 'file'; - input.accept = 'image/png, image/jpeg, image/jpg'; - input.multiple = true; - input.onchange = () => { - if (input.files) { - const files: File[] = Array.from(input.files); - files.forEach((f) => { - annotationHistoryStore.add(f, modelStore.model); - }); - } - }; - input.click(); + imageLoadStore.showLoadModal = true; } function openAnnotation(): boolean { diff --git a/src/components/OrientationScroller.vue b/src/components/OrientationScroller.vue new file mode 100644 index 0000000..b34367b --- /dev/null +++ b/src/components/OrientationScroller.vue @@ -0,0 +1,92 @@ + + + diff --git a/src/editor2d.ts b/src/editor2d.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/enums/orientation.ts b/src/enums/orientation.ts new file mode 100644 index 0000000..43d2ec6 --- /dev/null +++ b/src/enums/orientation.ts @@ -0,0 +1,6 @@ +export enum Orientation { + front, + left, + right, + unknown +} diff --git a/src/enums/threeDimView.ts b/src/enums/threeDimView.ts new file mode 100644 index 0000000..5f9cad3 --- /dev/null +++ b/src/enums/threeDimView.ts @@ -0,0 +1,5 @@ +export enum ThreeDimView { + left, + center, + right +} diff --git a/src/imageFile.ts b/src/imageFile.ts index dbefda1..ddd70db 100644 --- a/src/imageFile.ts +++ b/src/imageFile.ts @@ -4,11 +4,15 @@ */ import { calculateSHA } from '@/util/sha'; import { imageFromFile } from '@/util/imageFromFile'; +import { ThreeDimView } from '@/enums/threeDimView'; export class ImageFile { readonly file: File; sha: string = ''; - html: string = ''; + left: string = ''; + center: string = ''; + right: string = ''; + selected: ThreeDimView = ThreeDimView.center; static async create(file: File) { const sha = await calculateSHA(file); @@ -19,6 +23,19 @@ export class ImageFile { private constructor(file: File, sha: string, html: string) { this.file = file; this.sha = sha; - this.html = html; + this.center = html; + } + + get html(): string { + switch (this.selected) { + case ThreeDimView.left: + return this.left; + case ThreeDimView.center: + return this.center; + case ThreeDimView.right: + return this.right; + default: + return this.center; + } } } diff --git a/src/stores/imageLoadStore.ts b/src/stores/imageLoadStore.ts new file mode 100644 index 0000000..2ad8d8a --- /dev/null +++ b/src/stores/imageLoadStore.ts @@ -0,0 +1,11 @@ +import { defineStore } from 'pinia'; + +export const useImageLoadStore = defineStore({ + id: 'imageLoad', + state: (): { + showLoadModal: boolean; + } => ({ + showLoadModal: true + }), + actions: {} +}); diff --git a/src/util/orientationGuesser.ts b/src/util/orientationGuesser.ts new file mode 100644 index 0000000..55d3e95 --- /dev/null +++ b/src/util/orientationGuesser.ts @@ -0,0 +1,147 @@ +import { FaceLandmarker, FilesetResolver, type NormalizedLandmark } from '@mediapipe/tasks-vision'; +import { Orientation } from '@/enums/orientation'; +import type { ImageFile } from '@/imageFile'; + +export type orientationGuessResult = { + image: ImageFile; + orientation: Orientation; +}; + +export async function guessOrientation(images: ImageFile[]): Promise { + const tool = await getMeshAnnotationTool(); + + return await Promise.all( + images.map(async (image) => { + const data = await handleFile(image.file); + const res = tool.detect(data); + const mesh = res.faceLandmarks[0]; + const orientation = orientationFromMesh(mesh); + + const result: orientationGuessResult = { image, orientation }; + return result; + }) + ); +} + +function getMeshAnnotationTool() { + return FilesetResolver.forVisionTasks( + 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm' + ).then((filesetResolver) => + FaceLandmarker.createFromOptions(filesetResolver, { + baseOptions: { + modelAssetPath: + 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task', + // When adding user model of same type -> modelAssetBuffer + delegate: 'CPU' + }, + minFaceDetectionConfidence: 0.3, + minFacePresenceConfidence: 0.3, + runningMode: 'IMAGE', + numFaces: 1 + }) + ); +} + +function orientationFromMesh(mesh: NormalizedLandmark[]) { + // https://github.com/sshadmand/face-direction-detection/blob/main/src/utils/drawMesh.js + let noseTip, leftNose, rightNose; + try { + noseTip = mesh[1]; + leftNose = mesh[279]; + rightNose = mesh[49]; + } catch (error) { + console.log('error creating directional points', mesh, error); + } + + if (!noseTip || !leftNose || !rightNose) { + return Orientation.unknown; + } + + // MIDESCTION OF NOSE IS BACK OF NOSE PERPENDICULAR + const midpoint = { + x: (leftNose.x + rightNose.x) / 2, + y: (leftNose.y + rightNose.y) / 2, + z: (leftNose.z + rightNose.z) / 2 + }; + // const perpendicularUp = { x: midpoint.x, y: midpoint.y - 50, z: midpoint.z }; + + // CALC ANGLES + // const yaw = getAngleBetweenLines(midpoint, noseTip, perpendicularUp); + const turn = getAngleBetweenLines(midpoint, rightNose, noseTip); + + /* + turn is an angle between 0 and 180 + 0 : facing away to the LEFT from the cameras POV + 180: facing away to the RIGHT from the camera POV + */ + + // Todo - check what happens [180, 360] + if (turn < 60) { + return Orientation.left; + } + + if (turn > 120) { + return Orientation.right; + } + + if (turn >= 60 && turn <= 120) { + return Orientation.front; + } + + return Orientation.unknown; +} + +function getAngleBetweenLines( + midpoint: { x: number; y: number; z: number }, + point1: { x: number; y: number; z: number }, + point2: { x: number; y: number; z: number } +) { + const vector1 = { x: point1.x - midpoint.x, y: point1.y - midpoint.y }; + const vector2 = { x: point2.x - midpoint.x, y: point2.y - midpoint.y }; + + // Calculate the dot product of the two vectors + const dotProduct = vector1.x * vector2.x + vector1.y * vector2.y; + + // Calculate the magnitudes of the vectors + const magnitude1 = Math.sqrt(vector1.x * vector1.x + vector1.y * vector1.y); + const magnitude2 = Math.sqrt(vector2.x * vector2.x + vector2.y * vector2.y); + + // Calculate the cosine of the angle between the two vectors + const cosineTheta = dotProduct / (magnitude1 * magnitude2); + + // Use the arccosine function to get the angle in radians + const angleInRadians = Math.acos(cosineTheta); + + // Convert the angle to degrees + return (angleInRadians * 180) / Math.PI; +} + +async function handleFile(file: File): Promise { + const reader = new FileReader(); + + const dataUrl: string = await new Promise((resolve, reject) => { + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(new Error('Error reading file')); + reader.readAsDataURL(file); + }); + + const img = new Image(); + img.src = dataUrl; + + await new Promise((resolve, reject) => { + img.onload = () => resolve(null); + img.onerror = () => reject(new Error('Error loading image')); + }); + + const canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Could not get canvas context'); + } + + ctx.drawImage(img, 0, 0); + return ctx.getImageData(0, 0, img.width, img.height); +} diff --git a/static/css/bootstrap.scss b/static/css/bootstrap.scss index d18c45c..7a2f911 100644 --- a/static/css/bootstrap.scss +++ b/static/css/bootstrap.scss @@ -21,7 +21,8 @@ $utilities: map-merge( 10: 10%, 20: 20%, 70: 70%, - 15vh: 15vh, + 15vw: 15vw, + 5vw: 5vw, ), ), ), @@ -32,7 +33,9 @@ $utilities: map-merge( values: map-merge( map-get(map-get($utilities, "height"), "values"), ( + 75vh: 75vh, 15vh: 15vh, + 5vh: 5vh, 5: 5%, 10: 10%, 90: 90%, From e10a865ea6b2c83b921f4734bbeafcf03d17bc09 Mon Sep 17 00:00:00 2001 From: Marc-Lorenz <169900493+Marc-Lorenz@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:00:53 +0100 Subject: [PATCH 02/20] canvases are drawing now --- src/components/ImageLoadModal.vue | 110 ++++++++++++++++--------- src/components/OrientationScroller.vue | 92 --------------------- 2 files changed, 72 insertions(+), 130 deletions(-) delete mode 100644 src/components/OrientationScroller.vue diff --git a/src/components/ImageLoadModal.vue b/src/components/ImageLoadModal.vue index 1f07b71..eef2142 100644 --- a/src/components/ImageLoadModal.vue +++ b/src/components/ImageLoadModal.vue @@ -1,11 +1,11 @@ From 66959062def458f07bfd413b924e7262a5028a13 Mon Sep 17 00:00:00 2001 From: Marc-Lorenz <169900493+Marc-Lorenz@users.noreply.github.com> Date: Wed, 13 Nov 2024 13:06:59 +0100 Subject: [PATCH 03/20] fix scrolling --- src/components/ImageLoadModal.vue | 83 ++++++++++++++++++------------- static/css/bootstrap.scss | 2 +- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/src/components/ImageLoadModal.vue b/src/components/ImageLoadModal.vue index eef2142..50760de 100644 --- a/src/components/ImageLoadModal.vue +++ b/src/components/ImageLoadModal.vue @@ -11,7 +11,6 @@ const imageLoadStore = useImageLoadStore(); const disableHide = ref(true); const imageCount = ref(100); const progress = ref(50); -const imageScrollContainer = ref(null); const imageInput = ref(null); const orientations = ref([]); const screenHeight = ref(window.innerHeight); @@ -125,59 +124,73 @@ onBeforeUnmount(() => {
-
+
toggle disable hide
+ -
-
+
+ +

Left

-
- - - +
+
+ + + +
-
+ + +

Frontal

-
- - - +
+
+ + + +
-
+ + +

Right

-
- - - +
+
+ + + +
+

- Load + + + Load + Next
diff --git a/static/css/bootstrap.scss b/static/css/bootstrap.scss index 7a2f911..542352d 100644 --- a/static/css/bootstrap.scss +++ b/static/css/bootstrap.scss @@ -33,7 +33,7 @@ $utilities: map-merge( values: map-merge( map-get(map-get($utilities, "height"), "values"), ( - 75vh: 75vh, + 60vh: 60vh, 15vh: 15vh, 5vh: 5vh, 5: 5%, From 4589487a272904b35d379bb5b54f0d22412fa21a Mon Sep 17 00:00:00 2001 From: Marc-Lorenz <169900493+Marc-Lorenz@users.noreply.github.com> Date: Tue, 26 Nov 2024 14:24:55 +0100 Subject: [PATCH 04/20] finish loading images --- e2e/mainLayout.spec.ts | 1 - src/Editors/BackgroundDrawer.ts | 6 +- src/Editors/Editor.ts | 46 ++-- src/cache/fileAnnotationHistory.ts | 14 +- src/components/ImageLoadModal.vue | 230 +++++++++++++----- src/components/Main/CentralCanvas.vue | 16 +- src/components/Main/ThumbnailGallery.vue | 13 +- .../Main/__tests__/CentralCanvas.spec.ts | 27 +- .../Main/__tests__/ThumbnailGallery.spec.ts | 22 +- src/components/Navbar/EditButtons.vue | 3 +- src/components/Navbar/LoadSaveActions.vue | 5 +- src/components/ThumbnailContainer.vue | 19 +- src/enums/orientation.ts | 2 +- src/graph/graph.ts | 22 ++ src/model/mediapipe.ts | 32 +-- .../__tests__/annotationHistoryStore.spec.ts | 47 ++-- src/stores/annotationHistoryStore.ts | 33 ++- src/util/orientationGuesser.ts | 5 +- static/css/standard.css | 5 - 19 files changed, 348 insertions(+), 200 deletions(-) diff --git a/e2e/mainLayout.spec.ts b/e2e/mainLayout.spec.ts index 7e87836..7f130d8 100644 --- a/e2e/mainLayout.spec.ts +++ b/e2e/mainLayout.spec.ts @@ -1,5 +1,4 @@ import { test, expect } from '@playwright/test'; -import { i } from 'vite/dist/node/types.d-aGj9QkWt.js'; test.describe('check title and main page layout', () => { test('visits the app root url and checks elements layout', async ({ page }) => { diff --git a/src/Editors/BackgroundDrawer.ts b/src/Editors/BackgroundDrawer.ts index 0b33f59..456b8ff 100644 --- a/src/Editors/BackgroundDrawer.ts +++ b/src/Editors/BackgroundDrawer.ts @@ -5,6 +5,7 @@ import { Editor } from '@/Editors/Editor'; import { AnnotationTool } from '@/enums/annotationTool'; import { useAnnotationHistoryStore } from '@/stores/annotationHistoryStore'; +import { imageFromFile } from '@/util/imageFromFile'; export class BackgroundDrawer extends Editor { private readonly annotationHistoryStore = useAnnotationHistoryStore(); @@ -16,7 +17,10 @@ export class BackgroundDrawer extends Editor { if (!this.annotationHistoryStore.selectedHistory) { return; } - Editor.image.src = this.annotationHistoryStore.selectedHistory.file.html; + if (!this.annotationHistoryStore.selectedHistory.file.center) return; + imageFromFile(this.annotationHistoryStore.selectedHistory.file.center.image.file).then( + (r) => (Editor.image.src = r) + ); }); } diff --git a/src/Editors/Editor.ts b/src/Editors/Editor.ts index 439740a..a566365 100644 --- a/src/Editors/Editor.ts +++ b/src/Editors/Editor.ts @@ -1,6 +1,6 @@ import $ from 'jquery'; -import type { ImageFile } from '@/imageFile'; import { AnnotationTool } from '@/enums/annotationTool'; +import { imageFromFile } from '@/util/imageFromFile'; export abstract class Editor { protected static canvas: HTMLCanvasElement; @@ -20,6 +20,26 @@ export abstract class Editor { protected constructor() { Editor.add(this); + Editor.image.onload = () => { + if (Editor.image.width === 0) { + console.log('Tried to load image with 0 width'); + return; + } + if (Editor.image.height === 0) { + console.log('Tried to load image with 0 height'); + return; + } + // on success reset global zoom and pan + Editor.zoomScale = 1; + Editor.offsetX = 0; + Editor.offsetY = 0; + + return; + }; + Editor.image.onerror = (e) => { + console.error('Error loading image', e); + return; + }; } protected static add(editor: Editor) { @@ -49,27 +69,9 @@ export abstract class Editor { }; } - public static async setBackgroundSource(source: ImageFile): Promise { - return new Promise((resolve, reject) => { - Editor.image.src = source.html; - Editor.image.onload = () => { - if (Editor.image.width === 0) { - reject(new Error('Image loaded with width 0.')); - } - if (Editor.image.height === 0) { - reject(new Error('Image loaded with height 0.')); - } - // on success reset global zoom and pan - Editor.zoomScale = 1; - Editor.offsetX = 0; - Editor.offsetY = 0; - - resolve(); - }; - Editor.image.onerror = (e) => { - console.error('Error loading image', e); - reject(new Error('Failed to load image.')); - }; + public static async setBackgroundSource(source: File) { + imageFromFile(source).then((r) => { + Editor.image.src = r; }); } diff --git a/src/cache/fileAnnotationHistory.ts b/src/cache/fileAnnotationHistory.ts index f26e36a..d27d484 100644 --- a/src/cache/fileAnnotationHistory.ts +++ b/src/cache/fileAnnotationHistory.ts @@ -1,7 +1,7 @@ import { Point2D } from '@/graph/point2d'; import { Graph } from '@/graph/graph'; -import { ImageFile } from '@/imageFile'; import { SaveStatus } from '@/enums/saveStatus'; +import type { MultipleViewImage } from '@/components/ImageLoadModal.vue'; /** * Represents a history of annotations for a specific file. @@ -12,15 +12,15 @@ export class FileAnnotationHistory { private readonly cacheSize: number; private readonly history: Graph[] = []; private currentHistoryIndex: number = 0; - private readonly _file: ImageFile; + private readonly _file: MultipleViewImage; private _status: SaveStatus; /** * Creates a new FileAnnotationHistory instance. - * @param {ImageFile} file - The file associated with the annotations. - * @param {number} cacheSize - The maximum number of history entries to retain. + * @param file - The file associated with the annotations. + * @param cacheSize - The maximum number of history entries to retain. */ - constructor(file: ImageFile, cacheSize: number) { + constructor(file: MultipleViewImage, cacheSize: number) { this._file = file; this.cacheSize = cacheSize; this._status = SaveStatus.unedited; @@ -28,9 +28,9 @@ export class FileAnnotationHistory { /** * Gets the associated file. - * @returns {File} - The file associated with the annotations. + * @returns - The file associated with the annotations. */ - get file(): ImageFile { + get file(): MultipleViewImage { return this._file; } diff --git a/src/components/ImageLoadModal.vue b/src/components/ImageLoadModal.vue index 50760de..aba26eb 100644 --- a/src/components/ImageLoadModal.vue +++ b/src/components/ImageLoadModal.vue @@ -6,15 +6,30 @@ import { ImageFile } from '@/imageFile'; import { guessOrientation, type orientationGuessResult } from '@/util/orientationGuesser'; import { Orientation } from '@/enums/orientation'; import { imageFromFile } from '@/util/imageFromFile'; +import { useAnnotationHistoryStore } from '@/stores/annotationHistoryStore'; const imageLoadStore = useImageLoadStore(); const disableHide = ref(true); -const imageCount = ref(100); -const progress = ref(50); +const imageCount = ref(0); +const progress = ref(0); const imageInput = ref(null); const orientations = ref([]); const screenHeight = ref(window.innerHeight); const processing = ref(false); +const confirmModal = ref(false); +const result = ref([]); + +export interface MultipleViewImage { + left: orientationGuessResult | null; + center: orientationGuessResult | null; + right: orientationGuessResult | null; +} + +const selectedImages = ref({ + left: null, + center: null, + right: null +}); watch(orientations, (newVal) => { nextTick().then(() => { @@ -29,6 +44,13 @@ watch(orientations, (newVal) => { }); }); +/** + * Draws an image onto a given canvas element. + * + * @param canvas - The canvas element to draw the image on. + * @param image - The image file to be drawn. + * @return A promise that resolves when the image has been drawn to the canvas. + */ async function drawImageToCanvas(canvas: HTMLCanvasElement, image: ImageFile) { const context = canvas.getContext('2d'); if (!context) { @@ -56,6 +78,11 @@ async function loadImages() { imageInput.value.click(); } +/** + * Handles the escape event of the Main modal. Is used to stop the user from accidentally closing it. + * + * @param e - The event object representing the hide event. + */ function handleHide(e: BvTriggerableEvent) { if (disableHide.value) { e.preventDefault(); @@ -63,13 +90,70 @@ function handleHide(e: BvTriggerableEvent) { } /** + * Handles the logic for when an image is clicked by assigning the image to the corresponding + * orientation slot in the selectedImages object. * - * @param image + * @param image - The image that was clicked and needs to be assigned. + * @param direction - The direction (left, right, or center) to which the image should be assigned. */ -function imageClicked(image: ImageFile) {} +function imageClicked(image: orientationGuessResult, direction: Orientation) { + switch (direction) { + case Orientation.left: { + selectedImages.value.left = image; + break; + } + case Orientation.right: { + selectedImages.value.right = image; + break; + } + case Orientation.center: { + selectedImages.value.center = image; + break; + } + } +} + +function nextImage() { + // remove selected images + orientations.value = orientations.value.filter((value) => { + return ( + !( + selectedImages.value.left && + selectedImages.value.left.image.sha === value.image.sha && + selectedImages.value.left.orientation === Orientation.left + ) && + !( + selectedImages.value.right && + selectedImages.value.right.image.sha === value.image.sha && + selectedImages.value.right.orientation === Orientation.right + ) && + !( + selectedImages.value.center && + selectedImages.value.center.image.sha === value.image.sha && + selectedImages.value.center.orientation === Orientation.center + ) + ); + }); -function nextImage() {} + result.value.push(selectedImages.value); + + // clean up + selectedImages.value = { + left: null, + center: null, + right: null + }; + progress.value++; + if (progress.value === imageCount.value) { + disableHide.value = false; + confirmModal.value = true; + } +} + +/** + * Callback when the input component is clicked. Loads all marked files and guesses their orientation. + */ async function handleImageLoad() { if (!imageInput.value) { console.error('imageInput not found on change'); @@ -86,10 +170,20 @@ async function handleImageLoad() { ); processing.value = true; orientations.value = orientations.value.concat(await guessOrientation(import_images)); + imageCount.value = orientations.value.filter( + (value) => value.orientation === Orientation.center + ).length; + progress.value = 0; processing.value = false; } } +const save = () => { + const store = useAnnotationHistoryStore(); + store.merge(result.value); + imageLoadStore.showLoadModal = false; +}; + onMounted(() => { if (!imageInput.value) { console.error('imageInput not found on mount'); @@ -120,69 +214,92 @@ onBeforeUnmount(() => { size="lg" hide-footer scrollable + centered >
- -
-
- toggle disable hide -
-
- -
- -
-

Left

-
-
- - - +
+
+
+ +
+

Left

+
+
+ + + +
+
-
-
- -
-

Frontal

-
-
- - - + +
+

Frontal

+
+
+ + + +
+
-
-
- -
-

Right

-
-
- - - + +
+

Right

+
+
+ + + +
+
+
+

Press "Load" to get started

+
+
Finished
-

@@ -197,4 +314,5 @@ onBeforeUnmount(() => {
+ Confirm? diff --git a/src/components/Main/CentralCanvas.vue b/src/components/Main/CentralCanvas.vue index 7710a09..c2a1a0f 100644 --- a/src/components/Main/CentralCanvas.vue +++ b/src/components/Main/CentralCanvas.vue @@ -55,15 +55,13 @@ watch( () => annotationHistoryStore.selectedHistory, async (value) => { if (!value) return; - Editor.setBackgroundSource(value.file) - .then(() => { - Editor.center(); - Editor.draw(); - editors.value.forEach((editor) => { - editor.onBackgroundLoaded(); - }); - }) - .catch((reason) => console.error(reason)); + if (!value.file.center) return; + await Editor.setBackgroundSource(value.file.center.image.file); + Editor.center(); + Editor.draw(); + editors.value.forEach((editor) => { + editor.onBackgroundLoaded(); + }); } ); diff --git a/src/components/Main/ThumbnailGallery.vue b/src/components/Main/ThumbnailGallery.vue index f17924e..01ba921 100644 --- a/src/components/Main/ThumbnailGallery.vue +++ b/src/components/Main/ThumbnailGallery.vue @@ -1,27 +1,32 @@ diff --git a/src/components/Main/__tests__/CentralCanvas.spec.ts b/src/components/Main/__tests__/CentralCanvas.spec.ts index 6f91bd3..805d537 100644 --- a/src/components/Main/__tests__/CentralCanvas.spec.ts +++ b/src/components/Main/__tests__/CentralCanvas.spec.ts @@ -1,14 +1,27 @@ import { mount } from '@vue/test-utils'; -import { describe, it, vi, expect, beforeEach } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createPinia, setActivePinia } from 'pinia'; +import { Editor } from '../../../Editors/Editor'; +import { useAnnotationHistoryStore } from '../../../stores/annotationHistoryStore'; +import CentralCanvas from '../CentralCanvas.vue'; +import { FileAnnotationHistory } from '../../../cache/fileAnnotationHistory'; vi.mock('@/Editors/FaceMeshEditor'); vi.mock('@/Editors/BackgroundDrawer'); vi.mock('@/Editors/Editor'); -import { Editor } from '../../../Editors/Editor'; -import { useAnnotationHistoryStore } from '../../../stores/annotationHistoryStore'; -import CentralCanvas from '../CentralCanvas.vue'; +const mockData = { + center: { + image: { + file: new File([''], 'mock.png', { + type: 'image/png' + }) + }, + mesh: [] + }, + left: null, + right: null +}; describe('AnnotationCanvas.vue', () => { let wrapper; @@ -24,12 +37,10 @@ describe('AnnotationCanvas.vue', () => { it('should update the background source when selectedHistory changes', async () => { const annotationHistoryStore = useAnnotationHistoryStore(); - - const mockFile = { file: 'test-image.png' }; - annotationHistoryStore.selectedHistory = mockFile; + annotationHistoryStore.selectedHistory = new FileAnnotationHistory(mockData, 25); await wrapper.vm.$nextTick(); - expect(Editor.setBackgroundSource).toHaveBeenCalledWith(mockFile.file); + expect(Editor.setBackgroundSource).toHaveBeenCalledWith(mockData.center.image.file); }); }); diff --git a/src/components/Main/__tests__/ThumbnailGallery.spec.ts b/src/components/Main/__tests__/ThumbnailGallery.spec.ts index f6bb96c..d928be0 100644 --- a/src/components/Main/__tests__/ThumbnailGallery.spec.ts +++ b/src/components/Main/__tests__/ThumbnailGallery.spec.ts @@ -7,6 +7,7 @@ import { FileAnnotationHistory } from '../../../cache/fileAnnotationHistory'; import { Point2D } from '../../../graph/point2d'; import { useAnnotationHistoryStore } from '../../../stores/annotationHistoryStore'; import { ImageFile } from '../../../imageFile'; +import type { MultipleViewImage } from '../../ImageLoadModal.vue'; // Define a function to convert ArrayBuffer to Blob function arrayBufferToBlob(buffer: ArrayBuffer, type: string) { @@ -27,17 +28,26 @@ const arrayBuffer = Uint8Array.from(fileBuffer).buffer; const blob = arrayBufferToBlob(arrayBuffer, 'text/plain'); // Mock data -const mockData = { - file: null +const mockData: MultipleViewImage = { + center: { + image: { + file: new File([''], 'mock.png', { + type: 'image/png' + }) + }, + mesh: [] + }, + left: null, + right: null }; let store = null; beforeAll(async () => { - mockData.file = await ImageFile.create(blobToFile(blob, 'test.png')); + mockData.center.image = await ImageFile.create(blobToFile(blob, 'test.png')); setActivePinia(createPinia()); store = useAnnotationHistoryStore(); - store.histories.push(new FileAnnotationHistory(mockData.file, 25)); + store.histories.push(new FileAnnotationHistory(mockData, 25)); }); describe('ThumbnailGallery', () => { @@ -49,7 +59,7 @@ describe('ThumbnailGallery', () => { const thumbnailContainer = wrapper.find('#thumbnail-0'); expect(thumbnailContainer.exists()).toBe(true); - await thumbnailContainer.trigger('click', mockData.file); - expect(store.selectedHistory.file).toEqual(mockData.file); + await thumbnailContainer.trigger('click', mockData); + expect(store.selectedHistory.file).toEqual(mockData); }); }); diff --git a/src/components/Navbar/EditButtons.vue b/src/components/Navbar/EditButtons.vue index 5e246d4..c6bd1dc 100644 --- a/src/components/Navbar/EditButtons.vue +++ b/src/components/Navbar/EditButtons.vue @@ -25,7 +25,8 @@ function reset(): boolean { function runDetection() { const history = annotationHistoryStore.selectedHistory; if (!history) return; - modelStore.model?.detect(history.file).then((graph) => { + if (!history.file.center) return; + modelStore.model?.detect(history.file.center.image).then((graph) => { if (graph === null) { return; } diff --git a/src/components/Navbar/LoadSaveActions.vue b/src/components/Navbar/LoadSaveActions.vue index 8bd3f60..b4eaae0 100644 --- a/src/components/Navbar/LoadSaveActions.vue +++ b/src/components/Navbar/LoadSaveActions.vue @@ -90,12 +90,13 @@ function collectAnnotation() { } h.markAsSent(); const graph = h.get(); - const fileName = h.file.file.name; + if (!h.file.center) return; + const fileName = h.file.center?.image.file.name; result[fileName] = {}; if (graph) { result[fileName]['points'] = [graph.toDictArray()]; - result[fileName]['sha256'] = h.file.sha; + result[fileName]['sha256'] = h.file.center?.image.sha; } }); return result; diff --git a/src/components/ThumbnailContainer.vue b/src/components/ThumbnailContainer.vue index f3657ce..2e6fb19 100644 --- a/src/components/ThumbnailContainer.vue +++ b/src/components/ThumbnailContainer.vue @@ -3,6 +3,8 @@ import { ref, onMounted, watch, computed } from 'vue'; import { SaveStatus } from '@/enums/saveStatus'; import { FileAnnotationHistory } from '@/cache/fileAnnotationHistory'; import { Point2D } from '@/graph/point2d'; +import { imageFromFile } from '@/util/imageFromFile'; +import type { MultipleViewImage } from '@/components/ImageLoadModal.vue'; const props = defineProps({ history: { @@ -24,7 +26,10 @@ const image = new Image(); onMounted(() => { image.onload = () => draw(); - image.src = props.history.file.html; + if (!props.history.file.center) return; + imageFromFile(props.history.file.center?.image.file).then((r) => { + image.src = r; + }); }); const draw = () => { @@ -77,9 +82,15 @@ let iconDescription = computed(() => { }); watch( - () => props.history.file.html, - (newSrc) => { - image.src = newSrc; + () => props.history.file, + (newSrc: MultipleViewImage) => { + if (!newSrc.center) { + console.error('File render canceled'); + return; + } + imageFromFile(newSrc.center?.image.file).then((r) => { + image.src = r; + }); } ); diff --git a/src/enums/orientation.ts b/src/enums/orientation.ts index 43d2ec6..22fe28c 100644 --- a/src/enums/orientation.ts +++ b/src/enums/orientation.ts @@ -1,5 +1,5 @@ export enum Orientation { - front, + center, left, right, unknown diff --git a/src/graph/graph.ts b/src/graph/graph.ts index 5ae361d..a309575 100644 --- a/src/graph/graph.ts +++ b/src/graph/graph.ts @@ -1,6 +1,9 @@ +import { FaceLandmarker, type NormalizedLandmark } from '@mediapipe/tasks-vision'; import { Point2D } from './point2d'; import type { ModelApi } from '@/model/modelApi'; import type { ImageFile } from '@/imageFile'; +import { findNeighbourPointIds } from '@/graph/face_landmarks_features'; +import { Point3D } from '@/graph/point3d'; /** * Represents a graph of points in a 2D space. @@ -57,6 +60,25 @@ export class Graph

{ ); } + static fromMesh

(mesh: NormalizedLandmark[]): Graph

{ + const points: P[] = mesh + .map((dict, idx) => { + const ids = Array.from( + findNeighbourPointIds(idx, FaceLandmarker.FACE_LANDMARKS_TESSELATION, 1) + ); + return new Point3D(idx, dict.x, dict.y, dict.z, ids); + }) + .map((point) => point as unknown as P) + // filter out the iris markings + .filter((point) => { + return ![ + ...FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS.map((con) => con.start), + ...FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS.map((con) => con.start) + ].includes(point.id); + }); + return new Graph

(points); + } + /** * Retrieves a point from the graph by its ID. * @param {number} id - The ID of the point. diff --git a/src/model/mediapipe.ts b/src/model/mediapipe.ts index 9238b7a..d1141dd 100644 --- a/src/model/mediapipe.ts +++ b/src/model/mediapipe.ts @@ -4,10 +4,8 @@ import { FilesetResolver } from '@mediapipe/tasks-vision'; import type { ModelApi } from './modelApi'; -import { findNeighbourPointIds } from '@/graph/face_landmarks_features'; import { Graph } from '@/graph/graph'; import { Point2D } from '@/graph/point2d'; -import { Point3D } from '@/graph/point3d'; import { ModelType } from '@/enums/modelType'; import type { ImageFile } from '@/imageFile'; @@ -50,7 +48,7 @@ export class MediapipeModel implements ModelApi { if (!result) { reject(new Error('Face(s) could not be detected!')); } - const res = MediapipeModel.processResult(result as FaceLandmarkerResult); + const res = Graph.fromMesh((result as FaceLandmarkerResult).faceLandmarks[0]); if (!res) { reject(new Error('Face(s) could not be detected!')); } @@ -60,34 +58,6 @@ export class MediapipeModel implements ModelApi { }); } - private static processResult(result: FaceLandmarkerResult) { - const graphs = result.faceLandmarks - .map((landmarks) => - landmarks - .map((dict, idx) => { - const ids = Array.from( - findNeighbourPointIds(idx, FaceLandmarker.FACE_LANDMARKS_TESSELATION, 1) - ); - return new Point3D(idx, dict.x, dict.y, dict.z, ids); - }) - .map((point) => point as Point2D) - ) - // filter out the iris markings - .map((landmarks) => { - landmarks = landmarks.filter((point) => { - return ![ - ...FaceLandmarker.FACE_LANDMARKS_LEFT_IRIS.map((con) => con.start), - ...FaceLandmarker.FACE_LANDMARKS_RIGHT_IRIS.map((con) => con.start) - ].includes(point.id); - }); - return new Graph(landmarks); - }); - if (graphs) { - return graphs[0]; - } - return null; - } - async uploadAnnotations(_: string): Promise { return Promise.resolve(); } diff --git a/src/stores/__tests__/annotationHistoryStore.spec.ts b/src/stores/__tests__/annotationHistoryStore.spec.ts index b106af3..e89240b 100644 --- a/src/stores/__tests__/annotationHistoryStore.spec.ts +++ b/src/stores/__tests__/annotationHistoryStore.spec.ts @@ -1,45 +1,35 @@ import { test, expect, beforeEach } from 'vitest'; import { createPinia, setActivePinia } from 'pinia'; -import { Graph } from '../../graph/graph'; -import { ModelType } from '../../enums/modelType'; -import { Point2D } from '../../graph/point2d'; -import { type ModelApi } from '../../model/modelApi'; import { useAnnotationHistoryStore } from '../annotationHistoryStore'; +import type { MultipleViewImage } from '../../components/ImageLoadModal.vue'; beforeEach(() => { setActivePinia(createPinia()); }); -class MockApi implements ModelApi { - async detect(_: File): Promise | null> { - return null; - } - - type(): ModelType { - return ModelType.custom; - } - - async uploadAnnotations(_: string): Promise { - return; - } -} - -const mockFile = new File([''], 'mock.png', { - type: 'image/png' -}); - -const mockApi = new MockApi(); +const mockFile: MultipleViewImage = { + center: { + image: { + file: new File([''], 'mock.png', { + type: 'image/png' + }) + }, + mesh: [] + }, + left: null, + right: null +}; test('Test store is initially empty', async () => { const store = useAnnotationHistoryStore(); expect(store.empty()).toEqual(true); - await store.add(mockFile, mockApi); + await store.add(mockFile); expect(store.empty()).toEqual(false); }); test('Test adding', async () => { const store = useAnnotationHistoryStore(); - await store.add(mockFile, mockApi); + await store.add(mockFile); expect(store.histories.length).toEqual(1); expect(store.selectedHistory).not.toBeNull(); @@ -47,12 +37,9 @@ test('Test adding', async () => { test('Test find function', async () => { const store = useAnnotationHistoryStore(); - const mockFile = new File([''], 'mock.png', { - type: 'image/png' - }); - await store.add(mockFile, mockApi); + await store.add(mockFile); - const found = store.find('mock.png', store.histories[0].file.sha); + const found = store.find('mock.png', store.histories[0].file.center.image.sha); expect(found).toEqual(store.selectedHistory); }); diff --git a/src/stores/annotationHistoryStore.ts b/src/stores/annotationHistoryStore.ts index 1c74642..7a23003 100644 --- a/src/stores/annotationHistoryStore.ts +++ b/src/stores/annotationHistoryStore.ts @@ -1,10 +1,9 @@ import { defineStore } from 'pinia'; import { FileAnnotationHistory } from '@/cache/fileAnnotationHistory'; import { Point2D } from '@/graph/point2d'; -import { ImageFile } from '@/imageFile'; -import { Graph } from '@/graph/graph'; -import type { ModelApi } from '@/model/modelApi'; import { SaveStatus } from '@/enums/saveStatus'; +import type { MultipleViewImage } from '@/components/ImageLoadModal.vue'; +import { Graph } from '@/graph/graph'; export const useAnnotationHistoryStore = defineStore({ id: 'annotationHistory', @@ -18,13 +17,15 @@ export const useAnnotationHistoryStore = defineStore({ }), actions: { - async add(file: File, api: ModelApi) { - const imageFile = await ImageFile.create(file); - const history = new FileAnnotationHistory(imageFile, 25); - const anno = await Graph.detect(api, imageFile); - if (anno) { - history.add(anno); + async add(image: MultipleViewImage) { + if (!image.center?.image.file) { + return; + } + if (!image.center.mesh) { + return; } + const history = new FileAnnotationHistory(image, 25); + history.add(Graph.fromMesh(image.center.mesh)); this.histories.push(history); if (!this.selectedHistory) { this.selectedHistory = history; @@ -35,10 +36,22 @@ export const useAnnotationHistoryStore = defineStore({ }, find(fileName: string, sha256: string): FileAnnotationHistory { return this.histories.find( - (history) => history.file.file.name === fileName && history.file.sha === sha256 + (history) => + (history.file.left?.image.file.name === fileName && + history.file.left.image.sha === sha256) || + (history.file.right?.image.file.name === fileName && + history.file.right.image.sha === sha256) || + (history.file.center?.image.file.name === fileName && + history.file.center.image.sha === sha256) ) as FileAnnotationHistory; }, + async merge(data: MultipleViewImage[]) { + data.forEach((value) => { + this.add(value); + }); + }, + /** * Returns any files with pending changes */ diff --git a/src/util/orientationGuesser.ts b/src/util/orientationGuesser.ts index 55d3e95..09b5f75 100644 --- a/src/util/orientationGuesser.ts +++ b/src/util/orientationGuesser.ts @@ -5,6 +5,7 @@ import type { ImageFile } from '@/imageFile'; export type orientationGuessResult = { image: ImageFile; orientation: Orientation; + mesh: NormalizedLandmark[]; }; export async function guessOrientation(images: ImageFile[]): Promise { @@ -17,7 +18,7 @@ export async function guessOrientation(images: ImageFile[]): Promise= 60 && turn <= 120) { - return Orientation.front; + return Orientation.center; } return Orientation.unknown; diff --git a/static/css/standard.css b/static/css/standard.css index 59ddb78..162348e 100644 --- a/static/css/standard.css +++ b/static/css/standard.css @@ -26,11 +26,6 @@ a[shortcut]:after { content: 'CTRL + O'; } -.checkmark-container { - width: 2.0rem; - height: 2.0rem; -} - .overlap-container { display: grid; From 155e8369b36d9ec48d9dcba09e34094a4b7b0962 Mon Sep 17 00:00:00 2001 From: Marc-Lorenz <169900493+Marc-Lorenz@users.noreply.github.com> Date: Wed, 11 Dec 2024 15:10:59 +0100 Subject: [PATCH 05/20] fix build and remove parsing images to memory --- src/cache/fileAnnotationHistory.ts | 96 ++++++++++--------- src/components/ImageLoadModal.vue | 14 ++- src/components/Main/ThumbnailGallery.vue | 10 +- .../Main/__tests__/CentralCanvas.spec.ts | 3 +- .../Main/__tests__/ThumbnailGallery.spec.ts | 5 +- src/components/ThumbnailContainer.vue | 15 +-- src/graph/graph.ts | 37 +++---- src/imageFile.ts | 30 +----- src/interface/multiple_view_image.ts | 9 ++ src/model/mediapipe.ts | 54 +++++------ src/model/modelApi.ts | 3 +- src/model/webservice.ts | 87 ++++++++--------- .../__tests__/annotationHistoryStore.spec.ts | 15 ++- src/stores/annotationHistoryStore.ts | 2 +- src/stores/imageLoadStore.ts | 2 +- src/util/orientationGuesser.ts | 2 +- 16 files changed, 191 insertions(+), 193 deletions(-) create mode 100644 src/interface/multiple_view_image.ts diff --git a/src/cache/fileAnnotationHistory.ts b/src/cache/fileAnnotationHistory.ts index ee9c20c..06cd454 100644 --- a/src/cache/fileAnnotationHistory.ts +++ b/src/cache/fileAnnotationHistory.ts @@ -1,7 +1,8 @@ import { Point2D } from '@/graph/point2d'; import { Graph } from '@/graph/graph'; import { SaveStatus } from '@/enums/saveStatus'; -import type { MultipleViewImage } from '@/components/ImageLoadModal.vue'; + +import type { MultipleViewImage } from '@/interface/multiple_view_image'; export interface PointData { deleted: boolean; @@ -10,6 +11,7 @@ export interface PointData { z?: number; id: number; } + export interface GraphData { points?: PointData[][]; sha256?: string; @@ -22,10 +24,8 @@ export interface GraphData { */ export class FileAnnotationHistory { private readonly cacheSize: number; - private _history: Graph[] = []; private currentHistoryIndex: number = 0; private readonly _file: MultipleViewImage; - private _status: SaveStatus; /** * Creates a new FileAnnotationHistory instance. @@ -38,6 +38,16 @@ export class FileAnnotationHistory { this._status = SaveStatus.unedited; } + private _status: SaveStatus; + + get status(): SaveStatus { + return this._status; + } + + set status(value: SaveStatus) { + this._status = value; + } + /** * Gets the associated file. * @returns - The file associated with the annotations. @@ -46,13 +56,17 @@ export class FileAnnotationHistory { return this._file; } - get status(): SaveStatus { - return this._status; + /** + * returns the serialized data of the history, included file sha. + */ + get graphData(): GraphData { + return { + points: this.toDictArray, + sha256: this.file.center?.image.sha + }; } - set status(value: SaveStatus) { - this._status = value; - } + private _history: Graph[] = []; protected get history() { return this._history; @@ -67,13 +81,35 @@ export class FileAnnotationHistory { } /** - * returns the serialized data of the history, included file sha. + * Parses the provided parsed json data into a history. Expects the latest element to be at the end of the array. + * @param json the parsed data + * @param file the image file, to check the sha + * @param newObject a function to create a single Point, used to mitigate the templating. */ - get graphData(): GraphData { - return { - points: this.toDictArray, - sha256: this.file.center?.image.sha - }; + static fromJson( + json: GraphData, + file: MultipleViewImage, + newObject: (id: number, neighbors: number[]) => T + ): FileAnnotationHistory | null { + const h = new FileAnnotationHistory(file); + // skip files without annotation + if (Object.keys(json).length == 0) { + return null; + } + const sha = json.sha256; + if (!sha) throw new Error('Missing from API!'); + if (sha !== file.center?.image.sha) throw new Error('Mismatching sha sent from API!'); + let graphs = json.points; + if (!graphs) throw new Error("Didn't get any points from API!"); + /* backward compatibility if the file contains the old Points2D[] format instead of Points2D[][] */ + if (!Array.isArray(graphs[0])) { + graphs = [graphs as unknown as PointData[]]; + } + graphs.forEach((unparsedGraph) => { + const graph: Graph = Graph.fromJson(unparsedGraph, newObject); + h.add(graph); + }); + return h; } /** @@ -172,36 +208,4 @@ export class FileAnnotationHistory { markAsSent(): void { this._status = SaveStatus.unedited; } - - /** - * Parses the provided parsed json data into a history. Expects the latest element to be at the end of the array. - * @param json the parsed data - * @param file the image file, to check the sha - * @param newObject a function to create a single Point, used to mitigate the templating. - */ - static fromJson( - json: GraphData, - file: MultipleViewImage, - newObject: (id: number, neighbors: number[]) => T - ): FileAnnotationHistory | null { - const h = new FileAnnotationHistory(file); - // skip files without annotation - if (Object.keys(json).length == 0) { - return null; - } - const sha = json.sha256; - if (!sha) throw new Error('Missing from API!'); - if (sha !== file.center?.image.sha) throw new Error('Mismatching sha sent from API!'); - let graphs = json.points; - if (!graphs) throw new Error("Didn't get any points from API!"); - /* backward compatibility if the file contains the old Points2D[] format instead of Points2D[][] */ - if (!Array.isArray(graphs[0])) { - graphs = [graphs as unknown as PointData[]]; - } - graphs.forEach((unparsedGraph) => { - const graph: Graph = Graph.fromJson(unparsedGraph, newObject); - h.add(graph); - }); - return h; - } } diff --git a/src/components/ImageLoadModal.vue b/src/components/ImageLoadModal.vue index be17808..66605ad 100644 --- a/src/components/ImageLoadModal.vue +++ b/src/components/ImageLoadModal.vue @@ -7,6 +7,8 @@ import { guessOrientation, type orientationGuessResult } from '@/util/orientatio import { Orientation } from '@/enums/orientation'; import { imageFromFile } from '@/util/imageFromFile'; import { useAnnotationHistoryStore } from '@/stores/annotationHistoryStore'; +import { ThreeDimView } from '@/enums/threeDimView'; +import type { MultipleViewImage } from '@/interface/multiple_view_image'; const imageLoadStore = useImageLoadStore(); const disableHide = ref(true); @@ -19,16 +21,11 @@ const processing = ref(false); const confirmModal = ref(false); const result = ref([]); -export interface MultipleViewImage { - left: orientationGuessResult | null; - center: orientationGuessResult | null; - right: orientationGuessResult | null; -} - const selectedImages = ref({ left: null, center: null, - right: null + right: null, + selected: ThreeDimView.center }); watch(orientations, (newVal) => { @@ -141,7 +138,8 @@ function nextImage() { selectedImages.value = { left: null, center: null, - right: null + right: null, + selected: ThreeDimView.center }; progress.value++; diff --git a/src/components/Main/ThumbnailGallery.vue b/src/components/Main/ThumbnailGallery.vue index 21e9716..900f3bf 100644 --- a/src/components/Main/ThumbnailGallery.vue +++ b/src/components/Main/ThumbnailGallery.vue @@ -1,12 +1,12 @@ -