在此示例中,我将使用 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&, 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 > Project Settings > 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()。