JavaScript: Ovládání webové stránky gesty

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í.

Table of contents

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.

Příprava a nastavení projektu

Krok 1

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

Krok 2

Přejděte do vytvořeného adresáře, nainstalujte závislosti a spusťte vývojový server:

cd motion-controlsnpm inpm run dev

Krok 3

Upravte obsah těla v souboru index.html:

<video></video><canvas></canvas><script type="module" src="/js/get-video-data.js"></script>

Získání videodat a jejich vykreslení na plátno

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);}

Poznámka

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) {  // ...}

Result

 JavaScript: Ovládání webové stránky gesty

                               
                           
                       
                           

Detekce a sledování ruky

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á:

 

Hand 3D landmarks

Vytvořte soubor track-hand-motions.js ve složce js.

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:

 

How to control Javascript with gestures

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:

 

Definice gesta štípnutí:

Š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í:

  • chceme zaregistrovat a zpracovat začátek, pokračování a konec pinče (pinch_start, pinch_move a pinch_stop);
  • pro určení přechodu pinče z jednoho stavu do druhého (začátek -> konec nebo naopak) je nutné uložit předchozí stav;
  • detekce přechodu musí být provedena s určitým zpožděním, například 250 ms.

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í!

Read also

Blog posts you may be interested in

10
minut na čtení
September 24, 2024

Integrace AI v podnikání - pohled AI inženýra z Moravio

Ladislav Husty, zkušený inženýr AI, sdílí své zkušenosti s integrací AI do podnikání
18
minut na čtení
September 21, 2024

Co je WebRTC (Web Real Time Communications)?

V tomto článku Alexey Andrushchenko, zkušený vývojář Full-Stack, odhalí některé funkce používání WebRTC a zváží výhody a nevýhody této technologie.
8
minut na čtení
October 2, 2023

Technický dluh - 1. část - Co? Proč? Jak ovlivňuje vaše podnikání?

Co je technický dluh? Jak ovlivňuje váše podnikání? Jak mu můžete předejít a jak s ním naložit, když už vznikl? To vše se pokusíme vysvětlit v této dvoudílné sérii článků.
New articles

New blog posts you may be interested in

6
minut na čtení
November 26, 2024

Postřehy a možnosti v automatizaci náboru od Jiřího Kostova

Náš HR Manager sdílí poznatky o tom, jak jsme automatizovali náborové procesy ve společnosti Moravio. Vysvětluje, jak jsme optimalizovali naše interní pracovní postupy a pomohli klientům implementovat efektivní automatizovaná řešení pro jejich podnikání. Objevte přístupy, které fungují, a příležitosti, které může automatizace náboru odemknout.
11
minut na čtení
October 21, 2024

Jak jsme centralizovali naše data pro chytřejší rozhodování pomocí BI

Pavel Janko, Head of Delivery v Moravu, se s vámi podělí o to, jak jsme díky centralizaci dat pomocí BI zlepšili rozhodování a zefektivnili naši práci.
7
minut na čtení
October 10, 2024

Projektové řízení: Mezi flexibilitou a omezenými zdroji

Hsinyu Ko sdílí své postřehy k hledání rovnováhy mezi požadavky na flexibilní řízení projektů a efektivní využití zdrojů

Přemýšlíte o projektu? Napište nám.

Pomáháme korporacím, středním podnikům a startupům s digitálními produkty.

Napsat zprávu

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Odpovíme vám co nejdříve.
Vaše informace jsou u nás v bezpečí.
Rádi zodpovíme všechny vaše dotazy!

Zarezervujte si schůzku

Jakub Bílý

Vedoucí obchodu
Chcete s námi mluvit přímo? Zarezervujte si schůzku s Jakubem z rozvoje podnikání.
Zarezervujte si schůzku