четверг, 10 мая 2012 г.

Работа с камерой в Android-приложении


За что особенно люблю Andriod - так это за обилие аппаратных "плюшек". Тут тебе и микрофон/динамик и gps и wifi, тут и фото/видео камера и так далее... В результате способов применений у этого маленького девайса, а значит простора для программиста, который под него пишет - очень много.
Я уже писал о работе с микрофоном в Android-приложениях, сегодня удостоим внимания камеру. Примеров того, как реализовать простое приложения для фотографирования достаточно много, поэтому в этой статье я покажу кроме всего прочего, как работать с полученной фотографией, в частности как её обрезать. Наше простое приложение может не только сфотографировать вашего знакомого, но и выделить его лицо из кадра, например для сохранения в списке контактов.


Готовим камеру и управляем предварительным просмотром

Для работы с камерой в Android нам понадобится создать объект класса Camera. Одновременно с устройством может работать только одно приложение, поэтому камеру нужно захватить как можно позже (в методе onResume нашего Activity) и отдать как можно раньше (в onPause). Инициализация камеры выполняется методом camera = Camera.open(), освобождение - методом camera.release().

Чтобы навести камеру на "цель" нам потребуется включить предпросмотр методом camera.startPreview(), а чтобы было где его показывать нам потребуется SurfaceView. Чтобы направить результат предварительного просмотра камеры в наш SurfaceView, мы получаем из него SurfaceHolder и "отдаём" его камере: camera.setPreviewDisplay(holder). Делать это лучше в callback-методе surfaceCreated, который буде вызван, когда наш SurfaceView будет готов принимать картинку. Чтобы иметь возможность "поймать" событие surfaceCreated, реализуем в нашем Activity интерфейс SurfaceHolder.Callback. В этом же методе подгоняем размеры изображения с камеры под размеры нашего экрана и включаем предварительный просмотр методом camera.startPreview(). Останавливаем просмотр перед освобождением камеры. Также просмотр нужно повторно включить после фотографирования.

Получаем снимок

Чтобы получить фото с камеры самый простой способ - вызвать метод camera.takePicture(null, null, null, null). Последний параметр можно сделать не null, а передать туда реализацию интерфейса Camera.PictureCallback, и в методе onPictureTaken обработать сохранение полученной фотографии.
Это можно сделать по нажатию кнопки, которую мы тоже должны разместить на экране. Но можно поступить и хитрее. Большинство Android-устройств имеют сейчас камеру, которая может выполнять автофокусировку. Поэтому мы по нажатию кнопки выполняем camera.autoFocus, передавая в этот метод inner-класс, реализующий Camera.AutoFocusCallback. А в методе onAutoFocus этого класса, когда автофокус выполнился, уже, собственно, фотографируем.

Выбираем область для обрезки

Теперь о интересном: из вот этих исходников я вытащил (и немного подправил) реализацию выбора области для обрезки фотографии. Принцип тут следующий: используя FrameLayout, размещаем над нашим SurfaceView объект класса ViewfinderView, наследующий View. В этом классе мы переопредляем метод onDraw, чтобы нарисовать полупрозрачный фон и прозрачный прямоугольник по заданным координатам углов. Вот код этого класса:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.Display;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
 
public final class ViewfinderView extends View implements View.OnTouchListener {
 
 private final Paint paint;
 private final int maskColor;
 private final int frameColor;
 private final int cornerColor;
 private Rect frame;
 private Point screenResolution;
 
 private int lastX, lastY;
 
 private static final int MIN_FRAME_WIDTH = 50; // originally 240
 private static final int MIN_FRAME_HEIGHT = 20; // originally 240
 private static final int MAX_FRAME_WIDTH = 800; // originally 480
 private static final int MAX_FRAME_HEIGHT = 600; // originally 360
 
 public ViewfinderView(Context context) {
  super(context);
 
  paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  maskColor = Color.argb(200, 100, 100, 100);
  frameColor = Color.LTGRAY;
  cornerColor = Color.WHITE;
  WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
     Display display = manager.getDefaultDisplay();
     int width = display.getWidth();
     int height = display.getHeight();
 
     screenResolution = new Point(width, height);
     calcFramingRect();
 }
 
 public Rect getFramingRect() {
  return frame;
 }
 
 private void adjustFramingRect(int deltaWidth, int deltaHeight,
   Point screenResolution) {
 
  // Set maximum and minimum sizes
  if ((frame.width() + deltaWidth > screenResolution.x - 4)
    || (frame.width() + deltaWidth < 50)) {
   deltaWidth = 0;
  }
  if ((frame.height() + deltaHeight > screenResolution.y - 4)
    || (frame.height() + deltaHeight < 50)) {
   deltaHeight = 0;
  }
 
  int newWidth = frame.width() + deltaWidth;
  int newHeight = frame.height() + deltaHeight;
  int leftOffset = (screenResolution.x - newWidth) / 2;
  int topOffset = (screenResolution.y - newHeight) / 2;
  frame = new Rect(leftOffset, topOffset, leftOffset + newWidth,
    topOffset + newHeight);
 }
 
