JavaScript: Webseitensteuerung per Gesten

Während unserer Arbeit an Stardio, wurde uns die Aufgabe übertragen, die Fernsteuerung der Anwendung zu implementieren. Wir haben verschiedene Implementierungsoptionen untersucht, und einer der Ansätze, mit denen wir experimentierten, war die Computer Vision-Technologie. In diesem Artikel werde ich die Ergebnisse unserer Experimente mit einer der bekanntesten Bibliotheken für ComputerVision vorstellen - MEDIAPIPE, die von Google entwickelt wurde.
In der Vergangenheit war es nur in Science-Fiction-Filmen üblich, den Inhalt einer Webseite mithilfe von Gesten zu steuern. Aber heutzutage benötigen Sie nur noch eine Videokamera, einen Browser und eine Bibliothek von Google, um dies zu verwirklichen. In diesem Tutorial zeigen wir, wie die Gestensteuerung mit reinem JavaScript implementiert wird. Um Handgesten zu erkennen und zu verfolgen, werden wir MediaPipe verwenden, und um Abhängigkeiten zu verwalten, werden wir npm verwenden.
Der Beispielcode befindet sich in diesem Repository.
Erstellen Sie ein reines JS-Projekt mit Vite von Vanilla Template:
Garn Create Vite Motion-Controls --Template Vanille
Gehe in das erstellte Verzeichnis, installiere die Abhängigkeiten und starte den Entwicklungsserver:
cd motion-controlsnpm inpm run dev
Bearbeiten Sie den Inhalt des Hauptteils in index.html:
<video><canvas><script type="module" src="/js/get-video-data.js"></script></canvas></video>
Erstellen Sie ein js-Verzeichnis im Stammverzeichnis des Projekts und eine Datei get-video-data.js darin.
Hier finden Sie Verweise auf die Video- und Canvas-Elemente sowie auf den Kontext der 2D-Grafikzeichnung:
const video$ = document.querySelector („video“); const canvas$ = document.querySelector („Leinwand“); const ctx = canvas$.getContext („2d“);
Definieren Sie die Breite und Höhe der Leinwand sowie die Anforderungen (Einschränkungen) für den Videodatenstrom:
const width = 640; const height = 480; canvas$.width = width; canvas$.height = height; const constraints = {audio: false, video: {width, height},};
Erhalten Sie Zugriff auf das Videoeingabegerät des Benutzers mit dem Benutzermedien abrufen Methode; übergebe den Stream an das Videoelement mit dem SRC-Objekt Attribut; nach dem Laden der Metadaten beginnen wir das Video abzuspielen und rufen den Animationsframe anfordern Methode, wobei die drawVideoFrame-Funktion als Argument übergeben wird:
navigator.MediaDevices .getUserMedia (Einschränkungen) .then ((stream) => {video$.srcObject = stream; video$.onloadedmetadata = () => {video$.play (); requestAnimationFrame (drawVideoFrame);};}) .catch (console.error);
Schließlich definieren wir die Funktion zum Zeichnen des Videoframes auf der Leinwand mit dem Bild zeichnen Methode:
Funktion drawVideoFrame () {ctx.drawImage (video$, 0, 0, Breite, Höhe); requestAnimationFrame (drawVideoFrame);}
Wenn RequestAnimationFrame zweimal aufgerufen wird, wird eine unendliche Animationsschleife mit einer gerätespezifischen Bildrate ausgeführt, normalerweise jedoch mit 60 Bildern pro Sekunde (FPS). Die Bildrate kann mit dem Timestamp-Argument angepasst werden, das an den requestAnimationFrame-Callback () übergeben wird.Beispiel):
Funktion drawVideoFrame (Zeitstempel) {//...}
Um die Hand zu erkennen und zu verfolgen, benötigen wir ein paar zusätzliche Abhängigkeiten:
Garn hinzufügen @mediapipe /camera_utils @mediapipe /drawing_utils @mediapipe /hands
MediaPipe Hände erkennt zuerst die Hände und bestimmt dann 21 Schlüsselpunkte (3D-Landmarken), bei denen es sich um Gelenke handelt, für jede Hand. So sieht das aus:
Erstellen Sie eine Datei track-hand-motions.js im Verzeichnis js.
Abhängigkeiten importieren:
importiere {Camera} von "@mediapipe /camera_utils „; importiere {drawConnectors, drawLandmarks} aus" @mediapipe /drawing_utils „; importiere {Hands, HAND_CONNECTIONS} von" @mediapipe /hands „;
Der Kamera-Konstruktor ermöglicht das Erstellen von Instanzen zur Steuerung einer Videokamera und hat die folgende Signatur:
<void>export declare class Camera implementiert CameraInterface {constructor (video: HtmlVideoElement, options: cameraOptions); start (): Promise<void>;//Wir werden diese Methode nicht verwenden stop (): Promise;}
Der Konstruktor verwendet ein Videoelement und die folgenden Einstellungen:
export declare interface cameraOptions {//Callback für Bildunterschrift onFrame: () => Promise <void>| null;//camera facingMode? : 'user'|'environment';//Breite der Rahmenbreite? : Zahl;//Höhe der Rahmenhöhe? : Zahl;}
Die Startmethode startet den Frame-Capture-Prozess.
Der Hands-Konstruktor ermöglicht das Erstellen von Instanzen zur Erkennung von Händen und hat die folgende Signatur:
exportiere die Deklarationsklasse Hands implementiert HandsInterface {constructor (config? : handsConfig); onResults (listener: resultsListener): void; send (inputs: inputMap): Promise; <void>setOptions (options: Options): void;//weitere Methode, die wir nicht benutzt haben}
Konstruktoren haben diese Konfiguration:
Exportschnittstelle HandsConfig {LocateFile? : (Pfad: Zeichenfolge, Präfix? : Zeichenfolge) => Zeichenfolge;}
Dieser Callback lädt zusätzliche Dateien, die zum Erstellen einer Instanz benötigt werden:
hand_landmark_lite.tflitehands_solution_packed_assets_loader.jshands_solution_simd_wasm_bin.jshands.binarypbhands_solution_packed_assets.datahands_solution_simd_wasm_bin.wasm
Mit der setOptions-Methode können Sie die folgenden Discovery-Optionen festlegen:
Optionen für die Exportschnittstelle {SelfieMode? : boolesch; maxNumHands? : Zahl; ModelComplexity? : 0 | 1; Minimale Erkennungssicherheit? : Zahl; minTrackingConfidence? : Zahl;}
Sie können über diese Einstellungen lesen hier. Wir werden setzen Max. Anzahl Hände: 1, um nur eine Hand zu erkennen und Komplexität des Modells: 0, um die Leistung auf Kosten der Erkennungsgenauigkeit zu verbessern.
Das senden Methode wird verwendet, um einen einzelnen Datenrahmen zu verarbeiten. Sie wird in der **onFrame-**-Methode der Camera-Instanz aufgerufen.
Die Methode onResults akzeptiert einen Callback, um die Ergebnisse der Handerkennung zu verarbeiten.
Das Wahrzeichen zeichnenDiese Methode ermöglicht das Zeichnen von Handtastenpunkten und hat die folgende Signatur:
Exportdeklarationsfunktion drawLandmarks (ctx: CanvasRenderingContext2D, Landmarken? : Normalisierte LandmarkList, Stil? : drawingOptions): ungültig;
Es akzeptiert einen Zeichnungskontext, Schlüsselpunkte und die folgenden Stile:
Schnittstelle exportieren deklarieren DrawingOptions {color? <Data, string|CanvasGradient|CanvasPattern>: Zeichenfolge|CanvasGradient|Leinwandmuster| Fn; Füllfarbe? <Data, string|CanvasGradient|CanvasPattern>: Zeichenfolge | CanvasGradient | Leinwandmuster | Fn; Linienbreite? <Data, number>: Zahl|fn; Radius? <Data, number>: Zahl|fn; SichtbarkeitMin? : Zahl;}
Die DrawConnectors-Methode ermöglicht das Zeichnen von Verbindungslinien zwischen Schlüsselpunkten und hat die folgende Signatur:
Exportdeklarationsfunktion drawConnectors (ctx: CanvasRenderingContext2D, Landmarken? : NormalizedLandmarkList, Verbindungen? : LandmarkConnectionArray, Stil? : drawingOptions): ungültig;
Es kümmert sich um die Definition von Schlüsselpunkten, Start- und Endschlüsselpunkten, Indexpaaren (HAND_CONNECTIONS) und Stilen.
Zurück zur Bearbeitung von 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 = Höhe;
Wir definieren die Funktion zur Verarbeitung der Ergebnisse der Handerkennung:
function onResults (results) {//des gesamten Ergebnisobjekts, wir sind nur an der Eigenschaft `multiHandLandmarks` interessiert,//die Arrays von Kontrollpunkten der erkannten Hände enthält, wenn (! results.multiHandLandmarks.length) return;//wenn 2 Hände gefunden werden, enthält `multiHandLandmarks` zum Beispiel 2 Arrays von Kontrollpunkten console.log (“ @landmarks „, results.multiHandLandmarks [0]);//zeichne einen Videoframe ctx.save (); ctx.clearRect (0, 0, width, height); ctx.drawImage (results.image, 0, width, height));//über Arrays von Breakpoints iterieren//wir könnten auf eine Iteration verzichten, da wir nur ein Array haben,//aber diese Lösung ist flexibler für (const landmarks of results.multiHandLandmarks) {//draw keypoints drawLandmarks (ctx, landmarks, {color: "#FF0000 „, lineWidth: 2});//Linien zeichnen drawConnectors (ctx, landmarks, HAND_CONNECTIONS, {color:" #00FF00 „, lineWidth: 4,});} ctx.restore ();}
Erstellen Sie eine Instanz, um die Hand zu erkennen, legen Sie die Einstellungen fest und registrieren Sie den Ergebnishandler:
const hands = neue Hände ({locateFile: (file) => `.. /node_modules/ @mediapipe /hands/ $ {file} `,}); hands.setOptions ({maxNumHands: 1, modelComplexity: 0,}); hands.onResults (onResults);
Schließlich erstellen wir eine Instanz zur Steuerung der Videokamera, registrieren den Handler, legen die Einstellungen fest und starten den Frame-Capture-Prozess:
const camera = new Camera (video$, {onFrame: async () => {await hands.send ({image: video$});}, FacingMode: undefined, width, height,}); camera.start ();
Bitte beachten Sie: standardmäßig ist die FacingMode-Einstellung auf gesetzt Benutzer - Die Quelle der Videodaten ist die vordere (vordere) Laptopkamera. Da es sich bei dieser Quelle in meinem Fall um eine USB-Kamera handelt, sollte der Wert dieser Einstellung undefiniert sein.
Die Anordnung der Kontrollpunkte des erkannten Pinsels sieht wie folgt aus:
Die Indizes entsprechen den Handgelenken, wie in der Abbildung oben gezeigt. Zum Beispiel ist der Index des ersten Zeigefingergelenks von oben 7. Jeder Kontrollpunkt hat X-, Y- und Z-Koordinaten im Bereich von 0 bis 1.
Das Ergebnis der Ausführung des Beispielcodes:
Ein Kneifen als Geste ist das Zusammenführen der Zeigefinger- und Daumenspitzen auf einen relativ geringen Abstand.
Ihr fragt: ‚Was genau gilt als nah genug entfernt? '“
Wir haben beschlossen, diesen Abstand sowohl für die X- als auch für die Y-Koordinaten als 0,8 und für die Z-Koordinate als 0,11 zu definieren. Persönlich stimme ich diesen Berechnungen zu. Hier ist eine visuelle Darstellung:
const distance = {x: Math.abs (Fingertip.X - ThumbTip.X), y: Math.ABS (Fingertip.Y - ThumbTip.Y), z: Math.ABS (Fingertip.Z - ThumbTip.Z),}; const areFingers CloseEnough = distance.x < 0.08 && distance.z < 0.11;
Noch ein paar wichtige Dinge:
Erstellen Sie eine Datei detect-pinch-gesture.js im Verzeichnis js.
Der Anfang des Codes ist identisch mit dem Code des vorherigen Beispiels:
importiere {Camera} von "@mediapipe /camera_utils „; importiere {Hands} aus" @mediapipe /hands „; const video$ = document.querySelector („video“); const width = window.innerWidth; const height = window.innerHeight; const handParts = {Handgelenk: 0, Daumen: {Basis: 1, Mitte: 2, TopKnuckle: 3, Spitze: 4}, IndexFinger: {Basis: 5, Mitte: 6, TopKnuckle: 7, Spitze: 8}, Mittelfinger: {Basis: 9, Mitte: 10, TopKnuckle: 11, Spitze: 12}, RingFinger: {Basis: 13, Mitte: 14, TopKnuckle: 15, Spitze: 16}, kleiner Finger: {Basis: 17, Mitte: 18, TopKnuckle: 19, Tipp: 20},}; const hands = new Hands ({locateFile: (datei) => `.. /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, Breite, Höhe,}); camera.start (); const getFingerCoords = (Landmarken) => Orientierungspunkte [handparts.indexFinger.topKnuckle]; function onResults (HandData) {if (! HandData.MultiHandLandmarks.Length) return; updatePinchState (handData.MultiHandLandmarks [0]);}
Definieren Sie Ereignistypen, Verzögerung und Pinch-Status:
const PINCH_EVENTS = {START: „pinch_start“, MOVE: „pinch_move“, STOP: „pinch_stop“,}; const OPTIONS = {PINCH_DELAY_MS: 250,}; const state = {isPinched: falsch, pinchChangeTimeout: null,};
Deklarieren Sie eine Pinch-Erkennungsfunktion:
function isPinched (landmarks) {const FingerTip = Orientierungspunkte [handparts.indexFinger.Tip]; const thumbTip = Orientierungspunkte [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 areFingers CloseEnough = distance.x < 0.08 && distance.y < 0.08 && distance.y z < 0,11; gibt areFingersCloseEnough zurück;}
Definieren Sie eine Funktion, die ein benutzerdefiniertes Ereignis erstellt, indem Sie Benutzerdefiniertes Ereignis Konstruktor und ruft ihn mit dem auf Dispatch-Ereignis Methode:
//Die Funktion nimmt den Namen des Ereignisses und die Daten an - die Koordinaten der Fingerfunktion triggerEvent ({eventName, eventData}) {const event = new customEvent (eventName, {detail: eventData}); document.dispatchEvent (event);}
Definieren Sie eine Pinch State Update-Funktion:
function updatePinchState (landmarks) {//den vorherigen Zustand ermitteln const wasPinchedBefore = State.isPinched;//den Anfang oder das Ende des Pinches ermitteln const isPinchedNow = isPinched (landmarks);//definiere einen Zustandsübergang const hasPassedPinchThreshold = isPinchedNow! == wasPinchedBefore;//die Verzögerung beim Zustandsupdate ermitteln const hasWaitStarted =! state.pinchChangeTimeout;//wenn es einen Zustandsübergang gibt und wir uns nicht im Leerlaufmodus befinden, if (hasPassedPinchThreshold &&! hasWaitStarted) {//rufe das entsprechende Event verzögert auf registerChangeAfterWait (landmarks, isPinchedNow);}//wenn der Zustand gleich bleibt if (! hasPassedPinchThreshold) {//Bereitschaftsmodus abbrechen cancelWaitForChange ();//wenn der Pinch anhält if (isPinchedNow) {//das entsprechende Ereignis triggerEvent auslösen ({eventName: PINCH_EVENTS.MOVE, eventData: getFingerCoords (landmarks),});}}
Wir definieren die Funktionen zum Aktualisieren des Zustands und zum Abbrechen der Wartezeit:
Funktion registerChangeAfterWait (Landmarken, isPinchedNow) {state.pinchChangeTimeout = setTimeout (() => {state.isPinched = isPinchedNow; triggerEvent ({eventName: isPinchedNow? PINCH_EVENTS.START: PINCH_EVENTS.STOP, eventData: getFingerCoords (Landmarken),});}, OPTIONS.PINCH_DELAY_MS);} function cancelWaitForChange () {clearTimeout (state.pinchChangeTimeout); state.pinchChangeTimeout = null;}
Wir definieren die Handler für den Anfang, die Fortsetzung und das Ende des Pinches (wir drucken einfach die Koordinaten des oberen Gelenks des Zeigefingers auf die Konsole):
function onPinchStart (eventInfo) {const fingerCoords = eventInfo.Detail; console.log („Pinch gestartet“, fingerCoords);} function onPinchMove (EventInfo) {const fingerCoords = eventInfo.Detail; console.log („Pinch verschoben“, fingerCoords);} function onPinchStop (EventInfo) {const fingerCoords = eventInfo.detail; console.log („Pinch gestoppt“), fingerCoords);//ändere die Hintergrundfarbe auf STOP document.body.style.backgroundColor = „#“ + Math.floor (Math.random () * 16777215) .toString (16);}
Und registriere sie:
document.addEventListener (PINCH_EVENTS.START, onPinchStart); document.addEventListener (PINCH_EVENTS.MOVE, onPinchMove); document.addEventListener (PINCH_EVENTS.STOP, onPinchStop);
Ergebnis im Video:
https://www.youtube.com/watch?v=KsLQRb6BhbI
Jetzt, wo wir diesen Punkt erreicht haben, können wir mit unserer Webanwendung interagieren, wie wir es wünschen. Dazu gehören unter anderem das Ändern des Status und die Interaktion mit HTML-Elementen.
Wie Sie sehen, sind die potenziellen Anwendungen dieser Technologie praktisch unbegrenzt. Sie können sie also gerne erkunden und damit experimentieren.
Damit ist abgeschlossen, was ich in diesem Tutorial mit Ihnen teilen wollte. Ich hoffe, Sie fanden es informativ und ansprechend und es hat Ihr Wissen in gewisser Weise bereichert. Vielen Dank für Ihre Aufmerksamkeit und viel Spaß beim Programmieren!
Empfohlene Lektüre für Sie
Neue Blogbeiträge, die Sie interessieren könnten
Jakub Bílý
Leiter/in Geschäftsentwicklung