【问题标题】:How to use the ARCore camera image in OpenCV in an Unity Android app?如何在 Unity Android 应用程序中使用 OpenCV 中的 ARCore 相机图像?
【发布时间】:2019-08-24 23:24:21
【问题描述】:

我正在尝试在我的 Unity ARCore 游戏中使用 OpenCV 进行手势识别。但是,随着 TextureReaderAPI 的弃用,从相机捕获图像的唯一方法是使用 Frame.CameraImage.AcquireCameraImageBytes()。问题不仅在于图像的分辨率为 640x480(AFAIK 无法更改),而且它也是 YUV_420_888 格式。 好像这还不够,OpenCV 没有免费的 C#/Unity 包,所以如果我不想为付费包兑现 20 美元,我需要使用可用的 C++ 或 python 版本。如何将 YUV 图像移动到 OpenCV,将其转换为 RGB(或 HSV)颜色空间,然后对其进行一些处理或将其返回 Unity?

【问题讨论】:

标签: c# android c++ opencv unity3d


【解决方案1】:

看来你已经解决了这个问题。

但对于任何想要将 AR 与手势识别和跟踪相结合的人,请尝试 Manom​​otion:https://www.manomotion.com/

免费的 SDK 并在 2020 年 12 月工作。

使用 SDK 社区版并下载 ARFoundation 版本

