2012년 3월 21일 수요일

렌더스크립트로 레벨 바꾸기

Levels in Renderscript | 2012년 1월 10일 오후 1시 43분에 팀 브레이(Tim Bray)가 작성.


[이 글은 그래픽, 성능 튜닝, 소프트웨어 아키텍쳐 전문인 안드로이드 프레임워크 엔지니어 R. 제이슨 샘스(R. Jason Sams)가 작성하였습니다. - 팀 브레이]

애플리케이션에 계산 가속(compute acceleration)을 추가하는 것을 단순화 하기 위해 ICS의 렌더스크립트(RS, Renderscript) 는 몇가지 특성을 개선했습니다. 중요한 처리를 해야할 대용량 데이터 버퍼가 있다면 렌더스크립트는 흥미롭습니다. 예를 들어 비트맵에 레벨(level)과 포화도(saturation)를 적용하겠습니다. (주: 렌더스크립트는 계산 용도와 그래픽 용도로 분리되며 계산 용도의 렌더스크립트 코드는 rsForEach를 이용하여 분산 수행할 수 있습니다. 이 글에서 계산 가속은 전자를 의미합니다.)

포화도의 경우는 매 픽셀에 대해 컬러 메트릭스를 곱해 구현합니다.

레벨은 보통 여러 연산으로 구현됩니다. (주: 컴퓨터 이미지는 디스플레이에 선형적인 밝기로 표현되지 못하기 때문에 밝기가 왜곡되어 있습니다. 이미지 처리에서 이를 보정하는 것을 감마 보정이라 합니다.)
  1. 입력 레벨이 조정됩니다.
  2. 감마 보정합니다.
  3. 출력 레벨이 조정됩니다.
  4. 유효 범위로 자릅니다.
아래는 단순화된 구현입니다.

for (int i=0; i < mInPixels.length; i++) {
    float r = (float)(mInPixels[i] & 0xff);
    float g = (float)((mInPixels[i] >> 8) & 0xff);
    float b = (float)((mInPixels[i] >> 16) & 0xff);

    float tr = r * m[0] + g * m[3] + b * m[6];
    float tg = r * m[1] + g * m[4] + b * m[7];
    float tb = r * m[2] + g * m[5] + b * m[8];
    r = tr;
    g = tg;
    b = tb;

    if (r < 0.f) r = 0.f;
    if (r > 255.f) r = 255.f;
    if (g < 0.f) g = 0.f;
    if (g > 255.f) g = 255.f;
    if (b < 0.f) b = 0.f;
    if (b > 255.f) b = 255.f;

    r = (r - mInBlack) * mOverInWMinInB;
    g = (g - mInBlack) * mOverInWMinInB;
    b = (b - mInBlack) * mOverInWMinInB;

    if (mGamma != 1.0f) {
        r = (float)java.lang.Math.pow(r, mGamma);
        g = (float)java.lang.Math.pow(g, mGamma);
        b = (float)java.lang.Math.pow(b, mGamma);
    }

    r = (r * mOutWMinOutB) + mOutBlack;
    g = (g * mOutWMinOutB) + mOutBlack;
    b = (b * mOutWMinOutB) + mOutBlack;

    if (r < 0.f) r = 0.f;
    if (r > 255.f) r = 255.f;
    if (g < 0.f) g = 0.f;
    if (g > 255.f) g = 255.f;
    if (b < 0.f) b = 0.f;
    if (b > 255.f) b = 255.f;

    mOutPixels[i] = ((int)r) + (((int)g) << 8) + (((int)b) << 16)
                    + (mInPixels[i] & 0xff000000);
}
이 코드는 비트맵이 적재되고 처리를 위해 정수 배열로 옮겨졌다고 가정합니다. 비트맵이 이미 읽어졌다고 가정하면 단순합니다.

        mInPixels = new int[mBitmapIn.getHeight() * mBitmapIn.getWidth()];
        mOutPixels = new int[mBitmapOut.getHeight() * mBitmapOut.getWidth()];
        mBitmapIn.getPixels(mInPixels, 0, mBitmapIn.getWidth(), 0, 0,
                            mBitmapIn.getWidth(), mBitmapIn.getHeight());
반복문에 의해 데이터가 한번 처리한 후 그리기 위해 비트맵으로 넣는 것은 간단합니다.

        mBitmapOut.setPixels(mOutPixels, 0, mBitmapOut.getWidth(), 0, 0,
                             mBitmapOut.getWidth(), mBitmapOut.getHeight());
