Transform & preserve gainmaps

Have Bitmap.createBitmap(sourceBitmap, ...) preserve any gainmaps if
present, transforming the gainmaps in the same way.

This addresses 2 common usages:

1) Rotating bitmaps to handle EXIF orientations
2) Bitmap.createScaledBitmap() to do "static" scaling

Bug: 286131154
Test: SilkFX GainmapTransformsTest
Change-Id: I5a62dccbb2c70bc38cca581b161eef792c8b2a78
This commit is contained in:
John Reck
2023-06-07 17:58:16 -04:00
parent 64d7bea916
commit b1c20eea9e
7 changed files with 340 additions and 3 deletions

View File

@@ -997,12 +997,63 @@ public final class Bitmap implements Parcelable {
canvas.concat(m);
canvas.drawBitmap(source, srcR, dstR, paint);
canvas.setBitmap(null);
// If the source has a gainmap, apply the same set of transformations to the gainmap
// and set it on the output
if (source.hasGainmap()) {
Bitmap newMapContents = transformGainmap(source, m, neww, newh, paint, srcR, dstR,
deviceR);
if (newMapContents != null) {
bitmap.setGainmap(new Gainmap(source.getGainmap(), newMapContents));
}
}
if (isHardware) {
return bitmap.copy(Config.HARDWARE, false);
}
return bitmap;
}
private static Bitmap transformGainmap(Bitmap source, Matrix m, int neww, int newh, Paint paint,
Rect srcR, RectF dstR, RectF deviceR) {
Canvas canvas;
Bitmap sourceGainmap = source.getGainmap().getGainmapContents();
// Gainmaps can be scaled relative to the base image (eg, 1/4th res)
// Preserve that relative scaling between the base & gainmap in the output
float scaleX = (sourceGainmap.getWidth() / (float) source.getWidth());
float scaleY = (sourceGainmap.getHeight() / (float) source.getHeight());
int mapw = Math.round(neww * scaleX);
int maph = Math.round(newh * scaleY);
if (mapw == 0 || maph == 0) {
// The gainmap has been scaled away entirely, drop it
return null;
}
// Scale the computed `srcR` used for rendering the source bitmap to the destination
// to be in gainmap dimensions
Rect gSrcR = new Rect((int) (srcR.left * scaleX),
(int) (srcR.top * scaleY), (int) (srcR.right * scaleX),
(int) (srcR.bottom * scaleY));
// Note: createBitmap isn't used as that requires a non-null colorspace, however
// gainmaps don't have a colorspace. So use `nativeCreate` directly to bypass
// that colorspace enforcement requirement (#getColorSpace() allows a null return)
Bitmap newMapContents = nativeCreate(null, 0, mapw, mapw, maph,
sourceGainmap.getConfig().nativeInt, true, 0);
newMapContents.eraseColor(0);
canvas = new Canvas(newMapContents);
// Scale the translate & matrix to be in gainmap-relative dimensions
canvas.scale(scaleX, scaleY);
canvas.translate(-deviceR.left, -deviceR.top);
canvas.concat(m);
canvas.drawBitmap(sourceGainmap, gSrcR, dstR, paint);
canvas.setBitmap(null);
// Create a new gainmap using a copy of the metadata information from the source but
// with the transformed bitmap created above
return newMapContents;
}
/**
* Returns a mutable bitmap with the specified width and height. Its
* initial density is as per {@link #getDensity}. The newly created

View File

@@ -121,6 +121,16 @@ public final class Gainmap implements Parcelable {
this(gainmapContents, nCreateEmpty());
}
/**
* Creates a new gainmap using the provided gainmap as the metadata source and the provided
* bitmap as the replacement for the gainmapContents
* TODO: Make public, it's useful
* @hide
*/
public Gainmap(@NonNull Gainmap gainmap, @NonNull Bitmap gainmapContents) {
this(gainmapContents, nCreateCopy(gainmap.mNativePtr));
}
/**
* @return Returns the image data of the gainmap represented as a Bitmap. This is represented
* as a Bitmap for broad API compatibility, however certain aspects of the Bitmap are ignored
@@ -325,6 +335,7 @@ public final class Gainmap implements Parcelable {
private static native long nGetFinalizer();
private static native long nCreateEmpty();
private static native long nCreateCopy(long source);
private static native void nSetBitmap(long ptr, Bitmap bitmap);

View File

@@ -86,6 +86,16 @@ jlong Gainmap_createEmpty(JNIEnv*, jobject) {
return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
}
jlong Gainmap_createCopy(JNIEnv*, jobject, jlong sourcePtr) {
Gainmap* gainmap = new Gainmap();
gainmap->incStrong(0);
if (sourcePtr) {
Gainmap* src = fromJava(sourcePtr);
gainmap->info = src->info;
}
return static_cast<jlong>(reinterpret_cast<uintptr_t>(gainmap));
}
static void Gainmap_setBitmap(JNIEnv* env, jobject, jlong gainmapPtr, jobject jBitmap) {
android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap);
fromJava(gainmapPtr)->bitmap = sk_ref_sp(bitmap);
@@ -237,6 +247,7 @@ static void Gainmap_readFromParcel(JNIEnv* env, jobject, jlong nativeObject, job
static const JNINativeMethod gGainmapMethods[] = {
{"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer},
{"nCreateEmpty", "()J", (void*)Gainmap_createEmpty},
{"nCreateCopy", "(J)J", (void*)Gainmap_createCopy},
{"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap},
{"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin},
{"nGetRatioMin", "(J[F)V", (void*)Gainmap_getRatioMin},

View File

@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2023 The Android Open Source Project
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<com.android.test.silkfx.hdr.GainmapTransformsTest xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/original"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Original" />
<Button
android:id="@+id/scaled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Scaled (1/3)" />
<Button
android:id="@+id/rotate_90"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Rotate 90" />
<Button
android:id="@+id/rotate_90_scaled"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Rot90+Scale" />
<Button
android:id="@+id/crop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Crop" />
<Button
android:id="@+id/crop_200"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Crop 200" />
</LinearLayout>
<TextView
android:id="@+id/source_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:id="@+id/sdr_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/sdr_source"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:scaleType="fitStart" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:orientation="vertical"
android:layout_weight="1">
<TextView
android:id="@+id/gainmap_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/gainmap"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp"
android:scaleType="fitStart" />
</LinearLayout>
</LinearLayout>
</com.android.test.silkfx.hdr.GainmapTransformsTest>

View File

@@ -55,7 +55,9 @@ private val AllDemos = listOf(
Demo("Color Grid", R.layout.color_grid),
Demo("Gradient Sweep", R.layout.gradient_sweep),
Demo("Gainmap Image", R.layout.gainmap_image),
Demo("Gainmap Decode Test", R.layout.gainmap_decode_test, commonControls = false)
Demo("Gainmap Decode Test", R.layout.gainmap_decode_test, commonControls = false),
Demo("Gainmap Transform Test", R.layout.gainmap_transform_test,
commonControls = false)
)),
DemoGroup("Materials", listOf(
Demo("Glass", GlassActivity::class),

View File

@@ -17,7 +17,12 @@
package com.android.test.silkfx.hdr
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorMatrixColorFilter
import android.graphics.Gainmap
import android.graphics.ImageDecoder
import android.graphics.Paint
import android.graphics.Rect
import android.util.AttributeSet
import android.widget.Button
@@ -34,6 +39,25 @@ enum class DecodeMode {
CropedSquaredScaled33
}
fun gainmapVisualizer(gainmap: Gainmap): Bitmap {
val map = gainmap.gainmapContents
val gainmapVisualizer = Bitmap.createBitmap(map.width, map.height,
Bitmap.Config.ARGB_8888)
val canvas = Canvas(gainmapVisualizer!!)
val paint = Paint()
paint.colorFilter = ColorMatrixColorFilter(
floatArrayOf(
0f, 0f, 0f, 1f, 0f,
0f, 0f, 0f, 1f, 0f,
0f, 0f, 0f, 1f, 0f,
0f, 0f, 0f, 0f, 255f
)
)
canvas.drawBitmap(map, 0f, 0f, paint)
canvas.setBitmap(null)
return gainmapVisualizer
}
class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
private fun decode(mode: DecodeMode) {
@@ -71,7 +95,7 @@ class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(c
}
}
val gainmapContents = gainmapImage.gainmap!!.gainmapContents!!
val gainmapContents = gainmapImage.gainmap?.let { gainmapVisualizer(it) }
val sdrBitmap = gainmapImage.also { it.gainmap = null }
findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap)
@@ -80,7 +104,7 @@ class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(c
findViewById<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
findViewById<TextView>(R.id.gainmap_label)!!.text =
"Gainmap Size: ${gainmapContents.width}x${gainmapContents.height}"
"Gainmap Size: ${gainmapContents?.width ?: 0}x${gainmapContents?.height ?: 0}"
}
override fun onFinishInflate() {

View File

@@ -0,0 +1,116 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.test.silkfx.hdr
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.graphics.Matrix
import android.util.AttributeSet
import android.widget.Button
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import com.android.test.silkfx.R
class GainmapTransformsTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
private val sourceImage = loadSample()
private fun loadSample(): Bitmap {
val source = ImageDecoder.createSource(resources.assets,
"gainmaps/${context.assets.list("gainmaps")!![0]}")
return ImageDecoder.decodeBitmap(source) { decoder, info, source ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
}
private fun process(transform: (Bitmap) -> Bitmap) {
val result = transform(sourceImage)
val gainmapContents = result.gainmap?.let { gainmapVisualizer(it) }
val sdrBitmap = result.also { it.gainmap = null }
findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap)
findViewById<TextView>(R.id.sdr_label)!!.text =
"SDR Size: ${sdrBitmap.width}x${sdrBitmap.height}"
findViewById<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
findViewById<TextView>(R.id.gainmap_label)!!.text =
"Gainmap Size: ${gainmapContents?.width ?: 0}x${gainmapContents?.height ?: 0}"
}
override fun onFinishInflate() {
super.onFinishInflate()
val sourceInfo = findViewById<TextView>(R.id.source_info)!!
sourceInfo.text = "Original size ${sourceImage.width}x${sourceImage.height}"
process { it.copy(Bitmap.Config.ARGB_8888, false) }
findViewById<Button>(R.id.original)!!.setOnClickListener {
process { it.copy(Bitmap.Config.ARGB_8888, false) }
}
findViewById<Button>(R.id.scaled)!!.setOnClickListener {
process { Bitmap.createScaledBitmap(it, it.width / 3, it.height / 3, true) }
}
findViewById<Button>(R.id.rotate_90)!!.setOnClickListener {
process {
val width: Int = it.width
val height: Int = it.height
val m = Matrix()
m.setRotate(90.0f, (width / 2).toFloat(), (height / 2).toFloat())
Bitmap.createBitmap(it, 0, 0, width, height, m, false)
}
}
findViewById<Button>(R.id.rotate_90_scaled)!!.setOnClickListener {
process {
val width: Int = it.width
val height: Int = it.height
val m = Matrix()
m.setRotate(90.0f, (width / 2).toFloat(), (height / 2).toFloat())
m.preScale(.3f, .3f)
Bitmap.createBitmap(it, 0, 0, width, height, m, false)
}
}
findViewById<Button>(R.id.crop)!!.setOnClickListener {
process {
val width: Int = it.width
val height: Int = it.height
Bitmap.createBitmap(it, width / 2, height / 2,
width / 4, height / 4, null, false)
}
}
findViewById<Button>(R.id.crop_200)!!.setOnClickListener {
process {
val width: Int = it.width
val height: Int = it.height
val m = Matrix()
m.setRotate(200.0f, (width / 2).toFloat(), (height / 2).toFloat())
Bitmap.createBitmap(it, width / 2, height / 2,
width / 4, height / 4, m, false)
}
}
}
}