본문 바로가기
Dev.Mobile/Android

Android Camera2 API Step By Step : Preview

by Devkin 2017. 3. 23.

Android Lollipop (API 21) 버전 부터 기존 Camera API는 deprecated 되고 Camera2 API가 적용되었는데, Pipeline Stream 기반으로 기존에 비해 세밀한 세팅이 가능하고 비동기 루틴이 많아 Google의 샘플 소스 android-Camera2Basic을 봐서는 분석이 쉽지 않다. Google I/O 2014 프레젠테이션에서 Camera2 API에 대한 적용 흐름이 잘 설명되어 있어 해당 루틴을 기준으로 분석 및 이해를 목적으로 코드를 작성하였으며 실제 프로젝트에 적용하기에는 적절치 않다.

Architecture

source: https://source.android.com/devices/camera/index.html#architecture

Camera HAL(Hardware Abstraction Layer)에서 Application Framework단 사이에 구현된 스택. 왼쪽의 기존 Camera API와 비교하여 CameraService단에 Callback, Listener C++ Binder Interface가 보인다.

source: https://source.android.com/devices/camera/camera3_requests_hal.html

코드를 통해 다시 설명하겠지만, App - Camera2 API - HAL(Hardware Abstraction Layer)모델의 API 적용 프로세스를 표현한 그림.

Process

source: https://www.youtube.com/watch?v=92fgcUNCHic&feature=youtu.be&t=2130

Google I/O 2014 프레젠테이션에서 캡쳐한 Camera2 API 시퀀스. 다음은 영상의 프로세스 시퀸스 다이어그램.

  1. CameraManager
    • 사용가능한 카메라를 나열하고, CameraDevice를 취득하기 위한 Camera2 API의 첫번째 클래스.
  2. CameraCharacteristics
    • CameraManager에 의에 나열된 Camera 하드웨어, 사용가능 세팅등에 대한 정보 취득.
  3. CameraDevice
    • 실질적인 해당 카메라를 나타내는 클래스.
    • CameraManager에 의해 비동기 콜백으로 취득.
  4. CameraCaptureSession
    • CameraDevice에 의해 이미지 캡쳐를 위한 세션 연결.
    • 해당 세션이 연결될 surface를 전달.
  5. CaptureRequest
    • CameraDevice에 의해 Builder패턴으로 생성하며, 단일 이미지 캡쳐를 위한 하드웨어 설정(센서, 렌즈, 플래쉬) 및 출력 버퍼등의 정보(immutable).
    • 해당 리퀘스트가 연결될 세션의 surface를 타겟으로 지정.
  6. CaptureResult
    • CaptureRequest가 수행되고 비동기 CameraCaptureSession.CaptureCallback으로 취득.
    • 해당 세션의 리퀘스트 정보 뿐만 아니라 캡쳐 이미지의 Metadata정보도 포함.

Code Project

분석 및 이해를 용이하게 하기 위해 Camera2 APIs는 클래스 파일(Camera2APIs.java)하나만 작성하여 몰아 넣고 MainActivity에서 해당 클래스만 사용.

AndroidManifest.xml

카메라 디바이스 접근을 위한 권한. Android 6.0 이상에서는 권한요청 코드(requestPermission)가 추가되어야 한다.

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

activity_main.xml

Preview 화면을 위한 TextureView 추가. CaptureSessionsurface로 사용.

<TextureView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/textureView"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true" />

Camera2APIs.java

CameraManager

카메라 시스템 서비스 매니저 리턴.

public CameraManager CameraManager_1(Activity activity) {
    CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
    return cameraManager;
}

CameraCharacteristics

사용가능한 카메라 리스트를 가져와 후면 카메라(LENS_FACING_BACK) 사용하여 해당 cameraId 리턴. StreamConfiguratonMapCaptureSession을 생성할때 surfaces를 설정하기 위한 출력 포맷 및 사이즈등의 정보를 가지는 클래스. 사용가능한 출력 사이즈중 가장 큰 사이즈 선택.

public String CameraCharacteristics_2(CameraManager cameraManager) {
    try {
        for (String cameraId : cameraManager.getCameraIdList()) {
            CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
            if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
                StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                Size[] sizes = map.getOutputSizes(SurfaceTexture.class);

                mCameraSize = sizes[0];
                for (Size size : sizes) {
                    if (size.getWidth() > mCameraSize.getWidth()) {
                        mCameraSize = size;
                    }
                }

                return cameraId;
            }
        }
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }

    return null;
}

