[Android編程心得] Camera(OpenCV)自動對焦和觸摸對焦的實現

寫在前面

最近在從零開始寫一個移動端的AR系統,坑實在是太多瞭!!!整個項目使用瞭OpenCV第三方庫,但對於攝像機來說,和原生Camera的方法基本相同。

實現

以OpenCV的JavaCameraView為例,首先需要定制自己的Camera,主要代碼如下:

import java.util.ArrayList;
import java.util.List;

import org.opencv.android.JavaCameraView;

import android.R.integer;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.RectF;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.Toast;

public class MTCameraView extends JavaCameraView implements AutoFocusCallback {

	public MTCameraView(Context context, int attrs) {
		super(context, attrs);
		// TODO Auto-generated constructor stub
	}

	public List getResolutionList() {      
	    return  mCamera.getParameters().getSupportedPreviewSizes();      
	}
	
	public Camera.Size getResolution() {
	    Camera.Parameters params = mCamera.getParameters(); 
	    Camera.Size s = params.getPreviewSize();
	    return s;
	}
	
	public void setResolution(Camera.Size resolution) {
	    disconnectCamera();
	    connectCamera((int)resolution.width, (int)resolution.height);       
	}
	
	public void focusOnTouch(MotionEvent event) {
        Rect focusRect = calculateTapArea(event.getRawX(), event.getRawY(), 1f);
        Rect meteringRect = calculateTapArea(event.getRawX(), event.getRawY(), 1.5f);

        Camera.Parameters parameters = mCamera.getParameters();
        parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
        
        if (parameters.getMaxNumFocusAreas() > 0) {
        	List focusAreas = new ArrayList();
        	focusAreas.add(new Camera.Area(focusRect, 1000));
        
        	parameters.setFocusAreas(focusAreas);
        }

        if (parameters.getMaxNumMeteringAreas() > 0) {
        	List meteringAreas = new ArrayList();
        	meteringAreas.add(new Camera.Area(meteringRect, 1000));
        	
            parameters.setMeteringAreas(meteringAreas);
        }

        mCamera.setParameters(parameters);
        mCamera.autoFocus(this);
	}
	
	/**
	 * Convert touch position x:y to {@link Camera.Area} position -1000:-1000 to 1000:1000.
	 */
	private Rect calculateTapArea(float x, float y, float coefficient) {
		float focusAreaSize = 300;
	    int areaSize = Float.valueOf(focusAreaSize * coefficient).intValue();

	    int centerX = (int) (x / getResolution().width - 1000);
	    int centerY = (int) (y / getResolution().height - 1000);
	    
	    int left = clamp(centerX - areaSize / 2, -1000, 1000);
	    int top = clamp(centerY - areaSize / 2, -1000, 1000);

	    RectF rectF = new RectF(left, top, left + areaSize, top + areaSize);

	    return new Rect(Math.round(rectF.left), Math.round(rectF.top), Math.round(rectF.right), Math.round(rectF.bottom));
	}

	private int clamp(int x, int min, int max) {
	    if (x > max) {
	        return max;
	    }
	    if (x < min) {
	        return min;
	    }
	    return x;
	}
	
