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í
4
minut na čtení
September 5, 2023

Proměna webových zážitků pomocí MediaPipe a JavaScriptu: Komplexní hluboký ponor do problematiky

Tento článek se zabývá bezproblémovým spojením JavaScriptu a frameworku MediaPipe společnosti Google a ukazuje jejich společný potenciál na praktických příkladech kódu, reálných případech použití a návodech krok za krokem pro vytváření inovativních webových aplikací, zejména v oblasti rozšířené reality (AR), s rozšířenými interaktivními funkcemi.
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

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ů
6
minut na čtení
October 7, 2024

Sourcing Remote IT Talents by Barbora Thornton, COO in Moravio

Tips, Challenges, and Why It's the Right Choice

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