1. 程式人生 > >android camera2 詳解說明(一)

android camera2 詳解說明(一)

https://www.cnblogs.com/kingwild/articles/5422329.html

現在的手機一般都會提供相機功能,有些相機的鏡頭甚至支援1000萬以上畫素,有些甚至支援光學變焦,這些手機已經變成了專業數碼相機。為了充分利用手機上的相機功能,Android應用可以控制拍照和錄製視訊。

 使用Android 5.0的Camera v2拍照

Android 5.0對拍照API進行了全新的設計,新增了全新設計的Camera v2 API,這些API不僅大幅提高了Android系統拍照的功能,還能支援RAW照片輸出,甚至允許程式調整相機的對焦模式、曝光模式、快門等。

Android 5.0的Camera v2主要涉及如下API。

Ø CameraManager:攝像頭管理器。這是一個全新的系統管理器,專門用於檢測系統攝像頭、開啟系統攝像頭。除此之外,呼叫CameraManager的getCameraCharacteristics(String)方法即可獲取指定攝像頭的相關特性。

Ø CameraCharacteristics:攝像頭特性。該物件通過CameraManager來獲取,用於描述特定攝像頭所支援的各種特性。

Ø CameraDevice:代表系統攝像頭。該類的功能類似於早期的Camera類。

Ø CameraCaptureSession:這是一個非常重要的API,當程式需要預覽、拍照時,都需要先通過該類的例項建立Session。而且不管預覽還是拍照,也都是由該物件的方法進行控制的,其中控制預覽的方法為setRepeatingRequest();控制拍照的方法為capture()。

為了監聽CameraCaptureSession的建立過程,以及監聽CameraCaptureSession的拍照過程,Camera v2 API為CameraCaptureSession提供了StateCallback、CaptureCallback等內部類。

Ø CameraRequest和CameraRequest.Builder:當程式呼叫setRepeatingRequest()方法進行預覽時,或呼叫capture()方法進行拍照時,都需要傳入CameraRequest引數。CameraRequest代表了一次捕獲請求,用於描述捕獲圖片的各種引數設定,比如對焦模式、曝光模式……總之,程式需要對照片所做的各種控制,都通過CameraRequest引數進行設定。CameraRequest.Builder則負責生成CameraRequest物件。

理解了上面API的功能和作用之後,接下來即可使用Camera v2 API來控制攝像頭拍照了。控制拍照的步驟大致如下。

呼叫CameraManager的openCamera(String cameraId, CameraDevice.StateCallback callback, Handler handler)方法開啟指定攝像頭。該方法的第一個引數代表要開啟的攝像頭ID;第二個引數用於監聽攝像頭的狀態;第三個引數代表執行callback的Handler,如果程式希望直接在當前執行緒中執行callback,則可將handler引數設為null。

當攝像頭被開啟之後,程式即可獲取CameraDevice—即根據攝像頭ID獲取了指定攝像頭裝置,然後呼叫CameraDevice的createCaptureSession(List<Surface> outputs, CameraCaptureSession. StateCallback callback,Handler handler)方法來建立CameraCaptureSession。該方法的第一個引數是一個List集合,封裝了所有需要從該攝像頭獲取圖片的Surface,第二個引數用於監聽CameraCaptureSession的建立過程;第三個引數代表執行callback的Handler,如果程式希望直接在當前執行緒中執行callback,則可將handler引數設為null。

不管預覽還是拍照,程式都呼叫CameraDevice的createCaptureRequest(int templateType)方法建立CaptureRequest.Builder,該方法支援TEMPLATE_PREVIEW(預覽)、TEMPLATE_RECORD(拍攝視訊)、TEMPLATE_STILL_CAPTURE(拍照)等引數。

通過第3步所呼叫方法返回的CaptureRequest.Builder設定拍照的各種引數,比如對焦模式、曝光模式等。

呼叫CaptureRequest.Builder的build()方法即可得到CaptureRequest物件,接下來程式可通過CameraCaptureSession的setRepeatingRequest()方法開始預覽,或呼叫capture()方法拍照。

例項:拍照時自動對焦

