Merge "Transform & preserve gainmaps" into udc-dev

This commit is contained in:
John Reck
2023-06-08 20:18:42 +00:00
committed by Android (Google) Code Review
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.concat(m);
canvas.drawBitmap(source, srcR, dstR, paint); canvas.drawBitmap(source, srcR, dstR, paint);
canvas.setBitmap(null); 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) { if (isHardware) {
return bitmap.copy(Config.HARDWARE, false); return bitmap.copy(Config.HARDWARE, false);
} }
return bitmap; 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 * Returns a mutable bitmap with the specified width and height. Its
* initial density is as per {@link #getDensity}. The newly created * 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()); 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 * @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 * 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 nGetFinalizer();
private static native long nCreateEmpty(); private static native long nCreateEmpty();
private static native long nCreateCopy(long source);
private static native void nSetBitmap(long ptr, Bitmap bitmap); 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)); 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) { static void Gainmap_setBitmap(JNIEnv* env, jobject, jlong gainmapPtr, jobject jBitmap) {
android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap); android::Bitmap* bitmap = GraphicsJNI::getNativeBitmap(env, jBitmap);
fromJava(gainmapPtr)->bitmap = sk_ref_sp(bitmap); 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[] = { static const JNINativeMethod gGainmapMethods[] = {
{"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer}, {"nGetFinalizer", "()J", (void*)Gainmap_getNativeFinalizer},
{"nCreateEmpty", "()J", (void*)Gainmap_createEmpty}, {"nCreateEmpty", "()J", (void*)Gainmap_createEmpty},
{"nCreateCopy", "(J)J", (void*)Gainmap_createCopy},
{"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap}, {"nSetBitmap", "(JLandroid/graphics/Bitmap;)V", (void*)Gainmap_setBitmap},
{"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin}, {"nSetRatioMin", "(JFFF)V", (void*)Gainmap_setRatioMin},
{"nGetRatioMin", "(J[F)V", (void*)Gainmap_getRatioMin}, {"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("Color Grid", R.layout.color_grid),
Demo("Gradient Sweep", R.layout.gradient_sweep), Demo("Gradient Sweep", R.layout.gradient_sweep),
Demo("Gainmap Image", R.layout.gainmap_image), 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( DemoGroup("Materials", listOf(
Demo("Glass", GlassActivity::class), Demo("Glass", GlassActivity::class),

View File

@@ -17,7 +17,12 @@
package com.android.test.silkfx.hdr package com.android.test.silkfx.hdr
import android.content.Context 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.ImageDecoder
import android.graphics.Paint
import android.graphics.Rect import android.graphics.Rect
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.Button import android.widget.Button
@@ -34,6 +39,25 @@ enum class DecodeMode {
CropedSquaredScaled33 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) { class GainmapDecodeTest(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) {
private fun decode(mode: DecodeMode) { 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 } val sdrBitmap = gainmapImage.also { it.gainmap = null }
findViewById<ImageView>(R.id.sdr_source)!!.setImageBitmap(sdrBitmap) 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<ImageView>(R.id.gainmap)!!.setImageBitmap(gainmapContents)
findViewById<TextView>(R.id.gainmap_label)!!.text = 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() { 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)
}
}
}
}