Skip to content

Instantly share code, notes, and snippets.

@xettri
Created December 16, 2019 11:20
Show Gist options
  • Save xettri/6e05763616361ff65cc1f5fe90697ee2 to your computer and use it in GitHub Desktop.
Save xettri/6e05763616361ff65cc1f5fe90697ee2 to your computer and use it in GitHub Desktop.
Simple face detection in js
/*
Compact version of picojs https://github.com/tehnokv/picojs
live demo :- https://codepen.io/bcrazydreamer/pen/JjobdQM
*/
FaceDetect = {}
FaceDetect.unpack_cascade = function(bytes)
{
const dview = new DataView(new ArrayBuffer(4));
let p = 8;
dview.setUint8(0, bytes[p+0]), dview.setUint8(1, bytes[p+1]), dview.setUint8(2, bytes[p+2]), dview.setUint8(3, bytes[p+3]);
const tdepth = dview.getInt32(0, true);
p = p + 4
dview.setUint8(0, bytes[p+0]), dview.setUint8(1, bytes[p+1]), dview.setUint8(2, bytes[p+2]), dview.setUint8(3, bytes[p+3]);
const ntrees = dview.getInt32(0, true);
p = p + 4
const tcodes_ls = [];
const tpreds_ls = [];
const thresh_ls = [];
for(let t=0; t<ntrees; ++t)
{
Array.prototype.push.apply(tcodes_ls, [0, 0, 0, 0]);
Array.prototype.push.apply(tcodes_ls, bytes.slice(p, p+4*Math.pow(2, tdepth)-4));
p = p + 4*Math.pow(2, tdepth)-4;
for(let i=0; i<Math.pow(2, tdepth); ++i)
{
dview.setUint8(0, bytes[p+0]), dview.setUint8(1, bytes[p+1]), dview.setUint8(2, bytes[p+2]), dview.setUint8(3, bytes[p+3]);
tpreds_ls.push(dview.getFloat32(0, true));
p = p + 4;
}
dview.setUint8(0, bytes[p+0]), dview.setUint8(1, bytes[p+1]), dview.setUint8(2, bytes[p+2]), dview.setUint8(3, bytes[p+3]);
thresh_ls.push(dview.getFloat32(0, true));
p = p + 4;
}
const tcodes = new Int8Array(tcodes_ls);
const tpreds = new Float32Array(tpreds_ls);
const thresh = new Float32Array(thresh_ls);
function classify_region(r, c, s, pixels, ldim)
{
r = 256*r;
c = 256*c;
let root = 0;
let o = 0.0;
const pow2tdepth = Math.pow(2, tdepth) >> 0;
for(let i=0; i<ntrees; ++i)
{
idx = 1;
for(let j=0; j<tdepth; ++j)
idx = 2*idx + (pixels[((r+tcodes[root + 4*idx + 0]*s) >> 8)*ldim+((c+tcodes[root + 4*idx + 1]*s) >> 8)]<=pixels[((r+tcodes[root + 4*idx + 2]*s) >> 8)*ldim+((c+tcodes[root + 4*idx + 3]*s) >> 8)]);
o = o + tpreds[pow2tdepth*i + idx-pow2tdepth];
if(o<=thresh[i])
return -1;
root += 4*pow2tdepth;
}
return o - thresh[ntrees-1];
}
return classify_region;
}
FaceDetect.run_cascade = function(image, classify_region, params)
{
const pixels = image.pixels;
const nrows = image.nrows;
const ncols = image.ncols;
const ldim = image.ldim;
const shiftfactor = params.shiftfactor;
const minsize = params.minsize;
const maxsize = params.maxsize;
const scalefactor = params.scalefactor;
let scale = minsize;
const detections = [];
while(scale<=maxsize)
{
const step = Math.max(shiftfactor*scale, 1) >> 0;
const offset = (scale/2 + 1) >> 0;
for(let r=offset; r<=nrows-offset; r+=step)
for(let c=offset; c<=ncols-offset; c+=step)
{
const q = classify_region(r, c, scale, pixels, ldim);
if (q > 0.0)
detections.push([r, c, scale, q]);
}
scale = scale*scalefactor;
}
return detections;
}
FaceDetect.cluster_detections = function(dets, iouthreshold)
{
dets = dets.sort(function(a, b) {
return b[3] - a[3];
});
function calculate_iou(det1, det2)
{
const r1=det1[0], c1=det1[1], s1=det1[2];
const r2=det2[0], c2=det2[1], s2=det2[2];
const overr = Math.max(0, Math.min(r1+s1/2, r2+s2/2) - Math.max(r1-s1/2, r2-s2/2));
const overc = Math.max(0, Math.min(c1+s1/2, c2+s2/2) - Math.max(c1-s1/2, c2-s2/2));
return overr*overc/(s1*s1+s2*s2-overr*overc);
}
const assignments = new Array(dets.length).fill(0);
const clusters = [];
for(let i=0; i<dets.length; ++i)
{
if(assignments[i]==0)
{
let r=0.0, c=0.0, s=0.0, q=0.0, n=0;
for(let j=i; j<dets.length; ++j)
if(calculate_iou(dets[i], dets[j])>iouthreshold)
{
assignments[j] = 1;
r = r + dets[j][0];
c = c + dets[j][1];
s = s + dets[j][2];
q = q + dets[j][3];
n = n + 1;
}
clusters.push([r/n, c/n, s/n, q]);
}
}
return clusters;
}
FaceDetect.instantiate_detection_memory = function(size)
{
let n = 0;
const memory = [];
for(let i=0; i<size; ++i)
memory.push([]);
function update_memory(dets)
{
memory[n] = dets;
n = (n+1)%memory.length;
dets = [];
for(i=0; i<memory.length; ++i)
dets = dets.concat(memory[i]);
//
return dets;
}
return update_memory;
}
FaceDetect.update_memory = FaceDetect.instantiate_detection_memory(5);
FaceDetect.facefinder_classify_region = function (r, c, s, pixels, ldim) { return -1.0; };
var cascadeurl = 'https://raw.githubusercontent.com/nenadmarkus/pico/c2e81f9d23cc11d1a612fd21e4f9de0921a5d0d9/rnt/cascades/facefinder';
fetch(cascadeurl).then(function (response) {
response.arrayBuffer().then(function (buffer) {
var bytes = new Int8Array(buffer);
FaceDetect.facefinder_classify_region = FaceDetect.unpack_cascade(bytes);
})
})
FaceDetect.rgba_to_grayscale = function(rgba, nrows, ncols){
var gray = new Uint8Array(nrows * ncols);
for (var r = 0; r < nrows; ++r)
for (var c = 0; c < ncols; ++c)
gray[r * ncols + c] = (2 * rgba[r * 4 * ncols + 4 * c + 0] + 7 * rgba[r * 4 * ncols + 4 * c + 1] + 1 * rgba[r * 4 * ncols + 4 * c + 2]) / 10;
return gray;
}
FaceDetect.processfn = function (video, ctx, option) {
option = Object.prototype.toString.call(option) === "[object Object]" ? option : {};
ctx.drawImage(video, 0, 0);
var width = !isNaN(option.video_width) ? Number(option.video_width) : 640;
var height = !isNaN(option.video_height) ? Number(option.video_height) : 480;
var rgba = ctx.getImageData(0, 0, width, height).data;
image = {
"pixels": FaceDetect.rgba_to_grayscale(rgba, height, width),
"nrows": height,
"ncols": width,
"ldim": width
}
params = {
"shiftfactor": 0.1,
"minsize": 100,
"maxsize": 1000,
"scalefactor": 1.1
}
dets = FaceDetect.run_cascade(image, FaceDetect.facefinder_classify_region, params);
dets = FaceDetect.update_memory(dets);
dets = FaceDetect.cluster_detections(dets, 0.2);
for (i = 0; i < dets.length; ++i)
if (dets[i][3] > 50.0) {
ctx.beginPath();
ctx.arc(dets[i][1], dets[i][0], dets[i][2] / 2, 0, 2 * Math.PI, false);
ctx.lineWidth = 3;
ctx.strokeStyle = 'red';
ctx.stroke();
}
}
FaceDetect.start = function(ctx,option){
var process = FaceDetect.processfn;
var self = this
this.ctx = ctx
this.process = process
var streamContainer = document.createElement('div')
this.video = document.createElement('video')
this.video.setAttribute('autoplay', '1')
this.video.setAttribute('playsinline', '1')
this.video.setAttribute('width', 1)
this.video.setAttribute('height', 1)
streamContainer.appendChild(this.video)
document.body.appendChild(streamContainer)
navigator.mediaDevices.getUserMedia({video: true, audio: false}).then(function(stream) {
self.video.srcObject = stream
self.update()
}, function(err) {
throw err
})
this.update = function() {
var self = this
var last = Date.now()
var loop = function() {
var dt = Date.now - last
self.process(self.video,ctx,option)
last = Date.now()
requestAnimationFrame(loop)
}
requestAnimationFrame(loop)
}
}
/*-----------------Usage----------------------*/
//simply run
var ctx = document.getElementsByTagName('canvas');
//(optional) you can ignore option it will take default size its for quality of stream.
var option = {video_height:100, video_width : 100};
FaceDetect.start(ctx, option);
/*--------------------------------------------*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment