บิตแมป setPixels จะสูญเสียช่องอัลฟาเมื่อพื้นหลังเป็นสีดำ

ฉันกำลังพยายามวาดมุมมองที่กำหนดเองใน Android ด้วยวิธีการ canvas.drawBitmap() อย่างไรก็ตาม ฉันพบว่าช่องอัลฟ่าจะหายไปหากฉันทำเช่นนี้ด้วยโค้ด JNI ดั้งเดิมและพื้นหลังเป็นสีดำ สรุปกรณีคือ

  1. เรียก java bitmap.setPixels() และตั้งค่าสีพิกเซลบิตแมปเป็น NDK เมื่อพื้นหลังเป็น สีขาว บิตแมปทั้งสองแสดงอย่างถูกต้อง
  2. เรียก java bitmap.setPixels() และตั้งค่าสีพิกเซลบิตแมปใน NDK เมื่อพื้นหลังเป็น สีดำ เฉพาะบิตแมปที่วาดโดย java API เท่านั้นที่แสดงอย่างถูกต้อง บิตที่วาดด้วย NDK สูญเสียช่องอัลฟา

คำถามคือเหตุใดผลลัพธ์บนพื้นหลังสีขาวจึงใช้ได้ แต่ไม่ดีบนพื้นหลังสีดำ ฉันพลาดอะไรไปหรือทำอะไรผิดหรือเปล่า?

ไฟล์ XML เค้าโครง:

<?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 สำหรับโค้ดเนทิฟ:

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

ภาพหน้าจอของผลลัพธ์: ภาพหน้าจอของผลลัพธ์


person Robin    schedule 07.01.2015    source แหล่งที่มา
comment
ถ้าคุณเปลี่ยนลำดับไบต์ในโค้ดเนทีฟของคุณล่ะ? (เช่น 0x0000ff11)   -  person Michael    schedule 07.01.2015
comment
ลำดับไบต์ถูกต้องในรหัสของฉัน (สีแดงที่มีอัลฟ่า 0x11) บนแพลตฟอร์ม ARM การใช้ 0x0000ff11 จะทำให้บิตแมปเป็นสี่เหลี่ยมสีเขียวทึบ   -  person Robin    schedule 07.01.2015
comment
ฉันได้อัปเดตคำถามพร้อมซอร์สโค้ดโดยละเอียดและภาพหน้าจอ   -  person Robin    schedule 07.01.2015


คำตอบ (1)


Android เก็บบิตแมปด้วยอัลฟ่าคูณล่วงหน้า เมื่อคุณเรียก setPixels() จาก Java ค่าสี RGB จะถูกคูณด้วยค่าอัลฟ่าโดยอัตโนมัติและจัดเก็บไว้ในบิตแมป อย่างไรก็ตาม เมื่อคุณเรียก Android_lockPixels() จากโค้ดเนทีฟแล้วเขียนลงในหน่วยความจำโดยตรง คุณจะต้องทำการคูณล่วงหน้าด้วยตัวเอง ไม่เช่นนั้นมันจะผิดพลาด หากคุณเปลี่ยนรหัสของคุณเป็น:

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

ดังนั้น Bitmaps ทั้งสองควรแสดงผลเหมือนกัน

เหตุใดจึง ปรากฏ ราวกับว่าบิตแมปสูญเสียช่องอัลฟ่าเมื่อพื้นหลังเป็นสีดำ แต่ไม่ใช่สำหรับพื้นหลังสีขาว ปรากฎว่าเป็นเพียงเรื่องบังเอิญตามตัวเลขที่คุณเลือก

สูตรการผสมอัลฟ่าพื้นฐานคือ:

 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;

โดยที่ dest คือพิกเซลพื้นหลังและแหล่งที่มาคือพิกเซลในบิตแมปของคุณ การคูณอัลฟ่าล่วงหน้าจะเปลี่ยนเป็น:

 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;

ซึ่งจะช่วยประหยัดการคูณได้มาก ผลลัพธ์ทั้งหมดถูกบีบไว้ที่ 255 ฉันไม่ได้อ้างว่านี่เป็นสูตรที่ใช้เป๊ะๆ นะ แต่มันค่อนข้างใกล้เคียงกัน

