Created
December 16, 2019 11:20
-
-
Save xettri/6e05763616361ff65cc1f5fe90697ee2 to your computer and use it in GitHub Desktop.
Simple face detection in js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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