참고로, INFO_SUPPORTED_HARDWARE_LEVEL키값으로 카메라 디바이스의 레벨을 알 수 있는데 LEGACY < LIMITED < FULL < LEVEL_3 순으로 고성능이며 더 세밀한 카메라 설정이 가능하다. LEGACY 디바이스의 경우 구형 안드로이드 단말 호환을 위해 Camera2 API는 기존 Camera API의 인터페이스에 불과하다. 즉, 프레임 단위 컨트롤 등의 Camera2 기능은 사용할 수 없다.

CameraDevice

비동기 콜백 CameraDevice.StateCallback onOpened()로 취득. null파라미터는 MainThread를 이용하고, 작성한 Thread Handler를 넘겨주면 해당 Thread로 콜백이 떨어진다. 비교적 딜레이가 큰(~500ms) 작업이라 Thread 권장.

public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
    try {
        cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

onOpened()에서 취득한 CameraDeviceCaptureSession, CaptureRequest가 이뤄지는데 Camera2 APIs 처리과정을 MainActivity에서 일원화하여 표현하기 위해 인터페이스로 처리.

private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
    @Override
    public void onOpened(@NonNull CameraDevice camera) {
        mCameraDevice = camera;
        mInterface.onCameraDeviceOpened(camera, mCameraSize);
    }

    @Override
    public void onDisconnected(@NonNull CameraDevice camera) {
        camera.close();
    }

    @Override
    public void onError(@NonNull CameraDevice camera, int error) {
        camera.close();
    }
};

CaptureSession

일단 세션이 생성(비동기)된 후에는 해당 CameraDevice에서 새로운 세션이 생성되거나 종료하기 이전에는 유효.

public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
    try {
        cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

생성된 세션에 카메라 프리뷰를 위한 CaptureRequest정보 설정. 프리뷰 화면은 연속되는 이미지가 보여지기 때문에 CONTROL_AF_MODE_CONTINUOUS_PICTURE로 포커스를 지속적으로 맞추고, setRepeatingRequest로 해당 세션에 설정된 CaptureRequest세팅으로 이미지를 지속적으로 요청.

setRepeatingRequestnull파라미터는 MainThread를 사용하고, 작성한 Thread Handler를 넘겨주면 해당 Thread로 콜백이 떨어진다. 프리뷰 화면은 지속적으로 화면 캡쳐가 이뤄지기 때문에, MainThread 사용 시 Frame drop이 발생할 수 있다. Background Thread 사용 권장.

Google의 android-Camera2Basic샘플 코드에서는 CaptureRequest를 먼저 수행하는데, 여기서는 Google 프레젠테이션 자료 및 모델 그림의 프로세스를 기준으로 작성하기 위해 CaptureSession을 먼저 수행.

private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
    @Override
    public void onConfigured(CameraCaptureSession cameraCaptureSession) {
        try {
            mCaptureSession = cameraCaptureSession;
            mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
            cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {

    }
};

캡쳐된 이미지 정보 및 Metadata가 넘어오는데, 프리뷰에서는 딱히 처리할 작업은 없다. 사진 촬영의 경우라면, onCaptureCompleted()에서 촬영이 완료되고 이미지가 저장되었다는 메세지를 띄우는 시점. 캡쳐 이미지와 Metadata 매칭은 Timestamp로 매칭가능하다.

private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
    @Override
    public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
        super.onCaptureProgressed(session, request, partialResult);
    }

    @Override
    public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
        super.onCaptureCompleted(session, request, result);
    }
};

CaptureRequest

카메라 프리뷰(CameraDevice.TEMPLATE_PREVIEW)를 위한 Builder 패턴의 CaptureRequest생성. 예를 들어, 사진 촬영의 경우에는 CameraDevice.TEMPLATE_STILL_CAPTURE로 리퀘스트를 설정한다. surface는 해당 세션에 사용된 surface를 타겟으로 설정.

