ARCore – 如何在 C# 中将 YUV 相机帧转换为 RGB 帧?

ARCore – How to convert YUV camera frame to RGB frame in C#?

我想从 ARCore 会话中获取当前的相机图像。我正在使用 Frame.CameraImage.AcquireCameraImageBytes() 方法来获取此图像。然后我将这张图片以 TextureFormat.R8 的格式加载到 Texture2D。但是纹理是红色的,并且是颠倒的。我知道 ARCore 使用的格式是 YUV,但我找不到将此格式转换为 RGB 的方法。我该怎么做?

关于这个问题有 2 或 3 个问题,但没有给出解决方案。

代码如下:

CameraImageBytes image = Frame.CameraImage.AcquireCameraImageBytes();
int width = image.Width;
int height = image.Height;
int size = width*height;


Texture2D texture = new Texture2D(width, height, TextureFormat.R8, false, false);
byte[] m_EdgeImage = new byte[size];

System.Runtime.InteropServices.Marshal.Copy(image.Y, m_EdgeImage, 0, size);

texture.LoadRawTextureData(m_EdgeImage);
texture.Apply();

结果图片:

在您包含的代码中,您正在将相机纹理 (image.Y) 的 Y 通道复制到 单通道 RGB纹理(TextureFormat.R8),不做任何转换。

YUVRGB 有三个通道,但您只使用了一个。 在 RGB 中,通道通常具有相同的大小,但在 YUV 中,它们通常不同。 U和V可以是Y大小的分数,具体分数取决于使用的格式。

由于这张贴图来自Android相机,具体格式应该是Y′UV420p,也就是平面格式,详见维基百科页面useful visual representation 通道值的分组方式:

CameraImageBytes API 结构要求您分别提取通道,然后以编程方式将它们重新组合在一起。

仅供参考,有一种更简单的方法来获取已经转换为 RGB 的相机纹理,但它只能通过着色器访问,不能通过 C# 代码访问。

假设您仍想在 C# 中执行此操作,要从 YUV 纹理收集所有通道,您需要将 UV 通道与 Y 通道区别对待。 您必须为 UV 通道创建一个单独的缓冲区。在 Unity-Technologies/experimental-ARInterface github 存储库的 issue 中有一个如何执行此操作的示例:

//We expect 2 bytes per pixel, interleaved U/V, with 2x2 subsampling
bufferSize = imageBytes.Width * imageBytes.Height / 2;
cameraImage.uv = new byte[bufferSize];

//Because U an V planes are returned separately, while remote expects interleaved U/V
//same as ARKit, we merge the buffers ourselves
unsafe
{
    fixed (byte* uvPtr = cameraImage.uv)
    {
        byte* UV = uvPtr;

        byte* U = (byte*) imageBytes.U.ToPointer();
        byte* V = (byte*) imageBytes.V.ToPointer();

        for (int i = 0; i < bufferSize; i+= 2)
        {
            *UV++ = *U;
            *UV++ = *V;

            U += imageBytes.UVPixelStride;
            V += imageBytes.UVPixelStride;
        }
    }
}

此代码将生成可以加载到格式为 TextureFormat.RG16:

的 Texture2D 中的原始纹理数据
Texture2D texUVchannels = new Texture2D(imageBytes.Width / 2, imageBytes.Height / 2, TextureFormat.RG16, false, false);
texUVchannels.LoadRawTextureData(rawImageUV);
texUVchannels.Apply();

现在您已将所有 3 个通道都存储在 2 个 Texture2D 中,您可以通过着色器或在 C# 中转换它们。

用于Android相机YUV图像的具体转换公式可以在YUV wikipedia page上找到:

void YUVImage::yuv2rgb(uint8_t yValue, uint8_t uValue, uint8_t vValue,
        uint8_t *r, uint8_t *g, uint8_t *b) const {
    int rTmp = yValue + (1.370705 * (vValue-128));
    int gTmp = yValue - (0.698001 * (vValue-128)) - (0.337633 * (uValue-128));
    int bTmp = yValue + (1.732446 * (uValue-128));
    *r = clamp(rTmp, 0, 255);
    *g = clamp(gTmp, 0, 255);
    *b = clamp(bTmp, 0, 255);
}

转换为 Unity 着色器将是:

float3 YUVtoRGB(float3 c)
{
    float yVal = c.x;
    float uVal = c.y;
    float vVal = c.z;

    float r = yVal + 1.370705 * (vVal - 0.5);
    float g = yVal - 0.698001 * (vVal - 0.5) - (0.337633 * (uVal - 0.5));
    float b = yVal + 1.732446 * (uVal - 0.5);

    return float3(r, g, b);
}

通过这种方式获得的纹理与来自 ARCore 的背景视频相比大小不同,因此如果您希望它们在屏幕上匹配,则需要使用来自 [=79 的 UV 和其他数据=].

所以要将 UV 传递给着色器:

var uvQuad = Frame.CameraImage.ImageDisplayUvs;
mat.SetVector("_UvTopLeftRight",
              new Vector4(uvQuad.TopLeft.x, uvQuad.TopLeft.y, uvQuad.TopRight.x, uvQuad.TopRight.y));
mat.SetVector("_UvBottomLeftRight",
              new Vector4(uvQuad.BottomLeft.x, uvQuad.BottomLeft.y, uvQuad.BottomRight.x, uvQuad.BottomRight.y));

camera.projectionMatrix = Frame.CameraImage.GetCameraProjectionMatrix(camera.nearClipPlane, camera.farClipPlane);

要在着色器中使用它们,您需要像 EdgeDetectionBackground shader 中一样对它们进行 lerp。

在同一个着色器中,您将找到一个示例,说明如何从着色器访问 RGB 相机图像,而无需进行任何转换,这对于您的用例来说可能更容易。

对此有一些要求:

  • 着色器必须在 glsl
  • 只能在 OpenGL ES3 中完成
  • 需要支持 GL_OES_EGL_image_external_essl3 扩展