想从照片里提取主题色?ColorPicker 帮你搞定

你有没有注意过,很多 APP 的界面颜色会跟着当前显示的图片变?比如音乐播放器的背景色会随着专辑封面变化,天气 APP 的配色会跟着风景照片走。这种"从图片里提取主色调"的效果,在 HarmonyOS 里用 effectKitColorPicker 就能实现。

上一篇我们聊了 Filter(滤镜链),今天我们来看看 effectKit 的另一个主角——ColorPicker(智能取色器)。

ColorPicker 能干什么?

简单说,ColorPicker 可以从一张图片里"看出"它的主要颜色。它提供了好几种取色方式:

  • getMainColor:提取图片的"主色",就是把整张图片缩小到 1 个像素,那个像素的颜色。
  • getLargestProportionColor:提取占比最多的颜色。
  • getTopProportionColors:提取占比最多的前 N 种颜色。
  • getHighestSaturationColor:提取饱和度最高的颜色(最鲜艳的那个)。
  • getAverageColor:提取平均颜色。
  • isBlackOrWhiteOrGrayColor:判断某个颜色是不是黑白灰。

这些方法各有各的用处,我们一个一个来看。

下面是 ColorPicker 的整体使用流程:

读取图片

创建 PixelMap

createColorPicker 创建取色器

选择取色方式

getMainColor: 提取主色

getTopProportionColors: 提取前N种颜色

getHighestSaturationColor: 最鲜艳颜色

getAverageColor: 平均颜色

获取 Color 对象

将颜色用于 UI 主题/背景等

创建 ColorPicker

和 Filter 一样,ColorPicker 也需要先有一个 PixelMap。创建方式也很像:

import { image } from "@kit.ImageKit";
import { effectKit } from "@kit.ArkGraphics2D";
import { BusinessError } from "@kit.BasicServicesKit";

const color = new ArrayBuffer(96);
let opts: image.InitializationOptions = {
  editable: true,
  pixelFormat: 3,
  size: {
    height: 4,
    width: 6
  }
}

image.createPixelMap(color, opts).then((pixelMap) => {
  effectKit.createColorPicker(pixelMap).then(colorPicker => {
    console.info("Succeeded in creating colorPicker.");
  }).catch((err: BusinessError) => {
    console.error(`Failed to create colorPicker. Code: ${err.code}, message: ${err.message}`);
  })
})

这里有几个细节值得说一下:

  1. createColorPicker 返回的是 Promise:和 createEffect(返回 Filter)不同,创建 ColorPicker 是异步的。可能是因为取色算法需要一些计算时间,所以设计成了异步接口。

  2. 错误处理:既然是 Promise,就有 reject 的可能。记得用 .catch 或者 try/catch 捕获错误。错误码 401 表示参数错误(比如传了个空的 PixelMap 进去)。

  3. 两种回调风格:除了 Promise,还可以用 callback 风格:

    effectKit.createColorPicker(pixelMap, (error, colorPicker) => {
      if (error) {
        console.error('Failed to create color picker.');
      } else {
        console.info('Succeeded in creating color picker.');
      }
    })
    

    两种写法效果一样,看你喜欢哪种。

指定取色区域(API 10+)

有时候你不想从整张图片取色,只想取某个区域的。比如用户头像一般在图片的中心位置,你只想从中心区域取色。这时候可以用带 region 参数的版本:

effectKit.createColorPicker(pixelMap, [0, 0, 1, 1]).then(colorPicker => {
  console.info("Succeeded in creating colorPicker.");
})

region 是一个包含 4 个数字的数组,分别表示左、上、右、下的位置,取值范围 [0, 1]

打个比方:

  • [0, 0, 1, 1] — 整张图片(默认值)
  • [0.25, 0.25, 0.75, 0.75] — 图片中心区域(从 25% 到 75%)
  • [0, 0, 0.5, 1] — 图片左半边
  • [0, 0, 1, 0.5] — 图片上半边

注意:第三个值必须大于第一个(右 > 左),第四个必须大于第二个(下 > 上),不然会报 401 参数错误。

