Nuke Olaf - Log Store

[Android] 안드로이드 - natario1/CameraView 카메라 라이브러리 사용방법 분석 - 필터부분 (소스코드 포함) 본문

Android

[Android] 안드로이드 - natario1/CameraView 카메라 라이브러리 사용방법 분석 - 필터부분 (소스코드 포함)

NukeOlaf 2020. 1. 5. 17:14

1. cameraView 레이아웃에 attribute 를 추가하여 제스처를 통해 필터값을 조정할 수 있다

추가로 봐야할 것) 제스처가 아닌 seekBar 로 필터 tune 을 할 수 있는지 찾아보기

app:cameraGestureScrollHorizontal="filterControl1" 은 횡스크롤을 통해 필터를 컨트롤하겠다는 뜻

실험해본 결과 필터 컨트롤은 되지만 필터 컨트롤이 어떤 방식으로 동작하는지, 우리가 커스텀할 수 있는지까지는 찾아보지 못했음

<com.otaliastudios.cameraview.CameraView
    android:id="@+id/camera"
    android:keepScreenOn="true"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:cameraFilter="@string/cameraview_filter_none"
    app:cameraGestureScrollHorizontal="filterControl1"
    android:layout_weight="9"/>

 

2. 카메라에 필터를 적용하는 방법은 다음과 같다

camera.filter = Filters.BRIGHTNESS.newInstance()

< 기본적으로 제공하는 필터의 종류 >

기본 필터를 NoFilter( Filters.NONE ) 이라고하며 이전에 설정 한 다른 필터를 지우고 다시 정상으로 되돌릴 수 있다.

NoFilter Filters.NONE @string/cameraview_filter_none
AutoFixFilter Filters.AUTO_FIX @string/cameraview_filter_autofix
BlackAndWhiteFilter Filters.BLACK_AND_WHITE @string/cameraview_filter_black_and_white
BrightnessFilter Filters.BRIGHTNESS @string/cameraview_filter_brightness
ContrastFilter Filters.CONTRAST @string/cameraview_filter_contrast
CrossProcessFilter Filters.CROSS_PROCESS @string/cameraview_filter_cross_process
DocumentaryFilter Filters.DOCUMENTARY @string/cameraview_filter_documentary
DuotoneFilter Filters.DUOTONE @string/cameraview_filter_duotone
FillLightFilter Filters.FILL_LIGHT @string/cameraview_filter_fill_light
GammaFilter Filters.GAMMA @string/cameraview_filter_gamma
GrainFilter Filters.GRAIN @string/cameraview_filter_grain
GrayscaleFilter Filters.GRAYSCALE @string/cameraview_filter_grayscale
HueFilter Filters.HUE @string/cameraview_filter_hue
InvertColorsFilter Filters.INVERT_COLORS @string/cameraview_filter_invert_colors
LomoishFilter Filters.LOMOISH @string/cameraview_filter_lomoish
PosterizeFilter Filters.POSTERIZE @string/cameraview_filter_posterize
SaturationFilter Filters.SATURATION @string/cameraview_filter_saturation
SepiaFilter Filters.SEPIA @string/cameraview_filter_sepia
SharpnessFilter Filters.SHARPNESS @string/cameraview_filter_sharpness
TemperatureFilter Filters.TEMPERATURE @string/cameraview_filter_temperature
TintFilter Filters.TINT @string/cameraview_filter_tint
VignetteFilter Filters.VIGNETTE @string/cameraview_filter_vignette

 

3. 듀오톤 필터를 적용할 수 있다

var duotoneFilter = DuotoneFilter()
duotoneFilter!!.firstColor = Color.RED
duotoneFilter!!.secondColor = Color.BLUE

camera.filter = duotoneFilter

듀오톤 필터 객체를 생성하고, 첫번째 색상과 두번째 색상의 값을 설정해주면 된다.

Color.RED 와 Color.BLUE 가 아니더라도 16진수로 표현된 다른 색상을 지정할 수 있다.

