Android: 使用预览回调的相机异步任务

4
我已经成功地实现了自定义滤镜(灰度、色调等)的相机预览。这个自定义滤镜是通过使用预览回调来操作RGB数组并将其绘制回画布,然后在表面视图上显示出来。
但是,这样做的缺点是我得到了非常低的FPS。如果我不在后台线程中使用Asynctask来执行此操作,则在UI线程中会执行太多的工作。因此,我尝试使用Asynctask来进行相机操作(我的主要目的是即使在相机预览回调的重负下,也能完美地使UI正常工作)。
但即使我使用了Asynctask,它也没有起到太大的帮助。所以我想知道是我的实现有问题还是即使使用asynctask,UI线程仍然会受到影响?
以下是我的代码片段:
CameraActivity.java
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("ACTIVITY_LIFECYCLE","CameraActivity: onCreate");
    setContentView(R.layout.camera_layout);
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB)
@Override
protected void onResume() {
    Log.d("ACTIVITY_LIFECYCLE","CameraActivity: onResume");
    if(preview == null){
        preview = new CameraPreviewAsync(this,camera);
        preview.execute();
    }
    super.onResume();
}

@Override
protected void onPause() {
    Log.d("ACTIVITY_LIFECYCLE","CameraActivity: onPause");
    if(preview!=null){
        preview.cancel(true);
        camera = preview.getCamera();
        if(camera!=null){
            camera.stopPreview();
            camera.setPreviewCallback(null);
            camera.release();
            camera = null;
            preview.setCamera(camera);
        }
        preview = null;
    }
    super.onPause();
}

@Override
public void onDestroy(){
    Log.d("ACTIVITY_LIFECYCLE","CameraActivity: onDestroy");
    super.onDestroy();
} 

CameraPreviewAsync.java:

private final String TAG = "CameraPreviewAsync";

private CameraActivity camAct;
private Camera mCamera;
private int cameraId;
private SurfaceView mSurfaceView;
private SurfaceHolder mHolder;

private boolean isPreviewRunning = false;
private int[] rgbints;
private int width;
private int height;
private Bitmap mBitmap;

public CameraPreviewAsync(CameraActivity act, Camera cam){
    this.camAct = act;
    this.mCamera = cam;
    this.mSurfaceView = (SurfaceView) act.findViewById(R.id.surfaceView);
}

public void resetSurface(){
    if(mCamera!=null){
        mCamera.stopPreview();
        mCamera.setPreviewCallback(null);
        mCamera.release();
        mCamera = null;
    }
    int tempId = R.id.surfaceView;
    RelativeLayout buttonBar = (RelativeLayout) camAct.findViewById(R.id.buttonBar);
    ((RelativeLayout) camAct.findViewById(R.id.preview)).removeAllViews();

    SurfaceView newSurface = new SurfaceView(camAct);
    newSurface.setId(tempId);
    RelativeLayout.LayoutParams layParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    layParams.alignWithParent = true;
    newSurface.setLayoutParams(layParams);
    ((RelativeLayout) camAct.findViewById(R.id.preview)).addView(newSurface);
    ((RelativeLayout) camAct.findViewById(R.id.preview)).addView(buttonBar);
}

@Override
protected void onPreExecute() {
    //Things to do before doInBackground executed
    Log.d(TAG,"onPreExecute");

    RelativeLayout.LayoutParams layParams = new RelativeLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    layParams.alignWithParent = true;
    mSurfaceView.setLayoutParams(layParams);

    //Check number of camera in the device, if less than 2 then remove swap button
    if (Camera.getNumberOfCameras() < 2) {
        ((RelativeLayout) camAct.findViewById(R.id.buttonBar)).removeViewAt(R.id.cameraSwap);
    }

    //Opening the camera
    cameraId = findBackFacingCamera();
    if (cameraId < 0) {
        cameraId = findFrontFacingCamera();
        if (cameraId < 0)
            Toast.makeText(camAct, "No camera found.", Toast.LENGTH_LONG).show();
        else
            mCamera = Camera.open(cameraId);
    } else {
        mCamera = Camera.open(cameraId);
    }

    //invalidate the menu bar and show menu appropriately
    camAct.invalidateOptionsMenu();

    // get Camera parameters and set it to Auto Focus
    if(mCamera!=null){
        Camera.Parameters params = mCamera.getParameters();
        List<String> focusModes = params.getSupportedFocusModes();
        if (focusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
            // set the focus mode
            params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
            // set Camera parameters
            mCamera.setParameters(params);
        }
    }

    super.onPreExecute();
}

@Override
protected Void doInBackground(Void... params) {
    //Things to do in the background thread
    Log.d(TAG,"doInBackground");

    mHolder = mSurfaceView.getHolder();
    mHolder.addCallback(surfaceCallback);

    return null;
}      

