You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用CameraX时BitmapFactory.decodeByteArray返回null的问题

问题:CameraX捕获图像经JNI传递后BitmapFactory.decodeByteArray返回null

我正在开发基于Native C的简易Android应用,目标是通过CameraX捕获图像,经JNI传递给C处理后将结果显示在ImageView中。调试发现BitmapFactory.decodeByteArray(processedImageData, 0, processedImageData.length, options)始终返回null,processedImageData不为空且和原始imageData内容一致,直接用imageData调用该方法也返回null。

相关代码

CameraActivity.java

public class CameraActivity extends AppCompatActivity {

    // Used to load the 'ar' library on application startup.
    static {
        System.loadLibrary("ar");
    }

    private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;

    ProcessCameraProvider cameraProvider;
    CameraSelector cameraSelector;

    ImageView imageView;

    private void startImageAnalysis() {
        // Create an ImageAnalysis use case
        ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build();

        // Create a HandlerThread to handle the image analysis on a separate thread
        HandlerThread analyzerThread = new HandlerThread("ImageAnalyzer");
        analyzerThread.start();
        Handler analyzerHandler = new Handler(analyzerThread.getLooper());

        // Create an Executor using the Handler from the HandlerThread
        Executor analyzerExecutor = new Executor() {
            @Override
            public void execute(Runnable runnable) {
                analyzerHandler.post(runnable);
            }
        };

        imageAnalysis.setAnalyzer(analyzerExecutor, image -> {
            // Get the image buffer as a ByteBuffer
            ByteBuffer buffer = image.getPlanes()[0].getBuffer();
            int width = image.getWidth();
            int height = image.getHeight();

            // Convert the ByteBuffer to a byte[] array
            byte[] imageData = new byte[buffer.remaining()];
            buffer.get(imageData);

            Log.d("MyApp", "Processing image");

            // Call the JNI wrapper function with the buffer and its dimensions
            processImageWrapper(imageData, width, height);

            // Close the image to release the resources
            image.close();
        });

        // Bind the ImageAnalysis use case to the camera
        cameraProvider.bindToLifecycle(this, cameraSelector, imageAnalysis);
    }

    private void stopImageAnalysis() {
        // TODO: Unbind the ImageAnalysis use case from the camera
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_camera);

        com.example.ar.databinding.ActivityCameraBinding binding = ActivityCameraBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        setContentView(R.layout.activity_camera);
        imageView = findViewById(R.id.image_view);

        // Initialize the camera provider future
        cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        // Wait for the camera provider to be available
        cameraProviderFuture.addListener(() -> {
            try {
                // Get the camera provider
                cameraProvider = cameraProviderFuture.get();

                // Bind the camera to the preview
                bindCamera(cameraProvider);

            } catch (ExecutionException | InterruptedException e) {
                Log.e("CameraXActivity", "Error binding camera", e);
            }
        }, ContextCompat.getMainExecutor(this));



    }

    private void bindCamera(@NonNull ProcessCameraProvider cameraProvider) {

        // Create an image capture use case and set it up
        ImageCapture imageCapture = new ImageCapture.Builder()
                .build();

        // Select the back camera as the default
        cameraSelector = new CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_BACK)
                .build();


        startImageAnalysis();

    }

    // Declare the native function
    public native byte[] processImage(byte[] imageData, int width, int height);

    // Define the JNI wrapper function
    private void processImageWrapper(byte[] imageData, int width, int height) {
        // Call the native function with the buffer and its dimensions and receive the processed image
        byte[] processedImageData = processImage(imageData, width, height);

        // Create a BitmapFactory.Options object to specify the image format
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inPreferredConfig = Bitmap.Config.ARGB_8888; // Set the format to ARGB_8888

        // Convert the processed image byte array to a Bitmap
        Bitmap bitmap = BitmapFactory.decodeByteArray(processedImageData, 0, processedImageData.length, options);


        // Set the Bitmap to the ImageView to render it on the screen
        imageView.setImageBitmap(bitmap);
    }

}

