Camera相机研发介绍

1. Camera开发流程
在这里,我们先了解下相机开发的大致流程,然后再对里面的步骤进行详细的阐述。

  1. 利用open(int)获取Camera实例
  2. 利用getParameters()获取默认设置,如果需要修改参数,利用setParameters()重新设置
  3. 利用setDisplayOrientation()设置相机图像旋转角度,产生正确的预览画面
  4. 利用setPreviewDisplay(SurfaceHolder)关联相机与SurfaceView显示图层,让视频流显示在界面上
  5. 设置Preview的回调函数,获取帧数据并进行图像处理逻辑
  6. 调用startPreview()开始预览,调用stopPreview()停止预览
  7. 调用release()释放相机资源。


2. 打开相机
一般手机上都有多个相机,支持前置或后置相机来拍照,利用Camera.getNumberOfCameras()获取相机的个数;然后通过Camera.getCameraInfo()获取每个相机的朝向,该返回值有两种:
后置:CameraInfo.CAMERA_FACING_BACK
前置:CameraInfo.CAMERA_FACING_FRONT
我们根据项目需求选择对应朝向的相机,默认是开启后置相机,再通过Camera.open()打开即可。需要注意的是打开相机可能失败,所以一定要检查相机是否打开成功,判断Camera是否为null。

Camera.CameraInfo cameraInfo = new Camera.CameraInfo();    		
for(int id=0;id<Camera.getNumberOfCameras();id++)
{
    Camera.getCameraInfo(id, cameraInfo);
    if(cameraInfo.facing==Camera.CameraInfo.CAMERA_FACING_BACK){
        mCamera = Camera.open(id);
    	break;
    }
}   
3. 设置参数
相机涉及到的参数较多,通过Camera.getParameters()获取当前相机的参数信息,返回值为Parameters对象;再通过Camera.setParamerters()来实现相机参数的重新设置。下面介绍几个主要的参数设置。


3.1 设置帧率
我们可以通过getSupportedPreviewFpsRange获取手机支持的预览帧率,如下代码找出测试机(Huawei nova2)支持的帧率范围。
List<int[]> fpsSupport = parameters.getSupportedPreviewFpsRange();

for (int i = 0; i < fpsSupport.size(); i++)
{
    int[] fps = fpsSupport.get(i);
    Log.i("FPS", String.format("fps: %d, %d",fps[0],fps[1]));
}

输出结果:

fps: 30000, 30000
fps: 14000, 30000
fps: 14000, 20000
fps: 20000, 20000
fps: 14000, 25000
fps: 25000, 25000
fps: 12000, 15000
fps: 15000, 15000
fps: 14000, 14000


在知道了手机支持的最小和最大帧率后,通过parameters.setPreviewFpsRange来设定我们需要的视频帧率。

3.2 设置预览角度
为了在程序界面上看到与人眼观察一致的视频图像,我们需要对相机图像做一定的旋转才能达到目的,这就是本小结将要介绍的内容。我们设计的应用程序有横屏或竖屏等不同的布局方式,同时相机在出厂时也设定了一个默认的安装方式与成像角度;因此为了在界面上看到正常的图像,需要结合这两个信息来计算一个预览图像的旋转角度。为了说清楚这个问题,涉及到几个概念:自然方向、屏幕方向和相机图像方向,可能比较绕,下面做详细的介绍。

自然方向:一般我们使用手机都是采用竖屏的方式,因此定义手机的自然方向就是竖屏的状态。在这个“自然方向”,手机的左上角为它的渲染坐标系原点,向右为x方向,向下为y方向。而平板的自然方向是横屏,在此不再做介绍,后续都是围绕手机为例做的说明。



屏幕方向(rotation):大多数情况下,我们都采用“自然方向”的竖屏布局模式;也存在很多场景设定不同的布局方式,比如拍证件采用横屏,游戏时根据传感器来自动调整界面布局。为了知道屏幕当前的切换方向,我们可以通过activity.getWindowManager().getDefaultDisplay().getRotation() 方法来获取。这角度值是指从“自然方向”切换到当前画面正方向的顺时针旋转角度,这个旋转方向正好与实际手机的旋转方向相反。下图我们给出了详细的示例图,会有个更直观的理解。



