安卓:如何通过回调显示相机预览?

2022-09-04 03:25:59

我需要做的很简单,我想使用相机回调从相机手动显示预览,我想在真实设备上获得至少15fps。我甚至不需要颜色,我只需要预览灰度图像。来自相机的图像是YUV格式,您必须以某种方式对其进行处理,这是主要的性能问题。我使用的是 API 8。

在所有情况下,我都使用camera.setPreviewCallbackWithBuffer(),这比camera.setPreviewCallback()更快。如果我没有显示预览,似乎我无法在这里获得大约24 fps。所以没有问题。

我尝试了以下解决方案:

1. 将 SurfaceView 上的相机预览显示为位图。它可以工作,但性能约为6fps。

baos = new ByteOutputStream();
yuvimage=new YuvImage(cameraFrame, ImageFormat.NV21, prevX, prevY, null);

yuvimage.compressToJpeg(new Rect(0, 0, prevX, prevY), 80, baos);
jdata = baos.toByteArray();

bmp = BitmapFactory.decodeByteArray(jdata, 0, jdata.length); // Convert to Bitmap, this is the main issue, it takes a lot of time

canvas.drawBitmap(bmp , 0, 0, paint);


2. 在 GLSurfaceView 上将相机预览显示为纹理。在这里,我只显示亮度数据(灰度图像),这很容易,它只需要每帧上有一个arraycopy()。我可以得到大约12fps,但我需要对预览应用一些滤镜,似乎在OpenGL ES 1中无法快速完成。所以我不能使用这个解决方案。在另一个问题中对此的一些细节。


3. 在 (GL)SurfaceView 上使用 NDK 处理 YUV 数据,在 (GL)SurfaceView 上显示摄像机预览我在这里找到了一个使用一些C函数和NDK的解决方案。但是我没有设法使用它,这里有一些更多细节。但无论如何,这个解决方案是为了返回ByteBuffer以在OpenGL中将其显示为纹理,并且它不会比之前的尝试更快。因此,我必须修改它以返回int[]数组,该数组可以使用canvas.drawBitmap()绘制,但我对C的理解还不够多, 无法做到这一点。


那么,我错过了其他方式或对我尝试过的尝试有一些改进吗?


答案 1

我正在研究完全相同的问题,但还没有像你一样走得那么远。

您是否考虑过将像素直接绘制到画布上,而无需先将其编码为JPEG?在OpenCV工具包 http://sourceforge.net/projects/opencvlibrary/files/opencv-android/2.3.1/OpenCV-2.3.1-android-bin.tar.bz2/download(实际上并不使用opencv;不用担心),有一个名为 tutorial-0-androidcamera 的项目,它演示如何将YUV像素转换为RGB,然后直接将它们写入位图。

相关代码基本上是:

public void onPreviewFrame(byte[] data, Camera camera, int width, int height) {
    int frameSize = width*height;
    int[] rgba = new int[frameSize+1];

    // Convert YUV to RGB
    for (int i = 0; i < height; i++)
        for (int j = 0; j < width; j++) {
            int y = (0xff & ((int) data[i * width + j]));
            int u = (0xff & ((int) data[frameSize + (i >> 1) * width + (j & ~1) + 0]));
            int v = (0xff & ((int) data[frameSize + (i >> 1) * width + (j & ~1) + 1]));
            y = y < 16 ? 16 : y;

            int r = Math.round(1.164f * (y - 16) + 1.596f * (v - 128));
            int g = Math.round(1.164f * (y - 16) - 0.813f * (v - 128) - 0.391f * (u - 128));
            int b = Math.round(1.164f * (y - 16) + 2.018f * (u - 128));

            r = r < 0 ? 0 : (r > 255 ? 255 : r);
            g = g < 0 ? 0 : (g > 255 ? 255 : g);
            b = b < 0 ? 0 : (b > 255 ? 255 : b);

            rgba[i * width + j] = 0xff000000 + (b << 16) + (g << 8) + r;
        }

    Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    bmp.setPixels(rgba, 0/* offset */, width /* stride */, 0, 0, width, height);
    Canvas canvas = mHolder.lockCanvas();
    if (canvas != null) {
        canvas.drawBitmap(bmp, (canvas.getWidth() - width) / 2, (canvas.getHeight() - height) / 2, null);
        mHolder.unlockCanvasAndPost(canvas);
    } else {
        Log.w(TAG, "Canvas is null!");
    }
    bmp.recycle();
}

当然,您必须对其进行调整以满足您的需求(例如,不为每个帧分配rgba),但这可能是一个开始。我很想看看它是否适合你 - 我现在仍在与与你正交的问题作斗争。


答案 2

我认为迈克尔走在正确的轨道上。首先,您可以尝试此方法从RGB转换为灰度。显然,它正在做几乎与他相同的事情,但对于你想要的东西来说,它更简洁一些。

//YUV Space to Greyscale
static public void YUVtoGrayScale(int[] rgb, byte[] yuv420sp, int width, int height){
    final int frameSize = width * height;
    for (int pix = 0; pix < frameSize; pix++){
        int pixVal = (0xff & ((int) yuv420sp[pix])) - 16;
        if (pixVal < 0) pixVal = 0;
        if (pixVal > 255) pixVal = 255;
        rgb[pix] = 0xff000000 | (pixVal << 16) | (pixVal << 8) | pixVal;
    }
}

}

其次,不要为垃圾回收器创建大量工作。您的位图和数组将是固定大小。创建它们一次,而不是在 onFramePreview 中。

这样做,你最终会得到这样的东西:

    public PreviewCallback callback = new PreviewCallback() {
    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if ( (mSelectView == null) || !inPreview )
            return;
        if (mSelectView.mBitmap == null)
        {
            //initialize SelectView bitmaps, arrays, etc
            //mSelectView.mBitmap = Bitmap.createBitmap(mSelectView.mImageWidth, mSelectView.mImageHeight, Bitmap.Config.RGB_565);
           //etc

        }
        //Pass Image Data to SelectView
        System.arraycopy(data, 0, mSelectView.mYUVData, 0, data.length);
        mSelectView.invalidate();
    }
};

然后你想放它的画布看起来像这样:

class SelectView extends View {
Bitmap mBitmap;
Bitmap croppedView;
byte[] mYUVData;
int[] mRGBData;
int mImageHeight;
int mImageWidth;

public SelectView(Context context){
    super(context);
    mBitmap = null;
    croppedView = null;
}

@Override
protected void onDraw(Canvas canvas){
    if (mBitmap != null)
    {
        int canvasWidth = canvas.getWidth();
        int canvasHeight = canvas.getHeight();
        // Convert from YUV to Greyscale
        YUVtoGrayScale(mRGBData, mYUVData, mImageWidth, mImageHeight);
            mBitmap.setPixels(mRGBData, 0, mImageWidth, 0, 0, mImageWidth, mImageHeight);
            Rect crop = new Rect(180, 220, 290, 400);
        Rect dst = new Rect(0, 0, canvasWidth, (int)(canvasHeight/2));
        canvas.drawBitmap(mBitmap, crop, dst, null);
    }
    super.onDraw(canvas);
}

此示例实时显示相机预览的裁剪和扭曲选择,但您明白了。它在灰度的Nexus S上以高FPS运行,也应该满足您的需求。


推荐