C++处理函数

extern "C" JNIEXPORT jbyteArray JNICALL
Java_com_example_ar_CameraActivity_processImage(
        JNIEnv* env,
        jobject /* this */, jbyteArray image_data, jint width, jint height) {
    // Get a pointer to the input image data
    jbyte* input_image_data = env->GetByteArrayElements(image_data, NULL);

    // Check if the input image data is null
    if (input_image_data == NULL) {
        return NULL;
    }

    // Create a new byte array to hold the output image data
    jbyteArray output_image_data = env->NewByteArray(width * height * 4);

    // Check if the output image data is null
    if (output_image_data == NULL) {
        // Release the input image data
        env->ReleaseByteArrayElements(image_data, input_image_data, JNI_ABORT);
        return NULL;
    }

    // Copy the input image data to the output image data
    env->SetByteArrayRegion(output_image_data, 0, width * height * 4, input_image_data);

    // Release the input image data
    env->ReleaseByteArrayElements(image_data, input_image_data, JNI_ABORT);

    // Return the output image data
    return output_image_data;
}

核心原因

  • CameraX的ImageAnalysis默认输出YUV_420_888格式图像,image.getPlanes()[0]仅获取到Y分量的原始字节数据,并非ARGB/RGB等Bitmap可识别的像素格式。
  • BitmapFactory.decodeByteArray只支持JPEG/PNG等标准压缩图像格式,或已转换为ARGB/RGB的原始像素数组,无法直接解码YUV字节流。

解决方案

方案1:先将YUV转换为ARGB像素数据

在Java端把CameraX输出的YUV图像转为ARGB格式的字节数组,再传给JNI处理:

imageAnalysis.setAnalyzer(analyzerExecutor, image -> {
    // 创建ARGB格式Bitmap
    Bitmap bitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
    // 转换YUV到ARGB
    convertYuvToArgb(image, bitmap);
    
    // 将Bitmap转为字节数组
    int byteCount = bitmap.getByteCount();
    ByteBuffer buffer = ByteBuffer.allocate(byteCount);
    bitmap.copyPixelsToBuffer(buffer);
    byte[] imageData = buffer.array();
    
    // 调用JNI处理
    byte[] processedData = processImage(imageData, bitmap.getWidth(), bitmap.getHeight());
    
    // 将处理后的字节数组转回Bitmap
    ByteBuffer processedBuffer = ByteBuffer.wrap(processedData);
    Bitmap processedBitmap = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888);
    processedBitmap.copyPixelsFromBuffer(processedBuffer);
    
    imageView.setImageBitmap(processedBitmap);
    image.close();
});

// YUV转ARGB工具方法
private void convertYuvToArgb(Image image, Bitmap bitmap) {
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer yBuffer = planes[0].getBuffer();
    ByteBuffer uBuffer = planes[1].getBuffer();
    ByteBuffer vBuffer = planes[2].getBuffer();

    int width = image.getWidth();
    int height = image.getHeight();

    int yRowStride = planes[0].getRowStride();
    int uvRowStride = planes[1].getRowStride();
    int uvPixelStride = planes[1].getPixelStride();

    int[] pixels = new int[width * height];
    int pixelIndex = 0;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int yIndex = y * yRowStride + x;
            int uvIndex = uvRowStride * (y / 2) + uvPixelStride * (x / 2);

            int yValue = yBuffer.get(yIndex) & 0xFF;
            int uValue = uBuffer.get(uvIndex) & 0xFF;
            int vValue = vBuffer.get(uvIndex) & 0xFF;

            // YUV转RGB公式
            int r = (int) (yValue + 1.370705 * (vValue - 128));
            int g = (int) (yValue - 0.698001 * (vValue - 128) - 0.337633 * (uValue - 128));
            int b = (int) (yValue + 1.732446 * (uValue - 128));

            // 裁剪到0-255范围
            r = Math.max(0, Math.min(255, r));
            g = Math.max(0, Math.min(255, g));
            b = Math.max(0, Math.min(255, b));

            pixels[pixelIndex++] = 0xFF000000 | (r << 16) | (g << 8) | b;
        }
    }

    bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
}

