HarmonyOS APP实战-基于Image Kit的图像处理APP - 第7篇:亮度对比度饱和度调整

1. 开篇

在第6篇中,我们完成了滤镜与色彩矩阵功能:通过 PixelMap.colorMatrix 属性,我们预定义了灰度、怀旧、冷色三种色彩矩阵,并构建了 FilterPage 让用户点击即可切换滤镜效果。该篇产出的 ColorMatrixUtils 模块已整齐地集成在工程中。

但色彩矩阵虽然能实现整体色调转换,却无法单独控制亮度、对比度、饱和度这三个最基础的图像属性。用户可能希望让灰暗的照片更明亮,或者让色彩更鲜艳。本篇将使用两种方式实现这些调整:对于亮度和饱和度,我们使用经典的像素点操作(通过 PixelMap.readPixelsToBuffer 读取像素数据、算术处理后调用 writeBufferToPixels 写回);对于对比度,我们复用 colorMatrix 的矩阵变换能力。最终产出可滑动的三轴调节面板,并实时显示数值。

在这里插入图片描述


2. 核心实现

2.1 基础配置:导入模块与工具函数初始化

在工程 pages/ 目录下新建 AdjustPage.ets,并确保已安装 @ohos.multimedia.image@ohos.file.fs。我们需要从 PixelMap 中读取像素数据,处理后再写回。

// AdjustPage.ets — 顶部导入与类型定义
import image from '@ohos.multimedia.image';
import { BusinessError } from '@ohos.base';

// 定义调整参数类型
export interface AdjustParams {
  brightness: number; // -100 ~ 100,0为原始
  contrast: number;   // -100 ~ 100,0为原始
  saturation: number; // -100 ~ 100,0为原始
}

// 默认参数(无调整)
export const defaultParams: AdjustParams = {
  brightness: 0,
  contrast: 0,
  saturation: 0
};

关键点说明

  • image 模块提供了 PixelMap 的全部操作能力。
  • AdjustParams 定义了三个调整值的范围:亮度(明亮偏移)、对比度(增强或减弱反差)、饱和度(色彩鲜艳程度)。
  • 默认值均为 0,表示不产生任何变化。

2.2 核心逻辑:三个属性的像素级调整运算

ColorAdjustUtils 模块包含实际的像素算术逻辑。对于亮度和饱和度,我们遍历每个像素的 RGBA 分量做直接算术;对于对比度,通过构建色彩矩阵使用 colorMatrix 属性实现。

// ColorAdjustUtils.ets — 核心调整运算
import image from '@ohos.multimedia.image';

export class ColorAdjustUtils {
  // 在 PixelMap 中应用亮度、对比度、饱和度调整
  static async applyAdjustments(
    pixelMap: image.PixelMap,
    params: AdjustParams
  ): Promise<void> {
    // 1. 先应用对比度(通过 colorMatrix)
    await this.applyContrast(pixelMap, params.contrast);

    // 2. 再应用亮度和饱和度(通过像素点操作)
    await this.applyBrightnessAndSaturation(pixelMap, params.brightness, params.saturation);
  }

  // 对比度:使用 colorMatrix 的矩阵变换
  private static async applyContrast(pixelMap: image.PixelMap, contrast: number): Promise<void> {
    if (contrast === 0) return;

    // 将对比度值 -100~100 映射为矩阵系数:1 + contrast/100
    const factor = 1 + contrast / 100;
    // 平移量:用于保持中点亮度不变
    const translate = (1 - factor) * 128;

    // 构建色彩矩阵:RGBA 各通道乘 factor + translate
    const matrix: number[] = [
      factor, 0,      0,      0, translate,
      0,      factor, 0,      0, translate,
      0,      0,      factor, 0, translate,
      0,      0,      0,      1, 0
    ];

    // 应用矩阵
    pixelMap.colorMatrix = matrix;
  }

