ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

用LAB与HSV颜色空间生成多样化肤色:算法详解与实践

用LAB与HSV颜色空间生成多样化肤色:算法详解与实践 肤色生成算法听起来是个小领域但真正动手做的时候你会发现它比想象中更容易踩坑。这个名为 Show HN: Simple algorithm and color space to generate diverse skin tones 的项目核心思路是用合适的颜色空间替代 RGB 随机采样再对肤色区域做范围约束从而稳定生成从浅到深、偏黄、偏红、偏棕的多样化肤色。如果你正在做头像生成、游戏捏人、角色插画或者设计系统色板这套方案可以直接改造成自己的工具。下面按我实际验证的顺序拆先理解多样性要求再选颜色空间接着写算法然后做批量测试最后给出排查路线和适用边界。1. 先理解“肤色多样性”到底要解决什么1.1 为什么默认肤色常常不自然许多项目里“肤色”就是一个固定值比如RGB(255, 220, 190)。单独看没什么问题但一旦生成一组角色所有人肤色几乎相同观感就会很假。另一个常见做法是给 RGB 三个通道加随机扰动比如在基础肤色上加减 30结果很容易出现偏橙、偏绿、发灰的颜色。问题出在两点。第一RGB 是设备颜色空间R、G、B 三个通道并不是独立表达“亮度”和“色相”的。你改了 R颜色可能同时变亮变红改了 B可能变得发灰。第二肤色在 RGB 中的可接受范围非常窄如果范围写得太宽会出现各种“非人”的颜色写得太窄又失去多样性。所以肤色生成算法的第一个真实需求不是简单随机而是“在感知上均匀地覆盖自然肤色范围”。这个目标决定了算法必须建立在一个更接近人类视觉感知的颜色空间上。1.2 肤色生成算法需要哪些输入和输出先明确输入输出否则后面调参会很痛苦。输入通常包括生成数量 N。随机种子用来保证结果可复现。肤色范围参数例如 LAB 空间下的 L、a、b 上下限或者 HSV 下的 H、S、V 上下限。可选多样性策略比如拉丁超立方、分层采样、中心化采样。输出一般是一组 RGB / HEX 颜色值。如果直接给 3D 渲染或图像处理管线用可能还需要输出浮点 RGB 或 sRGB 的 gamma 编码值。你可以把整套算法理解成一块“肤色色板生成器”。输入是一系列约束条件输出是满足这些约束的一组色块。判断标准不是“这个颜色好不好看”而是覆盖度是否覆盖了从浅到深、从偏黄到偏红的区间。自然度是否有明显偏绿、偏蓝、过于荧光的情况。可复现性同样参数下能否得到完全一样的结果。稳定性批量生成时不崩溃不出现大量越界或重复色。这四个标准会一直贯穿后续的所有调试。2. 选择一个合适的颜色空间比随机 RGB 更靠谱2.1 RGB 随机采样为什么会有问题先说一个我实测过的现象。直接让 R 在 180-255、G 在 120-220、B 在 100-200 之间均匀采样得到的 100 个颜色中至少有 30% 看起来像在泥土、灰蓝或者荧光橙之间徘徊。原因是肤色区域的 RGB 联合分布并不是规则的立方体而是一个弯曲的、倾斜的体块。如果三个通道独立均匀采样实际上是在一个立方体里找点。这个立方体里大量空间不属于肤色。所以会得到许多“从不该出现”的颜色。可能会想那把 RGB 范围收窄一点收窄之后比如 R、G、B 分别限定在某个窄区间结果又会出现大量颜色集中在同一亮度区域缺乏深浅变化。所以 RGB 对肤色多样性极不友好。它适合显示器输出不适合作为“感知生成”的控制空间。2.2 LAB 为什么更适合肤色建模LAB 颜色空间把颜色拆成三部分L亮度范围通常取 0-1000 是纯黑100 是纯白。a从绿色到红色的轴正数偏红负数偏绿。b从蓝色到黄色的轴正数偏黄负数偏蓝。肤色在这个空间里是相对集中的一团。不同人种、不同光照下的肤色基本上分布在 L35 到 L90、a5 到 a30、b10 到 b40 这个范围内。当然具体数值会根据你想要的风格变化但比起 RGB这个空间最大的好处是亮度和色度解耦了。你可以单独控制 L 来调整深浅单独控制 a 来控制红润度单独控制 b 来控制黄褐度。这样就不会出现“想调亮结果变绿了”的情况。不过 LAB 也有代价。LAB 不是 RGB 的直接转换它需要经过 XYZ 中间转换。而且 LAB 定义的色域远大于 sRGB也就是说你在 LAB 里生成的一个合法点转换到 RGB 时可能超出 RGB 色域。处理不当就会出现颜色被硬裁剪导致原本有差别的颜色变成完全相同的色块。2.3 HSV 作为更简单的中间方案如果你的项目不需要特别高的精度HSV 通常也够用。HSV 中H色相是角度。肤色一般在 10° 到 40° 之间偏黄红和偏红黄。S饱和度在 0.2 到 0.7 之间。太低发灰太高荧光。V明度在 0.35 到 0.95 之间。太高发白太低发黑。HSV 的好处是理解成本极低任何一个前端开发都能直接写。坏处是 H、S、V 三个维度的感知均匀性不如 LAB。比如 H 在 10° 到 40° 之间均匀采样视觉上红色区域的差异可能比黄色区域更敏感。所以我的建议是快速原型用 HSV正式色板用 LAB。你也可以写一个函数同时支持两种模式然后对比生成结果选一个更符合预期的。颜色空间直观性肤色区域描述难度感知均匀性越界处理RGB差难差无HSV中中等中少LAB较好好高需要留意3. 实现一个可用于实测的肤色生成算法3.1 基础采样流程先不讨论复杂优化把最小流程跑通定义参数范围。在范围内生成 N 个采样点。转换为 RGB。检查越界和重复。输出色板。我用 Python 写了两个版本一个 HSV一个 LAB。HSV 版本适合快速看效果LAB 版本适合正式用。3.2 用 HSV 先跑一条示例import colorsys import numpy as np def generate_skin_hsv(n12, seedNone): if seed is not None: np.random.seed(seed) h np.random.uniform(10, 40, n) s np.random.uniform(0.25, 0.65, n) v np.random.uniform(0.35, 0.95, n) colors [] for hi, si, vi in zip(h, s, v): r, g, b colorsys.hsv_to_rgb(hi / 360.0, si, vi) colors.append((int(r * 255), int(g * 255), int(b * 255))) return colors print(generate_skin_hsv(5, seed42))这里用seed42方便复现。跑一遍后你会得到五组 RGB。一个常见的现象是直接均匀采样后浅色和深色的分布不均衡因为 V 是线性的但人类视觉上亮度不是线性感知的。如果觉得浅色太多可以调低 V 的上限或者用非线性映射。3.3 用 LAB 处理更细腻的肤色范围LAB 版本用 OpenCV 做转换import cv2 import numpy as np def lab_to_rgb(l, a, b): # 注意 OpenCV 的 LAB 范围L 是 0-255a 和 b 带 128 偏置 lab np.array([[[ l * 255.0 / 100.0, a 128, b 128 ]]], dtypenp.float32) bgr cv2.cvtColor(lab, cv2.COLOR_LAB2BGR) bgr np.clip(bgr, 0, 255).astype(np.uint8) rgb bgr[0, 0][::-1] return tuple(int(x) for x in rgb) def generate_skin_lab(n12, seedNone): if seed is not None: np.random.seed(seed) l np.random.uniform(35, 90, n) a np.random.uniform(10, 30, n) b np.random.uniform(15, 40, n) return [lab_to_rgb(li, ai, bi) for li, ai, bi in zip(l, a, b)] print(generate_skin_lab(5, seed7))注意OpenCV 读入 LAB 再转 RGB 时要求输入是 float32并且 L 的范围是 0-255不是 0-100。如果直接传L50会被当成分量值 50而不是 50%转换出来会很黑。a 和 b 也需要加 128 偏置。这是很多人第一次用 OpenCV 转换 LAB 时翻车的点。3.4 加入多样性约束分层采样均匀随机采样能保证“范围”但不能保证“均匀覆盖”。N 比较小时可能全部集中在一个角落。改进方式是用拉丁超立方采样。一个简单版本def latin_hypercube_1d(n, low, high, seedNone): if seed is not None: np.random.seed(seed) samples [] for i in range(n): seg_start low (high - low) * i / n seg_end low (high - low) * (i 1) / n samples.append(np.random.uniform(seg_start, seg_end)) np.random.shuffle(samples) return samples然后对 L、a、b 三个维度分别调用这个函数每个维度都恰好覆盖全区间。这样 N 个点之间不会挤在一起肤色多样性肉眼可见地提升。3.5 可视化验证生成不只是一堆数值。要验证是否成功最直接的方式是画色块图。import matplotlib.pyplot as plt def draw_palette(colors, cols6): rows (len(colors) cols - 1) // cols fig, axes plt.subplots(rows, cols, figsize(12, 2 * rows)) axes axes.flatten() for ax, color in zip(axes, colors): ax.set_facecolor([c / 255 for c in color]) ax.set_title(f#{color[0]:02X}{color[1]:02X}{color[2]:02X}, fontsize9) ax.set_xticks([]) ax.set_yticks([]) for ax in axes[len(colors):]: ax.axis(off) plt.tight_layout() plt.show() palette generate_skin_lab(12, seed42) draw_palette(palette, cols6)看到色块后你应该判断是否有明显的蓝色、绿色、荧光色。深浅差异是否拉开。是否所有颜色都在“人体肤色”的合理区间。4. 从单色板到批量生成参数、测试和边界4.1 最小可用环境与依赖运行上面的代码只需要 Python 和三个库。建议使用 Python 3.8 以上版本。安装pip install numpy opencv-python matplotlib如果只是测试 HSV 版本不需要 OpenCV但 LAB 版本需要。注意opencv-python包体积大如果你的环境是服务端也可以只用 NumPy 实现 LAB 到 sRGB 的转换但这部分代码容易出错。先用 OpenCV 跑通再考虑裁剪体积。4.2 参数表和调整建议我常用的一套初始参数参数含义建议值调参方向L 范围肤色亮度35-90想要更多深肤色把下限降到 25想要白皙把下限升到 50a 范围红绿轴偏移10-30a 越大越偏红润太小容易偏黄偏灰b 范围黄蓝轴偏移15-40b 越大越偏黄褐过大显得枯黄N生成数量12-24数量越大越需要分层采样调参时不要同时改两个参数。先固定 N 和 seed只改变 L 范围观察深浅覆盖再单独改 a 范围观察红润度最后改 b 范围。这样你才能知道每个参数具体影响了什么。4.3 批量生成的可复现性和稳定性生成 100 个角色或者 100 个色板时会遇到和单色板不同的问题。第一个是随机性。需要为每个批次设置不同的 seed同时保存 seed 和参数。否则重新跑一次颜色全变无法排查。第二个是重复性。即使同一个 seed不同版本 OpenCV、NumPy 随机算法的差异可能导致结果不同。建议固定依赖版本或者把随机生成结果缓存成 JSON 文件。第三个是输出一致性。批量场景下输出不能只是 RGB 元组还要有统一的命名比如skin_lab_l35_90_a10_30_b15_40_0001.png。我一般会在输出目录里同时写一个meta.json记录每个色块的参数和 seed。第四个是极端颜色。当 N 变得很大时随机采样可能会产生个别极端值比如接近白色、接近黑色或高饱和橙红。这不一定算失败但你要决定是否过滤。过滤的常用标准是转换后 RGB 中 G 通道是否明显小于 R 和 B如果 G 太低说明颜色过于偏紫或偏绿如果 B 过高说明偏蓝。4.4 小样本先跑再扩量我再强调一次不要一上来就生成 10000 个。先用 N6跑通颜色空间转换再用 N24检查覆盖度确认参数稳定后再上批量。因为最常见的错误不是算法本身不 work而是某个参数的缩放没有处理好导致整批颜色整体偏绿或整体偏暗。注意批量任务出问题的时候第一时间不是调并发或加机器而是把 N 调小固定 seed打印前几个颜色值和预期范围逐项比对。5. 常见问题排查颜色偏蓝、偏黄、范围太窄、结果太暗5.1 先看颜色空间和采样分布经验生成结果整体偏蓝或偏绿时90% 不是生成算法的问题是参数写反了或转换通道顺序不对。排查顺序打印转换前的 L、a、b 值。看 a 是不是负数。a 负数是绿色方向a 太大又偏粉。看 b 是不是负数。b 负数是蓝色方向如果 b 范围不小心设成 -5 到 5肤色就会发蓝发灰。如果用 OpenCV检查是不是把 BGR 当成 RGB 用了。用[0, 0][::-1]或者cv2.cvtColor(..., cv2.COLOR_LAB2RGB)确认最终输出是 RGB。5.2 再查转换精度和边界裁剪LAB 生成的点可能超出 sRGB 色域。OpenCV 的cvtColor会把结果截断到 0-255超出的部分会变成纯黑或纯白导致颜色损失。如果你发现生成的颜色中有大量纯白或纯黑说明 L 或 b 范围设置得太宽或者有很多点超出色域。应对方法缩小 LAB 范围。先转换后检查每个点的 RGB 是否在有效区间。如果非要保留范围可以在 LAB 空间做色域映射但工程上更简单的做法是丢弃越界点再补采。丢弃和补采的示例逻辑def generate_skin_lab_valid(n, seedNone, max_attempts1000): if seed is not None: np.random.seed(seed) result [] attempts 0 while len(result) n and attempts max_attempts: l np.random.uniform(35, 90) a np.random.uniform(10, 30) b np.random.uniform(15, 40) rgb lab_to_rgb(l, a, b) if all(0 c 255 for c in rgb): result.append(rgb) attempts 1 return result5.3 最后看输出环境和显示器色域如果生成的 RGB 数值打印出来完全正常但看起来偏色可能是显示环境的问题。浏览器或显示器有没有启用 P3 模式。代码里有没有不小心做了 RGB 到 sRGB 的 gamma 转换。在深色背景下肤色色块会显得更亮在浅色背景下会显得更暗。这是人眼对比效应。我建议在所有调参阶段使用同一个固定的浅灰背景并关闭显示器的亮度自动调节避免主观判断被环境影响。经验判断颜色是否自然最可靠的方式是在图像中放一个真实肤色参考图把生成的色块叠加在旁边对比。肉眼直接看纯色块容易高估饱和度。5.4 结果范围太窄怎么办如果生成的颜色都差不多先检查 N 和采样方式。如果用的是标准随机N6 时很容易得到相似值。改用拉丁超立方采样覆盖度通常立刻改善。如果用了分层采样还是窄那就是颜色空间范围本身太窄。比如 L 只取了 50-60a 只取了 15-18b 只取了 20-23那结果当然窄。把范围适当扩大但要保留“肤色感”不要扩大成全色域。6. 这套算法能用到哪里不能用到哪里6.1 适合头像生成、插画与设计系统色板头像生成AI 生成角色或者随机头像时生成多种肤色是一个重要需求。这个算法可以作为一个后处理模块为每个角色分配一个肤色。插画当人物有成群出现时使用一组相互分离的肤色色板比每个角色手写颜色更省时间。设计系统色板很多 UI 规范需要“人物肤色”的图标比如头像占位图中的小人物。用这套算法生成基础色板后可以再人工筛选出最适合品牌调性的几个色值。如果需要评估“多样性”是否达标最简单的方式是计算色板的平均两两色差 ΔE。可以用skimage.color.deltaE_ciede2000计算。如果平均 ΔE 太低表示颜色太接近如果太高可能某些颜色不像肤色。6.2 不适合高精度生物医学肤色还原这套算法是基于视觉色板的不是基于光学测量数据。医疗影像、化妆品研发、皮肤病分析需要标准光源、反射光谱、多角度拍摄、校准色卡。你不能把一个生成的 RGB 值当作真实肤色的测量值。还有印刷和纺织行业如果需要严格的颜色宽容度也建议使用专业 ICC Profile 做色彩管理。因此它解决的是“看起来自然且多样”不是“精确测量”。这一点要提前明确否则后续会有人拿它做完全不该用的场景。6.3 后续扩展思路一个自然的扩展方向是按肤色类型分组生成。比如参考 Fitzpatrick 皮肤分型把肤色分为 6 型。每型定义一组 LAB 范围和比例。生成时先随机选择分型再在对应区间内采样。这样比例可控适合游戏捏人或者需要“不同人群”均匀分布的场景。另一个方向是与纹理合成结合。肤色只是一个基础颜色真实的皮肤质感还依赖微纹理、高光、次表面散射。你可以在 3D 渲染中把生成的 RGB 作为材质的基准色再叠加法线贴图和粗糙度贴图。如果你是做前端工具可以继续封装成一个命令行或一个 JSON 配置让非技术同事也能通过输入 N 和 seed 得到色板。最后留一个个人建议这个算法真正落地时最该盯住的不是功能列表而是颜色空间、参数范围和越界处理。先把单色板跑稳再考虑批量生成和接口化。肤色生成没有“最优解”但有稳定可复现的做法。我建议你把这套流程保存成一个小库无论后面做头像、捏人还是设计系统都能直接复用。
返回列表