Tistory View

Android Develop/image

안드로이드 이미지의 색변경 ColorMatrix

God Dangchy What should I do? 2019. 12. 16. 00:46

동작방식

색변경을 위해 안드로이드에서 제공하는 행렬(Matrix)는 5x4로 다음과 같이 생겼다.

[ a b c d e
  f g h i j
  k l m n o
  p q r s t ]

이 행렬을 수식으로 나타내면 다음과 같이 된다.

R' = a*R + b*G + c*B + d*A + e
G' = f*R + g*G + h*B + i*A + j
B' = k*R + l*G + m*B + n*A + o
A' = p*R + q*G + r*B + s*A + t

행렬이 나왔다고 해서 겁먹을 필요는 없다. 이 수식을 천천히 살펴보자.

맨 오른쪽의 (e, j, o, t)값은 앞부분의 곱하기 연산과 관련 없이 그냥 더하고/빼는 값일 뿐이다. RGBA값을 강제로 끌어 올리거나 내리는 용도로 사용되는 값이다.

R' 값은 원본의 R, G, B, A값에 각각 일정한 값을 각각 곱하여 만들어지도록 되어있다. 즉, R'를 만들려면 R에 얼마를 적용하고 G에 얼마를 적용하고... (B,A도 마찬가지) 식으로 되어있다.

 

간단히 연습을 해보도록 하자.

이미지를 Grayscale로 만들어 버리고 싶다면, 다음과 같이 수식을 만들 수 있을 것이다. 물론 실제 Grayscale을 만들 때는 다른 값을 사용하지만 현재는 그냥 연습 중이다.

R' = (R + G + B)/3   => R/3 + G/3 + B/3
G' = (R + G + B)/3   => R/3 + G/3 + B/3
B' = (R + G + B)/3   => R/3 + G/3 + B/3
A' = A               => R*0 + G*0 + B*0 + A*1

이 수식을 행렬의 수식으로 바꾸면 다음과 같이 되고

R' = 0.333*R + 0.333*G + 0.333*B + 0*A + 0;
G' = 0.333*R + 0.333*G + 0.333*B + 0*A + 0;
B' = 0.333*R + 0.333*G + 0.333*B + 0*A + 0;
A' =     0*R +     0*G +     0*B + 1*A + 0;

이제 이 값을 행렬로 표현하면

[  0.333  0.333  0.333  0  0
   0.333  0.333  0.333  0  0
   0.333  0.333  0.333  0  0
   0      0      0      1  0 ]

이런 행렬이 만들어진다. 이 행렬을 적용시키면 GrayScale로 변환이 되게 된다.

이런 식으로 필요할 행렬을 만들거나 가져다 쓰면 된다.

 

 

혹시나해서, 실제 사용해야 되는 GrayScale행렬은 다음과 같다.

[  0.2989  0.5870  0.1140  0  0
   0.2989  0.5870  0.1140  0  0
   0.2989  0.5870  0.1140  0  0
   0.0000  0.0000  0.0000  1  0 ]

GrayScale

 

Clamp color range

만약 행렬연산의 결과가 0보다 작거나 255보다 크면 값이 잘려 0~255로 맞춰지게 된다.

0보다 작은 값은 0으로, 255보다 큰값은 255로 변경되어 그려지게 된다.

(알아서 되는 거니 내가 맞출 필요도 없고 맞출 방법도 없다.)

 

 

사용법 2가지

필자가 뒤져본 바로는 이 ColorMatrix는 ImageView와 Paint에 적용할 수 있다.

ImageView

ImageView의 경우 onDraw를 오버라이드를 해서 쓰기 힘든 상황이니, setColorFilter함수로 바로 적용할 수 있게 되어있다.

float[] GRAYSCALE = {
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.0000F, 0.0000F, 0.0000F, 1, 0
    };

[이미지뷰 객체].setColorFilter( new ColorMatrixColorFilter( GRAYSCALE ) );

이 예제는 ColorMatrixColorFilter가 바로 float array를 받을 수 있어서 가능한 것이라, 그럴 수 없는 경우는 다음과 같이 한다.

float[] GRAYSCALE = {
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.0000F, 0.0000F, 0.0000F, 1, 0
    };

ColorMatrix colorMatrix = new ColorMatrix( GRAYSCALE );
[이미지뷰 객체].setColorFilter( new ColorMatrixColorFilter( colorMatrix ) );

 

Canvas에 그리기(Bitmap에 적용)

Canvas에 그릴 경우 Paint에 적용 후 Bitmap을 그리면 된다.

