惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
Scott Helme
Scott Helme
爱范儿
爱范儿
WordPress大学
WordPress大学
博客园 - 三生石上(FineUI控件)
阮一峰的网络日志
阮一峰的网络日志
博客园 - Franky
V
V2EX
腾讯CDC
博客园_首页
博客园 - 司徒正美
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Tailwind CSS Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
小众软件
小众软件
J
Java Code Geeks
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog
雷峰网
雷峰网
Stack Overflow Blog
Stack Overflow Blog
IT之家
IT之家
罗磊的独立博客
Recorded Future
Recorded Future
博客园 - 聂微东
O
OpenAI News
S
Secure Thoughts
Hacker News: Ask HN
Hacker News: Ask HN
S
Schneier on Security
Hacker News - Newest:
Hacker News - Newest: "LLM"
Y
Y Combinator Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
Project Zero
Project Zero
宝玉的分享
宝玉的分享
K
Kaspersky official blog
N
Netflix TechBlog - Medium
T
The Exploit Database - CXSecurity.com
Google Online Security Blog
Google Online Security Blog
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Webroot Blog
Webroot Blog
云风的 BLOG
云风的 BLOG
Simon Willison's Weblog
Simon Willison's Weblog
C
Check Point Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 热门话题
美团技术团队
L
Lohrmann on Cybersecurity

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Распознавание лиц с потока камеры в .NET MAUI
nickkovalsky · 2026-04-24 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение10 мин

Охват и читатели368

Туториал

Как использовать элемент SkiaCamera для AI/ML локально и с API

В этой статье

Сегодняшние приложения для мобильных и настольных устройств умеют распознавать на изображениях почти что угодно, - от QR-кодов до количества калорий в еде на на фото. На платформах, которые поддерживает .NET MAUI, для этого можно использовать разные варианты, как локальные ML-движки вроде TensorFlow Lite, нативные SDK для конкретной платформы, типа ARKit на iOS, так и разные Vision API. Далее все зависит уже от реализации в приложении.

И вот, когда речь идет пойдет о распознавании изображений от камеры, наш вариант - пакет DrawnUi.Maui.Camera. В предыдущей статье я показывал, как использовать SkiaCamera для анализа аудио с AI в реальном времени, а сегодня займемся видео: разберем на примере распознавания лиц.

Приложение-пример, которое идет вместе с этой статьей, использует локальное распознавание лицевых точек с помощью MediaPipe Tasks. Я выбрал этот вариант ради максимально единообразного поведения на всех платформах: на iOS, Android и Windows.

А также наше приложение рисует оверлеи и приклеивает маски к движущимся лицам.

Важно: сегодня наша цель - показать как использовать живые видеокадры из SkiaCamera для AI/ML локально и через API в целом, не уходя глубоко в детали конкретного приложения.

Настройка

О том, как установить и инициализировать SkiaCamera, я писал в предыдущей статье. В данном примере мы используем XAML и размещаем унаследованный элемент внутри обычного лейаута .NET MAUI.

Для задач AI/ML нам нужно заставить элемент работать в режиме обработки поступающего видео-потока:

UseRealtimeVideoProcessing = true;

Точка подключения

Когда SkiaCamera показывает превью, кадры, которые вы видите на экране, находятся в GPU-памяти. Чтобы использовать их асинхронно для своих целей нам нужно вытащить кадр нужного размера в обычную память. Ключевой виртуальный метод: OnRawFrameAvailable(RawCameraFrame frame).

Приходящая структура RawCameraFrame содержит SKImage, живущий в GPU, а так же сопутствующие метаданные. Обычно для распознавания нам нужно уменьшить изображение, правильно его повернуть и в некоторых случаях еще чуток кропнуть, чтобы убрать лишние поля, которые не релевантны для распознавания. И все инструменты для этого в пакете у нас есть.

Для локальной ML модели

Структура RawCameraFrame предоставляет метод TryGetRgba(width, height, buffer, orientation, cropRatio), который заполняет заранее выделенный вами byte[] RGBA-пикселями в том финальном размере, который нужен вашей модели.

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

В приложении-примере используется cropRatio по умолчанию, то есть 1 без дополнительного зума (читай, обрезки полей), и orientation по умолчанию OutputOrientation.Display - в данном приложении нам не было важно, чтобы картинка была строго “головой вверх”; нам было важно получить ровно то, что пользователь видит на экране, даже если устройство повернуто в ландшафт.

