【Android 美颜相机】第十九天:GPUImageColorBalanceFilter (色彩平衡滤镜)

GPUImageColorBalanceFilter 源码详解

GPUImageColorBalanceFilter 是基于 Android 平台 GPUImage 框架实现的色彩平衡滤镜,核心通过 GLSL 着色器在 GPU 端完成图像阴影、中间调、高光区域的色彩偏移调整,同时支持"保留亮度"模式,兼顾色彩调整与亮度一致性。

本文将逐行解析代码结构、核心逻辑,并说明实际使用方式。

代码整体结构梳理

该类分为以下核心模块:

  1. 包声明与依赖导入
  2. 类定义与核心 GLSL 片段着色器(色彩平衡的核心计算逻辑)
  3. 着色器 Uniform 变量位置缓存(shadowsLocation、midtonesLocation 等)
  4. 滤镜参数成员变量(阴影/中间调/高光偏移值、亮度保留开关)
  5. 构造方法(初始化默认参数)
  6. 生命周期方法(onInit/onInitialized:初始化着色器变量、设置默认参数)
  7. Setter 方法(更新滤镜参数并同步到 GPU)

代码逐段解析(含逐行注释)

1. 包声明与导入类

java 复制代码
// 声明当前类所属的包路径,遵循 GPUImage 框架的滤镜包规范
package jp.co.cyberagent.android.gpuimage.filter;

// 导入 Android 原生 OpenGL ES 2.0 核心类,用于操作 GPU 着色器变量
import android.opengl.GLES20;

含义

  • 包声明遵循 GPUImage 框架的目录结构,确保滤镜能被框架识别;
  • 导入 GLES20 类,用于与 OpenGL ES 2.0 交互(如获取着色器 Uniform 变量位置、设置变量值)。

2. 类定义与核心成员变量

java 复制代码
/**
 * Created by edward_chiang on 13/10/16.
 */
// 继承 GPUImageFilter(GPUImage 框架的基础滤镜类),实现色彩平衡滤镜
public class GPUImageColorBalanceFilter extends GPUImageFilter {

    // 片段着色器字符串:核心色彩平衡计算逻辑(GLSL 代码)
    public static final String GPU_IMAGE_COLOR_BALANCE_FRAGMENT_SHADER = "" +
            // 纹理坐标(从顶点着色器传递过来)
            "varying highp vec2 textureCoordinate;\n" +
            // 输入纹理(待处理的图像)
            "uniform sampler2D inputImageTexture;\n" +
            // 阴影区域色彩偏移值(RGB 三维向量)
            "uniform lowp vec3 shadowsShift;\n" +
            // 中间调区域色彩偏移值(RGB 三维向量)
            "uniform lowp vec3 midtonesShift;\n" +
            // 高光区域色彩偏移值(RGB 三维向量)
            "uniform lowp vec3 highlightsShift;\n" +
            // 是否保留亮度(int 型:0=不保留,1=保留)
            "uniform int preserveLuminosity;\n" +
            
            // ========== 核心函数 1:RGB 转 HSL 颜色空间 ==========
            "lowp vec3 RGBToHSL(lowp vec3 color)\n" +
            "{\n" +
            "lowp vec3 hsl; // 定义 HSL 向量(H:色相, S:饱和度, L:亮度)\n" +

            "lowp float fmin = min(min(color.r, color.g), color.b);    // 取 RGB 最小值(计算亮度用)\n" +
            "lowp float fmax = max(max(color.r, color.g), color.b);    // 取 RGB 最大值(计算亮度/饱和度用)\n" +
            "lowp float delta = fmax - fmin;             // RGB 差值(计算饱和度用)\n" +

            "hsl.z = (fmax + fmin) / 2.0; // 计算亮度 L(0=黑,1=白)\n" +

            "if (delta == 0.0)		// 差值为0 → 灰度色(无饱和度)\n" +
            "{\n" +
            "    hsl.x = 0.0;	// 色相 H 设为0(灰度无色相)\n" +
            "    hsl.y = 0.0;	// 饱和度 S 设为0\n" +
            "}\n" +
            "else                                    // 非灰度色 → 计算色相/饱和度\n" +
            "{\n" +
            "    // 计算饱和度 S:亮度<0.5 时,S=delta/(max+min);否则 S=delta/(2-max-min)\n" +
            "    if (hsl.z < 0.5)\n" +
            "        hsl.y = delta / (fmax + fmin); \n" +
            "    else\n" +
            "        hsl.y = delta / (2.0 - fmax - fmin); \n" +
            "\n" +
            "    // 计算色相偏移量(用于最终色相 H 的计算)\n" +
            "    lowp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;\n" +
            "    lowp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;\n" +
            "    lowp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;\n" +
            "\n" +
            "    // 根据 RGB 最大值所在通道,计算色相 H(范围 0~1)\n" +
            "    if (color.r == fmax )\n" +
            "        hsl.x = deltaB - deltaG; \n" +
            "    else if (color.g == fmax)\n" +
            "        hsl.x = (1.0 / 3.0) + deltaR - deltaB; \n" +
            "    else if (color.b == fmax)\n" +
            "        hsl.x = (2.0 / 3.0) + deltaG - deltaR; \n" +

