block by emeeks 2acc14409f7108826fab

2acc14409f7108826fab

Full Screen

Refresh to see a new image.

Image processing with canvas. First, the RGB grid is read using getImageData then that grid is compacted so every 9x9 pixel region becomes a pixel with a value equal to the “Most dominant color” in that region. Color is defined as a hue region (with high or low luminosity defined as white and black). The resulting compacted grid is then fed into d3.geom.concaveHull to draw a concave hull around the regions of similar color, which is then filled with the hue and the average saturation and luminosity of that hue across the entire image.

index.html


<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="utf-8" />
<title>Image Hulling</title>
<style>
  canvas {
    position: absolute;
  }

  svg {
    position: absolute;
  }

  #cv-new {
    top: 500px;
  }

  #cv {
    opacity: 0;
  }
  img {
    position: fixed;
    top: 0;
    left: 600px;
    max-width: 500px;
    z-index: 99;
  }
</style>
</head>
<body>
<img></img>
<canvas id="cv" width="1000" height="500" ></canvas>
<svg width="1000" height="500" ></svg>

<script src="//d3js.org/d3.v3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="colornames.js" charset="utf-8" type="text/javascript"></script>
<script src="d3.geom.concaveHull.js" charset="utf-8" type="text/javascript"></script>

<script type="text/javascript">

var gridSize = 3;
var lessDominant = true;
var localColorization = true;

var canvas = document.getElementById('cv');
context = canvas.getContext('2d');

var img = new Image();
var rando = 'source' + (parseInt(Math.random() * 6) + 1) + '.jpg';

img.src = rando;

d3.select("img").attr("src", rando)

img.onload = function () {
  d3.select("canvas").attr("width", img.width).attr("height", img.height)
  d3.select("svg").attr("width", img.width).attr("height", img.height)
  process(img.width, img.height);
};


function process(imageWidth, imageHeight) {

    context.drawImage(img, 0, 0);
    var imageData = context.getImageData(0, 0, imageWidth, imageHeight);

    var hslArray = [];
    var hslGrid = [[]];

    var cx = 0;
    var cy = 0;

    for (var x = 0; x<imageData.data.length;x = x + 4) {
      var r = x;
      var g = x + 1;
      var b = x + 2;

      var hsl = d3.hsl(d3.rgb(imageData.data[r],imageData.data[g],imageData.data[b],255));

      var group = "Red";
      var hue = hsl.h;

      if (hsl.l <= .1) {
        group = "black"
        hsl.h = 360;
      } else if (hsl.l >= .9) {
        group = "white"
        hsl.h = 0;
      } else if (hue < 10) {
        group = "Red";
      } else if (hue < 20) {
          group = "Orange Red";
      } else if (hue < 30) {
          group = "Safety Orange";
      } else if (hue < 40) {
          group = "Dark Orange";
      } else if (hue < 50) {
          group = "Amber";
      } else if (hue < 60) {
          group = "Golden Yellow";
      } else if (hue < 70) {
          group = "Chartreuse Yellow";
      } else if (hue < 80) {
          group = "Electric Lime";
      } else if (hue < 90) {
          group = "Spring Bud";
      } else if (hue < 100) {
          group = "Bright Green";
      } else if (hue < 110) {
          group = "Harlequin";
      } else if (hue < 130) {
          group = "Lime";
      } else if (hue < 140) {
          group = "Free Speech Green";
      } else if (hue < 160) {
          group = "Spring Green";
      } else if (hue < 170) {
          group = "Medium Spring Green";
      } else if (hue < 190) {
          group = "Aqua";
      } else if (hue < 200) {
          group = "Deep Sky Blue";
      } else if (hue < 220) {
          group = "Dodger Blue";
      } else if (hue < 250) {
          group = "Blue";
      } else if (hue < 260) {
          group = "Han Purple";
      } else if (hue < 270) {
          group = "Electric Indigo";
      } else if (hue < 290) {
          group = "Electric Purple";
      } else if (hue < 300) {
          group = "Psychedelic Purple";
      } else if (hue < 310) {
          group = "Magenta";
      } else if (hue < 320) {
          group = "Hot Magenta";
      } else if (hue < 330) {
          group = "Hollywood Cerise";
      } else if (hue < 340) {
          group = "Deep Pink";
      } else if (hue < 350) {
          group = "Torch Red";
      }

      var hsl = {x: cx, y: cy, hsl: hsl, group: group};

      hslArray.push(hsl);
      hslGrid[cy].push(hsl);
      cx = cx + 1;
      if (cx === imageWidth) {
        cx = 0;
        cy = cy + 1;
        hslGrid[cy] = [];
      }
    }

    var data = hslArray.map(d => [d.x, d.y]);

    var canvasWidth = imageWidth;
    var canvasHeight = imageHeight;

    var gridX = 0;

    var gridY = 0;

    var compactGrid = [];

    while (gridY < canvasHeight) {
      while (gridX < canvasWidth) {

        var stepY = 0;
        var stepX = 0;

        var region = [];

        for (x = 0; x<gridSize; x++) {
          for (y = 0; y<gridSize; y++) {
            if(hslGrid[y + gridY] && hslGrid[y + gridY][x + gridX]) {
              region.push(hslGrid[y + gridY][x + gridX]);
            }
          }
        }

        var hue = dominantColor(region.map(d => d.group));
        var color = d3.hsl(hue, d3.mean(region.map(d => d.hsl.s)), d3.mean(region.map(d => d.hsl.l)))
        if (hue === undefined) {
          css = "none";
        }
        else {
          css = color.toString();
        }

        cArray = [];
        cArray.push(color.h);
        cArray.push(color.s);
        cArray.push(color.l);
        cArray.push(255);
        var compactCell = {color: cArray, css: css, x: gridX, y: gridY};

        compactGrid.push(compactCell);

        gridX = gridX + gridSize;
      }
      gridX = 0;
      gridY = gridY + gridSize;
    }
    hull = d3.geom.concaveHull().distance(gridSize * 3);

    for (x in colorHash) {
      onlyThisColor = compactGrid.filter(d => d.color[0] === colorHash[x]);
      var averageSaturation = d3.mean(onlyThisColor.map(d => d.color[1]))
      var averageLuminosity = d3.mean(onlyThisColor.map(d => d.color[2]))

      console.log("******************")
      console.log(x)
      console.log(x === "black" || x === "white" ? x : d3.hsl(colorHash[x], averageSaturation, averageLuminosity).toString())
      console.log(onlyThisColor)

    if (onlyThisColor.length > 3) {
      d3.select("svg").append("g")
        .attr("transform", "translate(0,0)")
        .selectAll("path")
          .data(hull(onlyThisColor.map(d => [d.x + (Math.random()), d.y + (Math.random())])))
        .enter().append("path")
          .attr("d", function(d) { return "M" + d.join("L") + "Z"; })
          .style("fill-opacity", .9)
          .style("fill", x === "black" || x === "white" ? x : d3.hsl(colorHash[x], averageSaturation, averageLuminosity).toString())

    }

    }

    function dominantColor(colorArray) {
      var colorCensus = {};
      var colorLength = colorArray.length;
      colorArray.forEach(color => {
        colorCensus[color] ? colorCensus[color]++ : colorCensus[color] = 1;
      });
      for (x in colorCensus) {
        if (colorCensus[x] * 2 >= colorLength) {
          return colorHash[x];
        }
      }
      if (lessDominant) {
        var max = 0;
        var value = "none";
        for (x in colorCensus) {
          if (colorCensus[x] > max) {
            value = colorHash[x];
          }
        }
      }
      return value;
    }



}

</script>
</body>
</html>

colornames.js

colorHash = {
            "white": 0,
            "black": 360,
            "Orange Red": 15,
            "Safety Orange": 25,
            "Dark Orange": 35,
            "Amber": 45,
            "Golden Yellow": 55,
            "Chartreuse Yellow": 65,
            "Electric Lime": 75,
            "Spring Bud": 85,
            "Bright Green": 95,
            "Harlequin": 120,
            "Lime": 125,
            "Free Speech Green": 135,
            "Spring Green": 160,
            "Medium Spring Green": 180,
            "Aqua": 185,
            "Deep Sky Blue": 195,
            "Dodger Blue": 210,
            "Blue": 235,
            "Han Purple": 255,
            "Electric Indigo": 280,
            "Psychedelic Purple": 295,
            "Magenta": 305,
            "Hot Magenta": 315,
            "Hollywood Cerise": 325,
            "Deep Pink": 335,
            "Torch Red": 345,
            "Red": 5
          }

d3.geom.concaveHull.js

