(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 @@
-
-
+
Images
@@ -53,9 +53,9 @@ function selectThumbnail(file: MultipleViewImage): void {
diff --git a/src/components/Main/__tests__/CentralCanvas.spec.ts b/src/components/Main/__tests__/CentralCanvas.spec.ts
index 1dafb9b..4192395 100644
--- a/src/components/Main/__tests__/CentralCanvas.spec.ts
+++ b/src/components/Main/__tests__/CentralCanvas.spec.ts
@@ -5,7 +5,8 @@ import { Editor } from '../../../Editors/Editor';
import { useAnnotationHistoryStore } from '../../../stores/annotationHistoryStore';
import CentralCanvas from '../CentralCanvas.vue';
import { FileAnnotationHistory } from '../../../cache/fileAnnotationHistory';
-import { MultipleViewImage } from '../../ImageLoadModal.vue';
+
+import { MultipleViewImage } from '../../../interface/multiple_view_image';
vi.mock('@/Editors/FaceMeshEditor');
vi.mock('@/Editors/BackgroundDrawer');
diff --git a/src/components/Main/__tests__/ThumbnailGallery.spec.ts b/src/components/Main/__tests__/ThumbnailGallery.spec.ts
index d928be0..f299bee 100644
--- a/src/components/Main/__tests__/ThumbnailGallery.spec.ts
+++ b/src/components/Main/__tests__/ThumbnailGallery.spec.ts
@@ -1,13 +1,14 @@
import * as fs from 'node:fs';
import { mount } from '@vue/test-utils';
-import { describe, it, expect, beforeAll } from 'vitest';
+import { beforeAll, describe, expect, it } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import ThumbnailGallery from '../ThumbnailGallery.vue';
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';
+
+import { MultipleViewImage } from '../../../interface/multiple_view_image';
// Define a function to convert ArrayBuffer to Blob
function arrayBufferToBlob(buffer: ArrayBuffer, type: string) {
diff --git a/src/components/ThumbnailContainer.vue b/src/components/ThumbnailContainer.vue
index 2f70965..7a387c5 100644
--- a/src/components/ThumbnailContainer.vue
+++ b/src/components/ThumbnailContainer.vue
@@ -1,10 +1,11 @@
-
-
-
+
+