WinUI(WASDK)使用MediaPipe檢查手部關鍵點並通過ML.NET進行手勢分類

来源:https://www.cnblogs.com/GreenShade/archive/2022/12/09/16967870.html
-Advertisement-
Play Games

前言 之所以會搞這個手勢識別分類,其實是為了滿足之前群友提的需求,就是針對稚暉君的ElectronBot機器人的上位機軟體的功能豐富,因為本來擅長的技術棧都是.NET,也剛好試試全能的.NET是不是真的全能就想著做下試試了,MediaPipe作為谷歌開源的機器視覺庫,功能很豐富了,而且也支持c++, ...


前言

之所以會搞這個手勢識別分類,其實是為了滿足之前群友提的需求,就是針對稚暉君的ElectronBot機器人的上位機軟體的功能豐富,因為本來擅長的技術棧都是.NET,也剛好試試全能的.NET是不是真的全能就想著做下試試了,MediaPipe作為谷歌開源的機器視覺庫,功能很豐富了,而且也支持c++,翻遍社區果然找到了一個基於MediaPipe包裝的C#版本,叫MediaPipe.NET,於是就開始整活了。

hand

所用框架介紹

1. WASDK

這個框架是微軟最新的UI框架,我主要是用來開發程式的主體,做一些交互和功能的承載,本質上和wpf,uwp這類程式沒什麼太大的區別,區別就是一些工具鏈的不同。

2. MediaPipe

MediaPipe offers open source cross-platform, customizable ML solutions for live and streaming media.

我主要使用MediaPipe進行手部的檢測和手部關鍵點坐標的提取,因為MediaPipe只能達到這種程度,對於手勢的分類什麼的需要我們自己處理計算數據,但是這樣也有好處,就是我們可以做出自己想要的手勢。

mediapipe

3. ML.NET

開放源代碼的跨平臺機器學習框架

ml

既然是個機器學習框架,那我們肯定可以通過框架提供的功能進行一些數據的處理學習。
ML.NET包含的一些功能如下:

  • 分類/類別劃分 自動將客戶反饋分為積極和消極兩類
  • 回歸/預測連續值 根據面積和地段預測房價
  • 異常檢測 檢測欺詐性的銀行交易
  • 建議 根據網購者以前的購買情況,推薦他們可能想購買的產品
  • 時序/順序數據 預測天氣/產品銷售額
  • 圖像分類 對醫學影像中的病狀進行分類
  • 文本分類 根據文檔內容對文檔進行分類
  • 句子相似性 測量兩個句子的相似程度

我在使用MediaPipe進行手部關鍵點檢測之後,就獲取了手部關鍵點的坐標數據,可以通過坐標數據整理成表格保存下來,然後通過ML.NET進行數據分析,主要使用文本分類功能。

mldata

整體的思路,MediaPipe檢測是是手部關鍵點的坐標,即我們的手部保持一個動作的話,坐標點之間的相對關係肯定差別不大,當我們的某個手勢的數據量足夠的多,那我們就可以通過ML.NET得到一個手勢的數據規則,當我們通過數據進行分類的時候就能夠匹配到最接近的手勢了。

目標我通過ML.NET訓練的手勢如下圖:

gesture

手勢的數據也上傳到倉庫了,大家可以進行查看詳細的在代碼講解的地方進行介紹。

主要得到啟發的項目是下麵的倉庫,大家可以自行學習。

DJI Tello Hand Gesture control

代碼講解(乾貨篇)

1. 項目介紹

項目地址

項目結構如下圖:

demo

註意由於MSIX打包的WASDK的路徑訪問為虛擬文件系統所以我們需要在項目裡加入VFS目錄,將引用的mediapipe的模塊和dll放進去,不然會導致代碼無法使用。

詳情見如下文檔:
打包的 VFS 位置

軟體處理過程如下:

WinUI(WASDK)項目調用攝像頭

=>OpencvSharp處理幀數據

=>轉換成ImageFrame

=>MediaPipe處理返回手部關鍵點數據

