Naše zkušenosti s implementací dálkového ovládání a experimentováním s různými přístupy, včetně technologie počítačového vidění. V tomto článku se podělíme o výsledky našich experimentů s použitím knihovny MEDIAPIPE od Googlu pro počítačové vidění.
Během naší práce na projektu Stardio jsme dostali úkol implementovat dálkové ovládání aplikace. Prozkoumali jsme různé možnosti implementace a jedním z přístupů, se kterými jsme experimentovali, byla technologie počítačového vidění. V tomto článku se podělím o výsledky našich experimentů s jednou z dobře známých knihoven pro počítačové vidění - MEDIAPIPE, kterou vyvinula společnost Google.
Dříve bylo ovládání obsahu webové stránky gesty viděno pouze ve sci-fi filmech. Ale dnes k tomu všemu, co potřebujete, abyste to udělali realitou, je videokamera, prohlížeč a knihovna od Googlu. V tomto návodu budeme demonstrovat, jak implementovat ovládání gest pomocí čistého JavaScriptu. Pro detekci a sledování gest rukou budeme používat MediaPipe a pro správu závislostí budeme používat npm.
Vzorový kód lze najít v tomto repozitáři.
Vytvořte projekt s čistým JS pomocí šablony Vite pod názvem:
motion-controls - název projektu
vanilla - název šablony
yarn create vite motion-controls --template vanilla
Přejděte do vytvořeného adresáře, nainstalujte závislosti a spusťte vývojový server:
cd motion-controlsnpm inpm run dev
Upravte obsah těla v souboru index.html:
<video></video><canvas></canvas><script type="module" src="/js/get-video-data.js"></script>
Vytvořte adresář js ve složce kořenového adresáře projektu a v něm soubor get-video-data.js.
Získejte reference na prvky video a canvas, jakož i na kontext kreslení 2D grafiky:
const video$ = document.querySelector("video");const canvas$ = document.querySelector("canvas");const ctx = canvas$.getContext("2d");
Definujte šířku a výšku plátna, stejně jako požadavky (omezení) pro proud dat videa:
const width = 640;const height = 480;canvas$.width = width;canvas$.height = height;const constraints = { audio: false, video: { width, height },};
Získejte přístup k zařízení pro vstup videa uživatele pomocí metody getUserMedia; předejte proud dat do prvku videa pomocí atributu srcObject; po načtení metadat začněte přehrávat video a zavolejte metodu requestAnimationFrame předávající funkci drawVideoFrame jako argument:
navigator.mediaDevices .getUserMedia(constraints) .then((stream) => { video$.srcObject = stream; video$.onloadedmetadata = () => { video$.play(); requestAnimationFrame(drawVideoFrame); }; }) .catch(console.error);
Nakonec definujeme funkci pro vykreslení snímku videa na plátno pomocí metody drawImage
function drawVideoFrame() { ctx.drawImage(video$, 0, 0, width, height); requestAnimationFrame(drawVideoFrame);}
To, že volání requestAnimationFrame dvakrát spouští nekonečnou smyčku animace s rychlostí rámce specifickou pro zařízení, ale obvykle 60 snímků za sekundu (FPS). Rychlost snímků lze upravit pomocí argumentu časového razítka předaného zpětnému volání requestAnimationFrame ([příklad])(https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame#examples)):
function drawVideoFrame(timestamp) { // ...}
Pro detekci a sledování ruky potřebujeme několik dalších závislostí:
yarn add @mediapipe/camera_utils @mediapipe/drawing_utils @mediapipe/hands
MediaPipe Hands nejprve detekuje ruce a poté určuje 21 klíčových bodů (3D landmarky), které jsou klouby, pro každou ruku. Zde je, jak to vypadá:
Importování závislostí:
import { Camera } from "@mediapipe/camera_utils";import { drawConnectors, drawLandmarks } from "@mediapipe/drawing_utils";import { Hands, HAND_CONNECTIONS } from "@mediapipe/hands";
Konstruktor Camera umožňuje vytvářet instance pro ovládání videokamery a má následující signaturu:
export declare class Camera implements CameraInterface { constructor(video: HTMLVideoElement, options: CameraOptions); start(): Promise<void>; // We will not use this method stop(): Promise<void>;}
Konstruktor přijímá prvek videa a následující nastavení:
export declare interface CameraOptions { // Callback for frame caption onFrame: () => Promise<void>| null; // camera facingMode?: 'user'|'environment'; // width of frame width?: number; // height of frame height?: number;}
Metoda start spustí proces zachycení snímku.
Konstruktor Hands umožňuje vytvořit instance pro detekci rukou a má následující signaturu:
export declare class Hands implements HandsInterface { constructor(config?: HandsConfig); onResults(listener: ResultsListener): void; send(inputs: InputMap): Promise<void>; setOptions(options: Options): void; // more method what we did not use}
Konstruktor má tuto konfiguraci:
export interface HandsConfig { locateFile?: (path: string, prefix?: string) => string;}
Toto zpětné volání načte další soubory potřebné k vytvoření instance:
hand_landmark_lite.tflitehands_solution_packed_assets_loader.jshands_solution_simd_wasm_bin.jshands.binarypbhands_solution_packed_assets.datahands_solution_simd_wasm_bin.wasm
Metoda setOptions umožňuje nastavit následující možnosti zjišťování:
export interface Options { selfieMode?: boolean; maxNumHands?: number; modelComplexity?: 0|1; minDetectionConfidence?: number; minTrackingConfidence?: number;}
Informace o těchto nastaveních najdete zde. Nastavíme maxNumHands: 1 pro detekci pouze jedné ruky a modelComplexity: 0 pro zvýšení výkonu na úkor přesnosti detekce.
Metoda send používá ke zpracování jednoho snímku dat. Je volána v **onFrame **metodě instance fotoaparátu.
Metoda onResults přijímá zpětné volání pro zpracování výsledků detekce ruky.
Metoda drawLandmarks umožňuje vykreslit klíčové body ruky a má následující signaturu:
export declare function drawLandmarks( ctx: CanvasRenderingContext2D, landmarks?: NormalizedLandmarkList, style?: DrawingOptions): void;
Přijímá kontext výkresu, klíčové body a následující styly:
export declare interface DrawingOptions { color?: string|CanvasGradient|CanvasPattern| Fn<Data, string|CanvasGradient|CanvasPattern>; fillColor?: string|CanvasGradient|CanvasPattern| Fn<Data, string|CanvasGradient|CanvasPattern>; lineWidth?: number|Fn<Data, number>; radius?: number|Fn<Data, number>; visibilityMin?: number;}
Metoda drawConnectors umožňuje kreslit spojnice mezi klíčovými body a má následující signaturu:
export declare function drawConnectors( ctx: CanvasRenderingContext2D, landmarks?: NormalizedLandmarkList, connections?: LandmarkConnectionArray, style?: DrawingOptions): void;
Stará se o definování počátečních a koncových párů indexů bodů klíče (HAND_CONNECTIONS) a stylů.
Zpět k úpravám souboru track-hand-motions.js:
const video$ = document.querySelector("video");const canvas$ = document.querySelector("canvas");const ctx = canvas$.getContext("2d");const width = 640;const height = 480;canvas$.width = width;canvas$.height = height;
Definujeme funkci pro zpracování výsledků detekce ruky:
function onResults(results) { // of the entire result object, we are only interested in the `multiHandLandmarks` property, // which contains arrays of control points of detected hands if (!results.multiHandLandmarks.length) return; // when 2 hand are found, for example `multiHandLandmarks` will contain 2 arrays of control points console.log("@landmarks", results.multiHandLandmarks[0]); // draw a video frame ctx.save(); ctx.clearRect(0, 0, width, height); ctx.drawImage(results.image, 0, 0, width, height); // iterate over arrays of breakpoints // we could do without iteration since we only have one array, // but this solution is more flexible for (const landmarks of results.multiHandLandmarks) { // draw keypoints drawLandmarks(ctx, landmarks, { color: "#FF0000", lineWidth: 2 }); // draw lines drawConnectors(ctx, landmarks, HAND_CONNECTIONS, { color: "#00FF00", lineWidth: 4, }); } ctx.restore();}
Vytvoření instance pro detekci ruky, nastavení a registraci obsluhy výsledku:
const hands = new Hands({ locateFile: (file) => `../node_modules/@mediapipe/hands/${file}`,});hands.setOptions({ maxNumHands: 1, modelComplexity: 0,});hands.onResults(onResults);
Nakonec vytvoříme instanci pro ovládání videokamery, zaregistrujeme obslužnou rutinu, nastavíme nastavení a spustíme proces snímání snímků:
const camera = new Camera(video$, { onFrame: async () => { await hands.send({ image: video$ }); }, facingMode: undefined, width, height,});camera.start();
Upozornění: ve výchozím nastavení je nastavení facingMode nastaveno na hodnotu user - zdrojem obrazových dat je přední (frontální) kamera notebooku. Protože v mém případě je tímto zdrojem kamera USB, měla by být hodnota tohoto nastavení nedefinovaná.
Pole řídicích bodů detekovaného štětce vypadá takto:
Ukazatele odpovídají kloubům ruky, jak je znázorněno na obrázku výše. Například index prvního kloubu ukazováčku shora je 7. Každý kontrolní bod má souřadnice x, y a z v rozsahu 0 až 1.
Výsledek provedení příkladového kódu:
Štípnutí jako gesto je přiblížení špiček ukazováku a palce na poměrně malou vzdálenost.
Ptáte se: "Co přesně se považuje za dostatečně blízkou vzdálenost?"
Rozhodli jsme se definovat tuto vzdálenost jako 0,8 pro souřadnice x a y a 0,11 pro souřadnici z. Osobně s těmito výpočty souhlasím. Zde je vizuální znázornění:
const distance = { x: Math.abs(fingerTip.x - thumbTip.x), y: Math.abs(fingerTip.y - thumbTip.y), z: Math.abs(fingerTip.z - thumbTip.z), };const areFingersCloseEnough = distance.x < 0.08 && distance.y < 0.08 && distance.z < 0.11;
Několik důležitějších věcí:
Vytvořte soubor detect-pinch-gesture.js v adresáři js.
Začátek kódu je totožný s kódem předchozího příkladu:
import { Camera } from "@mediapipe/camera_utils";import { Hands } from "@mediapipe/hands";const video$ = document.querySelector("video");const width = window.innerWidth;const height = window.innerHeight;const handParts = { wrist: 0, thumb: { base: 1, middle: 2, topKnuckle: 3, tip: 4 }, indexFinger: { base: 5, middle: 6, topKnuckle: 7, tip: 8 }, middleFinger: { base: 9, middle: 10, topKnuckle: 11, tip: 12 }, ringFinger: { base: 13, middle: 14, topKnuckle: 15, tip: 16 }, pinky: { base: 17, middle: 18, topKnuckle: 19, tip: 20 },};const hands = new Hands({ locateFile: (file) => `../node_modules/@mediapipe/hands/${file}`,});hands.setOptions({ maxNumHands: 1, modelComplexity: 0,});hands.onResults(onResults);const camera = new Camera(video$, { onFrame: async () => { await hands.send({ image: video$ }); }, facingMode: undefined, width, height,});camera.start();const getFingerCoords = (landmarks) => landmarks[handParts.indexFinger.topKnuckle];function onResults(handData) { if (!handData.multiHandLandmarks.length) return; updatePinchState(handData.multiHandLandmarks[0]);}
Definujte typy událostí, zpoždění a stav připnutí:
const PINCH_EVENTS = { START: "pinch_start", MOVE: "pinch_move", STOP: "pinch_stop",};const OPTIONS = { PINCH_DELAY_MS: 250,};const state = { isPinched: false, pinchChangeTimeout: null,};
Deklarujte funkci detekce štípnutí:
function isPinched(landmarks) { const fingerTip = landmarks[handParts.indexFinger.tip]; const thumbTip = landmarks[handParts.thumb.tip]; if (!fingerTip || !thumbTip) return; const distance = { x: Math.abs(fingerTip.x - thumbTip.x), y: Math.abs(fingerTip.y - thumbTip.y), z: Math.abs(fingerTip.z - thumbTip.z), }; const areFingersCloseEnough = distance.x < 0.08 && distance.y < 0.08 && distance.z < 0.11; return areFingersCloseEnough;}
Definujte funkci, která vytvoří vlastní událost pomocí konstruktoru CustomEvent a zavolá ji pomocí metody dispatchEvent:
``
// the function takes the name of the event and the data - the coordinates of the finger
function triggerEvent({ eventName, eventData }) {
const event = new CustomEvent(eventName, { detail: eventData });
document.dispatchEvent(event);
}
Definujte funkci aktualizace stavu špendlíku:
function updatePinchState(landmarks) {
// determine the previous state
const wasPinchedBefore = state.isPinched;
// determine the beginning or end of the pinch
const isPinchedNow = isPinched(landmarks);
// define a state transition
const hasPassedPinchThreshold = isPinchedNow !== wasPinchedBefore;
// determine the state update delay
const hasWaitStarted = !!state.pinchChangeTimeout;
// if there is a state transition and we are not in idle mode
if (hasPassedPinchThreshold && !hasWaitStarted) {
// call the corresponding event with a delay
registerChangeAfterWait(landmarks, isPinchedNow);
}
// if the state remains the same
if (!hasPassedPinchThreshold) {
// cancel standby mode
cancelWaitForChange();
// if the pinch continuesif (isPinchedNow) { // trigger the corresponding event triggerEvent({ eventName: PINCH_EVENTS.MOVE, eventData: getFingerCoords(landmarks), });}
}
}
Definujeme funkce pro aktualizaci stavu a zrušení čekání:
function registerChangeAfterWait(landmarks, isPinchedNow) {
state.pinchChangeTimeout = setTimeout(() => {
state.isPinched = isPinchedNow;
triggerEvent({ eventName: isPinchedNow ? PINCH_EVENTS.START : PINCH_EVENTS.STOP, eventData: getFingerCoords(landmarks),});
}, OPTIONS.PINCH_DELAY_MS);
}
function cancelWaitForChange() {
clearTimeout(state.pinchChangeTimeout);
state.pinchChangeTimeout = null;
}
Definujeme obslužné programy pro začátek, pokračování a konec stisku (jednoduše vypíšeme souřadnice horního kloubu ukazováčku na konzoli):
function onPinchStart(eventInfo) {
const fingerCoords = eventInfo.detail;
console.log("Pinch started", fingerCoords);
}
function onPinchMove(eventInfo) {
const fingerCoords = eventInfo.detail;
console.log("Pinch moved", fingerCoords);
}
function onPinchStop(eventInfo) {
const fingerCoords = eventInfo.detail;
console.log("Pinch stopped", fingerCoords);
// change background color on STOP
document.body.style.backgroundColor =
"#" + Math.floor(Math.random() * 16777215).toString(16);
}
A zaregistrujte je:
document.addEventListener(PINCH_EVENTS.START, onPinchStart);
document.addEventListener(PINCH_EVENTS.MOVE, onPinchMove);
document.addEventListener(PINCH_EVENTS.STOP, onPinchStop);
Výsledek na videu:https://www.youtube.com/watch?v=KsLQRb6BhbI### ZávěrNyní, když jsme dospěli do tohoto bodu, můžeme s naší webovou aplikací pracovat, jakkoli si přejeme. To mimo jiné zahrnuje změnu stavu a interakci s prvky HTML.Jak vidíte, možnosti využití této technologie jsou prakticky neomezené, takže neváhejte a zkoumejte a experimentujte s ní.Tímto končím to, o co jsem se s vámi chtěl v tomto tutoriálu podělit. Doufám, že pro vás byl poučný a poutavý a že nějakým způsobem obohatil vaše znalosti. Děkuji vám za pozornost a přeji vám šťastné kódování!
Blog posts you may be interested in
New blog posts you may be interested in
Pomáháme korporacím, středním podnikům a startupům s digitálními produkty.