Camera的一些总结
前言
闲谈
因为最近公司在做有关摄像头的项目(人脸识别、皮肤测试)。涉及到了usb摄像头和原生的摄像头,我们usb摄像头用的UVC的库来预览的,其实用Camera来预览也是可以的。开发期间查阅了一些资料,也走了很多弯路。所以现在项目上线了,写了这篇文章,希望能够帮到在Camera迷茫的小伙伴们…
Camera和Camera2
Android5.0以前,相机框架是Camera,Android5.0以后Google 引入了一套全新的相机框架 Camera2,相比之下,它更具灵活性,也增加了新的功能,如设置对焦模式、曝光模式、快门等。不过,很多文章里介绍,国内厂商对系统的定制导致对Camera2的支持不尽相同,但是我觉得还是值得一试的,下篇文章我也去探索一下Camera2(所以呢,本文总结的是Camera)。
选择Camera还是Camera2除了跟手机硬件的关系外,还有个需求是是否要适配5.0以下。Camera2是支持5.0+的。
SurfaceView
Surface
我们先看一下Surface
/**
* Handle onto a raw buffer that is being managed by the screen compositor.
*
Surface是用来处理屏幕显示内容合成器所管理的原始缓冲区的。
原始缓冲区用来保存窗口的像素数据。
简单的说Surface对应了一块屏幕缓冲区,每个window对应一个Surface,任何View都要画在Surface的Canvas上。传统的view共享一块屏幕缓冲区。
可以认为Surface用来管理数据的。
SurfaceView
/**
* Provides a dedicated drawing surface embedded inside of a view hierarchy.
* You can control the format of this surface and, if you like, its size; the
* SurfaceView takes care of placing the surface at the correct location on the
* screen
*
SurfaceView提供了嵌入视图层级中的专用surface。你可以控制surface的格式或大小。SurfaceView负责把surface显示在屏幕的正确位置。
SurfaceView是一个View,我们再实际开发中是在布局文件里写的,但是又比较特殊。原因有二:
- 普通的View都是共享一个屏幕缓冲区的(Surface),SurfaceView有单独的Surface。
- 普通的View只能在UI线程更新,SurfaceView没有限制。
SurfaceHolder
SurfaceHolder是个接口,具体的实现在SurfaceView里。实现了很多方法,比如添加回调的方法addCallback(Callback callback)、得到Surface的画布的方法lockCanvas()等等。大概可以说SurfaceHolder用来控制Surface的尺寸、格式与像素以及监听Surface的状态的。
监听Surface的状态回调在SurfaceHolder.Callback里。我们需要在自己去监听这几个回调,实现具体的业务逻辑。
-
surfaceCreated(SurfaceHolder holder);
当Surface被创建后调用
-
surfaceChanged(SurfaceHolder holder, int format, int width, int height);
当Surface的size、format等发生变化的时候调用
-
surfaceDestroyed(SurfaceHolder holder);
当Surface被销毁的时候调用
权限
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
Android6.0+需要动态申请权限,这里不多说了。
Camera的实践
SurfaceView
public class CameraFragment extends BaseFragment implements SurfaceHolder.Callback {
private SurfaceView surfaceView;
private SurfaceHolder surfaceHolder;
private int mCameraId;//摄像头id
private Camera camera;
public static CameraFragment newInstance() {
return new CameraFragment();
}
@Override
public int getLayoutRes() {
return R.layout.fragment_camera;
}
@Override
public void initView(Bundle savedInstanceState) {
surfaceView = findView(R.id.surfaceView);
surfaceHolder=surfaceView.getHolder();
surfaceHolder.addCallback(this);
}
@Override
public void initListener() {
}
@Override
public void initData() {
}
@Override
public void onResume() {
super.onResume();
Logger.d("gxh","onResume");
//startPreview();
}
/**
* 开启预览
*/
private void startPreview() {
//打开摄像头
camera = open();
try {
camera.setPreviewDisplay(surfaceHolder);
camera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打开相机
*
* @return
*/
private Camera open() {
Camera camera;
//获取摄像头数量
int numCameras = Camera.getNumberOfCameras();
if (numCameras == 0) {
Logger.d("gxh","摄像头数量"+numCameras);
return null;
}
int index = 0;
while (index < numCameras) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(index, cameraInfo);
Logger.d("gxh",index+";"+cameraInfo.facing);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
break;
}
index++;
}
if (index < numCameras) {
camera = Camera.open(index);
mCameraId = index;
} else {
camera = Camera.open(0);
mCameraId = 0;
}
return camera;
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
Logger.d("gxh","surfaceCreated");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Logger.d("gxh","surfaceChanged");
if (holder.getSurface() == null) {
return;
}
startPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Logger.d("gxh","surfaceDestroyed");
}
}
打开与预览
/**
* 开启预览
*/
private void startPreview() {
//打开摄像头
Camera camera = open();
try {
camera.setPreviewDisplay(surfaceHolder);
camera.setPreviewCallback(this);
camera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 打开摄像头
*
* @return
*/
private Camera open() {
Camera camera;
//获取摄像头数量
int numCameras = Camera.getNumberOfCameras();
if (numCameras == 0) {
Logger.d("gxh","摄像头数量"+numCameras);
return null;
}
int index = 0;
while (index < numCameras) {
Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
Camera.getCameraInfo(index, cameraInfo);
Logger.d("gxh",index+";"+cameraInfo.facing);
if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
break;
}
index++;
}
if (index < numCameras) {
camera = Camera.open(index);
mCameraId = index;
} else {
camera = Camera.open(0);
mCameraId = 0;
}
return camera;
}
没错,现在我们已经成功打开前置摄像头并预览到画面了,因为我们没有设置Camera的参数配置,所以现在画面方向、比例都有问题。这个先不着急,这里有几个问题需要谈一下:
-
Camera.open(index)
我总有种错觉,觉得打开前置就open(1),打开后置就open(0)。其实不是这样,index指的是numCameras中的第几个。比如魔镜系统只有一个前置摄像头(numCameras为1),打开的话还是open(0)。
-
预览摄像头应该在Surface创建之后。
-
setPreviewDisplay应该在startPreview之前。
-
一定记得要release释放资源。
设置一些参数
private void setupCameraParameters() {
if(parameters==null){
parameters = camera.getParameters();
}
//设置保存的图片格式
parameters.setPictureFormat(PixelFormat.JPEG);
//设置预览的图片格式
parameters.setPreviewFormat(ImageFormat.NV21);
//设置闪光灯模式
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
//设置对焦模式
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
//设置场景模式
parameters.setSceneMode(Camera.Parameters.SCENE_MODE_AUTO);
camera.setParameters(parameters);
}
调整预览的方向
图像的Sensor方向:手机Camera的图像数据都是来自于摄像头硬件的图像传感器(Image Sensor),这个Sensor被固定到手机之后是有一个默认的取景方向的,也就是手机横放Home键朝右这个方向,坐标原点位于手机横放时的左上角。
Camera的预览方向:预览方向默认情况下与图像的Sensor方向一致,所以我们看到的画面方向不正常。Google提供了setDisplayOrientation来让我们调整预览的方向。需要注意的是setDisplayOrientation只改变预览的方向,不改变拍照后图片的方向、回调的数据流方向。
Camera的拍照方向:与图像的Sensor方向一致。
/**
* 设置预览方向
*/
private void setCameraDisplayOrientation() {
int degress=displayOrientation(mActivity);
//如果确定你的应用是竖屏的,可以直接camera.setDisplayOrientation(90);
camera.setDisplayOrientation(degress);
}
private int displayOrientation(Context context) {
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
int rotation = windowManager.getDefaultDisplay().getRotation();
int degrees = 0;
switch (rotation) {
case Surface.ROTATION_0:
degrees = 0;
break;
case Surface.ROTATION_90:
degrees = 90;
break;
case Surface.ROTATION_180:
degrees = 180;
break;
case Surface.ROTATION_270:
degrees = 270;
break;
default:
degrees = 0;
break;
}
int result = 0;
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(mCameraId, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
result = (info.orientation + degrees) % 360;
result = (360 - result) % 360;
} else {
result = (info.orientation - degrees + 360) % 360;
}
Logger.d("gxh",degrees+";"+result+";"+info.orientation);
return result;
}
拍照
camera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
if (data == null) {
return;
}
String dirPath = mActivity.getExternalCacheDir().getAbsolutePath();
final String picturePath = dirPath + File.separator + System.currentTimeMillis() + ".jpg";
try {
FileOutputStream fileOutputStream = new FileOutputStream(new File(picturePath));
fileOutputStream.write(data);
fileOutputStream.close();
} catch (Exception error) {
} finally {
camera.startPreview();
}
}
});
拍照可以使用takePicture方法,拍照完要重新预览,拍照时有瞬间画面停顿。如果想不断获取图片,这个方法显然是不可取的,可以在数据流回调的方法里生成bitmap。
//视频流的回调
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
Logger.d("gxh", "onPreviewFrame");
//一般情况下,这个方法就是回调在UI线程的,所以如果想做耗时操作,需要自己开启子线程。
}
可以发现生成的图片方向还是不正确的,因为照片保存的方向还是由Camera的图像Sensor决定的,我们需要进行调整。可以拍完照片后进行旋转处理,也可以使用setRotation来设置角度。
private void setPictureOrientation() {
orientationEventListener = new MyOrientationEventListener(mActivity);
orientationEventListener.enable();
}
private class MyOrientationEventListener extends OrientationEventListener {
public MyOrientationEventListener(Context context) {
super(context);
}
@Override
public void onOrientationChanged(int orientation) {
Logger.d("gxh", "orientation:" + orientation);
if (orientation == OrientationEventListener.ORIENTATION_UNKNOWN)
return;
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(mCameraId, info);
orientation = (orientation + 45) / 90 * 90;
int rotation = 0;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
} else {
rotation = (info.orientation + orientation) % 360;
}
parameters.setRotation(rotation);
camera.setParameters(parameters);
}
}
这里值得提一下的是,前置摄像头拍的照片可能和预览的左右不一致,也就是镜像。这个好像是正常的,但是根据习惯来说可能感觉怪怪的吧。所以手机自带相机一般有个设置让我们自己决定到底拍照的图片什么样子。
那么自定义相机怎么办?如果我们就是不想要镜像效果呢?那只能对生成的图片修改了。
Matrix matrix = new Matrix();
matrix.preRotate(360 - displayOrientation);
//前置摄像头生成图片 左右镜像问题
matrix.postScale(-1, 1);
return Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp
.getHeight(), matrix, true);
预览尺寸
parameters.setPreviewSize(x, y);
我们可以用上述方法设置预览尺寸,但是参数不能随便写,如果Camera不支持我们设置的参数恐怕就要崩溃了…而又为了不使预览拉伸变形,我们应该找出Camera支持的预览尺寸里最接近我们SurfaceView宽高比例的。
public final class CameraPreviewUtils {
private static final String TAG = CameraPreviewUtils.class.getSimpleName();
private static final int MIN_PREVIEW_PIXELS = 640 * 480;
private static final int MAX_PREVIEW_PIXELS = 1280 * 720;
/**
* 获取最好的预览尺寸
* @param parameters
* @param screenResolution SurfaceView的宽高
* @return
*/
public static Point getBestPreview(Camera.Parameters parameters, Point screenResolution) {
List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
if (rawSupportedSizes == null) {
Camera.Size defaultSize = parameters.getPreviewSize();
return new Point(defaultSize.width, defaultSize.height);
}
List<Camera.Size> supportedPictureSizes = new ArrayList<Camera.Size>(rawSupportedSizes);
Collections.sort(supportedPictureSizes, new Comparator<Camera.Size>() {
@Override
public int compare(Camera.Size a, Camera.Size b) {
int aPixels = a.height * a.width;
int bPixels = b.height * b.width;
if (bPixels < aPixels) {
return -1;
}
if (bPixels > aPixels) {
return 1;
}
return 0;
}
});
final double screenAspectRatio = (screenResolution.x > screenResolution.y) ?
((double) screenResolution.x / (double) screenResolution.y) :
((double) screenResolution.y / (double) screenResolution.x);
Camera.Size selectedSize = null;
double selectedMinus = -1;
double selectedPreviewSize = 0;
Iterator<Camera.Size> it = supportedPictureSizes.iterator();
while (it.hasNext()) {
Camera.Size supportedPreviewSize = it.next();
int realWidth = supportedPreviewSize.width;
int realHeight = supportedPreviewSize.height;
Log.d("gxh", "preview size " + realWidth + " " + realHeight);
if (realWidth * realHeight < MIN_PREVIEW_PIXELS) {
it.remove();
continue;
} else if (realWidth * realHeight > MAX_PREVIEW_PIXELS) {
it.remove();
continue;
} else {
double aRatio = (supportedPreviewSize.width > supportedPreviewSize.height) ?
((double) supportedPreviewSize.width / (double) supportedPreviewSize.height) :
((double) supportedPreviewSize.height / (double) supportedPreviewSize.width);
double minus = Math.abs(aRatio - screenAspectRatio);
boolean selectedFlag = false;
if ((selectedMinus == -1 && minus <= 0.25f)
|| (selectedMinus >= minus && minus <= 0.25f)) {
selectedFlag = true;
}
if (selectedFlag) {
selectedMinus = minus;
selectedSize = supportedPreviewSize;
selectedPreviewSize = realWidth * realHeight;
}
}
}
if (selectedSize != null) {
Camera.Size preview = selectedSize;
return new Point(preview.width, preview.height);
} else {
Camera.Size defaultSize = parameters.getPreviewSize();
return new Point(defaultSize.width, defaultSize.height);
}
}
}
人脸识别
这个小节本来不打算写的,因为我也没打算展开写。但是还是谈几句吧…原生的api提供了人脸识别的方法。
一种是在Camera开启人脸识别,直接回调结果。但是要注意startFaceDetection()需要在startPreview()之后。
private void faceDetch() {
camera.startFaceDetection();
camera.setFaceDetectionListener(new Camera.FaceDetectionListener() {
@Override
public void onFaceDetection(Camera.Face[] faces, Camera camera) {
Logger.d("gxh", "onFaceDetection:" + faces.length + ";" + Thread.currentThread().getName());
if (faces == null || faces.length == 0) {
return;
}
Camera.Face face = faces[0];
//坐标转换
Matrix matrix = new Matrix();
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(mCameraId, info);
// Need mirror for front camera.
boolean mirror = (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT);
matrix.setScale(mirror ? -1 : 1, 1);
// This is the value for android.hardware.Camera.setDisplayOrientation.
matrix.postRotate(displayOrientation(mActivity));
// Camera driver coordinates range from (-1000, -1000) to (1000, 1000).
// UI coordinates range from (0, 0) to (width, height).
matrix.postScale(surfaceWidth / 2000f, surfaceHeight / 2000f);
matrix.postTranslate(surfaceWidth / 2f, surfaceHeight / 2f);
RectF srcRect=new RectF(face.rect);
RectF dstRect =new RectF(0f, 0f, 0f, 0f);
matrix.mapRect(dstRect,srcRect);
faceView.drawFace(dstRect);
}
});
}
另一种则是直接检测Bitmap。所以可以直接检测本地图片,也可以将Camera回调中onPreviewFrame(byte[] data, Camera camera)的data转为bitmap来检测。
FaceDetector faceDetector = new FaceDetector(bm.getWidth(), bm.getHeight(), MAX_FACE_NUM);
FaceDetector.Face[] faces = new FaceDetector.Face[MAX_FACE_NUM];
realFaceNum = faceDetector.findFaces(bm, faces);
不管是原生api的哪种方法,看起来效果并不是特别的好,要是商用的话恐怕不能满足。所以,可能要使用诸如虹软、百度、中科等的sdk了(绝对不是打广告!)。具体不多少了,只是基本上都是涉及到了onPreviewFrame(byte[] data, Camera camera)这个回调中的data数据。
释放
private void stopPreview() {
if (camera != null) {
try {
camera.setErrorCallback(null);
camera.setPreviewCallback(null);
camera.stopFaceDetection();
camera.stopPreview();
} catch (RuntimeException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
} finally {
camera.release();
camera = null;
}
}
}
Camera与子线程
我们知道,Camera的打开是耗时的,我们可以放在子线程里。那么我们既然要在onPreviewFrame(byte[] data, Camera camera)回调中进行人脸识别比对等等,还要自己再开子线程,我们想这个回调就是子线程回调的要怎么办?
假如我们在子线程打开的Camera,那么onPreviewFrame(byte[] data, Camera camera)就会在子线程?恐怕想得美。
public class HandlerThreadActivity2 extends Activity implements Callback {
static final String TAG = "jason";
Camera mCamera;
SurfaceView surfaceView;
SurfaceHolder surfaceHolder;
byte[] buffers;
HandlerThread mHandlerThread = new HandlerThread("my_handlerthread");
Handler subHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_thread2);
surfaceView = (SurfaceView) findViewById(R.id.surface_view);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(this);
}
class MyTask implements Runnable, PreviewCallback{
@Override
public void run() {
//打开相机
//子线程中打开
Log.d("jason", Thread.currentThread().getName() + "_open");
mCamera = Camera.open(CameraInfo.CAMERA_FACING_BACK);
try {
mCamera.setPreviewDisplay(surfaceHolder);
} catch (IOException e) {
e.printStackTrace();
}
Camera.Parameters parameters = mCamera.getParameters();
//设置相机参数
parameters.setPreviewSize(480, 320); //预览画面宽高
mCamera.setParameters(parameters);
//获取预览图像数据
buffers = new byte[480 * 320 * 4];
mCamera.addCallbackBuffer(buffers);
mCamera.setPreviewCallbackWithBuffer(this);
mCamera.startPreview();
Log.d(TAG, Thread.currentThread().getName()+ "_run");
}
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
if(mCamera != null){
mCamera.addCallbackBuffer(buffers);
//编码
Log.d(TAG, Thread.currentThread().getName()+ "_onPreviewFrame");
}
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
mHandlerThread.start();
subHandler = new Handler(mHandlerThread.getLooper());
subHandler.post(new MyTask());
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
Camera维护了一个EventHandler。 Handler.handleMessage的执行一定在它的Looper所在线程中。 onPreviewFrame的执行在Camera所持有的Looper所在线程中执行。
结束语
- 参考文章
Android开发实践:掌握Camera的预览方向和拍照方向
- 文章同步发布在黑白了落夜