카메라

Android의 카메라 HAL (Hardware Abstraction Layer)은 Camera 2의 상위 수준의 카메라 프레임 워크 API를 기본 카메라 드라이버 및 하드웨어에 연결합니다. 카메라 서브 시스템은 카메라 파이프 라인 구성 요소에 대한 구현을 포함하는 반면 카메라 HAL은 이러한 구성 요소의 버전을 구현하는 데 사용할 인터페이스를 제공합니다.

구조

다음 그림과 목록은 HAL 구성 요소를 설명합니다.

Android camera architecture

Application Framework

Application Framework 계층에서는 Camera2 API를 사용하여 카메라 하드웨어와 상호 작용하는 애플리케이션의 코드가 있습니다. 내부적으로 이 코드는 해당 바인더 인터페이스를 호출하여 카메라와 상호 작용하는 네이티브 코드에 액세스합니다.

AIDL

CameraService와 관련된 바인더 인터페이스는 frameworks / av / camera / aidl / android / hardware에서 찾을 수 있습니다. 생성 된 코드는 하위 계층의 네이티브 코드를 호출하여 실제 카메라에 액세스하고 프레임 워크 계층에서 CameraDeviceCameraCaptureSession 객체를 만드는 데 사용되는 데이터를 반환합니다.

Native Framework

frameworks / av /에 있는 이 프레임워크는 CameraDeviceCameraCaptureSession 클래스와 동등한 고유한 기능을 제공합니다. NDK Camera2도 확인해보세요.

Binder IPC interface

IPC 바인더 인터페이스는 프로세스간 통신을 용이하게합니다. 카메라 서비스를 호출하는 framework / av / camera / camera / aidl / android / hardware 디렉토리에 여러 카메라 바인더 클래스가 있습니다. ICameraService는 카메라 서비스에 대한 인터페이스입니다. ICameraDeviceUser는 특정 열린 카메라 장치에 대한 인터페이스입니다. ICameraServiceListenerICameraDeviceCallbacks는 애플리케이션 프레임 워크에 대한 각각의 CameraService 및 CameraDevice 콜백입니다.

Camera service

frameworks / av / services / camera / libcameraservice / CameraService.cpp에있는 카메라 서비스는 HAL과 상호 작용하는 실제 코드입니다.

HAL

하드웨어 추상화 레이어는 카메라 서비스가 호출하는 표준 인터페이스를 정의하며 카메라 하드웨어가 올바르게 작동하도록 구현해야합니다.

WorkFlow

Camera2를 사용하는 애플리케이션의 기본적인 워크플로우입니다. Camera와 비교하면 복잡하지만, 그렇게 어렵지는 않습니다. 구글 샘플 예제와 함께 차근차근 살펴보도록 하겠습니다.

Camera2 Basic 살펴보기

소스:https://github.com/googlesamples/android-Camera2Basic

Camera와는 달리 Camera2는 카메라를 열고 프리뷰를 얻기까지 모든 상황이 비동기적으로 동작하기 때문에 콜백이 많습니다.

@Override
public void onResume() {
    super.onResume();
    startBackgroundThread();
    if (mTextureView.isAvailable()) {
        openCamera(mTextureView.getWidth(), mTextureView.getHeight());
    } else {
        mTextureView.setSurfaceTextureListener(mSurfaceTextureListener);
    }
}

액티비티(프레그먼트)가 실행이 되면 onResume()이 호출됩니다. 카메라와 관련된 작업은 UI를 그리는 메인쓰레드를 방해해지 않기 위해 onResume에서 새로운 쓰레드와 핸들러를 생성합니다. 또한 Fragment에 포함된 TextureView 또한 인플레이팅과 동시에 초기화가 진행됩니다. 

일반적으로 처음 액티비티가 시작되면 위의 조건문에서 두번째의 else조건을 타지만, 다른 액티비티의 호출로 카메라 리소스와, 텍스쳐뷰들이 잠시 비활성화 되었다가 다시 재게 되는경우에는 if문안의 openCamera()를 바로 호출하게 됩니다. 

private final TextureView.SurfaceTextureListener mSurfaceTextureListener
        = new TextureView.SurfaceTextureListener() {
...
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) {
        openCamera(width, height);
    }
...
};

처음 액티비티가 실행되었다 가정하면, 다음과 같이 TextureView가 초기화가 완료되고 화면에 텍스쳐를 그릴준비가 되었을때 onSurfaceTextureAvailable()을 호출하게 됩니다. 

private void openCamera(int width, int height) {
    if (ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.CAMERA)
            != PackageManager.PERMISSION_GRANTED) {
        requestCameraPermission();
        return;
    }
    setUpCameraOutputs(width, height);
    configureTransform(width, height);
    Activity activity = getActivity();
    CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
    try {
        if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
            throw new RuntimeException("Time out waiting to lock camera opening.");
        }
        manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    } catch (InterruptedException e) {
        throw new RuntimeException("Interrupted while trying to lock camera opening.", e);
    }
}

openCamera()를 살펴보면 TextureView의 사이즈를 입력받아 여러가지작업들을 수행하게 됩니다. 
첫번째로 카메라 런타임 퍼미션이 있는지 확인하게 되고, 퍼미션을 얻었다면 setUpCameraOutputs()를 수행하게 됩니다. 

setUpCameraOutputs()가 하는일

  • 후면 카메라 선택
  • 캡쳐된 사진(이미지리더)의 해상도, 포맷 선택
  • 이미지의 방향
  • 적합한 프리뷰 사이즈 선택
  • 들어오는 영상의 비율에 맞춰 TextureView의 비율 변경(이 부분은 예제에 포함된 AutoFitTextureView 커스텀 뷰입니다)
  • 플래시 지원 여부

