65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 BoofCV 在 Android 上进行实时计算机视觉

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (30投票s)

2013年2月27日

CPOL

6分钟阅读

viewsIcon

80701

downloadIcon

3217

一个关于如何在Android上创建处理视频的应用程序的简单教程。

在Android手机上显示的彩色图像梯度。

引言

本文将分步介绍如何使用BoofCV在Android设备上编写一个简单的计算机视觉应用程序。教程结束后,您将了解如何处理视频流,计算图像梯度,可视化梯度以及显示结果。对于不知道BoofCV的人来说,它是一个用Java编写的开源计算机视觉库,非常适合Android设备。

本文的大部分内容将涉及Android API,当涉及到视频流时,它有点令人费解。在Android上使用BoofCV非常简单,无需修改或原生代码。自BoofCV v0.13发布以来,其改进的Android集成包使其使用起来更加容易。

在我们开始之前,这里有一些快速链接可以帮助您熟悉BoofCV及其功能。我假设您已经熟悉Android和Android开发的基础知识。

本教程的源代码可以从顶部的链接下载。该项目完全独立,包含您需要的所有库。

变更

自本文首次发布(2013年2月)以来,已修改(2014年1月)以解决“成员4367060”提出的问题,请参见下文讨论。虽然由于项目配置方式,这些更改的影响并不明显,但它更正确,并且可以应用于其他配置,问题更少。

  • 应用程序现在可以加载到Nexus 7设备上
  • 前置摄像头图像已翻转以正确显示
  • 修复了如果未调用surfaceCreated ,则相机捕获无法重新启动的错误

Android上的BoofCV

如前所述,BoofCV是一个Java库,这意味着无需为Android重新编译该库。其下载页面上的Jar文件无需修改即可使用。对于Android特定功能,请确保包含BoofCVAndroid.jar,它是标准Jar下载的一部分,也可以由您自己编译。有关更多说明,请参见项目网站。

在Android上编写快速计算机视觉代码的关键在于图像格式之间的有效转换。使用Bitmap中的RGB访问器函数非常慢,而且没有内置的好方法来转换NV21(视频图像格式)。这就是Android集成包的用武之地。它包含两个类,可以大大简化您的工作。

  • ConvertBitmap
  • ConvertNV21

使用这些类从Android图像类型转换为BoofCV图像类型。以下是一些使用示例

// Easiest way to convert a Bitmap into a BoofCV type
ImageUInt8 image = ConvertBitmap.bitmapToGray(bitmap, (ImageUInt8)null, null); 
 
// From NV21 to gray scale
ConvertNV21.nv21ToGray(bytes,width,height,gray);

在Android上捕获视频

在Android上,您通过监听相机预览来捕获视频流。为了增加趣味性,他们会强制您始终显示预览。这远非我见过的最佳视频流捕获API,但我们只能使用它。如果您尚未下载示例代码,现在是时候了。

Android上的视频步骤

  1. 打开并配置相机
  2. 创建SurfaceHolder 以显示相机预览
  3. 在相机预览视图之上添加视图以供显示
  4. 为相机的预览提供监听器
  5. 启动相机预览
  6. 在单独的线程中执行耗时的计算
  7. 渲染结果

在访问相机之前,必须先在AndroidManifest.xml中添加以下内容。

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" /> 

如果这是一个必须使用相机才能工作的计算机视觉应用程序,为什么会提示它不需要相机?事实是,如果您将其设置为需要相机,则将从Play商店中排除仅具有一个前置摄像头的设备(例如平板电脑)!在较新版本的Android操作系统中,显然有一种方法可以解决此问题。

查看VideoActivity.javaonCreate()onResume()中发生了几个重要的活动。

  1. 创建并配置用于显示相机预览的视图。
  2. 添加用于渲染输出的视图。
  3. 打开并配置相机。
@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    requestWindowFeature(Window.FEATURE_NO_TITLE);
    setContentView(R.layout.video);

    // Used to visualize the results
    mDraw = new Visualization(this);

    // Create our Preview view and set it as the content of our activity.
    mPreview = new CameraPreview(this,this,true);

    FrameLayout preview = (FrameLayout) findViewById(R.id.camera_preview);

    preview.addView(mPreview);
    preview.addView(mDraw);
}