            "    // 修正色相范围(确保 H 在 0~1 之间)\n" +
            "    if (hsl.x < 0.0)\n" +
            "        hsl.x += 1.0; \n" +
            "    else if (hsl.x > 1.0)\n" +
            "        hsl.x -= 1.0; \n" +
            "}\n" +
            "\n" +
            "return hsl; // 返回 HSL 颜色值\n" +
            "}\n" +

            // ========== 核心函数 2:色相转 RGB 分量 ==========
            "lowp float HueToRGB(lowp float f1, lowp float f2, lowp float hue)\n" +
            "{\n" +
            "    // 修正色相范围(0~1)\n" +
            "    if (hue < 0.0)\n" +
            "        hue += 1.0;\n" +
            "    else if (hue > 1.0)\n" +
            "        hue -= 1.0;\n" +
            "    lowp float res; // 存储转换后的 RGB 分量值\n" +
            "    // 根据色相区间,计算 RGB 分量\n" +
            "    if ((6.0 * hue) < 1.0)\n" +
            "        res = f1 + (f2 - f1) * 6.0 * hue;\n" +
            "    else if ((2.0 * hue) < 1.0)\n" +
            "        res = f2;\n" +
            "    else if ((3.0 * hue) < 2.0)\n" +
            "        res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;\n" +
            "    else\n" +
            "        res = f1;\n" +
            "    return res; // 返回 RGB 分量值\n" +
            "}\n" +

            // ========== 核心函数 3:HSL 转 RGB 颜色空间 ==========
            "lowp vec3 HSLToRGB(lowp vec3 hsl)\n" +
            "{\n" +
            "    lowp vec3 rgb; // 存储转换后的 RGB 颜色值\n" +

            "    if (hsl.y == 0.0) // 饱和度为0 → 灰度色(RGB 等于亮度)\n" +
            "        rgb = vec3(hsl.z); \n" +
            "    else\n" +
            "    {\n" +
            "        lowp float f2; // 辅助变量(计算 RGB 用)\n" +

            "        // 根据亮度计算 f2\n" +
            "        if (hsl.z < 0.5)\n" +
            "            f2 = hsl.z * (1.0 + hsl.y);\n" +
            "        else\n" +
            "            f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);\n" +

            "        lowp float f1 = 2.0 * hsl.z - f2; // 辅助变量\n" +

            "        // 分别计算 R/G/B 分量(基于色相偏移)\n" +
            "        rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0));\n" +
            "        rgb.g = HueToRGB(f1, f2, hsl.x);\n" +
            "        rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0));\n" +
            "    }\n" +

            "    return rgb;  // 返回 RGB 颜色值\n" +
            "}\n" +

            // ========== 核心函数 4:RGB 转亮度 L ==========
            "lowp float RGBToL(lowp vec3 color)\n" +
            "{\n" +
            "    lowp float fmin = min(min(color.r, color.g), color.b);    // RGB 最小值\n" +
            "    lowp float fmax = max(max(color.r, color.g), color.b);    // RGB 最大值\n" +

            "    return (fmax + fmin) / 2.0; // 返回亮度 L\n" +
            "}\n" +

            // ========== 主函数:片元着色器入口 ==========
            "void main()\n" +
            "{\n" +
            "    // 采样当前纹理坐标的像素颜色(RGBA)\n" +
            "    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);\n" +

            "    // 获取像素的 RGB 亮度(注:原代码注释的 alternative way 是直接用 RGB,实际取的是原像素 RGB)\n" +
            "    lowp vec3 lightness = textureColor.rgb;\n" +

            "    // 定义色彩平衡计算的辅助常量\n" +
            "    const lowp float a = 0.25;\n" +
            "    const lowp float b = 0.333;\n" +
            "    const lowp float scale = 0.7;\n" +

            "    // 计算阴影区域的色彩偏移:基于亮度区间筛选阴影像素,并乘以偏移值\n" +
            "    lowp vec3 shadows = shadowsShift * (clamp((lightness - b) / -a + 0.5, 0.0, 1.0) * scale);\n" +
            "    // 计算中间调区域的色彩偏移:筛选中间调像素,乘以偏移值\n" +
            "    lowp vec3 midtones = midtonesShift * (clamp((lightness - b) / a + 0.5, 0.0, 1.0) *\n" +
            "        clamp((lightness + b - 1.0) / -a + 0.5, 0.0, 1.0) * scale);\n" +
            "    // 计算高光区域的色彩偏移:筛选高光像素,乘以偏移值\n" +
            "    lowp vec3 highlights = highlightsShift * (clamp((lightness + b - 1.0) / a + 0.5, 0.0, 1.0) * scale);\n" +