configureTransform()가 하는일

  • 스크린과 카메라의 영상의 방향을 맞추기 위해 View를 매트릭스 연산으로 회전시킴

Camera를 열기위한 선작업들이 끝났다면 CameraManager를 통해 openCamera(카메라ID, CameraDevice.StateCallback, 핸들러)를 호출합니다.

private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() {
...
    @Override
    public void onOpened(@NonNull CameraDevice cameraDevice) {
        mCameraOpenCloseLock.release();
        mCameraDevice = cameraDevice;
        createCameraPreviewSession();
    }
...
};

카메라가 열리고 나면 onOpend(cameraDevice)가 호출됩니다. 카메라가 정상적으로 열렸으므로 이제 프리뷰세션을 만들어 보도록 하겠습니다. 

private void createCameraPreviewSession() {
    try {
        SurfaceTexture texture = mTextureView.getSurfaceTexture();
        assert texture != null;
        texture.setDefaultBufferSize(mPreviewSize.getWidth(), mPreviewSize.getHeight());
        
        Surface surface = new Surface(texture);
        
        mPreviewRequestBuilder
                = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);
        // Here, we create a CameraCaptureSession for camera preview.
        mCameraDevice.createCaptureSession(Arrays.asList(surface, mImageReader.getSurface()),
                new CameraCaptureSession.StateCallback() {
                    @Override
                    public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) {
                        // The camera is already closed
                        if (null == mCameraDevice) {
                            return;
                        }
                        // When the session is ready, we start displaying the preview.
                        mCaptureSession = cameraCaptureSession;
                        try {
                            // Auto focus should be continuous for camera preview.
                            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                                    CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                            // Flash is automatically enabled when necessary.
                            setAutoFlash(mPreviewRequestBuilder);
                            // Finally, we start displaying the camera preview.
                            mPreviewRequest = mPreviewRequestBuilder.build();
                            mCaptureSession.setRepeatingRequest(mPreviewRequest,
                                    mCaptureCallback, mBackgroundHandler);
                        } catch (CameraAccessException e) {
                            e.printStackTrace();
                        }
                    }
                    @Override
                    public void onConfigureFailed(
                            @NonNull CameraCaptureSession cameraCaptureSession) {
                        showToast("Failed");
                    }
                }, null
        );
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

createCameraPreviewSession()에서 프리뷰 세션 생성 하는 순서

  1. Preview 세션을 만들기 위해서 TextureView가 가지고 있는 SurfaceTexture를 가져옵니다. SurfaceTexture에 대한 내용은 이전 포스트로 대체합니다.
  2. SurfaceTexture에 setUpCameraOutputs()에서 계산한 기본 버퍼 사이즈를 설정합니다.
  3. SufaceTexture를 이용하여 Surface를 만듭니다.
  4. CaptureRequest.Builder에 surface를 타겟으로 지정합니다.
  5. 지정된 타겟은 실제 카메라 프레임 버퍼를 받아 처리하게 됩니다. 아직 CaptureRequest를 사용할순 없습니다. 캡쳐세션이 먼저 만들어져야 합니다.
  6. CameraDevice.createCaptureSession()을 통해 세션을 만듭니다.
  7. 캡쳐 세션이 만들어졌다면 CameraCaptureSession.StateCallback의 onConfigured()가 호출 되게 됩니다.
  8. onConfigured()에서 CaptureRequest.Builder인스턴스를 build()하여 CaptureRequest객체를 만듭니다.
  9. 반복적으로 이미지 버퍼를 얻기 위해 프리뷰 세션에서 setRepeatingRequest()를 호출합니다. 이렇게 하면 TextureView에 카메라 영상이 나오는것을 확인할 수 있습니다.

카메라 장치는 싱글톤 인스턴스이므로 사용후 반드시 시스템에 반환하여 다른 프로세스(앱)가 이용할 수 있도록 합니다.

@Override
public void onPause() {
    closeCamera();
    stopBackgroundThread();
    super.onPause();
}

사진을 찍는 부분의 코드는 다음과 같습니다.

private void takePicture() {
    lockFocus();
}

private void lockFocus() {
    try {

        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER,
                CameraMetadata.CONTROL_AF_TRIGGER_START);
        mState = STATE_WAITING_LOCK;
        mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler)
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

카메라에게 초점을 잡으라고 request를 보내기 위해 해당 파라미터를 설정해줍니다. 준비된 세션으로부터 capture()메소드와 함께 request인자를 넣어 호출합니다. 초점이 잡혔다면 mCaptureCallback으로부터 captureStillPicture()을 호출하게 될것입니다.

private void captureStillPicture() {
    try {
        ...
        final CaptureRequest.Builder captureBuilder =
                mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE);
        captureBuilder.addTarget(mImageReader.getSurface());
        ...
        mCaptureSession.capture(captureBuilder.build(), CaptureCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

또 다시 CaptureSession에 CaptureRequest를 넣어 사진을 캡쳐하는데 이때의 캡쳐한 이미지 버퍼를 받을 Surface는 아까 TextureView의 Surface가 아닌 ImageReader의 Surface입니다.

여기서 중요한것은 위에서 알아본 워크플로우대로 출력되는 이미지 버퍼의 스트림을 받아 처리할 수 있는 것은 SurfaceTexture와 ImageReader뿐만 아니라, Surface를 가지고 있는 MediaCodec, MediaRecorder 등도 버퍼를 받을 수 있다는 점입니다.

카테고리: Graphics

0개의 댓글

답글 남기기

Avatar placeholder

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다.