Если для вашей модели важна ориентация “головой вверх”, то можно использовать OutputOrientation.Portrait. И, возможно, вам еще захочется подрезать кадр, например, убрать края, если нужный объект почти наверняка находится ближе к центру. Для этого можно уменьшить cropRatio. Например: 0.9 будет означать, что вы обрежете пропорцию 0.1 по краям кадра.

В нашем примере метод вызывается вообще с дефолтными значения, без явной передачи orientation, cropRatio):

	if (!frame.TryGetRgba(targetWidth, targetHeight, _mlFrameBuffers[writeBufferIndex]))
		return;

Даже для локального модели лучше всего будет пропускать кадры из видео-потока, пока детектор еще занят. Это касается не только лиц, но и QR-сканирования, OCR, разпознавания объектов, в общем любого сценария, где модель работает нон-стоп. Лучше пропустить часть кадров, чем превью камеры начнет лагать.

Вот пример для абстрактного ML-сценария: не блокируем поток камеры, с пропуском кадров, пока предыдущее распознавание еще идет в другом потоке:

private readonly byte[] _rgbaBuffer = new byte[targetWidth * targetHeight * 4];
private readonly SemaphoreSlim _detectorBusy = new(1, 1);

protected override void OnRawFrameAvailable(RawCameraFrame frame)
{
	if (!_detectorBusy.Wait(0))
		return;

	if (!frame.TryGetRgba(targetWidth, targetHeight, _rgbaBuffer, OutputOrientation.Portrait, 0.8f))
	{
		_detectorBusy.Release();
		return;
	}

	var snapshot = _rgbaBuffer.ToArray();

	_ = Task.Run(async () =>
	{
		try
		{
			await detector.EnqueueDetectionAsync(snapshot, request);
		}
		finally
		{
			_detectorBusy.Release();
		}
	});
}

Следующий пример более оптимизирован: вместо ToArray() используется переиспользуемый пул буферов, а работа уходит идет в параллельном потоке, которым управляет ваш детектор, без лишнего оборачивания в Task.Run:

private readonly byte[][] _mlBuffers =
[
	new byte[targetWidth * targetHeight * 4],
	new byte[targetWidth * targetHeight * 4]
];
private const float MlCropRatio = 1f;
private readonly object _detectionSync = new();
private int _activeBufferIndex = -1;
private DetectionWorkItem? _queuedDetectionWorkItem;

protected override void OnRawFrameAvailable(RawCameraFrame frame)
{
	DetectionWorkItem? workItemToSubmit = null;

	lock (_detectionSync)
	{
		int writeBufferIndex = _activeBufferIndex == 0 ? 1 : 0;

		if (!frame.TryGetRgba(targetWidth, targetHeight, _mlBuffers[writeBufferIndex], OutputOrientation.Portrait, MlCropRatio))
			return;

		var workItem = new DetectionWorkItem(
			writeBufferIndex,
			targetWidth,
			targetHeight,
			0);

		if (_activeBufferIndex >= 0)
		{
			_queuedDetectionWorkItem = workItem;
			return;
		}

		_activeBufferIndex = workItem.BufferIndex;
		workItemToSubmit = workItem;
	}

	detectionPipeline.Submit(workItemToSubmit);
}

Здесь фоновый поток принадлежит самому детектору. OnRawFrameAvailable(...) только подготавливает кадр, решает, надо ли его пропустить или поставить в очередь, и затем передает дальше. В коллбэке завершения позже освобождается активный буфер и, если нужно, отправляется самый свежий отложенный кадр. Поскольку в этом примере используется OutputOrientation.Display, буфер детектора уже выровнен относительно живого превью, и потом не нужно отдельно компенсировать поворот в координатах детектора.

Для AI API

В приложении используется локальный ML движок, но та же точка подключения подойдет и в случае, если вы хотите работать через API.

Обычно, по соображениям производительности не стоит пытаться отправлять каждый возможный кадр превью. Например, можно разрешать не более одной отправки раз в 300 мс и при этом не слать новый кадр, пока не завершился предыдущий запрос.

Для публичных LLM vision API обычно отправляют JPEG или PNG. Параметр cropRatio доступен и здесь:

private const int RemoteUploadIntervalMs = 300;
private long _lastUploadStartedAtMs;
private readonly SemaphoreSlim _uploadGate = new(1, 1);

protected override void OnRawFrameAvailable(RawCameraFrame frame)
{
	if (!_uploadGate.Wait(0))
		return;

	long nowMs = Environment.TickCount64;
	if (nowMs - _lastUploadStartedAtMs < RemoteUploadIntervalMs)
	{
		_uploadGate.Release();
		return;
	}

	if (!frame.TryGetJpeg(targetWidth, targetHeight, out var payload, 100, OutputOrientation.Portrait, 1f))
	{
		_uploadGate.Release();
		return;
	}

	_lastUploadStartedAtMs = nowMs;

	_ = Task.Run(async () =>
	{
		try
		{
			await apiClient.UploadImageAsync(payload, "image/jpeg");
		}
		finally
		{
			_uploadGate.Release();
		}
	});
}