            "    // 合并阴影/中间调/高光的色彩偏移,得到新颜色\n" +
            "    mediump vec3 newColor = textureColor.rgb + shadows + midtones + highlights;\n" +
            "    // 修正颜色范围(确保 RGB 在 0~1 之间,避免过曝/过暗)\n" +
            "    newColor = clamp(newColor, 0.0, 1.0);\n    " +

            "    // 判断是否保留原亮度\n" +
            "    if (preserveLuminosity != 0) {\n   " +
            "        // 新颜色转 HSL(获取调整后的色相/饱和度)\n" +
            "        lowp vec3 newHSL = RGBToHSL(newColor);\n" +
            "        // 原颜色的亮度\n" +
            "        lowp float oldLum = RGBToL(textureColor.rgb);\n" +
            "        // 保留原亮度,将新色相/饱和度转回 RGB\n" +
            "        textureColor.rgb = HSLToRGB(vec3(newHSL.x, newHSL.y, oldLum));\n" +
            "        // 输出最终颜色(保留原 Alpha 通道)\n" +
            "        gl_FragColor = textureColor;\n" +
            "    } else {\n" +
            "        // 不保留亮度,直接输出新颜色(保留原 Alpha 通道)\n" +
            "        gl_FragColor = vec4(newColor.rgb, textureColor.w);\n" +
            "    }\n" +
            "}\n";

    // ========== Java 层成员变量:缓存着色器 Uniform 变量位置 ==========
    // 阴影偏移变量的 GPU 位置
    private int shadowsLocation;
    // 中间调偏移变量的 GPU 位置
    private int midtonesLocation;
    // 高光偏移变量的 GPU 位置
    private int highlightsLocation;
    // 亮度保留开关变量的 GPU 位置
    private int preserveLuminosityLocation;

    // ========== 滤镜参数:默认值 ==========
    // 阴影偏移值(RGB 三维数组,默认无偏移)
    private float[] showdows; // 注:变量名拼写错误(应为 shadows),属于代码小瑕疵
    // 中间调偏移值(默认无偏移)
    private float[] midtones;
    // 高光偏移值(默认无偏移)
    private float[] highlights;
    // 是否保留亮度(默认开启)
    private boolean preserveLuminosity;

核心含义

  • 着色器字符串 :是滤镜的核心,通过 GLSL 实现:
    1. 定义输入纹理、色彩偏移变量、亮度保留开关;
    2. 实现 RGB↔HSL 颜色空间转换(用于"保留亮度"功能);
    3. 主函数中根据像素亮度区分阴影/中间调/高光区域,叠加对应色彩偏移;
    4. 可选保留原亮度(仅调整色相/饱和度,不改变亮度)。
  • Java 层成员变量
    1. 缓存 Uniform 变量的 GPU 位置(避免重复查询,提升性能);
    2. 存储滤镜参数(默认无色彩偏移,开启亮度保留)。

3. 构造方法

java 复制代码
    // 构造方法:初始化滤镜默认参数
    public GPUImageColorBalanceFilter() {
        // 调用父类构造方法:顶点着色器用默认(NO_FILTER_VERTEX_SHADER),片段着色器用自定义的色彩平衡着色器
        super(NO_FILTER_VERTEX_SHADER, GPU_IMAGE_COLOR_BALANCE_FRAGMENT_SHADER);
        // 初始化阴影偏移:RGB 均为 0(无偏移)
        this.showdows = new float[]{0.0f, 0.0f, 0.0f};
        // 初始化中间调偏移:无偏移
        this.midtones = new float[]{0.0f, 0.0f, 0.0f};
        // 初始化高光偏移:无偏移
        this.highlights = new float[]{0.0f, 0.0f, 0.0f};
        // 默认保留亮度
        this.preserveLuminosity = true;
    }

含义

  • 父类 GPUImageFilter 的构造方法需要传入顶点着色器和片段着色器:
    • 顶点着色器用框架默认的 NO_FILTER_VERTEX_SHADER(仅处理纹理坐标变换);
    • 片段着色器用自定义的色彩平衡着色器。
  • 初始化所有参数为默认值,确保滤镜创建后可直接使用。

4. 初始化生命周期方法

java 复制代码
    // 滤镜初始化时调用:获取着色器 Uniform 变量的 GPU 位置
    @Override
    public void onInit() {
        super.onInit(); // 调用父类初始化逻辑
        // 获取 "shadowsShift" 变量的 GPU 位置
        shadowsLocation = GLES20.glGetUniformLocation(getProgram(), "shadowsShift");
        // 获取 "midtonesShift" 变量的 GPU 位置
        midtonesLocation = GLES20.glGetUniformLocation(getProgram(), "midtonesShift");
        // 获取 "highlightsShift" 变量的 GPU 位置
        highlightsLocation = GLES20.glGetUniformLocation(getProgram(), "highlightsShift");
        // 获取 "preserveLuminosity" 变量的 GPU 位置
        preserveLuminosityLocation = GLES20.glGetUniformLocation(getProgram(), "preserveLuminosity");
    }

