前面一篇文章Android Camera基本用法一 只是简单的介绍了Camera的基本用法,很多知识都很粗糙,今天开始一系列文章开始分别学习Camera的知识,这次的内容为Camera对焦。
Camera如果不进行对焦画面会很模糊,Camera的对焦分为自动对焦和触摸对焦,但由于Android各大厂商都可以修改相关源码所以适配存在较多问题。
Camera的对焦模式:
从api14开始,这种模式下应用可以调用autoFocus(AutoFocusCallback)进行对焦,焦点回调函数会很快进行回调让我们知道是否对焦成功。这种模式下调用autoFocus后焦点一直是固定的。如果应用想要重新开启自动对焦,需要首先调用cancelAutoFocus。重新开始预览不会重新开启连续自动对焦。
注意如果想要重新开始自动聚焦,需要首先调用cancelAutoFocus,然后设置自动对焦模式,在调用autoFocus(AutoFocusCallback)
if (mCamera != null){
mCamera.cancelAutoFocus();
mCamera.getParameters().setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if(success){
Toast.makeText(Main22Activity.this,"对焦成功",Toast.LENGTH_SHORT).show();
}else{
}
}
});
}
该模式下可调用autoFocus(AutoFocusCallback),如果当前正在对焦扫描,focus回调函数将在它完成对焦是回调;如果没有正在对焦扫描,将立即放回。autoFocus函数调用后对焦区域是固定的,如果应用想要重新开启自动连续对焦,需要首先调用cancelAutoFocus,重新开始预览无法开启自动连续对焦,需要重新调用autoFocus,如果想要停止自动连续对焦,应用可以修改对焦模式。
FOCUS_MODE_AUTO,FOCUS_MODE_CONTINUOUS_VIDEO,FOCUS_MODE_CONTINUOUS_PICTURE通常较为常用。
对焦的意义就是在手机晃动,移动或者改变位置时,拍摄画面依然清晰,如果不进行对焦则画面会很模糊。
示例代码:
public class Main22Activity extends AppCompatActivity implements SurfaceHolder.Callback {
private static int mOrientation = 0;
private static int mCameraID = Camera.CameraInfo.CAMERA_FACING_BACK;
private SurfaceView mSurfaceView;
private SurfaceHolder mSurfaceHolder;
private Camera mCamera;
private boolean havePermission = false;
private Button btnFocus;
private Button btnTakePic;
private Button btnRestar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main22);
btnFocus = findViewById(R.id.focus);
btnTakePic = findViewById(R.id.takepic);
btnRestar = findViewById(R.id.restar);
btnRestar.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mCamera != null){
mCamera.startPreview();
}
}
});
btnFocus.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
btnTakePic.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mCamera!= null){
mCamera.takePicture(null, null, new Camera.PictureCallback() {
@Override
public void onPictureTaken(byte[] data, Camera camera) {
// 获取Jpeg图片,并保存在sd卡上
String path = Environment.getExternalStorageDirectory()
.getPath() +"/focus/";
File pathDir = new File(path);
if (!pathDir.exists()){
pathDir.mkdir();
}
File pictureFile = new File(path+ "focusdemo.jpg");
if (pictureFile.exists()){
pictureFile.delete();
}
try {
FileOutputStream fos = new FileOutputStream(pictureFile);
fos.write(data);
fos.close();
} catch (Exception e) {
}
}
});
}
}
});
// Android 6.0相机动态权限检查,省略了
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
== PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED) {
havePermission = true;
init();
} else {
havePermission = false;
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, 100);
}
}
public void init(){
if(mSurfaceView == null){
mSurfaceView = findViewById(R.id.surfaceview);
mSurfaceView.setCustomEvent(new CustomSurfaceView.ONTouchEvent() {
@Override
public void onTouchEvent(MotionEvent event) {
handleFocus(event, mCamera);
}
});
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(this);
WindowManager wm = (WindowManager) Main23Activity.this.getSystemService(Context.WINDOW_SERVICE);
int width = wm.getDefaultDisplay().getWidth();
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mSurfaceView.getLayoutParams();
layoutParams.width = width;
layoutParams.height = width*4/3;
useWidth = width;
useHeight = width*4/3;
mSurfaceView.setLayoutParams(layoutParams);
}
}
private void initCamera() {
if (mCamera != null){
releaseCamera();
System.out.println("===================releaseCamera=============");
}
mCamera = Camera.open(mCameraID);
System.out.println("===================openCamera=============");
if (mCamera != null){
try {
mCamera.setPreviewDisplay(mSurfaceHolder);
} catch (IOException e) {
e.printStackTrace();
}
Camera.Parameters parameters = mCamera.getParameters();
parameters.setRecordingHint(true);
{
//设置获取数据
parameters.setPreviewFormat(ImageFormat.NV21);
//parameters.setPreviewFormat(ImageFormat.YUV_420_888);
//通过setPreviewCallback方法监听预览的回调:
mCamera.setPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
//这里面的Bytes的数据就是NV21格式的数据,或者YUV_420_888的数据
}
});
}
if(mCameraID == Camera.CameraInfo.CAMERA_FACING_BACK){
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
}
mCamera.setParameters(parameters);
calculateCameraPreviewOrientation(this);
Camera.Size tempSize = setPreviewSize(mCamera, useHeight,useWidth);
{
//此处可以处理,获取到tempSize,如果tempSize和设置的SurfaceView的宽高冲突,重新设置SurfaceView的宽高
}
setPictureSize(mCamera, useHeight,useWidth);
mCamera.setDisplayOrientation(mOrientation);
int degree = calculateCameraPreviewOrientation(Main23Activity.this);
mCamera.setDisplayOrientation(degree);
mCamera.startPreview();
}
}
public void releaseCamera(){
if (mCamera != null) {
mSurfaceHolder.removeCallback(this);
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
mCamera.lock();
mCamera.release();
mCamera = null;
}
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
//当SurfaceView变化时也需要做相应操作,这里未做相应操作
if (havePermission){
initCamera();
}
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
mCamera.stopPreview();
}
private void setPictureSize(Camera camera ,int expectWidth,int expectHeight){
Camera.Parameters parameters = camera.getParameters();
Point point = new Point(expectWidth, expectHeight);
Camera.Size size = findProperSize(point,parameters.getSupportedPreviewSizes());
parameters.setPictureSize(size.width, size.height);
camera.setParameters(parameters);
}
private Camera.Size setPreviewSize(Camera camera, int expectWidth, int expectHeight) {
Camera.Parameters parameters = camera.getParameters();
Point point = new Point(expectWidth, expectHeight);
Camera.Size size = findProperSize(point,parameters.getSupportedPictureSizes());
parameters.setPictureSize(size.width, size.height);
camera.setParameters(parameters);
return size;
}
/**
* 找出最合适的尺寸,规则如下:
* 1.将尺寸按比例分组,找出比例最接近屏幕比例的尺寸组
* 2.在比例最接近的尺寸组中找出最接近屏幕尺寸且大于屏幕尺寸的尺寸
* 3.如果没有找到,则忽略2中第二个条件再找一遍,应该是最合适的尺寸了
*/
private static Camera.Size findProperSize(Point surfaceSize, List<Camera.Size> sizeList) {
if (surfaceSize.x <= 0 || surfaceSize.y <= 0 || sizeList == null) {
return null;
}
int surfaceWidth = surfaceSize.x;
int surfaceHeight = surfaceSize.y;
List<List<Camera.Size>> ratioListList = new ArrayList<>();
for (Camera.Size size : sizeList) {
addRatioList(ratioListList, size);
}
final float surfaceRatio = (float) surfaceWidth / surfaceHeight;
List<Camera.Size> bestRatioList = null;
float ratioDiff = Float.MAX_VALUE;
for (List<Camera.Size> 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<List<Camera.Size>> ratioListList, Camera.Size size) {
float ratio = (float) size.width / size.height;
for (List<Camera.Size> ratioList : ratioListList) {
float mine = (float) ratioList.get(0).width / ratioList.get(0).height;
if (ratio == mine) {
ratioList.add(size);
return;
}
}
List<Camera.Size> ratioList = new ArrayList<>();
ratioList.add(size);
ratioListList.add(ratioList);
}
/**
* 排序
* @param list
*/
private static void sortList(List<Camera.Size> list) {
Collections.sort(list, new Comparator<Camera.Size>() {
@Override
public int compare(Camera.Size pre, Camera.Size after) {
if (pre.width > after.width) {
return 1;
} else if (pre.width < after.width) {
return -1;
}
return 0;
}
});
}
/**
* 设置预览角度,setDisplayOrientation本身只能改变预览的角度
* previewFrameCallback以及拍摄出来的照片是不会发生改变的,拍摄出来的照片角度依旧不正常的
* 拍摄的照片需要自行处理
* 这里Nexus5X的相机简直没法吐槽,后置摄像头倒置了,切换摄像头之后就出现问题了。
* @param activity
*/
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;
}
mOrientation = result;
System.out.println("=========orienttaion============="+result);
return result;
}
@Override
protected void onResume() {
super.onResume();
if (havePermission && mCamera != null)
mCamera.startPreview();
}
@Override
protected void onPause() {
super.onPause();
if (havePermission && mCamera != null)
mCamera.stopPreview();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
// 相机权限
case 100:
havePermission = true;
init();
break;
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<SurfaceView
android:id="@+id/surfaceview"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/focus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="focus"/>
<Button
android:id="@+id/takepic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="takePic"/>
<Button
android:id="@+id/restar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="restar"/>
</LinearLayout>
统一说明:由于采用竖屏拍摄照片,保存的照片和底层传感器方向一致还是横屏的,没有对保存的数据进行旋转,所以拍出的照片被旋转了,后面会开文章对前置后置摄像头拍出的照片进行处理。
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
设置这种模式之后,很多手机不会自动对焦,还是需要手动调用autoFocus函数才会对焦。
btnFocus.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mCamera != null){
mCamera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
if(success){
Toast.makeText(Main22Activity.this,"对焦成功",Toast.LENGTH_SHORT).show();
}else{
}
}
});
}
}
});
这种模式很多手机上无法实现自动对焦,改变手机位置或者拍照之前需要调用autoFocus进行对焦,效果一般。拍照之前进行对焦,对焦成功进行拍照。
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
这种模式可以实现自动对焦,但据网上大家反映对焦时会连续闪动。
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
大部分手机在这种模式下会自动对焦,网上说有部分手机不会自动对焦。
前面讲解了可以设置自动对焦模式,然后每次需要手动调用autoFocus函数进行对焦,此外Camera还提供了一种手动对焦模式叫做触摸对焦。
触摸对焦,就是在屏幕上点击某个点,通过setFocusAreas()指定这个对焦区域,应用到相机,相机就以此点内容进行对焦,保证此点最清晰。
利用setFocusAreas函数 ,在api14添加,api14之前只能调用autoFocus函数进行,研究setFocusAreas时官方文档指引我们看getFocusAreas()的说明。
setFocusAreas(List focusAreas)
只在FOCUS_MODE_AUTO,FOCUS_MODE_CONTINUOUS_VIDEO,FOCUS_MODE_CONTINUOUS_PICTURE这三种模式下有效。
要使用setFocusAreas首先要判断是否支持设置定点对焦,api14以下是不支持的,另外还需要判断getMaxNumFocusAreas()的值,如果小于等于0也是不支持定点对焦的。
目前市场大部分手机这个方法的返回值,前置摄像头都是0,后置摄像头都是1,说明前置摄像头一般不支持设置聚焦,而后置摄像头一般也只支持单个区域的聚焦。
Area类用于标识感光区域(测光)和聚焦区域,用于相机计算自动曝光、自动白平衡、自动聚焦。每一个Area是一个矩形区域,有一个weight标识它的重要程度。Camera的可以设置定点对焦的区域会被映射到一个左上角为(-1000,-1000)到右下角(1000,1000)的区域上。在这个范围之外设置无效,如果area的大小为0或者weight为负是不被允许的。
Camera.Area对象,包含两个参数:
Rect对象,它用于指定Camera预览窗口一块矩形区域(测光区域)
一个权重值(weight),它告诉Camera这块指定区域应该给予的测光或调焦计算的重要性等级,权重大的优先级高,权重最高为1000
Weight的取值范围必须是(1,1000),weight值越大,当我们添加多个聚焦区域时,weight大的区域起到的作用越大。
/**
* 转换对焦区域
* 范围(-1000, -1000, 1000, 1000)
* x,y是坐标位置,width,height SurfaceView的宽高,coefficient是区域比例大小
*/
private Rect calculateTapArea(float x, float y, int width, int height, float coefficient) {
float focusAreaSize = 200;
//这段代码可以看出coefficient的作用,只是为了扩展areaSize。
int areaSize = (int) (focusAreaSize * coefficient);
int surfaceWidth = width;
int surfaceHeight = height;
//解释一下为什么*2000,因为要把surfaceView的坐标转换为范围(-1000, -1000, 1000, 1000),则SurfaceView的中心点坐标会转化为(0,0),x/surfaceWidth ,得到当前x坐标占总宽度的比例,然后乘以2000就换算成了(0,0,2000,2000)的坐标范围内,然后减去1000,就换算为了范围(-1000, -1000, 1000, 1000)的坐标。
//得到了x,y转换后的坐标,利用areaSize就可以得到聚焦区域。
int centerX = (int) (x / surfaceHeight * 2000 - 1000);
int centerY = (int) (y / surfaceWidth * 2000 - 1000);
int left = clamp(centerX - (areaSize / 2), -1000, 1000);
int top = clamp(centerY - (areaSize / 2), -1000, 1000);
int right = clamp(left + areaSize, -1000, 1000);
int bottom = clamp(top + areaSize, -1000, 1000);
return new Rect(left, top, right, bottom);
}
//不大于最大值,不小于最小值
private int clamp(int x, int min, int max) {
if (x > max) {
return max;
}
if (x < min) {
return min;
}
return x;
}
在得到转换后的矩形后就可以直接通过setFocusAreas()应用到相机了?实际没这么简单。直接这么做往往不能达到理想的效果,因为Android本身的问题以及设备的差异,在常用的对焦模式为continuous-picture下,setFocusAreas()可能会不工作。目前常用的解决办法是在setFocusAreas()同时修改相机对焦模式为macro等,待对焦完毕后,再将对焦模式修改为用户之前定义的。
使用实例:
public class CustomSurfaceView extends SurfaceView {
private ONTouchEvent mTouchEvent;
public CustomSurfaceView(Context context) {
super(context);
}
public CustomSurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (MotionEvent.ACTION_DOWN == event.getAction()){
mTouchEvent.onTouchEvent(event);
}
return super.onTouchEvent(event);
}
public void setCustomEvent(ONTouchEvent onTouchEvent){
mTouchEvent = onTouchEvent;
}
public interface ONTouchEvent{
public void onTouchEvent(MotionEvent event);
}
}
mSurfaceView.setCustomEvent(new CustomSurfaceView.ONTouchEvent() {
@Override
public void onTouchEvent(MotionEvent event) {
handleFocus(event, mCamera);
}
});
private void handleFocus(MotionEvent event, Camera camera) {
int viewWidth = useWidth;
int viewHeight = useHeight;
Rect focusRect = calculateTapArea(event.getX(), event.getY(), viewWidth, viewHeight,1.0f);
//一定要首先取消,否则无法再次开启
camera.cancelAutoFocus();
Camera.Parameters params = camera.getParameters();
if (params.getMaxNumFocusAreas() > 0) {
List<Camera.Area> focusAreas = new ArrayList<>();
focusAreas.add(new Camera.Area(focusRect, 800));
params.setFocusAreas(focusAreas);
} else {
//focus areas not supported
}
//首先保存原来的对焦模式,然后设置为macro,对焦回调后设置为保存的对焦模式
final String currentFocusMode = params.getFocusMode();
params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
camera.setParameters(params);
camera.autoFocus(new Camera.AutoFocusCallback() {
@Override
public void onAutoFocus(boolean success, Camera camera) {
//回调后 还原模式
Camera.Parameters params = camera.getParameters();
params.setFocusMode(currentFocusMode);
camera.setParameters(params);
if(success){
Toast.makeText(Main22Activity.this,"对焦区域对焦成功",Toast.LENGTH_SHORT).show();
}
}
});
}
可以看到视频中有远近不同的两个物品,当近处物品清晰时远处不清晰,当远处物品清晰时近处不清晰,点击不同区域对焦area变换,可以转换近处和远处,如下图:
没有远近物品时:
还有一种增强Camera的方式叫做触摸测光,就是在屏幕上点击某个点,相机调整曝光亮度,保证此点亮度最为合适;
setMeteringAreas设置感光区域,也是利用Area设置测光的区域,使用之前要先调用getMaxNumMeteringAreas如果大于0说明支持测光,否则不支持无法使用。
触摸测光与触摸对焦类似,只是作用稍有不同,触摸对焦用于改变聚焦点,触摸测光用于改变光的强弱,一般两者结合使用。
注意:有时可能因为设置的区域过小或者权重weight过小导致效果不明显可以增大区域和weight。
上面说了利用对焦模式实现自动对焦会存在有的手机不支持的情况,所以要实现自动对焦还得利用触摸对焦的方式实现。
最通用的做法就是通过监听加速度传感器的变化,传感器数值改变超过了一定范围时调用对焦函数自动对焦。
简单实例:
@Override
public void onSensorChanged(SensorEvent event) {
//手机移动一段时间后静止,然后静止一段时间后进行对焦
// 读取加速度传感器数值,values数组0,1,2分别对应x,y,z轴的加速度
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
int x = (int) event.values[0];
int y = (int) event.values[1];
int z = (int) event.values[2];
//如果满足条件调用触摸对焦方法。
//具体实现参考:https://github.com/WellerV/SweetCamera
}
}
SweetCamera 实现方式:
/*
稍微解释一下SweetCamera的处理传感器的思路,通过传感器数据记录移动点和静止点,如果上一次是移动状态,现在是静止状态,且静止持续了一定时间就可以调用对焦函数进行对焦。
*/
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
int x = (int) event.values[0];
int y = (int) event.values[1];
int z = (int) event.values[2];
mCalendar = Calendar.getInstance();
long stamp = mCalendar.getTimeInMillis();
int second = mCalendar.get(Calendar.SECOND);
if (STATUE != STATUS_NONE) {
int px = Math.abs(mX - x);
int py = Math.abs(mY - y);
int pz = Math.abs(mZ - z);
double value = Math.sqrt(px * px + py * py + pz * pz);
if (value > 1.4) {
STATUE = STATUS_MOVE;
} else {
//上一次状态是move,记录静态时间点
if (STATUE == STATUS_MOVE) {
lastStaticStamp = stamp;
canFocusIn = true;
}
if (canFocusIn) {
if (stamp - lastStaticStamp > DELEY_DURATION) {
//移动后静止一段时间,可以发生对焦行为
if (!isFocusing) {
canFocusIn = false;
if (mCameraFocusListener != null) {
mCameraFocusListener.onFocus();
}
}
}
}
STATUE = STATUS_STATIC;
}
} else {
lastStaticStamp = stamp;
STATUE = STATUS_STATIC;
}
mX = x;
mY = y;
mZ = z;
}
PS:由于手头没有太多手机,没有进行机型适配,听说魅族的会问题多一点,以上gif图有点模糊是音频,利用screenrecorder命令进行录制只能录制为mp4,之后又利用gif软件录制的MP4视频,所以画面比较模糊。