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

Posted

技术标签:

【中文标题】Android:如何通过回调显示相机预览?【英文标题】:Android: how to display camera preview with callback? 【发布时间】:2011-12-02 00:15:14 【问题描述】:

我需要做的很简单,我想使用相机回调手动显示来自相机的预览,并且我想在真实设备上获得至少 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 中无法快速完成。所以我不能使用这个解决方案。 another question 中的一些细节。


3.使用 NDK 在 (GL)SurfaceView 上显示相机预览以处理 YUV 数据。我找到了一个解决方案here,它使用了一些 C 函数和 NDK。但我没有设法使用它,here some more details。但无论如何,这个解决方案是返回 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),但这可能是一个开始。我很想看看它是否适合你——我目前仍在与与你正交的问题作斗争。

【讨论】:

我明天会试试这个,但我很确定不会有任何性能提升,因为它的工作原理或多或少与带有 YuvImage 和 BitmapFactory 类的版本相同。但我会试一试,我们会看到的。谢谢。 所以,正如我所料,它的性能在所有情况下都是最差的,即使我尝试对其进行一点优化...... 我在这一行得到 java.lang.ArrayIndexOutOfBoundsException int u = (0xff & ((int) data[frameSize + (i >> 1) * width + (j & ~1) + 0 ]));【参考方案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 运行,应该也可以满足您的需求。

【讨论】:

Second, don't create a ton of work for the garbage collector.。您的onDraw 告诉我们否则。【参考方案3】:

这不是你想要的吗?只需在您的布局中使用 SurfaceView,然后在您的 init 中的某个位置,例如 onResume()

SurfaceView surfaceView = ...
SurfaceHolder holder = surfaceView.getHolder();
...
Camera camera = ...;
camera.setPreviewDisplay(holder);

它只是在帧到达时将帧直接发送到视图。

如果要灰度,用setColorEffect("mono")修改相机参数。

【讨论】:

谢谢,但我确实需要手动使用回调和显示图像。我不仅需要灰度图像,我正在使用更多过滤器,我在我的问题中没有提到......【参考方案4】:

对于非常基本和简单的效果,有

Camera.Parameters parameters = mCamera.getParameters();
parameters.setColorEffect(Parameters.EFFECT_AQUA);

我发现这种效果因设备而异。 例如,在我的手机(galaxy s II)上,它看起来有点像漫画效果,而与 Galaxy s 1 相比,它“只是”一个蓝色阴影。

它是专业的:它可以作为实时预览。

我查看了其他一些相机应用程序,他们显然也遇到了这个问题。 那么他们做了什么? 他们正在捕获默认的相机图像,对位图数据应用过滤器,并在简单的 ImageView 中显示该图像。它肯定没有实时预览那么酷,但你永远不会遇到性能问题。

【讨论】:

谢谢,我知道这些相机效果,但我想做一些对比度调整,颜色替换等。但是你是对的,看起来实时预览真的没有一个好的工作解决方案,所以我必须做你描述的事情......:/【参考方案5】:

我相信我在blog 中读到灰度数据位于前 x*y 字节中。 Yuv 应该代表亮度,所以数据就在那里,虽然它不是一个完美的灰度。它非常适合相对亮度,但不适用于灰度,因为在 rgb 中每种颜色都不像彼此那么亮。绿色通常在光度转换中被赋予更大的权重。希望这会有所帮助!

【讨论】:

【参考方案6】:

有什么特殊原因迫使您使用 GLES 1.0 吗?

因为如果不是,请在此处查看接受的答案: Android SDK: Get raw preview camera image without displaying it

通常它提到将 Camera.setPreviewTexture() 与 GLES 2.0 结合使用。 在 GLES 2.0 中,您可以在整个屏幕上渲染全屏四边形,并创建您想要的任何效果。

这很可能是最快的方式。

【讨论】:

示例:github.com/google/grafika/blob/master/src/com/android/grafika/…。它目前有一个着色器,可将黑白图像置于红色通道中(搜索“rosyFragment”)。为 R/G/B 分配相同的值将产生黑白图像。

以上是关于Android:如何通过回调显示相机预览?的主要内容,如果未能解决你的问题,请参考以下文章

Android:通过片段进行相机预览。从活动中确定

Android相机预览设置适配及显示方式

如何在一帧后面显示预览相机并拍照包括帧

使用 PreviewView 来展示相机预览

如何使用 Android API 将相机预览大小设置为全屏?

如何将相机预览流从android连接到Qt5?