@Override
protected void onPostExecute(Void values) {
    //Things to do after doInBackground
    Log.d(TAG,"onPostExecute");

}

@Override
protected void onCancelled(){
    super.onCancelled();
}

/*
 * ************************************************************************************
 * SURFACEHOLDER CALLBACK
 * ************************************************************************************
 */
SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        Log.d(TAG,"surfaceCreated!!");
        if(CameraActivity.filterMode == CameraActivity.NORMAL_FILTER){
            try {
                if (mCamera != null) {
                    mCamera.startPreview();
                    mCamera.setPreviewDisplay(holder);
                }else{
                    Log.d(TAG,"CAMERA IS NULL in surfaceCreated!!");
                }
            } catch (IOException exception) {
                Log.e(TAG, "IOException caused by setPreviewDisplay()", exception);
            }   
        }else{
            synchronized(mSurfaceView){
                if(isPreviewRunning){
                    return;
                }else{                      

                    mSurfaceView.setWillNotDraw(false);
                    if(mCamera!=null){
                        isPreviewRunning = true;
                        Camera.Parameters p = mCamera.getParameters();
                        List<Size> sizes = p.getSupportedPreviewSizes();

                        Size size = p.getPreviewSize();
                        width = size.width;
                        height = size.height;

                        p.setPreviewFormat(ImageFormat.NV21);
                        showSupportedCameraFormats(p);
                        mCamera.setParameters(p);

                        rgbints = new int[width * height];

                        mCamera.startPreview();
                        mCamera.setPreviewCallback(previewCallback);
                    }
                }
            }
        }
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        Log.d(TAG,"surfaceDestroyed!");

        if(CameraActivity.filterMode == CameraActivity.NORMAL_FILTER){
            if (mCamera != null) {
                mCamera.stopPreview();
                isPreviewRunning = false;
            }
        }else{
            synchronized(mSurfaceView){
                if(mCamera!=null){
                    mCamera.setPreviewCallback(null);
                    mCamera.stopPreview();
                    isPreviewRunning = false;
                }
            }
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width,
            int height) {
        Log.d(TAG,"surfaceChanged!");
    }
};


/*
 * ************************************************************************************
 * CAMERA PREVIEW CALLBACK
 * ************************************************************************************
 */

Camera.PreviewCallback previewCallback = new Camera.PreviewCallback() {

    @Override
    public void onPreviewFrame(byte[] data, Camera camera) {
        if (!isPreviewRunning)
            return;
        Canvas resCanvas = null;

        if (mHolder == null) {
            return;
        }

        try {
            synchronized (mHolder) {
                resCanvas = mHolder.lockCanvas(null);
                int resCanvasW = resCanvas.getWidth();
                int resCanvasH = resCanvas.getHeight();

                if(mBitmap == null){
                    mBitmap =  Bitmap.createBitmap (width, height, Bitmap.Config.ARGB_8888);
                }

                decodeYUV(rgbints, data, width, height);

                Canvas canvas = new Canvas(mBitmap);

                //Setting the filter
                if(camAct.getCustomFilter().equalsIgnoreCase("NORMAL")) ;//don't change the rgb value
                if(camAct.getCustomFilter().equalsIgnoreCase("GRAYSCALE")) rgbints = grayscale(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("INVERT")) rgbints = invert(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("BOOSTRED")) rgbints = boostColor(rgbints,1);
                if(camAct.getCustomFilter().equalsIgnoreCase("BOOSTGREEN")) rgbints = boostColor(rgbints,2);
                if(camAct.getCustomFilter().equalsIgnoreCase("BOOSTBLUE")) rgbints = boostColor(rgbints,3);
                if(camAct.getCustomFilter().equalsIgnoreCase("NOISE")) rgbints = noise(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("HUE")) rgbints = hue(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("SATURATION")) rgbints = saturation(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("ENGRAVE")) rgbints = engrave(rgbints);
                if(camAct.getCustomFilter().equalsIgnoreCase("EMBOSS")) rgbints = emboss(rgbints);

                // draw the decoded image, centered on canvas
                canvas.drawBitmap(rgbints, 0, width, 0,0, width, height, false, null);

                resCanvas.drawBitmap (mBitmap, resCanvasW-((width+resCanvasW)>>1), resCanvasH-((height+resCanvasH)>>1),null);
            }
        }  catch (Exception e){
            e.printStackTrace();
        } finally {
            // do this in a finally so that if an exception is thrown
            // during the above, we don't leave the Surface in an
            // inconsistent state
            if (resCanvas != null) {
                mHolder.unlockCanvasAndPost(resCanvas);
            }
        }
    }
};

