Android:如何在已经显示 Camara 预览的 Surfaceview 上绘图

Android: How to draw on a Surfaceview which already is displaying a Camara Preview

我正在尝试编写一个应用程序,该应用程序必须在手机 phone 的屏幕上显示前置摄像头拍摄的内容 [该应用程序不是 recording/saving 内存中的任何内容phone]。此外,如果一张脸被拍摄(并检测到),它必须显示为矩形。

为此,我使用:

  1. A Surfaceview 显示前置摄像头正在拍摄的内容。
  2. A FaceDetectionListener 用于检测相机输入中的人脸。

到目前为止,应用程序可以正确显示前置摄像头拍摄的内容。它还可以正确检测人脸。但是我无法在检测到的人脸周围绘制边界矩形。

这里有一些片段展示了我是如何尝试解决这个任务的。

Activity:

@SuppressLint("InflateParams")
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)

public class FaceDetectorTutorial extends Activity {

   MySurface mMySurface;
   private SurfaceView surfaceView;

   public void onCreate(Bundle savedInstanceState) {

      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_face_detector_tutorial);
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);

      surfaceView = (SurfaceView)findViewById(R.id.camPreview);

      mMySurface = new MySurface(this, surfaceView);

  }     

}    

MySurface class:

class MySurface extends SurfaceView implements SurfaceHolder.Callback {  

  Paint paint = new Paint();

  public Camera camera;

  String Tag = "Log: ";


  private SurfaceHolder surfaceHolder;
  boolean preview = false;

 ////////// CLASS CONSTRUCTOR   //////////
 MySurface(Context context, SurfaceView msurfaceView) {
      super(context);

      paint.setColor(Color.RED);
      paint.setStrokeWidth(3);

      surfaceHolder = msurfaceView.getHolder();
      surfaceHolder.addCallback(this);  
      surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

 }


 /////////  FACE DETECTION LISTENER /////////
 @SuppressLint("NewApi")
   FaceDetectionListener faceDetectionListener = new FaceDetectionListener(){

        @Override
        public void onFaceDetection(Face[] faces, Camera camera) {

            if (faces.length > 0){

                //Just for the first one detected
                Rect Boundary = faces[0].rect;

                System.out.println(Boundary);

                tryDrawing(Boundary);

            }


 }};//End of FaceDetectionListener

 ////////// SURFACE METHODS ////////
 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width,int height) {
         // TODO Auto-generated method stub
        if(preview){
               camera.stopFaceDetection();
               camera.stopPreview();
               preview = false;
         }

       if (camera != null){
               try {                
               camera.setPreviewDisplay(surfaceHolder);
               camera.startPreview();            
               camera.startFaceDetection();
               preview = true;
               } catch (IOException e) {
                   // TODO Auto-generated catch block
                  e.printStackTrace();
               }
           }

  }

  @Override
  public void surfaceCreated(SurfaceHolder holder) {
           // TODO Auto-generated method stub

        int cameraId = -1;      
        int numberOfCameras = camera.getNumberOfCameras();

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

            if (info.facing == CameraInfo.CAMERA_FACING_FRONT) {
                    cameraId = i;
                    break;
            }
        }

        camera = Camera.open(cameraId);
        camera.setFaceDetectionListener(faceDetectionListener);

  }

  @Override
  public void surfaceDestroyed(SurfaceHolder holder) {
           // TODO Auto-generated method stub
           camera.stopFaceDetection();
           camera.stopPreview();
           camera.release();
           camera = null;
           preview = false;
 }

 private void tryDrawing(Rect Boundary) {
      Log.i(Tag, "Trying to draw...");

      Canvas canvas = surfaceHolder.lockCanvas();

      if (canvas == null) {

          Log.e(Tag, "Cannot draw onto the canvas as it's null");

      } else {

          drawMyStuff(canvas,Boundary);
          surfaceHolder.unlockCanvasAndPost(canvas);
      }
  }

  private void drawMyStuff(final Canvas canvas, Rect Boundary) {

      canvas.drawRect(Boundary.left, Boundary.top, Boundary.right, Boundary.bottom, paint);

      Log.i(Tag, "Drawing...");

  }

}  