float[] GRAYSCALE = {
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.0000F, 0.0000F, 0.0000F, 1, 0
    };
    
ColorMatrix colorMatrix = new ColorMatrix( ColorMatrices.GRAYSCALE );
Paint       paint       = new Paint( Paint.ANTI_ALIAS_FLAG );
        
paint.setColorFilter( new ColorMatrixColorFilter(colorMatrix) );
        
[캔버스 객체].drawBitmap( bmp, 0.0F, 0.0F,  paint );
// drawBitmap은 여러함수가 Overload되어 있으니 필요한 것을 쓰면 된다.

drawBitmap함수에 전달한 bmp에 GrayScale이 적용되어 그려지게 될 것이다. 예제는 Bitmap을 이용했지만 Bitmap뿐아니라 모든 그리기작업에 적용이 될 것이다.

 

필터를 합치기

뭐 이런 단순한 것 때문에 머리아픈 행렬을 쓰나?.. 라는 생각이 들 수도 있지만, 행렬을 쓸 경우에 아주 좋은 장점이 있다. 이 것 수학자들한테 고마워해야 할 문제다..(물론 행렬이 만들어지지 않았다면 공부할게 줄어서 좋았겠지만...)

 

두가지 이상의 컬러필터를 사용을 할 경우 필터수 만큼의 연산이 늘어나게 된다. 특히 이미지를 다루는 작업이기때문에 필터가 10개가 되면 안그래도 시간이 많이 걸리는 이미지작업에 10배의 시간이 걸리게 된다. 행렬을 사용하면 행렬로만 연산을 한 결과로 한번만 적용하면 같은 결과를 얻을 수 있다는 장점이 바로 그것이다. 이 이유로 인해 굳이 행렬을 쓰는 것이다.

 

 

예제로 Invert필터와 GrayScale필터를 적용해보자.

final float[] GRAYSCALE = {
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.0000F, 0.0000F, 0.0000F, 1, 0
    };
    
final float[] INVERT = {
            -1,  0,  0,  0, 255,
            0,  -1,  0,  0, 255,
            0,   0, -1,  0, 255,
            0,   0,  0,  1,   0
    };
    

// INVERT후 GrayScale
ColorMatrix result = new ColorMatrix( INVERT );
result.postConcat( new ColorMatrix( GRAYSCALE ) );
// 필요하다면 다른 필터를 계속 적용한다.
// result.postConcat( new ColorMatrix( R과 G바꾸기 ) );
// result.postConcat( new ColorMatrix( B를 없애기 ) );

// Filter적용
[paint].setColorFilter( new ColorMatrixColorFilter(result) );
or
[imageview].setColorFilter( new ColorMatrixColorFilter(result) );

이런 식으로 postConcat()함수를 이용하여 여러게의 필터를 합칠 수가 있다.

 

INVERT 후 GrayScale

 

필터의 적용순서

필터를 적용할 때 순서에 따라 결과가 달라진다.

이미지에 (A필터를 적용 -> B필터적용-> C필터 적용)할 경우 행렬연산을 다음과 같이 된다.

                       <--------------------------------------------- 적용순서
    결과        =      C필터        x      B필터          x        A필터
[ a b c d e         [ a b c d e         [ a b c d e             [ a b c d e  
  f g h i j           f g h i j           f g h i j               f g h i j  
  k l m n o     =     k l m n o     x     k l m n o       x       k l m n o  
  p q r s t ]         p q r s t ]         p q r s t ]             p q r s t ]

위에서 사용한 postConcat함수가 이 역할을 해주게 되는 데, postConcat함수에 넘긴 파라미터를 앞(왼쪽:lhs)에 놓고 원래 가지고 있던 값을 뒤(오른쪽:rhs)에 놓고 연산을 한다.  이런 이유로 postConcat함수로 연산을 해서 원하는 결과를 얻을 수 있다. 거꾸로하면 다른 결과를 얻는다.

(preConcat함수는 lhs와 rhs가 postConcat과 반대로 놓고 연산을 한다.)

preConcat과 postConcat이 햇갈린다면, setConcat함수를 쓰는 편이 좋다.
setConcat은 lhs, rls순서로 파라미터를 받는다.
(적용은 rls후 lhs가 된다)

setConcat(ColorMatrix lhs, ColorMatrix rhs)

result = lhs * rhs

왼쪽 : RG교환 -> Sepia / 오른쪽 : Sepia -> RG교환

Matrix은 교환법칙이 적용되지 않으므로 원하는 순서대로 처리하여야 한다.

 

