Bitmap setPixels akan kehilangan saluran alfa ketika latar belakangnya hitam

Saya mencoba menggambar tampilan khusus di Android, dengan metode canvas.drawBitmap(). Namun, saya menemukan saluran alfa akan hilang jika saya melakukan ini dalam kode JNI asli dan latar belakangnya berwarna hitam. Singkatnya, kasusnya adalah:

  1. Panggil Java bitmap.setPixels() dan atur warna piksel bitmap di NDK ketika latar belakang putih, kedua bitmap ditampilkan dengan benar
  2. Panggil java bitmap.setPixels() dan setel warna piksel bitmap di NDK ketika latar belakang hitam, hanya bitmap yang digambar oleh java API yang ditampilkan dengan benar, bitmap yang digambar dengan NDK kehilangan saluran alfa

Pertanyaannya kenapa pada background putih hasilnya OK tapi pada background hitam tidak OK? Apakah saya melewatkan sesuatu atau melakukannya dengan cara yang salah?

File XML tata letak:

<?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 untuk kode asli:

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 hasil: ScreenShot hasil


person Robin    schedule 07.01.2015    source sumber
comment
Bagaimana jika Anda mengganti urutan byte dalam kode asli Anda? (yaitu 0x0000ff11)   -  person Michael    schedule 07.01.2015
comment
Urutan byte dalam kode saya benar (merah dengan alfa 0x11) pada platform ARM. Penggunaan 0x0000ff11 akan menyebabkan bitmap menjadi persegi panjang hijau buram.   -  person Robin    schedule 07.01.2015
comment
Saya telah memperbarui pertanyaan dengan kode sumber terperinci dan cuplikan layar.   -  person Robin    schedule 07.01.2015


Jawaban (1)


Android menyimpan bitmap dengan alfa yang telah dikalikan sebelumnya. Saat Anda memanggil setPixels() dari Java, nilai warna RGB secara otomatis dikalikan dengan nilai alfa dan disimpan dalam bitmap. Namun, ketika Anda memanggil Android_lockPixels() dari kode asli dan kemudian menulis langsung ke memori, Anda perlu melakukan pra-perkalian sendiri atau hasilnya akan salah. Jika Anda mengubah kode Anda menjadi:

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

maka kedua Bitmaps akan dirender sama.

Jadi mengapa tampak seolah-olah bitmap kehilangan saluran alfa ketika latar belakang berwarna hitam tetapi tidak untuk latar belakang putih? Nah ternyata itu hanya kebetulan saja berdasarkan angka yang anda pilih.

Rumus dasar pencampuran alfa adalah:

 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;

di mana dest adalah piksel latar belakang dan sumber adalah piksel dalam bitmap Anda. Mengalikan alfa terlebih dahulu akan mengubahnya menjadi:

 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;

yang menghemat banyak kelipatan. Hasilnya semua dijepit ke 255. Saya tidak mengklaim bahwa ini adalah rumus persis yang digunakan, tetapi rumusnya hampir mendekati rumus tersebut.

Memasukkan angka-angka, untuk bitmap Java Anda, r, g, b yang telah dikalikan sebelumnya akan menjadi 0x87 (atau 0x88 tergantung bagaimana mereka melakukan pembulatan dll), 0x00 dan 0x00. Untuk bitmap asli Anda, ukurannya akan menjadi 0xff, 0x00, dan 0x00 karena Anda tidak melakukan pra-kalikan. Memadukan alfa dengan latar belakang hitam sama saja dengan menambahkan nol, karena nilai dest. r, g, b semuanya nol. Jadi hasilnya terlihat berbeda.

Dalam kasus latar belakang putih, dest.g dan dest.b akan berakhir sama di kedua kasus, karena nilai g dan b yang telah dikalikan sebelumnya adalah nol di bitmap Java dan bitmap asli. Dalam kasus dest.r, hasilnya seharusnya 255. Dalam kasus bitmap asli, nilainya meluap karena nilai yang salah untuk r yang telah dikalikan sebelumnya, namun nilainya dijepit menjadi 255, sehingga hasilnya terlihat seperti sama.

Singkatnya, nilai r yang telah dikalikan sebelumnya terlalu tinggi untuk bitmap asli Anda, sehingga Anda akan mendapatkan nilai r yang terlalu tinggi jika hasilnya seharusnya ‹ 255. Jika hasilnya seharusnya 255, maka nilai tersebut akan menjadi ‹ 255. tidak masalah jika terlalu tinggi karena tetap dijepit pada 255.

person samgak    schedule 07.01.2015
comment
Saat Anda mengatakan premultipliedR, yang Anda maksud adalah premultipliedAlpha, bukan? Saya mengubah kode saya seperti yang Anda sarankan, hasilnya sama. Saya masih mendapatkan persegi panjang merah buram dengan latar belakang hitam berbeda dari persegi panjang semi-transparan yang digambar oleh Java api. Atau saya salah paham dengan jawaban Anda, haruskah saya mengubah *(dst+i)=??? Jika Anda dapat menggunakan kode saya dan memodifikasinya untuk mendapatkan hasil yang benar, itu akan luar biasa. - person Robin; 07.01.2015
comment
Satu pertanyaan lagi tentang topik ini, jika semua R/G/B disimpan sebagai praperkalian, apakah alfa masih perlu dipertahankan dalam piksel? Maksud saya, apakah *(dst+i) = (WHATEVER_VALUE‹‹24) | praperkalianR | 0x00‹‹8 | 0x00‹‹16); akan mendapatkan hasil yang sama? - person Robin; 07.01.2015
comment
Ya, Anda masih memerlukannya karena RGB tujuan dikalikan dengan 256 dikurangi nilai alpha dalam persamaan campuran. - person samgak; 07.01.2015
comment
Terima kasih banyak telah menyelamatkan saya dari masalah ini, saya terjebak di sini selama 2 hari. Pertanyaan terakhir: apakah ada API resmi yang bisa memberi tahu kerangka kerja bahwa konten buffer piksel saat ini TIDAK diprakalikan, sehingga perlu dikalikan dan mungkin kerangka kerja bisa menggunakan akselerasi perangkat keras lebih cepat daripada loop for{} saya? Saya mencoba bitmap.setPremultiplied(false) akan menyebabkan crash... - person Robin; 07.01.2015
comment
setPremultiplied adalah satu-satunya yang saya ketahui. Kerusakan tersebut mungkin merupakan RunTimeException yang disebabkan oleh pemanggilan createBitmap (lihat dokumen: developer.android.com/reference/android/graphics/ ). mungkin coba panggil setelah Anda memanggil createBitmap? Selain itu saya tidak tahu. - person samgak; 08.01.2015