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






4.90/5 (30投票s)
一个关于如何在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上的视频步骤
- 打开并配置相机
- 创建
SurfaceHolder
以显示相机预览 - 在相机预览视图之上添加视图以供显示
- 为相机的预览提供监听器
- 启动相机预览
- 在单独的线程中执行耗时的计算
- 渲染结果
在访问相机之前,必须先在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.java。onCreate()
和onResume()
中发生了几个重要的活动。
- 创建并配置用于显示相机预览的视图。
- 添加用于渲染输出的视图。
- 打开并配置相机。
@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平台上通过视频流执行计算机视觉!如果您有任何问题或意见,请告诉我。