本例項示範了使用Camera v2來進行拍照。當用戶按下拍照鍵時,該應用會自動對焦,當對焦成功時拍下照片。該程式的介面中提供了一個自定義TextureView來顯示預覽取景,十分簡單。該自定義TextureView類的程式碼如下。

程式清單:codes\11\3\CameraV2Test\app\src\main\java\org\crazyit\media\MainActivity.java

public class AutoFitTextureView extends TextureView

{

    private int mRatioWidth = 0;

    private int mRatioHeight = 0;

    public AutoFitTextureView(Context context, AttributeSet attrs)

    {

        super(context, attrs);

    }

    public void setAspectRatio(int width, int height)

    {

     mRatioWidth = width;

        mRatioHeight = height;

        requestLayout();

    }

    @Override

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)

    {

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = MeasureSpec.getSize(widthMeasureSpec);

        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (0 == mRatioWidth || 0 == mRatioHeight)

        {

            setMeasuredDimension(width, height);

        }

        else

        {

            if (width < height * mRatioWidth / mRatioHeight)

            {

                setMeasuredDimension(width, width * mRatioHeight / mRatioWidth);

            }

            else

            {

                setMeasuredDimension(height * mRatioWidth / mRatioHeight, height);

            }

        }

    }

}

接下來的MainActivity將會使用CameraManager來開啟CameraDevice,並通過CameraDevice建立CameraCaptureSession,然後即可通過CameraCaptureSession進行預覽或拍照了。該Activity的程式碼如下。

程式清單:codes\11\3\CameraV2Test\app\src\main\java\org\crazyit\media\MainActivity.java

public class MainActivity extends Activity implements View.OnClickListener

{

    private static final SparseIntArray ORIENTATIONS = new SparseIntArray();

    static

    {

        ORIENTATIONS.append(Surface.ROTATION_0, 90);

        ORIENTATIONS.append(Surface.ROTATION_90, 0);

        ORIENTATIONS.append(Surface.ROTATION_180, 270);

        ORIENTATIONS.append(Surface.ROTATION_270, 180);

    }

    private AutoFitTextureView textureView;

    // 攝像頭ID(通常0代表後置攝像頭,1代表前置攝像頭)

    private String mCameraId = "0";

    // 定義代表攝像頭的成員變數

    private CameraDevice cameraDevice;

    // 預覽尺寸

    private Size previewSize;

    private CaptureRequest.Builder previewRequestBuilder;

    // 定義用於預覽照片的捕獲請求

    private CaptureRequest previewRequest;

    // 定義CameraCaptureSession成員變數

    private CameraCaptureSession captureSession;

    private ImageReader imageReader;

    private final TextureView.SurfaceTextureListener mSurfaceTextureListener

        = new TextureView.SurfaceTextureListener()

    {

        @Override

        public void onSurfaceTextureAvailable(SurfaceTexture texture

            , int width, int height)

        {

            // 當TextureView可用時,開啟攝像頭

            openCamera(width, height);

        }

        @Override

        public void onSurfaceTextureSizeChanged(SurfaceTexture texture

            , int width, int height){ }

        @Override

        public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) { return true; }

        @Override

