草庐IT

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

绿荫阿广 2023-03-28 原文

前言

之所以会搞这个手势识别分类,其实是为了满足之前群友提的需求,就是针对稚晖君的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社区越来越好,很多好用的轮子就会越来越多了,社区大家记得多多贡献了。

参考推荐文档如下

demo地址

WASDK文档地址

MediaPipe

MediaPipe.NET

ML.NET

hand-gesture-recognition-using-mediapipe

Control DJI Tello drone with Hand gestures

有关WinUI(WASDK)使用MediaPipe检查手部关键点并通过ML.NET进行手势分类的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 使用 RubyZip 生成 ZIP 文件时设置压缩级别 - 2

    我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看ruby​​zip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  5. ruby - 在 Ruby 中使用匿名模块 - 2

    假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于

  6. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  7. ruby - 使用 ruby​​ 和 savon 的 SOAP 服务 - 2

    我正在尝试使用ruby​​和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我

  8. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  9. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  10. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

随机推荐