布局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:orientation="vertical" >

    <SurfaceView
        android:id="@+id/camPreview"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

从 logcat 我了解到错误是锁定了表面。但是我不明白为什么。

11-23 17:13:51.791: I/System.out(12515): Rect(-187, -495 - 328, 196)
11-23 17:13:51.791: I/Log:(12515): Trying to draw...
11-23 17:13:51.791: E/SurfaceHolder(12515): Exception locking surface
11-23 17:13:51.791: E/SurfaceHolder(12515): java.lang.IllegalArgumentException
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.view.Surface.nativeLockCanvas(Native Method)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.view.Surface.lockCanvas(Surface.java:452)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.view.SurfaceView.internalLockCanvas(SurfaceView.java:781)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.view.SurfaceView.lockCanvas(SurfaceView.java:757)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at com.example.facedetectiontutorial.MySurface.tryDrawing(FaceDetectorTutorial.java:175)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at com.example.facedetectiontutorial.MySurface.access[=14=](FaceDetectorTutorial.java:172)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at com.example.facedetectiontutorial.MySurface.onFaceDetection(FaceDetectorTutorial.java:109)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.hardware.Camera$EventHandler.handleMessage(Camera.java:815)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.os.Handler.dispatchMessage(Handler.java:99)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.os.Looper.loop(Looper.java:137)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at android.app.ActivityThread.main(ActivityThread.java:5041)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at java.lang.reflect.Method.invokeNative(Native Method)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at java.lang.reflect.Method.invoke(Method.java:511)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:793)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:560)
11-23 17:13:51.791: E/SurfaceHolder(12515):     at dalvik.system.NativeStart.main(Native Method)
11-23 17:13:51.893: E/Log:(12515): Cannot draw onto the canvas as it's null

我的解决方案基于以下问题的公认答案:

Android drawing on surfaceview and canvas

但我好像用错了。谁能告诉我应该更改什么?

如果您使用 SurfaceView 显示相机预览,则无法绘制到它。 lockCanvas 如您所见,将会失败。

我建议您使用两个单独的视图。一个预览相机的 SurfaceView,和第二个视图(SurfaceView 或只是一个自定义视图),它在预览的顶部绘制。您可以在 FrameLayoutRelativeLayout.

中同时拥有这两个视图

你不能。 Surface 是生产者-消费者对的生产者端,一次只能有一个生产者。您可以提供相机帧,使用 Canvas 在软件中渲染,或使用 OpenGL 在硬件中渲染,但不能在单个 Surface 上混合使用它们。

你有几个选择。一种方法是使用与第一个重叠的第二个 SurfaceView(通过 FrameLayout)。为此,您必须为第二个表面指定 Z 顺序——如果不这样做,系统将看到您有两个表面试图占据相同的 space,并且只有一个会获胜。使用 setZOrderMediaOverlay() method to place the new Surface in front of the camera preview but behind the View UI. You also need to change the Surface's color format from the default RGB565 to one that supports transparency; setColorFormat(TRANSLUCENT)RGB8888 都可以。确保使用适当的颜色传输模式将 Surface 擦除为透明黑色。

另一种方法是使用 custom View。这样效率更高,因为它不涉及创建单独的 Surface(您在 View UI 层上绘图),并且 Canvas 渲染可以硬件加速。最方便使用的视图是 SurfaceView 的一部分——无需更改布局。如果您的代码是 SurfaceView 的子类,那么您已经完成了一半。

有关多个重叠 SurfaceView 的示例,请参阅 Grafika's "multi-surface test" Activity. If you want to understand more about Android's graphics system, see the architecture doc