- > landmarks;
const PoseLandmarkerResult.empty() : landmarks = const [];
const PoseLandmarkerResult({required this.landmarks});
}
Библиотека возвращает список распознанных поз (первое измерение списка). Каждая поза — список точек с фиксированными индексами (второе измерение списка):
Базовый класс имплементаций
С этими типами мы теперь можем описать класс, который каждая имплементация будет наследовать:
abstract class FlutterMediapipeVisionPlatform extends PlatformInterface {
FlutterMediapipeVisionPlatform() : super(token: _token);
static final Object _token = Object();
static FlutterMediapipeVisionPlatform _instance =
FlutterMediapipeVisionMethodChannel();
static FlutterMediapipeVisionPlatform get instance => _instance;
static set instance(FlutterMediapipeVisionPlatform instance) {
PlatformInterface.verify(instance, _token);
_instance = instance;
}
Future
[Перевод] Распознаём позу человека во Flutter Web с MediaPipe
Давайте распознаем позу по видео с вебкамеры вот так:
Для этого есть библиотека MediaPipe, которая может распознавать много всего в картинках, текстах и звуках. Среди прочего там есть модель для распознания положения тела на изображениях.
Здесь можно попробовать официальное демо.
Ещё есть CodePen, чтобы быстро попробовать код на JavaScript:
https://codepen.io/mediapipe‑preview/pen/abRLMxN
Но эта модель работает только в Android, iOS, на Python и JavaScript, но не во Flutter напрямую.
Кто‑то сделал пакет flutter_mediapipe, но он заброшен уже 4 года и не поддерживает веб.
Поэтому давайте подключим официальную реализацию на JavaScript в качестве собственного веб‑плагина для Flutter.
Готовое демо моего приложения — здесь (только Chrome)
Скачайте исходники здесь (потому что я буду пропускать некоторые вещи):
https://github.com/alexeyinkin/flutter‑mediapipe
Создаём плагин
Плагин — это специальный вид пакета Dart, который подключает разные имплементации в зависимости от того, под какую платформу собираем приложение.
Вот отличный официальный учебник, как писать плагины:
https://docs.flutter.dev/packages‑and‑plugins/developing‑packages
Есть ещё прекрасное введение в написание именно веб‑плагинов, от автора официального пакета url_launcher. Там рассказано, как они добавили поддержку веба в этот пакет, когда Flutter только начал поддерживать веб:
Часть 1 объясняет базовый подход — такой же, как был в плагинах для Android и iOS: так называемый method channel, чтобы делегировать что‑то нативному коду на этих платформах.
Часть 2 упрощает это и убирает method channel, потому что веб‑плагины и так написаны на Dart, а значит, можно вызывать все методы имплементации напрямую.
Обе статьи обращаются только к стандартному API браузера и не обращаются к произвольному JavaScript, взятому где‑то ещё. Поэтому в этой статье я расскажу, как обращаться к произвольному JavaScript, основываясь на всём, что вы узнали в тех статьях.
Используя архитектуру из последней статьи по url_launcher, я сделал три пакета Dart:
flutter_mediapipe_vision — главный пакет. Все приложения, которые хотят распознавать позы на изображениях, подключают его как зависимость. И только его. Он уже подтягивает в проект другие пакеты как зависимости и вызывает методы конкретной имплементации. Ненужные имплементации Flutter сам стрясёт с помощью tree‑shaking.
flutter_mediapipe_vision_platform_interface описывает интерфейс, которому каждая имплементация должна соответствовать. Этот пакет не делает ничего полезного, только подменяет имплементацию, чтобы первый пакет об этом ничего не знал.
flutter_mediapipe_vision_web — реализация для веба, главный фокус этой статьи. Он зависит от второго пакета, потому что содержит имплементацию интерфейса, описанного там, и ничего не знает о первом пакете. Первый же пакет зависит от него и подтягивает его рекурсивно во все приложения, где используется.
flutter_mediapipe_vision
Каким должен быть интерфейс конечного пользователя? Статические функции хорошо работают с подменяемыми имплементациями:
class FlutterMediapipeVision {
static Future ensureInitialized() async {
await FlutterMediapipeVisionPlatform.instance.ensureInitialized();
}
static Future detect(Uint8List bytes) async {
return await FlutterMediapipeVisionPlatform.instance.detect(bytes);
}
}
Этот класс преобразует вызовы статических функций в вызовы методов конкретной имплементации.
Первая функция инициализирует модель. Её можно назвать как угодно, но ensureInitialized()
— это удобно. Вы же помните WidgetsFlutterBinding.ensureInitialized()
Вторая функция получает байты изображения (каждого кадра) и вызывает detect()
на модели. Эта функция называется именно так во всех имплементациях MediaPipe.
Обратите внимание на возвращаемый тип. Скоро мы его опишем.
flutter_mediapipe_vision_platform_interface
Типы данных
Начнём с типов. В библиотеке JavaScript, которую мы подключим, есть типы для распознанных точек и общего результата. Однако, наш плагин должен возвращать что‑то независимое от платформы, поэтому нужно описать собственные типы.
Это точка, распознанная в позе:
class NormalizedLandmark {
final double x;
final double y;
const NormalizedLandmark({required this.x, required this.y});
Offset get offset => Offset(x, y);
}
Она называется normalized, потому что x
и y
будут от 0
до 1
, если они помещаются в кадре. Они также могут быть меньше нуля или больше единицы, если изображение обрезано и модель думает, что эта конкретная точка находится за кадром — как мой локоть здесь:
А почему бы нам не использовать Offset
из dart:ui
? Библиотека ещё возвращает z
— расстояние от камеры, и ещё несколько интересных вещей, которые нам пока не нужны, но хорошо иметь возможность их потом добавить. Поэтому просто Offset
нам не хватит.
Кроме того, этот тип NormalizedLandmark
есть во всех имплементациях: TypeScript, Java, и др. Поэтому лучше, когда всё согласуется.
Дальше — результат распознания всего изображения:
class PoseLandmarkerResult {
final List