Android开发中如何快速将位图从sRGB转换为线性RGB?
以下方案按性能从高到低排序,覆盖不同Android版本兼容需求:
方案1:系统原生色彩空间转换API(API 26+ 首选)
该方案走系统底层Skia引擎的硬件加速路径,无需自行实现像素计算逻辑,大尺寸位图处理性能比手写Renderscript高30%以上,转换精度完全符合sRGB官方标准。
不同API段的实现代码:// API 31+ 最简写法 val linearSrgb = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB) val linearBitmap = sourceBitmap.copy(Bitmap.Config.ARGB_8888, false, linearSrgb)// API 26-30 兼容写法 val linearSrgb = ColorSpace.get(ColorSpace.Named.LINEAR_SRGB) val linearBitmap = Bitmap.createBitmap( sourceBitmap.width, sourceBitmap.height, Bitmap.Config.ARGB_8888 ).apply { colorSpace = linearSrgb } Canvas(linearBitmap).drawBitmap(sourceBitmap, 0f, 0f, null)注意:该转换由系统自动完成,不需要额外处理像素,只要源位图的
colorSpace属性正确标记为sRGB(绝大多数场景下系统加载的图片默认就是该配置)即可得到正确结果。方案2:合并逻辑的自定义Renderscript内核(兼容API 17+)
你当前已经在使用Renderscript做后续线性RGB到其他色彩空间的矩阵转换,不要单独为sRGB转线性做一次全像素遍历,把两段逻辑合并到同一个Renderscript内核中,减少一次像素读写的内存IO开销,性能可以提升近一倍。
参考Renderscript实现:#pragma version(1) #pragma rs java_package_name(替换为你自己的应用包名) #pragma rs_fp_relaxed // 开启快速浮点模式,pow运算速度提升明显,精度满足色彩转换需求 rs_allocation inputAlloc; rs_matrix3x3 targetColorMatrix; // 填入你原来用的线性RGB转LMS/XYZ的转换矩阵 uchar4 __attribute__((kernel)) pixelConvert(uint32_t x, uint32_t y) { const uchar4 srcPixel = rsGetElementAt_uchar4(inputAlloc, x, y); // 通道值归一化到0-1区间 float r = srcPixel.r / 255.0f; float g = srcPixel.g / 255.0f; float b = srcPixel.b / 255.0f; // sRGB分段伽马校正转线性RGB r = r <= 0.04045f ? r / 12.92f : pow((r + 0.055f)/1.055f, 2.4f); g = g <= 0.04045f ? g / 12.92f : pow((g + 0.055f)/1.055f, 2.4f); b = b <= 0.04045f ? b / 12.92f : pow((b + 0.055f)/1.055f, 2.4f); // 直接在同一次遍历中完成后续色彩矩阵转换 const float3 linearRgb = {r, g, b}; const float3 targetColor = rsMatrixMultiply(&targetColorMatrix, linearRgb); // 转换回8位通道值 uchar4 outPixel; outPixel.r = (uchar)clamp(targetColor.r * 255.0f + 0.5f, 0.0f, 255.0f); outPixel.g = (uchar)clamp(targetColor.g * 255.0f + 0.5f, 0.0f, 255.0f); outPixel.b = (uchar)clamp(targetColor.b * 255.0f + 0.5f, 0.0f, 255.0f); outPixel.a = srcPixel.a; return outPixel; }注意:不要尝试用Renderscript的ColorMatrix实现sRGB转线性,矩阵只能完成线性变换,无法处理sRGB的非线性分段伽马曲线,会出现明显色偏。
方案3:256阶LUT查表法(全API版本兼容)
8位深度的sRGB单通道取值范围只有0-255共256种可能,可以在应用启动时预计算好每个sRGB值对应的线性值存在数组中,转换时直接查表,不需要实时计算幂运算,性能非常稳定。
预生成LUT表的代码:// 应用启动时执行一次即可,无需每次转换位图都重新计算 val srgbToLinearLut = FloatArray(256) { srgbValue -> val normalized = srgbValue / 255.0f if (normalized <= 0.04045f) normalized / 12.92f else ((normalized + 0.055f) / 1.055f).pow(2.4f) }注意:该方案最好配合NDK实现C++层的像素遍历,比Java/Kotlin层逐像素循环快10倍以上;如果搭配Renderscript使用,可以把LUT作为全局常量绑定到脚本中,查表速度比实时计算pow快2-3倍。
避坑说明
不要在Java/Kotlin层通过getPixel()/setPixel()逐像素处理位图,这两个方法每次调用都会触发JNI跨边界通信,处理1080P分辨率位图时耗时会是上述方案的几十倍。
内容的提问来源于stack exchange,提问作者spiderduckpig