	public void setFocusMode (Context item, int type){
	    Camera.Parameters params = mCamera.getParameters(); 
	    List FocusModes = params.getSupportedFocusModes();

	    switch (type){
	    case 0:
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
	        else 
	            Toast.makeText(item, "Auto Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 1:         
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
	        else
	            Toast.makeText(item, "Continuous Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 2:         
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_EDOF))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_EDOF);
	        else
	            Toast.makeText(item, "EDOF Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 3:
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_FIXED))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_FIXED);
	        else
	            Toast.makeText(item, "Fixed Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 4:
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_INFINITY))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_INFINITY);
	        else
	            Toast.makeText(item, "Infinity Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 5:
	        if (FocusModes.contains(Camera.Parameters.FOCUS_MODE_MACRO))
	            params.setFocusMode(Camera.Parameters.FOCUS_MODE_MACRO);
	        else
	            Toast.makeText(item, "Macro Mode not supported", Toast.LENGTH_SHORT).show();
	        break;      
	    }

	    mCamera.setParameters(params);
	}
	
	public void setFlashMode (Context item, int type){
	    Camera.Parameters params = mCamera.getParameters();
	    List FlashModes = params.getSupportedFlashModes();

	    switch (type){
	    case 0:
	        if (FlashModes.contains(Camera.Parameters.FLASH_MODE_AUTO))
	            params.setFlashMode(Camera.Parameters.FLASH_MODE_AUTO);
	        else
	            Toast.makeText(item, "Auto Mode not supported", Toast.LENGTH_SHORT).show();
	        break;
	    case 1:
	        if (FlashModes.contains(Camera.Parameters.FLASH_MODE_OFF))
	            params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
	        else
	            Toast.makeText(item, "Off Mode not supported", Toast.LENGTH_SHORT).show();          
	        break;
	    case 2:
	        if (FlashModes.contains(Camera.Parameters.FLASH_MODE_ON))
	            params.setFlashMode(Camera.Parameters.FLASH_MODE_ON);
	        else
	            Toast.makeText(item, "On Mode not supported", Toast.LENGTH_SHORT).show();       
	        break;
	    case 3:
	        if (FlashModes.contains(Camera.Parameters.FLASH_MODE_RED_EYE))
	            params.setFlashMode(Camera.Parameters.FLASH_MODE_RED_EYE);
	        else
	            Toast.makeText(item, "Red Eye Mode not supported", Toast.LENGTH_SHORT).show();          
	        break;
	    case 4:
	        if (FlashModes.contains(Camera.Parameters.FLASH_MODE_TORCH))
	            params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH);
	        else
	            Toast.makeText(item, "Torch Mode not supported", Toast.LENGTH_SHORT).show();        
	        break;
	    }

	    mCamera.setParameters(params);
	}

	@Override
	public void onAutoFocus(boolean arg0, Camera arg1) {
		 
	}
}

在MainActivity中需要初始化MTCamera,並且實現OnTouchListener接口,以便在觸摸屏幕時可以調用onTouch函數。其中主要代碼如下:

	private MTCameraView mOpenCvCameraView;

	public void init() {
		mOpenCvCameraView = new MTCameraView(this, -1);
		mOpenCvCameraView.setCvCameraViewListener(this);
		mOpenCvCameraView.setFocusable(true);
		mOpenCvCameraView.setOnTouchListener(MainActivity.this);
		mOpenCvCameraView.enableView();
		
		FrameLayout frame = new FrameLayout(this);
		frame.addView(mOpenCvCameraView);
		
		setContentView(frame);
	     }

	@Override
	public boolean onTouch(View arg0, MotionEvent arg1) {
		// TODO Auto-generated method stub
		mOpenCvCameraView.focusOnTouch(arg1);
		return true;
	}

init()函數是自定義的初始化函數,可以在onCreate時使用。由於這裡需要使用OpenCV庫,所以本項目是在加載完OpenCV庫並判斷成功後才調用init()函數的。

解釋

在發生觸摸事件時,MainActivity由於實現瞭OnTouchListener接口,因此會調用重寫的onTouch函數,並把它的第二個參數MotionEvent傳遞給MTCamera,以便定位觸摸位置。

MTCamera的focusOnTouch函數繼續工作。它首先根據觸摸位置計算對焦和測光(metering)區域的大小(通過calculateTapArea函數),然後創建新的Camera.Parameters,並設置攝像機的對焦模式為Auto。

然後,它分別判斷該設備的相機是否支持設置對焦區域和測光區域,如果支持就分別為parameters設置之前計算好的聚焦和測光區域。

最後,讓Camera自動對焦。

calculateTapArea函數

這個函數主要實現從屏幕坐標系到對焦坐標系的轉換。由MotionEvent.getRowX()得到的是以屏幕坐標系(即屏幕左上角為原點,右下角為你的當前屏幕分辨率,單位是一個像素)為準的坐標,而setFocusAreas接受的List中的每一個Area的范圍是(-1000,-1000)到(1000, 1000),也就是說屏幕中心為原點,左上角為(-1000,-1000),右下角為(1000,1000)。註意,如果超出這個范圍的話,會報setParemeters
failed的錯誤哦!除此之外,我們還提前定義瞭一個對焦框(測光框)的大小,並且接受一個參數(第三個參數coefficient)作為百分比進行調節。

至此完成瞭觸摸對焦的功能。

但是,可以發現MTCamera裡還有很大部分代碼,主要是兩個函數setFocusMode和setFlashMode。這兩個函數,主要是因為在項目中我的圖像經常是模糊的,但不知道系統支持那麼對焦模式。這時,就可以使用這兩個函數進行測試。這還需要在MainActivity中添加菜單欄的代碼,以便進行選擇。代碼如下:

    private List mResolutionList;

    private MenuItem[] mResolutionMenuItems;
    private MenuItem[] mFocusListItems;
    private MenuItem[] mFlashListItems;

    private SubMenu mResolutionMenu;
    private SubMenu mFocusMenu;
    private SubMenu mFlashMenu;
    
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        Log.i(TAG, "called onCreateOptionsMenu");
        
        List mFocusList = new LinkedList();
	    int idx =0;

	    mFocusMenu = menu.addSubMenu("Focus");

	    mFocusList.add("Auto");
	    mFocusList.add("Continuous Video");
	    mFocusList.add("EDOF");
	    mFocusList.add("Fixed");
	    mFocusList.add("Infinity");
	    mFocusList.add("Makro");
	    mFocusList.add("Continuous Picture");

	    mFocusListItems = new MenuItem[mFocusList.size()];

	    ListIterator FocusItr = mFocusList.listIterator();
	    while(FocusItr.hasNext()){
	        // add the element to the mDetectorMenu submenu
	        String element = FocusItr.next();
	        mFocusListItems[idx] = mFocusMenu.add(2,idx,Menu.NONE,element);
	        idx++;
	    }

	    List mFlashList = new LinkedList();
	    idx = 0;

	    mFlashMenu = menu.addSubMenu("Flash");

	    mFlashList.add("Auto");
	    mFlashList.add("Off");
	    mFlashList.add("On");
	    mFlashList.add("Red-Eye");
	    mFlashList.add("Torch");

	    mFlashListItems = new MenuItem[mFlashList.size()];

	    ListIterator FlashItr = mFlashList.listIterator();
	    while(FlashItr.hasNext()){
	        // add the element to the mDetectorMenu submenu
	        String element = FlashItr.next();
	        mFlashListItems[idx] = mFlashMenu.add(3,idx,Menu.NONE,element);
	        idx++;
	    }

	    mResolutionMenu = menu.addSubMenu("Resolution");
	    mResolutionList = mOpenCvCameraView.getResolutionList();
	    mResolutionMenuItems = new MenuItem[mResolutionList.size()];

	    ListIterator resolutionItr = mResolutionList.listIterator();
	    idx = 0;
	    while(resolutionItr.hasNext()) {
	        Camera.Size element = resolutionItr.next();
	        mResolutionMenuItems[idx] = mResolutionMenu.add(1, idx, Menu.NONE,
	                Integer.valueOf((int) element.width).toString() + "x" + Integer.valueOf((int) element.height).toString());
	        idx++;
	     }

	    return true;
    }
    
    public boolean onOptionsItemSelected(MenuItem item) {
        Log.i(TAG, "called onOptionsItemSelected; selected item: " + item);

        if (item.getGroupId() == 1)
	    {
	        int id = item.getItemId();
	        Camera.Size resolution = mResolutionList.get(id);
	        mOpenCvCameraView.setResolution(resolution);
	        resolution = mOpenCvCameraView.getResolution();
	        String caption = Integer.valueOf((int) resolution.width).toString() + "x" + Integer.valueOf((int) resolution.height).toString();
	        Toast.makeText(this, caption, Toast.LENGTH_SHORT).show();
	    } 
	    else if (item.getGroupId()==2){

	       int focusType = item.getItemId();

	       mOpenCvCameraView.setFocusMode(this, focusType);
	    }
	    else if (item.getGroupId()==3){

	       int flashType = item.getItemId();

	       mOpenCvCameraView.setFlashMode(this, flashType);
	    }

        return true;
    }

這樣運行後,點擊菜單就可以看見有三個菜籃列表:Focus(對焦模式),Flash(視頻模式),Resolution(支持的分辨率)。對焦模式和視頻模式中提供瞭幾種常見的模式供選擇,代碼會判斷當前設備是否支持該模式。而分辨率菜單欄會顯示出當前設備支持的所有分辨率種類。

參考

StackOverFlow上關於觸摸對焦的討論Android多媒體和相機講解十解讀Android 4.0 Camera原生應用程序的設計思路OpenCV上的提問:ANDROID:
Use autofocus with CameraBridgeViewBase?

發佈留言