【讨论】:

    【解决方案2】:

    这是一个仅使用免费插件 OpenCV Plus Unity 的实现。如果您熟悉 OpenCV,则设置非常简单,文档也很棒。

    此实现使用 OpenCV 正确旋转图像,将它们存储到内存中,并在退出应用程序时将它们保存到文件中。我试图从代码中剥离所有 Unity 方面,以便函数 GetCameraImage() 可以在单独的线程上运行。

    我可以确认它可以在 Andoird (GS7) 上运行,我想它会非常普遍。

            using System;
            using System.Collections.Generic;
            using GoogleARCore;
            using UnityEngine;
            using OpenCvSharp;
            using System.Runtime.InteropServices;
    
            public class CamImage : MonoBehaviour
            {
    
                public static List<Mat> AllData = new List<Mat>();
    
                public static void GetCameraImage()
                {
    
                    // Use using to make sure that C# disposes of the CameraImageBytes afterwards
                    using (CameraImageBytes camBytes = Frame.CameraImage.AcquireCameraImageBytes())
                    {
    
                        // If acquiring failed, return null
                        if (!camBytes.IsAvailable)
                        {
                            return;
                        }
    
                        // To save a YUV_420_888 image, you need 1.5*pixelCount bytes.
                        // I will explain later, why.
                        byte[] YUVimage = new byte[(int)(camBytes.Width * camBytes.Height * 1.5f)];
    
                        // As CameraImageBytes keep the Y, U and V data in three separate
                        // arrays, we need to put them in a single array. This is done using
                        // native pointers, which are considered unsafe in C#.
                        unsafe
                        {
                            for (int i = 0; i < camBytes.Width * camBytes.Height; i++)
                            {
                                YUVimage[i] = *((byte*)camBytes.Y.ToPointer() + (i * sizeof(byte)));
                            }
    
                            for (int i = 0; i < camBytes.Width * camBytes.Height / 4; i++)
                            {
                                YUVimage[(camBytes.Width * camBytes.Height) + 2 * i] = *((byte*)camBytes.U.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                                YUVimage[(camBytes.Width * camBytes.Height) + 2 * i + 1] = *((byte*)camBytes.V.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                            }
                        }
    
                        // GCHandles help us "pin" the arrays in the memory, so that we can
                        // pass them to the C++ code.
                        GCHandle pinnedArray = GCHandle.Alloc(YUVimage, GCHandleType.Pinned);
    
                        IntPtr pointerYUV = pinnedArray.AddrOfPinnedObject();
    
                        Mat input = new Mat(camBytes.Height + camBytes.Height / 2, camBytes.Width, MatType.CV_8UC1, pointerYUV);
                        Mat output = new Mat(camBytes.Height, camBytes.Width, MatType.CV_8UC3);
    
                        Cv2.CvtColor(input, output, ColorConversionCodes.YUV2BGR_NV12);// YUV2RGB_NV12);
    
                        // FLIP AND TRANPOSE TO VERTICAL
                        Cv2.Transpose(output, output);
                        Cv2.Flip(output, output, FlipMode.Y);
    
                       AllData.Add(output);
                       pinnedArray.Free();
                    }
    
                }
            }
    

    然后我在退出程序时调用 ExportImages() 以保存到文件。

        private void ExportImages()
        {
            /// Write Camera intrinsics to text file
            var path = Application.persistentDataPath;
            StreamWriter sr = new StreamWriter(path + @"/intrinsics.txt");
            sr.WriteLine(CameraIntrinsicsOutput.text);
            Debug.Log(CameraIntrinsicsOutput.text);
            sr.Close();
            // Loop through Mat List, Add to Texture and Save.
            for (var i = 0; i < CamImage.AllData.Count; i++)
            {
                Mat imOut = CamImage.AllData[i];
                Texture2D result = Unity.MatToTexture(imOut);
                result.Apply();
    
                byte[] im = result.EncodeToJPG(100);
                string fileName = "/IMG" + i + ".jpg";
                File.WriteAllBytes(path + fileName, im);
                string messge = "Succesfully Saved Image To " + path + "\n";
                Debug.Log(messge);
                Destroy(result);
            }
        }
    

    【讨论】:

      【解决方案3】:

      对于所有想要使用 OpencvForUnity 进行尝试的人:

      public Mat getCameraImage()
      {
          // Use using to make sure that C# disposes of the CameraImageBytes afterwards
          using (CameraImageBytes camBytes = Frame.CameraImage.AcquireCameraImageBytes())
          {
      
              // If acquiring failed, return null
              if (!camBytes.IsAvailable)
              {
                  Debug.LogWarning("camBytes not available");
                  return null;
              }
      
              // To save a YUV_420_888 image, you need 1.5*pixelCount bytes.
              // I will explain later, why.
              byte[] YUVimage = new byte[(int)(camBytes.Width * camBytes.Height * 1.5f)];
      
              // As CameraImageBytes keep the Y, U and V data in three separate
              // arrays, we need to put them in a single array. This is done using
              // native pointers, which are considered unsafe in C#.
              unsafe
              {
                  for (int i = 0; i < camBytes.Width * camBytes.Height; i++)
                  {
                      YUVimage[i] = *((byte*)camBytes.Y.ToPointer() + (i * sizeof(byte)));
                  }
      
                  for (int i = 0; i < camBytes.Width * camBytes.Height / 4; i++)
                  {
                      YUVimage[(camBytes.Width * camBytes.Height) + 2 * i] = *((byte*)camBytes.U.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                      YUVimage[(camBytes.Width * camBytes.Height) + 2 * i + 1] = *((byte*)camBytes.V.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                  }
              }
      
              // Create the output byte array. RGB is three channels, therefore
              // we need 3 times the pixel count
              byte[] RGBimage = new byte[camBytes.Width * camBytes.Height * 3];
      
              // GCHandles help us "pin" the arrays in the memory, so that we can
              // pass them to the C++ code.
              GCHandle pinnedArray = GCHandle.Alloc(YUVimage, GCHandleType.Pinned);
              IntPtr pointer = pinnedArray.AddrOfPinnedObject();
      
      
              Mat input = new Mat(camBytes.Height + camBytes.Height / 2, camBytes.Width, CvType.CV_8UC1);
              Mat output = new Mat(camBytes.Height, camBytes.Width, CvType.CV_8UC3);
      
              Utils.copyToMat(pointer, input);
      
              Imgproc.cvtColor(input, output, Imgproc.COLOR_YUV2RGB_NV12);
      
              pinnedArray.Free();
      
              return output;
          }
      }
      

      【讨论】:

        【解决方案4】:

        我想出了如何在 Arcore 1.8 中获得全分辨率 CPU 图像。

        我现在可以使用 cameraimagebytes 获得完整的相机分辨率。

        把它放在你的类变量中:

        private ARCoreSession.OnChooseCameraConfigurationDelegate m_OnChoseCameraConfiguration = null;
        

        把它放在 Start() 中

        m_OnChoseCameraConfiguration = _ChooseCameraConfiguration; ARSessionManager.RegisterChooseCameraConfigurationCallback(m_OnChoseCameraConfiguration); ARSessionManager.enabled = false; ARSessionManager.enabled = true;
        

        将此回调添加到类中:

        private int _ChooseCameraConfiguration(List<CameraConfig> supportedConfigurations) { return supportedConfigurations.Count - 1; }
        

        添加后,您应该让 cameraimagebytes 返回相机的完整分辨率。

        【讨论】:

          【解决方案5】:

          在此示例中,我将使用 C++ OpenCV 库和 Visual Studio 2017,我将尝试捕获 ARCore 相机图像,将其移至 OpenCV(尽可能高效),将其转换为 RGB 颜色空间,然后将其移回Unity C# 代码并保存在手机内存中。

          首先,我们必须创建一个 C++ 动态库项目以与 OpenCV 一起使用。为此,我强烈建议您关注 both Pierre Baret 和 Ninjaman494 对此问题的回答:OpenCV + Android + Unity。该过程相当简单,如果您不会过多偏离他们的答案(即您可以安全地下载比 3.3.1 更新的 OpenCV 版本,但在编译为 ARM64 而不是 ARM 等时要小心),您应该能够从 C# 调用 C++ 函数。

          根据我的经验,我必须解决两个问题 - 首先,如果您将项目作为 C# 解决方案的一部分而不是创建新的解决方案,Visual Studio 将不断弄乱您的配置,例如尝试编译 x86 版本ARM 版本。为了省去麻烦,请创建一个完全独立的解决方案。另一个问题是某些函数无法为我链接,从而引发未定义的引用链接器错误(确切地说是undefined reference to 'cv::error(int, std::string const&amp;, char const*, char const*, int)。如果发生这种情况并且问题出在您并不真正需要的函数上,只需在代码中重新创建该函数 - 例如,如果您对 cv::error 有问题,请将此代码添加到 .cpp 文件的末尾:

          namespace cv {
              __noreturn void error(int a, const String & b, const char * c, const char * d, int e) {
                  throw std::string(b);
              }
          }
          

          当然,这是一种丑陋而肮脏的做事方式,所以如果您知道如何修复链接器错误,请这样做并告诉我。

          现在,您应该有一个可以从 Unity Android 应用程序编译并运行的工作 C++ 代码。但是,我们想要的是 OpenCV 不返回数字,而是转换图像。所以把你的代码改成这样:

          .h 文件

          extern "C" {
              namespace YOUR_OWN_NAMESPACE
              {
                  int ConvertYUV2RGBA(unsigned char *, unsigned char *, int, int);
              }
          }
          

          .cpp 文件

          extern "C" {
              int YOUR_OWN_NAMESPACE::ConvertYUV2RGBA(unsigned char * inputPtr, unsigned char * outputPtr, int width, int height) {
          
                  // Create Mat objects for the YUV and RGB images. For YUV, we need a 
                  // height*1.5 x width image, that has one 8-bit channel. We can also tell
                  // OpenCV to have this Mat object "encapsulate" an existing array, 
                  // which is inputPtr.
                  // For RGB image, we need a height x width image, that has three 8-bit
                  // channels. Again, we tell OpenCV to encapsulate the outputPtr array.
                  // Thanks to specifying existing arrays as data sources, no copying
                  // or memory allocation has to be done, and the process is highly
                  // effective.
                  cv::Mat input_image(height + height / 2, width, CV_8UC1, inputPtr);
                  cv::Mat output_image(height, width, CV_8UC3, outputPtr);
          
                  // If any of the images has not loaded, return 1 to signal an error.
                  if (input_image.empty() || output_image.empty()) {
                      return 1;
                  }
          
                  // Convert the image. Now you might have seen people telling you to use 
                  // NV21 or 420sp instead of NV12, and BGR instead of RGB. I do not
                  // understand why, but this was the correct conversion for me. 
                  // If you have any problems with the color in the output image,
                  // they are probably caused by incorrect conversion. In that case,
                  // I can only recommend you the trial and error method.
                  cv::cvtColor(input_image, output_image, cv::COLOR_YUV2RGB_NV12);
          
                  // Now that the result is safely saved in outputPtr, we can return 0.
                  return 0;
              }
          }
          

          现在,重建解决方案 (Ctrl + Shift + B) 并将 libProjectName.so 文件复制到 Unity 的 Plugins/Android 文件夹,如链接答案中所示。

          接下来是从 ARCore 中保存图像,将其移动到 C++ 代码中,然后将其取回。让我们在 C# 脚本中添加这个

          [DllImport("YOUR_OWN_NAMESPACE")]
              public static extern int ConvertYUV2RGBA(IntPtr input, IntPtr output, int width, int height);
          

          Visual Studio 将提示您添加 System.Runtime.InteropServices using 子句 - 这样做。 这允许我们在 C# 代码中使用 C++ 函数。现在,让我们将这个函数添加到我们的 C# 组件中:

          public Texture2D CameraToTexture()
              {
                  // Create the object for the result - this has to be done before the 
                  // using {} clause.
                  Texture2D result;
          
                  // Use using to make sure that C# disposes of the CameraImageBytes afterwards
                  using (CameraImageBytes camBytes = Frame.CameraImage.AcquireCameraImageBytes())
                  {
          
                      // If acquiring failed, return null
                      if (!camBytes.IsAvailable)
                      {
                          Debug.LogWarning("camBytes not available");
                          return null;
                      }
          
                      // To save a YUV_420_888 image, you need 1.5*pixelCount bytes.
                      // I will explain later, why.
          
                      byte[] YUVimage = new byte[(int)(camBytes.Width * camBytes.Height * 1.5f)];
          
                      // As CameraImageBytes keep the Y, U and V data in three separate
                      // arrays, we need to put them in a single array. This is done using
                      // native pointers, which are considered unsafe in C#.
                      unsafe
                      {
                          for (int i = 0; i < camBytes.Width * camBytes.Height; i++)
                          {
                              YUVimage[i] = *((byte*)camBytes.Y.ToPointer() + (i * sizeof(byte)));
                          }
          
                          for (int i = 0; i < camBytes.Width * camBytes.Height / 4; i++)
                          {
                              YUVimage[(camBytes.Width * camBytes.Height) + 2 * i] = *((byte*)camBytes.U.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                              YUVimage[(camBytes.Width * camBytes.Height) + 2 * i + 1] = *((byte*)camBytes.V.ToPointer() + (i * camBytes.UVPixelStride * sizeof(byte)));
                          }
                      }
          
                      // Create the output byte array. RGB is three channels, therefore
                      // we need 3 times the pixel count
                      byte[] RGBimage = new byte[camBytes.Width * camBytes.Height * 3];
          
                      // GCHandles help us "pin" the arrays in the memory, so that we can
                      // pass them to the C++ code.
                      GCHandle YUVhandle = GCHandle.Alloc(YUVimage, GCHandleType.Pinned);
                      GCHandle RGBhandle = GCHandle.Alloc(RGBimage, GCHandleType.Pinned);
          
                      // Call the C++ function that we created.
                      int k = ConvertYUV2RGBA(YUVhandle.AddrOfPinnedObject(), RGBhandle.AddrOfPinnedObject(), camBytes.Width, camBytes.Height);
          
                      // If OpenCV conversion failed, return null
                      if (k != 0)
                      {
                          Debug.LogWarning("Color conversion - k != 0");
                          return null;
                      }
          
                      // Create a new texture object
                      result = new Texture2D(camBytes.Width, camBytes.Height, TextureFormat.RGB24, false);
          
                      // Load the RGB array to the texture, send it to GPU
                      result.LoadRawTextureData(RGBimage);
                      result.Apply();
          
                      // Save the texture as an PNG file. End the using {} clause to
                      // dispose of the CameraImageBytes.
                      File.WriteAllBytes(Application.persistentDataPath + "/tex.png", result.EncodeToPNG());
                  }
          
                  // Return the texture.
                  return result;
              }
          

          为了能够运行unsafe 代码,您还需要在 Unity 中允许它。转到播放器设置(Edit &gt; Project Settings &gt; Player Settings 并选中 Allow unsafe code 复选框。)

          现在,您可以调用 CameraToTexture() 函数,假设从 Update() 中每 5 秒调用一次,并且相机图像应保存为 /Android/data/YOUR_APPLICATION_PACKAGE/files/tex.png。即使您以纵向模式握住手机,图像也可能是横向的,但这不再难以修复。此外,您可能会注意到每次保存图像时都会冻结 - 因此我建议在单独的线程中调用此函数。此外,这里最苛刻的操作是将图像保存为 PNG 文件,因此如果您出于任何其他原因需要它,应该没问题(但仍然使用单独的线程)。

          如果您想了解 YUV_420_888 格式,为什么需要一个 1.5*pixelCount 数组,以及为什么我们以我们的方式修改数组,请阅读 https://wiki.videolan.org/YUV/#NV12。其他网站似乎对这种格式的工作原理提供了不正确的信息。

          此外,如果您遇到任何问题,请随时向我发表评论,我会尽力提供帮助,以及对代码和答案的任何反馈。

          附录 1:根据https://docs.unity3d.com/ScriptReference/Texture2D.LoadRawTextureData.html,您应该使用 GetRawTextureData 而不是 LoadRawTextureData,以防止复制。为此,只需固定 GetRawTextureData 返回的数组,而不是 RGBimage 数组(可以删除)。另外,不要忘记调用 result.Apply();之后。

          附录 2:当您使用完两个 GCHandle 后,不要忘记在它们上调用 Free()。

          【讨论】:

          • 你是不是冒充别人问了一个问题然后回答?
          • 是的。 stackoverflow.blog/2011/07/01/…。我也不是假装是别人。我只是写成“我们”,这意味着需要在我们的 ARCore 项目中使用 OpenCV 的“我们”程序员。 “你”指的是阅读这篇文章的人——这不是我在撰写本文时所做的事情。
          • 不知道。酷!
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-12-11
          • 1970-01-01
          • 2013-01-20
          • 2011-07-31
          • 2013-12-14
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多