相机图像方向(info.orientation):预览视频数据来自于手机的图像传感器硬件,这个传感器在被安装到手机上后有一个默认的取景方向,与屏幕旋转、横竖屏无关。在“自然方向”下,大多数后置图像传感器的上边与右侧屏幕平行,因此它的坐标系为:右上角为原点,沿右侧屏幕向下为x正方向,向左为y正方向,因此我们获得的图像是横向的。为了在手机屏幕上正确显示图像,相机图像需要顺时针旋转90度。因此,大多情况下使用后置相机时,info.orientation=90,前置相机为270。当然,并不是所有设备都遵循这一规则,也存在一些特殊机器他们的orientation会是0或者180,所以需要通过代码来获取当前手机相机图像方向的真实值。


通过下图,可以更直接的看到测试机(Huawei nova2)获取的图像与现实世界图像的关系。


预览旋转角度:通过setDisplayOrientation设定预览图像旋转的角度,使得视频数据在界面上正常显示。这个操作本身只是改变了预览的角度,相机回调获得的帧图像数据角度并没有发生改变,所以后续对图像的处理仍需要做旋转处理。下图更直观的给出了后置摄像头在不同屏幕角度下,相机成像的画面,以及想要在屏幕上正常显示时需要设置的顺时针旋转角度值。


官方文档给出了获取预览方向的计算方法:

public static void setCameraDisplayOrientation(Activity activity,
         int cameraId, android.hardware.Camera camera) 
{
     android.hardware.Camera.CameraInfo info =
             new android.hardware.Camera.CameraInfo();
    
     android.hardware.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.setDisplayOrientation(result);
 }


3.3 设置预览大小
一般相机支持的预览大小会有很多种,通过getSupportedPreviewSize来获取实际支持的图片大小。比如通过下述代码获取测试机(Huawei nova2)支持的预览尺寸。

List<Size> previewSizes = parameters.getSupportedPreviewSizes();

for (int i = 0; i < previewSizes.size(); i++)
{
    Size sz = previewSizes.get(i);
    Log.i("size", String.format("w: %d, h: %d",sz.width,sz.height));
}

输出结果:

w: 1920, h: 1080

w: 1440, h:1080

w: 1280, h: 960

w: 1280, h:720

w: 960, h: 720

w: 960, h: 554

......

w: 176, h:144

既然手机支持这么多的尺寸,到底选择哪个合适呢?一般建议选择与预览界面宽高等比例的尺寸,这样可以确保相机预览的画面不变形,但这仅考虑了画面形变的适配性问题。对于图像检测识别等场合,图像分辨率是个很重要的参数,小分辨率会导致目标像素占比过低不利于检测,太大分辨率则导致计算耗时较多。因此,最佳的选择完全依赖于项目的需求。实践中,我们采取下面规则寻找最合适的尺寸: 1. 图像宽高比与屏幕宽高比例接近 2. 图像宽或高满足最小分辨率的要求

public Size getPreviewSize(Camera.Parameters para){
	
	List<Size> previewSizes = para.getSupportedPreviewSizes();
		
	float ratio;    
    if(bPortraint)
        ratio = ((float)ScreenHeight)/((float)ScreenWidth);		//竖屏	
    else
        ratio = ((float)ScreenWidth)/((float)ScreenHeight);	    //横屏
	
    int   minDim = Math.max(ScreenWidth,ScreenHeight);    //最小尺寸
    
    float min_diff = Float.MAX_VALUE;
    int min_diff_index = -1;
    for (int j=0; j<previewSizes.size(); j++)
    {
        float r = ((float)previewSizes.get(j).width)/((float)previewSizes.get(j).height);
		float diff = Math.abs(r - ratio);
        if(diff<min_diff && previewSizes.get(j).width>=minDim)
		{
            min_diff = diff;
            min_diff_index = j;			
		}
    }				
	
    if(min_diff_index != -1)
        return previewSizes.get(min_diff_index);

    //如果没找到合适的,则寻找尺寸与屏幕分辨率最接近的作为兜底
    int diff = Integer.MAX_VALUE;
    for (int j=0; j<previewSizes.size(); j++)
    {
        int newDiff;
        if(bPortraint)
            newDiff = Math.abs(previewSizes.get(j).width - ScreenHeight) + Math.abs(previewSizes.get(j).height - ScreenWidth);
		else
            newDiff = Math.abs(previewSizes.get(j).width - ScreenWidth) + Math.abs(previewSizes.get(j).height - ScreenHeight);
	    if (newDiff < diff) {
            min_diff_index = j;
            diff = newDiff;
        }		
    }
	return previewSizes.get(min_diff_index);		
}