任何帮助都非常感激!:)提前感谢大家!

1
你有运行任何测试来测量预览回调中花费的时间吗?我不确定问题是否在onPreviewFrame的工作负载上。 - Lior Ohana
@LiorOhana 没有,我不确定该怎么做,你能建议一种测试的方法吗?谢谢! - CodingBird
3个回答

21

也许我的回答对你来说有些晚了,但我正在研究同样的话题,所以想分享一下我的发现...

首先,如果在AsyncTask上调用Camera的"open"方法,然后该线程存在并停止存在 - 我们不能真正期望从中得到回调,不是吗?因此,如果我们想要回调 - 那么我们需要一个线程,其生存时间至少与我们想要回调的时间相同。

但是,请等一下... Camera.PreviewCallback的文档并不是最清晰的,但其中一个不好的提示是"该回调在从中调用open(int)方法的事件线程上被调用。"他们所说的"事件"线程是什么意思?嗯,这并不是很清楚-但是通过查看Android代码并进行实验-我们可以知道他们需要一个包含Looper的线程。可能太多细节了,但在Camera构造函数中(从open方法中调用),有尝试首先获取当前线程的Looper的代码,如果不存在-则尝试获取主线程的Looper-它位于UI线程上。然后Camera使用Handler通过初始化的looper分派回调和其他方法。现在你可能能看出为什么即使你开启摄像头的线程与主线程不同,你仍然在主线程上获得了回调 - 因为你的工作线程没有一个looper - 所以Camera默认使用主线程上的looper。

我让我的工作线程正常回调,我使用了类似下面这样的方法中的HandlerThread:

private void startCamera() {
    if (mCameraThread == null) {
        mCameraThread = new HandlerThread(CAMERA_THREAD_NAME);
        mCameraThread.start();
        mCameraHandler = new Handler(mCameraThread.getLooper());
    }
    mCameraHandler.post(new Runnable() {
        @Override
        public void run() {
            try {
                mCamera = Camera.open();

我使用调试器确认我的onPreviewFrame确实在工作线程上运行。我还在UI线程上运行了动画,在将帧处理从主线程转移到工作线程之前一直很卡顿,但现在非常流畅。

请注意,如果您杀死工作线程,那么您的回调也将停止,并且相机(实际上是Handler)将抱怨您尝试使用已停止的线程。

顺便说一句,作为替代解决方案,当然可以将回调调用在主线程上,但是帧数据的处理可以委托给独立的线程。


是的,它完全按照您描述的方式工作,我已经测试过了。谢谢! - CanC
2
能否请 @leonman30 发布您的整个相机活动和支持类?我正在寻找一个流畅、非 hacky 的相机解决方案...但我总是阻塞 UI 线程 :D - Wicked161089

4
我猜想你使用了错误的AsyncTask实现:
根据文档,相机的回调是在调用open()的线程上调用的。同样,onPreviewFrame回调也是如此。因此,onPreviewFrame并不总是在主线程上执行。
你在AsyncTask的onPreExecute()方法中打开相机,这个方法在UI线程上被调用,而不是在后台线程上,因此相机的回调会在主线程上执行。
我认为你应该在AsyncTask的doInBackground()方法中打开相机。

是的,根据文档你是正确的,但据我观察,无论你在哪里调用,回调函数总是在主线程上运行。如果你有能够证明我错误的演示代码,我会很乐意接受它。 - Daud Arfin

4
来自其他方法的回调将被传递到调用open()的线程的事件循环中。如果该线程没有事件循环,则回调将被传递到主应用程序事件循环。如果没有主应用程序事件循环,则不会传递回调。源代码

天啊!所以无论如何onPreviewFrame都会在主线程上运行?该死!好吧,现在我会转而查看OpenGL了!谢谢! - CodingBird
我一直在寻找使用OpenGL显示相机预览的方法,但是我找不到任何好的方法,即使我找到了一个,当我尝试时,我也无法应用自定义滤镜。你能提供一些建议吗?谢谢! - CodingBird
1
只是提醒一下:根据Android文档,在使用camera.open()获取相机的线程上调用onPreviewFrame方法。 - Boston Walker
1
@DaudArfin,很遗憾你错了,Boston Walker是正确的:来自其他方法的回调被传递到调用open()的线程的事件循环中。如果此线程没有事件循环,则回调将传递到主应用程序事件循环。如果没有主应用程序事件循环,则不会传递回调。https://developer.android.com/reference/android/hardware/Camera.PreviewCallback.html#onPreviewFrame(byte[], android.hardware.Camera) - Simon Marquis
@Simon Marquis 是的,我忘记尝试使用事件循环器了。我会编辑我的答案。 - Daud Arfin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接