Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using gradient orientations to direct brush stroke effect in Javascript

I'm trying to recreate in Javascript (specifically with p5.js) an effect others seem to have successfully accomplished using the Mathematica suite, as seen here https://mathematica.stackexchange.com/a/39049.

I'm 100% ignorant about Mathematica, but I see they are using a method called GradientOrientationFilter to create a pattern of strokes following the direction of the gradients of the image.

oil brushes

My results are still not satisfying.

The logic I'm attempting

  • create a histogram of oriented gradients, evaluating the luma values, then finding the horizontal and vertical gradient, and it's direction and magnitude;
  • draw a line at each pixel to represent the gradient direction with a random grayscale color. I will use these lines later, blended with the actual picture.

The code:

var img, vectors;

var pixelsToSkip = 2; // for faster rendering we can stroke less lines
var linesLength = 20;
var strokeThickness = 1; 

function preload() { 
  img = loadImage('http://lorempixel.com/300/400/people/1');
  img2 = loadImage('http://lorempixel.com/300/400/people/1');

  /* you can test in local if the directions are correct using a simple gradient as image
  img = loadImage('http://fornace.io/jstests/img/gradient.jpg');
  img2 = loadImage('http://fornace.io/jstests/img/gradient.jpg');
  */
}

function setup() {  
  createCanvas(img.width, img.height);
  noLoop();
  img.loadPixels();


  makeLumas();
  makeGradients();
  makeVectors();

  for ( var xx = 0; xx < img.width; xx = xx + pixelsToSkip) {
    for ( var yy = 0; yy < img.height; yy = yy + pixelsToSkip) {
      push();
        stroke(random(255));  // to color with pixel color change to stroke(img.get(xx, yy));
        strokeWeight(strokeThickness);
        translate(xx,yy);
        rotate( vectors[yy][xx].dir ); // here we use the rotation of the gradient
        line(-linesLength/2, 0, linesLength/2, 0);
      pop();
    }
  }

//      adding the image in overlay to evaluate if the map is good
//      tint(255, 255, 255, 100);
//      image(img2,0,0);


}

function draw() {
}




function makeLumas() {
// calculate the luma for each pixel to get a map of dark/light areas ("Rec. 601") https://en.wikipedia.org/wiki/Luma_(video)
  lumas = new Array(img.height);
  for (var y = 0; y < img.height; y++) {
    lumas[y] = new Array(img.width);

    for (var x = 0; x < img.height; x++) {
      var i = x * 4 + y * 4 * img.width;
      var r = img.pixels[i],
          g = img.pixels[i + 1],
          b = img.pixels[i + 2],
          a = img.pixels[i + 3];

      var luma = a == 0 ? 1 : (r * 299/1000 + g * 587/1000
        + b * 114/1000) / 255;

      lumas[y][x] = luma;
    }
  }
}

function makeGradients() {
// calculate the gradients (kernel [-1, 0, 1])

  var horizontalGradient = verticalGradient = [];

  for (var y = 0; y < img.height; y++) {
    horizontalGradient[y] = new Array(img.width);
    verticalGradient[y] = new Array(img.width);

    var row = lumas[y];

    for (var x = 0; x < img.width; x++) {
      var prevX = x == 0 ? 0 : lumas[y][x - 1];
      var nextX = x == img.width - 1 ? 0 : lumas[y][x + 1];
      var prevY = y == 0 ? 0 : lumas[y - 1][x];
      var nextY = y == img.height - 1 ? 0 : lumas[y + 1][x];

      horizontalGradient[y][x] = -prevX + nextX;
      verticalGradient[y][x] = -prevY + nextY;
    }
  }
}

function makeVectors() {
// calculate direction and magnitude

  vectors = new Array(img.height);

  for (var y = 0; y < img.height; y++) {
    vectors[y] = new Array(img.width);

    for (var x = 0; x < img.width; x++) {
      var prevX = x == 0 ? 0 : lumas[y][x - 1];
      var nextX = x == img.width - 1 ? 0 : lumas[y][x + 1];
      var prevY = y == 0 ? 0 : lumas[y - 1][x];
      var nextY = y == img.height - 1 ? 0 : lumas[y + 1][x];

      var gradientX = -prevX + nextX;
      var gradientY = -prevY + nextY;

      vectors[y][x] = {
        mag: Math.sqrt(Math.pow(gradientX, 2) + Math.pow(gradientY, 2)),
        dir: Math.atan2(gradientY, gradientX)
      }
    }
  }
}

The results on a image

My field made in javascript is much more noisy than the one made in Mathematica.

http://jsfiddle.net/frapporti/b4zxkcmL/

results

Final note

I'm quite new to p5.js, perhaps I'm reinventing the wheel in some passage. Feel free to correct me in this too.

Fiddle: http://jsfiddle.net/frapporti/b4zxkcmL/

like image 725
Francesco Frapporti Avatar asked Oct 31 '22 15:10

Francesco Frapporti


1 Answers

Blur those pixels!

The solution that worked for me was simply blurring before any pixel analysis to smooth out the results of the gradient orientation filter.

If you are using p5.js, this could be as easy as img.filter("blur",5);. Otherwise you can go with any other blurring technique of your choice.

You can see the code here: http://jsfiddle.net/frapporti/b4zxkcmL/21/

I will wait before marking this answer accepted in case someone has other ways.

enter image description here

like image 195
Francesco Frapporti Avatar answered Nov 10 '22 14:11

Francesco Frapporti