前言 之所以會搞這個手勢識別分類,其實是為了滿足之前群友提的需求,就是針對稚暉君的ElectronBot機器人的上位機軟體的功能豐富,因為本來擅長的技術棧都是.NET,也剛好試試全能的.NET是不是真的全能就想著做下試試了,MediaPipe作為谷歌開源的機器視覺庫,功能很豐富了,而且也支持c++, ...
前言
之所以會搞這個手勢識別分類,其實是為了滿足之前群友提的需求,就是針對稚暉君的ElectronBot機器人的上位機軟體的功能豐富,因為本來擅長的技術棧都是.NET,也剛好試試全能的.NET是不是真的全能就想著做下試試了,MediaPipe作為谷歌開源的機器視覺庫,功能很豐富了,而且也支持c++,翻遍社區果然找到了一個基於MediaPipe包裝的C#版本,叫MediaPipe.NET,於是就開始整活了。
所用框架介紹
1. WASDK
這個框架是微軟最新的UI框架,我主要是用來開發程式的主體,做一些交互和功能的承載,本質上和wpf,uwp這類程式沒什麼太大的區別,區別就是一些工具鏈的不同。
2. MediaPipe
MediaPipe offers open source cross-platform, customizable ML solutions for live and streaming media.
我主要使用MediaPipe進行手部的檢測和手部關鍵點坐標的提取,因為MediaPipe只能達到這種程度,對於手勢的分類什麼的需要我們自己處理計算數據,但是這樣也有好處,就是我們可以做出自己想要的手勢。
3. ML.NET
開放源代碼的跨平臺機器學習框架
既然是個機器學習框架,那我們肯定可以通過框架提供的功能進行一些數據的處理學習。
ML.NET包含的一些功能如下:
- 分類/類別劃分 自動將客戶反饋分為積極和消極兩類
- 回歸/預測連續值 根據面積和地段預測房價
- 異常檢測 檢測欺詐性的銀行交易
- 建議 根據網購者以前的購買情況,推薦他們可能想購買的產品
- 時序/順序數據 預測天氣/產品銷售額
- 圖像分類 對醫學影像中的病狀進行分類
- 文本分類 根據文檔內容對文檔進行分類
- 句子相似性 測量兩個句子的相似程度
我在使用MediaPipe進行手部關鍵點檢測之後,就獲取了手部關鍵點的坐標數據,可以通過坐標數據整理成表格保存下來,然後通過ML.NET進行數據分析,主要使用文本分類功能。
整體的思路,MediaPipe檢測是是手部關鍵點的坐標,即我們的手部保持一個動作的話,坐標點之間的相對關係肯定差別不大,當我們的某個手勢的數據量足夠的多,那我們就可以通過ML.NET得到一個手勢的數據規則,當我們通過數據進行分類的時候就能夠匹配到最接近的手勢了。
目標我通過ML.NET訓練的手勢如下圖:
手勢的數據也上傳到倉庫了,大家可以進行查看詳細的在代碼講解的地方進行介紹。
主要得到啟發的項目是下麵的倉庫,大家可以自行學習。
DJI Tello Hand Gesture control
代碼講解(乾貨篇)
1. 項目介紹
項目結構如下圖:
註意由於MSIX打包的WASDK的路徑訪問為虛擬文件系統所以我們需要在項目裡加入VFS目錄,將引用的mediapipe的模塊和dll放進去,不然會導致代碼無法使用。
詳情見如下文檔:
打包的 VFS 位置
軟體處理過程如下:
WinUI(WASDK)項目調用攝像頭
=>OpencvSharp處理幀數據
=>轉換成ImageFrame
=>MediaPipe處理返回手部關鍵點數據
=>ML.NET項目分析關鍵點手勢分類
=>返回手勢標簽
=>軟體進行業務處理
由於WASDK的攝像頭幀處理事件有點問題,所以我只能先用本地圖片做演示了。
2.核心代碼講解
初始化的代碼如下圖:
核心代碼如下:
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了,沒有它就沒有我的這篇文章,更沒有我的項目了。
個人感悟
又到了個人感悟環節,在最近測試的環節里,發現WASDK還是要有很長一段路要走,開發體驗和UWP差太大了,但是好處是它比UWP的自由度高了很多,也可以使用.NET的新特性,和一些輪子,就很舒服。
再者隨著.NET社區越來越好,很多好用的輪子就會越來越多了,社區大家記得多多貢獻了。
參考推薦文檔如下
hand-gesture-recognition-using-mediapipe
Control DJI Tello drone with Hand gestures