ZXing源碼解析三:相機的配置與數據的處理

上一篇文章主要是掌握ZXing解碼總體的步驟,關於細節方面的代碼就一筆帶過了,本篇文章將會深刻細節,更詳細的講解有關相機配置方面的知識。java

ZXing的相機初始配置

  直接看代碼,找到調用相機初始化配置的代碼,上篇文章已經分析了在CaptureActivity中怎麼調到initCamera方法的,這裏再次看下這個方法的代碼,以下git

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中的代碼,以下github

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方法中的代碼,以下app

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);
  }
複製代碼

雖然這個方法代碼有點多,可是由於這個方法是用來相機初始配置的,因此,要詳細的分析一下,首先看下這部分的代碼ide

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;
    }
複製代碼

相信沒有相機開發經驗的同窗,看到這段代碼會一臉懵逼,不要緊,咱們一步步來,在理解這段代碼前,須要咱們掌握下面的一些概念。post

  • 屏幕座標: 在Android系統中,屏幕的左上角是座標系統的原點(0,0)座標。原點向右延伸是X軸正方向,原點向下延伸是Y軸正方向。
  • 天然方向: 每一個設備都有一個天然方向,手機和平板的天然方向不一樣。手機的天然方向是portrait(豎屏),平板的天然方向是landscape(橫屏)。
  • 圖像傳感器(Image Sensor)方向: 手機相機的圖像數據都是來自於攝像頭硬件的圖像傳感器,這個傳感器在被固定到手機上後有一個默認的取景方向,這個方向以下圖所示,座標原點位於手機橫放時的左上角:

  • 相機圖像的預覽方向: Android 系統提供一個 API 來手動設置 Camera 的預覽方向,叫 setDisplayOrientation。默認狀況下這個值是0,與圖像 Sensor 方向一致,因此對於橫屏應用來講就不須要更改這個 Camera 預覽方向。 可是,若是你的應用是豎屏應用,就必須經過這個 API 將 Camera 的預覽方向旋轉 90 度,讓攝像頭預覽方向與手機屏幕方向保持一致,這樣纔會獲得正確的預覽畫面。
  • 相機採集照片的方向: 這個與相機的預覽方向無關,相機採集照片的方向與Image Sensor 方向一致,若是豎屏拍照後直接保存,這時候保存的照片會是橫屏的。

強烈建議你們先看下這篇文章 Android: Camera相機開發詳解(上) —— 知識儲備,相信看過以後,你就會理解上面的代碼了,其實,上面代碼的做用就是設置相機採集圖片的預覽方向,就是不管手機是橫屏仍是豎屏,你看到的圖像都是與手機方向一致的。優化

設置相機預覽圖像的最佳比例

  文章前部分,已經分析了ZXing設置預覽方向的代碼,可是隻設置預覽方向仍是不夠的,還要根據屏幕的寬高比來找到相機採集圖片最合適的預覽尺寸,不然就會出現相機預覽圖拉伸變形的問題。   繼續看initFromCameraParameters方法中的代碼,以下ui

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變量是屏幕分辨率,從這個變量中能夠分別獲取屏幕寬高的像素值。咱們來重點看下這兩句代碼this

cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution);
    
複製代碼

上面的一句代碼是獲取相機的最佳分辨率,下面的一句代碼是獲取獲取相機的最佳預覽尺寸。如今來看下是怎麼獲取最佳尺寸的,findBestPreviewSizeValue方法的代碼以下idea

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中,這裏就不詳細分析了。

旋轉採集圖片的方向

  這裏沒有處理採集的照片,採集到的照片數據仍是橫屏的,以下

這個圖片是我豎屏時掃描的,可是獲取相機採集的數據確是橫屏的,因此,須要進行一些處理。 首先,須要在相機捕獲圖像數據成功的回調方法 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方法。這裏我就補貼具體的代碼了,你們根據前文的內容和本身的思考來修改裏面的代碼。

結束語

  文章主要分析了相機配置的代碼,選擇拍攝圖像的最佳尺寸及處理相機採集到的數據,重點是要理解相機的數據採集與圖像預覽的設置。本篇修改的代碼在這裏

參考文章
Android: Camera相機開發詳解(上) —— 知識儲備
Android 相機預覽須要注意的幾點

本文已由公衆號「AndroidShared」首發

歡迎關注個人公衆號
掃碼關注公衆號,回覆「獲取資料」有驚喜
相關文章
相關標籤/搜索