brunch

You can make anything
by writing

C.S.Lewis

by 서준수 Feb 14. 2020

안드로이드 카메라 스티커 예제

스마트폰 카메라 앱에 여러 가지 기능이 있지만 수많은 카메라앱에서 스티커 기능을 기본적으로 제공하고 있다. 구현 방법은 다양하겠지만 대충 간단한 스티커 기능을 제공하는 카메라 예제를 만들어 보았다. 상용 수준엔 절대 못 미치고 그냥 이런 식으로 하면 스티커 기능이 된다라는 정도의 가벼운 예제이다. 날코딩 예제 ㄱㄱ


사용된 리소스 및 레이아웃, 모든 작업을 마친 소스파일은 다음 첨부파일을 받으면 된다.


기본 뼈대가 되는 카메라 앱은 구글의 오래된 예제를 사용한다. 예제는 다음 링크에서 받을 수 있다.


https://github.com/googlearchive/android-Camera2Raw


이 예제에 스티커 기능을 추가할 것이다. 앱 이름에서 알 수 있듯이 이 앱은 raw 파일까지 저장한다. 스티커를 만드는 것과는 아무 상관이 없고 그냥 이 예제를 예전에 받아 놓은 적이 있어서 사용했을 뿐이다. raw 파일이 저장되는 게 불편하면 해당 부분을 주석 처리하거나 지우 거나하면 된다. ^^;


몇 가지 제약 사항들이 있다.

1. 사진을 찍어보면 세로 모드에서 촬영 시 사진이 가로로 저장되는 것을 알 수 있다. 그에 맞는 별도의 스티커 방향 처리를 하지 않을 것이기 때문에 그냥 가로모드로 고정하도록 할 것이다. 스티커가 들어간 이미지를 만드는 게 목적이기 때문에 해당 사항은 필요시 추가하면 된다.


2. 프리뷰에서 보이는 스티커 위치와 실제 저장되는 이미지에서 스티커 위치가 완전히 일치하지 않는다. 이 오차는 프리뷰에서 보이고 있는 해상도(스마트폰 화면 해상도)와 실제 저장되는 이미지의 해상도가 불일치하기 때문에 정확히 맞추기 어려웠다. (비율을 계산해서 적절한 리소스를 사용해야함) 따라서 비슷한 위치에 놓이도록 캘리브레이션 했다. (대충 하드 코딩했다는 소리) 그래서 스티커 위치 조정이나 크기 변경 같은 것도 없다. 그냥 고정이다. 나중에 해봐야겠다.


카메라 스티커 기능을 추가하기 위해 한 일은 다음과 같다.

1. 카메라 프리뷰에 스티커 이미지 띄우기

2. 촬영 후 프리뷰 이미지와 스티커 이미지 합성하기

It's simple.


별 영양가도 없는 서론이 너무 길었다. 아주 간단하기 때문에 바로 코드를 보자.


먼저 가로로 고정하기 위해서 매니페스트에 다음과 같이 옵션을 준다.


<activity android:name="com.example.android.camera2raw.CameraActivity"
          android:screenOrientation="landscape"
          android:label="@string/app_name">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>



다음으로 프리뷰에 보일 스티커를 담을 이미지뷰를 추가한다.

layout-land/fragment_camera2_basic.xml에 마지막에 다음 내용을 추가한다.


<FrameLayout
    android:id="@+id/sticker"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_alignParentTop="true"
    android:layout_marginLeft="150dp"
    android:layout_marginTop="50dp"
    android:background="@android:color/transparent">

    <ImageView
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:background="@android:color/transparent"
        android:src="@drawable/pengsoo_bg">
    </ImageView>

</FrameLayout>


이미지뷰의 배경은 투명으로 지정했다. 스티커로 사용될 이미지(pengsoo_bg.png)도 배경은 투명한 파일이기 때문에 좀 더 자연스러운 모습을 연출할 수 있다. (쓸데없는 디테일)

스티커로 사용될 직접 그린 펭수


스티커로 사용할 이미지는 요즘 취미로 그림 그리기를 하고 있어서 직접 그린 펭수 이미지이다. (또 잡소리;)


