일모도원(日暮途遠) 개발자

[Android 개발] 카메라 앱 기본 기능 본문

안드로이드 개발/안드로이드

[Android 개발] 카메라 앱 기본 기능

달님개발자 2023. 9. 4. 14:46

카메라 기능을 구현하기 위하여 공부하던중, 아래에 있는 코드는 Kotlin으로 되어 있어, 사진 찍고 이미지 파일을 저장하는 부분까지만 자바로 구현해보았다. (자바 11 사용중)

 

https://developer.android.com/codelabs/camerax-getting-started?hl=ko#0 

 

CameraX 시작하기  |  Android Developers

이 Codelab에서는 CameraX를 사용하여 뷰파인더를 표시하고, 사진을 찍고, 카메라에서 이미지 스트림을 분석하는 카메라 앱을 만드는 방법을 소개합니다.

developer.android.com

먼저 Empty Views Activity를 선택하자.

이름은 적당히주자. 난 package이름을 com.android.example.camerajavaapp로 하였다.

만약 빈 프로젝트를 만들었는데, 에러가 발생하면 여기를 참고하자.

 

 

 

앱 모듈의 build.gradle의 dependencies에 아래를 추가하자. 현재 최신 버전은 1.2.1이다.

def camerax_version = "1.2.1"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"

implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"

앱 모듈의 build.gradle의 android {에는 viewBinding을 추가하고 Sync Now를 클릭하자.

buildFeatures {
  viewBinding true
}

activity_main.xml 파일은 아래내용으로 교체하자. 나는 image_capture_button 버튼만 사용한다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.camera.view.PreviewView
        android:id="@+id/viewFinder"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

    <Button
        android:id="@+id/image_capture_button"
        android:layout_width="110dp"
        android:layout_height="110dp"
        android:layout_marginBottom="50dp"
        android:elevation="2dp"
        android:text="@string/take_photo"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

strings.xml도 아래처럼 업데이트 하자. 앱이름은 본인껄로 수정해도 된다

<resources>
   <string name="app_name">Dalnim CameraXApp</string>
   <string name="take_photo">Take Photo</string>
</resources>

AndroidManifest.xml에 필요한 권한을 요청하자. 안드로이드 SDK 29이상이면 WRITE_EXTERNAL_STORAGE는 요청안해도 된다.

<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
   android:maxSdkVersion="28" />

 


카메라 권한 요청 코드. 카메라 권한을 얻으면, startCamera() 메소드를 실행한다.

public class MainActivity extends AppCompatActivity {
    private static final int REQUEST_CODE_PERMISSIONS = 10;

    private String[] REQUIRED_PERMISSIONS = {
            android.Manifest.permission.CAMERA
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {

        // 카메라 권한이 있으면 카메라 실행. 없으면 카메라 권한 요청
        if (allPermissionsGranted()) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }
    }
    
    //카메라 권한이 있는지 검사
    private boolean allPermissionsGranted() {
        for (String permission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    //카메라 권한 요청 결과 처리
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera();
            } else {
                Toast.makeText(this, "유저가 권한을 주어야 사용가능합니다.", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }
}

카메라를 실행하고 Preview에 카메라에 찍히는 영상을 보여주는 코드.

만약 사진 저장이 필요없으면, 다음처럼 imageCapture 인자를 생략하면 된다. (imageCapture관련 코드도 필요없어짐)
cameraProvider.bindToLifecycle(MainActivity.this, cameraSelector, preview);

public class MainActivity extends AppCompatActivity {
    private ImageCapture imageCapture;
    private ExecutorService cameraExecutor;
    
    //카메라를 실행하고 preview에 촬영되고 있는 영상을 보여준다.
    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        cameraProviderFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {

                    ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

                    // Preview
                    Preview preview = new Preview.Builder().build();
                    preview.setSurfaceProvider(viewBinding.viewFinder.getSurfaceProvider());

                    imageCapture = new ImageCapture.Builder().build();
                    // 후면 카메라 선택
                    CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

                    // 기존에 바인드 된게 있으면 해제부터 한다.
                    cameraProvider.unbindAll();

                    // 카메라의 생명주기를 이 액티비티와 동일하게 한다. (사진 저장이 필요없으면, imageCapture 인자를 생략하면 된다.)
                    cameraProvider.bindToLifecycle(
                            MainActivity.this, cameraSelector, preview, imageCapture);

                } catch (ExecutionException | InterruptedException exc) {
                    Log.e(TAG, "카메라 뷰어 바인딩 실패", exc);
                }
            }
        }, ContextCompat.getMainExecutor(this));
    }
}

 

버튼을 눌러서 사진을 찍고, 파일로 저장하는 코드. contentValues와 outputOptions은 파일을 저장하기 위한 정보를 담는다.

//사진을 찍어서 파일로 저장을 한다.
private void takePhoto() {
    if (imageCapture == null) {
        return;
    }

    ContentValues contentValues = getContentValues();

    ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
            getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            contentValues)
            .build();

    imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            new ImageCapture.OnImageSavedCallback() {
                @Override
                public void onError(@NonNull ImageCaptureException exc) {
                    Log.e(TAG, "사진 저장 실패 : " + exc.getMessage(), exc);
                }

                @Override
                public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
                    String msg = "사진 저장 성공 : " + output.getSavedUri();
                    Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
                    Log.d(TAG, msg);
                }
            }
    );
}

//찍은 사진이 저장될 경로
@NonNull
private static ContentValues getContentValues() {
    String name = new SimpleDateFormat(FILENAME_FORMAT, Locale.US)
            .format(System.currentTimeMillis());

    ContentValues contentValues = new ContentValues();
    contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
    contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");

    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
        contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Dalnim-CameraX-Image");
    }
    return contentValues;
}

 

