WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

前言

之所以会搞这个手势识别分类,其实是为了满足之前群友提的需求,就是针对稚晖君的ElectronBot机器人的上位机软件的功能丰富,因为本来擅长的技术栈都是.NET。

也刚好试试全能的.NET是不是真的全能就想着做下试试了,MediaPipe作为谷歌开源的机器视觉库,功能很丰富了,而且也支持c++,翻遍社区果然找到了一个基于MediaPipe包装的C#版本,叫MediaPipe.NET,于是就开始整活了。.

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

所用框架介绍

1、WASDK

这个框架是微软最新的UI框架,我主要是用来开发程序的主体,做一些交互和功能的承载,本质上和wpf,uwp这类程序没什么太大的区别,区别就是一些工具链的不同。

2、MediaPipe

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

我主要使用MediaPipe进行手部的检测和手部关键点坐标的提取,因为MediaPipe只能达到这种程度,对于手势的分类什么的需要我们自己处理计算数据,但是这样也有好处,就是我们可以做出自己想要的手势。

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

3. ML.NET

开放源代码的跨平台机器学习框架

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

既然是个机器学习框架,那我们肯定可以通过框架提供的功能进行一些数据的处理学习。ML.NET包含的一些功能如下:

  • 分类/类别划分 自动将客户反馈分为积极和消极两类
  • 回归/预测连续值 根据面积和地段预测房价
  • 异常检测 检测欺诈性的银行交易
  • 建议 根据网购者以前的购买情况,推荐他们可能想购买的产品
  • 时序/顺序数据 预测天气/产品销售额
  • 图像分类 对医学影像中的病状进行分类
  • 文本分类 根据文档内容对文档进行分类
  • 句子相似性 测量两个句子的相似程度

我在使用MediaPipe进行手部关键点检测之后,就获取了手部关键点的坐标数据,可以通过坐标数据整理成表格保存下来,然后通过ML.NET进行数据分析,主要使用文本分类功能。

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

整体的思路,MediaPipe检测是是手部关键点的坐标,即我们的手部保持一个动作的话,坐标点之间的相对关系肯定差别不大,当我们的某个手势的数据量足够的多,那我们就可以通过ML.NET得到一个手势的数据规则,当我们通过数据进行分类的时候就能够匹配到最接近的手势了。

目标我通过ML.NET训练的手势如下图:

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

手势的数据也上传到仓库了,大家可以进行查看详细的在代码讲解的地方进行介绍。

主要得到启发的项目是下面的仓库,大家可以自行学习。

DJI Tello Hand Gesture control

代码讲解(干货篇)

1、项目介绍

项目地址:https://github.com/GreenShadeZhang/WinUI-Tutorial-Code

项目结构如下图:

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

注意由于MSIX打包的WASDK的路径访问为虚拟文件系统所以我们需要在项目里加入VFS目录,将引用的mediapipe的模块和dll放进去,不然会导致代码无法使用。

详情见如下文档:打包的 VFS 位置:https://learn.microsoft.com/zh-cn/windows/msix/desktop/desktop-to-uwp-behind-the-scenes#packaged-vfs-locations

软件处理过程如下:

WinUI(WASDK)项目调用摄像头

=>OpencvSharp处理帧数据

=>转换成ImageFrame

=>MediaPipe处理返回手部关键点数据

=>ML.NET项目分析关键点手势分类

=>返回手势标签

=>软件进行业务处理

由于WASDK的摄像头帧处理事件有点问题,所以我只能先用本地图片做演示了。

2、核心代码讲解

初始化的代码如下图:

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

核心代码如下:

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了,没有它就没有我的这篇文章,更没有我的项目了。

WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类

个人感悟

又到了个人感悟环节,在最近测试的环节里,发现WASDK还是要有很长一段路要走,开发体验和UWP差太大了,但是好处是它比UWP的自由度高了很多,也可以使用.NET的新特性,和一些轮子,就很舒服。

再者随着.NET社区越来越好,很多好用的轮子就会越来越多了,社区大家记得多多贡献了。

参考推荐文档如下

  • demo地址:https://github.com/GreenShadeZhang/WinUI-Tutorial-Code

  • WASDK文档地址:https://learn.microsoft.com/zh-cn/windows/apps/desktop/

  • MediaPipe:https://google.github.io/mediapipe/

  • MediaPipe.NET:https://github.com/vignetteapp/MediaPipe.NET

  • ML.NET:https://dotnet.microsoft.com/zh-cn/apps/machinelearning-ai/ml-dotnet

  • hand-gesture-recognition-using-mediapipe:https://github.com/Kazuhito00/hand-gesture-recognition-using-mediapipe

  • Control DJI Tello drone with Hand gestures:https://towardsdatascience.com/control-dji-tello-drone-with-hand-gestures-b76bd1d4644f