Здесь аккуратнее всего работает SemaphoreSlim.Wait(0): он не блокирует коллбэк камеры, но при этом гарантирует, что одновременно в полете будет только одна отправка. Уже далее можно спокойно проверить минимальную паузу в 300 мс и обновить _lastUploadStartedAtMs. Если сетевой вызов занимает дольше 300 мс, то новые кадры будут просто пропускаться.

TryGetJpeg(...) и TryGetPng(...) возвращают изображение в том размере и с той ориентацией, которые вы запросили.

Если ваш ендпойнт принимает сырые данные RGBA8888, можно по-прежнему использовать TryGetRgbaBytes(...).

Отладка

Если нужно проверить, что вы реально отправляете в AI/ML, можно сохранить один кадр изображения в галерею устройства и посмотреть глазами. Простой способ убедиться, что с ориентацией, обрезкой все действительно так, как вы ожидаете. Не забудьте дать приложению доступ к галерее, см. README SkiaCamera, там всё описано.

Если приложение уже использует TryGetJpeg(...), можно сохранить ровно тот же самый JPEG:

private bool _saveNextDebugFrame; //установим в true когда надо сохранить текущий кадр в галерею

protected override void OnRawFrameAvailable(RawCameraFrame frame)
{
	if (_saveNextDebugFrame &&
		frame.TryGetJpeg(targetWidth, targetHeight, out var payload, 100, OutputOrientation.Portrait, 1f))
	{
		_saveNextDebugFrame = false;

		_ = Task.Run(async () =>
		{
			using var stream = new MemoryStream(payload);
			await NativeControl.SaveJpgStreamToGallery(
				stream,
				$"ml_debug_{DateTime.Now:yyyyMMdd_HHmmss}.jpg",
				new Metadata(),
				"DebugAlbum");
		});
	}

	// ...
}

Если же приложение использует TryGetRgbaBytes(...), то нужно закодировать полученный RGBA-буфер в JPEG и уже потом сохранить в галерею:

private bool _saveNextDebugFrame;

protected override void OnRawFrameAvailable(RawCameraFrame frame)
{
	if (_saveNextDebugFrame &&
		frame.TryGetRgbaBytes(targetWidth, targetHeight, out var rgbaBytes, OutputOrientation.Portrait, 1f))
	{
		_saveNextDebugFrame = false;

		_ = Task.Run(async () =>
		{
			var imageInfo = new SKImageInfo(
				targetWidth,
				targetHeight,
				SKColorType.Rgba8888,
				SKAlphaType.Unpremul);

			using var image = SKImage.FromPixelCopy(imageInfo, rgbaBytes, imageInfo.RowBytes);
			using var data = image.Encode(SKEncodedImageFormat.Jpeg, 100);
			using var stream = data.AsStream();

			await NativeControl.SaveJpgStreamToGallery(
				stream,
				$"ml_debug_rgba_{DateTime.Now:yyyyMMdd_HHmmss}.jpg",
				new Metadata(),
				"DebugAlbum");
		});
	}

	// ...
}

Чем меньше размер, который вы запрашиваете, тем быстрее пройдет операция GPU кадр -> CPU миниатюра.

Приложение-пример

Теперь, когда нам понятно, как получать изображения для AI/ML, читать исходники приложения будет проще. Я добавил и дополнительную документацию (на английском): Implementation.md, где разобрана архитектура, и Includes.md, где объясняется, как ML-модели зашиваются внутри ресурсов приложения для каждой платформы. Ибо всю нашу схему легко адаптировать и под другие MediaPipe Tasks: просто меняете модель и парсите другой результат. О том какие еще модели можно подключить, - чуть ниже.

Маска, наложенная на обнаруженные лицевые точки

Маска, наложенная на обнаруженные лицевые точки

Чтобы можно было рисовать маски-картинки, например маску Человека-паука или Смешную шляпу, мы используем конфигурации, которые задают позиционирование относительно найденного лица:

					config = ModePicker.SelectedIndex switch
					{
						3 => new MaskConfiguration
						{
							Filename = "hat_cake.png",
							Position = MaskPosition.Top,
							WidthMultiplier = 1.6f,
							YOffsetRatio = 0.05f
						},
						_ => new MaskConfiguration
						{
							Filename = "mask_spiderman.png",
							Position = MaskPosition.Inside,
							WidthMultiplier = 1.25f,
							YOffsetRatio = -0.2f
						}
					};

					await CameraControl.SetupMaskAsync(config);