만약 캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageSavedCallback대신 OnImageCapturedCallback을 호출하면 된다.

//캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageCapturedCallback을 호출하면 된다.
imageCapture.takePicture(
        ContextCompat.getMainExecutor(this),
        new ImageCapture.OnImageCapturedCallback() {
            @Override
            public void onCaptureSuccess(ImageProxy image) {
                super.onCaptureSuccess(image);
                //캡쳐된 image파일을 사용하면 된다.
                Log.e(TAG, "사진 캡처 성공");
            }

            @Override
            public void onError(ImageCaptureException exception) {
                Log.e(TAG, "사진 캡처 실패 : " + exception.getMessage(), exception);
            }
        });

 

전체 소스는 아래를 참고하자.

https://github.com/dalnim/CameraJavaApp

 

GitHub - dalnim/CameraJavaApp: Sample code to use Android's CameraX in Java

Sample code to use Android's CameraX in Java. Contribute to dalnim/CameraJavaApp development by creating an account on GitHub.

github.com

package com.android.example.camerajavaapp;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.camera.core.CameraSelector;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.ImageCaptureException;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Preview;
import androidx.camera.lifecycle.ProcessCameraProvider;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.content.ContentValues;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;

import com.android.example.camerajavaapp.databinding.ActivityMainBinding;
import com.google.common.util.concurrent.ListenableFuture;

import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding viewBinding;
    private ImageCapture imageCapture;
    private ExecutorService cameraExecutor;

    private static final String TAG = "CameraXApp";
    private static final String FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS";
    private static final int REQUEST_CODE_PERMISSIONS = 10;

    private String[] REQUIRED_PERMISSIONS = {
            android.Manifest.permission.CAMERA
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        viewBinding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(viewBinding.getRoot());

        // 카메라 권한이 있으면 카메라 실행. 없으면 카메라 권한 요청
        if (allPermissionsGranted()) {
            startCamera();
        } else {
            ActivityCompat.requestPermissions(
                    this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS);
        }

        viewBinding.imageCaptureButton.setOnClickListener( v -> takePhoto());
        cameraExecutor = Executors.newSingleThreadExecutor();
    }

    //사진을 찍어서 파일로 저장을 한다.
    private void takePhoto() {
        if (imageCapture == null) {
            return;
        }

        ContentValues contentValues = getContentValues();

        ImageCapture.OutputFileOptions outputOptions = new ImageCapture.OutputFileOptions.Builder(
                getContentResolver(),
                MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                contentValues)
                .build();

        //캡쳐된 이미지를 저장하지 않고, 바로 사용할경우는 outputOptions을 넘기지 않고, OnImageCapturedCallback을 호출하면 된다.
        imageCapture.takePicture(
                ContextCompat.getMainExecutor(this),
                new ImageCapture.OnImageCapturedCallback() {
                    @Override
                    public void onCaptureSuccess(ImageProxy image) {
                        super.onCaptureSuccess(image);
                        //캡쳐된 image파일을 사용하면 된다.
                        Log.e(TAG, "사진 캡처 성공");
                    }

                    @Override
                    public void onError(ImageCaptureException exception) {
                        Log.e(TAG, "사진 캡처 실패 : " + exception.getMessage(), exception);
                    }
                });


        imageCapture.takePicture(
                outputOptions,
                ContextCompat.getMainExecutor(this),
                new ImageCapture.OnImageSavedCallback() {
                    @Override
                    public void onError(@NonNull ImageCaptureException exc) {
                        Log.e(TAG, "사진 저장 실패 : " + exc.getMessage(), exc);
                    }

                    @Override
                    public void onImageSaved(@NonNull ImageCapture.OutputFileResults output) {
                        String msg = "사진 저장 성공 : " + output.getSavedUri();
                        Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
                        Log.d(TAG, msg);
                    }
                }
        );
    }

    //찍은 사진이 저장될 경로
    @NonNull
    private static ContentValues getContentValues() {
        String name = new SimpleDateFormat(FILENAME_FORMAT, Locale.US)
                .format(System.currentTimeMillis());

        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");

        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
            contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/Dalnim-CameraX-Image");
        }
        return contentValues;
    }

    //카메라를 실행하고 preview에 촬영되고 있는 영상을 보여준다.
    private void startCamera() {
        ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(this);

        cameraProviderFuture.addListener(new Runnable() {
            @Override
            public void run() {
                try {

                    ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

                    // Preview
                    Preview preview = new Preview.Builder().build();
                    preview.setSurfaceProvider(viewBinding.viewFinder.getSurfaceProvider());

                    imageCapture = new ImageCapture.Builder().build();
                    // 후면 카메라 선택
                    CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;

                    // 기존에 바인드 된게 있으면 해제부터 한다.
                    cameraProvider.unbindAll();

                    // 카메라의 생명주기를 이 액티비티와 동일하게 한다. (사진 저장이 필요없으면, imageCapture 인자를 생략하면 된다.)
                    cameraProvider.bindToLifecycle(
                            MainActivity.this, cameraSelector, preview, imageCapture);

                } catch (ExecutionException | InterruptedException exc) {
                    Log.e(TAG, "카메라 뷰어 바인딩 실패", exc);
                }
            }
        }, ContextCompat.getMainExecutor(this));
    }

    //카메라 권한이 있는지 검사
    private boolean allPermissionsGranted() {
        for (String permission : REQUIRED_PERMISSIONS) {
            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
                return false;
            }
        }
        return true;
    }

    //카메라 권한 요청 결과 처리
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                startCamera();
            } else {
                Toast.makeText(this, "유저가 권한을 주어야 사용가능합니다.", Toast.LENGTH_SHORT).show();
                finish();
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        imageCapture = null;
        cameraExecutor.shutdown();
    }
}