文章目錄
- 前言
- 一、camera包
- 二、CameraManager代碼分析
- 打開相機驅動方法 openDriver
- 1. 獲取手機的攝像頭
- 2. 相機引數初始化
- a. 初始化攝像頭引數 initFromCameraParameters
- b. 獲取相機最佳解析度 findBestPreviewSizeValue
- 3. 相機引數配置
- 相機引數配置 setDesiredCameraParameters
- 其他方法
- 總結
前言
在zxing專案中,掃碼的第一步就是要去呼叫 android 的相機服務,打開相機完成初始配置后以待使用,在相關代碼中,CameraManager類是整個camera包中的核心類,因此本篇博客重點對該部分代碼進行分析,了解攝像頭配置程序,為后續分析掃碼流程等核心代碼做好鋪墊,
一、camera包
首先先大體了解一下在Zxing中與Android系統camera服務相關的包和類:
- CameraManager:該類封裝了相機的所有服務,是camera包中的核心類
- CameraConfigurationManager:攝像頭引數的設定類(具體模式等引數配置在CameraConfigurationUtils類中,該類只是呼叫)
- CameraConfigurationUtils:攝像頭具體配置類,是為CameraConfigurationManager服務的工具類
- AutoFocusManager:自動對焦管理類,由于對焦不是一次性完成的任務(手抖),而系統提供的對焦僅有Camera.autoFocus()方法, 因此需要一個執行緒來不斷呼叫Camera.autoFocus()直到用戶滿意按下快門為止
- FrontLightMode:閃光燈列舉類(開,關,自動)
- PreviewCallback:該類的作用是在預覽界面加載好后向ui執行緒發訊息
- open包:里面是打開攝像頭的介面類
二、CameraManager代碼分析
打開相機驅動方法 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
在分析該部分代碼之前,需要先對Android系統camera方向進行一下大體了解

- 自然方向:每個設備都有一個自然方向,手機和平板的自然方向不同,手機的自然方向是portrait(豎屏),平板的自然方向是landscape(橫屏)
- 攝像頭方向:相機的影像資料都是來自于硬體的影像傳感器,攝像頭的方向取決于影像傳感器的安裝方向,絕大部分安卓手機中影像傳感器方向是橫向的,且不能改變,所以orientation是90或是270,也就是說,當點擊拍照后保存圖片的時候,需要對圖片做旋轉處理,使其為"自然方向",
- 相機預覽方向:Android 系統提供一個 API 來手動設定 Camera 的預覽方向,叫 setDisplayOrientation,默認情況下這個值是0,與影像傳感器方向一致,所以對于橫屏應用來說就不需要更改這個 Camera 預覽方向,但是,如果應用是豎屏應用,就必須通過這個 API 將 Camera 的預覽方向旋轉 90 度,讓攝像頭預覽方向與手機螢屏方向保持一致,這樣才會得到正確的預覽畫面,同時前置攝像頭在進行角度旋轉之前,影像會進行一個水平的鏡像翻轉,所以用戶在看預覽影像的時候就像照鏡子一樣,
下面分析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);
以上是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;
}
分析來看,這個方法就是對于通過相機引數所獲得的所有支持的預覽圖片尺寸,進行遍歷,排除掉那些與螢屏寬高比相差過大的一些尺寸后,優先選擇精確等于螢屏大小的尺寸,其次選擇最大尺寸,再其次回傳默認尺寸,
這里遺留下來一個問題,對于默認尺寸是未進行任何處理篩選的,相機默認的尺寸可能與螢屏的尺寸比有較大的差距,這樣就會出現預覽影像變形的問題,這里將作為后續優化的一個方面,
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直接使用了默認尺寸,這就有可能帶來圖形變形問題,為后續的影像二維碼決議帶來困難,因此這里也是一個可以進行優化的方面,
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/306271.html
標籤:其他
上一篇:Android Studio 自動生成注釋(作者、日期、聯系方式、描述)
下一篇:怎么判斷ImageView 為空
