Camera 简介

讲解编解码之前先对Camera进行简单的介绍,本篇介绍完之后只能保证小白会用Camera预览画面,其他的Camera知识会后续讲解。

image.png

考虑兼容性依然介绍Camera,目录为android.hardware.Camera,可以看到从api21开始这个类已经被标记为过时,谷歌大大推荐使用android.hardware.Camera2,但是Camera2要从api21才支持,但现在大部分开发还必须以4.+为基础进行开发,所以也只能不听google的坚持使用Camera了。

借助Camera可以利用设备的相机来预览画面,拍照和拍视频。要使用Camera需要在Manifest文件中添加Manifest.permission.CAMERA 权限同时如果要进行自动对焦,还需要特性声明。

完整地权限和特性声明设置为:

//存储权限也需要

注意:如果你只是想简单的实现拍照和拍照视频功能,可以利用Intent打开系统提供的功能。MediaStore.ACTION_IMAGE_CAPTURE 拍摄照片;MediaStore.ACTION_VIDEO_CAPTURE 拍摄视频;

利用Camera想要拍照,需要如下使用步骤:

1 利用open(int)获取Camera实例

2 利用getParameters()获取默认设置,如果需要利用setParameters(Camera.Parameters)进行参数设置

3 利用setDisplayOrientation(int)函数设置正确的预览方向

4 想要预览,需要配合SurfaceView,利用setPreviewDisplay(SurfaceHolder)设置SurfaceView的SurfaceHolder用于预览。

5 调用startPreview()开始预览,拍照之前必须已经开始预览

6 takePicture 拍摄照片

7 调用takePickture后预览会停止,想要继续预览需要调用startPreview()函数

8 调用stopPreview()停止预览

9 调用release()释放资源,为了节省资源在Activity.onPause是调用停止预览,在onResume是开始预览。

2 打开相机

检查是否有相机

/** Check if this device has a camera */

private boolean checkCameraHardware(Context context) {

if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA)){

// this device has a camera

return true;

} else {

// no camera on this device

return false;

}

}

获取相机数目:

Camera.getNumberOfCameras()

open或open(int)打开相机

open开启默认相机(后置相机),open(int)开启特定相机,打开相机是可能失败的,所以一定要检查相机是否打开成功,判断Camera是否为null, mCamera = Camera.open(cameraID);

后置相机和前置相机id常量:CameraInfo.CAMERA_FACING_BACK, CameraInfo.CAMERA_FACING_FRONT

打开特定相机Camera.open(cameraid)。

Camera.getCameraInfo() 可以获取CameraInfo,可以知道相机是位于前面还是后面。

public static class CameraInfo {

public static final int CAMERA_FACING_BACK = 0;

public static final int CAMERA_FACING_FRONT = 1;

/**

* 这个值就是标明相机是前置还是后置

* CAMERA_FACING_BACK or CAMERA_FACING_FRONT.

*/

public int facing;

public int orientation;

};

3 相机预览方向设置

相机的方向(0,90,180,270)有四种,预览需要设置正确的方向和尺寸,预览的图片才不会变形,可以利用Camera.setDisplayOrientaion(int)设置相机的预览方向。可设置的参数有0,90,180,270,默认为0,是指手机的左侧为摄像头顶部画面,所以相机默认为横屏,如果要竖屏预览,就需要设置90度。

如果想让相机跟随设备方向变化,改变预览的方向,需要结合相机已经旋转的角度和屏幕旋转的角度以及相机的前后(前置相机和后置相机预览界面是不同的,前置有镜面效果),最好固定Activity的方向。

特别注意:

设置预览角度,setDisplayOrientation本身只能改变预览的角度previewFrameCallback以及拍摄出来的照片是不会发生改变的,拍摄出来的照片角度依旧不正常的,所以拍摄最后得到的照片需要自行处理(旋转)。

在布局发生改变时要重新设置相机预览方向。

一般设置相机方向的通用方法:

public static int calculateCameraPreviewOrientation(Activity activity) {

Camera.CameraInfo info = new Camera.CameraInfo();

Camera.getCameraInfo(mCameraID, info);

int rotation = activity.getWindowManager().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;

}