 @Override
 public void onDraw(Canvas canvas) {
  if (frame == null) {
   return;
  }
  int width = canvas.getWidth();
  int height = canvas.getHeight();
 
  // Draw the exterior (i.e. outside the framing rect) darkened
  paint.setColor(maskColor);
  canvas.drawRect(0, 0, width, frame.top, paint);
  canvas.drawRect(0, frame.top, frame.left, frame.bottom + 1, paint);
  canvas.drawRect(frame.right + 1, frame.top, width, frame.bottom + 1,
    paint);
  canvas.drawRect(0, frame.bottom + 1, width, height, paint);
 
  // Draw a two pixel solid border inside the framing rect
  paint.setAlpha(0);
  paint.setStyle(Style.FILL);
  paint.setColor(frameColor);
  canvas.drawRect(frame.left, frame.top, frame.right + 1, frame.top + 2,
    paint);
  canvas.drawRect(frame.left, frame.top + 2, frame.left + 2,
    frame.bottom - 1, paint);
  canvas.drawRect(frame.right - 1, frame.top, frame.right + 1,
    frame.bottom - 1, paint);
  canvas.drawRect(frame.left, frame.bottom - 1, frame.right + 1,
    frame.bottom + 1, paint);
 
  // Draw the framing rect corner UI elements
  paint.setColor(cornerColor);
  canvas.drawRect(frame.left - 15, frame.top - 15, frame.left + 15,
    frame.top, paint);
  canvas.drawRect(frame.left - 15, frame.top, frame.left, frame.top + 15,
    paint);
  canvas.drawRect(frame.right - 15, frame.top - 15, frame.right + 15,
    frame.top, paint);
  canvas.drawRect(frame.right, frame.top - 15, frame.right + 15,
    frame.top + 15, paint);
  canvas.drawRect(frame.left - 15, frame.bottom, frame.left + 15,
    frame.bottom + 15, paint);
  canvas.drawRect(frame.left - 15, frame.bottom - 15, frame.left,
    frame.bottom, paint);
  canvas.drawRect(frame.right - 15, frame.bottom, frame.right + 15,
    frame.bottom + 15, paint);
  canvas.drawRect(frame.right, frame.bottom - 15, frame.right + 15,
    frame.bottom + 15, paint);
 
 }
 
 private void calcFramingRect() {
  if (frame == null) {
   int width = screenResolution.x * 3 / 5;
   if (width < MIN_FRAME_WIDTH) {
    width = MIN_FRAME_WIDTH;
   } else if (width > MAX_FRAME_WIDTH) {
    width = MAX_FRAME_WIDTH;
   }
   int height = screenResolution.y * 1 / 5;
   if (height < MIN_FRAME_HEIGHT) {
    height = MIN_FRAME_HEIGHT;
   } else if (height > MAX_FRAME_HEIGHT) {
    height = MAX_FRAME_HEIGHT;
   }
   int leftOffset = (screenResolution.x - width) / 2;
   int topOffset = (screenResolution.y - height) / 2;
   frame = new Rect(leftOffset, topOffset, leftOffset + width,
     topOffset + height);
  }
 }
 