아래는 16진수로 표현된 Cyan 과 Magenta 듀오톤 필터이다.

var duotoneFilter = DuotoneFilter()
duotoneFilter!!.firstColor = 0xFFFF00FF.toInt()
duotoneFilter!!.secondColor = 0xFF00FFFF.toInt()

camera.filter = duotoneFilter

 

Color.RED 와 Color.BLUE 가 적용된 듀오톤 필터를 적용하면 아래와 같은 결과가 출력된다

 

4. 멀티 필터를 적용할 수 있다

멀티필터 객체를 생성해서 나중에 멀티 필터에 다른 필터를 추가할 수도 있다.

그러나 필터를 많이 넣을수록 그래픽 메모리 소비가 많아진다고 공식문서에서 얘기하고 있으므로, 실제로 사용하려고 한다면 이 부분에 대해 고려해야 한다.

camera.filter = MultiFilter(Filters.BLACK_AND_WHITE.newInstance(), Filters.CONTRAST.newInstance())
var multiFilter = MultiFilter(Filters.BLACK_AND_WHITE.newInstance(), Filters.CONTRAST.newInstance())
multiFilter.addFilter(Filters.AUTO_FIX.newInstance())
camera.filter = multiFilter

어떤 필터들을 멀티 필터에 추가하느냐에 따라 출력결과가 천차만별인 것 같으니, 멀티필터 기능 사용을 염두할 생각이 있다면 실험을 많이 해야할 것으로 보인다. 그리고 위의 코드로 필터를 적용했을때, 필터들이 깜빡거리면서 한두개가 적용이 되었다가 안되었다가 함.

아래는 위의 코드로 실행한 멀티필터의 출력 결과물

 

 

5. Filter 를 간단하게 커스텀하기 위해서는 Filter 를 구현한 클래스를 만들어 사용하도록 권장하고 있다

For very simple filters that have a static fragment shader, you can create a working filter implementation by simply creating an instance of SimpleFilter

var filter: Filter = SimpleFilter(fragmentShader)

 

그런데, 이 simpleFilter 파라미터 안에 전달해주어야 하는 "fragmentShader" 변수 타입이 String 으로 되어있다.

String 으로 이루어진 fragmentShader 의 값을 전달해야하는 것으로 보인다.

public final class SimpleFilter extends BaseFilter {

    private final String fragmentShader;

    /**
     * Creates a new filter with the given fragment shader.
     * @param fragmentShader a fragment shader
     */
    @SuppressWarnings("WeakerAccess")
    public SimpleFilter(@NonNull String fragmentShader) {
        this.fragmentShader = fragmentShader;
    }

    @NonNull
    @Override
    public String getFragmentShader() {
        return fragmentShader;
    }

    @Override
    protected BaseFilter onCopy() {
        return new SimpleFilter(fragmentShader);
    }
}

 

 

6. Filter 를 섬세하게 커스텀하기 위해서는 BaseFilter 클래스를 상속받는 필터 클래스를 만들어 사용하도록 권장하고 있다

We recommend:

  • Subclassing BaseFilter instead of implementing Filter, since that takes care of most of the work
  • If accepting parameters, implementing OneParameterFilter or TwoParameterFilter as well

Most of all, the best way of learning is by looking at the current filters implementations in the com.otaliastudios.cameraview.filters package.

추가로 봐야할 것) BaseFilter 를 상속받는 Filter 클래스를 만들기 위해 어떤것 을 공부해야 하는지, 5번과 6번의 차이가 무엇이며 둘의 장단점이 무엇인지

아래는 기본적으로 제공되는 필터 중 하나인 BlackAndWhiteFilter 의 코드이다

package com.otaliastudios.cameraview.filters;

import androidx.annotation.NonNull;

import com.otaliastudios.cameraview.filter.BaseFilter;

/**
 * Converts the frames into black and white colors.
 */
public class BlackAndWhiteFilter extends BaseFilter {

