Threejs points - set gradient color to points - javascript

I am using threejs to create and render a point cloud. What I would like to do is give each point a specific color based on its Z coordinate. This color should be mapped from some color like green having the smallest Z coordinate, throughout blue, yellow and red having the largest Z values. What is the simplest way to give each point a specific color that follows this gradient approach?
My script is quite long, so I have simplified it to only these few lines of code:
function drawCloud(){
for(i = 0; i < totalPoints * 3; i = i + 3){
//Setting the new Z value of each point
//Each third value is the Z value, therefore using i+2
points.geometry.attributes.position.array[i+2] = theNewZValueForThisPoint;
//Now setting the color of each point
// i, i+1 AND i+2 is the R,G, B value of each point
//here each point is set to a green color
points.geometry.attributes.color.array[i] = 0;
points.geometry.attributes.color.array[i+1] = 1;
points.geometry.attributes.color.array[i+2] = 0;
}
}
function animate() {
requestAnimationFrame( animate );
drawCloud();
render();
}
function render() {
renderer.render( scene, camera );
}
I have already created a kind of segmentation approach where each point gets a fixed color within a range. Something like this:
function getColor() {
if(ZValue > 0 && ZValue < 100){
color = green;
}
if(ZValue > 100 && ZValue < 200){
color = blue;
}
}
This is not what I want, as there would be a region where the color is drastically changed. I would like it to have a more gradient approach that changes slowly as the Z value increases.
Keep in mind that this code has been simplified to a great extent to keep it simple and only show the basic idea. Any recommendations on other improvements would also be appreciated.

Given that you have the minimum z-value and maximum z-value of your point cloud, you can use this function to get the color of each point. You can customize the colors and breakpoints of your gradient in the gradient-array accordingly:
function getColor(z, zMin, zMax) {
// normalize v in the range of vMin and vMax
function normalize(v, vMin, vMax) {
return ((v - vMin) / (vMax - vMin));
}
// clamp a value between min and max inclusive
function clamp(value, min, max) {
if (value >= max) return max;
if (value <= min) return min;
return value;
}
// calculates the linear interpolation of two numbers
function lerp(a, b, alpha) {
return a + (b - a) * clamp(alpha, 0, 1);
}
const zNorm = normalize(z, zMin, zMax);
// gradient definition. Each element defines a breakpoint within the normalized z-range (0 - 1) and color
// important: has to be sorted ascendingly by bp
const gradient = [
{bp: 0, r: 0, g: 1, b: 0},
{bp: 1/3, r: 0, g: 0, b: 1},
{bp: 2/3, r: 1, g: 1, b: 0},
{bp: 1, r: 1, g: 0, b: 0}
];
let red, green, blue;
// find the color segment (between breakpoints), interpolate the color between breakpoints
for(let i = 0, g = gradient.length; i < g; i++) {
if(zNorm < gradient[i].bp || gradient[i].bp === 1) {
red = lerp(gradient[i-1].r, gradient[i].r, normalize(zNorm, gradient[i-1].bp, gradient[i].bp));
green = lerp(gradient[i-1].g, gradient[i].g, normalize(zNorm, gradient[i-1].bp, gradient[i].bp));
blue = lerp(gradient[i-1].b, gradient[i].b, normalize(zNorm, gradient[i-1].bp, gradient[i].bp));
break;
}
}
return {r: red, g: green, b: blue};
}

Related

JavaScript - Get RGBA values from bitmap buffer