3.4 设置数据格式
一般相机生产的视频数据都是按照一定格式来组织的,通过getSupportedPreviewFormats来获取实际支持的数据格式。比如通过下述代码获取测试机(Huawei nova2)支持的数据格式。

List<Integer> formats = parameters.getSupportedPreviewFormats();
	
for (int i = 0; i < formats.size(); i++)
{
    int v = formats.get(i);
    Log.i("format", String.format("%d",v));
}

输出结果:

842094169         //Planar 4:2:0 YCrCb format
17                       //NV21 encoding format

利用setPreviewFormat方法可以设置想要的数据格式,最常见的数据格式为NV21(YCbCr_420_SP)。如果不设置,默认返回数据也是NV21编码的数据格式。为了获得预览的数据,还需要给相机设置一个预览回调函数(PreviewCallback),在这个回调中实现一个方法 onPreviewFrame(byte[] data, Camera camera),然后就可以获取Camera预览时的视频帧数据。

public void initCamera(){
    ......
        
    //设置数据格式
    Camera.Parameters para = myCamera.getParameters();
    para.setPreviewFormat(PixelFormat.NV21); //PixelFormat.YCbCr_420_SP
    myCamera.setParameters(para);

    //设置监听预览回调
    myCamera.setPreviewCallback(videocb);
}

public PreviewCallback videocb = new Camera.PreviewCallback() {
    
    public void onPreviewFrame(byte[] data, Camera camera) {
		// TODO Auto-generated method stub
		//video process algorithm
	}
};


需要注意的是,onPreviewFrame函数返回的data数据格式是NV21,并不同于我们日常熟悉的RGB格式。在这里,我们有必要对该格式做一个详细介绍,后续解析灰度图或RGB彩色图都依赖于对该数据格式的理解。NV21图像格式属于YUV颜色空间的YUV420SP格式,每四个Y分量共用一组U分量和V分量。整个数据的大小为(w*h+w*h/2),其中w*h对应Y通道的数据大小,UV通道占的大小为 w*h/2=2*(w*h/4)。因为四个Y分量共用一组UV分量,因此U分量和V分量大小各占w*h/4。数据的排序方式如下所示,前w*h为Y通道的数据,后面w*h/2为UV通道的数据,其中UV是按顺序交叉存储。

通过下图可以更好的理解4个Y分量是如何共享一组UV分量的,我们利用相同的颜色块来说明Y与UV通道数据的关联性。比如,Y0、Y1、Y8、Y9四个Y分量,共享U0、V0分量;Y22、Y23、Y30和Y31四个Y分量共享 U7、V7分量。


在知道了YUV数据各通道间的关联关系后,我们就可以比较容易的获取每个像素位置的颜色值。首先获得YUV空间下的值,然后利用颜色转换公式计算RGB空间下的值。

像素位置

YUV颜色值

RGB颜色值

0

(Y0, U0, V0)


R = Y+1.4075*(V-128)
G = Y-0.3455*(U-128)-0.7169*(V-128)
B = Y+1.779*(U-128)

1

(Y1, U0, V0)

8

(Y8, U0, V0)

9

(Y9, U0, V0)

18

(Y19, U5, V5)

22

(Y22, U7, V7)

31

(Y31, U7, V7)


3.5 设置对焦
通过getSupportedFocusModes()可以获取到手机支持的对焦模式,比如通过下述代码可以打印出测试机(Huawei nova2)后置相机支持的对焦模型。

List<String> focusModeList = parameters.getSupportedFocusModes();
for (int i = 0; i < focusModeList.size(); i++)
{
    String focusMode = focusModeList.get(i);
    Log.i("FOCUS_MODE", String.format("%s",focusMode));
}

输出结果:

infinity
auto
continuous-video
continuous-picture


知道了相机支持的对焦模式后,就可以通过setFocusMode来设定一个模式。看起来很简单的一个设置问题,在实际的项目中要想获得很好的体验,对焦这件事又变的比较麻烦。往往会结合项目的实际需求来做一些定制化的处理,比如常见的有三种实现自动对焦的方式:

  1. 图像识别:通过对拍摄的图像进行分析,判断是否模糊,如果模糊调用1次自动对焦功能。这也符合人的主观认知,真正实现时会涉及图像模糊判断的算法准确度问题。
  2. 定时对焦:相对容易的方式,设定一个定时器,每隔一定的时间(比如2s)让相机做一次对焦操作。
  3. 传感器:基于手机传感器来判断运动状态,当运动结束时调用一次对焦,重点关注相机对准目标那一刻的清晰度。


4. 开启/结束预览
开启和结束预览的API比较简单,分别对应startPreview和stopPreview。开启预览后我们肯定是希望看到视频图像,因此在调用startPreview之前,需要将相机与显示图层相关联。需要用到的方法是camera.setPreviewDisplay(SurfaceHolder holder),这里就涉及到SurfaceView相关的知识。SurfaceView本身是一个View,但它与View的区别主要有几点:

  • View绘图效率不高,属于系统主动刷新绘制,适合界面动画较少的程序。而SurfaceView的效率较高,适合被动更新且界面刷新频繁的程序,比如游戏、视频播放和相机预览。
  • View在主线程,而SurfaceView在子线程进行页面刷新,因此不占用主线程资源。
  • SurfaceView实现了双缓冲机制,因此播放视频时画面更流畅。

基于这些特性,所以在相机预览时采用SurfaceView来实现。 SurfaceView有两个成员变量:一个是Surface对象,另一个是SurfaceHolder对象。通过getHolder()方法来获取当前SurfaceView的SurfaceHolder对象;通过SurfaceHolder中的回调可以知道Surface的状态(创建、变化、销毁)。SurfaceView的使用虽然比View要复杂,但是SurfaceView在使用时有一套使用的模板代码,大部分的SurfaceView绘图操作都可以套用这样的模板代码来进行编写。

private SurfaceView mysurface;
private SurfaceHolder myholder;

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    
    mysurface = (SurfaceView)findViewById(R.id.surface);
    myholder = mysurface.getHolder();
    myholder.addCallback(this);    
    ......
}

public void surfaceCreated(SurfaceHolder holder) {
    ; //surface第一次创建的时候回调
}

public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    //surface变化的时候回调(格式/大小)
    initCamera();
}

public void surfaceDestroyed(SurfaceHolder holder) {
	; //surface销毁的时候回调
}  

private void initCamera(){
    ......
   myCamera = Camera.open(id);
    
    //Sets the Surface to be used for live preview
    myCamera.setPreviewDisplay(myholder);
}


备注:更多有关SurfaceView和SurfaceHolder的内容可以参考官方介绍:Surface 和 SurfaceHolder  |  Android 开源项目  |  Android Open Source Project

5. 关闭相机
在程序退出或满足业务条件后希望关闭相机,先停止预览和回调,然后释放相机即可。

//Removes a previously added Callback interface from this holder
surfaceHolder.removeCallback(this);

mCamera.setPreviewCallback(null);
mCamera.stopPreview();

Re-locks the camera to prevent other processes from accessing it
mCamera.lock();    

//Disconnects and releases the Camera object resources
mCamera.release();
mCamera = null;


6. 附Camera主要的类和接口

若有收获,就点个赞吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值