ZXing源码解析三:相机的配置与数据的处理

栏目: IOS · Android · 发布时间: 6年前

内容简介:上一篇文章主要是掌握ZXing解码整体的步骤,关于细节方面的代码就一笔带过了,本篇文章将会深入细节,更详细的讲解有关相机配置方面的知识。直接看代码,找到调用相机初始化配置的代码,上篇文章已经分析了在上篇文章分析到这句代码

上一篇文章主要是掌握ZXing解码整体的步骤,关于细节方面的代码就一笔带过了,本篇文章将会深入细节,更详细的讲解有关相机配置方面的知识。

ZXing的相机初始配置

直接看代码,找到调用相机初始化配置的代码,上篇文章已经分析了在 CaptureActivity 中怎么调到 initCamera 方法的,这里再次看下这个方法的代码,如下

private void initCamera(SurfaceHolder surfaceHolder) {
    if (surfaceHolder == null) {
      throw new IllegalStateException("No SurfaceHolder provided");
    }
    //相机已经打开
    if (cameraManager.isOpen()) {
      Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?");
      return;
    }
    try {
      //打开相机并初始化硬件参数
      cameraManager.openDriver(surfaceHolder);
      // 实例化一个handler并开始预览.
      if (handler == null) {
        handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager);
      }
      decodeOrStoreSavedBitmap(null, null);
    } catch (IOException ioe) {
      Log.w(TAG, ioe);
      displayFrameworkBugMessageAndExit();
    } catch (RuntimeException e) {
      // Barcode Scanner has seen crashes in the wild of this variety:
      // java.?lang.?RuntimeException: Fail to connect to camera service
      Log.w(TAG, "Unexpected error initializing camera", e);
      displayFrameworkBugMessageAndExit();
    }
  }
复制代码

上篇文章分析到这句代码 cameraManager.openDriver(surfaceHolder); 就直接说了这句代码的作用,并没有进入 openDriver 方法详细的看代码,这里看下 openDriver 中的代码,如下

public synchronized void openDriver(SurfaceHolder holder) throws IOException {
    OpenCamera theCamera = camera;
    if (theCamera == null) {
      //更具requestedCameraId打开对应的摄像头
      theCamera = OpenCameraInterface.open(requestedCameraId);
      if (theCamera == null) {
        throw new IOException("Camera.open() failed to return object from driver");
      }
      camera = theCamera;
    }
    //是否已经初始化,没有初始化则进行初始化
    if (!initialized) {
      initialized = true;
      configManager.initFromCameraParameters(theCamera);//分析一
      if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) {
        setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight);
        requestedFramingRectWidth = 0;
        requestedFramingRectHeight = 0;
      }
    }

    Camera cameraObject = theCamera.getCamera();
    Camera.Parameters parameters = cameraObject.getParameters();
    String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily
    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");
        }
      }
    }
    cameraObject.setPreviewDisplay(holder);

  }
复制代码

这里重点看下“分析一” initFromCameraParameters 方法中的代码,如下

void initFromCameraParameters(OpenCamera camera) {
    Camera.Parameters parameters = camera.getCamera().getParameters();
    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //获取WindowManager默认的Display
    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:
        // Have seen this return incorrect values like -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);

    // Still not 100% sure about this. But acts like we need to flip this:
    if (camera.getFacing() == CameraFacing.FRONT) {
      cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360;
      Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera);
    }

    /*
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    String overrideRotationString;
    if (camera.getFacing() == CameraFacing.FRONT) {
      overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null);
    } else {
      overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION, null);
    }
    if (overrideRotationString != null && !"-".equals(overrideRotationString)) {
      Log.i(TAG, "Overriding camera manually to " + overrideRotationString);
      cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString);
    }
     */

    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);

    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);
  }
复制代码

虽然这个方法代码有点多,但是因为这个方法是用来相机初始配置的,所以,要详细的分析一下,首先看下这部分的代码

Camera.Parameters parameters = camera.getCamera().getParameters();
    WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //获取WindowManager默认的Display
    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:
        // Have seen this return incorrect values like -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);

    // Still not 100% sure about this. But acts like we need to flip this:
    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;
    }
复制代码

相信没有相机开发经验的同学,看到这段代码会一脸懵逼,没关系,我们一步步来,在理解这段代码前,需要我们掌握下面的一些概念。

  • 屏幕坐标: 在Android系统中,屏幕的左上角是坐标系统的原点(0,0)坐标。原点向右延伸是X轴正方向,原点向下延伸是Y轴正方向。
  • 自然方向: 每个设备都有一个自然方向,手机和平板的自然方向不同。手机的自然方向是portrait(竖屏),平板的自然方向是landscape(横屏)。
  • 图像传感器(Image Sensor)方向: 手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,这个方向如下图所示,坐标原点位于手机横放时的左上角:
ZXing源码解析三:相机的配置与数据的处理
  • 相机图像的预览方向: Android 系统提供一个 API 来手动设置 Camera 的预览方向,叫 setDisplayOrientation。默认情况下这个值是0,与图像 Sensor 方向一致,所以对于横屏应用来说就不需要更改这个 Camera 预览方向。 但是,如果你的应用是竖屏应用,就必须通过这个 API 将 Camera 的预览方向旋转 90 度,让摄像头预览方向与手机屏幕方向保持一致,这样才会得到正确的预览画面。
  • 相机采集照片的方向: 这个与相机的预览方向无关,相机采集照片的方向与Image Sensor 方向一致,如果竖屏拍照后直接保存,这时候保存的照片会是横屏的。

强烈建议大家先看下这篇文章 Android: Camera相机开发详解(上) —— 知识储备 ,相信看过之后,你就会理解上面的代码了,其实,上面代码的作用就是 设置相机采集图片的预览方向,就是无论手机是横屏还是竖屏,你看到的图像都是与手机方向一致的。