    // 滤镜初始化完成后调用:设置默认参数到 GPU
    @Override
    public void onInitialized() {
        super.onInitialized(); // 调用父类逻辑
        // 设置中间调偏移值到 GPU
        setMidtones(midtones);
        // 设置阴影偏移值到 GPU(变量名拼写错误不影响功能)
        setShowdows(showdows);
        // 设置高光偏移值到 GPU
        setHighlights(highlights);
        // 设置亮度保留开关到 GPU
        setPreserveLuminosity(preserveLuminosity);
    }

含义

  • onInit():滤镜初始化时,通过 GLES20.glGetUniformLocation 获取着色器中 Uniform 变量的 GPU 内存地址,缓存到成员变量中(避免每次绘制都查询,提升性能);
  • onInitialized():初始化完成后,将默认参数同步到 GPU,确保滤镜生效。

5. Setter 方法(更新滤镜参数)

java 复制代码
    // 设置阴影偏移值,并同步到 GPU
    public void setShowdows(float[] showdows) {
        this.showdows = showdows; // 更新本地变量
        // 将三维浮点数组设置到 GPU 的 shadowsShift 变量
        setFloatVec3(shadowsLocation, showdows);
    }

    // 设置中间调偏移值,并同步到 GPU
    public void setMidtones(float[] midtones) {
        this.midtones = midtones; // 更新本地变量
        setFloatVec3(midtonesLocation, midtones); // 同步到 GPU
    }

    // 设置高光偏移值,并同步到 GPU
    public void setHighlights(float[] highlights) {
        this.highlights = highlights; // 更新本地变量
        setFloatVec3(highlightsLocation, highlights); // 同步到 GPU
    }

    // 设置是否保留亮度,并同步到 GPU
    public void setPreserveLuminosity(boolean preserveLuminosity) {
        this.preserveLuminosity = preserveLuminosity; // 更新本地变量
        // 将布尔值转为 int(1/0),设置到 GPU 的 preserveLuminosity 变量
        setInteger(preserveLuminosityLocation, preserveLuminosity ? 1 : 0);
    }
}

含义

  • 每个 Setter 方法分为两步:更新本地参数 → 调用 GPUImage 框架的工具方法(setFloatVec3/setInteger)将参数同步到 GPU;
  • setFloatVec3:用于设置三维浮点数组(如 RGB 偏移);
  • setInteger:用于设置亮度保留开关(GLSL 中用 int 接收布尔逻辑)。

着色器深度解析

该着色器的设计核心分为两部分:一是实现RGB↔HSL颜色空间互转 (为"保留亮度"功能提供基础);二是通过亮度区间筛选实现分区域色彩偏移,让阴影、中间调、高光的调色相互独立,最终实现专业级的色彩平衡效果。

下面将按**「全局变量声明」→「核心工具函数」→「主函数执行逻辑」**三个核心模块逐段解析,附逐行注释+原理说明,同时讲清代码中的关键设计细节和数学逻辑。

先明确核心基础

  1. 执行规则 :片段着色器为图像的每个像素执行一次main函数,GPU并行计算,效率远高于CPU端调色;
  2. 精度限定符 :代码中highp/lowp/mediump是GLSL的精度修饰符,为移动端GPU做性能优化:
    • highp(高精度):用于纹理坐标textureCoordinate,需精准定位像素,避免偏移;
    • lowp(低精度):用于颜色、偏移值等计算,低精度足够满足视觉效果,节省GPU带宽和计算资源;
    • mediump(中精度):用于中间计算结果(如newColor),平衡精度和性能;
  3. 颜色值范围 :所有RGB/HSL值均为0.0~1.0的归一化值 (如纯红为vec3(1.0,0.0,0.0),黑色为vec3(0.0))。

完整逐行注释+模块解析

模块1:全局变量声明区

定义着色器的输入参数、可调配置、跨函数变量,是Java层与GPU层的参数交互入口,也是着色器的基础依赖。

c 复制代码
// 从顶点着色器传递的纹理坐标(经光栅化线性插值,每个像素唯一)
varying highp vec2 textureCoordinate;
// 2D纹理采样器,关联待处理的输入图像纹理ID(如相机帧、图片)
uniform sampler2D inputImageTexture;
// 阴影区域的RGB色彩偏移值(lowp vec3:r/g/b各0~1,Java层可动态调整)
uniform lowp vec3 shadowsShift;
// 中间调区域的RGB色彩偏移值(核心可调参数)
uniform lowp vec3 midtonesShift;
// 高光区域的RGB色彩偏移值(核心可调参数)
uniform lowp vec3 highlightsShift;
// 亮度保留开关(int:1=开启,0=关闭,Java层可配置)
uniform int preserveLuminosity;

核心作用

  • 接收顶点着色器的纹理坐标,实现像素精准采样;
  • 定义3个可动态调整的色彩偏移参数,分别控制阴影/中间调/高光的调色方向和强度;
  • 定义亮度保留开关,实现"仅调色彩不调亮度"的专业调色效果。