    private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
            + "precision mediump float;\n"
            + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n"
            + "uniform samplerExternalOES sTexture;\n" + "void main() {\n"
            + "  vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n"
            + "  float colorR = (color.r + color.g + color.b) / 3.0;\n"
            + "  float colorG = (color.r + color.g + color.b) / 3.0;\n"
            + "  float colorB = (color.r + color.g + color.b) / 3.0;\n"
            + "  gl_FragColor = vec4(colorR, colorG, colorB, color.a);\n"
            + "}\n";

    public BlackAndWhiteFilter() { }

    @NonNull
    @Override
    public String getFragmentShader() {
        return FRAGMENT_SHADER;
    }
}

 

다음은 기본적으로 제공되는 필터 중 하나인 LomoishFilter 가 어떤 구조로 되어있는지에 대한 코드이다.

BlackAndWhiteFilter 보다 상대적으로 복잡하다.

package com.otaliastudios.cameraview.filters;

import android.opengl.GLES20;

import androidx.annotation.NonNull;

import com.otaliastudios.cameraview.filter.BaseFilter;
import com.otaliastudios.cameraview.internal.GlUtils;

import java.util.Random;

/**
 * Applies a lomo-camera style effect to the input frames.
 */
public class LomoishFilter extends BaseFilter {

    private final static Random RANDOM = new Random();
    private final static String FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
            + "precision mediump float;\n"
            + "uniform samplerExternalOES sTexture;\n"
            + "uniform float stepsizeX;\n"
            + "uniform float stepsizeY;\n"
            + "uniform vec2 scale;\n"
            + "uniform float inv_max_dist;\n"
            + "vec2 seed;\n"
            + "float stepsize;\n"
            + "varying vec2 "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+";\n"
            + "float rand(vec2 loc) {\n"
            + "  float theta1 = dot(loc, vec2(0.9898, 0.233));\n"
            + "  float theta2 = dot(loc, vec2(12.0, 78.0));\n"
            + "  float value = cos(theta1) * sin(theta2) + sin(theta1) * cos(theta2);\n"
            // keep value of part1 in range: (2^-14 to 2^14).
            + "  float temp = mod(197.0 * value, 1.0) + value;\n"
            + "  float part1 = mod(220.0 * temp, 1.0) + temp;\n"
            + "  float part2 = value * 0.5453;\n"
            + "  float part3 = cos(theta1 + theta2) * 0.43758;\n"
            + "  return fract(part1 + part2 + part3);\n"
            + "}\n"
            + "void main() {\n"
            + "  seed[0] = " + RANDOM.nextFloat() + ";\n"
            + "  seed[1] = " + RANDOM.nextFloat() + ";\n"
            + "  stepsize = " + 1.0f / 255.0f + ";\n"
            // sharpen
            + "  vec3 nbr_color = vec3(0.0, 0.0, 0.0);\n"
            + "  vec2 coord;\n"
            + "  vec4 color = texture2D(sTexture, "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+");\n"
            + "  coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - 0.5 * stepsizeX;\n"
            + "  coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - stepsizeY;\n"
            + "  nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
            + "  coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x - stepsizeX;\n"
            + "  coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n"
            + "  nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
            + "  coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n"
            + "  coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y - 0.5 * stepsizeY;\n"
            + "  nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
            + "  coord.x = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".x + stepsizeX;\n"
            + "  coord.y = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+".y + 0.5 * stepsizeY;\n"
            + "  nbr_color += texture2D(sTexture, coord).rgb - color.rgb;\n"
            + "  vec3 s_color = vec3(color.rgb + 0.3 * nbr_color);\n"
            // cross process
            + "  vec3 c_color = vec3(0.0, 0.0, 0.0);\n"
            + "  float value;\n"
            + "  if (s_color.r < 0.5) {\n"
            + "    value = s_color.r;\n"
            + "  } else {\n"
            + "    value = 1.0 - s_color.r;\n"
            + "  }\n"
            + "  float red = 4.0 * value * value * value;\n"
            + "  if (s_color.r < 0.5) {\n"
            + "    c_color.r = red;\n"
            + "  } else {\n"
            + "    c_color.r = 1.0 - red;\n"
            + "  }\n"
            + "  if (s_color.g < 0.5) {\n"
            + "    value = s_color.g;\n"
            + "  } else {\n"
            + "    value = 1.0 - s_color.g;\n"
            + "  }\n"
            + "  float green = 2.0 * value * value;\n"
            + "  if (s_color.g < 0.5) {\n"
            + "    c_color.g = green;\n"
            + "  } else {\n"
            + "    c_color.g = 1.0 - green;\n"
            + "  }\n"
            + "  c_color.b = s_color.b * 0.5 + 0.25;\n"
            // blackwhite
            + "  float dither = rand("+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" + seed);\n"
            + "  vec3 xform = clamp((c_color.rgb - 0.15) * 1.53846, 0.0, 1.0);\n"
            + "  vec3 temp = clamp((color.rgb + stepsize - 0.15) * 1.53846, 0.0, 1.0);\n"
            + "  vec3 bw_color = clamp(xform + (temp - xform) * (dither - 0.5), 0.0, 1.0);\n"
            // vignette
            + "  coord = "+DEFAULT_FRAGMENT_TEXTURE_COORDINATE_NAME+" - vec2(0.5, 0.5);\n"
            + "  float dist = length(coord * scale);\n"
            + "  float lumen = 0.85 / (1.0 + exp((dist * inv_max_dist - 0.73) * 20.0)) + 0.15;\n"
            + "  gl_FragColor = vec4(bw_color * lumen, color.a);\n"
            + "}\n";

