이전 포스트에서 SurfaceView에 대해 알아보았습니다. Surface가 생성될 때 카메라 프리뷰를 설정하는 메소드를 실행해주었는데 본 포스트에서 그 메소드에 대해 알아보겠습니다.
private Handler mHandler;
private ImageReader mImageReader;
@TargetApi(19)
public void initCameraAndPreview() {
HandlerThread handlerThread = new HandlerThread("CAMERA2");
handlerThread.start();
mHandler = new Handler(handlerThread.getLooper());
Handler mainHandler = new Handler(getMainLooper());
try {
String mCameraId = "" + CameraCharacteristics.LENS_FACING_FRONT; // 후면 카메라 사용
CameraManager mCameraManager = (CameraManager) this.getSystemService(Context.CAMERA_SERVICE);
CameraCharacteristics characteristics = mCameraManager.getCameraCharacteristics(mCameraId);
StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
Size largestPreviewSize = map.getOutputSizes(ImageFormat.JPEG)[0];
Log.i("LargestSize", largestPreviewSize.getWidth() + " " + largestPreviewSize.getHeight());
setAspectRatioTextureView(largestPreviewSize.getHeight(),largestPreviewSize.getWidth());
mImageReader = ImageReader.newInstance(largestPreviewSize.getWidth(), largestPreviewSize.getHeight(), ImageFormat.JPEG,/*maxImages*/7);
mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mainHandler);
if (ActivityCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
return;
}
mCameraManager.openCamera(mCameraId, deviceStateCallback, mHandler);
} catch (CameraAccessException e) {
Toast.makeText(this, "카메라를 열지 못했습니다.", Toast.LENGTH_SHORT).show();
}
}
camera2 API를 먼저 알아봅시다. camera2 API는 기존 camera API의 단순 기능들을 보완한 상위버전의 API라고 보시면 되겠습니다. 즉 기능이 더 많아졌다 정도로 생각하면 되겠습니다.
HandlerThread는 스레드에 루퍼와 핸들러를 연결한 클래스입니다. 루퍼는 자신이 속한 스레드의 메시지큐에 추가되는 메시지를 기다리다가 꺼내서 이를 처리할 핸들러에 디스패치하는 기능을 합니다.
위 소스코드를 읽어보자면 CAMERA2 HandlerThread 클래스를 선언하고 시작합니다. 돌아가고 있는 스레드의 루퍼(getLooper())를 이용해서 디스패치할 핸들러(mHandler)를 정의해줍니다. 스레드의 핸들러는 mHandler이고 메인 핸들러는 mainHandler로 정의해줍니다.이후 디바이스 기반 코드이기 때문에 권한 및 하드웨어 에러를 고려하여 try catch문으로 접근합니다. mCameraId는 전면 또는 후면 카메라 사용 키 값입니다. (전면 카메라가 셀카죠)
- LENS_FACING_FRONT: 후면 카메라. value : 0
- LENS_FACING_BACK: 전면 카메라. value : 1
- LENS_FACING_EXTERNAL: 기타 카메라. value : 2
CameraManager 모든 카메라 관련 작업은 카메라 매니저와 함께합니다. 카메라 매니저를 통해 CameraCharacteristic을 가져옵니다 이건 말 그대로 카메라 특성을 담고있는 클래스라 보시면 됩니다. 위 소스코드에서 SCALER_STREAM_CONFIGURATION_MAP를 가져왔는데 이 안에 많은 정보들이 있으나 사용하는 정보는 getOutputSizes입니다.
getOutputSizes ( ) 함수에 이미지 포맷을 매개변수로 지정하면 지원하는 크기 목록이 Size 객체의 배열로 반환되는데, 이 값을 이용하여 사진 촬영 시 사진 크기를 지정할 수 있습니다.
출처: https://kkangsnote.tistory.com/48 [깡샘의 토마토]
setAspectRatioTextureView
는 현재 뷰의 종횡비를 설정해주는 것으로 위 코드에서 카메라의 가로 세로 사이즈를 입력했습니다.카메라 디바이스 세팅과 종횡비 설정을 한 것 같네요 그럼 카메라에서 입력받은 이미지들을 담을 객체를 만들어야겠습니다.ImageReader 객체를 정의해주고 파라미터로 가로, 세로, 파일 포맷, 버퍼 사이즈를 입력해줍니다. 그리고 리스너를 설정해주는데 위 코드에서 리스너를 커스텀하게 구현했습니다. 아래 리스너의 기능은 카메라에서 받은 이미지(바이트)를 버퍼에 넣고 바이트를 배열로 디코딩해준 후 비트맵으로 바꿔주는 기능입니다. 여기서 메인 핸들러에 할당한 이유는 카메라 스레드는 이미 카메라 데이터를 받아오는데 할당이 되어있는데 바이트2비트맵 연산까지 처리할 수 없기 때문에 메인 스레드에 할당하는겁니다.
private ImageReader.OnImageAvailableListener mOnImageAvailableListener = 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()];
buffer.get(bytes);
final Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
new SaveImageTask().execute(bitmap);
}
};
그 다음 카메라 허용에 대한 퍼미션 부분을 체크해주고
카메라 매니저의 오픈 카메라 메소드를 통해 카메라 세팅을 끝내줍니다. 여기서 파라미터로 deviceStateCallback객체가 존재하는데 다음과 같이 구현되어 있습니다.
private CameraDevice mCameraDevice;
private CameraDevice.StateCallback deviceStateCallback = new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
mCameraDevice = camera;
try {
takePreview();
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onDisconnected(@NonNull CameraDevice camera) {
if (mCameraDevice != null) {
mCameraDevice.close();
mCameraDevice = null;
}
}
@Override
public void onError(CameraDevice camera, int error) {
Toast.makeText(MainActivity.this, "카메라를 열지 못했습니다.", Toast.LENGTH_SHORT).show();
}
};
객체는 카메라가 열렸을 때(onOpened) 닫혔을 때(onDisconnected) 에러났을 때(onError)로 구성되어 있습니다. 볼만한건 열렸을 때 호출되는 takePreview()겠군요 아래 코드가 있습니다.
private CaptureRequest.Builder mPreviewBuilder;
public void takePreview() throws CameraAccessException {
mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
mPreviewBuilder.addTarget(mSurfaceViewHolder.getSurface());
mCameraDevice.createCaptureSession(Arrays.asList(mSurfaceViewHolder.getSurface(), mImageReader.getSurface()), mSessionPreviewStateCallback, mHandler);
}
코드를 읽어보자면 카메라 디바이스에서 촬영요청을 담당할 객체(mPreviewBuilder)를 생성하고 그 객체가 타겟팅할 객체(mSurfaceViewHolder.getSurface())를 설정해줍니다. 그리고 카메라 디바이스에 대한 캡쳐세션을 생성해주는데 mSessionPreviewStateCallback이 들어가있고 아래와 같이 구현되어 있습니다.
private CameraCaptureSession mSession;
private CameraCaptureSession.StateCallback mSessionPreviewStateCallback = new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(@NonNull CameraCaptureSession session) {
mSession = session;
try {
mPreviewBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);
mPreviewBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH);
mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mHandler);
} catch (CameraAccessException e) {
e.printStackTrace();
}
}
@Override
public void onConfigureFailed(@NonNull CameraCaptureSession session) {
Toast.makeText(MainActivity.this, "카메라 구성 실패", Toast.LENGTH_SHORT).show();
}
};
객체는 세션 설정과 실패로 구성되어 있습니다. 세션 설정 안에는 모드와 지속적으로 촬영하는지, 자동 플래시 기능 이런 설정들이 담겨있네요 물론 하드웨어와 연동되기 때문에 트라이 캐치문을 사용했습니다. 그리고 위 설정을 반복 요청을 하도록 세션에 세팅해줍니다.
'안드로이드' 카테고리의 다른 글
"Top 10 Android Apps for Boosting Productivity" (0) | 2023.05.03 |
---|---|
[안드로이드] 밑바닥부터 딥러닝 호환 카메라 앱 만들기 #3 onCreate() (1) | 2021.04.02 |
[안드로이드] 밑바닥부터 딥러닝 호환 카메라 앱 만들기 #2 액티비티 생명주기 (0) | 2021.03.31 |
[안드로이드] 밑바닥부터 딥러닝 호환 카메라 앱 만들기 #1-1 SurfaceView (0) | 2021.03.31 |
[안드로이드] 밑바닥부터 딥러닝 호환 카메라앱 만들기 #1 MainActivity.java, activity_main.xml (0) | 2021.03.31 |