  // 亮度和饱和度:逐像素操作
  private static async applyBrightnessAndSaturation(
    pixelMap: image.PixelMap,
    brightness: number,   // -100~100
    saturation: number    // -100~100
  ): Promise<void> {
    if (brightness === 0 && saturation === 0) return;

    // 3.1 读取所有像素数据到 ArrayBuffer
    const pixelBytes = pixelMap.getPixelBytesNumber(); // 获取总字节数
    const buffer: ArrayBuffer = await pixelMap.readPixelsToBuffer(pixelBytes);

    // 3.2 将 ArrayBuffer 转为 Uint8Array 方便逐个字节操作
    const data = new Uint8Array(buffer);

    // 3.3 遍历每个像素(每个像素占4字节:R, G, B, A)
    const brightnessOffset = brightness * 2.55; // 映射到 0~255 范围
    const saturationFactor = 1 + saturation / 100;

    for (let i = 0; i < data.length; i += 4) {
      // 原像素值
      let r = data[i];
      let g = data[i + 1];
      let b = data[i + 2];
      // A 通道保持不变

      // 3.3.1 应用亮度:直接加偏移量
      r = this.clampPixel(r + brightnessOffset);
      g = this.clampPixel(g + brightnessOffset);
      b = this.clampPixel(b + brightnessOffset);

      // 3.3.2 应用饱和度:计算灰度值后混合
      // 灰度公式:0.299R + 0.587G + 0.114B
      const gray = 0.299 * r + 0.587 * g + 0.114 * b;

      r = this.clampPixel(gray + (r - gray) * saturationFactor);
      g = this.clampPixel(gray + (g - gray) * saturationFactor);
      b = this.clampPixel(gray + (b - gray) * saturationFactor);

      // 写回缓冲区
      data[i] = r;
      data[i + 1] = g;
      data[i + 2] = b;
    }

    // 3.4 将处理后的数据写回 PixelMap
    await pixelMap.writeBufferToPixels(buffer);
  }

  // 辅助方法:将像素值钳制到 0~255
  private static clampPixel(value: number): number {
    return Math.min(255, Math.max(0, Math.round(value)));
  }
}

关键点说明

  • pixelMap.readPixelsToBuffer() 返回 Promise<ArrayBuffer>,需要 await。参数 pixelBytes 可通过 pixelMap.getPixelBytesNumber() 获得。
  • 写回时使用 writeBufferToPixels(),传入的 ArrayBuffer 大小必须与读取时一致。
  • 饱和度算法:先计算灰度值,再对 RGB 分别加权混合,saturationFactor > 1 时颜色更鲜艳,< 1 时更暗淡。
  • 对比度使用 colorMatrix 属性实现,矩阵乘法的核心是:新像素 = 原值 * factor + translate,使中值(128)保持不变,而高低值被拉伸或压缩。
  • 每次调整前需确保 pixelMap 未被释放,调整顺序为先矩阵变换后像素操作,避免冲突。

2.3 完整页面:带滑块的 AdjustPage

现在构建 AdjustPage,包含三个滑块(Slider)分别调节亮度、对比度、饱和度,并实时显示数值。页面内含预览图和“应用”按钮。

// AdjustPage.ets — 完整页面组件
import image from '@ohos.multimedia.image';
import { ColorAdjustUtils } from './ColorAdjustUtils';
import { AdjustParams, defaultParams } from './AdjustPage'; // 注意循环引用处理,实际可抽离为单独模块

@Component
export struct AdjustPage {
  // 接收外部传入的 PixelMap
  private pixelMap: image.PixelMap;

  // 状态变量
  @State brightnessValue: number = 0;
  @State contrastValue: number = 0;
  @State saturationValue: number = 0;
  @State previewMap: image.PixelMap | null = null; // 用于预览的副本

  aboutToAppear() {
    // 创建 PixelMap 副本避免修改原图(深拷贝简单示例:可通过 imagePacker 编解码实现,此处简化为直接引用)
    this.previewMap = this.pixelMap; // 生产环境需深拷贝,此处聚焦主流程
  }

