resizing and changing to gray scale - javascript

I'm trying to take a jpg photo, and make it grayscale and resize it to 48x48 for my model,
I tried this but it doesn't work:
let image = require("../assets/angry.jpg");
const imageAssetPath = Image.resolveAssetSource(image);
const response = await fetch(imageAssetPath.uri, {}, { isBinary: true });
const imageData = await response.arrayBuffer();
let imageTensor = imageToTensor(imageData);
const imageResize = tf.image.resizeBilinear(imageTensor, [48, 48], true);
const imageToTensor = (rawData: ArrayBuffer) => {
const { width, height, data } = jpeg.decode(rawData, true);
const buffer = new Uint8Array(width * height * 3);
let offset = 0;
for (let i = 0; i < buffer.length; i += 3) {
buffer[i] = data[offset]; //red
buffer[i + 1] = data[offset + 1]; //green
buffer[i + 2] = data[offset + 2]; //blue
offset += 4; //skips Alpha value
}
return tf.tensor4d(buffer, [1, height, width, 3]);
};
the image is resizing to 48x48 but how do I make it grayscale? I tried in imageToTensor function to change the array to [height,width,1] but it only messed up the picture, any suggestions?

I didn't know there are so many methods missing!
You can look at the source of rgb_to_grayscale in python
and you'll see, how they convert rgb images to grayscale.
I tried to implement it the same way in javascript, but there is no function called tf.tensordot.
Here's how you can do it.
image = tf.ones([224, 224, 3])
rgb_weights = [0.2989, 0.5870, 0.1140]
image = tf.mul(image, rgb_weights)
image = tf.sum(image, axis=-1)
image = tf.expandDims(image, axis=-1)

Related

How to optimize web performance to avoid thread blocking when i have many large images to deal with?

I'm using PanoJS to create a platform that can view and modify huge images.
What i need to do is render image tiles on multiple canvas items, and change brightness, contrast, opacity when user drag the range selector.
But the problem is, when I run this updateTiles function, tiles are not updated one by one, they wait until all the tiles were updated and render together, AND, this loading function is not working as expected neither, this opacity change with loading element renders after all the tiles were updated too, that's not what I'm expecting..
Here are the codes I wrote about this senario, any idea how am I supposed to do to get what I want?
Thanks a lot!
...
PanoJS.prototype.updateTiles = function () {
const handleCanvasUpdate = (element) => {
const ctx = element.getContext('2d')
const imgData = new ImageData(new Uint8ClampedArray(JSON.parse(element._imgData.data)), element._imgData.height, element._imgData.width)
const changedImgData = this.changeImgData(imgData) // -1 ~ 1
ctx.putImageData(changedImgData, 0, 0);
}
const tag = this.showLoading('updateTiles')
for (let i = 0; i < this.well.children.length; i++) {
const element = this.well.children[i];
if (element.tagName.toUpperCase() === 'CANVAS') {
handleCanvasUpdate(element)
}
}
this.hideLoading(tag)
}
PanoJS.prototype.changeImgData = function (imgdata) {
// this imgdata can be very large
const data = imgdata.data;
for (let i = 0; i < data.length; i += 4) {
// brightness
const hsv = this.rgb2hsv([data[i], data[i + 1], data[i + 2]]);
hsv[2] *= 1 + this.luminance;
const rgb = this.hsv2rgb([...hsv]);
data[i] = rgb[0];
data[i + 1] = rgb[1];
data[i + 2] = rgb[2];
// contrast
const _contrast = (this.contrast / 100) + 1; //convert to decimal & shift range: [0..2]
const intercept = 128 * (1 - _contrast);
data[i] = data[i] * _contrast + intercept;
data[i + 1] = data[i + 1] * _contrast + intercept;
data[i + 2] = data[i + 2] * _contrast + intercept;
// opacity
data[i + 3] = data[i + 3] * this.opacity;
}
return imgdata;
}
PanoJS.prototype.showLoading = function (name) {
counter++
const tag = `${counter}${name}`
console.time(tag)
this.loadingMask.style.opacity = 1
this.loadingCount++
return tag
}
PanoJS.prototype.hideLoading = function (tag) {
// requestAnimationFrame(() => {
if (this.loadingCount) {
this.loadingCount--
}
// console.log(this.loadingMask.style.opacity);
if (this.loadingCount === 0) {
this.loadingMask.style.opacity = 0
}
console.timeEnd(tag)
// console.log(this.loadingCount);
// })
}
...

