Canvas getImageData is always the same for 2 visually different images - javascript

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.

Related

resizing and changing to gray scale

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)

How to programmatically draw a thick line JavaScript?

I'm trying to define racecourse with a dynamic line that user can draw on a canvas element. So when line has been drawn, program should add sidelines for it as shown in the picture below:
I have managed to mimic the idea already by using line normals but can't get it done correctly. At the moment I put point in the midway of the lines in the direction of the line normals and draw outlines using those points. While generated line is relatively smooth in cases on large turns, tight turns tend to produce loops.
As seen in image below:
Here is current code that generates points for side lines above (I'm using p5.js JavaScript library):
var sketch = function (p) {
with(p) {
let handpoints;
let walkhandpoints;
let collect;
let parsepath;
let shapse;
let step;
let tmp;
let dorender;
let lineoffset;
p.setup = function() {
createCanvas(600, 600);
handpoints = [];
walkhandpoints = 10;
collect = true;
parsepath = false;
shapes = [];
step = 2;
tmp = [];
dorender = true;
lineoffset = 15;
};
p.draw = function() {
if(dorender) {
background(220);
update();
for (let shape of shapes) {
shape.show();
}
}
};
function update() {
if (mouseIsPressed) {
if (collect) {
let mouse = createVector(mouseX, mouseY);
handpoints.push(mouse);
Shape.drawPath(handpoints);
parsepath = true;
}
} else if (parsepath) {
let tmp1 = Shape.cleanPath(handpoints, step);
let s1 = new Shape(tmp1, 1, 'line', color(175));
shapes.push(s1);
let tmp2 = Line.sidePoints(tmp1, lineoffset);
let s2 = new Shape(tmp2.sideA, 1, 'line', color(175,120,0));
let s3 = new Shape(tmp2.sideB, 1, 'line', color(175,0, 120));
shapes.push(s2);
shapes.push(s3);
handpoints = [];
parsepath = false;
//dorender = false;
}
}
class Shape {
constructor(points, mag, type = 'line', shader = color(200, 0, 100)) {
this.points = points.slice().map(item => item.copy());
this.type = type;
this.mag = mag;
this.shader = shader;
}
static cleanPath(points, step) {
let tmp = [];
let output = [];
for (let i = 1; i < points.length; i++) {
let prev = points[i - 1];
let curr = points[i];
if (!prev.equals(curr)) {
tmp.push(prev.copy())
if (i === points.length - 1) {
tmp.push(curr.copy())
}
}
}
for (let i = 0; i < tmp.length; i++) {
if(i % step === 0) {
output.push(tmp[i]);
}
}
output.push(output[0]);
return output;
}
static drawPath(points, mag = 1, type = 'line', shader = color(175)) {
let s = new Shape(points, mag, type, shader);
s.show();
}
show() {
for (let i = 0; i < this.points.length; i++) {
if (this.type === 'line' && i > 0) {
let prev = this.points[i - 1];
let curr = this.points[i];
strokeWeight(this.mag);
stroke(this.shader);
line(prev.x, prev.y, curr.x, curr.y);
} else if (this.type === 'point') {
noStroke();
fill(this.shader);
ellipse(this.points[i].x, this.points[i].y, this.mag * 2, this.mag * 2);
}
}
}
}
class Line {
static sidePoints(points, lineoffset) {
let sideA = [];
let sideB = [];
for(let i = 1; i < points.length; i++) {
// take consecutive points
let prev = points[i-1];
let curr = points[i];
// calculate normals
let dx = curr.x-prev.x;
let dy = curr.y-prev.y;
let a = createVector(-dy, dx).normalize();
let b = createVector(dy, -dx).normalize();
// calculate midway of the two points
let px = (prev.x+curr.x)/2;
let py = (prev.y+curr.y)/2;
let p = createVector(px,py);
// put created points back along drawed line
a.mult(lineoffset).add(p);
b.mult(lineoffset).add(p);
sideA.push(a);
sideB.push(b);
}
// close paths
if(!sideA[0].equals(sideA[sideA.length-1])) {
sideA.push(sideA[0]);
}
if(!sideB[0].equals(sideB[sideB.length-1])) {
sideB.push(sideB[0]);
}
return {sideA, sideB};
}
}
}
};
let node = document.createElement('div');
window.document.getElementById('p5-container').appendChild(node);
new p5(sketch, node);
body {
background-color:#ffffff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.1.9/p5.js"></script>
<div id="p5-container"></div>
Firstly I'd like to find a way to draw those points in the corresponding corner points of the drawn line so when drawn line has only few points the outlines would retain the drawn shape.
Secondly is there some good way to reduce points on the areas where there are several of them to reduce those loops in small corners and other type errors in generated lines?
Idea is to get points for lines, so it would be easy to detect with line intersection if race car velocity vector crosses it.
Unfortunately I'm not very familiar with math notations so please try to use easy to understand version of them if there is some fancy math that would do the job.
In the future, please try to post a minimal example. I was not able to run your code as you posted it.
That being said, one option you could consider is using the strokeWeight() function to draw the path at different widths. Here's an example:
const path = [];
function setup() {
createCanvas(400, 400);
// Add some default points to the path.
path.push(createVector(0, 0));
path.push(createVector(width/4, height/4));
}
function draw() {
background(220);
// Draw the path with a thick gray line.
strokeWeight(50);
stroke(200);
for(let i = 1; i < path.length; i++){
const prevPoint = path[i-1];
const nextPoint = path[i];
line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
}
// Draw the path with a thin black line.
strokeWeight(1);
stroke(0);
for(let i = 1; i < path.length; i++){
const prevPoint = path[i-1];
const nextPoint = path[i];
line(prevPoint.x, prevPoint.y, nextPoint.x, nextPoint.y);
}
}
// Add a point to the path when the user clicks.
function mousePressed(){
path.push(createVector(mouseX, mouseY));
}
The trick here is to draw the path in two passes. First you draw the path using a thick line, and then you draw the path again, this time using a thin line.

ArrayBuffer and Float64Array in Swift

I'm trying to convert one of the javascript functions into Swift 5 function. But even after so much of extensive search I couldn't find anything on it.
function toArrayBuffer(type, ts, x, y, z, sc) {
let userId = userID.getUserId();
//console.log("Found GPS UserID: " + userId);
if (userId == "Unauthor") {
//console.log("GPS did not find the userID")
return null;
}
let buffer = new ArrayBuffer(8 * 4);
// Offset: 0
let float64Bytes = new Float64Array(buffer);
float64Bytes[0] = ts;
// Offset: 8
let bytes = new Float32Array(buffer);
bytes[2] = x;
bytes[3] = y;
bytes[4] = z;
bytes[5] = sc;
// Offset: 24
let uint8Bytes = new Uint8Array(buffer);
uint8Bytes[24] = type;
for (let i = 0; i < 8; i++) {
if (userId.charCodeAt(i)) {
uint8Bytes[i + 25] = userId.charCodeAt(i);
}
}
return buffer;
}
Basically I tried to build the same function with var byteArray = [UInt8](stringVal.utf8) but UInt8 can store only upto 256, but I had to store epoch time stamp. So, it doesn't work too. Any help would be appreciated.

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

requesting Canvas Text and requestAnimationFrame assistance

I have been working on an issue for a few days now and have been unable to solve it. Please note that I am relatively new to Javascript so not sure if what I have below is the best way to accomplish this but going through this over the last few days has definitely helped me learn some new things.
Also, please note I know I could achieve this very easily using CSS but I wanted to know if there was a Javascript/JQuery solution.
The Issue:
I am attempting to simulate a fadeIn animation on a canvas for some text.
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
The numbers represent font size.
Here is the full code:
$(document).ready(function ()
{
var canvas = $('#myCanvas')[0];
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext('2d');
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
function fadeText(timeStamp, t, x, y)
{
var opacity = timeStamp / 1000;
console.log('Timestamp: ' + timeStamp + ' Opacity: ' + opacity);
console.log('t, x, y |' + t +' | ' + x + ' | ' + y)
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//ctx.fillStyle = 'rgba(178, 34, 34, ' + opacity + ')';
//ctx.fillText(t, x, y);
if (opacity < 1)
{
requestAnimationFrame(function (timestamp)
{
fadeText(timestamp, t, x, y)
});
}
}
function MessageObject(x, y, f, fs, t)
{
this.x = x;
this.y = y;
this.f = f;
this.fs = fs;
this.t = t;
}
var msgArray = [];
function CreateMessageArray(myArray, callback)
{
var i = 0;
var v = 75;
var x = 0;
var y = 0;
var f = '';
var fs = '';
var t = '';
for (t in myArray)
{
fs = myArray[t]; //font size
f = 'italic ' + fs + 'px Bradley Hand'; //font type
x = (canvas.width / 2) //x pos of text
msgArray.push(new MessageObject(x, y, f, fs, t))
y = Number(fs);
//alert('x, y, f, t | ' + x + ' | ' + y + ' | ' + f + ' | ' + t);
}
return callback(msgArray);
}
let ProcessMessageArray = function (myArray)
{
var xPrime = 0;
var yPrime = 0;
for (i = 0; i < myArray.length; i++)
{
var msgObject = myArray[i];
var x = msgObject.x;
var y = msgObject.y;
var f = msgObject.f;
var fs = msgObject.fs;
var t = msgObject.t;
ctx.textBaseline = 'top';
ctx.font = f;
var txtWidth = ctx.measureText(t).width
xPrime = x - (txtWidth / 2);
requestAnimationFrame(function(timestamp)
{
fadeText(timestamp, t, x, y)
});
//ctx.fillStyle = 'rgba(178, 34, 34, 1)';
//ctx.fillText(t, xPrime, yPrime);
if (i === 0)
{
yPrime = Number(yPrime) + (2 * Number(fs) - 35);
}
else
{
yPrime = Number(yPrime) + Number(fs);
}
}
}
CreateMessageArray(introText, ProcessMessageArray)
});
The way it is supposed to work is that the CreateMessageArray function creates an array of objects that contain the x-pos, y-pos, etc. for each of the lines of text in the introTextArray.
The ProcessMessageArray is then responsible for outputting each line of text in the introTextArray into it proper position on the screen.
In the ProcessMessageArray there is a call to a requestAnimationFrame function where I was hoping it would "fade in" each line of the text but what is actually occurring is that I am only getting the last line of text in the IntroTextArray.
I am sure it has to do with the fact that I am calling a requestAnimationFrame within a loop but I am not sure how to accomplish what I want to otherwise. Any advice would be appreciated.
Animating with requestAnimationFrame
RAF requestAnimationFrame
There are many ways that you can use RAF to animate. You can have many RAF functions that will all present the content to the display at the same time if they are all called before the next refresh. But this does incur extra overhead (especially if the rendering load per item is small) and extra head pain. If you have a slow render (for whatever reason, browsers can hang for a 1/60th second without warning) it can mess up the presentation order and you will only get part of the frame rendered.
The easiest way to manage any type of animation is to have a single animation loop that is called via RAF. From that function you call your animations. This is safe from presentation order problems, and can easily switch state. (eg after text fade you may want something else fading in)
Fix
Your code was not able to be saved, and sorry, was DOA, so I rewrote the whole thing. I have added some notes in the comments as to why and what for but if you have questions please do ask in the comments.
The example displays the fading text as two sets (to illustrate changing display state, a state is an abstract and reference to related elements or render (eg intro, middle, end))
The function has one main loop that handles the rendering calls and the timing.
Text is displayed by a function that updates and then displays. it will return true when text has faded in so main loop can set up the next render state.
// Removed jQuery and I dont see the need to use it
// The convention in JavaScript is NOT to capitalize the first character
// unless you function / object is created using new ObjectName
const ctx = canvas.getContext("2d"); // canvas is named in HTML via its id
canvas.width = innerWidth; // window is the global scope you do not need
canvas.height = innerHeight; //to prefix when accesing its properties
// helper
//const log = (...data) => console.log(data.join(","));
// use an array it is better suited to the data you are storing
const introText = [
["Testing one two.", 75],
["Testing..." , 50],
["One, one two.", 50],
["Is this thing on?", 50],
["",1], // to delay next state
["",1], // to delay next state
];
const introText1 = [
["Second stage, state 2", 20],
["The End" , 40],
[":)", 30],
["",10],
["Thanks for watching..",12],
["",1],
["",1],
["Dont forget to vote for any answers you find helpfull..",10],
["",1],
["",10],
["This intro was brought to you by",12],
["",10],
["requestAnimationFrame",12],
["HTML5",12],
["canvas",12],
["JavaScript (AKA) ECMAScript6",12],
["",10],
["Staring...",14],
["Stackoverflow.com",18],
];
const TEXT_FADE_TIME = 1000; // in ms
// create the array of display arrays
const displayLists = [
createDisplayList(8, introText), // converts simple text array to display list
createDisplayList(8, introText1),
];
var curretDisplayListIdx = 0;
requestAnimationFrame(mainLoop); // will start when all code has been parsed
var startTime;
function mainLoop(time){ // keep it simple use one animation frame per frame
if(startTime === undefined) { startTime = time }
const timeSinceStart = time - startTime;
ctx.clearRect(0,0, canvas.width, canvas.height);
if (textFadeIn(timeSinceStart, curretDisplayListIdx )) {
if (curretDisplayListIdx < displayLists.length - 1) {
curretDisplayListIdx += 1;
startTime = time;
}
}
requestAnimationFrame(mainLoop);
}
// creates a display list from text array. top is the start y pos
function createDisplayList(top, array) {
const result = [];
var y = top;
var fontSize;
for (const item of array) {
const fontSize = item[1];
result.push({
font : 'italic ' + fontSize + 'px Bradley Hand',
text : item[0],
x : canvas.width / 2,
y , fontSize,
startTime : null,
fadeTime : TEXT_FADE_TIME,
alpha : 0, // starting alpha
});
y += fontSize;
}
return result;
}
// displays a text display list from the displayLists array.
// time is time since starting to display the list
// displayListIdx is the index
// returns true when text has faded in
function textFadeIn(time, displayListIdx) {
// complete will be true when all have faded in
const complete = updateDisplayList(displayLists[displayListIdx], time);
renderDisplayList(displayLists[displayListIdx]);
return complete;
}
// separate logic from rendering
function updateDisplayList(array, time) {
var fadeNext = true; // used to indicate that the next item should fade in
// for each item
for (const text of array) {
if (fadeNext) { // has the previous items done its thing?
if (text.startTime === null) { text.startTime = time }
}
if(text.startTime !== null){ // if start time is set then fade it in
text.alpha = Math.min(1, (time - text.startTime) / text.fadeTime);
if (text.alpha < 1) { fadeNext = false } // if not complete flag fadeNext to stop next text fading in
}
}
// if last item is fully faded in return true
return array[array.length - 1].alpha === 1;
}
// seperate rendering from logic
function renderDisplayList(array) {
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillStyle = "black";
for (const text of array) {
ctx.font = text.font;
ctx.globalAlpha = text.alpha;
ctx.fillText(text.text,text.x,text.y);
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Categories

Resources