@Override
protected void onResume() {
    super.onResume();
    setUpAndConfigureCamera();
}

变量mPreview,它是CameraPreview(下文讨论)的实例,需要捕获视频图像。mDraw在屏幕上绘制我们的输出。FrameLayout允许将两个视图堆叠在一起,这正是上面所做的。

配置相机

在'VideoActivity.onCreate()'中,它调用setUpAndConfigureCamera()函数。此函数使用selectAndOpenCamera()打开相机,将其配置为拍摄较小的预览图像,启动一个用于处理视频的线程,并将相机传递给mPreview

private void setUpAndConfigureCamera() {
    // Open and configure the camera
    mCamera = selectAndOpenCamera();

    Camera.Parameters param = mCamera.getParameters();

    // Select the preview size closest to 320x240
    // Smaller images are recommended because some computer vision operations are very expensive
    List<Camera.Size> sizes = param.getSupportedPreviewSizes();
    Camera.Size s = sizes.get(closest(sizes,320,240));
    param.setPreviewSize(s.width,s.height);
    mCamera.setParameters(param);

    // declare image data
   ....

    // start image processing thread
    thread = new ThreadProcess();
    thread.start();

    // Start the video feed by passing it to mPreview
    mPreview.setCamera(mCamera);
}

在Android上处理视频图像的一个好习惯是尽量减少在预览回调中花费的时间。如果您的处理过程花费太长时间,就会导致积压并导致崩溃。这就是为什么我们在上面的函数中启动一个新线程的原因。函数中的最后一行将相机传递给mPreview,以便显示预览并启动视频流。

为什么不直接调用Camera.open()而不是selectAndOpenCamera()?Camera.open() 只会返回设备上的第一个后置摄像头。为了支持只有前置摄像头的平板电脑,我们检查所有摄像头,并返回第一个后置摄像头或找到的任何前置摄像头。另请注意,前置摄像头设置为flipHorizontal true 。这对于正确查看它们是必需的。

private Camera selectAndOpenCamera() {
    Camera.CameraInfo info = new Camera.CameraInfo();
    int numberOfCameras = Camera.getNumberOfCameras();
    int selected = -1;

    for (int i = 0; i < numberOfCameras; i++) {
        Camera.getCameraInfo(i, info);

        if( info.facing == Camera.CameraInfo.CAMERA_FACING_BACK ) {
            selected = i;
            flipHorizontal = false;
            break;
        } else {
            // default to a front facing camera if a back facing one can't be found
            selected = i;
           flipHorizontal = true;
       }
    }

    if( selected == -1 ) {
        dialogNoCamera();
        return null; // won't ever be called
    } else {
        return Camera.open(selected);
    }
}

相机预览视图

CameraPreview.java的任务是“安抚”Android并“显示”相机预览,以便它开始流式传输。Android要求无论如何都要显示相机预览。

CameraPreview可以显示预览或通过将其变得非常小来隐藏它。它也很聪明,可以调整显示尺寸,以保持原始相机图像的纵横比。为简洁起见,下面显示了CameraPreview的骨架。有关详细信息,请参见代码。

public class CameraPreview extends ViewGroup implements SurfaceHolder.Callback {
 
    CameraPreview(Context context, Camera.PreviewCallback previewCallback, boolean hidden ) {
        // provide context, camera callback function and specify if the preview should be hidden
        ...
 
        // Create the surface for displaying the preview
        mSurfaceView = new SurfaceView(context);
        addView(mSurfaceView);
 
        // Install a SurfaceHolder.Callback so we get notified when the
        // underlying surface is created and destroyed.
        mHolder = mSurfaceView.getHolder();
        mHolder.addCallback(this);
        // deprecated setting, but required on Android versions prior to 3.0
        mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
    }
 
    public void setCamera(Camera camera) {
        ...
        if (mCamera != null) {
            // Without calling startPreview() here the video will not
            // wake up under certain conditions
            startPreview();
            requestLayout();
        }
    }
 
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        // adjusts the setMeasuredDimension to hide the preview 
        // or ensure that has the correct aspect ratio
        ...
    }
 
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // adjust the size of the layout so that the aspect ratio is maintained
        ...
    }

    public void surfaceCreated(SurfaceHolder holder) {
        startPreview();
    }

    protected void startPreview() {
        mCamera.setPreviewDisplay(mHolder);
        mCamera.setPreviewCallback(previewCallback);
        mCamera.startPreview();
  }
}