Hello I'm using NodeJs and a package named "Jimp".
I'm trying to get the Red, Green, Blue, and Alpha values for each pixel but am having trouble.
Here's my code:
Jimp.read("https://i.imgur.com/eOu89DY.png").then((image) => {
console.log(image)
for (var y = 0; y < image.bitmap.height; y = y + 3) {
for (var x = 0; x < image.bitmap.width; x = x + 3) {
//--Returns the whole bitmap buffer
console.log(red)
var red = image.bitmap.data;
}
}
and here's the example usage from https://www.npmjs.com/package/jimp :
image.scan(0, 0, image.bitmap.width, image.bitmap.height, function(x, y, idx) {
// x, y is the position of this pixel on the image
// idx is the position start position of this rgba tuple in the bitmap Buffer
// this is the image
var red = this.bitmap.data[idx + 0];
var green = this.bitmap.data[idx + 1];
var blue = this.bitmap.data[idx + 2];
var alpha = this.bitmap.data[idx + 3];
// rgba values run from 0 - 255
// e.g. this.bitmap.data[idx] = 0; // removes red from this pixel
});
Using my code, how could I get the red, green, blue values using variables from the buffer? How do I define IDX like the example usage from the npmjs site???
If you look down a little in the linked page.
Alternatively, you can manipulate individual pixels using the
following these functions:
image.getPixelColor(x, y); // returns the colour of that pixel e.g. 0xFFFFFFFF
...
...
Two static helper functions exist to convert RGBA values into single integer (hex) values:
Jimp.rgbaToInt(r, g, b, a); // e.g. converts 255, 255, 255, 255 to
Jimp.intToRGBA(hex); // e.g. converts 0xFFFFFFFF to {r: 255, g: 255, b: 255, a:255}

Finding the closest indexed color value to the current color in javascript / p5.js

I have an array of "indexed" RGBA color values, and for any given image that I load, I want to be able to run through all the color values of the loaded pixels, and match them to the closest of my indexed color values. So, if the pixel in the image had a color of, say, RGBA(0,0,10,1), and the color RGBA(0,0,0,1) was my closest indexed value, it would adjust the loaded pixel to RGBA(0,0,0,1).
I know PHP has a function imagecolorclosest
int imagecolorclosest( $image, $red, $green, $blue )
Does javascript / p5.js / processing have anything similar? What's the easiest way to compare one color to another. Currently I can read the pixels of the image with this code (using P5.js):
let img;
function preload() {
img = loadImage('assets/00.jpg');
}
function setup() {
image(img, 0, 0, width, height);
let d = pixelDensity();
let fullImage = 4 * (width * d) * (height * d);
loadPixels();
for (let i = 0; i < fullImage; i+=4) {
let curR = pixels[i];
let curG = pixels[i]+1;
let curB = pixels[i+2];
let curA = pixels[i+3];
}
updatePixels();
}
Each color consists 3 color channels. Imagine the color as a point in a 3 dimensional space, where each color channel (red, green, blue) is associated to one dimension. You've to find the closest color (point) by the Euclidean distance. The color with the lowest distance is the "closest" color.
In p5.js you can use p5.Vector for vector arithmetic. The Euclidean distance between to points can be calculated by .dist(). So the distance between points respectively "colors" a and b can be expressed by:
let a = createVector(r1, g1, b1);
let b = createVector(r2, g2, b2);
let distance = a.dist(b);
Use the expression somehow like this:
colorTable = [[r0, g0, b0], [r1, g1, b1] ... ];
int closesetColor(r, g, b) {
let a = createVector(r, g, b);
let minDistance;
let minI;
for (let i=0; i < colorTable; ++i) {
let b = createVector(...colorTable[i]);
let distance = a.dist(b);
if (!minDistance || distance < minDistance) {
minI = i; minDistance = distance;
}
}
return minI;
}
function setup() {
image(img, 0, 0, width, height);
let d = pixelDensity();
let fullImage = 4 * (width * d) * (height * d);
loadPixels();
for (let i = 0; i < fullImage; i+=4) {
let closestI = closesetColor(pixels[i], pixels[i+1], pixels[i+2])
pixels[i] = colorTable[closestI][0];
pixels[i+1] = colorTable[closestI][1];
pixels[i+2] = colorTable[closestI][2];
}
updatePixels();
}
If I understand you correctly you want to keep the colors of an image within a certain limited pallet. If so, you should apply this function to each pixel of your image. It will give you the closest color value to a supplied pixel from a set of limited colors (indexedColors).
// example color pallet (no alpha)
indexedColors = [
[0, 10, 0],
[0, 50, 0]
];
// Takes pixel with no alpha value
function closestIndexedColor(color) {
var closest = {};
var dist;
for (var i = 0; i < indexedColors.length; i++) {
dist = Math.pow(indexedColors[i][0] - color[0], 2);
dist += Math.pow(indexedColors[i][1] - color[1], 2);
dist += Math.pow(indexedColors[i][2] - color[2], 2);
dist = Math.sqrt(dist);
if(!closest.dist || closest.dist > dist){
closest.dist = dist;
closest.color = indexedColors[i];
}
}
// returns closest match as RGB array without alpha
return closest.color;
}
// example usage
closestIndexedColor([0, 20, 0]); // returns [0, 10, 0]
It works the way that the PHP function you mentioned does. If you treat the color values as 3d coordinate points then the closet colors will be the ones with the smallest 3d "distance" between them. This 3d distance is calculated using the distance formula:

Find specific color in image

I have an image like this:
and like to find a specific color in the image (#ffa400 / #ffffff) and get their percentage occurrence value (orange maybe: 75%, white: 25%)
NOTE: Keep in mind that I don't like to get the average color (in this case: orange) like Color Thief does, but I'D like to find a SPECIFIC color.
Below is a snippet, so this is all that I got so far, but the code doesn't seem working & my idea also doesn't seem like an efficient & fast way.
function extract_colors(img) {
var canvas = document.createElement("canvas");
var c = canvas.getContext('2d');
c.width = canvas.width = img.width;
c.height = canvas.height = img.height;
c.clearRect(0, 0, c.width, c.height);
c.drawImage(img, 0, 0, img.width, img.height);
return getColors(c);
}
function getColors(c) {
var col, colors = {};
var pixels, r, g, b, a;
r = g = b = a = 0;
pixels = c.getImageData(0, 0, c.width, c.height);
for (var i = 0, data = pixels.data; i < data.length; i + = 4) {
r = data[i];
g = data[i + 1];
b = data[i + 2];
a = data[i + 3];
if (a < (255 / 2))
continue;
col = rgbToHex(r, g, b);
if (col == 'ffa400') { // find color #ffa400
if (!colors[col])
colors[col] = 0;
colors[col] + +;
}
}
return colors;
}
function rgbToHex(r, g, b) {
return ((r << 16) | (g << 8) | b).toString(16);
}
var img = document.getElementById('img');
out.innerHTML = extract_colors(img)
<img id='img' width=100 src='https://i.stack.imgur.com/EPDlQ.png'>
<p id='out'>...</p>
EDIT 1.0
I'm trying to use a code like this, but it doesn't seems to work either.
var orangeMatches=0, whiteMatches=0
Jimp.read("https://i.stack.imgur.com/EPDlQ.png").then(function (image) {
image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (x, y, idx) {
var red = this.bitmap.data[ idx + 0 ];
var green = this.bitmap.data[ idx + 1 ];
var blue = this.bitmap.data[ idx + 2 ];
var alpha = this.bitmap.data[ idx + 3 ];
if (red == 255 && green == 164 && blue == 0 && alpha == 1){
orangeMatches++;
}
if (red == 255 && green == 255 && blue == 255 && alpha == 1){
whiteMatches++;
}
});
console.log(orangeMatches, whiteMatches)
});
Edit 2.0
I'd like to have some kind of tolerant value (10%) so that there is not every color to be counted, but the colors (e.g #ffa400 #ffa410) that are matching together are counted together.
A few notes
The main color in your picture is actually ffa500, not ffa400. There are in fact no occurances of ffa400 at all.
Also keep in mind that this is an anti-aliased bitmap graphic. Visually we see only two colors, but to smoothen the transition from one color to another a set of "in-between-colors" are generated. In other words, the sum of ffffff and ffa500 will not be exactly 100%. (This is why the output from my function below will display a lot more colors than the two in question)
Solution 1
The basic idea of this solution is to get every color in the picture, store these in one common object where the occurrence of each color is counted. When this object is returned, we can later count the occurrence of some specific color from the output.
This will return something like:
{
ffa500: 7802,
ffa501: 4,
ffa502: 2,
...,
ffffff: 1919,
total: 10000
}
Now you can access the occurences of a given color this way:
var imageData = extract_colors(img),
white = imageData.ffffff,
total = imageData.total;
And to display how many percent of the image a given color covers, do something like this (toFixed() is simply to limit the value to two decimals):
whitePct = (white / total * 100).toFixed(2);
Working sample
function getColors(ctx) {
// Get the canvas data
var pixels = ctx.getImageData(0, 0, ctx.width, ctx.height),
data = pixels.data,
// Set up our output object to collect color data
output = {};
// For each color we encounter, check the
// output object. If the color already exists
// there, simply increase its counted value.
// If it does not, create a new key.
for (var i = 0; i < data.length; i+=4) {
var r = data[i],
g = data[i + 1],
b = data[i + 2],
col = rgbToHex(r, g, b);
if( output[col] )
output[col]++
else
output[col] = 1
}
// Count total
var total = 0;
for(var key in output) {
total = total + parseInt(output[key])
}
output.total = total;
// Return the color data as an object
return output;
}
// Our elements
var img = document.getElementById('img'),
out = document.getElementById('out');
// Retrieve the image data
var imageData = extract_colors(img),
// Count our given colors
white = imageData.ffffff,
orange = imageData.ffa500,
total = imageData.total,
// Calculate percentage value
whitePct = (white / total * 100).toFixed(2),
orangePct = (orange / total * 100).toFixed(2);
out.innerHTML = `White: ${whitePct}%<br/>Orange: ${orangePct}%`;
// See the console for all colors identified
console.log(extract_colors(img))
// ----- These functions are left untouched ----- \\
function extract_colors(img) {
var canvas = document.createElement("canvas");
var c = canvas.getContext('2d');
c.width = canvas.width = img.width;
c.height = canvas.height = img.height;
c.clearRect(0, 0, c.width, c.height);
c.drawImage(img, 0, 0, img.width, img.height);
return getColors(c);
}
function rgbToHex(r, g, b) {
return ((r << 16) | (g << 8) | b).toString(16);
}
p, img {
margin: 10px 5px;
float: left
}
<!-- Image converted to base64 to keep it locally availible -->
<img id='img' width=100 src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAADImlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4QUUwNzk4N0I2QjIxMUUzQUQzMkIzMTA4REQ0Nzc1MiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4QUUwNzk4OEI2QjIxMUUzQUQzMkIzMTA4REQ0Nzc1MiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI1NDU5MjAyQjZCMTExRTNBRDMyQjMxMDhERDQ3NzUyIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjhBRTA3OTg2QjZCMjExRTNBRDMyQjMxMDhERDQ3NzUyIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwv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'>
<p id='out'>...</p>
Solution 2
(Consider this solution a draft. It might need some tweaking if to be used for a live website, but at least it should work!)
The idea here is to call extract_colors() with the colors we want to map as arguments. A common object is built to hold the output, and this is populated with one object for each color. The color objects consists of a counter and an rgb value. Using the isNeighborColor() function, the rgb value is used to match not only the specific color but also any color close to it. Adjust the tolerance level as you want. (Ideally this would be another argument I guess).
Call the function specifying the image and what colors to count the occurrence of:
var imageData = extract_colors(img, 'ffffff', 'ffa500'),
This will output an object for each color:
{
"ffffff": {
"counter": 1979,
"rgb": {
"r": 255,
"g": 255,
"b": 255
}
},
"ffa500": {
"counter": 7837,
"rgb": {
"r": 255,
"g": 165,
"b": 0
}
},
"total": 9816
}
What we want is to access the counter values:
var white = imageData.ffffff.counter;
Working sample
// Added a rest parameter for unlimited color input
function extract_colors(img, ...colors) {
var canvas = document.createElement("canvas");
var c = canvas.getContext('2d');
c.width = canvas.width = img.width;
c.height = canvas.height = img.height;
c.clearRect(0, 0, c.width, c.height);
c.drawImage(img, 0, 0, img.width, img.height);
return getColors(c, ...colors);
}
// Unchanged
function rgbToHex(r, g, b) {
return ((r << 16) | (g << 8) | b).toString(16);
}
// From: https://stackoverflow.com/a/5624139/2311559
function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
// From: https://stackoverflow.com/a/11506531/2311559
// Slightly modified
function isNeighborColor(color1, color2, tolerance) {
tolerance = tolerance || 32;
return Math.abs(color1.r - color2.r) <= tolerance
&& Math.abs(color1.g - color2.g) <= tolerance
&& Math.abs(color1.b - color2.b) <= tolerance;
}
function getColors(ctx, ...colors) {
var pixels = ctx.getImageData(0, 0, ctx.width, ctx.height),
data = pixels.data,
output = {};
// Build an 'output' object. This will hold one object for
// each color we want to check. The color objects will
// each consist of a counter and an rgb value. The counter
// is used to count the occurrence of each color. The rgb
// value is used to match colors with a certain tolerance
// using the 'isNeighborColor()' function above.
for (var i = 0; i < colors.length; i++) {
output[colors[i]] = {
counter: 0,
rgb: hexToRgb(colors[i])
};
}
// For each pixel, match its color against the colors in our
// 'output' object. Using the 'isNeighborColor()' function
// we will also match colors close to our input, given the
// tolerance defined.
for (var i = 0; i < data.length; i+=4) {
var r = data[i],
g = data[i + 1],
b = data[i + 2],
colobj = {r, g, b},
col = rgbToHex(r, g, b);
Object.keys(output).map(function(objectKey, index) {
var rgb = output[objectKey].rgb,
count = output[objectKey].counter;
if(isNeighborColor(rgb, colobj)) {
output[objectKey].counter = count+1;
}
});
}
// Count total
var total = 0;
for(var key in output) {
total = total + parseInt(output[key].counter)
}
output.total = total;
// Return the color data as an object
return output;
}
// Our elements
var img = document.getElementById('img'),
out = document.getElementById('out');
// Retrieve the image data
var imageData = extract_colors(img,'ffffff','ffa500'),
// Count our given colors
white = imageData.ffffff.counter,
orange = imageData.ffa500.counter,
total = imageData.total,
// Calculate percentage value
whitePct = (white / total * 100).toFixed(2),
orangePct = (orange / total * 100).toFixed(2);
out.innerHTML = `White: ${whitePct}%<br/>Orange: ${orangePct}%`;
// Optional console output
console.log(extract_colors(img,'ffffff','ffa500'));
p, img {
margin: 10px 5px;
float: left
}
<!-- Image converted to base64 to keep it locally availible -->
<img id='img' width=100 src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAAZdEVYdFNvZnR3YXJlAEFkb2JlIEltYWdlUmVhZHlxyWU8AAADImlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS4wLWMwNjAgNjEuMTM0Nzc3LCAyMDEwLzAyLzEyLTE3OjMyOjAwICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4QUUwNzk4N0I2QjIxMUUzQUQzMkIzMTA4REQ0Nzc1MiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo4QUUwNzk4OEI2QjIxMUUzQUQzMkIzMTA4REQ0Nzc1MiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkI1NDU5MjAyQjZCMTExRTNBRDMyQjMxMDhERDQ3NzUyIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjhBRTA3OTg2QjZCMjExRTNBRDMyQjMxMDhERDQ3NzUyIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwv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'>
<p id='out'>...</p>

Javascript algorithm/function to generate RGB values for a color along the visible light spectrum

I'm looking for a Javascript function/algorithm that will generate an RGB value for a color that's p% along the "visible light" spectrum.
For example - given the visible light spectrum...
... and given p=0, the function should return the RGB of red (0% across this spectrum. Given p=50, the function should approximately return the RBG of green (50% across the spectrum). Finally, given p=100, the function would return the RBG of violet.
The function doesn't need to be technically/scientifically precise - I'm just looking for a reasonably close approximation, with reasonable sensitivity. (E.g., the RGB for p=76 should be different than for p=77).
Ideally, I'm looking for a pure math formula. I.e., I'd like to avoid actually loading the image below onto a Canvas, than using context.getImageData to find the color of a pixel p% across the image.
Of course, I have no idea if that's possible...
You can use the HSVtoRGB function from this answer to create something easily.
function HSVtoRGB(h, s, v) {
var r, g, b, i, f, p, q, t;
if (arguments.length === 1) {
s = h.s, v = h.v, h = h.h;
}
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v, g = t, b = p; break;
case 1: r = q, g = v, b = p; break;
case 2: r = p, g = v, b = t; break;
case 3: r = p, g = q, b = v; break;
case 4: r = t, g = p, b = v; break;
case 5: r = v, g = p, b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
function rainbow(p) {
var rgb = HSVtoRGB(p/100.0*0.85, 1.0, 1.0);
return 'rgb('+rgb.r+','+rgb.g+','+rgb.b+')';
}
for(var i=0; i<100; i++) {
var span = document.createElement('span');
span.style.backgroundColor = span.style.color = rainbow(i);
span.textContent = 'i';
document.body.appendChild(span);
}
You can use this lib https://github.com/moagrius/Color to set a color and change hue (https://en.wikipedia.org/wiki/Hue).
Example:
var color = new Color('#FF0000'); // red
color.hue(150);
color.getRGB(); // rgb(0, 255, 128) cerulean
color.hue(300);
color.getRGB(); // rgb(255, 0, 255) violet
color.hue(0);
color.getRGB(); // rgb(255, 0, 0) red
Ideally, I'm looking for a pure math formula. I.e., I'd like to avoid
actually loading the image below onto a Canvas, than using
context.getImageData to find the color of a pixel p% across the image.
The visible light spectrum can't be put into a formula, because which color we see at a specific wavelength doesn't follow a mathematical term. The human eye and brain are much too complicated for that!
Yes, you can display every color using a red, green and blue light bulb like a PC screen. But that doesn't mean that yellow, cyan and magenta divide the light spectrum into accurate thirds! In computer science, however, this is easily assumed to be true.
This makes it easy to calculate on the colors:
Color hue [°] r g b hex
------------------------------------------
red 0 1 0 0 FF0000
yellow 60 1 1 0 FFFF00
green 120 0 1 0 00FF00
cyan 180 0 1 1 00FFFF
blue 240 0 0 1 0000FF
magenta 300 1 0 1 FF00FF
You can display the colors on a wheel: the left on is the one a programmer would prefer, the right one is more like a visible light spectrum:
Now you can easily see why the rgb value can't be calculated if you want to use the visible light spectrum: It doesn't follow any rule. Apart from that, there isn't just one visible light spectrum: Google it, you'll see that every image is slightly different.
I'd like to know what's the specific reason for getting the rgb value of a specific light wavelength: You can instead use the hue as the other answers suggest, but be aware that the hue has different proportions than the visible light spectrum.
If real proportions are important, you will have to load an image into a canvas (an image height of 1 pixel would be enough ;) ) or you can store all the values in a JSON array.

hough transform - javascript - node.js

So, i'm trying to implement hough transform, this version is 1-dimensional (its for all dims reduced to 1 dim optimization) version based on the minor properties.
Enclosed is my code, with a sample image... input and output.
Obvious question is what am i doing wrong. I've tripled check my logic and code and it looks good also my parameters. But obviously i'm missing on something.
Notice that the red pixels are supposed to be ellipses centers , while the blue pixels are edges to be removed (belong to the ellipse that conform to the mathematical equations).
also, i'm not interested in openCV / matlab / ocatve / etc.. usage (nothing against them).
Thank you very much!
var fs = require("fs"),
Canvas = require("canvas"),
Image = Canvas.Image;
var LEAST_REQUIRED_DISTANCE = 40, // LEAST required distance between 2 points , lets say smallest ellipse minor
LEAST_REQUIRED_ELLIPSES = 6, // number of found ellipse
arr_accum = [],
arr_edges = [],
edges_canvas,
xy,
x1y1,
x2y2,
x0,
y0,
a,
alpha,
d,
b,
max_votes,
cos_tau,
sin_tau_sqr,
f,
new_x0,
new_y0,
any_minor_dist,
max_minor,
i,
found_minor_in_accum,
arr_edges_len,
hough_file = 'sample_me2.jpg',
edges_canvas = drawImgToCanvasSync(hough_file); // make sure everything is black and white!
arr_edges = getEdgesArr(edges_canvas);
arr_edges_len = arr_edges.length;
var hough_canvas_img_data = edges_canvas.getContext('2d').getImageData(0, 0, edges_canvas.width,edges_canvas.height);
for(x1y1 = 0; x1y1 < arr_edges_len ; x1y1++){
if (arr_edges[x1y1].x === -1) { continue; }
for(x2y2 = 0 ; x2y2 < arr_edges_len; x2y2++){
if ((arr_edges[x2y2].x === -1) ||
(arr_edges[x2y2].x === arr_edges[x1y1].x && arr_edges[x2y2].y === arr_edges[x1y1].y)) { continue; }
if (distance(arr_edges[x1y1],arr_edges[x2y2]) > LEAST_REQUIRED_DISTANCE){
x0 = (arr_edges[x1y1].x + arr_edges[x2y2].x) / 2;
y0 = (arr_edges[x1y1].y + arr_edges[x2y2].y) / 2;
a = Math.sqrt((arr_edges[x1y1].x - arr_edges[x2y2].x) * (arr_edges[x1y1].x - arr_edges[x2y2].x) + (arr_edges[x1y1].y - arr_edges[x2y2].y) * (arr_edges[x1y1].y - arr_edges[x2y2].y)) / 2;
alpha = Math.atan((arr_edges[x2y2].y - arr_edges[x1y1].y) / (arr_edges[x2y2].x - arr_edges[x1y1].x));
for(xy = 0 ; xy < arr_edges_len; xy++){
if ((arr_edges[xy].x === -1) ||
(arr_edges[xy].x === arr_edges[x2y2].x && arr_edges[xy].y === arr_edges[x2y2].y) ||
(arr_edges[xy].x === arr_edges[x1y1].x && arr_edges[xy].y === arr_edges[x1y1].y)) { continue; }
d = distance({x: x0, y: y0},arr_edges[xy]);
if (d > LEAST_REQUIRED_DISTANCE){
f = distance(arr_edges[xy],arr_edges[x2y2]); // focus
cos_tau = (a * a + d * d - f * f) / (2 * a * d);
sin_tau_sqr = (1 - cos_tau * cos_tau);//Math.sqrt(1 - cos_tau * cos_tau); // getting sin out of cos
b = (a * a * d * d * sin_tau_sqr ) / (a * a - d * d * cos_tau * cos_tau);
b = Math.sqrt(b);
b = parseInt(b.toFixed(0));
d = parseInt(d.toFixed(0));
if (b > 0){
found_minor_in_accum = arr_accum.hasOwnProperty(b);
if (!found_minor_in_accum){
arr_accum[b] = {f: f, cos_tau: cos_tau, sin_tau_sqr: sin_tau_sqr, b: b, d: d, xy: xy, xy_point: JSON.stringify(arr_edges[xy]), x0: x0, y0: y0, accum: 0};
}
else{
arr_accum[b].accum++;
}
}// b
}// if2 - LEAST_REQUIRED_DISTANCE
}// for xy
max_votes = getMaxMinor(arr_accum);
// ONE ellipse has been detected
if (max_votes != null &&
(max_votes.max_votes > LEAST_REQUIRED_ELLIPSES)){
// output ellipse details
new_x0 = parseInt(arr_accum[max_votes.index].x0.toFixed(0)),
new_y0 = parseInt(arr_accum[max_votes.index].y0.toFixed(0));
setPixel(hough_canvas_img_data,new_x0,new_y0,255,0,0,255); // Red centers
// remove the pixels on the detected ellipse from edge pixel array
for (i=0; i < arr_edges.length; i++){
any_minor_dist = distance({x:new_x0, y: new_y0}, arr_edges[i]);
any_minor_dist = parseInt(any_minor_dist.toFixed(0));
max_minor = b;//Math.max(b,arr_accum[max_votes.index].d); // between the max and the min
// coloring in blue the edges we don't need
if (any_minor_dist <= max_minor){
setPixel(hough_canvas_img_data,arr_edges[i].x,arr_edges[i].y,0,0,255,255);
arr_edges[i] = {x: -1, y: -1};
}// if
}// for
}// if - LEAST_REQUIRED_ELLIPSES
// clear accumulated array
arr_accum = [];
}// if1 - LEAST_REQUIRED_DISTANCE
}// for x2y2
}// for xy
edges_canvas.getContext('2d').putImageData(hough_canvas_img_data, 0, 0);
writeCanvasToFile(edges_canvas, __dirname + '/hough.jpg', function() {
});
function getMaxMinor(accum_in){
var max_votes = -1,
max_votes_idx,
i,
accum_len = accum_in.length;
for(i in accum_in){
if (accum_in[i].accum > max_votes){
max_votes = accum_in[i].accum;
max_votes_idx = i;
} // if
}
if (max_votes > 0){
return {max_votes: max_votes, index: max_votes_idx};
}
return null;
}
function distance(point_a,point_b){
return Math.sqrt((point_a.x - point_b.x) * (point_a.x - point_b.x) + (point_a.y - point_b.y) * (point_a.y - point_b.y));
}
function getEdgesArr(canvas_in){
var x,
y,
width = canvas_in.width,
height = canvas_in.height,
pixel,
edges = [],
ctx = canvas_in.getContext('2d'),
img_data = ctx.getImageData(0, 0, width, height);
for(x = 0; x < width; x++){
for(y = 0; y < height; y++){
pixel = getPixel(img_data, x,y);
if (pixel.r !== 0 &&
pixel.g !== 0 &&
pixel.b !== 0 ){
edges.push({x: x, y: y});
}
} // for
}// for
return edges
} // getEdgesArr
function drawImgToCanvasSync(file) {
var data = fs.readFileSync(file)
var canvas = dataToCanvas(data);
return canvas;
}
function dataToCanvas(imagedata) {
img = new Canvas.Image();
img.src = new Buffer(imagedata, 'binary');
var canvas = new Canvas(img.width, img.height);
var ctx = canvas.getContext('2d');
ctx.patternQuality = "best";
ctx.drawImage(img, 0, 0, img.width, img.height,
0, 0, img.width, img.height);
return canvas;
}
function writeCanvasToFile(canvas, file, callback) {
var out = fs.createWriteStream(file)
var stream = canvas.createPNGStream();
stream.on('data', function(chunk) {
out.write(chunk);
});
stream.on('end', function() {
callback();
});
}
function setPixel(imageData, x, y, r, g, b, a) {
index = (x + y * imageData.width) * 4;
imageData.data[index+0] = r;
imageData.data[index+1] = g;
imageData.data[index+2] = b;
imageData.data[index+3] = a;
}
function getPixel(imageData, x, y) {
index = (x + y * imageData.width) * 4;
return {
r: imageData.data[index+0],
g: imageData.data[index+1],
b: imageData.data[index+2],
a: imageData.data[index+3]
}
}
It seems you try to implement the algorithm of Yonghong Xie; Qiang Ji (2002). A new efficient ellipse detection method 2. p. 957.
Ellipse removal suffers from several bugs
In your code, you perform the removal of found ellipse (step 12 of the original paper's algorithm) by resetting coordinates to {-1, -1}.
You need to add:
`if (arr_edges[x1y1].x === -1) break;`
at the end of the x2y2 block. Otherwise, the loop will consider -1, -1 as a white point.
More importantly, your algorithm consists in erasing every point which distance to the center is smaller than b. b supposedly is the minor axis half-length (per the original algorithm). But in your code, variable b actually is the latest (and not most frequent) half-length, and you erase points with a distance lower than b (instead of greater, since it's the minor axis). In other words, you clear all points inside a circle with a distance lower than latest computed axis.
Your sample image can actually be processed with a clearing of all points inside a circle with a distance lower than selected major axis with:
max_minor = arr_accum[max_votes.index].d;
Indeed, you don't have overlapping ellipses and they are spread enough. Please consider a better algorithm for overlapping or closer ellipses.
The algorithm mixes major and minor axes
Step 6 of the paper reads:
For each third pixel (x, y), if the distance between (x, y) and (x0,
y0) is greater than the required least distance for a pair of pixels
to be considered then carry out the following steps from (7) to (9).
This clearly is an approximation. If you do so, you will end up considering points further than the minor axis half length, and eventually on the major axis (with axes swapped). You should make sure the distance between the considered point and the tested ellipse center is smaller than currently considered major axis half-length (condition should be d <= a). This will help with the ellipse erasing part of the algorithm.
Also, if you also compare with the least distance for a pair of pixels, as per the original paper, 40 is too large for the smaller ellipse in your picture. The comment in your code is wrong, it should be at maximum half the smallest ellipse minor axis half-length.
LEAST_REQUIRED_ELLIPSES is too small
This parameter is also misnamed. It is the minimum number of votes an ellipse should get to be considered valid. Each vote corresponds to a pixel. So a value of 6 means that only 6+2 pixels make an ellipse. Since pixels coordinates are integers and you have more than 1 ellipse in your picture, the algorithm might detect ellipses that are not, and eventually clear edges (especially when combined with the buggy ellipse erasing algorithm). Based on tests, a value of 100 will find four of the five ellipses of your picture, while 80 will find them all. Smaller values will not find the proper centers of the ellipses.
Sample image is not black & white
Despite the comment, sample image is not exactly black and white. You should convert it or apply some threshold (e.g. RGB values greater than 10 instead of simply different form 0).
Diff of minimum changes to make it work is available here:
https://gist.github.com/pguyot/26149fec29ffa47f0cfb/revisions
Finally, please note that parseInt(x.toFixed(0)) could be rewritten Math.floor(x), and you probably want to not truncate all floats like this, but rather round them, and proceed where needed: the algorithm to erase the ellipse from the picture would benefit from non truncated values for the center coordinates. This code definitely could be improved further, for example it currently computes the distance between points x1y1 and x2y2 twice.

Categories

Resources