Canvas getImageData is always the same for 2 visually different images

I've got 3 images, 2 are visually identical but they have different file names and one is completely different. I load the images, put them on a canvas, get the image data and compare the image.
The 2 that are visually the same returns true which is correct
when comparing 2 that are not visually the same it also returns true which is not correct.
UPDATED from #obscure answer below
window.onload = function () {
setTimeout(process, 5000);
};
async function process() {
const img1 = document.getElementById("img1");
const img2 = document.getElementById("img2");
const img3 = document.getElementById("img3");
img1.crossOrigin = "Anonymous";
img2.crossOrigin = "Anonymous";
img3.crossOrigin = "Anonymous";
const canvas1 = document.createElement("canvas");
const ctx1 = canvas1.getContext("2d");
canvas1.width = img1.width;
canvas1.height = img1.height;
ctx1.drawImage(img1, 0, 0);
const pixData1 = ctx1.getImageData(0, 0, img1.width, img1.height).data;
const canvas2 = document.createElement("canvas");
const ctx2 = canvas2.getContext("2d");
canvas2.width = img2.width;
canvas2.height = img2.height;
ctx2.drawImage(img2, 0, 0);
const pixData2 = ctx2.getImageData(0, 0, img2.width, img2.height).data;
const canvas3 = document.createElement("canvas");
const ctx3 = canvas3.getContext("2d");
canvas3.width = img3.width;
canvas3.height = img3.height;
ctx3.drawImage(img3, 0, 0);
const pixData3 = ctx3.getImageData(0, 0, img3.width, img3.height).data;
const utf8A = new TextEncoder().encode(pixData1.toString());
let img1Hash = await crypto.subtle
.digest("SHA-256", utf8A)
.then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
});
const utf8B = new TextEncoder().encode(pixData2.toString());
let img2Hash = await crypto.subtle
.digest("SHA-256", utf8B)
.then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
});
const utf8C = new TextEncoder().encode(pixData3.toString());
let img3Hash = await crypto.subtle
.digest("SHA-256", utf8C)
.then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
});
console.log(img1Hash);
console.log(img2Hash);
console.log(img3Hash);
console.log(img1Hash === img2Hash);
console.log(img1Hash === img3Hash); // Should be false
console.log(img2Hash === img3Hash); // Should be false
}
<!DOCTYPE html>
<html>
<head>
<script src="index.js"></script>
</head>
<body>
<img src="https://i.imgur.com/M0K21iS.jpg" id="img1" />
<img src="https://i.imgur.com/uNbsNAd.jpg" id="img2" />
<img src="https://i.imgur.com/QdqhGb9.jpg" id="img3" />
</body>
</html>
To compare two array for equality you can indeed use a hashing algorithm. Utilizing crypto.subtle is an easy solution but I'm afraid you aren't aware what the .digest() method does/returns.
From your code it seems you think it's a synchronous operation:
let img1Hash = "";
const utf8A = new TextEncoder().encode(pixData1.toString());
crypto.subtle.digest("SHA-256", utf8A).then((hashBuffer) => {
img1Hash = Array.from(new Uint8Array(hashBuffer));
});
console.log(img1Hash); // nothing logged
Well it's an asynchronous operation and digest() returns a promise. So if you simply log img1Hash after calling digest() will be an empty string as the promise didn't fulfill yet. Likewise a comparison like img1Hash === img2Hash will yield true as both variables contain empty strings at that point in time.
So you need to wait until both promises are resolved. This can be done by wrapping your whole onload code block inside an async function process() and await the results of calling digest(). Unfortunately this would still not return true if you do a comparison because you make the result an array again:
Array.from(new Uint8Array(hashBuffer))
If you convert it to a String you can compare it for equality.
Here's the complete code:
window.onload = function() {
process();
};
async function process() {
const img1 = document.getElementById("img1");
const img2 = document.getElementById("img2");
img1.crossOrigin = "Anonymous";
img2.crossOrigin = "Anonymous";
const canvas1 = document.createElement("canvas");
const ctx1 = canvas1.getContext("2d");
canvas1.width = img1.width;
canvas1.height = img1.height;
ctx1.drawImage(img1, 0, 0);
const pixData1 = ctx1.getImageData(0, 0, img1.width, img1.height).data;
const canvas2 = document.createElement("canvas");
const ctx2 = canvas2.getContext("2d");
canvas2.width = img2.width;
canvas2.height = img2.height;
ctx2.drawImage(img2, 0, 0);
const pixData2 = ctx2.getImageData(0, 0, img2.width, img2.height).data;
const utf8A = new TextEncoder().encode(pixData1.toString());
let img1Hash = await crypto.subtle.digest("SHA-256", utf8A).then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
});
const utf8B = new TextEncoder().encode(pixData2.toString());
let img2Hash = await crypto.subtle.digest("SHA-256", utf8B).then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
});
console.log(img1Hash); // nothing logged
console.log(img2Hash); // nothing logged
console.log(img1Hash === img2Hash); // true
}
<img src="https://i.imgur.com/M0K21iS.jpg" id="img1" />
<img src="https://i.imgur.com/uNbsNAd.jpg" id="img2" />
Edit
As you're struggling to get the correct hashes for each of your images, let's do things a bit different. Instead of referencing a real html <img> element, let's create those dynamically and add 'em to the DOM if ready.
So the following snippet:
let sources = ['https://i.imgur.com/M0K21iS.jpg', 'https://i.imgur.com/uNbsNAd.jpg', 'https://i.imgur.com/QdqhGb9.jpg'];
let images = [];
let imageData = [];
let hashes = [];
let counter = 0;
function loaded(e) {
counter++;
if (counter == 3) {
process();
}
}
async function process() {
let utf8;
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = images[0].width;
canvas.height = images[0].height;
for (let a = 0; a < images.length; a++) {
ctx.drawImage(images[a], 0, 0);
imageData.push(ctx.getImageData(0, 0, canvas.width, canvas.height).data);
utf8 = new TextEncoder().encode(imageData[a].toString());
hashes.push(await crypto.subtle
.digest("SHA-256", utf8)
.then((hashBuffer) => {
return Array.from(new Uint8Array(hashBuffer)).toString();
}));
}
console.log(hashes[0]);
console.log(hashes[1]);
console.log(hashes[2]);
}
let img;
for (let a = 0; a < sources.length; a++) {
img = new Image();
images.push(img);
img.crossOrigin = 'anonymous';
document.body.appendChild(img);
img.onload = loaded;
img.src = sources[a];
}
returns three unique, completely different hashes.
100,172,184,128,122,59,32,239,211,133,243,51,25,159,237,239,175,140,198,232,133,184,77,224,174,85,38,1,164,52,30,68
88,209,142,171,42,213,152,27,60,14,200,193,162,134,50,183,110,70,166,231,237,163,215,129,184,249,106,41,16,147,151,97
72,2,137,13,168,131,212,29,170,19,57,24,39,91,164,32,38,2,170,231,124,72,78,64,168,135,84,1,108,11,161,216
As you've surely guessed by now using hashes for comparing two images visually isn't the way to go. What you could do instead is compare image A's color at x, y with image B's at the same position and sum up the differences. If the total difference is within a certain threshold the images should be considered equal.
To do this we need to convert the RGB colors to the HSV color model, as it's better suited for a 'human' color comparison.
let sources = ['https://i.imgur.com/M0K21iS.jpg', 'https://i.imgur.com/uNbsNAd.jpg', 'https://i.imgur.com/QdqhGb9.jpg'];
let images = [];
let imageData = [];
let hashes = [];
let counter = 0;
function loaded(e) {
counter++;
if (counter == 3) {
process();
}
}
async function process() {
let canvas = document.createElement("canvas");
let ctx = canvas.getContext("2d");
canvas.width = images[0].width;
canvas.height = images[0].height;
for (let a = 0; a < images.length; a++) {
ctx.drawImage(images[a], 0, 0);
imageData.push(ctx.getImageData(0, 0, canvas.width, canvas.height).data);
}
compare(imageData[0], imageData[1]);
compare(imageData[0], imageData[2]);
}
function compare(imgDataA, imgDataB) {
let hslA, hslB, avgH, avgS, avgL, difference;
let differences = 0;
let counter = 0;
for (let a = 0; a < imgDataA.length; a += 4) {
hslA = rgbToHsl(imgDataA[a], imgDataA[a + 1], imgDataA[a + 2]);
hslB = rgbToHsl(imgDataB[a], imgDataB[a + 1], imgDataB[a + 2]);
avgH = (hslA[0] + hslB[0]) / 2;
avgS = (hslA[1] + hslB[1]) / 2;
avgL = (hslA[2] + hslB[2]) / 2;
differences += (Math.abs(hslA[0] - avgH) + Math.abs(hslA[1] - avgS) + Math.abs(hslA[2] - avgL)) / 3;
counter++;
}
console.log(differences / (imgDataA.length / 4));
}
let img;
for (let a = 0; a < sources.length; a++) {
img = new Image();
images.push(img);
img.crossOrigin = 'anonymous';
document.body.appendChild(img);
img.onload = loaded;
img.src = sources[a];
}
// taken from: https://gist.github.com/mjackson/5311256#file-color-conversion-algorithms-js
function rgbToHsl(r, g, b) {
r /= 255, g /= 255, b /= 255;
var max = Math.max(r, g, b),
min = Math.min(r, g, b);
var h, s, l = (max + min) / 2;
if (max == min) {
h = s = 0;
} else {
var d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h, s, l];
}
As a side note: The rgbToHsl() function above was taken from here. If you run the example you get a difference of 0.012553120747668494 between the first and the second and 0.02681219030137108 for the first and the third image. So one could determine that images are equal if it's difference is less than or equal 0.018 for example.

