brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Apr 03. 2019

안드로이드 카메라 예제 (2/2)

Camera2 API

안드로이드 카메라 예제 스터디 (2)


1편에 이어서 사진을 촬영하여 파일로 저장하는 법에 대해서 살펴본다.


참고하는 예제에 빠진 권한이 있는데 촬영 후 사진 파일 저장을 위해서 다음 권한을 추가해야 한다.


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


또한 카메라 외에 저장소 권한 요청을 하는 부분이 구현되어 있지 않기 때문에 설정의 앱 정보에 가서 직접적으로 권한을 활성화시켜주어야 한다. (권한 요청 구현된 코드 참고 : https://brunch.co.kr/@mystoryg/96)


저장소 권한을 꼭 켜야 저장 가능


4. 사진 촬영하기

먼저 카메라로부터 image stream을 가져오기 위해서 CameraCaptureSession을 만들어야 한다. 다음으로 과 surface을 CameraCaptureSession 연결하기 위해 다음과 같이 createCaptureSession()을 구성한다.


mCameraDevice.createCaptureSession(outputSurfaces, new CameraCaptureSession.StateCallback() {
     @Override
     public void onConfigured(CameraCaptureSession session) {
         try {
             session.capture(captureBuilder.build(), captureListener, backgroudHandler);
         } catch (CameraAccessException e) {
             e.printStackTrace();
         }
     }
 
     @Override
     public void onConfigureFailed(CameraCaptureSession session) {
 
     }
 }, backgroudHandler);



outputSurfaces는 surface로 이루어진 List이다.


ImageReader reader = ImageReader.newInstance(width, height, ImageFormat.JPEG, 1);

List<Surface> outputSurfaces = new ArrayList<Surface>(2);
 outputSurfaces.add(reader.getSurface());
 outputSurfaces.add(new Surface(mTextureView.getSurfaceTexture()));


2가지 surface를 담았는데 하나는 ImageReader의 surface이고 다른 하나는 textureView의 surface이다.


(의문점) textureView의 surface는 이미 프리뷰를 위해서 사용했었다. 그런데 왜 또 사용하는 것일까?

만약 createCaptureSession()의 첫 번째 인자로 ImageReader의 surface만 넘겨주면 사진 촬영 시 이미지가 뿌옇게 나오는 현상이 있다. 현재 카메라 session의 출력을 ImageReader의 surface에 출력하고 저장한 것인데 왜 그런지 모르겠다. 추측하자면 CaptureRequest가 순간적으로 전환되면서 더 이상 프리뷰에 대한 캡처가 아닌 순간적인 캡처만 빠르게 일어난 후 그 이미지 정보가 ImageReader로 넘어간 것이 아닐까 싶다.

신기한 것은 하나는 textureView의 surface와 같이 넘겨주면 화면에 보이는 그대로 사진이 찍힌다는 것이다. 구글 예제를 보면 createCaptureSession() 자체를 한 번만 호출하면서 프리뷰를 위한 textureView와 ImageReader를 모두 넘겨주고 session 정보를 멤버 변수로 관리하여 좀 더 명확히 이해가 된다. 따라서 이 부분은 구글 예제를 보고 이해하는 편이 낫다. 그렇지만 의문점은 역시 계속해서 궁금하다.


프리뷰와 다르게 촬영을 위해서 CaptureRequest를 생성할 때 CameraDevice.TEMPLATE_STILL_CAPTURE을 인자로 사용한다.


final CaptureRequest.Builder captureBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
 captureBuilder.addTarget(reader.getSurface());


세션이 구성되면 다음과 같이 capture() 함수를 호출한다. 첫 번째 인자는 앞서 와 같이 설정한 CaptureRequest이다. 두 번째 인자는 촬영 후 콜백이다. 세 번째는 UI(프리뷰)를 계속해서 보여주기 위한 핸들러이다.

session.capture(captureBuilder.build(), captureListener, backgroudHandler);

사진 촬영 후 호출되는 콜백은 다음과 같이 약간 수정하였다. 1초 동안 촬영한 결과물을 보여준 후 다시 프리뷰가 시작되도록 했다.


final CameraCaptureSession.CaptureCallback captureListener = new CameraCaptureSession.CaptureCallback() {
                 @Override
                 public void onCaptureCompleted(CameraCaptureSession session,
                                                CaptureRequest request, TotalCaptureResult result) {
                     super.onCaptureCompleted(session, request, result);
                     Toast.makeText(mContext, "Saved:" + file, Toast.LENGTH_SHORT).show();
                     delayPreview.postDelayed(mDelayPreviewRunnable, 1000);
 //                    startPreview();
                 }
             };


final Handler delayPreview = new Handler();


private Runnable mDelayPreviewRunnable = new Runnable() {
     @Override
     public void run() {
         startPreview();
     }
 };


촬영된 사진의 저장은 다음과 같이 이뤄진다. 덮어쓰기 방지를 위해서 파일명을 현재 날짜와 시간 정보를 가져와 구성하도록 수정했다. ImageReader에 setOnImageAvailableListener를 설정하여 카메라 session으로부터 데이터를 획득하면 해당 정보를 바이트 정보로 가져온 후 파일로 쓰는 것이다.


※ (주의) 이렇게 생성된 jpg 파일은 기본 갤러리로 확인이 되지 않는다. 하지만 파일 탐색기로 저장 경로에 가면 촬영된 사진 파일이 있다. 이때 갤러리에 나타나지 않는 현상을 해결하기 위해서는 미디어 스캔을 해야한다. 미디어 스캔을 하려면 Intent.ACTION_MEDIA_SCANNER_SCAN_FILE 인텐트 날려주면 된다. (참고 : https://brunch.co.kr/@mystoryg/96)


Date date = new Date();
 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy_MM_dd_hh_mm_ss");
 
 final File file = new File(Environment.getExternalStorageDirectory() + "/DCIM", "pic_" + dateFormat.format(date) + ".jpg");


final Handler backgroudHandler = new Handler(thread.getLooper());
 reader.setOnImageAvailableListener(readerListener, backgroudHandler);


 ImageReader.OnImageAvailableListener readerListener = new ImageReader.OnImageAvailableListener() {
     @Override
     public void onImageAvailable(ImageReader reader) {
         Image image = null;
         try {
             image = reader.acquireLatestImage();
             ByteBuffer buffer = image.getPlanes()[0].getBuffer();
             byte[] bytes = new byte[buffer.capacity()];
             buffer.get(bytes);
             save(bytes);
         } catch (FileNotFoundException e) {
             e.printStackTrace();
         } catch (IOException e) {
             e.printStackTrace();
         } finally {
             if (image != null) {
                 image.close();
                 reader.close();
             }
         }
     }
 
     private void save(byte[] bytes) throws IOException {
         OutputStream output = null;
         try {
             output = new FileOutputStream(file);
             output.write(bytes);
         } finally {
             if (null != output) {
                 output.close();
             }
         }
     }
 };



기본 예제를 기준으로 앞서 수정한 모든 내용이 포함된 파일은 다음과 같다.



ref.)

사진 촬영 예제 : https://myandroidarchive.tistory.com/6

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari