2021SC@SDUSC Zxing开源代码(三)CameraManager代码分析


前言

通过上篇代码分析可知,在zxing项目中,进入页面后就需要调用 android 的相机服务,打开相机完成初始配置后以待使用。在相关代码中,CameraManager类是整个camera包中的核心类。因此本篇博客重点对该部分代码进行分析,了解摄像头配置过程,为后续分析扫码流程等核心代码做好铺垫。


一、camera包

首先大体了解一下在Zxing中与Android系统camera服务相关的包和类:

类名作用
CameraManager该类封装了相机的所有服务,是camera包中的核心类
CameraConfigurationManager摄像头参数的设置类(具体模式等参数配置在CameraConfigurationUtils类中,该类只是调用)
CameraConfigurationUtils摄像头具体配置类,是为CameraConfigurationManager服务的工具类
AutoFocusManager自动对焦管理类,由于对焦不是一次性完成的任务,而系统提供的对焦仅有Camera.autoFocus()方法, 因此需要一个线程来不断调用Camera.autoFocus(),直到用户按下快门为止
FrontLightMode闪光灯枚举类(开,关,自动)
PreviewCallback该类的作用是在预览界面加载好后向ui线程发消息
open包里面是打开摄像头的接口类

二、Camera内部类-CameraInfo

在Camera类中,有很多内部类和方法。其中CameraInfo类用来描述相机信息,主要包括以下两个成员变量:

1. facing

facing 代表相机的方向,它的值只能是CAMERA_FACING_BACK(后置摄像头) 或者CAMERA_FACING_FRONT(前置摄像头)

2. orientation

orientation是相机采集图片的角度。这个值是相机所采集的图片顺时针旋转至自然方向的角度值,其必须是0,90,180或270中的一个。(注意这个方向与预览方向不同)
在这里插入图片描述

  • 自然方向:每个设备都有一个自然方向,手机和平板的自然方向不同。手机的自然方向是portrait(竖屏),平板的自然方向是landscape(横屏)
  • 屏幕旋转角度:根据方法Activity.getWindowManager().getDefaultDisplay().getRotation()得到,可以理解为将手机旋转的角度,有四个取值:ROTATION_0 (portrait)、ROTATION_90 (landscape)、ROTATION_180 (reverse-portrait)、ROTATION_270 (reverse-landscape)
  • 相机采集图片方向(orientation):相机所采集的图片顺时针旋转至自然方向的角度值。相机的图像数据都是来自于硬件的图像传感器,摄像头的方向取决于图像传感器的安装方向。一般来讲前置摄像头是270,后置摄像头是90,是固定值。也就是说,当点击拍照后保存图片的时候,需要对图片做旋转处理,使其为"自然方向"。
    在这里插入图片描述
  • 相机预览方向(setDisplayOrientation):实际上是相机预览方向顺时针旋转到自然方向的角度值。默认情况下这个值是0,与图像传感器方向一致,所以对于横屏应用来说就不需要更改这个 Camera 预览方向。但是,如果应用是竖屏应用,就必须通过这个 API 将 Camera 的预览方向旋转 90 度,让摄像头预览方向与手机屏幕方向保持一致,这样才会得到正确的预览画面。同时前置摄像头在进行角度旋转之前,图像会进行一个水平的镜像翻转,所以用户在看预览图像的时候就像照镜子一样。
    在这里插入图片描述

这里需要重点理解一下:相机采集图片方向(orientation)是一个固定值,在拍照后为了保证采集图像所显示的方向与人眼所见保持一致,仍需旋转的一定角度。而相机预览方向(setDisplayOrientation)是在拍照前预览图像的旋转角度

三、CameraManager代码分析

构造函数 CameraManager

由上一篇博客可知,在 CaptureActivity 的 onResume 方法中有

cameraManager = new CameraManager(getApplication());

即一打开页面就要创建一个CameraManager的对象,所以首先看一下CameraManager的构造函数

  public CameraManager(Context context) {
  	// 获取环境上下文
    this.context = context;
    // 创建相机工具管理类,同时传入上下文(Application)
    this.configManager = new CameraConfigurationManager(context);
    // 创建预览回调类
    previewCallback = new PreviewCallback(configManager);
  }
  • 这里需要注意一下传入的参数,在调用时传入的是getApplication(),由于在一个程序运行时Application对象只有一个,使用getApplication()就可以获取Application的唯一对象实例。
  • 而构造函数的参数类型是Context,这是因为Application和Activity都继承自Context,他们都是环境上下文,只不过Application是随着应用(或者包)启动的时候就存在的环境,Activity是一个界面的环境。

