CameraX ImageAnalysis only providing grayscale frames - need RGB image for Luxand FaceSDK liveness detection
I am developing a .NET MAUI Android application using CameraX and Luxand FaceSDK.
Currently, my face detection pipeline receives a grayscale image buffer and passes it to Luxand:
public List<FaceRect> ProcessFrame(byte[] gray, int width, int height, int rotationDegrees)
{
int image;
int rc = FSDK.LoadImageFromBuffer(
out image,
gray,
width,
height,
width,
FSDK.FSDK_IMAGEMODE.FSDK_IMAGE_GRAYSCALE_8BIT
);
...
}
Face detection and recognition work correctly.
However, I am now implementing Luxand liveness (anti-spoofing) detection and Luxand support informed me that liveness requires a COLOR image and grayscale frames will be rejected.
My current pipeline extracts only the Y plane from CameraX and creates a grayscale buffer that is sent to:
FSDK.LoadImageFromBuffer(
out image,
gray,
width,
height,
width,
FSDK.FSDK_IMAGEMODE.FSDK_IMAGE_GRAYSCALE_8BIT
);
Questions:
How can I get a full RGB image from CameraX ImageAnalysis?
CameraX appears to provide YUV_420_888 frames. What is the recommended way to convert YUV_420_888 to RGB?
After conversion, what buffer format should be passed to Luxand FaceSDK?
Is there a performant solution suitable for real-time face recognition and liveness detection?
Environment:
.NET MAUI
Android
CameraX ImageAnalysis
Luxand FaceSDK
Real-time face recognition
camerax analyzer code:
using AndroidX.Camera.Core;
using JTClockV2.Platforms.Android.CameraX;
using System;
using Java.Nio;
using Android.App;
using Android.Util;
using ASize = Android.Util.Size;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Diagnostics;
namespace JTClockV2.Platforms.Android.CameraX
{
public class FrameAnalyzer : Java.Lang.Object, ImageAnalysis.IAnalyzer
{
private readonly ICameraFrameListener _listener;
public FrameAnalyzer(ICameraFrameListener listener)
{
_listener = listener;
}
ASize ImageAnalysis.IAnalyzer.DefaultTargetResolution
=> new ASize(640, 480);
//public void Analyze(IImageProxy image)
//{
// try
// {
// // var argb =YuvToRgb24( image);
// // var bgra = ConvertArgbToBgra(argb);
// // _listener.OnFrame(argb, image.Width, image.Height);
// // Log("CAMERAX", $"First byte = {image.GetPlanes()[0].Buffer.Get(0)}"); //time add
// int rotationDegrees = image.ImageInfo.RotationDegrees;
// var yPlane = image.GetPlanes()[0];
// var yBuffer = yPlane.Buffer;
// byte[] gray = new byte[image.Width * image.Height];
// yBuffer.Get(gray);
// _listener.OnFrame(gray, image.Width, image.Height, rotationDegrees);
// }
// finally
// {
// image.Close();
// }
//}
public void Analyze(IImageProxy image)
{
try
{
int width = image.Width;
int height = image.Height;
int rotationDegrees = image.ImageInfo.RotationDegrees;
var yPlane = image.GetPlanes()[0];
var yBuffer = yPlane.Buffer;
int rowStride = yPlane.RowStride;
byte[] gray = new byte[width * height];
int pos = 0;
for (int row = 0; row < height; row++)
{
yBuffer.Position(row * rowStride);
yBuffer.Get(gray, pos, width);
pos += width;
}
// ✅ PASS ROTATION ALSO
_listener.OnFrame(gray, width, height, rotationDegrees);
}
finally
{
image.Close();
}
}
private void Log(string msg, string v)
{
Debug.WriteLine($"[{DateTime.Now:HH:mm:ss.fff}] 🔵 LUXAND: " + msg);
}
private static byte[] ConvertArgbToBgra(byte[] argb)
{
byte[] bgra = new byte[argb.Length];
for (int i = 0; i < argb.Length; i += 4)
{
byte a = argb[i];
byte r = argb[i + 1];
byte g = argb[i + 2];
byte b = argb[i + 3];
bgra[i] = b;
bgra[i + 1] = g;
bgra[i + 2] = r;
bgra[i + 3] = a;
}
return bgra;
}
private static byte[] YuvToRgb24(IImageProxy image)
{
int width = image.Width;
int height = image.Height;
var yPlane = image.GetPlanes()[0];
var uPlane = image.GetPlanes()[1];
var vPlane = image.GetPlanes()[2];
ByteBuffer yBuf = yPlane.Buffer;
ByteBuffer uBuf = uPlane.Buffer;
ByteBuffer vBuf = vPlane.Buffer;
int yRowStride = yPlane.RowStride;
int uvRowStride = uPlane.RowStride;
int uvPixelStride = uPlane.PixelStride;
byte[] rgb = new byte[width * height * 3];
int idx = 0;
for (int y = 0; y < height; y++)
{
int yOffset = y * yRowStride;
int uvOffset = (y / 2) * uvRowStride;
for (int x = 0; x < width; x++)
{
int Y = yBuf.Get(yOffset + x) & 0xFF;
int U = (uBuf.Get(uvOffset + (x / 2) * uvPixelStride) & 0xFF) - 128;
int V = (vBuf.Get(uvOffset + (x / 2) * uvPixelStride) & 0xFF) - 128;
int r = (int)(Y + 1.402 * V);
int g = (int)(Y - 0.344 * U - 0.714 * V);
int b = (int)(Y + 1.772 * U);
rgb[idx++] = (byte)Math.Clamp(r, 0, 255);
rgb[idx++] = (byte)Math.Clamp(g, 0, 255);
rgb[idx++] = (byte)Math.Clamp(b, 0, 255);
}
}
return rgb;
}
// 🔥 CORE CONVERSION (YUV_420_888 → RGBA)
private static byte[] Yuv4ToRgba(IImageProxy image)
{
int width = image.Width;
int height = image.Height;
var yPlane = image.GetPlanes()[0];
var uPlane = image.GetPlanes()[1];
var vPlane = image.GetPlanes()[2];
ByteBuffer yBuffer = yPlane.Buffer;
ByteBuffer uBuffer = uPlane.Buffer;
ByteBuffer vBuffer = vPlane.Buffer;
int yRowStride = yPlane.RowStride;
int uvRowStride = uPlane.RowStride;
int uvPixelStride = uPlane.PixelStride;
byte[] rgba = new byte[width * height * 4];
int index = 0;
for (int row = 0; row < height; row++)
{
int yRowOffset = row * yRowStride;
int uvRowOffset = (row / 2) * uvRowStride;
for (int col = 0; col < width; col++)
{
int yIndex = yRowOffset + col;
int uvIndex = uvRowOffset + (col >> 1) * uvPixelStride;
int y = yBuffer.Get(yIndex) & 0xFF;
int u = (uBuffer.Get(uvIndex) & 0xFF) - 128;
int v = (vBuffer.Get(uvIndex) & 0xFF) - 128;
// YUV → RGB conversion
int c = y - 16;
int d = u - 128;
int e = v - 128;
int r = (298 * c + 409 * e + 128) >> 8;
int g = (298 * c - 100 * d - 208 * e + 128) >> 8;
int b = (298 * c + 516 * d + 128) >> 8;
rgba[index++] = (byte)Clamp(r);
rgba[index++] = (byte)Clamp(g);
rgba[index++] = (byte)Clamp(b);
rgba[index++] = 255; // Alpha
}
}
return rgba;
}
private static int Clamp(int value)
{
if (value < 0) return 0;
if (value > 255) return 255;
return value;
}
}
}
Any sample code or recommendations would be greatly appreciated.