设置相机预览图像的最佳比例

文章前部分,已经分析了ZXing设置预览方向的代码,但是只设置预览方向还是不够的,还要根据屏幕的宽高比来找到相机采集图片最合适的预览尺寸,否则就会出现相机预览图拉伸变形的问题。 继续看 initFromCameraParameters 方法中的代码,如下

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);
    }
复制代码

上面代码中的 screenResolution 变量是屏幕分辨率,从这个变量中可以分别获取屏幕宽高的像素值。我们来重点看下这两句代码

cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    
复制代码

上面的一句代码是获取相机的最佳分辨率,下面的一句代码是获取获取相机的最佳预览尺寸。现在来看下是怎么获取最佳尺寸的, findBestPreviewSizeValue 方法的代码如下

public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) {
    //获取相机支持的尺寸,手机不同会有不同的值
    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;

    // Find a suitable size, with max resolution
    int maxResolution = 0;
    Camera.Size maxResPreviewSize = null;
    //for循环的作用是找到相机合适的尺寸和最大的分辨率,这里
    //合适的尺寸指的是和屏幕宽高比相同的尺寸。
    for (Camera.Size size : rawSupportedSizes) {
      int realWidth = size.width;
      int realHeight = size.height;
      int resolution = realWidth * realHeight;
      if (resolution < MIN_PREVIEW_PIXELS) {
        continue;
      }

      boolean isCandidatePortrait = realWidth < realHeight;
      int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth;
      int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight;
      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;
      }

      // Resolution is suitable; record the one with max resolution
      if (resolution > maxResolution) {
        maxResolution = resolution;
        maxResPreviewSize = size;
      }
    }

    // If no exact match, use largest preview size. This was not a great idea on older devices because
    // of the additional computation needed. We're likely to get here on newer Android 4+ devices, where
    // the CPU is much more powerful.
    if (maxResPreviewSize != null) {
      Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height);
      Log.i(TAG, "Using largest suitable preview size: " + largestSize);
      return largestSize;
    }

    // If there is nothing at all suitable, return current preview size
    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;
  }

复制代码

从上面代码中的注释可以看到这里存在一些小问题, 上面代码的逻辑是有与屏幕像素比例相同的相机尺寸才返回,否则就用相机默认的尺寸,相机默认的尺寸可能与屏幕的尺寸比有较大的差距,这样就会出现预览图像变形的问题。

这里可以将代码优化为,返回最接近屏幕宽高比的相机尺寸。这里的优化将会在后面的文章中进行详细的讲解。

上面的代码是将一些变量的值设置好,最终,配置相机的参数在 CameraConfigurationManager 类中的 setDesiredCameraParameters 中,这里就不详细分析了。

旋转采集图片的方向

这里没有处理采集的照片,采集到的照片数据还是横屏的,如下

ZXing源码解析三:相机的配置与数据的处理
这个图片是我竖屏时扫描的,但是获取相机采集的数据确是横屏的,所以,需要进行一些处理。 首先,需要在相机捕获图像数据成功的回调方法 onPreviewFrame

中改变代码,更改后的代码如下

@Override
  public void onPreviewFrame(byte[] data, Camera camera) {
    Point cameraResolution = configManager.getCameraResolution();
    Handler thePreviewHandler = previewHandler;
    if (cameraResolution != null && thePreviewHandler != null) {
      Point screenResolution = configManager.getScreenResolution();
      Message message;
      if (screenResolution.x < screenResolution.y){
        // 手机为竖屏时
        message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.y,
                cameraResolution.x, data);
      } else {
        // 手机为横屏时
        message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x,
                cameraResolution.y, data);
      }
      message.sendToTarget();
      previewHandler = null;
    } else {
      Log.d(TAG, "Got preview callback, but no handler or resolution available");
    }
复制代码

解释:手机竖屏时,相机传感器采集的数据为横屏的数据,为了与竖屏相对应,需要将相机采集的图片宽高互换,这里只是互换了宽高,但是采集的数据宽高并没有转换,因此还需要将数据的宽高转换。

代码如下

//将原始图像传感器的数据转换为竖屏
    if (width < height) {
      // portrait
      byte[] rotatedData = new byte[data.length];
      for (int x = 0; x < width; x++) {
        for (int y = 0; y < height; y++)
          rotatedData[y * width + width - x - 1] = data[y + x * height];
      }
      data = rotatedData;
    }

复制代码

将上面的代码,加入到 DecodeHandler 类中的 decode 方法开头即可。

虽然,这时已经将相机采集的横屏数据转化为竖屏的了,但是,工作还没有完成,还需要设置获取二维码的区域,设置的方法是CameraManager类中的getFramingRectInPrevie方法。这里我就补贴具体的代码了,大家根据前文的内容和自己的思考来修改里面的代码。

结束语

文章主要分析了相机配置的代码,选择拍摄图像的最佳尺寸及处理相机采集到的数据,重点是要理解相机的数据采集与图像预览的设置。本篇修改的代码在 这里

本文已由公众号“AndroidShared”首发

ZXing源码解析三:相机的配置与数据的处理
扫码关注公众号,回复“获取资料”有惊喜

以上所述就是小编给大家介绍的《ZXing源码解析三:相机的配置与数据的处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

图形程序开发人员指南

图形程序开发人员指南

Michael Abrash / 前导工作室 / 机械工业出版社 / 1998 / 128

Michael Abrash's classic Graphics Programming Black Book is a compilation of Michael's previous writings on assembly language and graphics programming (including from his "Graphics Programming" column......一起来看看 《图形程序开发人员指南》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试