处理相机预览

每次相机捕获新帧时,都会调用下面的函数。该函数在VideoActivity中定义,但将引用传递给CameraPreview,因为它处理预览的初始化。为了避免造成积压,函数中执行的处理量保持在最低限度。

/**
 * Called each time a new image arrives in the data stream.
 */
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
 
    // convert from NV21 format into gray scale
    synchronized (lockGray) {
        ConvertNV21.nv21ToGray(bytes,gray1.width,gray1.height,gray1);
    }
 
    // Can only do trivial amounts of image processing inside this function or else bad stuff happens.
    // To work around this issue most of the processing has been pushed onto a thread and the call below
    // tells the thread to wake up and process another image
    thread.interrupt();
}

onPreviewFrame()中的最后一行调用thread.interrupt(),这将唤醒图像处理线程,请参见下一个代码块。请注意,为了避免onPreviewFrame()run()同时操作同一图像数据,已采取谨慎措施,因为它们在不同的线程中运行。

@Override
public void run() {
    while( !stopRequested ) {
 
        // Sleep until it has been told to wake up
        synchronized ( Thread.currentThread() ) {
            try {
                wait();
            } catch (InterruptedException ignored) {}
        }
 
        // process the most recently converted image by swapping image buffered
        synchronized (lockGray) {
            ImageUInt8 tmp = gray1;
            gray1 = gray2;
            gray2 = tmp;
        }

       if( flipHorizontal )
           GImageMiscOps.flipHorizontal(gray2);

        // process the image and compute its gradient
        gradient.process(gray2,derivX,derivY);
 
        // render the output in a synthetic color image
        synchronized ( lockOutput ) {
            VisualizeImageData.colorizeGradient(derivX,derivY,-1,output,storage);
        }
        mDraw.postInvalidate();
    }
    running = false;
}

如上所述,所有更耗时的图像处理操作都在此线程中完成。本示例中的计算实际上很少,但它们是为了演示最佳实践而在自己的线程中完成的。图像处理完成后,它将通过调用mDraw.postInvalidate()来通知GUI应该更新显示。然后GUI线程将唤醒并在相机预览之上绘制我们的图像。

这个函数也是BoofCV工作的地方。Gradient计算图像梯度,如上所示,并已在前面声明。在计算了图像梯度之后,使用BoofCV的VisualizeImageData类对其进行可视化。这就是本示例中BoofCV的内容。

ImageGradient<ImageUInt8,ImageSInt16> gradient =
     FactoryDerivative.three(ImageUInt8.class, ImageSInt16.class);

可视化显示

预览处理完毕后,将显示结果。上面讨论的线程更新了将在下面的视图中显示的Bitmap图像“output”。请注意,线程如何小心地避免在读/写“output”时互相干扰。

**
 * Draws on top of the video stream for visualizing computer vision results
 */
private class Visualization extends SurfaceView {
 
    Activity activity;
 
    public Visualization(Activity context ) {
        super(context);
        this.activity = context;
 
        // This call is necessary, or else the
        // draw method will not be called.
        setWillNotDraw(false);
    }
 
    @Override
    protected void onDraw(Canvas canvas){
 
        synchronized ( lockOutput ) {
            int w = canvas.getWidth();
            int h = canvas.getHeight();
 
            // fill the window and center it
            double scaleX = w/(double)output.getWidth();
            double scaleY = h/(double)output.getHeight();
 
            double scale = Math.min(scaleX,scaleY);
            double tranX = (w-scale*output.getWidth())/2;
            double tranY = (h-scale*output.getHeight())/2;
 
            canvas.translate((float)tranX,(float)tranY);
            canvas.scale((float)scale,(float)scale);
 
            // draw the image
            canvas.drawBitmap(output,0,0,null);
        }
    }
}

结论

现在您知道如何使用BoofCV在Android平台上通过视频流执行计算机视觉!如果您有任何问题或意见,请告诉我。

© . All rights reserved.