안드로이드에서 blur효과 구현하기 : 성능 개선 및 LiveBlurView 구현하기

이번 포스팅은 지난시간에 다룬 Box BlurStackBlur편에 이어 세번째 이야기입니다.

스택블러(Stack Blur)는 2Pass 전략을 사용하여 빠른 이미지 프로세싱 시간과 품질을 보장한다.
스택블러 알고리즘과는 별개로 안드로이드에서 조금 더 성능을 개선하는 방법에 대해서 알아보자.

성능 개선 방법

이전 시간에 다룬 스택블러로 HD품질의 이미지를 처리 할 경우 평균 60ms의 시간이 소요되었다. (s9기준)

이미지 한장을 처리한다고 생각하면 그리 긴 시간이 아니며, 그 정도는 사용자들도 기다려줄 수 있는 시간이다. 하지만 연속적인 이미지들을 실시간으로 블러처리를 해야하는 경우가 생긴다면 이야기가 달라진다.예를들면 다음과 같은 앱을 구현한다고 생각해보자. 이때는 정말 빠른 블러처리가 필요하다

 

(LiveBlurView를 구현한 모습)

일반적인 안드로이드 디바이스의 디스플레이는 60Hz의 주사율로 화면을 갱신한다.
(최근들어 120Hz이상의 디스플레이를 가진 기기도 출시되고 있지만, 여기에서는 60Hz라고 가정하자. )

60Hz로 화면을 갱신한다는 의미는, 1초에 60프레임(이미지)을 화면에 그려낸다는 뜻이다.
1초는 1000ms이므로 한 프레임을 그려내는데 필요한 시간은 최대 1000ms / 60 = 16.6ms 라는 계산이 나온다. 

만약 위의 영상과 같은 내용을 구현한다고 가정하면 하나의 프레임을 16.6ms 이내에 블러처리하고 렌더링까지 완료해야한다.

이미지를 축소하자(Downsampling)

이미지 전체를 원본해상도 그대로 블러처리 할 필요 없다. 이미지가 클수록 블러처리하는데 더 많은 시간이 필요하게 되기 떄문이다. 원본이미지를 블러 처리하나 축소된 이미지를 블러 처리하나 사람눈으로 보기에는 큰 차이가 없다. 어짜피 radius값이 높으면 높을수록 구분하기도 힘들어진다. 큰 이미지를 다루다 보면 OOM이 발생하기도 쉽다. 특히나 안드로이드 처럼 제한된 heap메모리 영역을 갖는 플랫폼에서는 저해상도 이미지로 다루는게 이상적이다.

다음과 같이 다운샘플링된 Bitmap 으로 이미지를 불러올 수 있다.

final BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 8;
Bitmap blurTemplate = BitmapFactory.decodeResource(getResources(), R.drawable.myImage, options);

inSampleSize를 8로 다운샘플링 하는경우 원본이미지의 1/64 사이즈로 로드 된다. 적당한 샘플 사이즈를 사용하되 스케일링으로 인한 품질 저하를 피하려면 2의 거듭제곱값 (2^n)으로 지정하는 것이 좋다.

다른 솔루션을 이용하는 것보다 이미지를 축소하는 것이 이미지 프로세싱(블러처리) 시간을 줄이는 가장 좋은 방법이다.

더 자세한 내용은 구글 공식 문서를 참조하자 (축소 버전을 메모리로 로드)

가능하다면 Bitmap 객체를 재사용하자

실시간으로 블러처리를 하거나 이와 유사하게 여러 이미지를 블러처리 해야하는 경우를 생각해보자. 메모리에 Bitmap을 여러번 로드하면 메모리가 부족해질 수 있지만, 그보다 메모리를 할당하는 시간 또한 작지 않기 때문에 Bitmap을 멤버변수로 관리하여 캐시된 상태로 유지하는 것이 좋다. 항상 동일한 변수를 사용하여 가비지 콜렉션을 최소화 할수도 있다.

또한 BitmapFactory.Options.inBitmap 옵션을 활용해보자. 이 옵션을 설정하면 Options 개체를 디코딩하는 메서드는 콘텐츠를 로드할 때 기존 비트맵을 재사용하도록 시도한다. 

RenderScript 또는 NDK를 사용하자

RenderScript

렌더 스크립트(Render Script)는 계산 집약적인 작업을 Android에서 고성능으로 실행하기 위한 프레임워크다. 렌더스크립트는 멀티 코어 및 GPU 등을 사용하여 병렬적으로 데이터를 연산하는데 적합하며, 이미지 처리에 특히 유용하다.

NDK(Native Development Kit)

