1. 程式人生 > >利用Android Camera2 的照相機api 實現 實時的影象採集與預覽

利用Android Camera2 的照相機api 實現 實時的影象採集與預覽

https://blog.csdn.net/DavidWillo/article/details/63688319

  最近想要做一個客戶端往伺服器推送實時畫面的功能,首先可以考慮到兩種思路,一種是在客戶端進行視訊流的推送,主要利用RTSP等流媒體協議進行傳輸,而另外一種是通過攝像頭獲取當前畫面,將每一幀作為物件單獨傳輸。

   專案想要實現的功能最終目的是對實時畫面的每一幀進行處理,可以考慮客戶端推流到伺服器,再在伺服器進行幀解析的操作,但由於目前很多的流媒體推送框架在推流端或者服務端都或多或少存在限制,很少有完全開源的專案,再加上傳送畫面的同時需要附帶部分的資料,仍然需要另外建立連線進行傳輸,所以暫時擱置這一方案。選擇第二種思路,獲取每一幀的畫面,單獨傳輸。

   要想獲取實時畫面,我們必須通過對安卓裝置上的攝像頭進行呼叫。

   從API21開始,安卓引入了android.hardware.camera2這個包,來替代原有的camera類,原有的camera類已經不再建議使用了。camera2中最重要的變化是,攝像頭的呼叫不再是簡單地進行例項化,而是用一種類似服務申請的方式來進行呼叫。通過CameraManager來管理攝像服務,需要通過建立CameraCaptureSession來建立一個呼叫攝像裝置CameraDevices的會話,來實現對攝像頭的呼叫。而CaptureRequest.Builder類用於建立實際的呼叫請求,具體的引數設定也可以通過這個類來實現(而不是對camera裝置進行直接設定),這樣做的目的是把對攝像頭的控制與攝像頭本身分離開來,使用者可以通過不同的session根據不同的配置來使用攝像頭。

   我們可以結合具體的程式碼來分析新api中攝像頭呼叫的過程。

   首先我們想要對攝像裝置進行操作,需要獲得CameraManager的例項

  1.     CameraManager cameraManager;  
  2.     cameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);  

   我們可以呼叫openCamera函式開啟攝像頭裝置

  1.     cameraManager.openCamera(cameraId, cameraCallback, mainHandler);  

   這裡需要傳入三個引數,cameraId是裝置編號,cameraCallback控制攝像服務的回撥,最後一個引數指定HandlerThread物件 

  1.      cameraId = Integer.toString(CameraCharacteristics.LENS_FACING_FRONT);  
  2.      private CameraDevice.StateCallback cameraCallback = new CameraDevice.StateCallback() {  
  3.         @Override
  4.         publicvoid onOpened(CameraDevice camera) {  
  5.             Log.d("CameraCallback""Camera Opened");  
  6.             cameraDevice = camera;  
  7.             takePreview();  
  8.         }  
  9.         @Override
  10.         publicvoid onDisconnected(CameraDevice cameraDevice) {  
  11.             Log.d("CameraCallback""Camera Disconnected");  
  12.             closeCameraDevice();  
  13.         }  
  14.         @Override
  15.         publicvoid onError(CameraDevice cameraDevice, int i) {  
  16.             Log.d("CameraCallback""Camera Error");  
  17.             Toast.makeText(PusherSurface.this"攝像頭開啟失敗", Toast.LENGTH_SHORT).show();  
  18.         }  
  19.     };  

 回撥函式用於指定連線攝像頭裝置時不同狀態的操作。在這裡,我們在攝像頭成功連線的時候呼叫  takePreview()函式開啟攝像頭畫面的預覽。

  1. privatevoid takePreview() {  
  2.     try {  
  3.         final CaptureRequest.Builder previewRequestBuilder  
  4.                 = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);  
  5.         previewRequestBuilder.addTarget(surfaceHolder.getSurface());  
  6.         previewRequestBuilder.addTarget(previewReader.getSurface());  
  7.         cameraDevice.createCaptureSession(Arrays.asList(surfaceHolder.getSurface(),  
  8.                 previewReader.getSurface(),  
  9.                 imageReader.getSurface()), new CameraCaptureSession.StateCallback() {  
  10.             @Override
  11.             publicvoid onConfigured(CameraCaptureSession cameraCaptureSession) {  
  12.                 if (cameraDevice == nullreturn;  
  13.                 mCameraCaptureSession = cameraCaptureSession;  
  14.                 try {  
  15.                     previewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,  
  16.                             CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);  
  17.                     previewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE,  
  18.                             CaptureRequest.CONTROL_AE_MODE_OFF);  
  19.                     CaptureRequest previewRequest = previewRequestBuilder.build();  
  20.                     mCameraCaptureSession.setRepeatingRequest(previewRequest, null, childHandler);  
  21.                 } catch (CameraAccessException e) {  
  22.                     e.printStackTrace();  
  23.                 }  
  24.             }  
  25.             @Override
  26.             publicvoid onConfigureFailed(CameraCaptureSession cameraCaptureSession) {  
  27.                 Toast.makeText(PusherSurface.this"配置失敗", Toast.LENGTH_SHORT).show();  
  28.             }  
  29.         }, childHandler);  
  30.     } catch (CameraAccessException e) {  
  31.         e.printStackTrace();  
  32.     }  
  33. }  