        public void onSurfaceTextureUpdated(SurfaceTexture texture){}

    };

    private final CameraDevice.StateCallback stateCallback = new CameraDevice. StateCallback()

    {

        // 攝像頭被開啟時激發該方法

        @Override

        public void onOpened(CameraDevice cameraDevice)

        {

            MainActivity.this.cameraDevice = cameraDevice;

            // 開始預覽

            createCameraPreviewSession();  // ②

        }

        // 攝像頭斷開連線時激發該方法

        @Override

        public void onDisconnected(CameraDevice cameraDevice)

        {

            cameraDevice.close();

            MainActivity.this.cameraDevice = null;

        }

        // 開啟攝像頭出現錯誤時激發該方法

        @Override

        public void onError(CameraDevice cameraDevice, int error)

        {

            cameraDevice.close();

            MainActivity.this.cameraDevice = null;

            MainActivity.this.finish();

        }

    };

    @Override

    protected void onCreate(Bundle savedInstanceState)

    {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.main);

        textureView = (AutoFitTextureView) findViewById(R.id.texture);

        // 為該元件設定監聽器

        textureView.setSurfaceTextureListener(mSurfaceTextureListener);

        findViewById(R.id.capture).setOnClickListener(this);

    }

    @Override

    public void onClick(View view)

    {

        captureStillPicture();

    }

    private void captureStillPicture()

    {

        try

        {

            if (cameraDevice == null)

            {

                return;

            }

            // 建立作為拍照的CaptureRequest.Builder

            final CaptureRequest.Builder captureRequestBuilder = cameraDevice

                .createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);

            // 將imageReader的surface作為CaptureRequest.Builder的目標

            captureRequestBuilder.addTarget(imageReader.getSurface());

            // 設定自動對焦模式

            captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,

                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

            // 設定自動曝光模式

            captureRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,

                CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

            // 獲取裝置方向

            int rotation = getWindowManager().getDefaultDisplay().getRotation();

            // 根據裝置方向計算設定照片的方向

            captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION

                , ORIENTATIONS.get(rotation));

            // 停止連續取景

            captureSession.stopRepeating();

            // 捕獲靜態影象

            captureSession.capture(captureRequestBuilder.build()

                , new CameraCaptureSession.CaptureCallback()  // ⑤

            {

                // 拍照完成時激發該方法

                @Override

                public void onCaptureCompleted(CameraCaptureSession session

                        , CaptureRequest request, TotalCaptureResult result)

                {

                    try

                    {

                        // 重設自動對焦模式

                        previewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,

                            CameraMetadata.CONTROL_AF_TRIGGER_CANCEL);

                        // 設定自動曝光模式

                        previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,

                            CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

                        // 開啟連續取景模式

                        captureSession.setRepeatingRequest(previewRequest, null, null);

                    }

                    catch (CameraAccessException e)

                    {

                        e.printStackTrace();

                    }

                }

            }, null);

        }

        catch (CameraAccessException e)

        {

            e.printStackTrace();

        }

    }

    // 開啟攝像頭

    private void openCamera(int width, int height)

    {

        setUpCameraOutputs(width, height);

        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_ SERVICE);

        try

        {

            // 開啟攝像頭

            manager.openCamera(mCameraId, stateCallback, null); // ①

        }

        catch (CameraAccessException e)

        {

            e.printStackTrace();

        }

    }

    private void createCameraPreviewSession()

    {

        try

        {

            SurfaceTexture texture = textureView.getSurfaceTexture();

            texture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight());

            // 建立作為預覽的CaptureRequest.Builder

            previewRequestBuilder = cameraDevice

                .createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);

            // 將textureView的surface作為CaptureRequest.Builder的目標

            previewRequestBuilder.addTarget(new Surface(texture));

            // 建立CameraCaptureSession,該物件負責管理處理預覽請求和拍照請求

            cameraDevice.createCaptureSession(Arrays.asList(surface

                , imageReader.getSurface()), new CameraCaptureSession.StateCallback() // ③

                {

                    @Override

                    public void onConfigured(CameraCaptureSession cameraCaptureSession)

                    {

                        // 如果攝像頭為null,直接結束方法

                        if (null == cameraDevice)

                        {

                            return;

                        }

                        // 當攝像頭已經準備好時,開始顯示預覽

                        captureSession = cameraCaptureSession;

                        try

                        {

                            // 設定自動對焦模式

                            previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,

                                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

                            // 設定自動曝光模式

                            previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,

                                CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);

                            // 開始顯示相機預覽

                            previewRequest = previewRequestBuilder.build();

                            // 設定預覽時連續捕獲影象資料

                            captureSession.setRepeatingRequest(previewRequest,

                                    null, null);  // ④

                        }

                        catch (CameraAccessException e)

                        {

                            e.printStackTrace();

                        }

                    }

                    @Override

                    public void onConfigureFailed(CameraCaptureSession cameraCaptureSession)

                    {

                        Toast.makeText(MainActivity.this, "配置失敗!"

                            , Toast.LENGTH_SHORT).show();

                    }

                }, null

            );

        }

        catch (CameraAccessException e)

        {

            e.printStackTrace();

        }

    }

    private void setUpCameraOutputs(int width, int height)

    {

        CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_ SERVICE);

        try

        {

            // 獲取指定攝像頭的特性

            CameraCharacteristics characteristics

                = manager.getCameraCharacteristics(mCameraId);

            // 獲取攝像頭支援的配置屬性

            StreamConfigurationMap map = characteristics.get(

                CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);

            // 獲取攝像頭支援的最大尺寸

            Size largest = Collections.max(

                Arrays.asList(map.getOutputSizes(ImageFormat.JPEG)),

                new CompareSizesByArea());

            // 建立一個ImageReader物件,用於獲取攝像頭的影象資料

            imageReader = ImageReader.newInstance(largest.getWidth(), largest.getHeight(),

                ImageFormat.JPEG, 2);

            imageReader.setOnImageAvailableListener(

                new ImageReader.OnImageAvailableListener()

                {

                    // 當照片資料可用時激發該方法

                    @Override

                    public void onImageAvailable(ImageReader reader)

                    {

                        // 獲取捕獲的照片資料

                        Image image = reader.acquireNextImage();

                        ByteBuffer buffer = image.getPlanes()[0].getBuffer();

                        byte[] bytes = new byte[buffer.remaining()];

                        // 使用IO流將照片寫入指定檔案

                        File file = new File(getExternalFilesDir(null), "pic.jpg");

                        buffer.get(bytes);

                        try (

                            FileOutputStream output = new FileOutputStream(file))

                        {

                            output.write(bytes);

                            Toast.makeText(MainActivity.this, "儲存: "

                                + file, Toast.LENGTH_SHORT).show();

                        }

                        catch (Exception e)

                        {

                            e.printStackTrace();

                        }

                        finally

                        {

                            image.close();

                        }

                    }

                }, null);

            // 獲取最佳的預覽尺寸

            previewSize = chooseOptimalSize(map.getOutputSizes(

                SurfaceTexture.class), width, height, largest);

            // 根據選中的預覽尺寸來調整預覽元件(TextureView)的長寬比

            int orientation = getResources().getConfiguration().orientation;

            if (orientation == Configuration.ORIENTATION_LANDSCAPE)

            {

                textureView.setAspectRatio(previewSize.getWidth(), previewSize.

getHeight());

            }

            else

            {

                textureView.setAspectRatio(previewSize.getHeight(),

previewSize.getWidth());

            }

        }

        catch (CameraAccessException e)

        {

            e.printStackTrace();

        }

        catch (NullPointerException e)

        {

            System.out.println("出現錯誤。");

        }

    }

    private static Size chooseOptimalSize(Size[] choices

        , int width, int height, Size aspectRatio)

    {

        // 收集攝像頭支援的大過預覽Surface的解析度

        List<Size> bigEnough = new ArrayList<>();

        int w = aspectRatio.getWidth();

        int h = aspectRatio.getHeight();

        for (Size option : choices)

        {

            if (option.getHeight() == option.getWidth() * h / w &&

                option.getWidth() >= width && option.getHeight() >= height)

            {

                bigEnough.add(option);

            }

        }

        // 如果找到多個預覽尺寸,獲取其中面積最小的

        if (bigEnough.size() > 0)

        {

            return Collections.min(bigEnough, new CompareSizesByArea());

        }

        else

        {

            System.out.println("找不到合適的預覽尺寸!!!");

            return choices[0];

        }

    }

    // 為Size定義一個比較器Comparator

    static class CompareSizesByArea implements Comparator<Size>

    {

        @Override

        public int compare(Size lhs, Size rhs)

        {

            // 強轉為long保證不會發生溢位

            return Long.signum((long) lhs.getWidth() * lhs.getHeight() -

                (long) rhs.getWidth() * rhs.getHeight());

        }

    }

}

 

