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
),不做任何转换。
YUV 和 RGB 有三个通道,但您只使用了一个。
在 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
扩展
我想从 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
),不做任何转换。
YUV 和 RGB 有三个通道,但您只使用了一个。 在 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 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
扩展