 @Override
 public boolean onTouch(View v, MotionEvent event) {
  switch (event.getAction()) {
  case MotionEvent.ACTION_DOWN:
   lastX = -1;
   lastY = -1;
   return true;
  case MotionEvent.ACTION_MOVE:
   int currentX = (int) event.getX();
   int currentY = (int) event.getY();
 
   try {
    Rect rect = getFramingRect();
 
    final int BUFFER = 50;
    final int BIG_BUFFER = 60;
    if (lastX >= 0) {
     if (((currentX >= rect.left - BIG_BUFFER && currentX <= rect.left
       + BIG_BUFFER) || (lastX >= rect.left - BIG_BUFFER && lastX <= rect.left
       + BIG_BUFFER))
       && ((currentY <= rect.top + BIG_BUFFER && currentY >= rect.top
         - BIG_BUFFER) || (lastY <= rect.top
         + BIG_BUFFER && lastY >= rect.top
         - BIG_BUFFER))) {
      adjustFramingRect(2 * (lastX - currentX),
        2 * (lastY - currentY), screenResolution);
     } else if (((currentX >= rect.right - BIG_BUFFER && currentX <= rect.right
       + BIG_BUFFER) || (lastX >= rect.right - BIG_BUFFER && lastX <= rect.right
       + BIG_BUFFER))
       && ((currentY <= rect.top + BIG_BUFFER && currentY >= rect.top
         - BIG_BUFFER) || (lastY <= rect.top
         + BIG_BUFFER && lastY >= rect.top
         - BIG_BUFFER))) {
      adjustFramingRect(2 * (currentX - lastX),
        2 * (lastY - currentY), screenResolution);
     } else if (((currentX >= rect.left - BIG_BUFFER && currentX <= rect.left
       + BIG_BUFFER) || (lastX >= rect.left - BIG_BUFFER && lastX <= rect.left
       + BIG_BUFFER))
       && ((currentY <= rect.bottom + BIG_BUFFER && currentY >= rect.bottom
         - BIG_BUFFER) || (lastY <= rect.bottom
         + BIG_BUFFER && lastY >= rect.bottom
         - BIG_BUFFER))) {
      adjustFramingRect(2 * (lastX - currentX),
        2 * (currentY - lastY), screenResolution);
     } else if (((currentX >= rect.right - BIG_BUFFER && currentX <= rect.right
       + BIG_BUFFER) || (lastX >= rect.right - BIG_BUFFER && lastX <= rect.right
       + BIG_BUFFER))
       && ((currentY <= rect.bottom + BIG_BUFFER && currentY >= rect.bottom
         - BIG_BUFFER) || (lastY <= rect.bottom
         + BIG_BUFFER && lastY >= rect.bottom
         - BIG_BUFFER))) {
      adjustFramingRect(2 * (currentX - lastX),
        2 * (currentY - lastY), screenResolution);
     } else if (((currentX >= rect.left - BUFFER && currentX <= rect.left
       + BUFFER) || (lastX >= rect.left - BUFFER && lastX <= rect.left
       + BUFFER))
       && ((currentY <= rect.bottom && currentY >= rect.top) || (lastY <= rect.bottom && lastY >= rect.top))) {
      adjustFramingRect(2 * (lastX - currentX), 0,
        screenResolution);
     } else if (((currentX >= rect.right - BUFFER && currentX <= rect.right
       + BUFFER) || (lastX >= rect.right - BUFFER && lastX <= rect.right
       + BUFFER))
       && ((currentY <= rect.bottom && currentY >= rect.top) || (lastY <= rect.bottom && lastY >= rect.top))) {
      adjustFramingRect(2 * (currentX - lastX), 0,
        screenResolution);
     } else if (((currentY <= rect.top + BUFFER && currentY >= rect.top
       - BUFFER) || (lastY <= rect.top + BUFFER && lastY >= rect.top
       - BUFFER))
       && ((currentX <= rect.right && currentX >= rect.left) || (lastX <= rect.right && lastX >= rect.left))) {
      adjustFramingRect(0, 2 * (lastY - currentY),
        screenResolution);
     } else if (((currentY <= rect.bottom + BUFFER && currentY >= rect.bottom
       - BUFFER) || (lastY <= rect.bottom + BUFFER && lastY >= rect.bottom
       - BUFFER))
       && ((currentX <= rect.right && currentX >= rect.left) || (lastX <= rect.right && lastX >= rect.left))) {
      adjustFramingRect(0, 2 * (currentY - lastY),
        screenResolution);
     }
    }
   } catch (NullPointerException e) {
    e.printStackTrace();
   }
   v.invalidate();
   lastX = currentX;
   lastY = currentY;
   return true;
  case MotionEvent.ACTION_UP:
   lastX = -1;
   lastY = -1;
   return true;
  }
  return false;
 }
}


Как видим, метод adjustFramingRect устанавливает границы прозрачной области. Мы должны вызвать этот метод при "перетаскивании" этих границ пальцем, т.е. ViewfinderView должен реализовать ещё и интерфейс View.OnTouchListener. Получаем границы для обрезки методом getFramingRect

Обрезаем снимок

В оригинальном исходнике обрезалось изображение, которое даёт  предварительный просмотр. Мы тоже могли бы использовать его, если бы реализовали интерфейс Camera.PreviewCallback и передали его камере методом camera.setPreviewCallback. В этом случае в методе onPreviewFrame можно манипулировать с изображением препросмотра (само собой, отдавая его в отдельный поток, чтобы не подвесить этот самый предпросмотр). Нам, в принципе, это не нужно, поэтому обрезать будем уже полученный снимок. Делаем это в методе onPictureTaken, как я уже указывал ранее. Принцип тут очень прост: сохраняем изображение во временный файл, потом читаем его в Bitmap, указывая размеры исходя из размеров выбранной области, а затем сохраняем полученный Bitmap окончательно. Это всё реализовано в нашем методе cropFile.
Итого, получаем код главного Activity, котоый вместе с вышеприведённым кодом и составляет весь наш небольшой проект:

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
 
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.os.Environment;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
 
