kotlin - 显示HDR图(heic格式),使用GainMap算法,速度从5秒提升到0.6秒

class HdrImageDecoderActivity : AppCompatActivity() , View.OnClickListener{
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.hdr_image_decoder_main)
findViewById<Button>(R.id.hdr_show_btn1).setOnClickListener(this)
findViewById<Button>(R.id.hdr_show_btn2).setOnClickListener(this)
}
override fun onClick(v: View?) {
v?:return
when(v.id){
R.id.hdr_show_btn1 -> {
show1()
}
R.id.hdr_show_btn2 -> {
show2()
}
}
}
private fun show2(){
LogUtils.i("AAA", "11111")
val resultImageView = findViewById<ImageView>(R.id.hdr_show_result)
val resultImageView2 = findViewById<ImageView>(R.id.hdr_show_result2)
//val filePath = "sdcard/DCIM/AAA/IMG_20251101_154711.HEIC"
val filePath = "sdcard/DCIM/Camera/IMG_20251101_154711.HEIC"
//val filePath = "sdcard/DCIM/Camera/IMG_20251026_100717.HEIC"
//val filePath = "sdcard/DCIM/Camera/IMG_20251102_162507.HEIC"
val options = BitmapFactory.Options()
options.inSampleSize = 2
val bitmap = BitmapFactory.decodeFile(filePath, options)
resultImageView.setImageBitmap(bitmap)
LogUtils.i("BBB", "111")
val gainMapResult = GainMapDecoder.decodeGainMap(filePath)
LogUtils.i("BBB", "222")
if(gainMapResult != null){
val bitmap2 = GainMapProcessor.applyGainMapConservative(gainMapResult.baseImage, gainMapResult.gainMap)
LogUtils.i("BBB", "333")
resultImageView2.setImageBitmap(bitmap2)
}
/*val bitmap2 = HDRHelper.loadProXdrImage(filePath)
resultImageView2.setImageBitmap(bitmap2)
LogUtils.i("AAA", "33333")*/
}
private fun show1(){
val resultImageView = findViewById<ImageView>(R.id.hdr_show_result)
val resultImageView2 = findViewById<ImageView>(R.id.hdr_show_result2)
val filePath = "sdcard/DCIM/Camera/IMG_20251026_111918.HEIC"
LogUtils.i("AAA", "exist = " + File(filePath).exists())
//HeifLoader.loadHeifImage(filePath, resultImageView)
val bitmap = HDRHelper.loadProXdrImage(filePath)
resultImageView.setImageBitmap(bitmap)
val options = BitmapFactory.Options()
//options.inSampleSize = 20
val bitmap2 = BitmapFactory.decodeFile(filePath)
resultImageView2.setImageBitmap(bitmap2)
}
}
package com.example.androidkotlindemo2.hdr;
/**
* Author : wn
* Email : maoning20080809@163.com
* Date : 2025/11/1 14:47
* Description :
*/
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.DisplayMetrics;
import android.util.Log;
import androidx.exifinterface.media.ExifInterface;
import com.example.androidkotlindemo2.utils.LogUtils;
public class GainMapDecoder {
private static final String TAG = "GainMapDecoder";
public static class GainMapResult {
public Bitmap baseImage;
public Bitmap gainMap;
public float gamma;
public float hdrCapacityMin;
public float hdrCapacityMax;
public float offsetSdr;
public float offsetHdr;
public GainMapResult() {
// 默认值
this.gamma = 1.0f;
this.hdrCapacityMin = 0.0f;
this.hdrCapacityMax = 1.0f;
this.offsetSdr = 0.0f;
this.offsetHdr = 0.0f;
}
}
public static GainMapResult decodeGainMap(String imagePath) {
GainMapResult result = new GainMapResult();
try {
// 第一步:获取图片尺寸而不加载到内存
BitmapFactory.Options sizeOptions = new BitmapFactory.Options();
sizeOptions.inJustDecodeBounds = true;
BitmapFactory.decodeFile(imagePath, sizeOptions);
int imageWidth = sizeOptions.outWidth;
int imageHeight = sizeOptions.outHeight;
// 第二步:根据屏幕宽度计算合适的采样率
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
int screenWidth = displayMetrics.widthPixels ;
// 计算采样率,确保解码后的图片宽度接近屏幕宽度
int inSampleSize = calculateInSampleSize(imageWidth , screenWidth);
//int inSampleSize = 2;
LogUtils.Companion.e("BBB", "decodeGainMap imageWidth = " + imageWidth +" , imageHeight = " + imageHeight +" , inSampleSize = " + inSampleSize +" ,screenWidth = " + screenWidth);
// 第三步:使用计算出的采样率解码图片
BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
decodeOptions.inPreferredConfig = Bitmap.Config.ARGB_8888;
decodeOptions.inSampleSize = inSampleSize;
result.baseImage = BitmapFactory.decodeFile(imagePath, decodeOptions);
LogUtils.Companion.e("BBB", "decodeGainMap result imageWidth = " + result.baseImage.getWidth() +" , imageHeight = " + result.baseImage.getHeight());
if (result.baseImage == null) {
Log.e(TAG, "Failed to decode base image");
return null;
}
// 尝试从EXIF数据读取GainMap信息
ExifInterface exif = new ExifInterface(imagePath);
// 检查是否存在GainMap相关标签
boolean hasGainMap = hasGainMapMetadata(exif);
if (hasGainMap || true) {
Log.d(TAG, "GainMap metadata found, attempting to decode...");
result.gainMap = extractGainMapFromExif(exif);
readGainMapParameters(exif, result);
} else {
Log.d(TAG, "No GainMap metadata found, using base image only");
}
} catch (Exception e) {
Log.e(TAG, "Error decoding GainMap: " + e.getMessage(), e);
// 出错时至少返回基础图像
if (result.baseImage == null) {
try {
result.baseImage = BitmapFactory.decodeFile(imagePath);
} catch (Exception ex) {
Log.e(TAG, "Failed to decode base image as fallback", ex);
}
}
}
return result;
}
/**
* 计算合适的采样率
* @param imageWidth 图片原始宽度
* @param targetWidth 目标宽度(屏幕宽度)
* @return 采样率,总是2的幂次方
*/
private static int calculateInSampleSize(int imageWidth, int targetWidth) {
int inSampleSize = 1;
if (imageWidth > targetWidth) {
// 计算理论采样率
float ratio = (float) imageWidth / targetWidth;
inSampleSize = Math.round(ratio);
// 确保采样率是2的幂次方(BitmapFactory的要求)
inSampleSize = roundToPowerOfTwo(inSampleSize);
}
// 设置最小和最大采样率限制
inSampleSize = Math.max(1, inSampleSize);
inSampleSize = Math.min(16, inSampleSize); // 防止采样率过大
return inSampleSize;
}
/**
* 将数值向上取整到最近的2的幂次方
* 例如:3→4, 5→8, 9→16
*/
private static int roundToPowerOfTwo(int value) {
int power = 1;
while (power < value) {
power *= 2;
}
return power;
}
private static boolean hasGainMapMetadata(ExifInterface exif) {
// 检查常见的GainMap相关EXIF标签
String makerNote = exif.getAttribute(ExifInterface.TAG_MAKER_NOTE);
String userComment = exif.getAttribute(ExifInterface.TAG_USER_COMMENT);
LogUtils.Companion.e("BBB", "hasGainMapMetadata makerNote = " + makerNote +" , userComment = " + userComment);
return (makerNote != null && makerNote.contains("GainMap")) ||
(userComment != null && userComment.contains("GainMap")) ||
exif.getAttribute("GainMapVersion") != null;
}
private static Bitmap extractGainMapFromExif(ExifInterface exif) {
try {
// 尝试从MakerNote或其他EXIF字段提取GainMap数据
byte[] gainMapData = exif.getThumbnail();
if (gainMapData != null && gainMapData.length > 0) {
return BitmapFactory.decodeByteArray(gainMapData, 0, gainMapData.length);
}
} catch (Exception e) {
Log.e(TAG, "Failed to extract GainMap from EXIF", e);
}
return null;
}
private static void readGainMapParameters(ExifInterface exif, GainMapResult result) {
try {
// 读取GainMap参数
String gamma = exif.getAttribute("GainMapGamma");
String hdrMin = exif.getAttribute("GainMapHDRMin");
String hdrMax = exif.getAttribute("GainMapHDRMax");
String offsetSdr = exif.getAttribute("GainMapOffsetSDR");
String offsetHdr = exif.getAttribute("GainMapOffsetHDR");
if (gamma != null) result.gamma = Float.parseFloat(gamma);
if (hdrMin != null) result.hdrCapacityMin = Float.parseFloat(hdrMin);
if (hdrMax != null) result.hdrCapacityMax = Float.parseFloat(hdrMax);
if (offsetSdr != null) result.offsetSdr = Float.parseFloat(offsetSdr);
if (offsetHdr != null) result.offsetHdr = Float.parseFloat(offsetHdr);
} catch (Exception e) {
Log.e(TAG, "Error reading GainMap parameters", e);
}
}
}
package com.example.androidkotlindemo2.hdr;
import android.graphics.Bitmap;
import com.example.androidkotlindemo2.utils.LogUtils;
/**
* Author : wn
* Email : maoning20080809@163.com
* Date : 2025/11/1 14:43
* Description :
*/
public class GainMapProcessor {
private static final String TAG = "GainMapProcessor";
/**
* 应用GainMap算法 - 优化曝光版本
*/
public static Bitmap applyGainMapAlgorithm(Bitmap baseImage, Bitmap gainMap,
float gamma, float hdrCapacityMin,
float hdrCapacityMax, float offsetSdr,
float offsetHdr) {
if (baseImage == null) return null;
if (gainMap == null) return baseImage;
int width = baseImage.getWidth();
int height = baseImage.getHeight();
LogUtils.Companion.d("BBB", "applyGainMapAlgorithm " + baseImage.getWidth() +" , " + baseImage.getHeight() +" , " + gainMap.getWidth() +" , " + gainMap.getHeight());
// 调整GainMap尺寸以匹配基础图像
Bitmap scaledGainMap = Bitmap.createScaledBitmap(gainMap, width, height, true);
// 创建结果Bitmap
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
// 应用GainMap算法
applyGainMapPixelsOptimized(baseImage, scaledGainMap, result,
gamma, hdrCapacityMin, hdrCapacityMax,
offsetSdr, offsetHdr);
if (scaledGainMap != gainMap) {
scaledGainMap.recycle();
}
return result;
}
private static void applyGainMapPixelsOptimized(Bitmap baseImage, Bitmap gainMap,
Bitmap result, float gamma,
float hdrCapacityMin, float hdrCapacityMax,
float offsetSdr, float offsetHdr) {
int width = baseImage.getWidth();
int height = baseImage.getHeight();
int[] basePixels = new int[width * height];
int[] gainPixels = new int[width * height];
int[] resultPixels = new int[width * height];
// 获取像素数据
baseImage.getPixels(basePixels, 0, width, 0, 0, width, height);
gainMap.getPixels(gainPixels, 0, width, 0, 0, width, height);
// 直接复制基础图像到结果,避免暗像素的额外处理
System.arraycopy(basePixels, 0, resultPixels, 0, basePixels.length);
// 关键修复:analyzeBrightness 只调用一次!
float[] brightnessStats = analyzeBrightness(basePixels, width, height);
int brightCount = 0;
// 处理每个像素
for (int i = 0; i < basePixels.length; i++) {
int basePixel = basePixels[i];
// 快速亮度计算,避免函数调用开销
int r = (basePixel >> 16) & 0xFF;
int g = (basePixel >> 8) & 0xFF;
int b = basePixel & 0xFF;
// 快速亮度近似计算
float pixelBrightness = (r * 0.299f + g * 0.587f + b * 0.114f) / 255.0f;
// 提高亮度阈值,只处理真正的高光区域
if (pixelBrightness < 0.85f) { // 从0.8提高到0.85
continue;
}
brightCount++;
int gainPixel = gainPixels[i];
// 提取RGB分量
float baseR = r / 255.0f;
float baseG = g / 255.0f;
float baseB = b / 255.0f;
float gainR = ((gainPixel >> 16) & 0xFF) / 255.0f;
float gainG = ((gainPixel >> 8) & 0xFF) / 255.0f;
float gainB = (gainPixel & 0xFF) / 255.0f;
// 应用自适应GainMap算法
float[] hdrColor = applyAdaptiveGainMap(
baseR, baseG, baseB,
gainR, gainG, gainB,
pixelBrightness, brightnessStats,
gamma, hdrCapacityMin, hdrCapacityMax,
offsetSdr, offsetHdr
);
// 转换回ARGB并钳制
int resultR = Math.max(0, Math.min(255, (int)(hdrColor[0] * 255)));
int resultG = Math.max(0, Math.min(255, (int)(hdrColor[1] * 255)));
int resultB = Math.max(0, Math.min(255, (int)(hdrColor[2] * 255)));
resultPixels[i] = (basePixel & 0xFF000000) | (resultR << 16) | (resultG << 8) | resultB;
}
LogUtils.Companion.d("BBB", "brightCount = " + brightCount);
result.setPixels(resultPixels, 0, width, 0, 0, width, height);
}
/**
* 分析图像亮度统计信息
*/
private static float[] analyzeBrightness(int[] pixels, int width, int height) {
float totalBrightness = 0;
float minBrightness = 1.0f;
float maxBrightness = 0.0f;
int sampleCount = 0;
// 采样分析亮度
for (int i = 0; i < pixels.length; i += 4) {
int pixel = pixels[i];
float r = ((pixel >> 16) & 0xFF) / 255.0f;
float g = ((pixel >> 8) & 0xFF) / 255.0f;
float b = (pixel & 0xFF) / 255.0f;
float brightness = calculateLuminance(r, g, b);
totalBrightness += brightness;
minBrightness = Math.min(minBrightness, brightness);
maxBrightness = Math.max(maxBrightness, brightness);
sampleCount++;
}
float avgBrightness = totalBrightness / sampleCount;
return new float[] {
avgBrightness, // 平均亮度
minBrightness, // 最小亮度
maxBrightness, // 最大亮度
maxBrightness - minBrightness // 亮度范围
};
}
/**
* 自适应GainMap应用算法 - 优化曝光
*/
private static float[] applyAdaptiveGainMap(float baseR, float baseG, float baseB,
float gainR, float gainG, float gainB,
float pixelBrightness, float[] brightnessStats,
float gamma, float hdrCapacityMin,
float hdrCapacityMax, float offsetSdr,
float offsetHdr) {
// 1. 对基础图像进行伽马解码
float[] linearBase = srgbToLinear(baseR, baseG, baseB);
// 2. 计算基础增益值 - 使用更保守的增益计算
float baseGain = calculateConservativeGainFromGainMap(gainR, gainG, gainB, gamma,
hdrCapacityMin, hdrCapacityMax);
// 3. 根据像素亮度自适应调整增益 - 使用更保守的策略
float adaptiveGain = calculateConservativeAdaptiveGain(baseGain, pixelBrightness, brightnessStats);
// 4. 应用增益 - 使用更保守的增益应用
float[] hdrLinear = applyConservativeGainToLinear(linearBase, adaptiveGain,
pixelBrightness, offsetSdr, offsetHdr);
// 5. 伽马编码回sRGB空间
return linearToSrgb(hdrLinear[0], hdrLinear[1], hdrLinear[2]);
}
/**
* 保守的增益计算 - 大幅降低增益强度
*/
private static float calculateConservativeGainFromGainMap(float gainR, float gainG, float gainB,
float gamma, float hdrCapacityMin,
float hdrCapacityMax) {
// 使用增益图的亮度信息
float gainLuminance = calculateLuminance(gainR, gainG, gainB);
// 对增益图进行伽马解码
float decodedGain = (float) Math.pow(gainLuminance, 1.0 / gamma);
// 归一化处理 - 使用更窄的范围
float normalizedGain = (decodedGain - hdrCapacityMin) / (hdrCapacityMax - hdrCapacityMin);
normalizedGain = Math.max(0.1f, Math.min(0.8f, normalizedGain)); // 上限从1.0降到0.8
// 转换为线性增益值 - 大幅降低增益范围
return 1.0f + normalizedGain * 0.6f; // 增益范围:1.0x - 1.6x (原来是1.0x - 2.5x)
}
/**
* 保守的自适应增益调整
*/
private static float calculateConservativeAdaptiveGain(float baseGain, float pixelBrightness,
float[] brightnessStats) {
float avgBrightness = brightnessStats[0];
float minBrightness = brightnessStats[1];
float maxBrightness = brightnessStats[2];
float brightnessRange = brightnessStats[3];
// 更保守的自适应因子
float adaptiveFactor;
if (pixelBrightness < 0.3f) {
// 暗部区域:轻微增加增益
adaptiveFactor = 1.0f + (0.3f - pixelBrightness) * 0.3f; // 从1.5降到0.3
} else if (pixelBrightness > 0.7f) {
// 高光区域:显著降低增益
adaptiveFactor = 0.7f + (1.0f - pixelBrightness) * 0.1f; // 从0.2降到0.1
} else {
// 中间调:基本不调整
adaptiveFactor = 1.0f;
}
// 根据图像整体亮度进一步调整 - 更保守
float imageBrightnessFactor;
if (avgBrightness < 0.3f) {
// 整体偏暗的图像:轻微增加整体增益
imageBrightnessFactor = 1.0f + (0.3f - avgBrightness) * 0.3f; // 从1.0降到0.3
} else if (avgBrightness > 0.7f) {
// 整体偏亮的图像:降低整体增益
imageBrightnessFactor = 0.8f; // 从0.7提高到0.8
} else {
imageBrightnessFactor = 1.0f;
}
return baseGain * adaptiveFactor * imageBrightnessFactor;
}
/**
* 保守的增益应用 - 防止过曝
*/
private static float[] applyConservativeGainToLinear(float[] linearBase, float gain,
float pixelBrightness,
float offsetSdr, float offsetHdr) {
float[] result = new float[3];
for (int i = 0; i < 3; i++) {
float baseChannel = linearBase[i];
// 更小的偏移量
float dynamicOffsetSdr = offsetSdr * (1.0f - pixelBrightness) * 0.05f; // 从0.1降到0.05
float dynamicOffsetHdr = offsetHdr * (1.0f - pixelBrightness) * 0.05f; // 从0.1降到0.05
// 应用偏移
float adjustedBase = baseChannel + dynamicOffsetSdr;
// 应用增益
float hdrChannel = adjustedBase * gain;
// 应用HDR偏移
hdrChannel += dynamicOffsetHdr;
// 更严格的动态钳制
float maxValue;
if (pixelBrightness > 0.9f) {
maxValue = 1.0f; // 极高光区域严格限制
} else if (pixelBrightness > 0.8f) {
maxValue = 1.2f; // 高光区域较严格限制
} else {
maxValue = 1.5f; // 其他区域适中限制
}
hdrChannel = Math.max(0, Math.min(maxValue, hdrChannel));
result[i] = hdrChannel;
}
return result;
}
/**
* sRGB到线性颜色空间转换
*/
private static float[] srgbToLinear(float r, float g, float b) {
return new float[] {
srgbToLinearSingle(r),
srgbToLinearSingle(g),
srgbToLinearSingle(b)
};
}
private static float srgbToLinearSingle(float channel) {
if (channel <= 0.04045f) {
return channel / 12.92f;
} else {
return (float) Math.pow((channel + 0.055f) / 1.055f, 2.4f);
}
}
/**
* 计算亮度
*/
private static float calculateLuminance(float r, float g, float b) {
return 0.2126f * r + 0.7152f * g + 0.0722f * b;
}
/**
* 线性到sRGB颜色空间转换
*/
private static float[] linearToSrgb(float r, float g, float b) {
return new float[] {
linearToSrgbSingle(r),
linearToSrgbSingle(g),
linearToSrgbSingle(b)
};
}
private static float linearToSrgbSingle(float channel) {
if (channel <= 0.0031308f) {
return channel * 12.92f;
} else {
return (float) (1.055f * Math.pow(channel, 1.0 / 2.4) - 0.055f);
}
}
/**
* 超保守版本 - 防止过曝
*/
public static Bitmap applyGainMapConservative(Bitmap baseImage, Bitmap gainMap) {
// 使用更保守的参数
float gamma = 1.8f; // 更高的gamma值
float hdrCapacityMin = 0.3f; // 提高最小值
float hdrCapacityMax = 0.6f; // 降低最大值
float offsetSdr = 0.005f; // 减小偏移
float offsetHdr = 0.002f; // 减小偏移
return applyGainMapAlgorithm(baseImage, gainMap,
gamma, hdrCapacityMin, hdrCapacityMax, offsetSdr, offsetHdr);
}
/**
* 轻度增强版本 - 平衡效果和自然度
*/
public static Bitmap applyGainMapBalanced(Bitmap baseImage, Bitmap gainMap) {
float gamma = 1.5f;
float hdrCapacityMin = 0.25f;
float hdrCapacityMax = 0.65f;
float offsetSdr = 0.008f;
float offsetHdr = 0.004f;
return applyGainMapAlgorithm(baseImage, gainMap,
gamma, hdrCapacityMin, hdrCapacityMax, offsetSdr, offsetHdr);
}
}
hdr_image_decoder_main.xml布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
tools:context=".MainActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/hdr_show_btn1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:textAllCaps="false"
android:textSize="22sp"
android:text="图片1"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/hdr_show_btn2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_gravity="center_horizontal"
android:textAllCaps="false"
android:textSize="22sp"
android:text="图片2 - GainMap算法"/>
</LinearLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hdr_show_result"
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:layout_height="260dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginLeft="20dp"
android:textSize="28sp"
android:textColor="@color/black"
android:text="GainMap图"/>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/hdr_show_result2"
android:layout_width="match_parent"
android:scaleType="centerCrop"
android:layout_height="260dp"/>
</LinearLayout>