模块2:核心工具函数区

实现RGB↔HSL颜色空间互转快速亮度计算,是"保留亮度"功能的底层支撑,也是色彩平衡的关键辅助。

函数1:RGBToHSL ------ RGB转HSL颜色空间

将RGB归一化值转换为HSL(色相H/饱和度S/亮度L),HSL空间中亮度L与色相H、饱和度S相互独立,这是"保留亮度"的核心原理(只需替换H/S,保留原L即可)。

c 复制代码
// 入参:待转换的RGB颜色(vec3);返回值:转换后的HSL颜色(vec3,x=H,y=S,z=L)
lowp vec3 RGBToHSL(lowp vec3 color)
{
    lowp vec3 hsl; // 定义HSL结果向量,x=色相(0~1), y=饱和度(0~1), z=亮度(0~1)

    lowp float fmin = min(min(color.r, color.g), color.b);    // 取RGB三通道最小值(计算亮度/饱和度用)
    lowp float fmax = max(max(color.r, color.g), color.b);    // 取RGB三通道最大值(计算亮度/饱和度用)
    lowp float delta = fmax - fmin;             // RGB最大值-最小值,反映色彩的鲜艳程度(计算饱和度用)

    hsl.z = (fmax + fmin) / 2.0; // 计算亮度L:RGB最大/最小值的平均值,0=纯黑,1=纯白

    if (delta == 0.0)		// delta=0 → RGB三通道值相等,是灰度色(无色彩信息)
    {
        hsl.x = 0.0;	// 灰度色无色相,设为0
        hsl.y = 0.0;	// 灰度色饱和度为0
    }
    else                                    // delta≠0 → 有色彩信息,计算色相H和饱和度S
    {
        // 计算饱和度S:分亮度L<0.5和L≥0.5两种情况,保证S始终在0~1之间
        if (hsl.z < 0.5)
            hsl.y = delta / (fmax + fmin); // 低亮度时,饱和度=差值/(最大值+最小值)
        else
            hsl.y = delta / (2.0 - fmax - fmin); // 高亮度时,饱和度=差值/(2-最大值-最小值)

        // 计算色相偏移量:为后续按RGB最大值通道计算色相做准备
        lowp float deltaR = (((fmax - color.r) / 6.0) + (delta / 2.0)) / delta;
        lowp float deltaG = (((fmax - color.g) / 6.0) + (delta / 2.0)) / delta;
        lowp float deltaB = (((fmax - color.b) / 6.0) + (delta / 2.0)) / delta;

        // 根据RGB最大值所在通道,计算基础色相H(色相环上R/G/B各差1/3)
        if (color.r == fmax )
            hsl.x = deltaB - deltaG; // 最大值为红,基础色相从红通道计算
        else if (color.g == fmax)
            hsl.x = (1.0 / 3.0) + deltaR - deltaB; // 最大值为绿,基础色相+1/3
        else if (color.b == fmax)
            hsl.x = (2.0 / 3.0) + deltaG - deltaR; // 最大值为蓝,基础色相+2/3

        // 修正色相范围:确保H始终在0~1之间(超出则循环,如H=1.2→0.2,H=-0.1→0.9)
        if (hsl.x < 0.0)
            hsl.x += 1.0;
        else if (hsl.x > 1.0)
            hsl.x -= 1.0;
    }

    return hsl; // 返回转换后的HSL颜色
}

关键原理

  • 亮度L:采用简易亮度计算法(最大/最小值平均),而非人眼感知的亮度(如YCrCb的Y),兼顾计算效率和视觉效果;
  • 饱和度S:分亮度计算是为了避免高亮度时饱和度溢出,保证调色后色彩自然;
  • 色相H:将RGB映射到0~1的色相环,R(0/1)、G(1/3)、B(2/3)为三个基准点,实现RGB到色相的线性转换。
函数2:HueToRGB ------ 色相值转单个RGB分量

HSL转RGB的辅助函数,将色相值映射为对应的R/G/B单通道值,是HSLToRGB的核心子步骤。

c 复制代码
// 入参:f1/f2(亮度相关辅助值)、hue(待转换的色相值);返回值:对应的RGB单通道值(0~1)
lowp float HueToRGB(lowp float f1, lowp float f2, lowp float hue)
{
    // 先修正色相范围,确保在0~1之间
    if (hue < 0.0)
        hue += 1.0;
    else if (hue > 1.0)
        hue -= 1.0;
    lowp float res; // 存储转换后的RGB分量结果
    // 根据色相所在区间,线性映射为RGB分量值(色相环分6个区间,对应RGB的渐变)
    if ((6.0 * hue) < 1.0)
        res = f1 + (f2 - f1) * 6.0 * hue;
    else if ((2.0 * hue) < 1.0)
        res = f2;
    else if ((3.0 * hue) < 2.0)
        res = f1 + (f2 - f1) * ((2.0 / 3.0) - hue) * 6.0;
    else
        res = f1;
    return res; // 返回单个RGB分量值
}