打开相机驱动方法 openDriver

上篇博客中提到,在 CaptureActivity 的 initCamera 方法中进行了相机的初始化,调用了openDriver方法,这个方法的主要功能是打开相机驱动并且初始化硬件参数

public synchronized void openDriver(SurfaceHolder holder) throws IOException
  • 这里加上了synchronized,保证同步性,即一次只能有一个线程进入该方法,其他线程要想在此时调用该方法,只能排队等候。
  • SurfaceHolder是一个接口,类似于一个surface的监听器
1. 获取手机的摄像头
    //OpenCamera是open包中的一个类,里面有一个Camera类作为属性
    OpenCamera theCamera = camera;
    if (theCamera == null) {
      //直接调用打开相机的接口,其中requestedCameraId标识当前要打开的camera
      theCamera = OpenCameraInterface.open(requestedCameraId);
      if (theCamera == null) {
        throw new IOException("Camera.open() failed to return object from driver");
      }
      camera = theCamera;
    }

设备上每一个物理摄像都是有一个id的,id从0开始,到getNumberOfCameras() - 1 结束;比如一般的手机上都有前后两个摄像头,那么后置摄像头id就是0,前置摄像头id就是1

2. 相机参数初始化
    //是否已经初始化,没有初始化则进行初始化
    if (!initialized) {
      initialized = true;
      //设置相机初始化参数
      configManager.initFromCameraParameters(theCamera);
      //设置相机界面矩形框的位置和大小
      if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
        setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
        requestedFramingRectWidth = 0;
        requestedFramingRectHeight = 0;
      }
    }

在这里首先调用了CameraConfigurationManager实例对象的 initFromCameraParameters 方法,也就是初始化摄像头的参数,下面重点分析一下该方法。

a. 初始化摄像头参数 initFromCameraParameters

下面分析 initFromCameraParameters 方法中处理图像方向的代码

   void initFromCameraParameters(OpenCamera camera) {
    Camera.Parameters parameters = camera.getCamera().getParameters();
    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = manager.getDefaultDisplay();  
    //获取相机预览方向
    int displayRotation = display.getRotation();
    //这个角度值是相机预览图片需要顺时针旋转至自然方向的角度值
    int cwRotationFromNaturalToDisplay;
    switch (displayRotation) {
      case Surface.ROTATION_0:
        cwRotationFromNaturalToDisplay = 0;
        break;
      case Surface.ROTATION_90:
        cwRotationFromNaturalToDisplay = 90;
        break;
      case Surface.ROTATION_180:
        cwRotationFromNaturalToDisplay = 180;
        break;
      case Surface.ROTATION_270:
        cwRotationFromNaturalToDisplay = 270;
        break;
      default:
        // 特殊情况下,可能返回值是负数如-90,需要进行下处理
        if (displayRotation % 90 == 0) {
          cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360;
        } else { //其他值报错
          throw new IllegalArgumentException("Bad rotation: " + displayRotation);
        }
    }
    Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay);

    //这个角度值是相机所采集的图片需要顺时针旋转至自然方向的角度值
    int cwRotationFromNaturalToCamera = camera.getOrientation();
    Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera);
    // 使用前置摄像头时需要进行镜像翻转
    if (camera.getFacing() == CameraFacing.FRONT) {
      cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
      Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
    }
	//计算最终需要调整的角度
    cwRotationFromDisplayToCamera =
        (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360;
    Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera);
    // 使用前置摄像头时需要进行镜像翻转
    if (camera.getFacing() == CameraFacing.FRONT) {
      Log.i(TAG, "Compensating rotation for front camera");
      cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360;
    } else {
      cwNeededRotation = cwRotationFromDisplayToCamera;
    }
    Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation);

这里涉及到了比较多个有关角度的变量,下面来梳理一下:

  • 这里调整方向的目的就是使拍照后得到的图片画面(相机采集方向)和手机屏幕中显示的画面(相机预览方向)效果相同。
  • 通过 display.getRotation() 得到的是从相机预览方向旋转到自然方向的角度值
  • 通过camera.getOrientation()得到的是从相机采集图片方向旋转到自然方向的角度值
  • 二者相减就是从相机采集图片方向到相机预览方向的角度值(加360度后取模来调整正负)
  • 如果是前置摄像头的话,还需要多一步镜像翻转

以上是Zxing在设置预览方向的代码,但是只设置预览方向还是不够的,还要根据屏幕的宽高比来找到相机采集图片最合适的预览尺寸,否则就会出现相机预览图拉伸变形的问题

 	//获取屏幕分辨率,从这个变量中可以分别获取屏幕宽高的像素值
 	Point theScreenResolution = new Point();
    display.getSize(theScreenResolution);
    screenResolution = theScreenResolution;
    Log.i(TAG, "Screen resolution in current orientation: " + screenResolution);
    //获取相机的最佳分辨率
    cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    Log.i(TAG, "Camera resolution: " + cameraResolution);
    //获取相机的最佳预览尺寸
    bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    Log.i(TAG, "Best available preview size: " + bestPreviewSize);

    boolean isScreenPortrait = screenResolution.x < screenResolution.y;
    boolean isPreviewSizePortrait = bestPreviewSize.x < bestPreviewSize.y;

    if (isScreenPortrait == isPreviewSizePortrait) {
      previewSizeOnScreen = bestPreviewSize;
    } else {
      previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x);
    }
    Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen);

在上述代码中,最为重要部分的就是获取相机的最佳分辨率(预览尺寸)了,这里调用了CameraConfigurationUtils 类中 findBestPreviewSizeValue 方法,下面详细分析下这部分代码

b. 获取相机最佳分辨率 findBestPreviewSizeValue
  public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
    //首先获取相机参数,获得相机支持的预览图片大小,返回值是一个List<Size>数组
    List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes();
    if (rawSupportedSizes == null) {
      Log.w(TAG, "Device returned no supported preview sizes; using default");
      // 如果未获取到相机支持的预览图片大小,直接设置默认值
      Camera.Size defaultSize = parameters.getPreviewSize();
      if (defaultSize == null) {
        throw new IllegalStateException("Parameters contained no preview size!");
      }
      //返回默认的宽高
      return new Point(defaultSize.width, defaultSize.height);
    }
    if (Log.isLoggable(TAG, Log.INFO)) {
      StringBuilder previewSizesString = new StringBuilder();
      for (Camera.Size size : rawSupportedSizes) {
        previewSizesString.append(size.width).append('x').append(size.height).append(' ');
      }
      Log.i(TAG, "Supported preview sizes: " + previewSizesString);
    }
   //计算屏幕宽高比
    double screenAspectRatio = screenResolution.x / (double) screenResolution.y;

    // 找的合适的size以及最大分辨率
    int maxResolution = 0;
    Camera.Size maxResPreviewSize = null;
    for (Camera.Size size : rawSupportedSizes) {
      int realWidth = size.width;
      int realHeight = size.height;
      int resolution = realWidth * realHeight;
      if (resolution < MIN_PREVIEW_PIXELS) {
        continue;
      }
      //判断size是竖向还是横向
      boolean isCandidatePortrait = realWidth < realHeight;
      int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
      int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
      //根据宽高比值差异(当前size分辨率和屏幕分辨率的差异)进行淘汰,差异大于MAX_ASPECT_DISTORTION,这个值就会从列表中删除
      double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight;
      double distortion = Math.abs(aspectRatio - screenAspectRatio);
      if (distortion > MAX_ASPECT_DISTORTION) {
        continue;
      }
      //当前的尺寸与屏幕大小相等,则作为最优尺寸返回
      if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) {
        Point exactPoint = new Point(realWidth, realHeight);
        Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint);
        return exactPoint;
      }
      // 遍历中记录下最大的分辨率
      if (resolution > maxResolution) {
        maxResolution = resolution;
        maxResPreviewSize = size;
      }
    }
    //如果没有找到精确等于屏幕大小的尺寸,则选择最大的预览尺寸
    if (maxResPreviewSize != null) {
      Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
      Log.i(TAG, "Using largest suitable preview size: " + largestSize);
      return largestSize;
    }
    // 如果没有找到精确尺寸和最大尺寸,则返回默认尺寸
    Camera.Size defaultPreview = parameters.getPreviewSize();
    if (defaultPreview == null) {
      throw new IllegalStateException("Parameters contained no preview size!");
    }
    Point defaultSize = new Point(defaultPreview.width, defaultPreview.height);
    Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize);
    return defaultSize;
  }