    private int width = 1;
    private int height = 1;

    private int scaleLocation = -1;
    private int maxDistLocation = -1;
    private int stepSizeXLocation = -1;
    private int stepSizeYLocation = -1;

    public LomoishFilter() { }

    @Override
    public void setSize(int width, int height) {
        super.setSize(width, height);
        this.width = width;
        this.height = height;
    }

    @NonNull
    @Override
    public String getFragmentShader() {
        return FRAGMENT_SHADER;
    }

    @Override
    public void onCreate(int programHandle) {
        super.onCreate(programHandle);
        scaleLocation = GLES20.glGetUniformLocation(programHandle, "scale");
        GlUtils.checkLocation(scaleLocation, "scale");
        maxDistLocation = GLES20.glGetUniformLocation(programHandle, "inv_max_dist");
        GlUtils.checkLocation(maxDistLocation, "inv_max_dist");
        stepSizeXLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeX");
        GlUtils.checkLocation(stepSizeXLocation, "stepsizeX");
        stepSizeYLocation = GLES20.glGetUniformLocation(programHandle, "stepsizeY");
        GlUtils.checkLocation(stepSizeYLocation, "stepsizeY");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        scaleLocation = -1;
        maxDistLocation = -1;
        stepSizeXLocation = -1;
        stepSizeYLocation = -1;
    }

    @Override
    protected void onPreDraw(long timestampUs, float[] transformMatrix) {
        super.onPreDraw(timestampUs, transformMatrix);
        float[] scale = new float[2];
        if (width > height) {
            scale[0] = 1f;
            scale[1] = ((float) height) / width;
        } else {
            scale[0] = ((float) width) / height;
            scale[1] = 1f;
        }
        float maxDist = ((float) Math.sqrt(scale[0] * scale[0] + scale[1] * scale[1])) * 0.5f;
        GLES20.glUniform2fv(scaleLocation, 1, scale, 0);
        GlUtils.checkError("glUniform2fv");
        GLES20.glUniform1f(maxDistLocation, 1.0F / maxDist);
        GlUtils.checkError("glUniform1f");
        GLES20.glUniform1f(stepSizeXLocation, 1.0F / width);
        GlUtils.checkError("glUniform1f");
        GLES20.glUniform1f(stepSizeYLocation, 1.0F / height);
        GlUtils.checkError("glUniform1f");
    }
}
Comments