필터 핵심부를 위한 상수, 제어의 조작, 이미지 표시를 포함한 애플리케이션의 전체 코드는 232 줄입니다. 800x423 이미지를 실제 기기에서 처리하는데 대략 140~180 밀리 초가 소요되었습니다.

꽤 빠르죠?

이 이미지 처리의 핵심을 렌더스크립트로 옮기는 것(android-renderscript-samples에 코드가 있습니다.)은 정말 쉽습니다. 위의 픽셀 처리 핵심을 렌더스크립트로 재구현하였습니다.

void root(const uchar4 *in, uchar4 *out, uint32_t x, uint32_t y) {
    float3 pixel = convert_float4(in[0]).rgb;
    pixel = rsMatrixMultiply(&colorMat, pixel);
    pixel = clamp(pixel, 0.f, 255.f);
    pixel = (pixel - inBlack) * overInWMinInB;
    if (gamma != 1.0f)
        pixel = pow(pixel, (float3)gamma);
    pixel = pixel * outWMinOutB + outBlack;
    pixel = clamp(pixel, 0.f, 255.f);
    out->xyz = convert_uchar3(pixel);
}

부동소수점 벡터에 대한 내장 지원, 매트릭스 연산, 포맷 변환 때문에 몇 줄의 코드로 됩니다. 게다가 반복문도 없습니다.

설정 코드는 스크립트를 불러야 하기 때문에 약간 더 복잡합니다.

        mRS = RenderScript.create(this);
        mInPixelsAllocation = Allocation.createFromBitmap(mRS, mBitmapIn,
                                                          Allocation.MipmapControl.MIPMAP_NONE,
                                                          Allocation.USAGE_SCRIPT);
        mOutPixelsAllocation = Allocation.createFromBitmap(mRS, mBitmapOut,
                                                           Allocation.MipmapControl.MIPMAP_NONE,
                                                           Allocation.USAGE_SCRIPT);
        mScript = new ScriptC_levels(mRS, getResources(), R.raw.levels);

이 코드는 렌드 스크립트 문맥을 만듭니다. 다음으로 이 문맥을 이용하여 비트맵 데이터의 렌더스크립트 복제본을 유지하기 위해 두 개의 메모리 할당을 만듭니다. 마지막으로 데이터를 처리하기 위해 스크립트를 읽습니다.

또 소스에서 값이 바뀌었을 때 스크립트로 계산 상수를 복사하기 위한 작은 코드 블록이 있습니다. 스크립트로 부터 전역적으로 반영하기 때문에 이 과정은 쉽습니다.

        mScript.set_inBlack(mInBlack);
        mScript.set_outBlack(mOutBlack);
        mScript.set_inWMinInB(mInWMinInB);
        mScript.set_outWMinOutB(mOutWMinOutB);
        mScript.set_overInWMinInB(mOverInWMinInB);

이전에 모든 픽셀을 처리하기 위해 루프가 필요없다는 점을 이야기했습니다. 비트맵 데이터를 처리하고 그 결과를 다시 복제하는 렌더스크립트 코드는 다음과 같습니다.

        mScript.forEach_root(mInPixelsAllocation, mOutPixelsAllocation);
        mOutPixelsAllocation.copyTo(mBitmapOut);

첫 줄은 스크립트를 이용하여 입력 할당을 처리하고 결과를 출력 할당에 넣는 것입니다. 이는 위에 나온 스크립트를 네이티브로 컴파일하여  할당의 모든 픽셀에 한번씩 호출합니다. 달빅 구현과는 다르게 기본으로 일을 처리하기 위한 여분의 스레드를 시작합니다. 네이티브 코드와 결합되어 큰 성능을 만듭니다. 감마 기능이 비용이 많이 들어서 그것을 넣은 결과와 넣지 않은 결과를 나누어 열거합니다.

800x423 image

DeviceDalvikRSGain
Xoom174ms39ms4.5x
Galaxy Nexus139ms30ms4.6x
Tegra 30 device136ms19ms7.2x

800x423 image with gamma correction

DeviceDalvikRSGain
Xoom994ms259ms3.8x
Galaxy Nexus787ms213ms3.7x
Tegra 30 device783ms104ms7.5x

위에 보인 간단한 코드를 투자해 큰 결과를 얻은 것을 볼 수 있습니다.

댓글 없음:

댓글 쓰기