上面程式中的①號粗體字程式碼用於開啟系統攝像頭,openCamera()方法的第一個引數代表請求開啟的攝像頭ID,此處傳入的攝像頭ID為"0",這代表開啟裝置後置攝像頭;如果需要開啟裝置指定攝像頭(比如前置攝像頭),可以在呼叫openCamera()方法時傳入相應的攝像頭ID。

CameraManager提供了getCameraIdList()方法來獲取裝置的攝像頭列表,還提供了getCameraCharacteristics(String cameraId)方法來獲取指定攝像頭的特性。例如如下程式碼:

// 獲取裝置上攝像頭列表

String[] ids = CameraManager.getCameraIdList();

// 建立一個空的CameraInfo物件,用於獲取攝像頭資訊

Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

for ( String id : ids)

{

    CameraCharacteristics cc = getCameraCharacteristics(id);

    // 接下來的程式碼就可以通過cc來獲取該攝像頭的特性了

    ...

}

上面程式中的①號粗體字程式碼開啟後置攝像頭時傳入了一個stateCallback引數,該引數代表的物件可檢測攝像頭的狀態改變,當攝像頭的狀態發生改變時,程式將會自動回撥該物件的相應方法。該程式的關鍵是重寫了stateCallback的onOpened(CameraDevice cameraDevice)方法—當攝像頭被開啟時將會自動激發該方法,通過該方法的引數即可讓程式獲取被開啟的攝像頭裝置。除此之外,程式在onOpened()方法的②號粗體字程式碼處呼叫createCameraPreviewSession()方法建立了CameraCaptureSession,並開始預覽取景。