public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
    try {
        mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
        mPreviewRequestBuilder.addTarget(surface);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

이상으로 프리뷰를 위한 Camera2 APIs 코드 작성은 완료.

Camera2APIs.java 전체 코드.

public class Camera2APIs {

    interface Camera2Interface {
        void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize);
    }

    private Camera2Interface mInterface;
    private Size mCameraSize;

    private CameraCaptureSession mCaptureSession;
    private CameraDevice mCameraDevice;
    private CaptureRequest.Builder mPreviewRequestBuilder;

    public Camera2APIs(Camera2Interface impl) {
        mInterface = impl;
    }

    public CameraManager CameraManager_1(Activity activity) {
        CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);
        return cameraManager;
    }

    public String CameraCharacteristics_2(CameraManager cameraManager) {
        try {
            for (String cameraId : cameraManager.getCameraIdList()) {
                CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
                if (characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_BACK) {
                    StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                    Size[] sizes = map.getOutputSizes(SurfaceTexture.class);

                    mCameraSize = sizes[0];
                    for (Size size : sizes) {
                        if (size.getWidth() > mCameraSize.getWidth()) {
                            mCameraSize = size;
                        }
                    }

                    return cameraId;
                }
            }
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        return null;
    }

    private CameraDevice.StateCallback mCameraDeviceStateCallback = new CameraDevice.StateCallback() {
        @Override
        public void onOpened(@NonNull CameraDevice camera) {
            mCameraDevice = camera;
            mInterface.onCameraDeviceOpened(camera, mCameraSize);
        }

        @Override
        public void onDisconnected(@NonNull CameraDevice camera) {
            camera.close();
        }

        @Override
        public void onError(@NonNull CameraDevice camera, int error) {
            camera.close();
        }
    };

    public void CameraDevice_3(CameraManager cameraManager, String cameraId) {
        try {
            cameraManager.openCamera(cameraId, mCameraDeviceStateCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private CameraCaptureSession.StateCallback mCaptureSessionCallback = new CameraCaptureSession.StateCallback() {
        @Override
        public void onConfigured(CameraCaptureSession cameraCaptureSession) {
            try {
                mCaptureSession = cameraCaptureSession;
                mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
                cameraCaptureSession.setRepeatingRequest(mPreviewRequestBuilder.build(), mCaptureCallback, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {

        }
    };

    public void CaptureSession_4(CameraDevice cameraDevice, Surface surface) {
        try {
            cameraDevice.createCaptureSession(Collections.singletonList(surface), mCaptureSessionCallback, null);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    public void CaptureRequest_5(CameraDevice cameraDevice, Surface surface) {
        try {
            mPreviewRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
            mPreviewRequestBuilder.addTarget(surface);
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }
    }

    private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() {
        @Override
        public void onCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult) {
            super.onCaptureProgressed(session, request, partialResult);
        }

        @Override
        public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) {
            super.onCaptureCompleted(session, request, result);
        }
    };

    public void closeCamera() {
        if (null != mCaptureSession) {
            mCaptureSession.close();
            mCaptureSession = null;
        }

        if (null != mCameraDevice) {
            mCameraDevice.close();
            mCameraDevice = null;
        }
    }
}

MainActivity.java

준비

TextureViewsurface가 사용가능할 때 카메라 오픈을 위한 Listener 및 MainActivity에서 CaptureSession, CaptureRequest호출을 위한 Camera2Interface 설정.

public class MainActivity extends AppCompatActivity
        implements Camera2APIs.Camera2Interface, TextureView.SurfaceTextureListener {

    private TextureView mTextureView;
    private Camera2APIs mCamera;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mTextureView = (TextureView)findViewById(R.id.textureView);
        mTextureView.setSurfaceTextureListener(this);

        mCamera = new Camera2APIs(this);
    }
}

Open Camera

openCamera()만 호출하면 5단계 과정이 전부 수행되며, 프리뷰가 이뤄진다.

private void openCamera() {
    CameraManager cameraManager = mCamera.CameraManager_1(this);
    String cameraId = mCamera.CameraCharacteristics_2(cameraManager);
    mCamera.CameraDevice_3(cameraManager, cameraId);
}

@Override
public void onCameraDeviceOpened(CameraDevice cameraDevice, Size cameraSize) {
    SurfaceTexture texture = mTextureView.getSurfaceTexture();
    texture.setDefaultBufferSize(cameraSize.getWidth(), cameraSize.getHeight());
    Surface surface = new Surface(texture);

    mCamera.CaptureSession_4(cameraDevice, surface);
    mCamera.CaptureRequest_5(cameraDevice, surface);
}

Surface Texture가 준비 완료된 콜백을 받으면, 카메라 오픈.

@Override
protected void onResume() {
    super.onResume();

    if (mTextureView.isAvailable()) {
        openCamera();
    } else {
        mTextureView.setSurfaceTextureListener(this);
    }
}

/* Surface Callbacks */
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int i, int i1) {
    openCamera();
}

@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int i, int i1) {

}

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
    return true;
}

@Override
public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {

}

Close Camera

private void closeCamera() {
    mCamera.closeCamera();
}

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

실행화면

References

반응형

댓글