int result;

if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {

result = (info.orientation + degrees) % 360;

result = (360 - result) % 360;

} else {

result = (info.orientation - degrees + 360) % 360;

}

return result;

}

最终算出来的值result是0,90,180,270中的一个,要获取info.orientation的目的是不知道相机默认角度是多少。

4 设置预览大小

相机的宽度和高度跟屏幕坐标不一样,相机的宽高和手机屏幕的宽高是反过来的。例如如果设置展示预览图的SurfaceView的宽高比3:4,选取Camera预览图的尺寸时应该是4:3(因为预览默认对应横向,如果要竖向显示时,就是反过来的。)。

设备上的相机支持的预览大小是确定的,当然也不是只有一种,会有很多种,我们只能从它支持的大小的列表中选取一个最接近我们需要比例的宽高。

为了保证相机预览画面不变形,预览界面大小的设置必须和选取的相机支持的尺寸的宽高的比例相同。

举个例子,获取手机上支持的所有预览尺寸:

Camera.Parameters parameters = camera.getParameters();

parameters.getSupportedPreviewSizes()

结果:

==支持的预览尺寸=宽高= 176 144

==支持的预览尺寸=宽高= 320 240

==支持的预览尺寸=宽高= 352 288

==支持的预览尺寸=宽高= 640 480

==支持的预览尺寸=宽高= 1280 720

==支持的预览尺寸=宽高= 1280 960

==支持的预览尺寸=宽高= 176 144

==支持的预览尺寸=宽高= 320 240

==支持的预览尺寸=宽高= 352 288

==支持的预览尺寸=宽高= 640 480

==支持的预览尺寸=宽高= 1280 720

==支持的预览尺寸=宽高= 1280 960

再次强调:可以看到getSupportedPreviewSizes获取了相机支持的所有预览尺寸,注意一点,屏幕的宽高是按照竖屏获取的,而getSupportedPreviewSizes获得支持的尺寸是按照横屏来说的,也就是说我们上面获取的宽高实际上是反过来的,高是宽,宽是高,不知道大家理解没。

例如选取上面获取到的640:480,其实对应到竖向屏幕上是480:640。,如果我们选取640x480的尺寸,那么预览Camera的SurfaceView的宽高比也必须为3:4,这样预览的画面才不会变形,否则可能导致变形。

expectWidth和expectHeight 分别对应期望的宽和高,是对应相机的宽高,所以如果希望在view中看到的是640*480的预览,则期望宽高应该写入3:4的宽高,也就是把希望的对应view的宽高反过来。

查找最合适尺寸的规则:

找出最合适的尺寸,规则如下:

1.将尺寸按比例分组,找出比例最接近屏幕比例的尺寸组

2.在比例最接近的尺寸组中找出最接近屏幕尺寸且大于屏幕尺寸的尺寸

3.如果没有找到,则忽略2中第二个条件再找一遍,应该是最合适的尺寸了

/**

* 找出最合适的尺寸,规则如下:

* 1.将尺寸按比例分组,找出比例最接近屏幕比例的尺寸组

* 2.在比例最接近的尺寸组中找出最接近屏幕尺寸且大于屏幕尺寸的尺寸

* 3.如果没有找到,则忽略2中第二个条件再找一遍,应该是最合适的尺寸了

*/