Opencv.js to detect rectangular shape from image and cut it

i cant see output and also no error .... i try to detect rectangle shape from image and cut it and save it with opencv.js
onFilePicked() {
let imgElement = document.getElementById('imageSrc');
const files = event.target.files;
imgElement.src = URL.createObjectURL(files[0]);
var app = this
imgElement.onload = function () {
let mat = cv.imread(imgElement)
let dst = new cv.Mat();
cv.cvtColor(mat, mat, cv.COLOR_RGB2GRAY);
// gray = cv.bilateralFilter(gray, 11, 17, 17)
cv.Canny(mat, dst, 30, 200, 3, false);
let contours = new cv.MatVector();
let hierarchy = new cv.Mat();
var transformed = null
cv.findContours(dst, contours, hierarchy, cv.RETR_LIST, cv.CHAIN_APPROX_SIMPLE)
var sortableContours = []
for (let i = 0; i < contours.size(); i++) {
let cnt = contours.get(i);
let area = cv.contourArea(cnt, false);
let perim = cv.arcLength(cnt, false);
sortableContours.push({
areaSize: area,
perimiterSize: perim,
contour: cnt
});
let color = new cv.Scalar(255, 0, 0, 255);
let hierarchy2 = new cv.Mat();
cv.drawContours(mat, contours, -1, (0, 255, 0), 3);
}
cv.imshow('canvasOutput', mat);
let foundContour = null;
for (let sortableContour of sortableContours) {
let peri = cv.arcLength(sortableContour.contour, true);
let approx = new cv.Mat();
cv.approxPolyDP(sortableContour.contour, approx, 0.1 * peri, true);
if (approx.rows == 4) {
foundContour = approx
transformed = app.perspective_transform(mat, foundContour)
break;
} else {
approx.delete();
}
}
let rotate = app.rotate_image(transformed, 90)
cv.imshow('canvasOutput', rotate)
};
},
transform
perspective_transform(image, foundContour) {
let corner1 = new cv.Point(foundContour.data32S[0], foundContour.data32S[1]);
let corner2 = new cv.Point(foundContour.data32S[2], foundContour.data32S[3]);
let corner3 = new cv.Point(foundContour.data32S[4], foundContour.data32S[5]);
let corner4 = new cv.Point(foundContour.data32S[6], foundContour.data32S[7]);
//Order the corners
let cornerArray = [{
corner: corner1
}, {
corner: corner2
}, {
corner: corner3
}, {
corner: corner4
}];
//Sort by Y position (to get top-down)
cornerArray.sort((item1, item2) => {
return (item1.corner.y < item2.corner.y) ? -1 : (item1.corner.y > item2.corner.y) ? 1 : 0;
}).slice(0, 5);
//Determine left/right based on x position of top and bottom 2
let tl = cornerArray[0].corner.x < cornerArray[1].corner.x ? cornerArray[0] : cornerArray[1];
let tr = cornerArray[0].corner.x > cornerArray[1].corner.x ? cornerArray[0] : cornerArray[1];
let bl = cornerArray[2].corner.x < cornerArray[3].corner.x ? cornerArray[2] : cornerArray[3];
let br = cornerArray[2].corner.x > cornerArray[3].corner.x ? cornerArray[2] : cornerArray[3];
//Calculate the max width/height
let widthBottom = Math.hypot(br.corner.x - bl.corner.x, br.corner.y - bl.corner.y);
let widthTop = Math.hypot(tr.corner.x - tl.corner.x, tr.corner.y - tl.corner.y);
let theWidth = (widthBottom > widthTop) ? widthBottom : widthTop;
let heightRight = Math.hypot(tr.corner.x - br.corner.x, tr.corner.y - br.corner.y);
let heightLeft = Math.hypot(tl.corner.x - bl.corner.x, tr.corner.y - bl.corner.y);
let theHeight = (heightRight > heightLeft) ? heightRight : heightLeft;
//Transform!
let finalDestCoords = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, theWidth - 1, 0, theWidth - 1, theHeight - 1, 0, theHeight - 1]);
// corners
let srcCoords = cv.matFromArray(4, 1, cv.CV_32FC2, [tl.corner.x, tl.corner.y, tr.corner.x, tr.corner.y, br.corner.x, br.corner.y, bl.corner.x, bl.corner.y]);
let dsize = new cv.Size(theWidth, theHeight);
let M = cv.getPerspectiveTransform(srcCoords, finalDestCoords)
let dst = new cv.Mat();
cv.warpPerspective(image, dst, M, dsize);
return dst
},
rotate image
rotate_image(image, angle) {
let dst = new cv.Mat();
let dsize = new cv.Size(image.rows, image.cols);
let center = new cv.Point(image.cols / 2, image.rows / 2);
// You can try more different parameters
let M = cv.getRotationMatrix2D(center, angle, 1);
cv.warpAffine(image, dst, M, dsize, cv.INTER_LINEAR, cv.BORDER_CONSTANT, new cv.Scalar());
return dst
},
You are just grabbing the first contour in the entire array of sortableContours that has 4 points, and running the transform on that one. You need to sort them to those with the largest area first.
//sorts the contours by largest area first
let slicer = Math.min(sortableContours.length, 4);
let sortedContours = sortableContours.sort((a,b) => (a.areaSize < b.areaSize) ? 1 : -1).slice(0, slicer);
Also, I would recommend removing this line of code from within the for loop as it can be performed once outside the loop and slows the process down quite a bit (runs a few thousand times)
cv.drawContours(mat, contours, -1, (0, 255, 0), 3);
My final note would be that the following line might need to be tweaked from .1 to a smaller number like .02 if you are getting poor results. .1 is more forgiving but .02 is more precise. Alternatively, you can do both, keep all the results in an array, and pick the one with the largest area when you are done for the best of both worlds.
cv.approxPolyDP(sortableContour.contour, approx, 0.1 * peri, true);
Best of both worlds:
//iterates through the largest contours and creates transformed images if the contour's shape is a rectangle
let transformedOptions = [];
for (let sortedContour of sortedContours) {
let perimeter = cv.arcLength(sortedContour.contour, true);
let precisePoly = new cv.Mat();
let approxPoly = new cv.Mat();
cv.approxPolyDP(sortedContour.contour, precisePoly, 0.02 * perimeter, true); //the smaller number (0.02) is more precise
cv.approxPolyDP(sortedContour.contour, approxPoly, 0.1 * perimeter, true); //the larger number (0.1) is more forgiving
//if the polygon has 4 points (rectangle-ish)
if (precisePoly.rows == 4) {
transformedOptions.push(this.perspectiveTransform(originalImage, precisePoly, imageHeight, imageWidth))
}
if(approxPoly.rows == 4) {
transformedOptions.push(this.perspectiveTransform(originalImage, approxPoly, imageHeight, imageWidth))
}
precisePoly.delete();
approxPoly.delete();
}
let transformed = this.getLargestTransformation(transformedOptions);
//this could be optimized a bit
private getLargestTransformation(options) {
var transformed = null;
for(let option of options) {
if(option == null) continue;
var largestArea = 0;
var area = option.rows * option.cols;
if(transformed == null || area > largestArea) {
transformed = option;
largestArea = area;
}
}
return transformed;
}