NDK는 안드로이드 앱에서 C 또는 C++ 코드를 활용할 수 있도록 해준다. 소스코드를 작성하거나 기존에 빌드된 (prebuilt) 라이브러리를 활용할 수도 있다.

NDK는 대부분의 앱개발자에게는 유용하지 않다. 하지만 게임, 이미지 처리, 물리 시뮬레이션 같은 계산 집약적인 애플리케이션을 만드는 경우 Java 또는 Kotlin으로만 작성된 앱보다 성능을 더 끌어올릴 수 있다.

RenderScript와 NDK의 특징을 요약하면 다음과 같다.

RenderScript NDK
CPU, GPU 또는 기타 처리 장치를 사용하여 성능을 향상 시킴 범용적인 C++ 코드 및 라이브러리 사용
더 쉬운 병렬처리 API에 대한 제약사항이 없다
아키텍처 독립성 성능을 향상시키지만 아키텍처별로 컴파일해야함
이미지 처리, 3D렌더링, 컴퓨터비전에 적합 디버깅이 더 쉽다

LiveBlurView 구현하기

앞에서 언급한 내용들을 종합하여 실시간으로 블러처리를 하는 View를 구현할 수 있다.

  1. Activity가 가지고 있는 Window로부터 최상위 DecorView를 가져온다. (눈에 보이는 화면 DecorView)
  2. DecorView로부터 LiveBlurView의 영역만큼 크롭하여 다운샘플링된 Bitmap으로 관리한다.
  3. 크롭한 Bitmap을 Blur처리 한다. (C++로 작성된 StackBlur 사용)
  4. LiveBlurView에 Blur처리된 결과를 그린다.

이 내용을 반복하면 실시간으로 Blur처리된 화면을 확인할 수 있다.

LiveBlurView에서 300dp * 300dp 기준으로 블러처리하는데 평균 4ms정도 소요되는것을 확인했다. 화면전체를 Blur처리해도 S9에서는 프레임드랍이 없었으나, 저사양기기에서는 프레임드랍이 발생할지도 모르겠다.

마치며

일반적인 경우 RenderScript가 C++로 작성된 코드보다 더 빠르다고 이전 시간에 이야기 했지만, 실제로 샘플 앱을 작성해 본 결과 C++코드로 작성된 StackBlur 알고리즘이 더 빨랐다. 일단 RenderScript의 경우 초기 메모리 할당에서 많은 시간이 소요되는 것을 확인했지만, 그래도 이미지 처리 시간은 C++보다 빠르리라 예측 했는데 그렇지 않았다.  추측하건데 RenderScript의 경우 GPU를 포함한 다른 장치도 사용하는데 이 부분에서 병목이 발생하지 않았을까 추측해본다. 일부기기에서는 RenderScript가 더 빠르게 동작할지도 모르겠다. (아니면 내가 RenderScript용 스크립트를 잘못 작성했거나…)

샘플 앱 및 LiveBlurView 코드는 github에서 확인 하실 수 있습니다.

Buy me a coffeeBuy me a coffee

4개의 댓글

안드 공부중 · 2020년 5월 11일 12:29 오전

camera preview 상태에서 blur 처리를 하기 힘들까요?
surfaceview위에 저 레이아웃을 올려놔도 제대로 안될꺼같고 카메라 자체를 처리 해야될꺼같은데
camera api 에서 저게 가능할지 질문드립니다! ㅠㅠ
만약 preview 상태에서 blur 처리 할려면 openCV나 opengles같은걸 이용해야될까요?

    안드 공부중 · 2020년 5월 11일 12:31 오전

    추가적으로 frameLayout처럼 저런 blur 효과 레이아웃 위에 정적인 텍스트나 이미지를 올렸을때 blur효과 처리가 안되게끔도 될까요?

      Charlezz · 2020년 5월 12일 2:33 오후

      현재는 최상위 DecorView를 가지고 와서 Blur 처리를 하고 있지만

      텍스트나 이미지가 포함되지 않은 ViewGroup을 가지고 Blur처리를 하시면 가능할 것 같습니다.

    Charlezz · 2020년 5월 12일 2:34 오후

    SurfaceView는 SurfaceFlinger에 합성되기 전 별도의 레이어에 그리기 때문에 LiveBlurView 는 동작하지 않습니다.
    만약 LiveBlurView 사용을 원하시면 TextureView를 사용하여 카메라 프리뷰를 렌더링 해보세요.

    꼭 SurfaceView를 사용하셔야 한다면 GLSurfaceView를 사용하시고 OpenGL API(+쉐이더)를 이용하시면 됩니다.

답글 남기기

이메일은 공개되지 않습니다.