createCameraPreviewSession()方法中的③號粗體字程式碼呼叫了CameraDevice的createCaptureSession()方法來建立CameraCaptureSession,呼叫該方法時也傳入了一個CameraCaptureSession.StateCallback引數,這樣即可保證當CameraCaptureSession被建立成功之後立即開始預覽。

createCameraPreviewSession()方法的第一行粗體字程式碼將texture元件新增為previewRequestBuilder的target,這意味著程式通過previewRequestBuilder獲取的影象資料將會被顯示在texture元件上。

程式重寫了CameraCaptureSession.StateCallback的onConfigured()方法—當CameraCaptureSession建立成功時將會自動回撥該方法,該方法先通過previewRequestBuilder設定了預覽引數,然後呼叫CameraCaptureSession物件的setRepeatingRequest()方法開始預覽。

當單擊程式介面上的拍照按鈕時,程式將會激發該Activity的captureStillPicture()方法。該方法的實現邏輯同樣很簡單:程式先建立一個CaptureRequest.Builder物件,該方法中第一行粗體字程式碼將ImageReader新增成CaptureRequest.Builder的target—這意味著當程式拍照時,影象資料將會被傳給此ImageReader。接下來程式通過CaptureRequest.Builder設定了拍照引數,然後通過⑤號粗體字程式碼呼叫CameraCaptureSession的capture()方法拍照即可,呼叫該方法時也傳入了CaptureCallback引數,這樣可以保證拍照完成之後會重新開始預覽。

注意:該應用開啟攝像頭、建立CameraCaptureSession、預覽、拍照時都沒有傳入Handler引數,這意味著程式直接在主執行緒中完成相應的Callback任務,這樣可能導致程式響應變慢。對於實際的應用,我們建議傳入Handler引數,這樣即可讓Handler使用新執行緒來執行Callback任務,這樣才可提高應用的響應速度。

由於該程式需要使用手機的攝像頭,因此還需要在AndroidManifest.xml檔案中增加如下配置:

 

<!-- 授予該程式使用攝像頭的許可權 -->

<uses-permission android:name="android.permission.CAMERA" />

 

在Genymotion模擬器上執行該程式可能看到如圖1所示的預覽介面—這是因為Genymotion模擬器可以使用宿主電腦上的攝像頭作為相機攝像頭。

為了讓模擬器能顯示圖1所示的預覽介面,建議讀者啟用Genymotion模擬器的攝像頭支援:單擊Genymotion模擬器右邊的攝像頭圖示,即可看到如圖2所示的對話方塊。按該圖上標出的提示即可開啟Genymotion模擬器的攝像頭支援。

執行該程式,按下右下角的“拍照”鍵,程式將會把拍得的照片儲存下來,介面上也會顯示該照片的儲存目錄。

 

<ignore_js_op>8.png


圖1  預覽介面
 

<ignore_js_op>

 

9.png 
圖2  開啟Genymotion模擬器的攝像頭

本文摘自《瘋狂Android講義(第2版)》

 

<ignore_js_op>

 

封面.jpg