Get the color of one pixel at Photoshop scripts

I am trying to figure out how to get the color of one defined Pixel.
In my imagination its shoud look like:
color = get.color.Pixel(x,y);
Maybe someone can help me with this piece of code?
Photoshop's JavaScript API doesn't provide a mechanism like you imagine in your question.
You'll need to utilize the Document.colorSamplers.add([x, y]) method, then read each component color value via its properties:
The following gist shows how to obtain either rgb or cmyk values for a given x,y coordinate:
#target photoshop
// Define the x and y coordinates for the pixel to sample.
var x = 1;
var y = 1;
// Add a Color Sampler at a given x and y coordinate in the image.
var pointSample = app.activeDocument.colorSamplers.add([(x - 1),(y - 1)]);
// Obtain array of RGB values.
var rgb = [
pointSample.color.rgb.red,
pointSample.color.rgb.green,
pointSample.color.rgb.blue
];
// Obtain array of rounded CMYK values.
var cmyk = [
Math.round(pointSample.color.cmyk.cyan),
Math.round(pointSample.color.cmyk.magenta),
Math.round(pointSample.color.cmyk.yellow),
Math.round(pointSample.color.cmyk.black)
];
// Remove the Color Sampler.
pointSample.remove();
// Display the complete RGB values and each component color.
alert('RGB: ' + rgb)
alert('red: ' + rgb[0])
alert('green: ' + rgb[1])
alert('blue: ' + rgb[2])
// Display the complete CMYK values and each component color.
alert('CMYK: ' + cmyk)
alert('cyan: ' + cmyk[0])
alert('magenta: ' + cmyk[1])
alert('yellow: ' + cmyk[2])
alert('black: ' + cmyk[3])
Here's a simple script using a ColorSampler. It's set up to return RGB values.
function PixelSampler(doc) {
this.doc = doc
this.doc.colorSamplers.removeAll();
this.sampler = this.doc.colorSamplers.add([0, 0]);
}
// Return an array of R, G, B pixel values for a particular coordinate.
PixelSampler.prototype.get = function (x, y) {
this.sampler.move([x, y]);
const R = this.sampler.color.rgb.red;
const G = this.sampler.color.rgb.green;
const B = this.sampler.color.rgb.blue;
return [R, G, B];
}
////////////////////////////////////////////////////////
/// SOME TESTS /////////////////////////////////////////
////////////////////////////////////////////////////////
const p = new PixelSampler(app.activeDocument);
alert("Pixel 0 =\n\n" + p.get(0, 0));
$.hiresTimer;
var n = 1000; //p.width * p.height;
for (var i = 0; i < n; i++) p.get(i, 0);
sec = ($.hiresTimer / 1000 / 1000);
alert("Got " + (n / 1000) + " kilopixels in " + sec.toFixed(2) + " seconds.");
This gives me pixel values at about 100 pixels per second on my machine.
I found this and cleaned up the script a bit. Basically, the idea is to:
Save the current image as a raw bitmap.
Read it back in, but on the javascript side.
Do all access to pixels on the javascript side.
This gives me pixel values at about 72,000 pixels per second, not including the overhead of writing the raw data to disk and reading it back in. It has the added benefit that you can set pixel values, too.
// Adapted from https://community.adobe.com/t5/photoshop/get-index-of-each-pixel/td-p/10022899?page=1
// The purpose is to query (and change) pixel values quickly.
//
// The secret to speed is doing everything on the script side rather than ask Photoshop to do things.
// We use files on disk as an intermediary; on the script side, we read / write it as a binary file; on the
// Photoshop side, we save / open it as a raw bitmap.
//
// Only works on RGB 8bpp images, but this could be easily extended to support others.
function RawPixels(doc) {
this.doc = doc;
const currentActiveDoc = app.activeDocument;
// Obtain the width and height in pixels of the desired document.
const currentRulerUnits = app.preferences.rulerUnits;
app.preferences.rulerUnits = Units.PIXELS;
app.activeDocument = doc;
this.width = Number(doc.width.value);
this.height = Number(doc.height.value);
this.length = this.width * this.height;
this.pixelData = "";
// Return the ruler to its previous state.
app.preferences.rulerUnits = currentRulerUnits;
try {
// We're going to save this document as a raw bitmap to be able to read back in the pixel values
// themselves.
const file = new File(Folder.temp.fsName + "/" + Math.random().toString().substr(2) + ".raw");
// Set up the save action.
// See https://helpx.adobe.com/photoshop/using/file-formats.html#photoshop_raw_format for some info,
// and more technical at https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
var rawFormat = new ActionDescriptor();
rawFormat.putString(stringIDToTypeID("fileCreator"), "8BIM");
rawFormat.putBoolean(stringIDToTypeID("channelsInterleaved"), true);
var saveAction = new ActionDescriptor();
saveAction.putObject(stringIDToTypeID("as"), stringIDToTypeID("rawFormat"), rawFormat);
saveAction.putPath(stringIDToTypeID("in"), file);
saveAction.putBoolean(stringIDToTypeID("copy"), false);
executeAction(stringIDToTypeID("save"), saveAction, DialogModes.NO);
// File is saved; now read it back in as raw bytes.
file.open("r");
file.encoding = "BINARY";
this.pixelData = file.read();
const err = file.error;
file.close();
file.remove();
file = null;
if (err) alert(err);
}
catch (e) { alert(e); }
// Return focus to whatever the user had.
app.activeDocument = currentActiveDoc;
}
// Calculate offset from x, y coordinates. Does not check for valid bounds.
getOffset = function(x, y) {
if (y == undefined) {
// allow linear indices too
y = Math.floor(x / this.width);
x = x - y * this.width;
}
return (y * this.width + x) * 3;
}
// Return an array of R, G, B pixel values for a particular coordinate.
RawPixels.prototype.get = function (x, y) {
const off = getOffset(x, y);
const R = this.pixelData.charCodeAt(off + 0);
const G = this.pixelData.charCodeAt(off + 1);
const B = this.pixelData.charCodeAt(off + 2);
return [R, G, B];
}
// Set the pixel at x, y to the values in RGB.
RawPixels.prototype.set = function (RGB, x, y) {
const off = getOffset(x, y);
// note: note checking that length of p = 3!
const R = String.fromCharCode(RGB[0]);
const G = String.fromCharCode(RGB[1]);
const B = String.fromCharCode(RGB[2]);
this.pixelData = this.pixelData.substr(0, off) + R + G + B + this.pixelData.substr(off + 3);
}
// If any changes were made to the pixels, we need to save them to disk and have Photoshop read that file back in.
// We do that by creating a new layer in the desired document.
RawPixels.prototype.create_layer = function () {
try {
const file = new File(Folder.temp.fsName + "/" + Math.random().toString().substr(2) + ".raw");
file.open("w");
file.encoding = "BINARY";
file.write(this.pixelData);
const err = file.error;
file.close();
if (err) { file.remove(); alert(err); return; }
var rawFormat = new ActionDescriptor();
rawFormat.putInteger(stringIDToTypeID("width"), this.width);
rawFormat.putInteger(stringIDToTypeID("height"), this.height);
rawFormat.putInteger(stringIDToTypeID("channels"), 3);
rawFormat.putBoolean(stringIDToTypeID("channelsInterleaved"), true);
rawFormat.putInteger(stringIDToTypeID("depth"), 8);
var openAction = new ActionDescriptor();
openAction.putPath(stringIDToTypeID("null"), file);
openAction.putObject(stringIDToTypeID("as"), stringIDToTypeID("rawFormat"), rawFormat);
executeAction(stringIDToTypeID("open"), openAction, DialogModes.NO);
file.remove();
// The new active document is the file we just opened. Duplicate its contents into
// a new layer in our desired document, then close this temporary file.
app.activeDocument.activeLayer.duplicate(this.doc.layers[0], ElementPlacement.PLACEBEFORE);
const tempDoc = app.activeDocument;
app.activeDocument = this.doc;
this.doc.layers[0].name = "Pixels";
app.activeDocument = tempDoc;
app.activeDocument.close(SaveOptions.DONOTSAVECHANGES);
app.activeDocument = this.doc;
}
catch (e) { alert(e); }
}
////////////////////////////////////////////////////////
/// SOME TESTS /////////////////////////////////////////
////////////////////////////////////////////////////////
$.hiresTimer;
const p = new RawPixels(app.activeDocument);
var sec = ($.hiresTimer / 1000 / 1000);
alert("Init RawPixels in " + sec.toFixed(2) + " seconds");
alert("Pixel 0 =\n\n" + p.get(0));
var a = new Array();
for (var i = 0; i < 100; i++) a.push(p.get(i));
alert("Pixel 0-99 = \n\n" + a.toSource());
p.set(0, [1, 200, 3]);
alert("New Pixel 0=\n\n" + p.get(0));
$.hiresTimer;
var n = p.width * p.height;
for (var i = 0; i < n; i++) p.get(i);
sec = ($.hiresTimer / 1000 / 1000);
alert("Got " + (n / 1000 / 1000) + " megapixels in " + sec.toFixed(2) + " seconds.");
$.hiresTimer;
n = 10;
for (var i = 0; i < n; i++) p.set([255, i * 20, i * 10], 1 + i * 2);
sec = ($.hiresTimer / 1000 / 1000);
//alert("Set " + n + " pixels in " + sec.toFixed(2) + " seconds");
p.create_layer();
alert("New layer created with new pixels");

