寫在開頭
需求方:上傳試卷的時候,用戶自己拍的照片有很多問題。如:不清晰、圖片歪了、錯誤圖片等。我們要是能夠對拍攝照片進行識別處理就好了,能夠裁切矯正就更好了,最好可以像二維碼掃描一樣,直接識別處理~
開發:滿足你!
一、整體框架邏輯
試卷掃描模塊,最核心的邏輯就是數據采集、解碼識別、圖片裁切,再加上對識別結果和裁切結果的處理,就構成了整個模塊的主邏輯(感謝多媒體同事對圖片識別與處理提供庫的支持)。整個邏輯的實現如下圖所示:
在模塊中,除了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對象。這樣的話主線程和解碼線程就建立了聯系,它們之間就可以方便得進行消息傳遞了。最終實現的模塊采集界面如下所示:
二、模塊開發相關實現
整個掃碼拍照模塊的邏輯比較瑣碎,就不一一說明了。以下是整理的幾個開發中比較關鍵的點和Camera硬件開發一些經驗,在這里做記錄,避免以后重復造輪子。
閃光燈設置
- 開啟閃光燈
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();
}
}
}
- 關閉閃光燈
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;
}
拍照圖片分辨率選擇
在硬件支持的拍照圖片分辨率列表中,拍照圖片分辨率選擇邏輯:
- 有1920*1080則選之
- 選擇大於屏幕分辨率且圖片比例為16:9的
- 選擇圖片分辨率盡可能大且圖片比例為16:9的
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");
}
}
預覽畫面不失真展示
如果預覽圖片的分辨率比例和手機畫面上展示拍攝畫面的區域比例不一致的話,就會出現畫面拉伸或者壓縮的現象。為了解決這個問題,取得更好的用戶體驗。模塊在布局的時候,對屏幕展示區域是動態計算的,以保證預覽區域比例與圖片的分辨率比例是一致的。
三、模塊開發中的那些坑
掃碼模塊開發,因為是跟手機硬件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()方法即可。
本文來自網易雲社區,經作者鄭睿授權發布。
原文地址:Android Camera開發經驗總結以及踩過的那些坑
更多網易研發、產品、運營經驗分享請訪問網易雲社區。