=>ML.NET項目分析關鍵點手勢分類

=>返回手勢標簽

=>軟體進行業務處理

由於WASDK的攝像頭幀處理事件有點問題,所以我只能先用本地圖片做演示了。

2.核心代碼講解

初始化的代碼如下圖:

code

核心代碼如下:

 private async void CameraHelper_FrameArrived(object sender, CommunityToolkit.WinUI.Helpers.FrameEventArgs e)
    {
        try
        {
            // Gets the current video frame
            VideoFrame currentVideoFrame = e.VideoFrame;

            // Gets the software bitmap image
            SoftwareBitmap softwareBitmap = currentVideoFrame.SoftwareBitmap;

            if (softwareBitmap != null)
            {
                //if (softwareBitmap.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
                // softwareBitmap.BitmapAlphaMode == BitmapAlphaMode.Straight)
                //{
                //    softwareBitmap = SoftwareBitmap.Convert(
                //        softwareBitmap, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
                //}

                //using IRandomAccessStream stream = new InMemoryRandomAccessStream();

                //var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);

                //// Set the software bitmap
                //encoder.SetSoftwareBitmap(softwareBitmap);

                //await encoder.FlushAsync();

                //var image = new Bitmap(stream.AsStream());

                //var matData = OpenCvSharp.Extensions.BitmapConverter.ToMat(image);

                var matData = new OpenCvSharp.Mat(Package.Current.InstalledLocation.Path + $"\\Assets\\hand.png");

                var mat2 = matData.CvtColor(OpenCvSharp.ColorConversionCodes.BGR2RGB);

                var dataMeta = mat2.Data;

                var length = mat2.Width * mat2.Height * mat2.Channels();

                var data = new byte[length];

                Marshal.Copy(dataMeta, data, 0, length);

                var widthStep = (int)mat2.Step();

                var imgframe = new ImageFrame(ImageFormat.Types.Format.Srgb, mat2.Width, mat2.Height, widthStep, data);

                var handsOutput = calculator.Compute(imgframe);

                Bitmap bitmap = BitmapConverter.ToBitmap(matData);

                var ret = await BitmapToBitmapImage(bitmap);

                if (ret.BitmapPixelFormat != BitmapPixelFormat.Bgra8 ||
                        ret.BitmapAlphaMode == BitmapAlphaMode.Straight)
                {
                    ret = SoftwareBitmap.Convert(ret, BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
                }

                if (handsOutput.MultiHandLandmarks != null)
                {
                    var landmarks = handsOutput.MultiHandLandmarks[0].Landmark;

                    Debug.WriteLine($"Got hands output with {landmarks.Count} landmarks" + $" at frame {frameCount}");

                    var result = HandDataFormatHelper.PredictResult(landmarks.ToList(), modelPath);


                    this.DispatcherQueue.TryEnqueue(async() =>
                    {
                        var source = new SoftwareBitmapSource();

                        await source.SetBitmapAsync(ret);


                        HandResult.Text = result;
                        VideoFrame.Source = source;
                    });
                }
                else
                {
                    Debug.WriteLine("No hand landmarks");
                }
            }
        }
        catch (Exception ex)
        {

        }
        frameCount++;
    }

主要註意的點是圖片格式的轉換,opencv載入出來的格式轉換成RGB的時候要看下是BGR2RGB還是BGRA2RGBA。

如果不確定的話,可以使用源碼里採用FFmpeg封裝的demo代碼進行使用,那個包含了攝像頭幀讀取,和數據轉換。

核心代碼如下:

   private static async void onFrameEventHandler(object? sender, FrameEventArgs e)
        {
            if (calculator == null)
                return;

            Frame frame = e.Frame;
            if (frame.Width == 0 || frame.Height == 0)
                return;

            converter ??= new FrameConverter(frame, PixelFormat.Rgba);
            Frame cFrame = converter.Convert(frame);

            ImageFrame imgframe = new ImageFrame(ImageFormat.Types.Format.Srgba,
                cFrame.Width, cFrame.Height, cFrame.WidthStep, cFrame.RawData);

            HandsOutput handsOutput = calculator.Compute(imgframe);

            if (handsOutput.MultiHandLandmarks != null)
            {
                var landmarks = handsOutput.MultiHandLandmarks[0].Landmark;
                Console.WriteLine($"Got hands output with {landmarks.Count} landmarks"
                    + $" at frame {frameCount}");

                //await HandDataFormatHelper.SaveDataToTextAsync(landmarks.ToList());

                HandDataFormatHelper.PredictResult(landmarks.ToList());
                //Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(landmarks));
            }
            else
            {
                Console.WriteLine("No hand landmarks");
            }

            frameCount++;
        }

特別感謝的項目就是這個MediaPipe.NET了,沒有它就沒有我的這篇文章,更沒有我的項目了。

tsethand

個人感悟

又到了個人感悟環節,在最近測試的環節里,發現WASDK還是要有很長一段路要走,開發體驗和UWP差太大了,但是好處是它比UWP的自由度高了很多,也可以使用.NET的新特性,和一些輪子,就很舒服。

再者隨著.NET社區越來越好,很多好用的輪子就會越來越多了,社區大家記得多多貢獻了。

參考推薦文檔如下

demo地址

WASDK文檔地址

MediaPipe

MediaPipe.NET

ML.NET

hand-gesture-recognition-using-mediapipe

Control DJI Tello drone with Hand gestures


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. time模塊 import time *一*#時間戳--》結構化時間--》格式化的字元串時間 res1=time.localtime(654126574) print(res1 ) #res1 time.struct_time(tm_year=1990, tm_mon=9, tm_mday=2 ...
  • 原文:【開源庫推薦】 #4 Poi-辦公文檔處理庫 - Stars-One的雜貨小窩 github倉庫apache/poi Apache POI是Apache軟體基金會的開放源碼函式庫,POI提供API給Java程式對Microsoft Office格式檔案讀和寫的功能。.NET的開發人員則可以利用 ...
  • 1. 過期 key 處理 Redis 之所以性能強,最主要的原因就是基於記憶體存儲。然而單節點的 Redis 其記憶體大小不宜過大,會影響持久化或主從同步性能。 我們可以通過修改配置文件來設置 Redis 的最大記憶體: maxmemory 1gb 當記憶體使用達到上限時,就無法存儲更多數據了。為瞭解決這個 ...
  • laravel-route-notes laravel框架擴展,原生註解生成路由 優點是直接生成路由文件,不在運行中解析路由,提升效率 使用環境 [PHP] >= 8.0 [Laravel] >= 9.0 如何安裝 直接使用composer進行安裝: composer require --dev l ...
  • JZ36 二叉搜索樹與雙向鏈表 描述 輸入一棵二叉搜索樹,將該二叉搜索樹轉換成一個排序的雙向鏈表 註意: 1.要求不能創建任何新的結點,只能調整樹中結點指針的指向。當轉化完成以後,樹中節點的左指針需要指向前驅,樹中節點的右指針需要指向後繼 2.返回鏈表中的第一個節點的指針 3.函數返回的TreeNo ...
  • 類模版std::function是一種通用、多態的函數封裝。std::function的實例可以對任何可以調用的目標實體進行存儲、複製、和調用操作,這些目標實體包括普通函數、Lambda表達式、函數指針、以及其它函數對象等。std::function對象是對C++中現有的可調用實體的一種類型安全的包 ...
  • Listener記憶體馬 0x01Lintener機制分析 Java Web 開發中的監聽器(Listener)就是 Application、Session 和 Request 三大對象創建、銷毀或者往其中添加、修改、刪除屬性時自動執行代碼的功能組件。 Listener 三個域對象 ServletCo ...
  • 就像黑火藥時代里突然誕生的核彈一樣,OpenAI的ChatGPT語言模型的橫空出世,是人工智慧技術發展史上的一個重要里程碑。這是一款無與倫比、超凡絕倫的模型,能夠進行自然語言推理和對話,並且具有出色的語言生成能力。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...