1.Android sdk中的Camera
2.Camera开发中遇到的一些疑问
3.Camera实例
4.camera2实例
5.总结
一、Android sdk中的Camera
在Android Studio中敲下Camera,会给出两个提示类:
1.android.graphics.Camera ,一个照相机实例可以被用于计算3D变换,生成一个可以被使用的Matrix矩阵,一个实例,用在画布上。
2.android.hardware.Camera,用于设置图像捕获设置、开始/停止预览、快照图片以及检索用于视频编码的帧。这个类是相机服务的客户端,它管理实际的相机硬件。
今天要讲的是相机管理类android.hardware.Camera,
但在API 21+之后该类已被废弃,新的相机管理类在 android.hardware.camera2 包下,所以本文会覆盖camera和camera2两种API。
二、Camera开发中遇到的一些疑问
拍摄的照片为什么会旋转90度
预览的图像被拉伸
预览时返回的图像帧数据为什么不能正常解析成bitmap
针对以上的问题我们从camera成像说起,camera其实应该叫image sensor。
image sensor有两大类CMOS和CCD,手机中常常使用的是价格低廉和整合性高的CMOS,sensor的成像原理是景物通过镜头(LENS)生成的光学 图像投射到图像传感器(Sensor)表面上,然后转为模拟的电信号,经过A/D(模数转换)转换后变为数字图像信号,再送到数字信号处理芯片(DSP) 中加工处理,再通过IO接口传输到CPU中处理,通过LCD 就可以看到图像了。
sensor一般的输出格式:
1)YUV sensor
YUV sensor输出的Data格式为YUV,也是使用android.hardware.Camera进行预览返回预览帧的常见格式。
2)Raw sensor
Raw Sensor输出的Data格式为原始未经处理的Raw,图像感应器将捕捉到的光源信号转化为数字信号的原始数据。RAW文件是一种记录了数码相机传感器 的原始信息,同时记录了由相机拍摄所产生的一些原数据(如ISO的设置、快门速度、光圈值、白平衡等)的文件,在camera2中可查看相机是否支持 raw格式并获取raw数据。
从sensor输出格式可以知道预览时的帧数据为什么不能简单生成bitmap,因为bitmap创建需要的是rgb格式,而预览帧数据一般是YUV格式。
image sensor的成像扫描方向是固定的,但image sensor安装在手机的成像方向不一定和手机自然方向(一般为竖屏)一致。
上图是常见的后置摄像头与手机自然方向的对比,image sensor 顺时针旋转90度就和手机自然方向一致,这就是为什么我们要给camera设置90度才能竖屏预览。
但90度不是定势,所以实际编码中可以获取image sensor的方向和phone display的方向进行计算得出正确的旋转值。
来看看Android官方给出的建议计算方案
setCameraDisplayOrientation
/**
* 设置camera sensor 展示方向和屏幕自然方向一致
*/
public static void setCameraDisplayOrientation(Activity activity, int cameraId, android.hardware.Camera camera) {
//通过cameraId获取当前camera信息
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, 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; // compensate the mirror
} else { // back-facing
result = (info.orientation - degrees + 360) % 360;
}
//设置camera显示图像角度
camera.setDisplayOrientation(result);
}
值得注意的是,以上操作只是让预览中显示的图像正常,对于预览返回帧数据和拍照取得的数据并不会真正改变其方向,如果要得到和预览图像一致的照片还需要对图像数据进行额外的旋转操作,但如果只需要拍照图像,则不需要对数据进行旋转,使用camera.getParameters().setRotation(rotation)可以直接得到正确方向的jpeg图像,对于rotation值的计算官方给出的算法在setRotation方法注释中,要特别注意的是这个值只改变jpeg方向,并不能改变预览帧YUV数据方向,在camera2中的使用会更直观一点,后面再讲。
预览时图像显示为什么被拉伸? 这和image sensor返回的预览图像大小有关,只有预览图的大小和显示预览视图大小相近相等时才最自然。
一台手机image sensor支持的属性设置都是有固定值的,绝大多数时候拍摄的图片好不好看取决于image sensor的硬件成本,成像不好,再如何p图也枉然。
3.Camera实例
camera1 中CameraService运行在mediaserver进程中,虽然camera2在5.0就存在,但CameraServide依然存在于 mediaserver进程中,在Android7.0+系统中CameraService才作为一个独立的系统服务进程存在。
看下应用层的使用流程。
使用camera2进行预览拍照最好的例子可查看官方demo。
先看两个最关键的问题
1、怎么设置预览/拍照图像参数?
2、从哪里拿预览/拍照的图像数据?
第一点,camera1的参数设置都在camera.getParameters()中,camera2在哪里呢?在CaptureRequest.Builder中,比如设置对焦和曝光模式:
set
//设置对焦模式为快速连续对焦
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
前面讲过camera的3A模式,这里看参数命名就直接明了了,更多参数详解可以看3A 模式和状态转换。
另 外值得注意的是camera2相比camera1支持的数据格式、预览大小等都有很大的差异,比如在荣耀8上使用camera1进行预览获取的最大预览大 小是1920x1080, 拍照支持最大size是3968x2240, 使用camera2的最大预览尺寸为3968x2240,拍照最大支持3968x2240,camera2可支持的预览size更加丰富,使用 camera2获取的Size根据格式的不同返回不同, 如果不支持输出的格式则返回null,使用方式如下;
获取输出size
StreamConfigurationMap map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size[] sizes = map.getOutputSizes(SurfaceTexture.class);
for (Size size : sizes) {
System.out.println(">>>>>>>camera2 preview size : >>>>> " + size.toString());
}
Size[] jpegSizes = map.getOutputSizes(ImageFormat.JPEG);
for (Size size : jpegSizes) {
System.out.println(">>>>>> jpeg : size " + size.toString());
}
Size[] rawSizes = map.getOutputSizes(ImageFormat.RAW_SENSOR);
for (Size size : rawSizes) {
System.out.println(">>>>>> raw : size " + size.toString());
第二点,camera2中预览和拍照都没有直接的图像数据回调,它引入了 ImageReader类,在createCaptureSession时可传入surface数组入参,通过 CaptureRequest.Builder.addTarget添加多个surface接收图像数据:
SurfaceView、TextureView等呈现的surface数据不能被直接访问,而ImageReader类允许应用程序直接访问呈现到surface的图像数据,使用方式如下:
createCameraPreviewSession()
private void createCameraPreviewSession() {
try {
SurfaceTexture texture = textureView.getSurfaceTexture();
//设置预览大小
texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());
Surface surface = new Surface(texture);
previewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
previewRequestBuilder.addTarget(surface);
imageReader = ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), ImageFormat.JPEG, 1);
// 暂不加入预览目标,在需要获取图像数据时添加即可
// previewRequestBuilder.addTarget(imageReader.getSurface());
imageReader.setOnImageAvailableListener(onImageAvailableListener, handler);
cameraDevice.createCaptureSession(Arrays.asList(surface, imageReader.getSurface()), new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
captureSession = session;
previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
// Flash is automatically enabled when necessary.
previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
try {
previewRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, getOrientation());
// Finally, we start displaying the camera preview.
captureRequest = previewRequestBuilder.build();
captureSession.setRepeatingRequest(captureRequest, captureCallback, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
}
}, handler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
private ImageReader.OnImageAvailableListener onImageAvailableListener = new ImageReader.OnImageAvailableListener() {
@Override
public void onImageAvailable(ImageReader reader) {
// 请求下一个有效的图像
Image img = reader.acquireNextImage();
ByteBuffer buffer = img.getPlanes()[0].getBuffer();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
bms.add(data);
// 请求之后必须关闭
img.close();
}
};
在camera1中支持预览返回格式有ImageFormat.NV21和ImageFormat.YV12,但是camera2中通常仅开放ImageFormat.JPEG、ImageFormat.RAW_SENSOR和ImageFormat.YUV_420_888,其他格式通常是私有的,不可读取,尤其不支持ImageFormat.NV21。
本 例的实现就是通过锁定对焦,预览计时3秒,从ImageReader的OnImageAvailableListener中获取可用Image放入 list,解析list生成bitmap,生成原始GIF,自定义view创建画布,获得画布上用户选取绘制像素点,根据像素点列表,获取第一帧之后的每 一帧相关像素,循环替换第一帧选取区域像素点,生成微动gif,核心代码如下:
生成微动gif
/**
* 生成gif图片
*/
void makeGif(final ArrayList
points, final GifCallback callback) {
new Thread() {
@Override
public void run() {
AnimatedGifEncoder gifEncoder = new AnimatedGifEncoder();
if (!gifFile.exists())
try {
gifFile.createNewFile();
} catch (IOException e1) {
e1.printStackTrace();
}
OutputStream os;
try {
os = new FileOutputStream(gifFile);
gifEncoder.start(os); //注意顺序
gifEncoder.setRepeat(0);
Bitmap one = null;
for (int i = 0; i
byte[] bytes = list.get(i);
if (i == 0) {
//取第一帧为背景
one = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
gifEncoder.addFrame(one);
} else {
//获取后续帧图像
Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
//第一帧作为后续所有帧的原图
Bitmap other = Bitmap.createBitmap(one);
for (Point point : points) {
//第一帧中的选取像素点替换成后续帧的像素点,形成第一帧的局部动态
other.setPixel(point.x, point.y, bm.getPixel(point.x, point.y));
}
gifEncoder.addFrame(other);
}
}
gifEncoder.finish();
System.out.println("保存地址:" + gifFile.getAbsolutePath());
runOnUiThread(new Runnable() {
@Override
public void run() {
callback.complete();
}
});
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}.start();
}
选取像素点操作的自定义view核心代码如下:
DrawView
private void init() {
path = new Path();
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(0x88cccccc);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(20);
p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(20);
p.setColor(0xffff0000);
}
@Override
protected void onDraw(Canvas canvas) {
if (null != bitmap) {
// canvas.drawBitmap(bitmap, 0, 0, new Paint());
canvas.drawPath(path, paint);
canvasb.drawPath(path, p);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
path.moveTo(event.getX(), event.getY());
break;
case MotionEvent.ACTION_MOVE:
path.lineTo(event.getX(), event.getY());
postInvalidate();
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* 像素点分析
*/
private void test() {
list.clear();
int w = bm.getWidth();
int h = bm.getHeight();
for (int i = 0; i
for (int j = 0; j
if (bm.getPixel(i, j) == 0xffff0000) {
list.add(new Point(i, j));
}
}
}
}
5.总结
从camera2和camera1的使用体验来说,新的api带来了更多的可能性,但厂商定制手机五花八门,同样版本的手机也不一定都支持同样的功能,所以做相机开发需要考虑到相当多的 兼容特性。
从Android API出发,需要考虑camera使用版本、预览view使用类,从设备出发,需要考虑相机支持特性、支持数据格式等,谷歌官方给出的一份兼容方案如下:
https://github.com/google/cameraview
参考资料:
http://lib.csdn.net/article/embeddeddevelopment/67528
http://tangzm.com/blog/?p=20
http://www.cnblogs.com/azraelly/archive/2013/01/01/2841269.htm
http://www.fourcc.org/yuv.php
https://source.android.google.cn/devices/camera/
[ShareSDK] 轻松实现社会化功能 强大的社交分享
[SMSSDK] 快速集成短信验证 联结通讯录社交圈
[MobLink] 打破App孤岛 实现Web与App无缝链接
[MobPush] 快速集成推送服务 应对多样化推送场景
[AnalySDK] 精准化行为分析 + 多维数据模型 + 匹配全网标签 + 垂直行业分析顾问
BBSSDK | ShareREC | MobAPI | MobPay | ShopSDK | MobIM | App工厂
截止2018 年4 月,Mob 开发者服务平台全球设备覆盖超过84 亿,SDK下载量超过3,300,000+次,服务超过380,000+款移动应用
领取专属 10元无门槛券
私享最新 技术干货