이렇게 하면 카메라 프리뷰 실행 시 펭수 이미지가 오버레이 되어 스티커 효과를 낼 수 있다.

그럼 이제 JPEG가 만들어지는 부분을 찾아서 파일이 저장되기 전에 이미지를 합성하는 처리를 하면 된다.


이미지 합성은 Camera2RawFragment.java에서 run()을 검색하여 다음 내용을 추가하면 된다.


추가한 내용을 살펴보자.

일단 resizingImage는 촬영한 사진 이미지 해상도를 조정한 이미지이다. 원본 이미지 해상도가 너무 높기 때문에 거기에 200dp x 200dp 사이즈의 스티커를 넣으면 매우 작게 보여 프리뷰에서 보는 것과 차이가 심하다. 그래서 createScaledBitmap()를 사용하여 그냥 적당히 1920x1440으로 조정했다. 이렇게 날코딩한 것에도 나름의 이유가 있다. 다음과 같이 카메라 앱을 실행하고 해당 화면을 캡처한 후 거기서 프리뷰가 나오는 영역의 사이즈(빨간 박스)가 1920x1440인 것이다. (매우 과학적)



createScaledBitmap()의 마지막 인자는 filter인데 true로 해주면 사이즈를 조절할 때 이미지가 덜 깨지도록 해준다.


이렇게 나온 결과물과 펭수 스티커 이미지를 합성하기 위해서 일단 비어있는 Canvas를 하나 만든다. Canvas는 말 그대로 캔버스이다. 개인적으로 좀 더 쉽게 캔버스 개념을 생각하자면 이젤(easel)에 가깝다고 생각한다. 실제 캔버스(도화지)는 Paint 객체나 지금 사용하는 것처럼 bitmap을 설정해주는 것이 아닐까 싶다.


아무튼 캔버스에 resultImage를 깔면 그다음부터 캔버스에 그려지는 내용들이 resultImage에 반영된다.

resultImage는 createBitmap()을 통해서 만들었다. 첫 번째 두 번째 인자는 width, height 값인데 resizingImage의 크기와 동일하게 준다. 세 번째 인자는 config인데 ARGB_8888은 투명 값을 이용할 수 있는 옵션이다.


이렇게 생성된 도화지에 촬영한 사진 이미지(resizingImage)를 그리고 다음으로 펭수 이미지를 그린다.


펭수 이미지를 그릴 때 mPengsoo는 리소스를 bitmap으로 읽어온 것이다.


mPengsoo = BitmapFactory.decodeResource(getResources(), R.drawable.pengsoo_bg);


그리고 위치 값으로 189*sDensity, 97*sDensity를 사용했는데 숫자는 적당히 캘리브... 아니 하드 코딩해서 맞춘 것이다. sDensity는 CameraActivity.java에서 다음과 같이 단말기 density를 가져온 값이다.


sDensity = getApplicationContext().getResources().getDisplayMetrics().density;



여기까지 진행하면 합성된 이미지가 되는 것이다. 말로만 들으면 와 닿지 않을 수 있어서 다음과 같이 그림을 그려봤다.

1. Canvas 생성

2. 투명 값 사용 가능한 빈 이미지 resultImage를 도화지로 설정

3. (크기를 조절한) 촬영한 사진 이미지를 도화지에 그림

4. 펭수도 추가로 그림


이렇게 열심히 준비한 이미지는 compress() 함수를 이용하여 파일로 저장한다.

compress()의 첫 번째 인자는 압축할 포맷이고 두 번째 인자는 퀄리티인데 100으로 주면 최상의 퀄리티이다. 이미지에서 퀄리티의 수치이니 압축률 정도가 되겠다. 100%로 압축한다는 말은 곧 압축하지 않는다는 의미이니 최상의 퀄리티인 것이다. 추가적으로 설명하면 png 포맷이 비손실 압축이라 퀄리티 값은 무시된다. 세 번째 인자는 OutputStream인데 파일로 저장하기 위해서 미리 생성한 FileOutputSteam을 넘겨주고 있다.


스티커를 적용하여 촬영한 결과물은 다음과 같다. 완성~!

펭하, 아니 펭바!
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari