I added this question on Stackoverflow, but it was suggested that I post it here.
Currently, I create 2000x2000 pixels canvas and tile the image on it, and then tiling the texture on top and applying multiply
and alpha
effects to it to make it look more natural.
So, in detail, function imageUrlToFile
is used to get the image from URL and convert it to File
, function replaceColors
used to first convert the colors from black/white/gray into the given colors, tile both the image and texture in a 2000x2000
pixel canvas and apply the multiply
effect to the texture to make it look like a knitting pattern.
const imageUrl = 'https://i.imgur.com/JbzgtBV.png';
const textureUrl = 'https://i.imgur.com/Nl2A3ZC.png';
const colors = [
[0, 0, 0],
[70, 200, 100]
];
async function imageUrlToFile(imageUrl) {
try {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error(`Failed to fetch image (status ${response.status})`);
}
const blob = await response.blob();
const filename = imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
const file = new File([blob], filename, {
type: blob.type
});
return file;
} catch (error) {
console.error('Error in imageUrlToFile:', error);
throw error;
}
}
async function replaceColors(imageDataFile, colorArray, textureFile) {
return new Promise((resolve, reject) => {
const image = new Image();
const texture = new Image();
const reader = new FileReader();
const textureReader = new FileReader();
const canvasSize = 2000;
// Image loading and tiling
reader.onload = function(event) {
image.onload = async() => {
const canvas = new OffscreenCanvas(canvasSize, canvasSize);
const context = canvas.getContext('2d');
const imageWidth = image.width;
const imageHeight = image.height;
if (imageWidth >= canvasSize && imageHeight >= canvasSize) {
context.drawImage(image, 0, 0, canvasSize, canvasSize);
} else {
// Tile the image only if necessary
const numTilesX = Math.ceil(canvasSize / imageWidth);
const numTilesY = Math.ceil(canvasSize / imageHeight);
for (let x = 0; x < numTilesX; x++) {
for (let y = 0; y < numTilesY; y++) {
context.drawImage(image, x * imageWidth, y * imageHeight, imageWidth, imageHeight);
}
}
}
const imageData = context.getImageData(0, 0, canvasSize, canvasSize);
const imageDataArray = imageData.data;
const numColors = colorArray.length;
const thresholdStep = 255 / numColors;
for (let i = 0; i < imageDataArray.length; i += 4) {
const avg = (imageDataArray[i] + imageDataArray[i + 1] + imageDataArray[i + 2]) / 3;
let colorIndex = Math.floor(avg / thresholdStep);
colorIndex = Math.min(colorIndex, numColors - 1);
imageDataArray[i] = colorArray[colorIndex][0];
imageDataArray[i + 1] = colorArray[colorIndex][1];
imageDataArray[i + 2] = colorArray[colorIndex][2];
}
context.putImageData(imageData, 0, 0);
textureReader.onload = (e) => {
texture.onload = () => {
context.globalAlpha = 1.0;
context.globalCompositeOperation = 'multiply';
const textureWidth = texture.width;
const textureHeight = texture.height;
const numTextureTilesX = Math.ceil(canvasSize / textureWidth);
const numTextureTilesY = Math.ceil(canvasSize / textureHeight);
for (let x = 0; x < numTextureTilesX; x++) {
for (let y = 0; y < numTextureTilesY; y++) {
context.drawImage(texture, x * textureWidth, y * textureHeight, textureWidth, textureHeight);
}
}
context.globalAlpha = 1;
context.globalCompositeOperation = 'source-over';
canvas.convertToBlob({
type: 'image/png'
}).then((blob) => {
if (blob) {
const file = new File([blob], 'replaced.png', {
type: 'image/png'
});
resolve(file);
} else {
reject(new Error('Failed to create blob from canvas.'));
}
}).catch(reject);
};
texture.onerror = () => reject(new Error('Failed to load texture.'));
texture.src = e.target.result;
};
textureReader.onerror = () => reject(new Error('Failed to read texture file.'));
textureReader.readAsDataURL(textureFile);
};
image.onerror = () => reject(new Error('Failed to load image.'));
image.src = event.target.result;
};
reader.onerror = () => reject(new Error('Failed to read image file.'));
reader.readAsDataURL(imageDataFile);
});
}
async function displayImage() {
try {
const imageFile = await imageUrlToFile(imageUrl);
const textureFile = await imageUrlToFile(textureUrl);
const finalFile = await replaceColors(imageFile, colors, textureFile);
const imgElement = document.getElementById('resultImage');
const imageBlobUrl = URL.createObjectURL(finalFile);
imgElement.src = imageBlobUrl;
imgElement.onload = () => URL.revokeObjectURL(imageBlobUrl);
} catch (error) {
console.error('Error displaying image:', error);
}
}
// Call the displayImage function when the page loads
window.onload = displayImage;
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
<img id="resultImage" alt="Result Image" />
Code works as expected, however in my old question I was told that for different users it took more than a minute to get the result image, which is bad because it is part of our big project.
I have debugged the code and also run it in following machines:
- Macbook Pro 2016 (13 inch)
- Asus ROG Strix G15
- Macbook Pro 2017 (15 inch)
But, in my case it was taking maximum 3-4 seconds to run and return the image with 2000x2000
pixels. I have tried it with different browsers including Chrome, Firefox and Safari and all of them had similar results.
Thus, my question is, if possible, is there any better way to optimize the algorithm to make it faster?