核心作用

将抽象的色相值(0~1)转换为具体的RGB单通道值,解决了"HSL中色相如何映射为RGB三通道"的核心问题,保证转换后的色彩无失真。

函数3:HSLToRGB ------ HSL转RGB颜色空间

将HSL值还原为RGB归一化值,与RGBToHSL互为逆操作,用于"保留亮度"时,将「新色相+新饱和度+原亮度」的组合还原为RGB颜色。

c 复制代码
// 入参:待转换的HSL颜色(vec3);返回值:转换后的RGB颜色(vec3)
lowp vec3 HSLToRGB(lowp vec3 hsl)
{
    lowp vec3 rgb; // 定义RGB结果向量

    if (hsl.y == 0.0)
        rgb = vec3(hsl.z); // 饱和度为0 → 灰度色,RGB三通道值均等于亮度L
    else
    {
        lowp float f2; // 亮度相关辅助值,用于计算RGB分量

        // 根据亮度L计算f2,与RGBToHSL的饱和度计算逻辑对应
        if (hsl.z < 0.5)
            f2 = hsl.z * (1.0 + hsl.y);
        else
            f2 = (hsl.z + hsl.y) - (hsl.y * hsl.z);

        lowp float f1 = 2.0 * hsl.z - f2; // 辅助值,与f2配合实现色相到RGB的映射

        // 分别计算R/G/B三通道值:每个通道在色相环上偏移1/3(120°),实现RGB色彩的完整覆盖
        rgb.r = HueToRGB(f1, f2, hsl.x + (1.0/3.0));
        rgb.g = HueToRGB(f1, f2, hsl.x);
        rgb.b= HueToRGB(f1, f2, hsl.x - (1.0/3.0));
    }

    return rgb; // 返回转换后的RGB颜色
}

关键设计

R/G/B三通道分别偏移1/3色相值,对应色相环上R(0)、G(1/3)、B(2/3)的基准位置,确保任意色相值都能准确还原为对应的RGB色彩。

函数4:RGBToL ------ 快速计算RGB亮度

提取RGB颜色的亮度L,与RGBToHSL中的亮度计算逻辑完全一致,用于"保留亮度"时快速获取原图像素的亮度值,无需重复执行完整的RGBToHSL转换,提升计算效率。

复制代码
// 入参:RGB颜色(vec3);返回值:亮度L(0~1)
lowp float RGBToL(lowp vec3 color)
{
    lowp float fmin = min(min(color.r, color.g), color.b);    // 取RGB最小值
    lowp float fmax = max(max(color.r, color.g), color.b);    // 取RGB最大值

    return (fmax + fmin) / 2.0; // 返回亮度L(与RGBToHSL的L计算一致)
}
模块3:主函数main ------ 色彩平衡核心执行逻辑

着色器的入口函数,每个像素执行一次,通过「像素采样→分区域权重计算→色彩偏移叠加→亮度保留处理→颜色输出」的步骤,实现最终的色彩平衡效果,是整个着色器的核心。

c 复制代码
void main()
{
    // 步骤1:采样当前像素的RGBA颜色(根据纹理坐标从输入纹理中获取)
    lowp vec4 textureColor = texture2D(inputImageTexture, textureCoordinate);

    // 步骤2:定义亮度区域筛选的基础值(注释为备选方案:用RGBToL计算单亮度值,实际用RGB三通道值)
    //lowp vec3 lightness = RGBToL(textureColor.rgb);
    lowp vec3 lightness = textureColor.rgb;

    // 步骤3:定义色彩平衡的核心常量(固定值,控制区域筛选范围和偏移强度)
    const lowp float a = 0.25;     // 区域范围系数:控制阴影/高光/中间调的亮度区间宽度
    const lowp float b = 0.333;    // 区域中心系数:定位阴影/高光的亮度区间中心
    const lowp float scale = 0.7;  // 偏移强度系数:限制色彩偏移的最大值,避免调色过度、色彩失真

    // 步骤4:分区域计算色彩偏移权重 + 叠加偏移值
    // 4.1 阴影区域:仅对低亮度像素叠加shadowsShift偏移,高亮度像素权重为0
    lowp vec3 shadows = shadowsShift * (clamp((lightness - b) / -a + 0.5, 0.0, 1.0) * scale);
    // 4.2 中间调区域:仅对中等亮度像素叠加midtonesShift偏移,阴影/高光像素权重为0(两个clamp乘积实现区间筛选)
    lowp vec3 midtones = midtonesShift * (clamp((lightness - b) / a + 0.5, 0.0, 1.0) *
        clamp((lightness + b - 1.0) / -a + 0.5, 0.0, 1.0) * scale);
    // 4.3 高光区域:仅对高亮度像素叠加highlightsShift偏移,低亮度像素权重为0
    lowp vec3 highlights = highlightsShift * (clamp((lightness + b - 1.0) / a + 0.5, 0.0, 1.0) * scale);

    // 步骤5:叠加所有区域的色彩偏移,得到初步调整后的新颜色
    mediump vec3 newColor = textureColor.rgb + shadows + midtones + highlights;
    // 颜色溢出保护:确保RGB值始终在0~1之间(超出则截断为0或1,避免过曝/过暗)
    newColor = clamp(newColor, 0.0, 1.0);
    
    // 步骤6:亮度保留处理(核心分支逻辑)
    if (preserveLuminosity != 0) { // 开启亮度保留(preserveLuminosity=1)
        lowp vec3 newHSL = RGBToHSL(newColor); // 将新颜色转换为HSL,提取新的色相H和饱和度S
        lowp float oldLum = RGBToL(textureColor.rgb); // 获取原图像素的亮度L(保留原亮度)
        // 用「新H + 新S + 原L」重新转换为RGB,实现"仅调色彩,不调亮度"
        textureColor.rgb = HSLToRGB(vec3(newHSL.x, newHSL.y, oldLum));
        gl_FragColor = textureColor; // 输出最终颜色(保留原Alpha通道)
    } else { // 关闭亮度保留(preserveLuminosity=0)
        // 直接输出调整后的新颜色,保留原Alpha通道(textureColor.w为Alpha值)
        gl_FragColor = vec4(newColor.rgb, textureColor.w);
    }
}
主函数关键细节深度解析