getMainColor:最常用的取色方式

colorPicker.getMainColor().then(color => {
  console.info(`color[ARGB]=${color.alpha},${color.red},${color.green},${color.blue}`);
})

getMainColor 的原理是:通过图像缩放算法,根据周围像素的加权计算,把整张图片缩小到 1 个像素。这 1 个像素的颜色就是"主色"。

你可以把它想象成:把一张照片缩小到 1x1 的缩略图,那个缩略图的颜色就是主色。这其实也是很多"提取主题色"算法的基础思路。

返回的 Color 对象有四个属性:

  • alpha:透明度,[0, 0xFF]
  • red:红色分量,[0, 0xFF]
  • green:绿色分量,[0, 0xFF]
  • blue:蓝色分量,[0, 0xFF]

如果你想要同步版本(不返回 Promise),可以用 getMainColorSync()

let color = colorPicker.getMainColorSync();
console.info('get main color =' + color);

同步版本会在当前线程直接计算,如果图片很大可能会卡一下。一般建议用异步版本。

getLargestProportionColor:占比最多的颜色

let color = colorPicker.getLargestProportionColor();
console.info('get largest proportion color =' + color);

这个方法使用中位切分算法(Median Cut)来划分颜色空间。简单说就是:把图片里所有颜色按 RGB 值分成很多组,每组里颜色数量差不多,然后取数量最多的那组的平均颜色。

getMainColor 的区别是什么?getMainColor 是"加权平均",可能得到一个"不存在"的颜色(比如图片全是红色和蓝色,主色可能是紫色)。而 getLargestProportionColor 返回的是"真实存在"的颜色——图片里确实有大量像素是这个颜色。

getTopProportionColors:取前 N 种颜色

let colors = colorPicker.getTopProportionColors(2);
for (let index = 0; index < colors.length; index++) {
  if (colors[index]) {
    console.info('get top proportion colors: index ' + index + ', color ' + colors[index]);
  }
}

colorCount 参数指定你要取几种颜色。返回一个 Color 数组,按占比从高到低排序。

这个方法很适合做"主题色卡片"——展示一张图片的 3-5 种主要颜色,让用户选择用哪个做主题色。

注意:colorCount 的取值范围是 [1, 20](HarmonyOS 6.1.0 之前是 [1, 10])。如果你传的值比实际颜色种类多,返回的数组会比你要求的短——有多少返回多少。如果取色失败或者传了小于 1 的值,返回 [null]

getHighestSaturationColor:最鲜艳的颜色

let color = colorPicker.getHighestSaturationColor();
console.info('get highest saturation color =' + color);

饱和度是什么?你可以理解为"颜色的鲜艳程度"。纯红色饱和度很高,灰色饱和度为 0。这个方法会找到图片里最鲜艳的那个颜色。

适合什么场景?比如你在做一个"提取强调色"的功能——APP 里需要一个醒目的颜色来高亮按钮或标签,用饱和度最高的颜色就很合适。

getAverageColor:平均颜色

let color = colorPicker.getAverageColor();
console.info('get average color =' + color);

这个最直白——把所有像素的 RGBA 值各自加起来,除以像素总数,得到平均值。

getMainColor 有什么区别?getMainColor 是缩放算法(考虑了像素的位置权重),getAverageColor 是简单平均(每个像素权重一样)。结果可能很接近,但不完全相同。

isBlackOrWhiteOrGrayColor:判断黑白灰

let bJudge = colorPicker.isBlackOrWhiteOrGrayColor(0xFFFFFFFF);
console.info('is black or white or gray color[bool](white) =' + bJudge);

这个方法接收一个颜色值(number 类型,范围 [0, 0xFFFFFFFF]),返回 truefalse,告诉你这个颜色是不是黑白灰。

什么时候用?比如你在做一个"自动适配深色模式"的功能——如果图片的主色是黑色或深灰,前景文字就用白色;如果是白色或浅灰,前景文字就用黑色。先用 isBlackOrWhiteOrGrayColor 判断一下,再决定用什么颜色。

完整示例:从照片提取主题色

