Краткое руководство по TypeScript и машинному обучению

Сегодня я хочу поделиться кратким руководством о том, как создать собственное приложение с функцией распознавания лиц, используя следующий стек технологий:
Ionic, Capacitor и MediaPipe.

Ну, я буду представлять не Ionic или Capacitor, а MediaPipe, потому что это свежая и открытая технология, предоставленная Google. MediaPipe — это, по сути, SDK для разработчиков, созданный на основе TensorFlow для предоставления возможностей машинного обучения на любом устройстве.

Зайдите и проверьте Центр MediaPipe, чтобы узнать больше. Первой общедоступной версии MediaPipe 6 месяцев, а официально о ней было объявлено менее недели назад на мероприятии Google I/O.

Это было введение! Что вы найдете в этом документе:

1) Initial Setup
  - Create Your Project
  - Install Packages
  - Add the Mobile version of your App
2) Implementing Machine Learning
  - Template & Styles 
  - Imports
  - Declarations
  - Functions

Начальная настройка

Прежде всего, нам нужно создать новый проект и настроить несколько конфигураций для начала. Если у вас уже есть проект, перейдите в раздел «Машинное обучение» ниже.

Создайте свой проект

Мы создадим базовое «пустое» приложение-шаблон, для него вам понадобится одна строка:

npx ionic start ml-pol blank --type=angular-standalone --capacitor --package-id=ml.pol

Установить пакеты

Теперь установим tasks-vision от MediaPipe на проект как зависимость:

npm i @mediapipe/tasks-vision

Поскольку мы работаем с проектом Typescript, вам необходимо предоставить типы для Offscreen Canvas, так как мы будем использовать их позже для правильной работы с MediaPipe:

npm i @types/offscreencanvas -D
// analyze the source which uses offscreencanvas if you still having trouble: node_modules\@mediapipe\tasks-vision/vision.d.ts

Добавьте мобильную версию вашего приложения.

Вы можете продолжить здесь, создав версию приложения для Android, iOS или обеих версий. Я продолжу сборку версии Android для этого примера:

npm install @capacitor/[email protected] --save-exact
npx cap add android && npx cap sync android

И, наконец, установите разрешение камеры в файле AndroidManifest.xml.

<uses-permission android:name="android.permission.CAMERA" />

Поздравляем! 🎉 На данный момент у нас есть настройка с использованием Angular v15, Ionic v7, Capacitor v5 и Tasks-Vision v0.10.

Вы можете попробовать запустить свое приложение в Интернете или на Android! Чтобы протестировать его на Android, используйте Android Studio (эмулируемое устройство) или подключите телефон через USB в режиме отладки.

npx ionic build && npx cap copy android && npx cap run android --target=[your_mobile_id] --external --no-build --no-sync --configuration=production

Внедрение машинного обучения

Во втором разделе мы увидим, как создать экземпляр контекста видения задач и модель для использования распознавания лиц, но сначала — шаблон :)

Шаблон и стили

Сделаем несколько корректировок в шаблоне. Замените все, что находится внутри div#containerв домашнем компоненте, следующим фрагментом HTML:

<ion-button [color]="tracking ? 'danger' : 'primary'">
    <ion-label class="ion-padding" (click)="toggleTracking()">Start Tracking</ion-label>
    <ion-icon [name]="tracking ? 'close' : 'finger-print'"></ion-icon>
</ion-button>
<video class="user-media" id="user-video" autoplay playsinline></video>
<canvas class="user-media" id="user-canvas" ></canvas>

Обновите CSS с помощью этих правил:

#container{
  /* 
  comment these positioning rules generated by Ionic template
  top: 50%;
  transform: translateY(-50%);
  (...other default styles) 
  */
  display: flex;
  flex-direction: column;
  align-items: center;
}
.user-media{
  min-height: 500px;
  max-width: 400px;
  margin-inline: auto;
}
#user-canvas{
  position: absolute; /* Will overlay the video stream. */
  top: 45px; /* Place it aprox. after the button space */
  object-fit: contain;
}

Теперь переключитесь на Typescript.

Импорт

Мы будем использовать следующий импорт из tasks-vision в компоненте, в котором мы хотим запустить распознавание лиц. В этом примере я размещу его на домашнем компоненте.

import { Category, DrawingUtils, FaceLandmarker, FilesetResolver } from '@mediapipe/tasks-vision';
// Category: a classification of a detected object or feature
// DrawingUtils: utility functions for drawing on a canvas
// FaceLandmarker: a machine learning model for facial landmark detection
// FilesetResolver: a utility class for resolving file paths for ML models

