diff --git a/demo/browser.js b/demo/browser.js index 81869c61..82c342bd 100644 --- a/demo/browser.js +++ b/demo/browser.js @@ -1,31 +1,32 @@ -/* global QuickSettings */ - import human from '../dist/human.esm.js'; import draw from './draw.js'; +import Menu from './menu.js'; // ui options const ui = { - baseColor: 'rgba(255, 200, 255, 0.3)', - baseLabel: 'rgba(255, 200, 255, 0.9)', + baseColor: 'rgba(173, 216, 230, 0.3)', // this is 'lightblue', just with alpha channel + baseLabel: 'rgba(173, 216, 230, 0.9)', baseFontProto: 'small-caps {size} "Segoe UI"', baseLineWidth: 16, baseLineHeightProto: 2, - columns: 3, + columns: 2, busy: false, - facing: 'user', + facing: true, useWorker: false, worker: 'worker.js', - samples: ['../assets/sample1.jpg', '../assets/sample2.jpg', '../assets/sample3.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample6.jpg'], + samples: ['../assets/sample6.jpg', '../assets/sample1.jpg', '../assets/sample4.jpg', '../assets/sample5.jpg', '../assets/sample3.jpg', '../assets/sample2.jpg'], drawBoxes: true, drawPoints: false, drawPolygons: true, fillPolygons: true, useDepth: true, console: true, + maxFrames: 10, }; // 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 face: { enabled: true, detector: { maxFaces: 10, skipFrames: 10, minConfidence: 0.5, iouThreshold: 0.3, scoreThreshold: 0.7 }, @@ -40,7 +41,7 @@ const config = { }; // global variables -let settings; +let menu; let worker; let timeStamp; const fps = []; @@ -63,12 +64,11 @@ const log = (...msg) => { }; // draws processed results and starts processing of a next frame -async function drawResults(input, result, canvas) { +function drawResults(input, result, canvas) { // update fps - settings.setValue('FPS', Math.round(1000 / (performance.now() - timeStamp))); fps.push(1000 / (performance.now() - timeStamp)); - if (fps.length > 20) fps.shift(); - settings.setValue('FPS', Math.round(10 * fps.reduce((a, b) => a + b) / fps.length) / 10); + if (fps.length > ui.maxFrames) fps.shift(); + menu.updateChart('FPS', fps); // eslint-disable-next-line no-use-before-define requestAnimationFrame(() => runHumanDetect(input, canvas)); // immediate loop @@ -81,7 +81,7 @@ async function drawResults(input, result, canvas) { draw.body(result.body, canvas, ui); draw.hand(result.hand, canvas, ui); // update log - const engine = await human.tf.engine(); + const engine = human.tf.engine(); const memory = `${engine.state.numBytes.toLocaleString()} bytes ${engine.state.numDataBuffers.toLocaleString()} buffers ${engine.state.numTensors.toLocaleString()} tensors`; const gpu = engine.backendInstance ? `GPU: ${engine.backendInstance.numBytesInGPU.toLocaleString()} bytes` : ''; document.getElementById('log').innerText = ` @@ -98,7 +98,7 @@ async function setupCamera() { const canvas = document.getElementById('canvas'); const output = document.getElementById('log'); const live = video.srcObject ? ((video.srcObject.getVideoTracks()[0].readyState === 'live') && (video.readyState > 2) && (!video.paused)) : false; - let msg = `Setting up camera: live: ${live} facing: ${ui.facing}`; + let msg = `Setting up camera: live: ${live} facing: ${ui.facing ? 'front' : 'back'}`; output.innerText += `\n${msg}`; log(msg); // setup webcam. note that navigator.mediaDevices requires that page is accessed via https @@ -112,7 +112,7 @@ async function setupCamera() { try { stream = await navigator.mediaDevices.getUserMedia({ audio: false, - video: { facingMode: ui.facing, width: window.innerWidth, height: window.innerHeight }, + video: { facingMode: (ui.facing ? 'user' : 'environment'), width: window.innerWidth, height: window.innerHeight }, }); } catch (err) { output.innerText += '\nCamera permission denied'; @@ -150,7 +150,7 @@ function webWorker(input, image, canvas) { } // main processing function when input is webcam, can use direct invocation or web worker -async function runHumanDetect(input, canvas) { +function runHumanDetect(input, canvas) { timeStamp = performance.now(); // perform detect if live video or not video at all if (input.srcObject) { @@ -170,36 +170,23 @@ async function runHumanDetect(input, canvas) { // perform detection in worker webWorker(input, data, canvas); } else { - let result = {}; - try { - // perform detection - result = await human.detect(input, config); - } catch (err) { - log('Error during execution:', err.message); - } - if (result.error) log(result.error); - else drawResults(input, result, canvas); + human.detect(input, config).then((result) => { + if (result.error) log(result.error); + else drawResults(input, result, canvas); + }); } } } // main processing function when input is image, can use direct invocation or web worker async function processImage(input) { - const cfg = { - backend: 'webgl', - console: true, - face: { - enabled: true, - detector: { maxFaces: 10, skipFrames: 0, minConfidence: 0.1, iouThreshold: 0.3, scoreThreshold: 0.3 }, - mesh: { enabled: true }, - iris: { enabled: true }, - age: { enabled: true, skipFrames: 0 }, - gender: { enabled: true }, - emotion: { enabled: true, minConfidence: 0.1, useGrayscale: true }, - }, - body: { enabled: true, maxDetections: 10, scoreThreshold: 0.7, nmsRadius: 20 }, - hand: { enabled: true, skipFrames: 0, minConfidence: 0.5, iouThreshold: 0.3, scoreThreshold: 0.5 }, - }; + // must be zero for images + config.face.detector.skipFrames = 0; + config.face.emotion.skipFrames = 0; + config.face.age.skipFrames = 0; + config.hand.skipFrames = 0; + + timeStamp = performance.now(); return new Promise((resolve) => { const image = document.getElementById('image'); image.onload = async () => { @@ -209,11 +196,13 @@ async function processImage(input) { image.height = image.naturalHeight; canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; - const result = await human.detect(image, cfg); - await drawResults(image, result, canvas); + const result = await human.detect(image, config); + drawResults(image, result, canvas); const thumb = document.createElement('canvas'); - thumb.width = window.innerWidth / (ui.columns + 0.02); + thumb.width = (window.innerWidth - menu.width) / (ui.columns + 0.1); thumb.height = canvas.height / (window.innerWidth / thumb.width); + thumb.style.margin = '8px'; + thumb.style.boxShadow = '4px 4px 4px 0 dimgrey'; const ctx = thumb.getContext('2d'); ctx.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, thumb.width, thumb.height); document.getElementById('samples').appendChild(thumb); @@ -253,74 +242,68 @@ async function detectSampleImages() { for (const sample of ui.samples) await processImage(sample); } -// setup settings panel -function setupUI() { - settings = QuickSettings.create(10, 10, 'Settings', document.getElementById('main')); - const style = document.createElement('style'); - style.innerHTML = ` - .qs_main { font: 1rem "Segoe UI"; } - .qs_label { font: 0.8rem "Segoe UI"; } - .qs_content { background: darkslategray; } - .qs_container { background: transparent; color: white; margin: 6px; padding: 6px; } - .qs_checkbox_label { top: 2px; } - .qs_button { width: -webkit-fill-available; font: 1rem "Segoe UI"; cursor: pointer; } - `; - document.getElementsByTagName('head')[0].appendChild(style); - settings.addButton('Play/Pause WebCam', () => detectVideo()); - settings.addButton('Process Images', () => detectSampleImages()); - settings.addDropDown('Backend', ['webgl', 'wasm', 'cpu'], async (val) => config.backend = val.value); - settings.addHTML('title', 'Enabled Models'); settings.hideTitle('title'); - settings.addBoolean('Face Detect', config.face.enabled, (val) => config.face.enabled = val); - settings.addBoolean('Face Mesh', config.face.mesh.enabled, (val) => config.face.mesh.enabled = val); - settings.addBoolean('Face Iris', config.face.iris.enabled, (val) => config.face.iris.enabled = val); - settings.addBoolean('Face Age', config.face.age.enabled, (val) => config.face.age.enabled = val); - settings.addBoolean('Face Gender', config.face.gender.enabled, (val) => config.face.gender.enabled = val); - settings.addBoolean('Face Emotion', config.face.emotion.enabled, (val) => config.face.emotion.enabled = val); - settings.addBoolean('Body Pose', config.body.enabled, (val) => config.body.enabled = val); - settings.addBoolean('Hand Pose', config.hand.enabled, (val) => config.hand.enabled = val); - settings.addHTML('title', 'Model Parameters'); settings.hideTitle('title'); - settings.addRange('Max Objects', 1, 20, 5, 1, (val) => { +function setupMenu() { + menu = new Menu(document.body); + menu.addButton('Start Video', 'Pause Video', (evt) => detectVideo(evt)); + menu.addButton('Process Images', 'Process Images', () => detectSampleImages()); + + menu.addHTML('
'); + menu.addLabel('Enabled Models'); + menu.addBool('Face Detect', config.face, 'enabled'); + menu.addBool('Face Mesh', config.face.mesh, 'enabled'); + menu.addBool('Face Iris', config.face.iris, 'enabled'); + menu.addBool('Face Age', config.face.age, 'enabled'); + menu.addBool('Face Gender', config.face.gender, 'enabled'); + menu.addBool('Face Emotion', config.face.emotion, 'enabled'); + menu.addBool('Body Pose', config.body, 'enabled'); + menu.addBool('Hand Pose', config.hand, 'enabled'); + + menu.addHTML('
'); + menu.addLabel('Model Parameters'); + menu.addRange('Max Objects', config.face.detector, 'maxFaces', 0, 50, 1, (val) => { config.face.detector.maxFaces = parseInt(val); config.body.maxDetections = parseInt(val); + config.hand.maxHands = parseInt(val); }); - settings.addRange('Skip Frames', 1, 20, config.face.detector.skipFrames, 1, (val) => { + menu.addRange('Skip Frames', config.face.detector, 'skipFrames', 0, 50, 1, (val) => { config.face.detector.skipFrames = parseInt(val); config.face.emotion.skipFrames = parseInt(val); config.face.age.skipFrames = parseInt(val); config.hand.skipFrames = parseInt(val); }); - settings.addRange('Min Confidence', 0.1, 1.0, config.face.detector.minConfidence, 0.05, (val) => { + menu.addRange('Min Confidence', config.face.detector, 'minConfidence', 0.0, 1.0, 0.05, (val) => { config.face.detector.minConfidence = parseFloat(val); config.face.emotion.minConfidence = parseFloat(val); config.hand.minConfidence = parseFloat(val); }); - settings.addRange('Score Threshold', 0.1, 1.0, config.face.detector.scoreThreshold, 0.05, (val) => { + menu.addRange('Score Threshold', config.face.detector, 'scoreThreshold', 0.1, 1.0, 0.05, (val) => { config.face.detector.scoreThreshold = parseFloat(val); config.hand.scoreThreshold = parseFloat(val); config.body.scoreThreshold = parseFloat(val); }); - settings.addRange('IOU Threshold', 0.1, 1.0, config.face.detector.iouThreshold, 0.05, (val) => { + menu.addRange('IOU Threshold', config.face.detector, 'iouThreshold', 0.1, 1.0, 0.05, (val) => { config.face.detector.iouThreshold = parseFloat(val); config.hand.iouThreshold = parseFloat(val); }); - settings.addHTML('title', 'UI Options'); settings.hideTitle('title'); - settings.addBoolean('Use Web Worker', ui.useWorker, (val) => ui.useWorker = val); - settings.addBoolean('Camera Front/Back', true, (val) => { - ui.facing = val ? 'user' : 'environment'; - setupCamera(); - }); - settings.addBoolean('Use 3D Depth', ui.useDepth, (val) => ui.useDepth = val); - settings.addBoolean('Draw Boxes', ui.drawBoxes, (val) => ui.drawBoxes = val); - settings.addBoolean('Draw Points', ui.drawPoints, (val) => ui.drawPoints = val); - settings.addBoolean('Draw Polygons', ui.drawPolygons, (val) => ui.drawPolygons = val); - settings.addBoolean('Fill Polygons', ui.fillPolygons, (val) => ui.fillPolygons = val); - settings.addHTML('line1', '
'); settings.hideTitle('line1'); - settings.addRange('FPS', 0, 100, 0, 1); + + menu.addHTML('
'); + 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('
'); + menu.addValue('State', ''); + menu.addChart('FPS', 'FPS'); } async function main() { log('Human demo starting ...'); - setupUI(); + setupMenu(); const msg = `Human ready: version: ${human.version} TensorFlow/JS version: ${human.tf.version_core}`; document.getElementById('log').innerText += '\n' + msg; log(msg); diff --git a/demo/index.html b/demo/index.html index 3e6e2955..0d9270c8 100644 --- a/demo/index.html +++ b/demo/index.html @@ -14,7 +14,6 @@ - diff --git a/demo/menu.js b/demo/menu.js new file mode 100644 index 00000000..c6f47c76 --- /dev/null +++ b/demo/menu.js @@ -0,0 +1,166 @@ +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); } + .menu { display: flex; white-space: nowrap; background: darkslategray; padding: 0.2rem; width: max-content; } + .menu-title { padding: 0; } + .menu-hr { margin: 0.2rem; border: 1px solid rgba(0, 0, 0, 0.5) } + .menu-label { width: 1.3rem; height: 0.8rem; cursor: pointer; position: absolute; top: 0.1rem; left: 0.1rem; z-index: 1; background: lightcoral; border-radius: 1rem; transition: left 0.6s ease; } + + .menu-chart-title { align-items: center; } + .menu-chart-canvas { background: transparent; height: 40px; width: 180px; margin: 0.2rem 0.2rem 0.2rem 1rem; } + + .menu-button { border: 0; background: lightblue; width: -webkit-fill-available; padding: 8px; margin: 8px 0 8px 0; cursor: pointer; box-shadow: 4px 4px 4px 0 dimgrey; } + .menu-button:hover { background: lightgreen; } + + .menu-checkbox { width: 2.8rem; height: 1rem; background: black; margin: 0.5rem 0.8rem 0 0; position: relative; border-radius: 1rem; } + .menu-checkbox:after { content: 'OFF'; color: lightcoral; position: absolute; right: 0.2rem; top: -0.4rem; font-weight: 800; font-size: 0.5rem; } + .menu-checkbox:before { content: 'ON'; color: lightgreen; position: absolute; left: 0.3rem; top: -0.4rem; font-weight: 800; font-size: 0.5rem; } + input[type=checkbox] { visibility: hidden; } + input[type=checkbox]:checked + label { left: 1.4rem; background: lightgreen; } + + .menu-range { margin: 0 0.8rem 0 0; width: 5rem; background: transparent; color: lightblue; } + .menu-range:before { content: attr(value); color: white; margin: 0 0.4rem 0 0; font-weight: 800; font-size: 0.6rem; position: relative; top: 0.3rem; } + input[type=range] { -webkit-appearance: none; } + input[type=range]::-webkit-slider-runnable-track { width: 100%; height: 1rem; cursor: pointer; background: black; border-radius: 1rem; border: 1px; } + input[type=range]::-webkit-slider-thumb { border: 1px solid #000000; margin-top: 0.05rem; height: 0.9rem; width: 1.5rem; border-radius: 1rem; background: lightblue; cursor: pointer; -webkit-appearance: none; } + `; + +function createCSS() { + const el = document.createElement('style'); + el.innerHTML = css; + document.getElementsByTagName('head')[0].appendChild(el); +} + +function createElem(parent) { + const el = document.createElement('div'); + el.id = 'menu'; + el.className = 'menu-container'; + if (typeof parent === 'object') parent.appendChild(el); + else document.getElementById(parent).appendChild(el); + return el; +} + +class Menu { + constructor(parent) { + createCSS(); + this.menu = createElem(parent); + this._id = 0; + this._maxFPS = 0; + } + + get newID() { + this._id++; + return `menu-${this._id}`; + } + + get ID() { + return `menu-${this._id}`; + } + + get width() { + return this.menu.offsetWidth; + } + + get height() { + return this.menu.offsetHeight; + } + + async addLabel(title) { + const el = document.createElement('div'); + el.className = 'menu menu-title'; + el.id = this.newID; + el.innerHTML = title; + this.menu.appendChild(el); + } + + async addBool(title, object, variable, callback) { + const el = document.createElement('div'); + el.className = 'menu'; + el.innerHTML = `${title}`; + this.menu.appendChild(el); + document.getElementById(this.ID).addEventListener('change', (evt) => { + object[variable] = evt.target.checked; + if (callback) callback(evt.target.checked); + }); + } + + async addRange(title, object, variable, min, max, step, callback) { + const el = document.createElement('div'); + el.className = 'menu'; + el.innerHTML = `${title}`; + this.menu.appendChild(el); + document.getElementById(this.ID).addEventListener('change', (evt) => { + object[variable] = evt.target.value; + evt.target.setAttribute('value', evt.target.value); + if (callback) callback(evt.target.value); + }); + } + + async addHTML(html) { + const el = document.createElement('div'); + el.className = 'menu'; + el.id = this.newID; + if (html) el.innerHTML = html; + this.menu.appendChild(el); + } + + async addButton(titleOn, titleOff, callback) { + const el = document.createElement('button'); + el.className = 'menu menu-button'; + el.type = 'button'; + el.id = this.newID; + el.innerText = titleOn; + this.menu.appendChild(el); + document.getElementById(this.ID).addEventListener('click', () => { + if (el.innerText === titleOn) el.innerText = titleOff; + else el.innerText = titleOn; + if (callback) callback(el.innerText !== titleOn); + }); + } + + async addValue(title, val) { + const el = document.createElement('div'); + el.className = 'menu'; + el.id = title; + el.innerText = `${title}: ${val}`; + this.menu.appendChild(el); + } + + // eslint-disable-next-line class-methods-use-this + async updateValue(title, val) { + const el = document.getElementById(title); + el.innerText = `${title}: ${val}`; + } + + async addChart(title, id) { + const el = document.createElement('div'); + el.className = 'menu menu-chart-title'; + el.id = this.newID; + el.innerHTML = `${title}`; + this.menu.appendChild(el); + } + + // eslint-disable-next-line class-methods-use-this + async updateChart(id, values) { + if (!values || (values.length === 0)) return; + const canvas = document.getElementById(`menu-canvas-${id}`); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + ctx.fillStyle = 'darkslategray'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + const width = canvas.width / values.length; + const max = 1 + Math.max(...values); + const height = canvas.height / max; + for (const i in values) { + const gradient = ctx.createLinearGradient(0, (max - values[i]) * height, 0, 0); + gradient.addColorStop(0.1, 'lightblue'); + gradient.addColorStop(0.4, 'darkslategray'); + ctx.fillStyle = gradient; + ctx.fillRect(i * width, 0, width - 4, canvas.height); + ctx.fillStyle = 'black'; + ctx.font = '12px "Segoe UI"'; + ctx.fillText(Math.round(values[i]), i * width, canvas.height - 2, width); + } + } +} + +export default Menu;