public class MainActivity extends Activity implements SurfaceHolder.Callback,
  Camera.PictureCallback {
 
 private Camera camera;
 private SurfaceView preview;
 private ViewfinderView vfw;
 
 @Override
 public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 
  setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
  getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
  requestWindowFeature(Window.FEATURE_NO_TITLE);
 
  FrameLayout fl = new FrameLayout(this);
  preview = new SurfaceView(this);
  SurfaceHolder surfaceHolder = preview.getHolder();
  surfaceHolder.addCallback(this);
  surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
  fl.addView(preview, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
 
  vfw = new ViewfinderView(this);
  vfw.setBackgroundColor(Color.TRANSPARENT);
  vfw.setOnTouchListener(vfw);
  fl.addView(vfw, LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
 
  ImageButton shotBtn = new ImageButton(this);
  shotBtn.setImageResource(android.R.drawable.ic_menu_camera);
  shotBtn.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    camera.autoFocus(new Camera.AutoFocusCallback() {
     @Override
     public void onAutoFocus(boolean success, Camera camera) {
      camera.takePicture(null, null, null, MainActivity.this);
     }
    });
   }
  });
  fl.addView(shotBtn, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
 
  setContentView(fl);
 }
 
 @Override
 protected void onResume() {
  super.onResume();
  camera = Camera.open();
 }
 
 @Override
 protected void onPause() {
  super.onPause();
 
  if (camera != null) {
   camera.setPreviewCallback(null);
   camera.stopPreview();
   camera.release();
   camera = null;
  }
 }
 
 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width,
   int height) {
 }
 
 @Override
 public void surfaceCreated(SurfaceHolder holder) {
  try {
   camera.setPreviewDisplay(holder);
  } catch (IOException e) {
   e.printStackTrace();
  }
 
  Size previewSize = camera.getParameters().getPreviewSize();
  float aspect = (float) previewSize.width / previewSize.height;
 
  int previewSurfaceWidth = preview.getWidth();
 
  LayoutParams lp = preview.getLayoutParams();
 
  // здесь корректируем размер отображаемого preview, чтобы не было
  // искажений
 
  camera.setDisplayOrientation(0);
  lp.width = previewSurfaceWidth;
  lp.height = (int) (previewSurfaceWidth / aspect);
 
  preview.setLayoutParams(lp);
  camera.startPreview();
 }
 
 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {
 }
 
 @Override
 public void onPictureTaken(byte[] paramArrayOfByte, Camera camera) {
  try {
   String base = Environment.getExternalStorageDirectory()+"/img/";
   File saveDir = new File(base);
 
   if (!saveDir.exists()) {
    saveDir.mkdirs();
   }
 
   String tmpFile = base+"tmp.jpg";
   FileOutputStream tos = new FileOutputStream(tmpFile);
   tos.write(paramArrayOfByte);
   tos.flush();
   tos.close();
 
   cropFile(new File(tmpFile), 
     new File(base+String.valueOf(System.currentTimeMillis())+".jpg"), 
     vfw.getFramingRect());
 
  } catch (Exception e) {
   e.printStackTrace();
  }
 
  camera.startPreview();
 }
 
 private void cropFile(File in, File out, Rect rect){
     try {
         BitmapFactory.Options o = new BitmapFactory.Options();
 
         Bitmap inb = BitmapFactory.decodeStream(new FileInputStream(in),null,o);
         Bitmap outb = Bitmap.createBitmap(inb,rect.bottom, rect.top, rect.width(), rect.height());
         FileOutputStream os = new FileOutputStream(out);
         outb.compress(Bitmap.CompressFormat.JPEG, 85, os);
         os.flush();
         os.close();
         in.delete();
     } catch (Exception e) {
      e.printStackTrace();
     }
 }
}

Вот, собственно и всё. Не забудьте добавить в AndroidManifest.xml запрос разрешений android.permission.CAMERA и android.permission.WRITE_EXTERNAL_STORAGE





5 комментариев:

  1. Здравствуйте, у Вас есть рецепт, как по нажатию кнопки сделать фотографию и сохранить ее в галерею бес вызова предварительного просмотра? Иначе говоря забрать изображения из матрице камеры в фоновом режиме. Очень заинтересовал это вопрос но негде не могу найти как ... Неделю назад начал изучать Android не судите строго.

    ОтветитьУдалить
  2. у вас ошибка в конце: не rect.bottom, а rect.left

    ОтветитьУдалить
  3. Этот комментарий был удален автором.

    ОтветитьУдалить
  4. Не пойму.

    Rendering Problems Custom view ViewfinderView is not using the 2- or 3-argument View constructors; XML attributes will not work

    Какие аргументы нужно добавить?

    ОтветитьУдалить