面对不同的业务场景,如何选择合适的取色方式?可以参考下面的决策流程:

通用主色调

需要真实存在的颜色

展示多种主题色供选择

需要醒目的强调色

需要整体平均色调

需要从图片提取颜色

需要什么类型的颜色?

getMainColor

getLargestProportionColor

getTopProportionColors

getHighestSaturationColor

getAverageColor

结果是否为黑白灰?

使用该颜色

好,来一个完整的例子。我们读取一张图片,提取它的主色,然后用这个颜色作为页面背景:

import { image } from "@kit.ImageKit";
import { effectKit } from "@kit.ArkGraphics2D";
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  @State mainColor: string = '#FFFFFF';
  @State topColors: string[] = [];

  async aboutToAppear(): Promise<void> {
    const context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    const fileData: Uint8Array = await context.resourceManager.getRawFileContent('image.png');
    const buffer: ArrayBuffer = fileData.buffer.slice(0);

    let imageSource = image.createImageSource(buffer);
    let pixelMap = await imageSource.createPixelMap();

    // 提取主色
    effectKit.createColorPicker(pixelMap).then(colorPicker => {
      let color = colorPicker.getMainColorSync();
      this.mainColor = `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.alpha / 255})`;

      // 提取前 3 种主要颜色
      let colors = colorPicker.getTopProportionColors(3);
      this.topColors = colors
        .filter((c): c is effectKit.Color => c !== null)
        .map(c => `rgb(${c.red}, ${c.green}, ${c.blue})`);
    });
  }

  build() {
    Column() {
      Text('主题色展示')
        .fontSize(24)
        .fontColor('#FFFFFF')
        .margin({ bottom: 20 })

      // 展示主色
      Row() {
        Text('主色:')
          .fontSize(16)
        Rectangle()
          .width(60)
          .height(60)
          .fill(this.mainColor)
      }
      .margin({ bottom: 20 })

      // 展示前 N 种颜色
      Row() {
        ForEach(this.topColors, (color: string) => {
          Rectangle()
            .width(40)
            .height(40)
            .fill(color)
            .margin(5)
        })
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor(this.mainColor)
  }
}

这段代码做了什么:

  1. rawfile 读取图片,创建 PixelMap。
  2. createColorPicker 创建取色器。
  3. getMainColorSync 获取主色,转换成 CSS 颜色字符串。
  4. getTopProportionColors(3) 获取前 3 种主要颜色。
  5. 在页面上展示这些颜色,并把主色设为背景色。

几个实用技巧

1. 先判断颜色类型再决定用法

如果你提取出来的主色是黑白灰(用 isBlackOrWhiteOrGrayColor 判断),可能不太适合作为主题色——太素了。这时候可以改用 getHighestSaturationColor,取最鲜艳的颜色。

2. 取色区域很重要

一张风景照,天空可能占了大半,如果你从整张图取色,主色可能是天空的蓝色。但你真正想要的可能是前景建筑的颜色。这时候用 region 参数缩小取色范围,效果会好很多。

3. 取色结果可以缓存

getMainColor 这类方法每次调用都会重新计算。如果图片没变,结果也不会变,所以可以把结果缓存起来,避免重复计算。

4. 注意线程安全

ColorPicker 的方法都在当前线程执行,如果图片很大,计算可能比较耗时。建议在子线程或者用 Promise 异步处理,不要在 UI 线程同步调用。

小结

ColorPicker 提供了多种取色方式,适合不同的场景:

方法 适用场景
getMainColor 通用取色,适合大多数情况
getLargestProportionColor 需要"真实存在"的颜色
getTopProportionColors 展示多种主题色供用户选择
getHighestSaturationColor 提取最鲜艳的强调色
getAverageColor 需要整体色调的平均值
isBlackOrWhiteOrGrayColor 判断颜色类型,辅助决策

配合 region 参数指定取色区域,基本能满足各种"从图片提取颜色"的需求。

下一篇我们进入 ArkGraphics 2D 的 drawing 模块,看看怎么用 Canvas、Brush、Pen 来画图——这才是真正的 2D 绘制能力。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