private static Camera.Size findProperSize(Point surfaceSize, List sizeList) {

if (surfaceSize.x <= 0 || surfaceSize.y <= 0 || sizeList == null) {

return null;

}

int surfaceWidth = surfaceSize.x;

int surfaceHeight = surfaceSize.y;

List> ratioListList = new ArrayList<>();

for (Camera.Size size : sizeList) {

addRatioList(ratioListList, size);

}

final float surfaceRatio = (float) surfaceWidth / surfaceHeight;

List bestRatioList = null;

float ratioDiff = Float.MAX_VALUE;

for (List ratioList : ratioListList) {

float ratio = (float) ratioList.get(0).width / ratioList.get(0).height;

float newRatioDiff = Math.abs(ratio - surfaceRatio);

if (newRatioDiff < ratioDiff) {

bestRatioList = ratioList;

ratioDiff = newRatioDiff;

}

}

Camera.Size bestSize = null;

int diff = Integer.MAX_VALUE;

assert bestRatioList != null;

for (Camera.Size size : bestRatioList) {

int newDiff = Math.abs(size.width - surfaceWidth) + Math.abs(size.height - surfaceHeight);

if (size.height >= surfaceHeight && newDiff < diff) {

bestSize = size;

diff = newDiff;

}

}

if (bestSize != null) {

return bestSize;

}

diff = Integer.MAX_VALUE;

for (Camera.Size size : bestRatioList) {

int newDiff = Math.abs(size.width - surfaceWidth) + Math.abs(size.height - surfaceHeight);

if (newDiff < diff) {

bestSize = size;

diff = newDiff;

}

}

return bestSize;

}

private static void addRatioList(List> ratioListList, Camera.Size size) {

float ratio = (float) size.width / size.height;

for (List ratioList : ratioListList) {

float mine = (float) ratioList.get(0).width / ratioList.get(0).height;

if (ratio == mine) {

ratioList.add(size);

return;

}

}

List ratioList = new ArrayList<>();

ratioList.add(size);

ratioListList.add(ratioList);

}

设置SurfaceView的宽高为3:4的图像。

设置SurfaceView的宽高为4:3的图像,明显变形了。

5 设置拍摄图片大小

调用camera的takePicture方法后,获得拍照的图像数据,如何设置保存图片的大小,类似设置预览大小,利用parameters.getSupportedPictureSizes()可以获取支持的保存图片的大小,图片尺寸同样只能从支持的列表中选取一个设置。

picturesize和previewsize的宽高比也要保证一致,否则获取的图片会将preview时的图像裁剪成picturesize的比例。

previewsize的分辨率,只会影响预览时的分辨率,不会影响获取图片的分辨率,所以preview只是确定了图像的取景最大范围(所谓的取景范围就是展示多大的画面),最终图片的分辨率是由picturesize来决定。

6 Camera设置帧率

/**

* 选择合适的FPS

* @param parameters

* @param expectedThoudandFps 期望的FPS

* @return

*/

public static int chooseFixedPreviewFps(Camera.Parameters parameters, int expectedThoudandFps) {

List supportedFps = parameters.getSupportedPreviewFpsRange();

for (int[] entry : supportedFps) {

if (entry[0] == entry[1] && entry[0] == expectedThoudandFps) {

parameters.setPreviewFpsRange(entry[0], entry[1]);

return entry[0];

}

}

int[] temp = new int[2];

int guess;

parameters.getPreviewFpsRange(temp);

if (temp[0] == temp[1]) {

guess = temp[0];

} else {

guess = temp[1] / 2;

}

return guess;

}

getSupportedPreviewFpsRange函数可以获取设备支持的帧率,fps不是越高越好,FPS不宜过高,一般30fps足够了。

7 如何读取Camera的NV21数据和YUV数据

给camera对象设置一个 Camera.PreviewCallback,在这个回调中实现一个方法onPreviewFrame(byte[] data, Camera camera),就可以去Camera预览图片时的数据。

当然如果设置了camera.setPreviewCallback(callback),onPreviewFrame这个方法会被一直调用,可以在摄像头对焦成功后设置camera.setOneShotPreviewCallback(previewCallback),这样设置onPreviewFrame这个方法就会被调用一次,处理data数据,bitmap来做相应的处理就行了。这两个方法都是系统自动配置缓冲区。

setPreviewFormat 函数可以设置预览是onPreviewFrame返回数据的格式。

最常见的获取的数据格式为NV21,如果不设置默认返回数据也是NV21编码的数据。

Camera.Parameters parameters = mCamera.getParameters();

parameters.setRecordingHint(true);