Декларации

Теперь давайте создадим переменные, которые мы будем использовать.

// ML Model and properties (WASM & Model provided by Google, you can place your own).
faceLandmarker!: FaceLandmarker;
wasmUrl: string = "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@latest/wasm";
modelAssetPath: string = "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task";
// Native elements we need to interact to later.
video!: HTMLVideoElement;
canvasElement!: HTMLCanvasElement;
canvasCtx!: CanvasRenderingContext2D;
// A state to toggle functionality.
showingPreview: boolean = false;
// A challenge state for the user.
userDidBlink: boolean = false;

Функции

Настройте экземпляр FaceLandmarker в хуке OnInit и получите держатели HTML после монтирования представления:

async ngOnInit(): Promise<void> {
  this.faceLandmarker = await FaceLandmarker.createFromOptions(await FilesetResolver.forVisionTasks(this.wasmUrl), {
    baseOptions: { modelAssetPath: this.modelAssetPath, delegate: "GPU" },
    outputFaceBlendshapes: true, // We will draw the face mesh in canvas.
    runningMode: "VIDEO",
  }); // When FaceLandmarker is ready, you'll see in the console: Graph successfully started running.
}
async ngAfterViewInit(): Promise<void> {
  this.video = document.getElementById("user-video") as HTMLVideoElement;
  this.canvasElement = document.getElementById("user-canvas") as HTMLCanvasElement;
  this.canvasCtx = this.canvasElement.getContext("2d") as CanvasRenderingContext2D;
}

Это событие переключения, срабатывающее при нажатии кнопки:

toggleTracking = () => (this.tracking = !this.tracking, this.tracking ? this.startTracking() : this.stopTracking());

И, наконец, функции startTracking и stopTracking:

startTracking() {
  // Check if we can access user media api.
  (!(!!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)) || !this.faceLandmarker) && (console.warn("user media or ml model is not available"), false);
  
  // Everything is ready to go!
  navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => (this.video.srcObject = stream, this.video.addEventListener("loadeddata", predictWebcam)));
  let lastVideoTime = -1; let results: any = undefined; const drawingUtils = new DrawingUtils(this.canvasCtx!);
  
  let predictWebcam = async () => {
    // Resize the canvas to match the video size.
    this.canvasElement.width = this.video.videoWidth; this.canvasElement.height = this.video.videoHeight;

    // Send the video frame to the model.
    lastVideoTime !== this.video.currentTime && (lastVideoTime = this.video.currentTime, results = this.faceLandmarker.detectForVideo(this.video, Date.now()));

    // Draw the results on the canvas (comment this out to improve performance or add even more markers like mouth, etc).
    if (results.faceLandmarks) for (const landmarks of results.faceLandmarks) {
      [FaceLandmarker.FACE_LANDMARKS_TESSELATION, FaceLandmarker.FACE_LANDMARKS_RIGHT_EYE, FaceLandmarker.FACE_LANDMARKS_LEFT_EYE]
        .every((type, i) => drawingUtils.drawConnectors(landmarks, type, { color: "#C0C0C070", lineWidth: i == 0 ? 1 : 4 }))
    };

    // Check if the user blinked (you can customize this to expect a smile, etc). Let's assume there is only one face.
    if (results.faceLandmarks && results.faceBlendshapes && results.faceBlendshapes[0] && results.faceBlendshapes![0].categories?.find(
      (shape: Category) => shape?.categoryName == "eyeBlinkRight")?.score > 0.4) (this.userDidBlink = true, alert('Guiño Guiño'));

    // Call this function again to keep predicting when the browser is ready.
    this.tracking == true && window.requestAnimationFrame(predictWebcam);
  }
}

stopTracking() { // Stop and clear the video & canvas
  this.tracking = false; (this.video.srcObject as MediaStream).getTracks().forEach(track => track.stop());
  this.video.srcObject = null; this.canvasCtx.clearRect(0, 0, this.canvasElement.width, this.canvasElement.height);
}

Мы сделали! все готово для запуска и тестирования нашего приложения (веб-сайта или мобильного приложения), а также для его дальнейшей настройки по нашему вкусу.

🐱‍👤Вы можете проверить исходный код здесь: https://github.com/GiustiRo/ml-pol

Если вы любите технологии, рекомендую вам проверить все анонсы Google I/O 2023, там есть удивительные вещи!

Увидимся в следующий раз! Ро.