主函数的核心难点是分区域亮度筛选的数学逻辑lightness的特殊设计,下面逐一讲清:

细节1:lightness = textureColor.rgb的设计巧思

代码中并未使用注释中的RGBToL(textureColor.rgb)(单亮度值),而是直接将lightness赋值为RGB三通道值(vec3),这是该着色器的核心设计亮点

  • 若用单亮度值,所有颜色通道会共享同一个区域权重(如红色暗部和绿色暗部的阴影偏移权重相同);
  • 用RGB三通道值作为lightness每个颜色通道会独立计算区域权重 (如红色暗部的阴影偏移权重≠绿色暗部的权重),实现通道级的精细色彩平衡,调色效果更细腻、更贴合专业调色工具的逻辑。
细节2:分区域亮度筛选的数学逻辑(核心)

通过clamp函数结合线性公式,实现阴影/中间调/高光的自动区间筛选 ,核心原理是:仅当像素亮度在目标区间时,计算出的权重为0~1的非0值,否则为0,最终实现"仅目标区域叠加色彩偏移"。

阴影区域为例,公式为:

glsl 复制代码
clamp((lightness - b) / -a + 0.5, 0.0, 1.0)
  • 当像素为低亮度 (如lightness=0.1):lightness - b = -0.233,除以-a(-0.25)0.932,加0.51.432clamp后为1.0,阴影权重为1,完全叠加shadowsShift
  • 当像素为中/高亮度 (如lightness=0.5):lightness - b = 0.167,除以-a-0.668,加0.5-0.168clamp后为0.0,阴影权重为0,不叠加shadowsShift

高光区域 公式逻辑与阴影相反,仅高亮度像素会得到非0权重;中间调区域 通过两个clamp的乘积 实现筛选------只有中等亮度像素能同时满足两个clamp的非0条件,最终得到非0权重,完美实现三个区域的互斥独立调色

细节3:scale=0.7的作用

scale是偏移强度限制系数,将色彩偏移的最大权重限制为0.7,避免因偏移值过大导致调色过度(如纯红叠加红色偏移后变成过曝的红),保证调色后的色彩自然、无失真。

细节4:颜色溢出保护(clamp(newColor, 0.0, 1.0))

色彩偏移叠加后,RGB值可能超出01的归一化范围(如`1.2`或`-0.1`),GPU会将超出范围的值直接截断,导致**色彩失真、过曝/过暗**,通过`clamp`函数强制将值限制在01之间,从根源避免该问题。

核心亮点与应用场景

核心亮点

  1. 分区域独立调色:阴影、中间调、高光三个区域互斥独立,可分别调整RGB偏移,实现专业级色彩平衡;
  2. 通道级精细调色:lightness采用RGB三通道值,每个颜色通道独立计算区域权重,调色效果更细腻;
  3. 亮度保留功能:基于RGB↔HSL互转,实现"仅调色相/饱和度,不改变亮度",避免调色后图像过曝/过暗;
  4. GPU并行加速:所有计算在GPU端完成,效率远高于CPU,支持相机实时预览、短视频实时调色等高性能场景;
  5. 色彩保护机制:通过scale系数和clamp函数,避免调色过度和颜色溢出,保证色彩自然;
  6. 参数可动态配置:阴影/中间调/高光偏移值、亮度保留开关均可由Java层动态调整,适配不同调色需求。

