mirror of https://github.com/vladmandic/human
implemented image filters
parent
5884c8cfe4
commit
aaeb842cf6
19
README.md
19
README.md
|
@ -223,6 +223,23 @@ config = {
|
|||
scoped: false, // enable scoped runs
|
||||
// some models *may* have memory leaks, this wrapps everything in a local scope at a cost of performance
|
||||
// typically not needed
|
||||
filter: {
|
||||
enabled: true, // enable image pre-processing filters
|
||||
return: true, // return processed canvas imagedata in result
|
||||
brightness: 0, // range: -1 (darken) to 1 (lighten)
|
||||
contrast: 0, // range: -1 (reduce contrast) to 1 (increase contrast)
|
||||
sharpness: 0, // range: 0 (no sharpening) to 1 (maximum sharpening)
|
||||
blur: 0, // range: 0 (no blur) to N (blur radius in pixels)
|
||||
saturation: 0, // range: -1 (reduce saturation) to 1 (increase saturation)
|
||||
hue: 0, // range: 0 (no change) to 360 (hue rotation in degrees)
|
||||
negative: false, // image negative
|
||||
sepia: false, // image sepia colors
|
||||
vintage: false, // image vintage colors
|
||||
kodachrome: false, // image kodachrome colors
|
||||
technicolor: false, // image technicolor colors
|
||||
polaroid: false, // image polaroid camera effect
|
||||
pixelate: 0, // range: 0 (no pixelate) to N (number of pixels to pixelate)
|
||||
},
|
||||
face: {
|
||||
enabled: true, // controls if specified modul is enabled
|
||||
// face.enabled is required for all face models: detector, mesh, iris, age, gender, emotion
|
||||
|
@ -352,6 +369,7 @@ result = {
|
|||
backend, // time to initialize tf backend
|
||||
load, // time to load models
|
||||
sanity, // time for input verification
|
||||
image, // time for image processing
|
||||
body, // model time
|
||||
hand, // model time
|
||||
face, // model time
|
||||
|
@ -416,5 +434,6 @@ Library can also be used on mobile devices
|
|||
- Body Pose Detection: [**PoseNet**](https://medium.com/tensorflow/real-time-human-pose-estimation-in-the-browser-with-tensorflow-js-7dd0bc881cd5)
|
||||
- Age & Gender Prediction: [**SSR-Net**](https://github.com/shamangary/SSR-Net)
|
||||
- Emotion Prediction: [**Oarriaga**](https://github.com/oarriaga/face_classification)
|
||||
- Image Filters: [**WebGLImageFilter**](https://github.com/phoboslab/WebGLImageFilter)
|
||||
|
||||
<hr>
|
||||
|
|
17
config.js
17
config.js
|
@ -7,6 +7,23 @@ export default {
|
|||
scoped: false, // enable scoped runs
|
||||
// some models *may* have memory leaks, this wrapps everything in a local scope at a cost of performance
|
||||
// typically not needed
|
||||
filter: {
|
||||
enabled: true, // enable image pre-processing filters
|
||||
return: true, // return processed canvas imagedata in result
|
||||
brightness: 0, // range: -1 (darken) to 1 (lighten)
|
||||
contrast: 0, // range: -1 (reduce contrast) to 1 (increase contrast)
|
||||
sharpness: 0, // range: 0 (no sharpening) to 1 (maximum sharpening)
|
||||
blur: 0, // range: 0 (no blur) to N (blur radius in pixels)
|
||||
saturation: 0, // range: -1 (reduce saturation) to 1 (increase saturation)
|
||||
hue: 0, // range: 0 (no change) to 360 (hue rotation in degrees)
|
||||
negative: false, // image negative
|
||||
sepia: false, // image sepia colors
|
||||
vintage: false, // image vintage colors
|
||||
kodachrome: false, // image kodachrome colors
|
||||
technicolor: false, // image technicolor colors
|
||||
polaroid: false, // image polaroid camera effect
|
||||
pixelate: 0, // range: 0 (no pixelate) to N (number of pixels to pixelate)
|
||||
},
|
||||
face: {
|
||||
enabled: true, // controls if specified modul is enabled
|
||||
// face.enabled is required for all face models: detector, mesh, iris, age, gender, emotion
|
||||
|
|
|
@ -27,6 +27,7 @@ const ui = {
|
|||
// configuration overrides
|
||||
const config = {
|
||||
backend: 'webgl', // if you want to use 'wasm' backend, enable script load of tf and tf-backend-wasm in index.html
|
||||
filter: { enabled: true, brightness: 0, contrast: 0, sharpness: 0, blur: 0, saturation: 0, hue: 0, negative: false, sepia: false, vintage: false, kodachrome: false, technicolor: false, polaroid: false, pixelate: 0 },
|
||||
face: {
|
||||
enabled: true,
|
||||
detector: { maxFaces: 10, skipFrames: 10, minConfidence: 0.5, iouThreshold: 0.3, scoreThreshold: 0.7 },
|
||||
|
@ -42,6 +43,7 @@ const config = {
|
|||
|
||||
// global variables
|
||||
let menu;
|
||||
let menuFX;
|
||||
let worker;
|
||||
let timeStamp;
|
||||
const fps = [];
|
||||
|
@ -75,7 +77,8 @@ function drawResults(input, result, canvas) {
|
|||
|
||||
// draw image from video
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(input, 0, 0, input.width, input.height, 0, 0, canvas.width, canvas.height);
|
||||
if (result.canvas) ctx.drawImage(result.canvas, 0, 0, result.canvas.width, result.canvas.height, 0, 0, canvas.width, canvas.height);
|
||||
else ctx.drawImage(input, 0, 0, input.width, input.height, 0, 0, canvas.width, canvas.height);
|
||||
// draw all results
|
||||
draw.face(result.face, canvas, ui, human.facemesh.triangulation);
|
||||
draw.body(result.body, canvas, ui);
|
||||
|
@ -193,7 +196,7 @@ async function processImage(input) {
|
|||
const result = await human.detect(image, config);
|
||||
drawResults(image, result, canvas);
|
||||
const thumb = document.createElement('canvas');
|
||||
thumb.width = (window.innerWidth - menu.width) / (ui.columns + 0.1);
|
||||
thumb.width = window.innerWidth / (ui.columns + 0.1);
|
||||
thumb.height = canvas.height / (window.innerWidth / thumb.width);
|
||||
thumb.style.margin = '8px';
|
||||
thumb.style.boxShadow = '4px 4px 4px 0 dimgrey';
|
||||
|
@ -237,11 +240,12 @@ async function detectSampleImages() {
|
|||
}
|
||||
|
||||
function setupMenu() {
|
||||
menu = new Menu(document.body);
|
||||
menu.addTitle('...');
|
||||
menu = new Menu(document.body, '...', { top: '1rem', right: '1rem' });
|
||||
menu.addButton('Start Video', 'Pause Video', (evt) => detectVideo(evt));
|
||||
menu.addButton('Process Images', 'Process Images', () => detectSampleImages());
|
||||
|
||||
menu.addHTML('<hr style="min-width: 200px; border-style: inset; border-color: dimgray">');
|
||||
menu.addBool('Use Web Worker', ui, 'useWorker');
|
||||
menu.addHTML('<hr style="min-width: 200px; border-style: inset; border-color: dimgray">');
|
||||
menu.addLabel('Enabled Models');
|
||||
menu.addBool('Face Detect', config.face, 'enabled');
|
||||
|
@ -281,18 +285,33 @@ function setupMenu() {
|
|||
config.hand.iouThreshold = parseFloat(val);
|
||||
});
|
||||
|
||||
menu.addHTML('<hr style="min-width: 200px; border-style: inset; border-color: dimgray">');
|
||||
menu.addLabel('UI Options');
|
||||
menu.addBool('Use Web Worker', ui, 'useWorker');
|
||||
menu.addBool('Camera Front/Back', ui, 'facing', () => setupCamera());
|
||||
menu.addBool('Use 3D Depth', ui, 'useDepth');
|
||||
menu.addBool('Draw Boxes', ui, 'drawBoxes');
|
||||
menu.addBool('Draw Points', ui, 'drawPoints');
|
||||
menu.addBool('Draw Polygons', ui, 'drawPolygons');
|
||||
menu.addBool('Fill Polygons', ui, 'fillPolygons');
|
||||
|
||||
menu.addHTML('<hr style="min-width: 200px; border-style: inset; border-color: dimgray">');
|
||||
menu.addChart('FPS', 'FPS');
|
||||
|
||||
menuFX = new Menu(document.body, '...', { top: '1rem', right: '18rem' });
|
||||
menuFX.addLabel('UI Options');
|
||||
menuFX.addBool('Camera Front/Back', ui, 'facing', () => setupCamera());
|
||||
menuFX.addBool('Use 3D Depth', ui, 'useDepth');
|
||||
menuFX.addBool('Draw Boxes', ui, 'drawBoxes');
|
||||
menuFX.addBool('Draw Points', ui, 'drawPoints');
|
||||
menuFX.addBool('Draw Polygons', ui, 'drawPolygons');
|
||||
menuFX.addBool('Fill Polygons', ui, 'fillPolygons');
|
||||
menuFX.addHTML('<hr style="min-width: 200px; border-style: inset; border-color: dimgray">');
|
||||
menuFX.addLabel('Image Filters');
|
||||
menuFX.addBool('Enabled', config.filter, 'enabled');
|
||||
menuFX.addRange('Brightness', config.filter, 'brightness', -1.0, 1.0, 0.05, (val) => config.filter.brightness = parseFloat(val));
|
||||
menuFX.addRange('Contrast', config.filter, 'contrast', -1.0, 1.0, 0.05, (val) => config.filter.contrast = parseFloat(val));
|
||||
menuFX.addRange('Sharpness', config.filter, 'sharpness', 0, 1.0, 0.05, (val) => config.filter.sharpness = parseFloat(val));
|
||||
menuFX.addRange('Blur', config.filter, 'blur', 0, 20, 1, (val) => config.filter.blur = parseInt(val));
|
||||
menuFX.addRange('Saturation', config.filter, 'saturation', -1.0, 1.0, 0.05, (val) => config.filter.saturation = parseFloat(val));
|
||||
menuFX.addRange('Hue', config.filter, 'hue', 0, 360, 5, (val) => config.filter.hue = parseInt(val));
|
||||
menuFX.addRange('Pixelate', config.filter, 'pixelate', 0, 32, 1, (val) => config.filter.pixelate = parseInt(val));
|
||||
menuFX.addBool('Negative', config.filter, 'negative');
|
||||
menuFX.addBool('Sepia', config.filter, 'sepia');
|
||||
menuFX.addBool('Vintage', config.filter, 'vintage');
|
||||
menuFX.addBool('Kodachrome', config.filter, 'kodachrome');
|
||||
menuFX.addBool('Technicolor', config.filter, 'technicolor');
|
||||
menuFX.addBool('Polaroid', config.filter, 'polaroid');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
|
|
86
demo/menu.js
86
demo/menu.js
|
@ -1,7 +1,12 @@
|
|||
let instance = 0;
|
||||
|
||||
const css = `
|
||||
.menu-container { display: block; background: darkslategray; position: fixed; top: 0rem; right: 0; width: fit-content; padding: 0 0.8rem 0 0.8rem; line-height: 1.8rem; z-index: 10; max-height: calc(100% - 4rem); box-shadow: 0 0 8px dimgrey; }
|
||||
.menu-container:hover { box-shadow: 0 0 8px lightgrey; }
|
||||
.menu { display: flex; white-space: nowrap; background: darkslategray; padding: 0.2rem; width: max-content; }
|
||||
.menu { position: fixed; top: 0rem; right: 0; width: fit-content; padding: 0 0.8rem 0 0.8rem; line-height: 1.8rem; z-index: 10; max-height: calc(100% - 4rem); box-shadow: 0 0 8px dimgrey; background: darkslategray; }
|
||||
.menu:hover { box-shadow: 0 0 8px lightgrey; }
|
||||
.menu-container { display: block; max-height: 100vh; }
|
||||
.menu-container-fadeout { max-height: 0; overflow: hidden; transition: max-height, 0.5s ease; }
|
||||
.menu-container-fadein { max-height: 100vh; overflow: hidden; transition: max-height, 0.5s ease; }
|
||||
.menu-item { display: flex; white-space: nowrap; background: darkslategray; padding: 0.2rem; width: max-content; }
|
||||
.menu-title { text-align: right; cursor: pointer; }
|
||||
.menu-hr { margin: 0.2rem; border: 1px solid rgba(0, 0, 0, 0.5) }
|
||||
.menu-label { padding: 0; }
|
||||
|
@ -33,31 +38,54 @@ function createCSS() {
|
|||
document.getElementsByTagName('head')[0].appendChild(el);
|
||||
}
|
||||
|
||||
function createElem(parent) {
|
||||
function createMenu(parent, title, position = { top: null, left: null, bottom: null, right: null }) {
|
||||
const el = document.createElement('div');
|
||||
el.id = 'menu';
|
||||
el.className = 'menu-container';
|
||||
el.id = `menu-${instance}`;
|
||||
el.className = 'menu';
|
||||
if (position) {
|
||||
if (position.top) el.style.top = position.top;
|
||||
if (position.bottom) el.style.bottom = position.bottom;
|
||||
if (position.left) el.style.left = position.left;
|
||||
if (position.right) el.style.right = position.right;
|
||||
}
|
||||
|
||||
const elContainer = document.createElement('div');
|
||||
elContainer.id = `menu-container-${instance}`;
|
||||
elContainer.className = 'menu-container menu-container-fadein';
|
||||
|
||||
const elTitle = document.createElement('div');
|
||||
elTitle.className = 'menu-title';
|
||||
elTitle.id = `menu-title-${instance}`;
|
||||
elTitle.innerHTML = title;
|
||||
el.appendChild(elTitle);
|
||||
elTitle.addEventListener('click', () => {
|
||||
elContainer.classList.toggle('menu-container-fadeout');
|
||||
elContainer.classList.toggle('menu-container-fadein');
|
||||
});
|
||||
el.appendChild(elContainer);
|
||||
if (typeof parent === 'object') parent.appendChild(el);
|
||||
else document.getElementById(parent).appendChild(el);
|
||||
return el;
|
||||
return [el, elContainer];
|
||||
}
|
||||
|
||||
class Menu {
|
||||
constructor(parent) {
|
||||
constructor(parent, title, position) {
|
||||
createCSS();
|
||||
this.menu = createElem(parent);
|
||||
this._id = 0;
|
||||
[this.menu, this.container] = createMenu(parent, title, position);
|
||||
this.id = 0;
|
||||
this.instance = instance;
|
||||
instance++;
|
||||
this._maxFPS = 0;
|
||||
this.hidden = 0;
|
||||
}
|
||||
|
||||
get newID() {
|
||||
this._id++;
|
||||
return `menu-${this._id}`;
|
||||
this.id++;
|
||||
return `menu-${this.instance}-${this.id}`;
|
||||
}
|
||||
|
||||
get ID() {
|
||||
return `menu-${this._id}`;
|
||||
return `menu-${this.instance}-${this.id}`;
|
||||
}
|
||||
|
||||
get width() {
|
||||
|
@ -77,23 +105,25 @@ class Menu {
|
|||
el.addEventListener('click', () => {
|
||||
this.hidden = !this.hidden;
|
||||
const all = document.getElementsByClassName('menu');
|
||||
for (const item of all) item.style.display = this.hidden ? 'none' : 'flex';
|
||||
for (const item of all) {
|
||||
item.style.display = this.hidden ? 'none' : 'flex';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async addLabel(title) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu menu-label';
|
||||
el.className = 'menu-item menu-label';
|
||||
el.id = this.newID;
|
||||
el.innerHTML = title;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
}
|
||||
|
||||
async addBool(title, object, variable, callback) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu';
|
||||
el.className = 'menu-item';
|
||||
el.innerHTML = `<div class="menu-checkbox"><input class="menu-checkbox" type="checkbox" id="${this.newID}" ${object[variable] ? 'checked' : ''}/><label class="menu-checkbox-label" for="${this.ID}"></label></div>${title}`;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
el.addEventListener('change', (evt) => {
|
||||
object[variable] = evt.target.checked;
|
||||
if (callback) callback(evt.target.checked);
|
||||
|
@ -102,9 +132,9 @@ class Menu {
|
|||
|
||||
async addRange(title, object, variable, min, max, step, callback) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu';
|
||||
el.className = 'menu-item';
|
||||
el.innerHTML = `<input class="menu-range" type="range" id="${this.newID}" min="${min}" max="${max}" step="${step}" value="${object[variable]}">${title}`;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
el.addEventListener('change', (evt) => {
|
||||
object[variable] = evt.target.value;
|
||||
evt.target.setAttribute('value', evt.target.value);
|
||||
|
@ -114,22 +144,22 @@ class Menu {
|
|||
|
||||
async addHTML(html) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu';
|
||||
el.className = 'menu-item';
|
||||
el.id = this.newID;
|
||||
if (html) el.innerHTML = html;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
}
|
||||
|
||||
async addButton(titleOn, titleOff, callback) {
|
||||
const el = document.createElement('button');
|
||||
el.className = 'menu menu-button';
|
||||
el.className = 'menu-item menu-button';
|
||||
el.style.fontFamily = document.body.style.fontFamily;
|
||||
el.style.fontSize = document.body.style.fontSize;
|
||||
el.style.fontVariant = document.body.style.fontVariant;
|
||||
el.type = 'button';
|
||||
el.id = this.newID;
|
||||
el.innerText = titleOn;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
el.addEventListener('click', () => {
|
||||
if (el.innerText === titleOn) el.innerText = titleOff;
|
||||
else el.innerText = titleOn;
|
||||
|
@ -139,10 +169,10 @@ class Menu {
|
|||
|
||||
async addValue(title, val) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu';
|
||||
el.className = 'menu-item';
|
||||
el.id = title;
|
||||
el.innerText = `${title}: ${val}`;
|
||||
this.menu.appendChild(el);
|
||||
this.contaner.appendChild(el);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
@ -153,10 +183,10 @@ class Menu {
|
|||
|
||||
async addChart(title, id) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'menu menu-chart-title';
|
||||
el.className = 'menu-item menu-chart-title';
|
||||
el.id = this.newID;
|
||||
el.innerHTML = `${title}<canvas id="menu-canvas-${id}" class="menu-chart-canvas" width="180px" height="40px"></canvas>`;
|
||||
this.menu.appendChild(el);
|
||||
this.container.appendChild(el);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"rimraf": "^3.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation demo/node.js",
|
||||
"start": "node --trace-warnings --unhandled-rejections=strict --trace-uncaught --no-deprecation src/node.js",
|
||||
"lint": "eslint src/*.js demo/*.js",
|
||||
"build-iife": "esbuild --bundle --platform=browser --sourcemap --target=esnext --format=iife --minify --external:fs --global-name=human --metafile=dist/human.json --outfile=dist/human.js src/human.js",
|
||||
"build-esm-bundle": "esbuild --bundle --platform=browser --sourcemap --target=esnext --format=esm --minify --external:fs --metafile=dist/human.esm.json --outfile=dist/human.esm.js src/human.js",
|
||||
|
|
|
@ -34,7 +34,6 @@ class HandDetector {
|
|||
async getBoundingBoxes(input) {
|
||||
const batchedPrediction = this.model.predict(input);
|
||||
const prediction = batchedPrediction.squeeze();
|
||||
console.log(prediction);
|
||||
// Regression score for each anchor point.
|
||||
const scores = tf.tidy(() => tf.sigmoid(tf.slice(prediction, [0, 0], [-1, 1])).squeeze());
|
||||
// Bounding box for each anchor point.
|
||||
|
|
81
src/human.js
81
src/human.js
|
@ -4,11 +4,14 @@ const ssrnet = require('./ssrnet/ssrnet.js');
|
|||
const emotion = require('./emotion/emotion.js');
|
||||
const posenet = require('./posenet/posenet.js');
|
||||
const handpose = require('./handpose/handpose.js');
|
||||
const fxImage = require('./imagefx.js');
|
||||
const defaults = require('../config.js').default;
|
||||
const app = require('../package.json');
|
||||
|
||||
let config;
|
||||
let fx;
|
||||
let state = 'idle';
|
||||
let offscreenCanvas;
|
||||
|
||||
// object that contains all initialized models
|
||||
const models = {
|
||||
|
@ -93,26 +96,75 @@ function sanity(input) {
|
|||
|
||||
async function load(userConfig) {
|
||||
if (userConfig) config = mergeDeep(defaults, userConfig);
|
||||
if (config.face.enabled && !models.facemesh) models.facemesh = await facemesh.load(config.face);
|
||||
if (config.body.enabled && !models.posenet) models.posenet = await posenet.load(config.body);
|
||||
if (config.hand.enabled && !models.handpose) models.handpose = await handpose.load(config.hand);
|
||||
if (config.face.enabled && config.face.age.enabled && !models.age) models.age = await ssrnet.loadAge(config);
|
||||
if (config.face.enabled && config.face.gender.enabled && !models.gender) models.gender = await ssrnet.loadGender(config);
|
||||
if (config.face.enabled && config.face.emotion.enabled && !models.emotion) models.emotion = await emotion.load(config);
|
||||
if (config.face.enabled && !models.facemesh) {
|
||||
log('Load model: Face');
|
||||
models.facemesh = await facemesh.load(config.face);
|
||||
}
|
||||
if (config.body.enabled && !models.posenet) {
|
||||
log('Load model: Body');
|
||||
models.posenet = await posenet.load(config.body);
|
||||
}
|
||||
if (config.hand.enabled && !models.handpose) {
|
||||
log('Load model: Hand');
|
||||
models.handpose = await handpose.load(config.hand);
|
||||
}
|
||||
if (config.face.enabled && config.face.age.enabled && !models.age) {
|
||||
log('Load model: Age');
|
||||
models.age = await ssrnet.loadAge(config);
|
||||
}
|
||||
if (config.face.enabled && config.face.gender.enabled && !models.gender) {
|
||||
log('Load model: Gender');
|
||||
models.gender = await ssrnet.loadGender(config);
|
||||
}
|
||||
if (config.face.enabled && config.face.emotion.enabled && !models.emotion) {
|
||||
log('Load model: Emotion');
|
||||
models.emotion = await emotion.load(config);
|
||||
}
|
||||
}
|
||||
|
||||
function tfImage(input) {
|
||||
let image;
|
||||
// let imageData;
|
||||
let filtered;
|
||||
if (tf.ENV.flags.IS_BROWSER && config.filter.enabled && !(input instanceof tf.Tensor)) {
|
||||
const width = input.naturalWidth || input.videoWidth || input.width || (input.shape && (input.shape[1] > 0));
|
||||
const height = input.naturalHeight || input.videoHeight || input.Height || (input.shape && (input.shape[2] > 0));
|
||||
// if (!offscreenCanvas) offscreenCanvas = new OffscreenCanvas(width, height);
|
||||
if (!offscreenCanvas) {
|
||||
offscreenCanvas = document.createElement('canvas');
|
||||
offscreenCanvas.width = width;
|
||||
offscreenCanvas.height = height;
|
||||
}
|
||||
const ctx = offscreenCanvas.getContext('2d');
|
||||
ctx.drawImage(input, 0, 0, width, height, 0, 0, offscreenCanvas.width, offscreenCanvas.height);
|
||||
if (!fx) fx = new fxImage.Canvas();
|
||||
else fx.reset();
|
||||
fx.addFilter('brightness', config.filter.brightness); // must have at least one filter enabled
|
||||
if (config.filter.contrast !== 0) fx.addFilter('contrast', config.filter.contrast);
|
||||
if (config.filter.sharpness !== 0) fx.addFilter('sharpen', config.filter.sharpness);
|
||||
if (config.filter.blur !== 0) fx.addFilter('blur', config.filter.blur);
|
||||
if (config.filter.saturation !== 0) fx.addFilter('saturation', config.filter.saturation);
|
||||
if (config.filter.hue !== 0) fx.addFilter('hue', config.filter.hue);
|
||||
if (config.filter.negative) fx.addFilter('negative');
|
||||
if (config.filter.sepia) fx.addFilter('sepia');
|
||||
if (config.filter.vintage) fx.addFilter('brownie');
|
||||
if (config.filter.sepia) fx.addFilter('sepia');
|
||||
if (config.filter.kodachrome) fx.addFilter('kodachrome');
|
||||
if (config.filter.technicolor) fx.addFilter('technicolor');
|
||||
if (config.filter.polaroid) fx.addFilter('polaroid');
|
||||
if (config.filter.pixelate !== 0) fx.addFilter('pixelate', config.filter.pixelate);
|
||||
filtered = fx.apply(offscreenCanvas);
|
||||
}
|
||||
let tensor;
|
||||
if (input instanceof tf.Tensor) {
|
||||
image = tf.clone(input);
|
||||
tensor = tf.clone(input);
|
||||
} else {
|
||||
const pixels = tf.browser.fromPixels(input);
|
||||
const pixels = tf.browser.fromPixels(filtered || input);
|
||||
const casted = pixels.toFloat();
|
||||
image = casted.expandDims(0);
|
||||
tensor = casted.expandDims(0);
|
||||
pixels.dispose();
|
||||
casted.dispose();
|
||||
}
|
||||
return image;
|
||||
return { tensor, canvas: config.filter.return ? filtered : null };
|
||||
}
|
||||
|
||||
async function detect(input, userConfig = {}) {
|
||||
|
@ -167,7 +219,10 @@ async function detect(input, userConfig = {}) {
|
|||
|
||||
analyze('Start Detect:');
|
||||
|
||||
const imageTensor = tfImage(input);
|
||||
timeStamp = now();
|
||||
const image = tfImage(input);
|
||||
perf.image = Math.trunc(now() - timeStamp);
|
||||
const imageTensor = image.tensor;
|
||||
|
||||
// run posenet
|
||||
state = 'run:body';
|
||||
|
@ -239,7 +294,7 @@ async function detect(input, userConfig = {}) {
|
|||
analyze('End Scope:');
|
||||
|
||||
perf.total = Math.trunc(now() - timeStart);
|
||||
resolve({ face: faceRes, body: poseRes, hand: handRes, performance: perf });
|
||||
resolve({ face: faceRes, body: poseRes, hand: handRes, performance: perf, canvas: image.canvas });
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,608 @@
|
|||
/* eslint-disable no-shadow */
|
||||
/* eslint-disable prefer-rest-params */
|
||||
/* eslint-disable no-sequences */
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-unused-expressions */
|
||||
/* eslint-disable no-multi-assign */
|
||||
/* eslint-disable no-use-before-define */
|
||||
/*
|
||||
WebGLImageFilter - MIT Licensed
|
||||
2013, Dominic Szablewski - phoboslab.org
|
||||
*/
|
||||
|
||||
const WebGLProgram = function (gl, vertexSource, fragmentSource) {
|
||||
const _collect = function (source, prefix, collection) {
|
||||
const r = new RegExp('\\b' + prefix + ' \\w+ (\\w+)', 'ig');
|
||||
source.replace(r, (match, name) => {
|
||||
collection[name] = 0;
|
||||
return match;
|
||||
});
|
||||
};
|
||||
|
||||
const _compile = function (gl, source, type) {
|
||||
const shader = gl.createShader(type);
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
|
||||
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||
throw new Error('Filter: GL compile failed', gl.getShaderInfoLog(shader));
|
||||
}
|
||||
return shader;
|
||||
};
|
||||
|
||||
this.uniform = {};
|
||||
this.attribute = {};
|
||||
|
||||
const _vsh = _compile(gl, vertexSource, gl.VERTEX_SHADER);
|
||||
const _fsh = _compile(gl, fragmentSource, gl.FRAGMENT_SHADER);
|
||||
|
||||
this.id = gl.createProgram();
|
||||
gl.attachShader(this.id, _vsh);
|
||||
gl.attachShader(this.id, _fsh);
|
||||
gl.linkProgram(this.id);
|
||||
|
||||
if (!gl.getProgramParameter(this.id, gl.LINK_STATUS)) {
|
||||
throw new Error('Filter: GL link failed', gl.getProgramInfoLog(this.id));
|
||||
}
|
||||
|
||||
gl.useProgram(this.id);
|
||||
|
||||
// Collect attributes
|
||||
_collect(vertexSource, 'attribute', this.attribute);
|
||||
for (const a in this.attribute) {
|
||||
this.attribute[a] = gl.getAttribLocation(this.id, a);
|
||||
}
|
||||
|
||||
// Collect uniforms
|
||||
_collect(vertexSource, 'uniform', this.uniform);
|
||||
_collect(fragmentSource, 'uniform', this.uniform);
|
||||
for (const u in this.uniform) {
|
||||
this.uniform[u] = gl.getUniformLocation(this.id, u);
|
||||
}
|
||||
};
|
||||
|
||||
const WebGLImageFilter = function (params) {
|
||||
if (!params) params = { };
|
||||
let _drawCount = 0;
|
||||
let _sourceTexture = null;
|
||||
let _lastInChain = false;
|
||||
let _currentFramebufferIndex = -1;
|
||||
let _tempFramebuffers = [null, null];
|
||||
let _filterChain = [];
|
||||
let _width = -1;
|
||||
let _height = -1;
|
||||
let _vertexBuffer = null;
|
||||
let _currentProgram = null;
|
||||
const _canvas = params.canvas || document.createElement('canvas');
|
||||
|
||||
// key is the shader program source, value is the compiled program
|
||||
const _shaderProgramCache = { };
|
||||
|
||||
const gl = _canvas.getContext('webgl') || _canvas.getContext('experimental-webgl');
|
||||
if (!gl) throw new Error('Filter: getContext() failed');
|
||||
|
||||
this.addFilter = function (name) {
|
||||
const args = Array.prototype.slice.call(arguments, 1);
|
||||
const filter = _filter[name];
|
||||
|
||||
_filterChain.push({ func: filter, args });
|
||||
};
|
||||
|
||||
this.reset = function () {
|
||||
_filterChain = [];
|
||||
};
|
||||
|
||||
this.apply = function (image) {
|
||||
_resize(image.width, image.height);
|
||||
_drawCount = 0;
|
||||
|
||||
// Create the texture for the input image if we haven't yet
|
||||
if (!_sourceTexture) _sourceTexture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, _sourceTexture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
|
||||
|
||||
// No filters? Just draw
|
||||
if (_filterChain.length === 0) {
|
||||
const program = _compileShader(SHADER.FRAGMENT_IDENTITY);
|
||||
_draw();
|
||||
return _canvas;
|
||||
}
|
||||
|
||||
for (let i = 0; i < _filterChain.length; i++) {
|
||||
_lastInChain = (i === _filterChain.length - 1);
|
||||
const f = _filterChain[i];
|
||||
f.func.apply(this, f.args || []);
|
||||
}
|
||||
|
||||
return _canvas;
|
||||
};
|
||||
|
||||
const _resize = function (width, height) {
|
||||
// Same width/height? Nothing to do here
|
||||
if (width === _width && height === _height) { return; }
|
||||
|
||||
_canvas.width = _width = width;
|
||||
_canvas.height = _height = height;
|
||||
|
||||
// Create the context if we don't have it yet
|
||||
if (!_vertexBuffer) {
|
||||
// Create the vertex buffer for the two triangles [x, y, u, v] * 6
|
||||
const vertices = new Float32Array([
|
||||
-1, -1, 0, 1, 1, -1, 1, 1, -1, 1, 0, 0,
|
||||
-1, 1, 0, 0, 1, -1, 1, 1, 1, 1, 1, 0,
|
||||
]);
|
||||
_vertexBuffer = gl.createBuffer(),
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, _vertexBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
||||
|
||||
// Note sure if this is a good idea; at least it makes texture loading
|
||||
// in Ejecta instant.
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
|
||||
}
|
||||
|
||||
gl.viewport(0, 0, _width, _height);
|
||||
|
||||
// Delete old temp framebuffers
|
||||
_tempFramebuffers = [null, null];
|
||||
};
|
||||
|
||||
const _getTempFramebuffer = function (index) {
|
||||
_tempFramebuffers[index] = _tempFramebuffers[index]
|
||||
|| _createFramebufferTexture(_width, _height);
|
||||
|
||||
return _tempFramebuffers[index];
|
||||
};
|
||||
|
||||
const _createFramebufferTexture = function (width, height) {
|
||||
const fbo = gl.createFramebuffer();
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
|
||||
|
||||
const renderbuffer = gl.createRenderbuffer();
|
||||
gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
|
||||
|
||||
const texture = gl.createTexture();
|
||||
gl.bindTexture(gl.TEXTURE_2D, texture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
|
||||
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
|
||||
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, null);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
|
||||
|
||||
return { fbo, texture };
|
||||
};
|
||||
|
||||
const _draw = function (flags) {
|
||||
let source = null;
|
||||
let target = null;
|
||||
let flipY = false;
|
||||
|
||||
// Set up the source
|
||||
if (_drawCount === 0) {
|
||||
// First draw call - use the source texture
|
||||
source = _sourceTexture;
|
||||
} else {
|
||||
// All following draw calls use the temp buffer last drawn to
|
||||
source = _getTempFramebuffer(_currentFramebufferIndex).texture;
|
||||
}
|
||||
_drawCount++;
|
||||
|
||||
// Set up the target
|
||||
if (_lastInChain && !(flags & DRAW.INTERMEDIATE)) {
|
||||
// Last filter in our chain - draw directly to the WebGL Canvas. We may
|
||||
// also have to flip the image vertically now
|
||||
target = null;
|
||||
flipY = _drawCount % 2 === 0;
|
||||
} else {
|
||||
// Intermediate draw call - get a temp buffer to draw to
|
||||
_currentFramebufferIndex = (_currentFramebufferIndex + 1) % 2;
|
||||
target = _getTempFramebuffer(_currentFramebufferIndex).fbo;
|
||||
}
|
||||
|
||||
// Bind the source and target and draw the two triangles
|
||||
gl.bindTexture(gl.TEXTURE_2D, source);
|
||||
gl.bindFramebuffer(gl.FRAMEBUFFER, target);
|
||||
|
||||
gl.uniform1f(_currentProgram.uniform.flipY, (flipY ? -1 : 1));
|
||||
gl.drawArrays(gl.TRIANGLES, 0, 6);
|
||||
};
|
||||
|
||||
const _compileShader = function (fragmentSource) {
|
||||
if (_shaderProgramCache[fragmentSource]) {
|
||||
_currentProgram = _shaderProgramCache[fragmentSource];
|
||||
gl.useProgram(_currentProgram.id);
|
||||
return _currentProgram;
|
||||
}
|
||||
|
||||
// Compile shaders
|
||||
_currentProgram = new WebGLProgram(gl, SHADER.VERTEX_IDENTITY, fragmentSource);
|
||||
|
||||
const floatSize = Float32Array.BYTES_PER_ELEMENT;
|
||||
const vertSize = 4 * floatSize;
|
||||
gl.enableVertexAttribArray(_currentProgram.attribute.pos);
|
||||
gl.vertexAttribPointer(_currentProgram.attribute.pos, 2, gl.FLOAT, false, vertSize, 0 * floatSize);
|
||||
gl.enableVertexAttribArray(_currentProgram.attribute.uv);
|
||||
gl.vertexAttribPointer(_currentProgram.attribute.uv, 2, gl.FLOAT, false, vertSize, 2 * floatSize);
|
||||
|
||||
_shaderProgramCache[fragmentSource] = _currentProgram;
|
||||
return _currentProgram;
|
||||
};
|
||||
|
||||
let DRAW = { INTERMEDIATE: 1 };
|
||||
|
||||
let SHADER = {};
|
||||
SHADER.VERTEX_IDENTITY = [
|
||||
'precision highp float;',
|
||||
'attribute vec2 pos;',
|
||||
'attribute vec2 uv;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform float flipY;',
|
||||
|
||||
'void main(void) {',
|
||||
'vUv = uv;',
|
||||
'gl_Position = vec4(pos.x, pos.y*flipY, 0.0, 1.);',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
SHADER.FRAGMENT_IDENTITY = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform sampler2D texture;',
|
||||
|
||||
'void main(void) {',
|
||||
'gl_FragColor = texture2D(texture, vUv);',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
let _filter = {};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Color Matrix Filter
|
||||
|
||||
_filter.colorMatrix = function (matrix) {
|
||||
// Create a Float32 Array and normalize the offset component to 0-1
|
||||
const m = new Float32Array(matrix);
|
||||
m[4] /= 255;
|
||||
m[9] /= 255;
|
||||
m[14] /= 255;
|
||||
m[19] /= 255;
|
||||
|
||||
// Can we ignore the alpha value? Makes things a bit faster.
|
||||
const shader = (m[18] === 1 && m[3] === 0 && m[8] === 0 && m[13] === 0 && m[15] === 0 && m[16] === 0 && m[17] === 0 && m[19] === 0)
|
||||
? _filter.colorMatrix.SHADER.WITHOUT_ALPHA
|
||||
: _filter.colorMatrix.SHADER.WITH_ALPHA;
|
||||
|
||||
const program = _compileShader(shader);
|
||||
gl.uniform1fv(program.uniform.m, m);
|
||||
_draw();
|
||||
};
|
||||
|
||||
_filter.colorMatrix.SHADER = {};
|
||||
_filter.colorMatrix.SHADER.WITH_ALPHA = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform sampler2D texture;',
|
||||
'uniform float m[20];',
|
||||
|
||||
'void main(void) {',
|
||||
'vec4 c = texture2D(texture, vUv);',
|
||||
'gl_FragColor.r = m[0] * c.r + m[1] * c.g + m[2] * c.b + m[3] * c.a + m[4];',
|
||||
'gl_FragColor.g = m[5] * c.r + m[6] * c.g + m[7] * c.b + m[8] * c.a + m[9];',
|
||||
'gl_FragColor.b = m[10] * c.r + m[11] * c.g + m[12] * c.b + m[13] * c.a + m[14];',
|
||||
'gl_FragColor.a = m[15] * c.r + m[16] * c.g + m[17] * c.b + m[18] * c.a + m[19];',
|
||||
'}',
|
||||
].join('\n');
|
||||
_filter.colorMatrix.SHADER.WITHOUT_ALPHA = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform sampler2D texture;',
|
||||
'uniform float m[20];',
|
||||
|
||||
'void main(void) {',
|
||||
'vec4 c = texture2D(texture, vUv);',
|
||||
'gl_FragColor.r = m[0] * c.r + m[1] * c.g + m[2] * c.b + m[4];',
|
||||
'gl_FragColor.g = m[5] * c.r + m[6] * c.g + m[7] * c.b + m[9];',
|
||||
'gl_FragColor.b = m[10] * c.r + m[11] * c.g + m[12] * c.b + m[14];',
|
||||
'gl_FragColor.a = c.a;',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
_filter.brightness = function (brightness) {
|
||||
const b = (brightness || 0) + 1;
|
||||
_filter.colorMatrix([
|
||||
b, 0, 0, 0, 0,
|
||||
0, b, 0, 0, 0,
|
||||
0, 0, b, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.saturation = function (amount) {
|
||||
const x = (amount || 0) * 2 / 3 + 1;
|
||||
const y = ((x - 1) * -0.5);
|
||||
_filter.colorMatrix([
|
||||
x, y, y, 0, 0,
|
||||
y, x, y, 0, 0,
|
||||
y, y, x, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.desaturate = function () {
|
||||
_filter.saturation(-1);
|
||||
};
|
||||
|
||||
_filter.contrast = function (amount) {
|
||||
const v = (amount || 0) + 1;
|
||||
const o = -128 * (v - 1);
|
||||
|
||||
_filter.colorMatrix([
|
||||
v, 0, 0, 0, o,
|
||||
0, v, 0, 0, o,
|
||||
0, 0, v, 0, o,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.negative = function () {
|
||||
_filter.contrast(-2);
|
||||
};
|
||||
|
||||
_filter.hue = function (rotation) {
|
||||
rotation = (rotation || 0) / 180 * Math.PI;
|
||||
const cos = Math.cos(rotation);
|
||||
const sin = Math.sin(rotation);
|
||||
const lumR = 0.213;
|
||||
const lumG = 0.715;
|
||||
const lumB = 0.072;
|
||||
|
||||
_filter.colorMatrix([
|
||||
lumR + cos * (1 - lumR) + sin * (-lumR), lumG + cos * (-lumG) + sin * (-lumG), lumB + cos * (-lumB) + sin * (1 - lumB), 0, 0,
|
||||
lumR + cos * (-lumR) + sin * (0.143), lumG + cos * (1 - lumG) + sin * (0.140), lumB + cos * (-lumB) + sin * (-0.283), 0, 0,
|
||||
lumR + cos * (-lumR) + sin * (-(1 - lumR)), lumG + cos * (-lumG) + sin * (lumG), lumB + cos * (1 - lumB) + sin * (lumB), 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.desaturateLuminance = function () {
|
||||
_filter.colorMatrix([
|
||||
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
|
||||
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
|
||||
0.2764723, 0.9297080, 0.0938197, 0, -37.1,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.sepia = function () {
|
||||
_filter.colorMatrix([
|
||||
0.393, 0.7689999, 0.18899999, 0, 0,
|
||||
0.349, 0.6859999, 0.16799999, 0, 0,
|
||||
0.272, 0.5339999, 0.13099999, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.brownie = function () {
|
||||
_filter.colorMatrix([
|
||||
0.5997023498159715, 0.34553243048391263, -0.2708298674538042, 0, 47.43192855600873,
|
||||
-0.037703249837783157, 0.8609577587992641, 0.15059552388459913, 0, -36.96841498319127,
|
||||
0.24113635128153335, -0.07441037908422492, 0.44972182064877153, 0, -7.562075277591283,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.vintagePinhole = function () {
|
||||
_filter.colorMatrix([
|
||||
0.6279345635605994, 0.3202183420819367, -0.03965408211312453, 0, 9.651285835294123,
|
||||
0.02578397704808868, 0.6441188644374771, 0.03259127616149294, 0, 7.462829176470591,
|
||||
0.0466055556782719, -0.0851232987247891, 0.5241648018700465, 0, 5.159190588235296,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.kodachrome = function () {
|
||||
_filter.colorMatrix([
|
||||
1.1285582396593525, -0.3967382283601348, -0.03992559172921793, 0, 63.72958762196502,
|
||||
-0.16404339962244616, 1.0835251566291304, -0.05498805115633132, 0, 24.732407896706203,
|
||||
-0.16786010706155763, -0.5603416277695248, 1.6014850761964943, 0, 35.62982807460946,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.technicolor = function () {
|
||||
_filter.colorMatrix([
|
||||
1.9125277891456083, -0.8545344976951645, -0.09155508482755585, 0, 11.793603434377337,
|
||||
-0.3087833385928097, 1.7658908555458428, -0.10601743074722245, 0, -70.35205161461398,
|
||||
-0.231103377548616, -0.7501899197440212, 1.847597816108189, 0, 30.950940869491138,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.polaroid = function () {
|
||||
_filter.colorMatrix([
|
||||
1.438, -0.062, -0.062, 0, 0,
|
||||
-0.122, 1.378, -0.122, 0, 0,
|
||||
-0.016, -0.016, 1.483, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.shiftToBGR = function () {
|
||||
_filter.colorMatrix([
|
||||
0, 0, 1, 0, 0,
|
||||
0, 1, 0, 0, 0,
|
||||
1, 0, 0, 0, 0,
|
||||
0, 0, 0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Convolution Filter
|
||||
|
||||
_filter.convolution = function (matrix) {
|
||||
const m = new Float32Array(matrix);
|
||||
const pixelSizeX = 1 / _width;
|
||||
const pixelSizeY = 1 / _height;
|
||||
|
||||
const program = _compileShader(_filter.convolution.SHADER);
|
||||
gl.uniform1fv(program.uniform.m, m);
|
||||
gl.uniform2f(program.uniform.px, pixelSizeX, pixelSizeY);
|
||||
_draw();
|
||||
};
|
||||
|
||||
_filter.convolution.SHADER = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform sampler2D texture;',
|
||||
'uniform vec2 px;',
|
||||
'uniform float m[9];',
|
||||
|
||||
'void main(void) {',
|
||||
'vec4 c11 = texture2D(texture, vUv - px);', // top left
|
||||
'vec4 c12 = texture2D(texture, vec2(vUv.x, vUv.y - px.y));', // top center
|
||||
'vec4 c13 = texture2D(texture, vec2(vUv.x + px.x, vUv.y - px.y));', // top right
|
||||
|
||||
'vec4 c21 = texture2D(texture, vec2(vUv.x - px.x, vUv.y) );', // mid left
|
||||
'vec4 c22 = texture2D(texture, vUv);', // mid center
|
||||
'vec4 c23 = texture2D(texture, vec2(vUv.x + px.x, vUv.y) );', // mid right
|
||||
|
||||
'vec4 c31 = texture2D(texture, vec2(vUv.x - px.x, vUv.y + px.y) );', // bottom left
|
||||
'vec4 c32 = texture2D(texture, vec2(vUv.x, vUv.y + px.y) );', // bottom center
|
||||
'vec4 c33 = texture2D(texture, vUv + px );', // bottom right
|
||||
|
||||
'gl_FragColor = ',
|
||||
'c11 * m[0] + c12 * m[1] + c22 * m[2] +',
|
||||
'c21 * m[3] + c22 * m[4] + c23 * m[5] +',
|
||||
'c31 * m[6] + c32 * m[7] + c33 * m[8];',
|
||||
'gl_FragColor.a = c22.a;',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
_filter.detectEdges = function () {
|
||||
_filter.convolution.call(this, [
|
||||
0, 1, 0,
|
||||
1, -4, 1,
|
||||
0, 1, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.sobelX = function () {
|
||||
_filter.convolution.call(this, [
|
||||
-1, 0, 1,
|
||||
-2, 0, 2,
|
||||
-1, 0, 1,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.sobelY = function () {
|
||||
_filter.convolution.call(this, [
|
||||
-1, -2, -1,
|
||||
0, 0, 0,
|
||||
1, 2, 1,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.sharpen = function (amount) {
|
||||
const a = amount || 1;
|
||||
_filter.convolution.call(this, [
|
||||
0, -1 * a, 0,
|
||||
-1 * a, 1 + 4 * a, -1 * a,
|
||||
0, -1 * a, 0,
|
||||
]);
|
||||
};
|
||||
|
||||
_filter.emboss = function (size) {
|
||||
const s = size || 1;
|
||||
_filter.convolution.call(this, [
|
||||
-2 * s, -1 * s, 0,
|
||||
-1 * s, 1, 1 * s,
|
||||
0, 1 * s, 2 * s,
|
||||
]);
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Blur Filter
|
||||
|
||||
_filter.blur = function (size) {
|
||||
const blurSizeX = (size / 7) / _width;
|
||||
const blurSizeY = (size / 7) / _height;
|
||||
|
||||
const program = _compileShader(_filter.blur.SHADER);
|
||||
|
||||
// Vertical
|
||||
gl.uniform2f(program.uniform.px, 0, blurSizeY);
|
||||
_draw(DRAW.INTERMEDIATE);
|
||||
|
||||
// Horizontal
|
||||
gl.uniform2f(program.uniform.px, blurSizeX, 0);
|
||||
_draw();
|
||||
};
|
||||
|
||||
_filter.blur.SHADER = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform sampler2D texture;',
|
||||
'uniform vec2 px;',
|
||||
|
||||
'void main(void) {',
|
||||
'gl_FragColor = vec4(0.0);',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-7.0*px.x, -7.0*px.y))*0.0044299121055113265;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-6.0*px.x, -6.0*px.y))*0.00895781211794;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-5.0*px.x, -5.0*px.y))*0.0215963866053;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-4.0*px.x, -4.0*px.y))*0.0443683338718;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-3.0*px.x, -3.0*px.y))*0.0776744219933;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-2.0*px.x, -2.0*px.y))*0.115876621105;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2(-1.0*px.x, -1.0*px.y))*0.147308056121;',
|
||||
'gl_FragColor += texture2D(texture, vUv )*0.159576912161;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 1.0*px.x, 1.0*px.y))*0.147308056121;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 2.0*px.x, 2.0*px.y))*0.115876621105;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 3.0*px.x, 3.0*px.y))*0.0776744219933;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 4.0*px.x, 4.0*px.y))*0.0443683338718;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 5.0*px.x, 5.0*px.y))*0.0215963866053;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 6.0*px.x, 6.0*px.y))*0.00895781211794;',
|
||||
'gl_FragColor += texture2D(texture, vUv + vec2( 7.0*px.x, 7.0*px.y))*0.0044299121055113265;',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Pixelate Filter
|
||||
|
||||
_filter.pixelate = function (size) {
|
||||
const blurSizeX = (size) / _width;
|
||||
const blurSizeY = (size) / _height;
|
||||
|
||||
const program = _compileShader(_filter.pixelate.SHADER);
|
||||
|
||||
// Horizontal
|
||||
gl.uniform2f(program.uniform.size, blurSizeX, blurSizeY);
|
||||
_draw();
|
||||
};
|
||||
|
||||
_filter.pixelate.SHADER = [
|
||||
'precision highp float;',
|
||||
'varying vec2 vUv;',
|
||||
'uniform vec2 size;',
|
||||
'uniform sampler2D texture;',
|
||||
|
||||
'vec2 pixelate(vec2 coord, vec2 size) {',
|
||||
'return floor( coord / size ) * size;',
|
||||
'}',
|
||||
|
||||
'void main(void) {',
|
||||
'gl_FragColor = vec4(0.0);',
|
||||
'vec2 coord = pixelate(vUv, size);',
|
||||
'gl_FragColor += texture2D(texture, coord);',
|
||||
'}',
|
||||
].join('\n');
|
||||
};
|
||||
|
||||
exports.Canvas = WebGLImageFilter;
|
Loading…
Reference in New Issue