{

//设置获取数据的格式

parameters.setPreviewFormat(ImageFormat.NV21);

//parameters.setPreviewFormat(ImageFormat.YV12);

//通过setPreviewCallback方法监听预览的回调:

byte[] imageByte;

Bitmap bitmap;

mCamera.setPreviewCallback(new Camera.PreviewCallback() {

@Override

public void onPreviewFrame(byte[] bytes, Camera camera) {

//这里面的Bytes的数据就是NV21格式的数据,或者YV12的数据

Camera.Size previewSize = camera.getParameters().getPreviewSize();//获取尺寸,格式转换的时候要用到

BitmapFactory.Options newOpts = new BitmapFactory.Options();

newOpts.inJustDecodeBounds = true;

YuvImage yuvimage = new YuvImage(

data,

ImageFormat.NV21,

previewSize.width,

previewSize.height,

null);

baos = new ByteArrayOutputStream();

yuvimage.compressToJpeg(new Rect(0, 0, previewSize.width, previewSize.height), 100, baos);// 80--JPG图片的质量[0-100],100最高

imageByte = baos.toByteArray();

//将imageByte转换成bitmap

BitmapFactory.Options options = new BitmapFactory.Options();

options.inPreferredConfig = Bitmap.Config.RGB_565;

bitmap = BitmapFactory.decodeByteArray(imageByte, 0, imageByte.length, options);

}

});

}

mCamera.setParameters(parameters);

注意,onPreviewFrame()方法跟Camera.open()是运行于同一个线程,所以为了防止onPreviewFrame()会阻塞UI线程,将Camera.open()放置在子线程中运行。

为什么要用到这个函数,因为如果调用takePicture别管怎么设置界面都会卡顿,如果通过onPreviewFrame的回调处理函数,不会导致界面卡顿。

setPreviewCallbackWithBuffer 类似setPreiewCallback,一般配合setPreviewCallback使用

setPreviewCallbackWithBuffer (Camera.PreviewCallback cb) 要求指定一个字节数组作为缓冲区,用于预览帧数据,这样能够更好的管理预览帧数据时使用的内存。

setPreviewCallbackWithBuffer需要在startPreview()之前调用,因为setPreviewCallbackWithBuffer使用时需要指定一个字节数组作为缓冲区,用于预览帧数据,所以我们需要在setPreviewCallbackWithBuffer之前调用addCallbackBuffer,这样onPreviewFrame的data才有值。然后需要在onPreviewFrame中调用,如果在onPreviewFrame中不调用,那么预览帧数据就不会回调给onPreviewFrame。

代码示例:

//通过setPreviewCallback方法监听预览的回调:

mCamera.setPreviewCallback(new Camera.PreviewCallback() {

@Override

public void onPreviewFrame(byte[] bytes, Camera camera) {

//这里面的Bytes的数据就是NV21格式的数据,或者YUV_420_888的数据

}

});

mCamera.setPreviewCallbackWithBuffer(new Camera.PreviewCallback() {

@Override

public void onPreviewFrame(byte[] data, Camera camera) {

}

});

}

mCamera.setOneShotPreviewCallback(new Camera.PreviewCallback() {

@Override

public void onPreviewFrame(byte[] data, Camera camera) {

}

});

8 其他

camera的切换(前后摄像头)

要先释放前一个Camera,然后打开新相机,打开预览。

takePicture获取图片

调用takePicture后预览会停止,需用重新调用startPreview才能再次开始预览。预览开始后,就可以通过Camera.takePicture()方法拍摄一张照片,返回的照片数据通过Callback接口获取。

takePicture()接口可以获取三个类型的照片:

第一个,ShutterCallback接口,在拍摄瞬间瞬间被回调,通常用于播放“咔嚓”这样的音效;

第二个,PictureCallback接口,返回未经压缩的RAW类型照片;

第三个,PictureCallback接口,返回经过压缩的JPEG类型照片;

是否支持自动对焦

List modes = Parameters.getSupportedFocusModes();

modes.contains(Camera.Parameters.FOCUS_MODE_AUTO);

getSupportedFocusModes函数获取所有的焦点模式,FOCUS_MODE_AUTO标识自动对焦,对焦方式还有FOCUS_MODE_CONTINUOUS_VIDEO使用视频录制,FOCUS_MODE_CONTINUOUS_PICTURE 用于拍照。