필터를 합칠 때 주의점

필터를 합칠 경우 위에서 말한 연산순서도 주의사항 중에 하나지만, 더 신중히 처리해야할 문제가 있다.

다음의 필터 두가지를 적용한다고 가정해보자.

 

첫번째 필터는 색을 다 없애는 필터고 두번째 필터는 색을 강제로 끌어올리는 필터이다.

(물론 예제일 뿐 실제 이런 필터를 쓸 일이...)

    필터A                    필터B
[ 1 0 0 0 -255           [ 1 0 0 0 255  
  0 1 0 0 -255             0 1 0 0 255  
  0 0 1 0 -255             0 0 1 0 255  
  0 0 0 1    0 ]           0 0 0 1   0 ]

 

위 : 이미지에 직접 2번 적용 / 아래 : Matrix로 연산

 

 

이 두가지 필터를 이미지에 차례로 적용하면(Matrix연산말고) 첫번째 필터에서 모든 색이 0이 되고, 두번째 필터에 의해 모든 색이 255로 되어 결과로 흰색만을 그리게 될 것이다. 하지만 Matrix로 연산을 한 결과는 원본과 동일한 결과를 보여주게 된다.

 

첫번째 필터에서 위에서 설명한 Clamp로 인해, 이미지가 모두 검게되어 이미지의 모든 정보가 사라지게 된다. 하지만 Matrix연산은 그렇지 않다.

 

Matrix연산

  결과필터               필터A                    필터B
[ 1 0 0 0 0        [ 1 0 0 0 255            [ 1 0 0 0 -255  
  0 1 0 0 0          0 1 0 0 255              0 1 0 0 -255  
  0 0 1 0 0    =     0 0 1 0 255      x       0 0 1 0 -255  
  0 0 0 1 0 ]        0 0 0 1   0 ]            0 0 0 1    0 ]
  
Matrix연산의 결과가 Identity(단위행렬)하다. 원본이 그대로 보여진다.

(이게 행렬연산이 되는게 신기하다.. 내부에서는 마지막행[5]을 1로해서 5x5로 처리한다. )

 

바로 Clamp에 의해 발생할 수 있는 부분을 고려하여, 필터를 합칠건지, 필터를 따로 따로 적용할지 신중히 고려해야 한다. 이문제 말고도 다른 문제가 더 있으니, 상황에 따라 신중해야 한다.

예를 들어, 색을 죽이기 위해, RGB에 1/10 후 다시 곱하기 10하면... Matrix연산은 원본이 되어버린다.

 

 

YUV 스페이스용 함수

YUV Space를 다루려면 일단 YUV space로 변환 후, 작업하고, 작업이 완료되면, 다시 RGB로 변경해서 그리는 방식을 써야 한다.

 

다음은 YUV를 이용한 GrayScale이다.

// YUV용 Grayscale matrix
// Y값만 남기고 UV값은 지워버린다.
// R->Y    G->U    B->V로  생각하면 된다.(UV가 바뀌었을 가능성은 확인못했다.)
float[] grayscale4YUV = new float[] {
                1, 0, 0, 0, 0,
                0, 0, 0, 0, 0,
                0, 0, 0, 0, 0,
                0, 0, 0, 1, 0
        };
        
        

// grayscale 필터와 (yuv -> rgb) 필터를 미리 만들어 둔다.
ColorMatrix GrayScale4YUV = new ColorMatrix( grayscale4YUV );

ColorMatrix yuv2rgb = new ColorMatrix();
yuv2rgb.setYUV2RGB();


// 연산작업
ColorMatrix cm = new ColorMatrix();
cm.setRGB2YUV();  // YUV로 변환


// 여기서부터 YUV Color Space로 작업한다.
cm.postConcat( GrayScale4YUV );
// YUV연산이 끝


//화면에 그리기위해 다시 RGB로 변환한다.
cm.postConcat( yuv2rgb );

// 매트릭스 적용
[imageView or paint].setColorMatrix( cm );

 

YUV를 이용한 GrayScale

 

HSV 스페이스용 함수

결과를 먼저 말하면 간단히 HSV는 사용할 수 없다고 일단 생각해야 한다.

 

RGB Color Space를 이용하는 경우 기본적으로 Bitmap들이 RGB로 구성되어 있으니 문제가 없지만, HSV Space를 다루는 건 좀 까다로운 일이라, Android에서는 함수로 지원을 해주고 있다. ( 땡큐~ 구글 )

 

HSV를 지원하는 함수는 다음과 같다.

(ColorMatrix)
public void setSaturation(float sat)
public void setRotate(int axis, float degrees)