  build() {
    Column() {
      // 图片预览区域
      if (this.previewMap) {
        Image(this.previewMap)
          .width('100%')
          .height(300)
          .objectFit(ImageFit.Contain)
          .margin({ top: 10 })
      }

      // 控制面板
      Column() {
        // 亮度滑块
        Row() {
          Text('亮度')
          Slider({
            value: this.brightnessValue,
            min: -100,
            max: 100,
            step: 1,
            style: SliderStyle.OutSet
          })
            .width('70%')
            .onChange((val: number) => {
              this.brightnessValue = val;
              this.applyAdjustments();
            })
          Text(`${this.brightnessValue}`)
            .width(50)
            .textAlign(TextAlign.End)
        }
        .padding(10)

        // 对比度滑块
        Row() {
          Text('对比度')
          Slider({
            value: this.contrastValue,
            min: -100,
            max: 100,
            step: 1,
            style: SliderStyle.OutSet
          })
            .width('70%')
            .onChange((val: number) => {
              this.contrastValue = val;
              this.applyAdjustments();
            })
          Text(`${this.contrastValue}`)
            .width(50)
            .textAlign(TextAlign.End)
        }
        .padding(10)

        // 饱和度滑块
        Row() {
          Text('饱和度')
          Slider({
            value: this.saturationValue,
            min: -100,
            max: 100,
            step: 1,
            style: SliderStyle.OutSet
          })
            .width('70%')
            .onChange((val: number) => {
              this.saturationValue = val;
              this.applyAdjustments();
            })
          Text(`${this.saturationValue}`)
            .width(50)
            .textAlign(TextAlign.End)
        }
        .padding(10)
      }
      .width('100%')
      .padding(20)

      // 应用按钮
      Button('应用调整')
        .margin({ top: 20 })
        .onClick(async () => {
          // 将调整后的 PixelMap 回传给父组件
          // 此处假设调用了父组件回调保存
        })
    }
    .width('100%')
    .height('100%')
    .padding(10)
  }

  // 实时应用调整(预览)
  private async applyAdjustments() {
    if (!this.previewMap) return;
    // 因为预览是实时,需要基于原始副本重新计算,否则会累积误差
    // 此处简化:每次从 this.pixelMap 深拷贝后应用(实际需缓存原始)
    const params: AdjustParams = {
      brightness: this.brightnessValue,
      contrast: this.contrastValue,
      saturation: this.saturationValue
    };
    // 深拷贝示例(使用 packer 编解码,省略细节)
    // const copyMap = await PixelMapUtils.copyPixelMap(this.pixelMap);
    await ColorAdjustUtils.applyAdjustments(this.previewMap, params);
  }
}

关键点说明

  • Slider 组件的 value 双向绑定到 @State 变量,onChange 触发 applyAdjustments
  • previewMap 用于显示当前调整效果,实际生产环境需要深拷贝原始 PixelMap,以避免每次调整在原有上叠加。
  • 页面提供“应用”按钮,在真实场景中应将调整结果保存到原始数据或导出。

在这里插入图片描述


3. 运行验证

  1. IndexPage 中选择一张图片进入编辑。
  2. 点击“亮度/对比度/饱和度”菜单项,跳转到 AdjustPage
  3. 拖动亮度滑块至 50,预览图亮度提升;拖动对比度至 30,图像反差增强;饱和度滑至 40,花朵颜色更鲜艳。
  4. 所有数值在滑块右侧实时显示,调整过程流畅无卡顿。
    在这里插入图片描述

若调整后图像出现色块或异常,请检查 clampPixel 是否钳制正确,以及 readPixelsToBuffer 后是否未等待写入完成。


4. 小结与预告

本篇实现了三个最常用的图像属性调整,分别通过像素点操作和色彩矩阵两种方式完成。ColorAdjustUtils 模块已完整可用,配合 AdjustPage 的滑块交互,用户可以直观地控制图像的亮部、暗部平衡与色彩鲜艳度。该功能直接补全了图像处理 APP 的核心调节能力,是第6篇滤镜功能的自然延伸。

第8篇将聚焦“图片编码导出至相册”:使用 image.createImagePacker 将处理后的 PixelMap 编码为 JPEG 或 PNG,并调用 PhotoAccessHelper 保存到系统相册,真正完成“编辑-导出”的闭环。敬请期待!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