(function() {
d3.geom.concaveHull = function() {
  var calculateDistance = stdevDistance,
  padding = 0,
  delaunay;


function distance(a, b) {
    var dx = a[0]-b[0],
    dy = a[1]-b[1];
    return Math.sqrt((dx * dx) + (dy * dy));
}

function stdevDistance(delaunay) {
  var sides = [];
  delaunay.forEach(function (d) {
    sides.push(distance(d[0],d[1]));
    sides.push(distance(d[0],d[2]));
    sides.push(distance(d[1],d[2]));
  });

  var dev = d3.deviation(sides);
  var mean = d3.mean(sides);

  return mean + dev;
}

function concaveHull(vertices) {

  delaunay = d3.geom.delaunay(vertices);

  var longEdge = calculateDistance(delaunay);

  mesh = delaunay.filter(function (d) {
    return distance(d[0],d[1]) < longEdge && distance(d[0],d[2]) < longEdge && distance(d[1],d[2]) < longEdge
  })

  var counts = {},
      edges = {},
      r,
      result = [],
      deletionList = [];
  // Traverse the edges of all triangles and discard any edges that appear twice.
  mesh.forEach(function(triangle) {
    for (var i = 0; i < 3; i++) {
      var edge = [triangle[i], triangle[(i + 1) % 3]].sort(ascendingCoords).map(String);
      (edges[edge[0]] = (edges[edge[0]] || [])).push(edge[1]);
      (edges[edge[1]] = (edges[edge[1]] || [])).push(edge[0]);
      var k = edge.join(":");
      if (counts[k]) deletionList.push(k);
      else counts[k] = 1;
    }
  });

  deletionList.forEach(function (k) {
    delete counts[k];
  })

  while (1) {
    var k = null;
    // Pick an arbitrary starting point on a boundary.
    for (k in counts) break;
    if (k == null) break;
    result.push(r = k.split(":").map(function(d) { return d.split(",").map(Number); }));
    delete counts[k];
    var q = r[1];
    while (q[0] !== r[0][0] || q[1] !== r[0][1]) {
      var p = q,
          qs = edges[p.join(",")],
          n = qs.length;
      for (var i = 0; i < n; i++) {
        q = qs[i].split(",").map(Number);
        var edge = [p, q].sort(ascendingCoords).join(":");
        if (counts[edge]) {
          delete counts[edge];
          r.push(q);
          break;
        }
      }
    }
  }

  if (padding !== 0) {
    result = pad(result, padding);
  }


  return result;
}

function pad(bounds, amount) {
  var result = [];
  bounds.forEach(function(bound) {
    var padded = [];

    var area = 0;
    bound.forEach(function(p, i) {
      // http://forums.esri.com/Thread.asp?c=2&f=1718&t=174277
      // Area = Area + (X2 - X1) * (Y2 + Y1) / 2

      var im1 = i - 1;
      if(i == 0) {
        im1 = bound.length - 1;
      }
      var pm = bound[im1];
      area += (p[0] - pm[0]) * (p[1] + pm[1]) / 2; 
    });
    var handedness = 1;
    if(area > 0) handedness = -1
    bound.forEach(function(p, i) {
      // average the tangent between 
      var im1 = i - 1;
      if(i == 0) {
        im1 = bound.length - 2;
      }
      //var tp = getTangent(p, bound[ip1]);
      var tm = getTangent(p, bound[im1]);
      //var avg = { x: (tp.x + tm.x)/2, y: (tp.y + tm.y)/2 };
      //var normal = rotate2d(avg, 90);
      var normal = rotate2d(tm, 90 * handedness);
      padded.push([p[0] + normal.x * amount, p[1] + normal.y * amount ])
    })
    result.push(padded)
  })
  return result
}

function getTangent(a, b) {
  var vector = { x: b[0] - a[0], y: b[1] - a[1] }
  var magnitude = Math.sqrt(vector.x*vector.x + vector.y*vector.y);
  vector.x /= magnitude;
  vector.y /= magnitude;
  return vector
}
function rotate2d(vector, angle) {
  //rotate a vector
  angle *= Math.PI/180; //convert to radians
  return {
    x: vector.x * Math.cos(angle) - vector.y * Math.sin(angle),
    y: vector.x * Math.sin(angle) + vector.y * Math.cos(angle)
  }
}

function ascendingCoords(a, b) {
  return a[0] === b[0] ? b[1] - a[1] : b[0] - a[0];
}

concaveHull.padding = function (newPadding) {
  if (!arguments.length) return padding;
  padding = newPadding;
  return concaveHull;
}

concaveHull.distance = function (newDistance) {
  if (!arguments.length) return calculateDistance;
  calculateDistance = newDistance;
  if (typeof newDistance === "number") {
    calculateDistance = function () {return newDistance};
  }
  return concaveHull;
}

return concaveHull;
}
})()