Если захотите сделать свою маску, можно просто добавить новые конфиги поверх уже существующих.

Чтобы рисовать с максимальным фпс, мы держим текущий растр маски в текстуре на GPU:

   //грузим из ресурсов
   using var stream = await FileSystem.OpenAppPackageFileAsync(config.Filename);
   using var managed = new MemoryStream();
   await stream.CopyToAsync(managed);
   managed.Position = 0;

   MaskBitmap = SKBitmap.Decode(managed);
   
   //выполняем в GPU потоке: сохраняем в GPU текстуру
   SafeAction(() => //выполняется в конце отрисовки холста с помощью SkiaSharp
   {
	   using var gpu = this.CreateSurface(MaskBitmap.Width, MaskBitmap.Height, isGpu: true);
	   gpu.Canvas.Clear(SKColors.Transparent);
	   gpu.Canvas.DrawBitmap(MaskBitmap, 0, 0);
	   gpu.Canvas.Flush();
	   MaskImage = gpu.Snapshot();
   });

После этого мы можем рисовать MaskImage прямо в коллбэке ProcessFrame у SkiaCamera, с правильной проекцией поворота и позиции.

Тот же код рисования оверлея, который мы используем в ProcessFrame, работает у нас и при сохранении снятых фотографий. Фото может быть очень большим, например 4000x3000, и если рисовать найденные landmark-точки или маски со толщиной stroke, рассчитанной для маленького превью, примитивы SkiaSharp на таком размере будут почти не видны. Мы решаем это масштабированием толщины линии от безопасной базы в 300 пикселей:

var density = Math.Min(frame.Width, frame.Height) / 300f; 
_paintDetectionDotsStroke.StrokeWidth = Math.Max(2f, 2f * density); 
//рисуем лендмарки - лицевые точки
frame.Canvas.DrawPoints(SKPointMode.Points, pts, _paintDetectionDotsStroke);

Так маски и точки визуально сохраняют одинаковый масштаб и в живом превью, и на итоговой фотографии.

Чтобы перемещения маски в кадре при движении головы выглядело плавнее, мы используем One Euro фильтр. Он работает отдельно для каждой landmark-точки, по X и Y, поэтому на неподвижном лице хорошо убирает дрожание, а на движущемся уменьшает шаги перемещения. Дополнительный, шаг обработки prediction step (предсказание) экстраполирует положение по двум последним распознаваниям и помогает компенсировать задержку детектора, когда голова двигается быстро.

Что еще можно распознавать

Архитектура - MediaPipeTasksVision на мобильных платформах и MediaPipe.Net TFLite graphs на Windows - так же переносится и на другие задачи: достаточно заменить файл модели:

  • Hand landmarks (hand_landmarker.task) - 21 3D-точка суставов на каждую руку, отслеживание жестов

  • Pose landmarks (pose_landmarker.task) - 33 суставные точки тела, отслеживание движений (фитнес, 3D…)

  • Object detection (efficientdet.task) - определение объектов

  • Image segmentation (image_segmenter.task) - попиксельное разделение фон/бэкграунд (тот же механизм лежит в основе размытия фона в Zoom)

  • Image classification - классификация всего изображения

В приложении-примере мы уже решили множество сложностей реализации и смена модели в основном сводется к разбору другого формата результата.

Используемые пакеты

  • Windows: Mediapipe.Net и Mediapipe.Net.Runtime.CPU.

  • iOS: MediaPipeTasksVision.iOS из проекта MediaPipeTasks.

  • Android: AppoMobi.Preview.MediaPipeTasksVision.Android, мой форк MediaPipeTasksVision.Android с дополнительными методами для пакетного чтения landmark-точек, что уменьшает время обработки кадра примерно в 3 раза. PR уже отправлен в основной репозиторий, так что позже, возможно, получится вернуться к оригинальному NuGet-пакету из MediaPipeTasks.

Заключение

Отправлять кадры из живого превью камеры в локальную ML-модель или на API в .NET MAUI вполне реально и достаточно комфортно. Показатели производительности в строке состояния в приложении-примере помогут вам в настройке.

Надеюсь, что статья окажется для вас полезной. Если она поможет вам создать что-то интересное, пожалуйста, напишите. Вопросы тоже можно смело оставлять в комментариях!

Ссылки и ресурсы


Автор открыт для сотрудничества в создании мобильных приложений на .NET MAUI, и кастомных UI элементов.