Bitmap setPixels will lose alpha channel if background is black

I am trying to create a custom view in Android using the method canvas.drawBitmap()

. However, I found that the alpha channel will be lost if I do it in native JNI code and the background is black. To the summary, case:

  • Call java bitmap.setPixels()

    and set the color of the bitmaps to the NDK, when the background is white , both bitmaps display correctly
  • Call java bitmap.setPixels()

    and set the color of the bitmap pixels to the NDK, when the background is black , only the bitmap drawn by the java API is displayed correctly, the one that was output with the NDK lost alpha channel

The question is, why is the result on a white background okay but not on a black background? Am I missing something or have I done it wrong?

Layout XML file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/black"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview1"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview2"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:orientation="horizontal"
        android:padding="16dp" >

        <com.example.android.TestView
            android:id="@+id/testview3"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />

        <com.example.android.TestView
            android:id="@+id/testview4"
            android:layout_width="320px"
            android:layout_height="320px"
            android:layout_margin="16dp" />
    </LinearLayout>

</LinearLayout>

      

MainActivity.java:

package com.example.android;
import com.example.android.R;

import android.app.Activity;
import android.os.Bundle;

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        TestView tv2 = (TestView) findViewById(R.id.testview2);
        TestView tv4 = (TestView) findViewById(R.id.testview4);
        tv2.setDrawFromNative();
        tv4.setDrawFromNative();
    }
}

      

TestView.java:

package com.example.android;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.View;

public class TestView extends View {
    private Bitmap mBitmap;
    private boolean mDrawFromNative;
    private static final int WIDTH = 320;
    private static final int HEIGHT = 320;

    static {
        System.loadLibrary("bitmaptest");
    }
    private native void nativeDrawBitmap(Object bitmap);

    private static void javaDrawBitmap(Bitmap bitmap) {
        int pixels[] = new int[WIDTH * HEIGHT];
        for (int i = 0; i < pixels.length; i++) {
            pixels[i] = 0x88FF0000;
        }
        bitmap.setPixels(pixels, 0, WIDTH, 0, 0, WIDTH, HEIGHT);
    }

    public TestView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mBitmap = Bitmap.createBitmap(WIDTH, HEIGHT, Bitmap.Config.ARGB_8888);
    }

    public void setDrawFromNative() {
        mDrawFromNative = true;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if(mDrawFromNative) {
            nativeDrawBitmap(mBitmap);
        } else {
            javaDrawBitmap(mBitmap);
        }
        canvas.drawBitmap(mBitmap, 0, 0, null);
    }
}

      

TestNative.cpp:

#include <jni.h>
#include <android/bitmap.h>
#include <android/Log.h>

#define LOGD(...)  __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)
#define LOGW(...)  __android_log_print(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define LOG_TAG "BMPTEST"

extern "C" {
void Java_com_example_android_TestView_nativeDrawBitmap(JNIEnv* env, jobject thiz, jobject bitmap) {

    AndroidBitmapInfo info;
    void* dst_pixels;
    int   ret;

    if((ret = AndroidBitmap_getInfo(env, bitmap, &info)) < 0) {
        LOGE("AndroidBitmap_getInfo() failed ! error=%d", ret);
        return;
    }
    if ((ret = AndroidBitmap_lockPixels(env, bitmap, &dst_pixels)) < 0) {
        LOGE("AndroidBitmap_lockPixels() failed ! error=%d", ret);
        return;
    }

    unsigned int *dst = (unsigned int *)dst_pixels;
    for(int i=0; i< info.width * info.height; i++) {
            *(dst+i) = (0x88<<24 | 0xff | 0x00<<8 | 0x00<<16); //(ARGB->ABGR)
    }
    AndroidBitmap_unlockPixels(env, bitmap);
}
}

      

Android.mk for native code:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libbitmaptest
LOCAL_SRC_FILES := \
    TestNative.cpp
LOCAL_LDLIBS += -llog -ljnigraphics
include $(BUILD_SHARED_LIBRARY)

      

ScreenShot of the result: ScreenShot of the result

+3


source to share


1 answer


Android stores bitmaps with pre-multiplied alpha. When you call setPixels()

from Java, the RGB color values ​​are automatically multiplied by the alpha values ​​and stored in the bitmap. However, when you call Android_lockPixels()

from your own code and then write directly to memory, you need to pre-multiply yourself, otherwise it will be an error. If you change your code to:

 int premultipliedR = (0xff * 0x88) >> 8;
 for(int i=0; i< info.width * info.height; i++) {
        *(dst+i) = (0x88<<24 | premultipliedR | 0x00<<8 | 0x00<<16);

      

then both Bitmap

should do the same.

So why does it look like bitmaps lose their alpha channel when the background is black, but not for a white background? Well, it turns out that just a match is based on the numbers you chose.

Basic Alpha Blending Formula:

 dest.r = ((dest.r * (256 - source.a)) + (source.r * source.a)) >> 8;
 dest.g = ((dest.g * (256 - source.a)) + (source.g * source.a)) >> 8;
 dest.b = ((dest.b * (256 - source.a)) + (source.b * source.a)) >> 8;

      



where dest

is the background pixel and the source is the pixel in your bitmap. Pre-multiplying the alpha changes this to:

 dest.r = ((dest.r * (256 - source.a)) >> 8) + source.premultiplied_r;
 dest.g = ((dest.g * (256 - source.a)) >> 8) + source.premultiplied_g;
 dest.b = ((dest.b * (256 - source.a)) >> 8) + source.premultiplied_b;

      

which saves a bunch of multiplications. All results are clamped down to 255. I am not claiming that this is an exact formula, but it is something pretty close.

Inserting numbers into, for your Java bitmap, the pre-multiplied r, g, b will be 0x87 (or 0x88 depending on how they round, etc.), 0x00, and 0x00. For your native bitmap, they will be 0xff, 0x00, and 0x00 because you haven't shared. Alpha blending with black background coincides with addition of zero, since the values dest.

r

, g

, b

are equal to zero. Thus, the results look different.

In the case of a white background dest.g

and dest.b

in both cases, they will be the same, since the values ​​are pre-multiplied g

and b

equal to zero in both Java and native bitmaps. In the case, the dest.r

result should be 255. In the case of the native bitmap, the value overflows due to the wrong value for pre-multiplication r

, but it clamps to 255, so the results end up looking the same.

In short, the pre-assembled value is r

too high for your native bitmap, so you get too high a value r

in cases where the result should be <255. In cases where the result should be 255, it doesn't matter if it's too high. because it clamps to 255 anyway.

+5


source







All Articles