From e5230850e0c1933bec13f21d1658986ed02cf24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Pumar?= Date: Wed, 11 Sep 2024 11:10:53 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=E2=9C=A8=20Add=20POC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../container/fields/RecordFields.vue | 18 +- .../SpanAnnotationImageField.vue | 48 ++++ .../useSpanAnnotationImageField.ts | 213 ++++++++++++++++++ 3 files changed, 264 insertions(+), 15 deletions(-) create mode 100644 argilla-frontend/components/features/annotation/container/fields/image-annotation/SpanAnnotationImageField.vue create mode 100644 argilla-frontend/components/features/annotation/container/fields/image-annotation/useSpanAnnotationImageField.ts diff --git a/argilla-frontend/components/features/annotation/container/fields/RecordFields.vue b/argilla-frontend/components/features/annotation/container/fields/RecordFields.vue index c73974d6c9..34fa695aba 100644 --- a/argilla-frontend/components/features/annotation/container/fields/RecordFields.vue +++ b/argilla-frontend/components/features/annotation/container/fields/RecordFields.vue @@ -13,29 +13,17 @@ :class="[isImageType ? 'fields__container--image' : '']" :key="id" > - - - + + diff --git a/argilla-frontend/components/features/annotation/container/fields/image-annotation/useSpanAnnotationImageField.ts b/argilla-frontend/components/features/annotation/container/fields/image-annotation/useSpanAnnotationImageField.ts new file mode 100644 index 0000000000..3762dabb87 --- /dev/null +++ b/argilla-frontend/components/features/annotation/container/fields/image-annotation/useSpanAnnotationImageField.ts @@ -0,0 +1,213 @@ +import { onMounted } from "vue"; +import { Question } from "~/v1/domain/entities/question/Question"; +import { SpanQuestionAnswer } from "~/v1/domain/entities/question/QuestionAnswer"; + +interface SpanAnnotationImageFieldProps { + imageURL: string; + spanQuestion: Question; +} + +type EntitySelected = { + id: string; + value: string; + text: string; + color: string; +}; +type UISpan = { + entity: EntitySelected; + id: string; + x: number; + y: number; + width: number; + height: number; +}; + +export const useSpanAnnotationImageField = ({ + imageURL, + spanQuestion, +}: SpanAnnotationImageFieldProps) => { + const img = new Image(); + let annotations: UISpan[] = + JSON.parse(localStorage.getItem("annotations")) || []; + let currentRect: UISpan = null; + let entitySelected: EntitySelected = null; + + let isDrawing = false; + let canvas; + let context; + let startX; + let startY; + + onMounted(() => { + canvas = document.getElementById("canvas") as HTMLCanvasElement; + context = canvas.getContext("2d"); + + img.src = imageURL; + + img.onload = () => { + canvas.width = img.width; + canvas.height = img.height; + context.drawImage(img, 0, 0); + drawAnnotations(); + }; + + canvas.addEventListener("mousedown", (e) => { + startX = e.offsetX; + startY = e.offsetY; + isDrawing = true; + const answer = spanQuestion.answer as SpanQuestionAnswer; + entitySelected = answer.options.find((e) => e.isSelected); + if (!entitySelected) return; + + currentRect = { + x: startX, + y: startY, + width: 0, + height: 0, + entity: { + ...entitySelected, + }, + id: createID(), + }; + }); + + canvas.addEventListener("mousemove", (e) => { + if (isDrawing) { + const currentX = e.offsetX; + const currentY = e.offsetY; + currentRect.width = currentX - startX; + currentRect.height = currentY - startY; + + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(img, 0, 0); + + drawAnnotations(); + + drawRectangle(currentRect); + } + }); + + canvas.addEventListener("mouseup", () => { + isDrawing = false; + + addNewAnnotation(currentRect); + + drawAnnotations(); + + saveAnnotations(); + }); + + canvas.addEventListener("mouseout", () => { + isDrawing = false; + }); + }); + + const removeAnnotation = (rect) => { + context.clearRect(0, 0, canvas.width, canvas.height); + context.drawImage(img, 0, 0); + + annotations = annotations.filter((r) => r.id !== rect.id); + saveAnnotations(); + + drawAnnotations(); + }; + + const addNewAnnotation = (rect) => { + if (rect.width === 0 || rect.height === 0) { + return; + } + + if (rect.width < 0) { + rect.x += rect.width; + rect.width *= -1; + } + + if (rect.height < 0) { + rect.y += rect.height; + rect.height *= -1; + } + + if (!entitySelected) return; + + annotations = [ + ...annotations, + { + id: createID(), + entity: { + ...entitySelected, + }, + ...rect, + }, + ]; + + saveAnnotations(); + }; + + const drawAnnotations = () => { + clearButtons(); + clearText(); + annotations.forEach((rect) => { + drawRectangle(rect); + + drawDeleteButton(rect); + drawText(rect); + }); + }; + + const saveAnnotations = () => { + localStorage.setItem("annotations", JSON.stringify(annotations)); + }; + + const createID = () => { + return Math.random().toString(36).substring(2, 9); + }; + + const drawRectangle = (rect: UISpan) => { + context.beginPath(); + context.rect(rect.x, rect.y, rect.width, rect.height); + context.strokeStyle = rect.entity.color; + context.lineWidth = 2; + context.stroke(); + }; + + const drawDeleteButton = (rect: UISpan) => { + const btn = document.createElement("button"); + btn.className = "delete-btn"; + btn.innerText = "X"; + btn.style.left = `${canvas.offsetLeft + rect.x + rect.width - 10}px`; + btn.style.top = `${canvas.offsetTop + rect.y - 10}px`; + btn.onclick = () => { + removeAnnotation(rect); + }; + + document.getElementById("span-image-field-container").appendChild(btn); + }; + + const drawText = (rect: UISpan) => { + const p = document.createElement("p"); + p.className = "annotation-text"; + p.innerText = rect.entity.text; + p.style.left = `${canvas.offsetLeft + rect.x}px`; + p.style.top = `${canvas.offsetTop + rect.y + rect.height + 5}px`; + document.getElementById("span-image-field-container").appendChild(p); + }; + + const clearButtons = () => { + const buttons = document.querySelectorAll(".delete-btn"); + buttons.forEach((button) => button.remove()); + }; + + const clearText = () => { + const texts = document.querySelectorAll(".annotation-text"); + texts.forEach((text) => text.remove()); + }; + + // const clearAll = () => { + // annotations.forEach((rect) => { + // removeAnnotation(rect); + // }); + + // clearText(); + // clearButtons(); + // }; +}; From 983129e736998c5b3a112335c885b581e6eabbd2 Mon Sep 17 00:00:00 2001 From: Leire Aguirre Date: Wed, 11 Sep 2024 12:48:42 +0200 Subject: [PATCH 2/2] show raw image size and canvas --- .../fields/image-annotation/SpanAnnotationImageField.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/argilla-frontend/components/features/annotation/container/fields/image-annotation/SpanAnnotationImageField.vue b/argilla-frontend/components/features/annotation/container/fields/image-annotation/SpanAnnotationImageField.vue index 01ab90efca..1d6143a267 100644 --- a/argilla-frontend/components/features/annotation/container/fields/image-annotation/SpanAnnotationImageField.vue +++ b/argilla-frontend/components/features/annotation/container/fields/image-annotation/SpanAnnotationImageField.vue @@ -24,8 +24,12 @@ export default {