Skip to content

Commit

Permalink
Merge pull request #577 from AFP-Medialab/571-synthetic-images-resizi…
Browse files Browse the repository at this point in the history
…ng-to-2mpx

571-synthetic-images-resizing-to-2mpx
  • Loading branch information
ttramb authored Sep 6, 2024
2 parents 660b5b2 + 51da729 commit c57b4ee
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 27 deletions.
130 changes: 103 additions & 27 deletions src/components/NavItems/tools/SyntheticImageDetection/index.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -13,9 +13,12 @@ import {
Box,
Card,
CardHeader,
FormControlLabel,
FormGroup,
Grid,
LinearProgress,
Stack,
Switch,
} from "@mui/material";

import useMyStyles from "../../../Shared/MaterialUiStyles/useMyStyles";
Expand All @@ -31,6 +34,7 @@ import StringFileUploadField from "../../../Shared/StringFileUploadField";
import { preprocessFileUpload } from "../../../Shared/Utils/fileUtils";
import { syntheticImageDetectionAlgorithms } from "./SyntheticImageDetectionAlgorithms";
import { useLocation } from "react-router-dom";
import { ROLES } from "../../../../constants/roles";

const SyntheticImageDetection = () => {
const location = useLocation();
Expand Down Expand Up @@ -59,13 +63,49 @@ const SyntheticImageDetection = () => {

const [imageType, setImageType] = useState(undefined);

const [autoResizeLocalFile, setAutoResizeLocalFile] = useState(true);

const dispatch = useDispatch();

const IMAGE_FROM = {
URL: "url",
UPLOAD: "local",
};

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);

workerInstance.onerror = function (e) {
reject(e.error);
};

workerInstance.onmessage = function (e) {
// console.log(e);
resolve(e.data);
};
});
};

const getSyntheticImageScores = async (
url,
processURL,
Expand Down Expand Up @@ -249,14 +289,28 @@ const SyntheticImageDetection = () => {
* @returns {Promise<void>}
*/
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 =
urlInput && typeof urlInput === "string"
? IMAGE_FROM.URL
: IMAGE_FROM.UPLOAD;

await getSyntheticImageScores(urlInput, true, dispatch, type, imageFile);
await getSyntheticImageScores(
urlInput,
true,
dispatch,
type,
processedFile,
);
};

useEffect(() => {
Expand Down Expand Up @@ -298,6 +352,10 @@ const SyntheticImageDetection = () => {
}
}, [imageFile, input, result]);

const toggleAutoResizeLocalFile = () => {
setAutoResizeLocalFile((prev) => !prev);
};

return (
<Box>
<HeaderTool
Expand Down Expand Up @@ -334,31 +392,49 @@ const SyntheticImageDetection = () => {
/>

<Box p={3}>
<form>
<StringFileUploadField
labelKeyword={keyword("synthetic_image_detection_link")}
placeholderKeyword={keyword(
"synthetic_image_detection_placeholder",
)}
submitButtonKeyword={keyword("submit_button")}
localFileKeyword={keyword("button_localfile")}
urlInput={input}
setUrlInput={setInput}
fileInput={imageFile}
setFileInput={setImageFile}
handleSubmit={handleSubmit}
fileInputTypesAccepted={"image/*"}
handleCloseSelectedFile={handleClose}
preprocessLocalFile={preprocessImage}
isParentLoading={isLoading}
/>
</form>

{isLoading && (
<Box mt={3}>
<LinearProgress />
</Box>
)}
<Stack direction="column" spacing={2}>
<form>
<StringFileUploadField
labelKeyword={keyword("synthetic_image_detection_link")}
placeholderKeyword={keyword(
"synthetic_image_detection_placeholder",
)}
submitButtonKeyword={keyword("submit_button")}
localFileKeyword={keyword("button_localfile")}
urlInput={input}
setUrlInput={setInput}
fileInput={imageFile}
setFileInput={setImageFile}
handleSubmit={handleSubmit}
fileInputTypesAccepted={"image/*"}
handleCloseSelectedFile={handleClose}
preprocessLocalFile={preprocessImage}
isParentLoading={isLoading}
/>
</form>

{role.includes(ROLES.EXTRA_FEATURE) && (
<FormGroup>
<FormControlLabel
control={
<Switch
checked={autoResizeLocalFile}
onChange={toggleAutoResizeLocalFile}
size="small"
disabled={isLoading}
/>
}
label="Auto-Resize"
/>
</FormGroup>
)}

{isLoading && (
<Box mt={3}>
<LinearProgress />
</Box>
)}
</Stack>
</Box>
</Card>

Expand Down
81 changes: 81 additions & 0 deletions src/components/Shared/Utils/imageUtils.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Converts an ImageData object to File
* @param imageData {ImageData}
* @param fileName {string}
* @param imageType {string}
* @returns {Promise<File | Error>}
*/
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
return new File([blob], fileName, { type: blob.type }, imageType);
};

/**
*
* @param imageData {File | Blob}
* @returns {Promise<File | Blob | Error>}
*/
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
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) => {
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);
};
12 changes: 12 additions & 0 deletions src/workers/resizeImageWorker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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);

// Send the result back to the main thread
self.postMessage(result);
};

0 comments on commit c57b4ee

Please sign in to comment.