要從攝像裝置中獲取影象,我們首先需要建立一個camera capture session。函式

createCaptureSession(List, CameraCaptureSession.StateCallback, Handler)的第一個引數傳入了我們想要繪製的檢視列表,第二個引數傳入的是建立攝像會話的狀態回撥函式,第三個引數傳入相應的handler處理器。然後,我們需要利用capturerequest來定義攝像頭捕獲影象時候的具體引數,比如是否開啟攝像頭,是否自動對焦等。最後通過CamraCaptureSession.setRepeatingRequest來開啟請求。這樣我們就可以從capturesession傳入的list中的surface列表獲得連續的影象。留意到

  1. previewRequestBuilder.addTarget(surfaceHolder.getSurface());  
  2. previewRequestBuilder.addTarget(previewReader.getSurface());  

這裡除了傳入xml介面佈局中的surfaceHolder的surface外,還傳入了一個previewReader的surface。

previewReader是一個自定義的ImageReader物件。

  1. previewReader = ImageReader.newInstance(10801920, ImageFormat.YUV_420_888, 2);  
  2.         previewReader.setOnImageAvailableListener(new ImageReader.OnImageAvailableListener() {  
  3.             @Override
  4.             publicvoid onImageAvailable(ImageReader imageReader) {  
  5.                 Image image = null;  
  6.                 try {  
  7.                     image = imageReader.acquireLatestImage();  
  8.                     Log.d("PreviewListener""GetPreviewImage");  
  9.                     if (image == null) {  
  10.                         return;  
  11.                     }  
  12.                     byte[] bytes = ImageUtil.imageToByteArray(image);  
  13.                     if (pushFlag == false)  
  14.                         uploadImg(bytes);  
  15.                 } finally {  
  16.                     if (image != null) {  
  17.                         image.close();  
  18.                     }  
  19.                 }  
  20.             }  
  21.         }, mainHandler);  

ImageReader是一個可以讓我們對繪製到surface的影象進行直接操作的類。在這裡我們從攝像裝置中傳入了連續的預覽圖片,也就是我們在螢幕上看到的畫面,它們的格式都是未經壓縮的YUV_420_888型別的(同樣的如果要操作拍攝後的圖片,就要設定成jpeg格式)。我們呼叫imageReader.acquireLatestImage或者acquireNextImage來獲取影象佇列中的圖片。並進行操作。在這裡我利用一個函式將影象壓縮後轉化成byte[]格式,並呼叫uploadImg函式上傳至伺服器。這樣,整個攝像頭的呼叫到預覽影象的處理也就完成了。想要實現拍照功能也是大同小異,在這裡我就不一一貼出了。

  歡迎更多安卓開發者一同交流。