Constantly increasing memory usage when passing around huge arrays to webworker

I am currently doing some 3d modeling using babylonjs. I need to create a pressure map from given pressure at specific points. I am doing that using IDW. However this means that even with my map being a size of 70x90 grid requires me to have an array of 25200 (4 rgba values for each pixel) entries. Then this buffer is passed to a RawTexture for assigning it to a material, that is overlaid on the object
I am using a web worker, because I have to update the pressure values every 100ms and I don't want to block the main thread.The issue occurs when I am return that array (created in calculate function) from a service worker.
For some reason the memory usage just keeps going up, without stopping. It eventually goes up to around 1.5 gigabytes and I have to kill it.
The question : Is there any way to prevent this and what could be causing such high memory usage?
Worker:
// #flow
import { find, propEq, both } from 'ramda';
import { colorFromValue } from './color';
import { inverseDistance, distanceValues } from './math';
const findPoint = (x: number, y: number) =>
find(both(propEq('x', x), propEq('y', y)));
const distanceDict = {};
/* eslint-disable */
function calculate(options: Object, pList: Array<*>) {
const points = pList || [];
const { height, width } = options;
const gridWidth = width * 4;
const grid = new Uint8Array(options.width * options.height * 4);
for (let y = 0; y < height; y += 1) {
const rW = y * gridWidth;
for (let i = 0; i < gridWidth; i += 4) {
const index = i + rW;
const x = i / 4;
const dictKey = `${x}--${y}`;
let bottoms = distanceDict[dictKey];
if (bottoms === undefined) {
bottoms = distanceValues(points, x, y);
distanceDict[dictKey] = bottoms;
}
const point = findPoint(x, y)(points);
const value = point !== undefined && point !== null ?
point.value : inverseDistance(points, bottoms);
const color = colorFromValue(value);
grid[index] = color[0];
grid[index + 1] = color[1];
grid[index + 2] = color[2];
grid[index + 3] = 255;
}
}
return grid;
}
self.onmessage = (e) => {
const { points, options } = e.data;
const grid = calculate(options, points);
self.postMessage(grid.buffer, [grid.buffer]);
};
Painting:
modifyNodes = (points: Array<*>) => new Promise((res, rej) => {
this.worker.onmessage = (e) => {
this._texture.update(new Uint8Array(e.data));
res();
}
const data = {
options: this._options,
points,
};
this.worker.postMessage(data);
})
So it seems the issue was in the colorFromValue function that was memoized. Because the values had quite few decimal points it could create up to 9! new entries into cache, so it drove up the memory usage...

Categories

Resources