可见Zxing项目在寻找最佳尺寸值的方法如下:

  • 首先,查找手机支持的预览尺寸集合,如果集合为空,就返回默认的尺寸;否则,对尺寸集合根据尺寸的像素从小到大进行排序
  • 其次,移除不满足最小像素要求的所有尺寸
  • 再者,在剩余的尺寸集合中,剔除预览宽高比与屏幕分辨率宽高比之差的绝对值大于0.15的所有尺寸
  • 最后,寻找能够精确的与屏幕宽高匹配上的预览尺寸,如果存在则返回该宽高比;如果不存在,则使用尺寸集合中最大的那个尺寸。如果说尺寸集合已经在前面的过滤中被全部排除,则返回相机默认的尺寸值。

这里遗留下来一个问题,如果没有找到精准匹配的尺寸,直接使用最大尺寸的话,可能与屏幕的尺寸比有较大的差距,这样就会出现预览图像变形的问题。这里将作为后续优化的一个方面。

3. 相机参数配置
    Camera cameraObject = theCamera.getCamera();
    Camera.Parameters parameters = cameraObject.getParameters();
    String parametersFlattened = parameters == null ? null : parameters.flatten(); // flatten()是android.hardware.camera中的一个方法,把相机的所有参数都放到一个字符串里
    try {
    	//设置相机模式等配置参数
      configManager.setDesiredCameraParameters(theCamera, false);
    } catch (RuntimeException re) {
      // Driver failed
      Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters");
      Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened);
      // Reset:
      if (parametersFlattened != null) {
        parameters = cameraObject.getParameters();
        parameters.unflatten(parametersFlattened);
        try {
          cameraObject.setParameters(parameters);
          configManager.setDesiredCameraParameters(theCamera, true);
        } catch (RuntimeException re2) {
          // Well, darn. Give up
          Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration");
        }
      }
    }
    //设置一个Surface对象用来实时预览
    cameraObject.setPreviewDisplay(holder);

在这里调用了CameraConfigurationManager实例对象的 setDesiredCameraParameters 方法,为相机配置其他相关参数

相机参数配置 setDesiredCameraParameters
  void setDesiredCameraParameters(OpenCamera camera, boolean safeMode) {
    //获取设备的参数
    Camera theCamera = camera.getCamera();
    Camera.Parameters parameters = theCamera.getParameters();
    if (parameters == null) {
      Log.w(TAG, "Device error: no camera parameters are available. Proceeding without configuration.");
      return;
    }
    Log.i(TAG, "Initial camera parameters: " + parameters.flatten());
    //判断是否处于安全模式
    if (safeMode) {
      Log.w(TAG, "In camera config safe mode -- most settings will not be honored");
    }
    //SharedPreferences是一个轻量级的存储类,特别适合用于保存软件配置参数
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    //初始化闪光灯
    initializeTorch(parameters, prefs, safeMode);
    //设置聚焦
    CameraConfigurationUtils.setFocus(
        parameters,
        //是否自聚焦(当光线较暗时自动打开闪光灯)
        prefs.getBoolean(PreferencesActivity.KEY_AUTO_FOCUS, true),
        //是否持续聚焦
        prefs.getBoolean(PreferencesActivity.KEY_DISABLE_CONTINUOUS_FOCUS, false),
        safeMode);

    if (!safeMode) {
      //是否反置颜色
      if (prefs.getBoolean(PreferencesActivity.KEY_INVERT_SCAN, false)) {
        CameraConfigurationUtils.setInvertColor(parameters);
      }
      //是否设置条形码场景
      if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_BARCODE_SCENE_MODE, true)) {
        CameraConfigurationUtils.setBarcodeSceneMode(parameters);
      }
      if (!prefs.getBoolean(PreferencesActivity.KEY_DISABLE_METERING, true)) {
        //设置视频稳定模式
        CameraConfigurationUtils.setVideoStabilization(parameters);
        //设置焦点区域
        CameraConfigurationUtils.setFocusArea(parameters);
        //设置自动白平衡和自动曝光补偿
        CameraConfigurationUtils.setMetering(parameters);
      }
      parameters.setRecordingHint(true);
    }
    //设置相机预览尺寸
    parameters.setPreviewSize(bestPreviewSize.x, bestPreviewSize.y);
    //为相机配置参数
    theCamera.setParameters(parameters);
    //将捕获的画面旋转cwRotationFromDisplayToCamera角度显示
    theCamera.setDisplayOrientation(cwRotationFromDisplayToCamera);
    //获取相机预览尺寸
    Camera.Parameters afterParameters = theCamera.getParameters();
    Camera.Size afterSize = afterParameters.getPreviewSize();
    if (afterSize != null && (bestPreviewSize.x != afterSize.width || bestPreviewSize.y != afterSize.height)) {
      Log.w(TAG, "Camera said it supported preview size " + bestPreviewSize.x + 'x' + bestPreviewSize.y +
          ", but after setting it, preview size is " + afterSize.width + 'x' + afterSize.height);
      bestPreviewSize.x = afterSize.width;
      bestPreviewSize.y = afterSize.height;
    }
  }