การเสียบตัวเลขสำหรับบิตแมป Java ของคุณ การคูณล่วงหน้า r, g, b จะเป็น 0x87 (หรือ 0x88 ขึ้นอยู่กับวิธีการปัดเศษ ฯลฯ ), 0x00 และ 0x00 สำหรับบิตแมปดั้งเดิมของคุณ มันจะเป็น 0xff, 0x00 และ 0x00 เนื่องจากคุณไม่ได้คูณล่วงหน้า การผสมอัลฟ่ากับพื้นหลังสีดำจะเหมือนกับการเพิ่มศูนย์ เนื่องจากค่า dest. r, g, b ล้วนเป็นศูนย์ ผลลัพธ์จึงดูแตกต่างออกไป

ในกรณีของพื้นหลังสีขาว dest.g และ dest.b จะจบลงที่เหมือนกันในทั้งสองกรณี เนื่องจากค่า g และ b ที่คูณไว้ล่วงหน้าจะเป็นศูนย์ทั้งในบิตแมป Java และ Native ในกรณีของ dest.r ผลลัพธ์ควรเป็น 255 ในกรณีของบิตแมปดั้งเดิม ค่าจะล้นเนื่องจากค่าที่ไม่ถูกต้องสำหรับการคูณล่วงหน้า r แต่จะถูกบีบไว้ที่ 255 ดังนั้น ผลลัพธ์จึงจบลงด้วยการมองหา เหมือนกัน.

กล่าวโดยสรุป ค่า r ที่คูณล่วงหน้านั้นสูงเกินไปสำหรับบิตแมปดั้งเดิมของคุณ ดังนั้นคุณจึงได้ค่า r สูงเกินไปในกรณีที่ผลลัพธ์ควรเป็น ‹ 255 ในกรณีที่ผลลัพธ์ควรเป็น 255 มันจะสูงเกินไปก็ไม่เป็นไรเพราะมันจะบีบอยู่ที่ 255 อยู่ดี

person samgak    schedule 07.01.2015
comment
เมื่อคุณพูดว่า premultipliedR คุณหมายถึง premultipliedAlpha ใช่ไหม? ฉันเปลี่ยนรหัสตามที่คุณแนะนำแล้วผลลัพธ์ก็เหมือนเดิม ฉันยังคงเห็นสี่เหลี่ยมสีแดงทึบบนพื้นหลังสีดำ แตกต่างจากสี่เหลี่ยมกึ่งโปร่งใสที่วาดโดย Java api หรือฉันเข้าใจคำตอบของคุณผิด ฉันควรเปลี่ยน *(dst+i)=??? หากคุณสามารถใช้โค้ดของฉันและแก้ไขเพื่อให้ได้ผลลัพธ์ที่ถูกต้องจะยอดเยี่ยมมาก - person Robin; 07.01.2015
comment
อีกคำถามหนึ่งในหัวข้อนี้ ถ้า R/G/B ทั้งหมดถูกจัดเก็บเป็นแบบคูณล่วงหน้า คุณยังจำเป็นต้องเก็บอัลฟ่าไว้ในพิกเซลหรือไม่ ฉันหมายถึง ทำ *(dst+i) = (WHATEVER_VALUE‹‹24) | คูณล่วงหน้าR | 0x00‹‹8 | 0x00‹‹16); จะได้รับผลลัพธ์เดียวกันไหม? - person Robin; 07.01.2015
comment
ใช่ คุณยังต้องการมันอยู่เนื่องจาก RGB ปลายทางกำลังคูณด้วย 256 ลบด้วยค่าอัลฟ่าในสมการการผสมผสาน - person samgak; 07.01.2015
comment
ขอบคุณมากที่ช่วยฉันในปัญหานี้ ฉันติดอยู่ที่นี่เป็นเวลา 2 วัน คำถามสุดท้าย: มี API อย่างเป็นทางการใดบ้างที่สามารถบอกเฟรมเวิร์กได้ว่าเนื้อหาบัฟเฟอร์พิกเซลปัจจุบันไม่ได้ถูกคูณล่วงหน้า ดังนั้นจึงจำเป็นต้องคูณและเฟรมเวิร์กอาจใช้การเร่งด้วยฮาร์ดแวร์เร็วกว่า for{} ลูปของฉัน ฉันลอง bitmap.setPremultiplied(false) แล้วจะทำให้เกิดข้อขัดข้อง... - person Robin; 07.01.2015
comment
setPremultiplied เป็นสิ่งเดียวที่ฉันรู้ ข้อขัดข้องอาจเป็น RunTimeException ที่เกิดจากการเรียก createBitmap (ดูเอกสาร: developer.android.com/reference/android/graphics/ ) อาจลองเรียกมันหลังจากที่คุณเรียก createBitmap? นอกเหนือจากนั้นฉันไม่รู้ - person samgak; 08.01.2015