Value값을 바꾸려면 RGB용 함수인 setScale()에 같은 rgb값을 넘겨주면 된다.
public void setScale(float rScale, float gScale, float bScale, float aScale);

이 함수들을 이용하여 HSV를 작업하려고 했지만, setRotate함수 사용법이 너무 까다롭고, 이건 다시 수학문제를 풀어야 하는 상황이라 필자는 안쓰기고 했다. ( 땡큐~ 구글...... 취소.. 구글 꾸짐..)

 

이게 원래 Matrix만으로 RGB를 HSV로 만드는 것이 불가능한 것으로 생각된다. H를 만들어내는 과정이 단순히 ( H = f1*R + f2*G + f3*B + f4*A + Offset ) 형태로 만들 수가 없는 것으로 생각된다.

(혹시만 풀린다면 댓글로..)

 

그래서 setSaturation함수만 유용할 뿐, HSV는 사용이 불가능한 것이라 봐도 된다.

만약 굳이 HSV로 행렬연산을 하고 싶다면, 그림(Bitmap등) 차체를 HSV로 바꾸고, Matrix를 처리한 다음, 다시 RGB로 바꾸는 코드를 만들어야 할 것이다.

 

YUV와는 달리, 위의 함수를 쓴다고 해도 Color Space가 HSV로 바뀌거나 하는 것이 아니다.
그냥 RGB를 유지한 상태에서 변경이 이루어지게 된다.

 

주로 쓰는 필터

솔직히 아무리 이렇게 산수문제도 풀고해도 실제 필요한 것은 잘 만들어진 것을 가져다 쓰는 게 가장 빠르고 효율적이다. 어쩔수없이 만들어야 하는 경우도 있지만, 우리는 대부분 시간이 없다.(놀시간?)

public class ColorMatrices {

    public static final float[] IDENTITY = {
            1, 0, 0, 0, 0,
            0, 1, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] INVERT = {
            -1,  0,  0,  0, 255,
            0,  -1,  0,  0, 255,
            0,   0, -1,  0, 255,
            0,   0,  0,  1,   0
    };

    public static final float[] REDONLY = {
            1, 0, 0, 0, 0,
            0, 0, 0, 0, 0,
            0, 0, 0, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] GREENONLY = {
            0, 0, 0, 0, 0,
            0, 1, 0, 0, 0,
            0, 0, 0, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] BLUEONLY = {
            0, 0, 0, 0, 0,
            0, 0, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] SWAP_R_G = {
            0, 1, 0, 0, 0,
            1, 0, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] SWAP_R_B = {
            0, 0, 1, 0, 0,
            0, 1, 0, 0, 0,
            1, 0, 0, 0, 0,
            0, 0, 0, 1, 0
    };

    public static final float[] SWAP_G_B = {
            1, 0, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 1, 0, 0, 0,
            0, 0, 0, 1, 0
    };


    public static final float[] GRAYSCALE = {
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.2989f, 0.5870f, 0.1140f, 0, 0,
            0.0000F, 0.0000F, 0.0000F, 1, 0
    };

    public static final float[] SEPIA =  {
            0.393F, 0.769F, 0.189F, 0, 0,
            0.349F, 0.686F, 0.168F, 0, 0,
            0.272F, 0.534F, 0.131F, 0, 0,
            0.000F, 0.000F, 0.000F, 1, 0
    };



    // warm 5000K 1.0000 0.7992 0.6045
    public static final float[] WARM =  {
            1.000F, 0.000F, 0.000F, 0, 0,
            0.000F, 0.780F, 0.000F, 0, 0,
            0.000F, 0.000F, 0.605F, 0, 0,
            0.000F, 0.000F, 0.000F, 1, 0
    };
    // cool 8000K 0.7644 0.8139 1.0000
    public static final float[] COOL =  {
            0.765F, 0.000F, 0.000F, 0, 0,
            0.000F, 0.814F, 0.000F, 0, 0,
            0.000F, 0.000F, 1.000f, 0, 0,
            0.000F, 0.000F, 0.000F, 1, 0
    };
}

/*
// Usage
ColorMatrix result = new ColorMatrix( ColorMatrices.INVERT );

[paint].setColorFilter( new ColorMatrixColorFilter(result) );
or
[imageview].setColorFilter( new ColorMatrixColorFilter(result) );
*/

 

Identity
Invert
grayscale
red only
green only
blue only
swap red<->green
swap red<->blue
swap green<->blue
sepia
warm
cool

 

Replies
Reply Write