方案2:直接操作Bitmap像素缓冲区(性能更优)

避免字节数组拷贝,直接将Bitmap的内存地址传给C++处理:

  1. 修改Java中的Native方法和调用逻辑:
// 声明Native方法
public native void processBitmap(Bitmap bitmap);

private void processImageWrapper(Image image) {
    Bitmap bitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888);
    convertYuvToArgb(image, bitmap);
    
    // 调用Native方法处理Bitmap
    processBitmap(bitmap);
    
    imageView.setImageBitmap(bitmap);
    image.close();
}
  1. 修改C++代码:
#include <android/bitmap.h>

extern "C" JNIEXPORT void JNICALL
Java_com_example_ar_CameraActivity_processBitmap(
        JNIEnv* env,
        jobject /* this */, jobject bitmap) {
    AndroidBitmapInfo bitmapInfo;
    void* pixelBuffer;

    // 获取Bitmap信息
    if (AndroidBitmap_getInfo(env, bitmap, &bitmapInfo) != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }
    // 锁定Bitmap像素缓冲区
    if (AndroidBitmap_lockPixels(env, bitmap, &pixelBuffer) != ANDROID_BITMAP_RESULT_SUCCESS) {
        return;
    }

    // 处理ARGB像素数据(示例:反转颜色)
    uint32_t* argbPixels = static_cast<uint32_t*>(pixelBuffer);
    int width = bitmapInfo.width;
    int height = bitmapInfo.height;

    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            int index = y * width + x;
            uint32_t pixel = argbPixels[index];
            // 反转RGB通道
            argbPixels[index] = 0xFF000000 | 
                                ((pixel & 0x000000FF) << 16) | 
                                (pixel & 0x0000FF00) | 
                                ((pixel & 0x00FF0000) >> 16);
        }
    }

    // 解锁像素缓冲区
    AndroidBitmap_unlockPixels(env, bitmap);
}

方案3:将YUV转为JPEG字节流

如果不需要复杂的像素级处理,可先把YUV转为JPEG格式,再传给JNI,处理后直接解码:

imageAnalysis.setAnalyzer(analyzerExecutor, image -> {
    // 将YUV转为JPEG字节流
    YuvImage yuvImage = new YuvImage(
            getYuvByteArray(image),
            ImageFormat.NV21,
            image.getWidth(),
            image.getHeight(),
            null);
    ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
    yuvImage.compressToJpeg(new Rect(0, 0, image.getWidth(), image.getHeight()), 90, outputStream);
    byte[] jpegData = outputStream.toByteArray();

    // 调用JNI处理
    byte[] processedJpeg = processImage(jpegData, image.getWidth(), image.getHeight());
    
    // 解码JPEG为Bitmap
    Bitmap bitmap = BitmapFactory.decodeByteArray(processedJpeg, 0, processedJpeg.length);
    imageView.setImageBitmap(bitmap);

    image.close();
});

private byte[] getYuvByteArray(Image image) {
    Image.Plane[] planes = image.getPlanes();
    ByteBuffer yBuffer = planes[0].getBuffer();
    ByteBuffer uBuffer = planes[1].getBuffer();
    ByteBuffer vBuffer = planes[2].getBuffer();

    int ySize = yBuffer.remaining();
    int uSize = uBuffer.remaining();
    int vSize = vBuffer.remaining();

    byte[] yuvBytes = new byte[ySize + uSize + vSize];
    yBuffer.get(yuvBytes, 0, ySize);
    vBuffer.get(yuvBytes, ySize, vSize);
    uBuffer.get(yuvBytes, ySize + vSize, uSize);

    return yuvBytes;
}

内容的提问来源于stack exchange,提问作者Ghiles_1010

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 20:42:00