LEColorPicker取色原理全景图:从32×32缩放像素到YUV投票的完整算法流程
【免费下载链接】LEColorPickerA Cocoa-Touch system for iOS to get a color scheme in function of an image, like iTunes 11 does.项目地址: https://gitcode.com/gh_mirrors/le/LEColorPicker
LEColorPicker 是一款面向 iOS 的开源取色库,它能像 iTunes 11 一样,从一张图片中自动提取出背景色、主文字色和次文字色,生成完整的配色方案。本文将用尽量通俗的方式,完整拆解它的核心算法:图片如何被缩放到 32×32、像素如何上传 GPU、YUV 空间中的"投票"如何找出主导色,以及文字色是如何经过对比度过滤最终得到的。
LEColorPicker 是什么:一套图片驱动取色方案
它的目标很单一也很实用:给定任意一张UIImage,输出一组 LEColorScheme 对象,其中包含三个属性:
| 属性 | 含义 | 典型用途 |
|---|---|---|
backgroundColor | 图片的主导色(背景色) | 页面底色 |
primaryTextColor | 与背景反差最强且票最高的文字色 | 标题文字 |
secondaryTextColor | 第二反差色,不足时回退为纯黑/纯白 | 正文文字 |
核心实现集中在 LEColorPicker/LEColorPicker.m,YUV 颜色空间的数学工具在 LEColorPicker/UIColor+YUVSpace.m,示例调用在 LEColorPickerDemo/ViewController.m。
完整算法流程:从输入图片到三个颜色的四步流水线
整体流程可以概括为:缩放 → GPU 投票渲染 → 读回像素 → 过滤文字色。
第一步:把任意图片缩放为 32×32 方形
在 colorSchemeFromImage: 中,无论原图是 1080P 还是手机壁纸,第一步都是压成 32×32 的方形小图(宏LECOLORPICKER_GPU_DEFAULT_SCALED_SIZE,见 LEColorPicker.m 第 24 行)。
这一步的意义在于:取色本质是统计问题,32×32 已经足够代表画面的"色彩构成",而后续计算量与像素数成正比——从百万级像素降到 1024 个,这是性能的关键。缩放实现很朴素,就是 Core Graphics 的drawInRect重绘,见 scaleImage:width:height:。
第二步:OpenGL ES 上传纹理并离屏渲染
缩放后的小图被逐像素转成 RGBA 字节流,通过glTexImage2D上传为 GPU 纹理,采样过滤设为GL_NEAREST(不做插值,保证投票结果不被"糊"掉),见 setupTextureFromImage:。同时项目创建了一个 32×32 的离屏 Framebuffer 和深度缓冲(setupOpenGL),整个渲染不经过屏幕。
渲染时只画一个覆盖全屏的四边形,真正的计算全部在片元着色器里完成(renderDominant)。注意一个细节:着色器只处理 32×32 的左上 16×16 区域(ProccesedWidth = 32/2),其余部分输出纯黑,这是为了把算力集中在有效区域。
第三步:片段着色器中的 YUV 投票——主导色提取的核心
这是整个取色算法的灵魂。对左上 16×16 区域里的每一个像素,着色器做两件事:
- RGB 转 YUV:采用电视标准 BT.601 的权重,例如亮度分量为
Y = 0.299R + 0.587G + 0.114B,完整的三通道转换见 kDominantFragmentShaderString 以及 CPU 侧的 UIColor+YUVSpace.m。 - 遍历全部 256 个像素"拉票":当前像素与区域内每个采样点计算 YUV 空间中的欧氏距离
d,若d < 0.1就记一票(累加 0.0039,256 票恰好累加到 1.0)。
最后输出gl_FragColor = vec4(R, G, B, 票数)——RGB 保留原色,Alpha 通道被巧妙地借用来携带"得票数"。票数越高的颜色,说明画面中越多像素与它相似,也就是我们直觉中的"主导色"。
第四步:CPU 读回像素,筛出两个文字色
渲染完成后,colorWithBiggerCountFromImageWidth:height: 通过glReadPixels把 32×32 的 RGBA 缓冲读回 CPU,遍历 16×16 区域,Alpha(票数)最大的像素的 RGB 值就是背景色。
接着 findTextColorsTaskForColorScheme: 在同一份缓冲上做两轮"排除法投票":
| 颜色 | 筛选条件(RGB 平方距离) |
|---|---|
| 主文字色 | 与背景色距离 >(255 × 0.5)²,在幸存者中取票数最高 |
| 次文字色 | 与背景距离 >(255 × 0.5)²且与主文字色距离 >(255 × 0.3)²,取票数最高 |
这里用到的容差宏定义在 文件头部的 Preprocessor 区。最后还有一道"可读性保险"(isSufficienteContrastBetweenBackground:andForground:):用 YUV 的亮度分量 Y 判断背景与次文字色的亮度差,乘以 255 后需 ≥ 125,且 RGB 各通道极差之和 > 500;不达标时直接回退——背景偏暗就选白色文字,偏亮就选黑色文字。
为什么选 YUV 投票而不是 RGB 距离?
这是本文最重要的概念点。RGB 空间里"看起来一样"的颜色(如浅灰与淡蓝)坐标距离可能很远,而人眼感知的相似性更接近亮度+色度分离后的距离。YUV 把颜色拆成:
- Y(亮度):0.299R + 0.587G + 0.114B,决定"深浅"
- U、V(色度):决定"冷暖色调"
在 YUV 空间用 0.1 的距离阈值投票,统计出来的主导色更符合视觉直觉;同时 GPU 对这种逐像素的向量运算有天然加速,1024 次"两两比较"(16×16 × 16×16 = 65536 次距离计算)在着色器里一帧即可完成。这套思路借鉴自社区用 Mathematica 复现 iTunes 11 取色逻辑的经典方案(见 README.md 致谢部分)。
如何在项目中调用 LEColorPicker
使用极其简单,三步走:
- 工程中加入 OpenGLES 框架,把
LEColorPicker文件夹拖入项目; - 创建实例并同步取色:
LEColorScheme *scheme = [colorPicker colorSchemeFromImage:image]; - 把三个属性赋给视图颜色,例如 ViewController.m 的 configureOutPutView: 就是这样做的。
如果不想阻塞当前线程,用异步接口 pickColorsFromImage:onComplete:,它会在一条低优先级串行 GCD 队列上完成全部计算,结果回主线程回调;App 切到后台时还会自动挂起该队列(appDidEnterBackground)。旧版类方法 pickColorFromImage:onComplete: 内部也是这个实现,返回字典而非对象。
关键参数速查表与性能要点
| 参数 | 值 | 位置 | 作用 |
|---|---|---|---|
LECOLORPICKER_GPU_DEFAULT_SCALED_SIZE | 32 | LEColorPicker.m#L24 | 缩放边长,决定计算量 |
| 投票相似度阈值 | 0.1 | 着色器 | YUV 距离小于它算"同款颜色" |
| 单票权重 | 0.0039 | 着色器 | 256 像素满票 = Alpha 1.0 |
| 背景过滤容差 | 0.5 | LEColorPicker.m#L25 | 文字色必须与背景拉开的距离 |
| 主文字过滤容差 | 0.3 | LEColorPicker.m#L26 | 次文字色与主文字色的最小距离 |
| 颜色差阈值 | 500 | LEColorPicker.m#L27 | 可读性校验的 RGB 通道极差 |
| 亮度差阈值 | 125 | LEColorPicker.m#L28 | 可读性校验的 Y 亮度差 |
性能要点:作者特意注明真机比模拟器更快(README.md),因为核心路径跑在 GPU 上;另外GL_NEAREST纹理过滤、16×16 有效区域裁剪、Alpha 通道携带票数(一次渲染同时输出"颜色+统计")都是值得借鉴的技巧。
总结
LEColorPicker 的取色流程可以一句话概括:把图片压成 32×32,交给 GPU 在 YUV 空间里让每个像素为相似色"投票",票数最高者即背景色;再用 RGB 平方距离两轮排除法筛出两个足够反差的文字色,最后用亮度对比度兜底保证可读性。整条流水线没有复杂的聚类算法,却用最小计算量实现了接近 iTunes 11 的视觉效果——这也是它至今仍被当作"图片取色库"参考实现的原因。想动手验证,可以直接打开 LEColorPickerDemo 工程翻页浏览 Resources 目录下的示例图片,观察配色方案的实时变化。
【免费下载链接】LEColorPickerA Cocoa-Touch system for iOS to get a color scheme in function of an image, like iTunes 11 does.项目地址: https://gitcode.com/gh_mirrors/le/LEColorPicker
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考