From 3f89b65e438da4110d5ad38a377f12d8c85a13d1 Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:11:54 +0200 Subject: [PATCH 1/6] Added auto-resizing option for the synthetic image detection --- .../tools/SyntheticImageDetection/index.jsx | 137 ++++++++++++++---- src/components/Shared/Utils/imageUtils.jsx | 95 ++++++++++++ src/workers/resizeImageWorker.js | 13 ++ 3 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 src/components/Shared/Utils/imageUtils.jsx create mode 100644 src/workers/resizeImageWorker.js diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx index 21989f05e..2932beee5 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { resetSyntheticImageDetectionImage, @@ -16,6 +16,7 @@ import { Grid, LinearProgress, Stack, + Switch, } from "@mui/material"; import useMyStyles from "../../../Shared/MaterialUiStyles/useMyStyles"; @@ -31,6 +32,10 @@ import StringFileUploadField from "../../../Shared/StringFileUploadField"; import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils"; import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms"; import { useLocation } from "react-router-dom"; +import FormGroup from "@mui/material/FormGroup"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import { ROLES } from "../../../../constants/roles"; +// import worker from "../../../../workers/resizeImageWorker"; const SyntheticImageDetection = () => { const location = useLocation(); @@ -59,12 +64,54 @@ const SyntheticImageDetection = () => { const [imageType, setImageType] = useState(undefined); + const [autoResizeLocalFile, setAutoResizeLocalFile] = useState(true); + const dispatch = useDispatch(); const IMAGE_FROM = { URL: "url", UPLOAD: "local", }; + // const worker = new Worker("../../../../workers/resizeImageWorker.js"); + + const workerRef = useRef(null); + + useEffect(() => { + workerRef.current = new Worker( + new URL("../../../../workers/resizeImageWorker", import.meta.url), + ); + + return () => { + workerRef.current.terminate(); + }; + }, []); + + /** + * + * @param image + */ + const resizeImageWithWorker = (image) => { + return new Promise((resolve, reject) => { + const workerInstance = new Worker( + new URL("../../../../workers/resizeImageWorker", import.meta.url), + ); + workerInstance.postMessage(image); + + //let res; + + workerInstance.onerror = function (e) { + reject(e.error); + }; + + workerInstance.onmessage = function (e) { + console.log(e); + resolve(e.data); + // res = e.data; + }; + }); + + // return res; + }; const getSyntheticImageScores = async ( url, @@ -249,6 +296,14 @@ const SyntheticImageDetection = () => { * @returns {Promise} */ const handleSubmit = async (url) => { + const processedFile = autoResizeLocalFile + ? await resizeImageWithWorker(imageFile) + : imageFile; + + if (autoResizeLocalFile && processedFile) { + setImageFile(processedFile); + } + dispatch(resetSyntheticImageDetectionImage()); const urlInput = url ? url : input; const type = @@ -256,7 +311,13 @@ const SyntheticImageDetection = () => { ? IMAGE_FROM.URL : IMAGE_FROM.UPLOAD; - await getSyntheticImageScores(urlInput, true, dispatch, type, imageFile); + await getSyntheticImageScores( + urlInput, + true, + dispatch, + type, + processedFile, + ); }; useEffect(() => { @@ -298,6 +359,10 @@ const SyntheticImageDetection = () => { } }, [imageFile, input, result]); + const toggleAutoResizeLocalFile = () => { + setAutoResizeLocalFile((prev) => !prev); + }; + return ( { /> -
- - - - {isLoading && ( - - - - )} + +
+ + + + {role.includes(ROLES.EXTRA_FEATURE) && ( + + + } + label="Auto resize local image if too large" + /> + + )} + + {isLoading && ( + + + + )} +
diff --git a/src/components/Shared/Utils/imageUtils.jsx b/src/components/Shared/Utils/imageUtils.jsx new file mode 100644 index 000000000..18b37d960 --- /dev/null +++ b/src/components/Shared/Utils/imageUtils.jsx @@ -0,0 +1,95 @@ +/** + * Converts an ImageData object to File + * @param imageData {ImageData} + * @param fileName {string} + * @param imageType {string} + * @returns {Promise} + */ +const imageDataToFile = async (imageData, fileName, imageType) => { + const offscreenCanvas = new OffscreenCanvas( + imageData.width, + imageData.height, + ); + + //1. Create an OffscreenCanvas + const ctx = offscreenCanvas.getContext("2d"); + ctx.putImageData(imageData, 0, 0); + + //2. Convert the OffscreenCanvas to a Blob + const blob = await offscreenCanvas.convertToBlob({ type: imageType }); + if (!blob) { + return new Error("OffscreenCanvas to Blob conversion failed"); + } + + //3. Convert the Blob to a File + const file = new File([blob], fileName, { type: blob.type }, imageType); + + return file; +}; + +/** + * + * @param imageData {File | Blob} + * @returns {Promise} + */ +export const resizeImage = async (imageData) => { + const MAX_PIXEL_SIZE = 2073599; // < 2Mpx + + // Create ImageBitmap from image data + const imageBitmap = await createImageBitmap(imageData); + + // Check if the image exceeds the max pixel size + const originalPixelSize = imageBitmap.width * imageBitmap.height; + + if (originalPixelSize <= MAX_PIXEL_SIZE) { + // If the image is within the max pixel size, return as is + return imageData; + } + + // Calculate new dimensions maintaining the aspect ratio + const aspectRatio = imageBitmap.width / imageBitmap.height; + let newWidth, newHeight; + + if (aspectRatio > 1) { + newWidth = Math.sqrt(MAX_PIXEL_SIZE * aspectRatio); + newHeight = newWidth / aspectRatio; + } else { + newHeight = Math.sqrt(MAX_PIXEL_SIZE / aspectRatio); + newWidth = newHeight * aspectRatio; + } + + // Resize the image using an OffscreenCanvas (or fallback to regular canvas) + const resizedImageData = await resizeUsingCanvas( + imageBitmap, + newWidth, + newHeight, + ); + + return imageDataToFile( + resizedImageData, + imageData.name ?? "image", + imageData.type, + ); +}; + +// Helper function to resize the image using a canvas +const resizeUsingCanvas = async (imageBitmap, newWidth, newHeight) => { + // Check if OffscreenCanvas is available + if (typeof OffscreenCanvas !== "undefined") { + const offscreenCanvas = new OffscreenCanvas(newWidth, newHeight); + const context = offscreenCanvas.getContext("2d"); + context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); + + // Get ImageData from the OffscreenCanvas + return context.getImageData(0, 0, newWidth, newHeight); + } else { + // Fallback to regular canvas (only necessary in rare cases) + const canvas = document.createElement("canvas"); + canvas.width = newWidth; + canvas.height = newHeight; + const context = canvas.getContext("2d"); + context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); + + return context.getImageData(0, 0, newWidth, newHeight); + } +}; diff --git a/src/workers/resizeImageWorker.js b/src/workers/resizeImageWorker.js new file mode 100644 index 000000000..e91b8f00a --- /dev/null +++ b/src/workers/resizeImageWorker.js @@ -0,0 +1,13 @@ +import { resizeImage } from "../components/Shared/Utils/imageUtils"; + +self.onmessage = async function (e) { + const data = e.data; + + // console.log(e); + const result = await resizeImage(data); + // console.log(result); + //const result = data; + + // Send the result back to the main thread + self.postMessage(result); +}; From 61fe5ea9a3f2ca4a6f8a421a5c7a77278addff0e Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:12:32 +0200 Subject: [PATCH 2/6] Fixed warning --- src/components/Shared/Utils/imageUtils.jsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Shared/Utils/imageUtils.jsx b/src/components/Shared/Utils/imageUtils.jsx index 18b37d960..649d3036c 100644 --- a/src/components/Shared/Utils/imageUtils.jsx +++ b/src/components/Shared/Utils/imageUtils.jsx @@ -22,9 +22,7 @@ const imageDataToFile = async (imageData, fileName, imageType) => { } //3. Convert the Blob to a File - const file = new File([blob], fileName, { type: blob.type }, imageType); - - return file; + return new File([blob], fileName, { type: blob.type }, imageType); }; /** From ccfe9b835066d556651247888633b4848bbb6fd9 Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:15:10 +0200 Subject: [PATCH 3/6] Updated label --- src/components/NavItems/tools/SyntheticImageDetection/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx index 2932beee5..f618523ff 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx @@ -431,7 +431,7 @@ const SyntheticImageDetection = () => { disabled={isLoading} /> } - label="Auto resize local image if too large" + label="Auto-Resize" /> )} From d8ab704940a18402261d48688888dca1cbaf1659 Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:19:03 +0200 Subject: [PATCH 4/6] Cleanup --- src/workers/resizeImageWorker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/workers/resizeImageWorker.js b/src/workers/resizeImageWorker.js index e91b8f00a..bff134680 100644 --- a/src/workers/resizeImageWorker.js +++ b/src/workers/resizeImageWorker.js @@ -6,7 +6,6 @@ self.onmessage = async function (e) { // console.log(e); const result = await resizeImage(data); // console.log(result); - //const result = data; // Send the result back to the main thread self.postMessage(result); From e3bde7277169b7587488e29b664c699aff7c43cb Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:21:56 +0200 Subject: [PATCH 5/6] Cleanup --- .../tools/SyntheticImageDetection/index.jsx | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx index f618523ff..d21803bb4 100644 --- a/src/components/NavItems/tools/SyntheticImageDetection/index.jsx +++ b/src/components/NavItems/tools/SyntheticImageDetection/index.jsx @@ -13,6 +13,8 @@ import { Box, Card, CardHeader, + FormControlLabel, + FormGroup, Grid, LinearProgress, Stack, @@ -32,10 +34,7 @@ import StringFileUploadField from "../../../Shared/StringFileUploadField"; import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils"; import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms"; import { useLocation } from "react-router-dom"; -import FormGroup from "@mui/material/FormGroup"; -import FormControlLabel from "@mui/material/FormControlLabel"; import { ROLES } from "../../../../constants/roles"; -// import worker from "../../../../workers/resizeImageWorker"; const SyntheticImageDetection = () => { const location = useLocation(); @@ -72,7 +71,6 @@ const SyntheticImageDetection = () => { URL: "url", UPLOAD: "local", }; - // const worker = new Worker("../../../../workers/resizeImageWorker.js"); const workerRef = useRef(null); @@ -97,20 +95,15 @@ const SyntheticImageDetection = () => { ); workerInstance.postMessage(image); - //let res; - workerInstance.onerror = function (e) { reject(e.error); }; workerInstance.onmessage = function (e) { - console.log(e); + // console.log(e); resolve(e.data); - // res = e.data; }; }); - - // return res; }; const getSyntheticImageScores = async ( From 51da72967aa78a6c6ee870adf23596b1d5f777a1 Mon Sep 17 00:00:00 2001 From: Valentin Porcellini Date: Thu, 5 Sep 2024 12:24:45 +0200 Subject: [PATCH 6/6] Cleanup -- removed offscreencanvas fallback --- src/components/Shared/Utils/imageUtils.jsx | 24 ++++++---------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/components/Shared/Utils/imageUtils.jsx b/src/components/Shared/Utils/imageUtils.jsx index 649d3036c..87640464b 100644 --- a/src/components/Shared/Utils/imageUtils.jsx +++ b/src/components/Shared/Utils/imageUtils.jsx @@ -56,7 +56,7 @@ export const resizeImage = async (imageData) => { newWidth = newHeight * aspectRatio; } - // Resize the image using an OffscreenCanvas (or fallback to regular canvas) + // Resize the image using an OffscreenCanvas const resizedImageData = await resizeUsingCanvas( imageBitmap, newWidth, @@ -72,22 +72,10 @@ export const resizeImage = async (imageData) => { // Helper function to resize the image using a canvas const resizeUsingCanvas = async (imageBitmap, newWidth, newHeight) => { - // Check if OffscreenCanvas is available - if (typeof OffscreenCanvas !== "undefined") { - const offscreenCanvas = new OffscreenCanvas(newWidth, newHeight); - const context = offscreenCanvas.getContext("2d"); - context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); + const offscreenCanvas = new OffscreenCanvas(newWidth, newHeight); + const context = offscreenCanvas.getContext("2d"); + context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); - // Get ImageData from the OffscreenCanvas - return context.getImageData(0, 0, newWidth, newHeight); - } else { - // Fallback to regular canvas (only necessary in rare cases) - const canvas = document.createElement("canvas"); - canvas.width = newWidth; - canvas.height = newHeight; - const context = canvas.getContext("2d"); - context.drawImage(imageBitmap, 0, 0, newWidth, newHeight); - - return context.getImageData(0, 0, newWidth, newHeight); - } + // Get ImageData from the OffscreenCanvas + return context.getImageData(0, 0, newWidth, newHeight); };