典型应用场景

  1. 图片编辑:如修图软件中的"色彩平衡"功能,调整人像/风景的阴影偏冷、高光偏暖等;
  2. 相机实时滤镜:直播、拍照时的实时色彩调整,实现个性化调色;
  3. 短视频后期:短视频的批量色彩平衡处理,提升画面质感;
  4. 影视级调色:基础色彩校正,如修复画面偏色、调整明暗区域的色彩氛围。

代码小细节说明

代码中有一处注释备选方案,无功能影响,仅为设计思路参考:

  • 注释中的lowp vec3 lightness = RGBToL(textureColor.rgb);是单亮度值方案,调色效率更高,但效果较粗糙;
  • 实际使用的lowp vec3 lightness = textureColor.rgb;是通道级方案,调色效果更细腻,是框架的最终选择。

该着色器是GPUImage框架中色彩调整类滤镜的经典实现,其分区域调色、亮度保留的设计思路,也被广泛应用于其他图像编辑框架(如Glide、FFmpeg)的色彩平衡功能中。

实际使用示例

1. 依赖准备

确保项目中引入 GPUImage 框架(如通过 Gradle):

gradle 复制代码
dependencies {
    implementation 'jp.co.cyberagent.android:gpuimage:2.1.0'
}

2. 代码使用示例

java 复制代码
// 1. 创建 GPUImage 实例(关联 SurfaceView/TextureView 用于显示)
GPUImage gpuImage = new GPUImage(context);
gpuImage.setGLSurfaceView((GLSurfaceView) findViewById(R.id.gpu_surface_view));

// 2. 加载待处理的图像(Bitmap 或 Uri)
Bitmap sourceBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test_image);
gpuImage.setImage(sourceBitmap);

// 3. 创建色彩平衡滤镜实例
GPUImageColorBalanceFilter colorBalanceFilter = new GPUImageColorBalanceFilter();

// 4. 设置滤镜参数(核心步骤)
// 示例:调整阴影偏红、中间调偏绿、高光偏蓝,关闭亮度保留
float[] shadowsShift = new float[]{0.2f, 0.0f, 0.0f}; // 阴影红通道 +0.2
float[] midtonesShift = new float[]{0.0f, 0.2f, 0.0f}; // 中间调绿通道 +0.2
float[] highlightsShift = new float[]{0.0f, 0.0f, 0.2f}; // 高光蓝通道 +0.2
colorBalanceFilter.setShowdows(shadowsShift); // 设置阴影偏移(注意变量名拼写)
colorBalanceFilter.setMidtones(midtonesShift); // 设置中间调偏移
colorBalanceFilter.setHighlights(highlightsShift); // 设置高光偏移
colorBalanceFilter.setPreserveLuminosity(false); // 关闭亮度保留

// 5. 应用滤镜到 GPUImage
gpuImage.setFilter(colorBalanceFilter);

// 6. (可选)获取处理后的 Bitmap
Bitmap filteredBitmap = gpuImage.getBitmapWithFilterApplied();

3. 参数调整说明

  • 色彩偏移值范围 :建议在 -1.0 ~ 1.0 之间(超出范围会被 clamp 修正为 0~1);
  • 保留亮度(preserveLuminosity)
    • true:仅调整色相/饱和度,图像整体亮度不变(适合人像、风景调色,避免过曝/过暗);
    • false:直接叠加色彩偏移,亮度会随偏移值变化(适合创意调色);
  • 阴影/中间调/高光区分 :通过亮度区间(由常量 a=0.25b=0.333 控制),无需手动指定区间,算法自动筛选。

总结

  1. 色彩平衡调整:分阴影、中间调、高光三个区域独立调整 RGB 偏移,精准控制图像不同亮度区域的色彩;
  2. 亮度保留:基于 RGB↔HSL 颜色空间转换,确保调色后亮度与原图一致,提升调色自然度;
  3. GPU 加速:所有计算在 GPU 端完成,相比 CPU 处理更高效,适合实时预览(如相机滤镜)。
相关推荐
格林威2 小时前
Baumer相机铁轨表面裂纹巡检:提升铁路安全监测能力的 7 个关键技术,附 OpenCV+Halcon 实战代码!
人工智能·数码相机·opencv·算法·安全·计算机视觉·分类
广药门徒2 小时前
WS2812_CONTROL使用手册
android·java·数据库
云游云记2 小时前
php自动加载
android·php·android studio
警醒与鞭策2 小时前
Cursor Agent Skill 原理及LLM , Agent, MCP ,Skill区别
android·unity·ai·cursor
TheNextByte13 小时前
如何将通话记录从Android传输到PC
android
灵感菇_3 小时前
Android Fragment全面解析
android·生命周期·fragment
web_Hsir3 小时前
uniapp + vue2 + pfdjs + web-view 实现安卓、iOS App PDF预览
android·前端·uni-app
一起养小猫3 小时前
Flutter for OpenHarmony 实战:Container与Padding布局完全指南
android·flutter·harmonyos
HeDongDong-3 小时前
详解Kotlin的各种类(使用场景导向)
android·开发语言·kotlin