这部分代码中,使用了许多CameraConfigurationUtils类中参数设置的方法,这也是前面讲其为CameraConfigurationManager的工具类的原因。而在CameraConfigurationUtils类中也是调用android.hardware.Camera.Parameters中的服务进行摄像头参数配置,层级调用,界限分明。

CameraManager类中以 openDriver 作为关键方法进行了详细分析,下面还有一些其他方法,这里进行简要说明,如果在后续扫码流程中用到再进行展开分析:

其他方法

  • 关闭相机驱动 closeDriver:这里需要调用camera.getCamera().release()方法释放摄像头资源
  • 开始预览 startPreview:使相机硬件在屏幕上绘制预览界面
  • 结束预览 stopPreview:停止绘制预览界面
  • 设置闪光灯 setTorch
  • 返回相机预览界面中的一帧 requestPreviewFrame
  • 获取相机预览界面的矩形框 getFramingRectInPreview
  • 设置相机预览界面矩形框的位置和大小 setManualFramingRect
  • 构造基于平面的YUV亮度源 buildLuminanceSource

总结

经过以上的代码分析,对于Android系统中的camera服务有了大致了解,基本上理清了摄像头开启并且配置的相关流程。但是在使用真机调试的过程中发现,Zxing 项目的Demo是只支持横屏预览的,同时在寻找相机最佳预览尺寸上,在没有找到最佳尺寸的情况下,Zxing直接使用了默认尺寸,这就有可能带来图形变形问题,为后续的图像二维码解析带来困难。因此在相机相关配置方面,的确仍需要修改代码进行优化

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
@zxing/library是一个源的Java条码图像处理库。它可以解码和生成不同类型的条码,如二维码和一维码。该库提供简单易用的API,可以将条码图像转换为对应的文本数据,或者将文本数据生成为条码图像。 使用@zxing/library,我们可以轻松地实现将一维码和二维码解码为文本数据的功能。只需提供相应的条码图像,调用库中的解码方法即可获取到条码所代表的文本数据。这对于快速处理扫描到的条码信息非常有用,例如在移动支付、电商购物和物流快递等场景下。 除了解码外,@zxing/library还提供了生成条码图像的功能。我们可以根据需要的条码类型和文本数据,调用库中的生成方法得到相应的条码图像。这样,我们可以方便地将文本数据转化为可供扫描的条码图像,用于商品标识、会员卡等应用。 @zxing/library支持多种常见的条码类型,如EAN-13、UPC-A、Code39、QR Code等。而且它还可以处理包含错误修正级别、尺寸和颜色等参数的高度定制化的条码生成需求。 由于它是源的,@zxing/library拥有一个活跃的社区,更新频繁且稳定。对于发者来说,可以轻松集成并使用该库,无需自己从头始编写条码图像处理的代码。 总之,@zxing/library是一个功能强大且易用的条码图像处理库,可以实现条码的解码和生成。无论是解码扫描到的条码信息,还是生成可供扫描的条码图像,@zxing/library都是一个值得推荐的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值