寫在開頭
需求方:上傳試卷的時候,用戶本身拍的照片有不少問題。如:不清晰、圖片歪了、錯誤圖片等。咱們要是可以對拍攝照片進行識別處理就行了,可以裁切矯正就更好了,最好能夠像二維碼掃描同樣,直接識別處理~
開發:知足你!
1、總體框架邏輯
試卷掃描模塊,最核心的邏輯就是數據採集、解碼識別、圖片裁切,再加上對識別結果和裁切結果的處理,就構成了整個模塊的主邏輯(感謝多媒體同事對圖片識別與處理提供庫的支持)。整個邏輯的實現以下圖所示:android
在模塊中,除了UI線程,還開啓了一個Deocde線程,用來處理圖片的解碼識別和裁切。這麼作的緣由是由於對於圖片數據的處理,是比較耗時的,若是在UI線程處理,會有ANR的風險。同時採用這種處理方式,整個模塊的流暢性也更加好,且模塊的結構更加清晰。
那麼線程之間是如何交互的呢?這裏模塊中是採用了最經常使用的Handler消息傳遞機制。由於經過Handler的Message能夠在線程間傳遞較大的圖片數據(注意若是在Intent的Bundle中傳遞較大的數據,會崩潰報錯)。請看下面這段代碼:
@Override
public void run() {
Looper.prepare();
handler = new DecodeHandler(activity);
handlerInitLatch.countDown();
Looper.loop();
}
上面這個方法是DecodeThread的run方法,在方法中,咱們初始化了當前線程對應的Handler對象DecodeHandler。而DecodeHandler初始化是須要傳入當前主線程的上下文activity,經過activity咱們能夠拿到主線程的Handler對象。這樣的話主線程和解碼線程就創建了聯繫,它們之間就能夠方便得進行消息傳遞了。最終實現的模塊採集界面以下所示:框架
2、模塊開發相關實現
整個掃碼拍照模塊的邏輯比較瑣碎,就不一一說明了。如下是整理的幾個開發中比較關鍵的點和Camera硬件開發一些經驗,在這裏作記錄,避免之後重複造輪子。
閃光燈設置ide
public void turnOnFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}oop
public void turnOffFlash(){
if(camera != null){
try {
Camera.Parameters parameters = camera.getParameters();
parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
camera.setParameters(parameters);
} catch (Exception e) {
e.printStackTrace();
}
}
}
預覽圖片分辨率選擇
預覽圖片的分辨率選擇邏輯是:有1920*1080則選之,不然選硬件支持的最大的分辨率,且知足圖片比例爲16:9
private static Point findBestPreviewSizeValue(List<Camera.Size> sizeList, Point screenResolution) {
int bestX = 0;
int bestY = 0;
int size = 0;
for(int i = 0; i < sizeList.size(); i ++){
// 若是有符合的分辨率,則直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default preview size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
int newX = sizeList.get(i).width;
int newY = sizeList.get(i).height;
int newSize = Math.abs(newX * newX) + Math.abs(newY * newY);
float ratio = (float)newY / (float)newX;
Log.d(TAG, newX + ":" + newY + ":" + ratio);
if (newSize >= size && ratio != 0.75) { // 確保圖片是16:9的
bestX = newX;
bestY = newY;
size = newSize;
} else if (newSize < size) {
continue;
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}
return null;
}
拍照圖片分辨率選擇
在硬件支持的拍照圖片分辨率列表中,拍照圖片分辨率選擇邏輯: 佈局
private static Point findBestPictureSizeValue(List<Camera.Size> sizeList, Point screenResolution){
List<Camera.Size> tempList = new ArrayList<>();
for(int i = 0; i < sizeList.size(); i ++){
// 若是有符合的分辨率,則直接返回
if(sizeList.get(i).width == DEFAULT_WIDTH && sizeList.get(i).height == DEFAULT_HEIGHT){
Log.d(TAG, "get default picture size!!!");
return new Point(DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
if(sizeList.get(i).width >= screenResolution.x && sizeList.get(i).height >= screenResolution.y){
tempList.add(sizeList.get(i));
}
}
int bestX = 0;
int bestY = 0;
int diff = Integer.MAX_VALUE;
if(tempList != null && tempList.size() > 0){
for(int i = 0; i < tempList.size(); i ++){
int newDiff = Math.abs(tempList.get(i).width - screenResolution.x) + Math.abs(tempList.get(i).height - screenResolution.y);
float ratio = (float)tempList.get(i).height / tempList.get(i).width;
Log.d(TAG, "ratio = " + ratio);
if(newDiff < diff && ratio != 0.75){ // 確保圖片是16:9的
bestX = tempList.get(i).width;
bestY = tempList.get(i).height;
diff = newDiff;
}
}
}
if (bestX > 0 && bestY > 0) {
return new Point(bestX, bestY);
}else {
return findMaxPictureSizeValue(sizeList);
}
}
預覽模式循環自動對焦
預覽模式時,支持自動對焦。當前處理邏輯是在AutoFocusCallback的回調方法onAutoFocus中,延遲發送Message信息。這樣在上一次聚焦完成後,固定時間的延遲後會發送下一次的自動聚焦消息,如此達到循環聚焦的目的。
@Override
public void onAutoFocus(boolean success, Camera camera) {
Log.d(TAG, "onAutoFocus");
PaperScanConstant.isAutoFocusSuccess = true;
if (autoFocusHandler != null) {
Message message = autoFocusHandler.obtainMessage(autoFocusMessage, success);
autoFocusHandler.sendMessageDelayed(message, AUTOFOCUS_INTERVAL_MS);
autoFocusHandler = null;
} else {
Log.d(TAG, "Got auto-focus callback, but no handler for it");
}
}
預覽畫面不失真展現
若是預覽圖片的分辨率比例和手機畫面上展現拍攝畫面的區域比例不一致的話,就會出現畫面拉伸或者壓縮的現象。爲了解決這個問題,取得更好的用戶體驗。模塊在佈局的時候,對屏幕展現區域是動態計算的,以保證預覽區域比例與圖片的分辨率比例是一致的。
3、模塊開發中的那些坑
掃碼模塊開發,由於是跟手機硬件Camera打交道,基於目前市場中Android手機衆多的型號和搭載的五花八門的ROM,沒坑那是不可能的!!!下面是本模塊開發過程當中的相關坑。
部分機子拍攝照片分辨率不高
開發過程當中碰到過這麼一種狀況,在部分機子上,明明已經聚焦,手機的分辨率也很高,可是拍出的照片分辨率卻很小。究其緣由,就是不一樣的手機ROM,獲取的默認的照片分辨率是不一樣的。有的手機默認照片分辨率高,則照片就清晰;有的默認分辨率是最低的一檔,則不管你手機分辨率多高,拍出來的照片仍是很模糊的。解決方案就是須要顯示設置拍照的圖片分辨率:
parameters.setPreviewSize(cameraResolution.x, cameraResolution.y);
parameters.setPictureSize(pictureResolution.x, pictureResolution.y);
部分機子拍攝照片發生了旋轉
仍是因爲Android手機碎片化的問題,每一個手機默認拍照的旋轉角度是不同的。剛開始模塊中是按照默認旋轉90度處理,在大多數機子上是沒有問題的。可是在碰到Nexus 5X的時候就出問題了,圖片上下致使了。查閱了相關資料,Google官方提供了下面的方法,解決了這個問題。
public void setCameraDisplayOrientation(int cameraId, android.hardware.Camera camera) {
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
android.hardware.Camera.getCameraInfo(cameraId, info);
int rotation = BaseApplication.getInstance().getCurrentActivity().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;
}
// 記錄本機子相機的旋轉角度
PaperScanConstant.cameraRotation = result;
camera.setDisplayOrientation(result);
}
private int findFrontFacingCameraID() {
int cameraId = -1;
// Search for the back facing camera
int numberOfCameras = Camera.getNumberOfCameras();
for (int i = 0; i < numberOfCameras; i++) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(i, info);
if (info.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
Log.d(TAG, "Camera found");
cameraId = i;
break;
}
}
return cameraId;
}
頻繁點擊屏幕應用崩潰
由於應用支持點擊屏幕自動聚焦功能,但在某些機子上,用戶頻繁點擊屏幕進行自動聚焦,應用發生了崩潰。究其緣由是由於在某些ROM上,當上一次聚焦沒有完成時,就進行下一次聚焦,就會發生崩潰。解決方案是經過設置標誌位,只有在上一次聚焦完成後,才能進行下一次聚焦。
第三方ROM禁止了應用的攝像頭權限
有些第三方ROM會有本身的權限管理機制,當應用的攝像頭權限被禁止了,進入掃碼頁,會發生崩潰。這樣的交互體驗確定不是很好,交互要求這邊權限被禁止之後,仍是須要有一個溫和的提示,提醒用戶去設置頁面從新賦予應用攝像頭權限。可是系統也沒有提供接口說當前應用這個權限被禁止了。所以模塊中採用了一個折中的方案,監獄應用沒有攝像頭權限時候,開啓攝像頭會崩潰。所以咱們捕獲開啓Camera的異常,在捕獲異常時候彈框提醒用戶去開啓權限。
try {
CameraManager.get().openDriver(surfaceHolder);
} catch (Throwable tr){
showOpenCameraErrorDialog();
return;
}
Pad進入掃碼頁應用崩潰
實際上線時候,發現用戶使用pad的話,一進入掃碼頁面就崩潰。由於咱們應用首次進入掃碼頁面默認是開啓設備閃光燈的。可是pad沒有閃光燈,所以就崩潰了。剛開始用以下方式檢測設備是否支持閃光燈:
getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)
可是失敗了。緣由是好多pad的ROM是從手機ROM改過去的,有可能改得不是那麼完全。因此在Pad上調用如上代碼進行判斷時,仍是會返回true。這是隻能求助於try catch了。就是在開關閃光燈的時候進行異常捕獲,這樣在Pad上開關閃光燈崩潰問題就解決了。
部分機子拍照後閃光燈自動關閉
部分機子,在閃光燈開啓的狀態下,點擊拍照按鈕,閃光燈關閉了。目前沒有找到緣由,只能在模塊中加了特殊處理。針對當前有此問題的手機,拍照完後主動再去開關一次閃光燈,這樣拍照完成後,閃光燈仍是能夠亮着。只是在拍照的過程當中,會出現閃光燈閃爍的狀況。
部分機子拍照完後預覽畫面卡住了
部分機子,當點擊拍照完成一張照片的拍攝後,後面就中止不動了。出現這種現象是由於在拍照的時候,Camera會中止Preview,拍照完成後,有的機子能夠恢復回來從新Preview,有的則不會。所以只需在拍照完成後,手動調用一次Camera的startPreview()方法便可。線程
本文來自網易雲社區,經做者鄭睿受權發佈。 3d
原文地址:Android Camera開發經驗總結以及踩過的那些坑code
更多網易研發、產品、運營經驗分享請訪問網易雲社區。對象