恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

HSI色彩空间块可视化:从公式推导到Python实现

  • 首页
  • 资讯中心
  • /
  • HSI色彩空间块可视化:从公式推导到Python实现

相关资讯

LED数码管数据集构建与YOLOv8检测实战:从采集标注到边缘部署 2026/10/12 3:08:53
tokscale 与 9Router 桥接实战:用 gjc 格式 JSONL 打通路由网关的用量、图表与成本估算 2026/10/12 3:03:53
Claude记忆层claude-mem:从上下文窗口到外挂记忆的完整实践 2026/10/12 3:03:53

最新资讯

WASI 文件系统路径解析与沙箱机制深度剖析:从 openat 手动算法到 openat2 内核原语
基于Django+Vue.js的租房推荐系统设计与实现
AnyPS5项目解析:技术定位与合规开发边界
4个工具型网站帮你快速读懂陌生项目源码
智能桌面宠物开发实战:从悬浮窗透明到AI对话的完整工程路径
双离线支付技术拆解:原理、风险与测试方案

今日推荐

Debian新手入门:从部署到日常操作的完整指南
MongoDB复制集扩缩容实战:从rs.add到选主事故复盘
条形码目标检测数据集实战:从YOLOv8训练到部署

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

HSI色彩空间块可视化:从公式推导到Python实现

发布时间:2026/10/12 3:08:53
HSI色彩空间块可视化:从公式推导到Python实现 前两天整理图像处理笔记时翻到一篇讲“生成HSI块可视化”的转载文章。很多人第一次看到“HSI块可视化”这个词会以为是某种医疗信息系统的图表其实这里是色彩空间领域的经典玩法把HSI色彩空间按色相Hue、饱和度Saturation、强度Intensity三个维度切成一格格色块用最直观的方式把“颜色之间的关系”铺满整张画布。这个思路非常适合图像处理初学者、做颜色算法调试的工程师以及需要向团队解释色彩模型差异的人。我把它重新实现了一遍顺带梳理了公式推导、工程实现和踩坑记录这篇就当作我自己的复现笔记。1. 从色彩空间聊起为什么HSI值得做可视化1.1 HSI与RGB/HSV的定位差异先明确一个前提我们平时看到的屏幕图像默认是RGB色彩空间R、G、B三个通道直接对应显示器的三个发光单元。但RGB有一个很难受的问题——它对“人类如何理解颜色”并不友好。你说“这个颜色更鲜艳一点”“那个颜色有点发暗”在RGB里很难直接映射到某个通道的加减值。HSV和HSL就是为了解决这个问题出现的它们把颜色拆成色相、饱和度、明度/亮度。HSI同样如此但它的强度分量I用的是三个通道的算术平均值I (R G B) / 3这看起来只是换了个亮度定义但影响很大。HSV里的V取的是max(R,G,B)HSL里的L取的是(maxmin)/2而HSI用均值所以HSI的色域形状是一个双锥体不像HSV那样是单锥体。这个几何差异直接决定了HSI块可视化的画法在H固定时S和I组成的截面不是完整的矩形而是梯形或三角形区域。你会在后面的色块图中看到右上角的格子经常被“截掉”那不是Bug是色域边界。用表格对比几个常见色彩空间会更清楚色彩空间亮度/明度分量饱和度定义立体形状典型场景RGB无独立亮度通道无立方体显示、存储HSVV max(R,G,B)(max - min) / max单锥体颜色拾取、目标检测HSLL (max min) / 2与HSV略有差异双六棱锥CSS调色、绘画软件HSII (R G B) / 31 - min / I双锥体图像增强、彩色分割我个人做彩色图像分割时更喜欢HSI因为强度分量更贴近人眼感知的平均亮度而且它的饱和度定义和亮度相关不会像HSV那样在暗部出现饱和度被过度放大的问题。但前提是你得把它的色域边界搞清楚否则分割阈值调起来非常痛苦。1.2 块可视化到底在表达什么信息所谓“块可视化”核心就是离散化加网格化。把色相H从0到360度切成几十条把饱和度S从0到1切成十六份把强度I从0到1也切成十六份然后每一个坐标点(H, S, I)都换算成一个RGB颜色用一个小色块画在画布上。这样你就能一眼看到色相H变化时颜色沿着红、黄、绿、青、蓝、品红循环饱和度S变化时颜色从灰到纯色逐渐饱和强度I变化时颜色从黑到白逐渐变亮。这种信息如果用口头描述来回绕半天都不一定讲得清。但做成色块图以后哪怕是从没接触过色彩模型的人也能立刻看懂“饱和度是往哪个方向拉”“强度是往哪个方向拉”。块可视化的另一个好处是方便检查转换代码。每当你写了一个RGB转HSI或者HSI转RGB的算法最直接的验证方式就是把转换结果铺成色块图肉眼观察颜色过渡是否连续、边界是否正常。如果某些区域出现异常的颜色跳跃几乎可以断定公式写错了或者取值范围没有处理好。1.3 做这件事最典型的三个应用场景第一个场景是教学和科普。给新手讲色彩空间“HSI三通道”光靠术语很难形成直觉一张分块图比十页PPT都管用。第二个场景是算法调试。比如你在做肤色检测、颜色阈值分割通常需要确定H和S的合适范围这时候生成一组固定H的S-I网格图可以直观看出哪些色块是你要的目标颜色。第三个场景是图像处理库的验收。新封装了一个颜色转换函数想要批量验证结果是否正确生成多视角色块图做对比比逐个像素比较效率高得多。这三个场景其实都指向同一个需求把抽象的色彩模型变成可以肉眼审查的图形。这篇博文要解决的就是如何用Python快速生成这些图形。2. 核心细节解析HSI的分量组织与换算公式2.1 H、S、I的物理含义与取值范围HHue是色相单位通常用角度0到360度也可以归一化成0到1。0度对应红色120度对应绿色240度对应蓝色360度和0度重合。需要注意OpenCV里做HSV转换时H的范围被压缩到了0到179因为OpenCV默认用uint8存放角度一个字节最大255为了安全它把360度缩小了一半。这个问题在后面排查时会单独说。SSaturation是饱和度范围0到1。S0表示完全没有颜色只有灰色S1表示纯色比如纯红、纯绿。HSI里S的定义依赖于强度I公式是S 1 - min(R, G, B) / I如果我把这个公式再变形一下min(R,G,B) I * (1 - S)。所以当S1时RGB三个分量里至少有一个是0当S0时RGB三个分量都等于I也就是灰色。IIntensity是强度范围0到1。0是纯黑1是纯白。对于HSI空间来说不是所有(H, S, I)组合都合法。比如H0、S1、I0.8换算成RGB会得到R2.4超出正常范围。这就是双锥体色域边界的体现。做可视化时必须对最终RGB值做截断否则会出现溢出颜色。2.2 RGB转HSI的计算过程RGB转HSI的标准流程是先把R、G、B归一化到0到1然后按顺序计算I、S、H。I的计算最简单三个通道取平均。S的计算依赖于最小值S 1 - min(R, G, B) / I注意当I0时这个公式里会出现除零需要做保护。通常在工程实现里当I0时直接把S设为0因为强度为0就是纯黑谈不上饱和度。H的计算稍微复杂一点。先用余弦定理计算出角度thetatheta arccos( 0.5 * ((R - G) (R - B)) / sqrt((R - G)^2 (R - B) * (G - B)) )得到的是一个0到180度之间的角度此时还需要根据B和G的大小关系区分如果B G则H theta如果B G则H 360 - theta。这个细节点非常容易写错。我最早实现时就漏了B和G的判断导致画出来的色相条上半部分和下半部分完全错乱颜色不是连续渐变而是来回跳动。2.3 HSI转RGB的逆变换与边界陷阱从HSI回到RGB要把360度分成三个扇区每个扇区120度。以第一扇区0到120度为例B I * (1 - S) R I * (1 S * cos(H) / cos(60 - H)) G 3 * I - R - B第二扇区120到240度先把H减去120度然后旋转角色R I * (1 - S) G I * (1 S * cos(H) / cos(60 - H)) B 3 * I - R - G第三扇区240到360度再减120度G I * (1 - S) B I * (1 S * cos(H) / cos(60 - H)) R 3 * I - G - B这里面有几个陷阱。第一个是角度单位Python的math.cos和NumPy的np.cos默认接受弧度不是角度写代码时千万不能漏掉np.deg2rad。第二个是分母cos(60 - H)虽然H在对应扇区时cos值最小也有0.5不会出现真正的除零但浮点误差仍然可能让结果偏离建议加一个很小的epsilon保护。第三个问题也是新手的重灾区三个分量算出来以后可能超过0到1区间尤其是S偏大、I偏大时必须对最终结果做clip否则转成uint8时会因为负数或者大于255变成乱七八糟的颜色。2.4 可视化的映射策略色轮、网格与展开图确定了转换公式后就可以设计怎么把三维空间画到二维画布上。最常见的四种策略色相条一行格子固定S和I让H从0到360度均匀变化。这个最简单适合验证H映射是否正确。S-I二维网格固定一个H横轴是S纵轴是I把每个(S,I)组合画成色块。这个能看出某个色相下的饱和度和强度分布。色相网格矩阵选六个或更多个H值每个H画一张S-I网格然后按矩阵排列。这个就是我最后生成的“六色块可视化”信息量最大。双锥体展开图把HSI双锥体沿母线剪开展平理论上更接近色彩空间的真实几何形态但实现复杂度高日常调试用不太上。我建议普通场景先做策略2和策略3它们最容易实现也最能说明问题。如果想要更华丽的展示再考虑双锥体展开。3. 实操过程与核心环节实现3.1 环境准备与依赖安装我用的是Python 3环境主要依赖三个库NumPy处理数组和向量化计算Pillow生成色块图并保存Matplotlib把多个色块图组合成矩阵方便对比。安装命令一行搞定pip install numpy pillow matplotlib注意Pillow在Python 3里直接import PIL使用很多人会漏掉安装。如果只是生成简单色块图其实只用NumPy和Pillow就够Matplotlib只在最后做多子图拼接时用到。3.2 从HSI到RGB的块生成器整个可视化流程的核心是一个hsi_to_rgb函数。我写了向量化版本方便直接用NumPy批量计算import numpy as np def hsi_to_rgb_vec(h, s, i): h np.mod(h, 360.0) s np.clip(s, 0.0, 1.0) i np.clip(i, 0.0, 1.0) r np.zeros_like(h) g np.zeros_like(h) b np.zeros_like(h) eps 1e-10 h_rad np.deg2rad(h) mask1 h 120 h1 h[mask1] b[mask1] i[mask1] * (1.0 - s[mask1]) r[mask1] i[mask1] * (1.0 s[mask1] * np.cos(np.deg2rad(h1)) / (np.cos(np.deg2rad(60.0 - h1)) eps)) g[mask1] 3.0 * i[mask1] - r[mask1] - b[mask1] mask2 (h 120) (h 240) h2 h[mask2] - 120.0 r[mask2] i[mask2] * (1.0 - s[mask2]) g[mask2] i[mask2] * (1.0 s[mask2] * np.cos(np.deg2rad(h2)) / (np.cos(np.deg2rad(60.0 - h2)) eps)) b[mask2] 3.0 * i[mask2] - r[mask2] - g[mask2] mask3 h 240 h3 h[mask3] - 240.0 g[mask3] i[mask3] * (1.0 - s[mask3]) b[mask3] i[mask3] * (1.0 s[mask3] * np.cos(np.deg2rad(h3)) / (np.cos(np.deg2rad(60.0 - h3)) eps)) r[mask3] 3.0 * i[mask3] - g[mask3] - b[mask3] rgb np.stack([r, g, b], axis-1) return np.clip(rgb, 0.0, 1.0)这个函数接受数组输入返回形状相同的RGB数组值都在0到1之间。它比我最早写的纯Python循环版本快了不止一个数量级后面生成大图时才不会卡顿。3.3 实现固定色相下的S-I二维块图有了转RGB函数下一步就是生成二维网格。假设每个色块由24×24像素组成饱和度方向取16格强度方向取16格from PIL import Image, ImageDraw def generate_si_grid(hue, s_count16, i_count16, block24): s_vals np.linspace(0, 1, s_count) i_vals np.linspace(0, 1, i_count) S, I np.meshgrid(s_vals, i_vals) H np.full_like(S, hue) rgb hsi_to_rgb_vec(H, S, I) rgb (rgb * 255).astype(np.uint8) canvas Image.new(RGB, (s_count * block, i_count * block), black) draw ImageDraw.Draw(canvas) for row in range(i_count): for col in range(s_count): x0 col * block y0 row * block color tuple(rgb[row, col]) draw.rectangle([x0, y0, x0 block - 1, y0 block - 1], fillcolor) return canvas有一点要强调我生成网格时S和I都取的是每个格的均匀采样点而不是格子的左下角或中心点。这里取中心点会更准确比如第0格的中心是s 1/(2*16)但为了简单也可以取linspace的等分点。两者差别不大但在色块比较细时会影响边缘亮度。调用方式很简单img generate_si_grid(hue0) img.save(hsi_h0.png)生成的图里左上角是S0、I1的位置颜色偏白右下角是S1、I0的位置颜色偏黑。横向看饱和度从灰到纯色过渡纵向看强度从亮到暗过渡。3.4 实现色相条与六色色块板的拼接单张S-I网格图只能看一个色相但H与H之间的连续性也很重要所以先做一条色相条验证H映射def generate_hue_bar(hue_count360, block3, s1.0, i0.5): canvas Image.new(RGB, (hue_count * block, 24), black) draw ImageDraw.Draw(canvas) for h in range(hue_count): r, g, b hsi_to_rgb_vec(np.array([h]), np.array([s]), np.array([i]))[0] color tuple((np.clip([r, g, b], 0, 1) * 255).astype(np.uint8)) draw.rectangle([h * block, 0, (h 1) * block - 1, 23], fillcolor) return canvas这里我固定S1、I0.5也就是全饱和、中等强度色相条最接近画图软件里的色轮展开。保存后你会看到红、黄、绿、青、蓝、品红六段均匀过渡顺序和混合色原理一致。更常用的做法是六个固定色相每个色相一张S-I网格拼成2×3的大图。用Matplotlib实现import matplotlib.pyplot as plt hues [0, 60, 120, 180, 240, 300] fig, axes plt.subplots(2, 3, figsize(18, 10)) for ax, hue in zip(axes.ravel(), hues): img generate_si_grid(hue, s_count16, i_count16, block20) ax.imshow(img) ax.set_title(fH {hue}°, fontsize14) ax.axis(off) plt.tight_layout() plt.savefig(hsi_si_grids_6hues.png, dpi150)运行这段代码得到的图能很清楚地看到六个色相各自的色域形状。你会发现H0和H60的图颜色分布并不完全一样这是S和I约束导致的正常现象。3.5 用真实图片做HSI分布块状化固定网格是“从色彩空间出发去找颜色”反过来还可以“从真实图片出发看颜色分布”。做法是读取一张图片把每个像素的RGB值转成HSI然后把所有像素按(H,S,I)放进一个三维直方图再可视化出来。核心代码如下def rgb_to_hsi_arr(arr): arr arr.astype(np.float32) / 255.0 r, g, b arr[..., 0], arr[..., 1], arr[..., 2] i (r g b) / 3.0 i_safe np.where(i 0, 1e-6, i) s 1.0 - np.minimum(np.minimum(r, g), b) / i_safe diff 0.5 * ((r - g) (r - b)) denom np.sqrt((r - g) ** 2 (r - b) * (g - b)) 1e-6 theta np.arccos(np.clip(diff / denom, -1, 1)) h np.degrees(theta) h np.where(b g, 360.0 - h, h) return h, s, i统计每个块的像素数量后可以用散点图或者色块透明度图表示。这个操作在做颜色筛选时特别直观。有一次我需要从一大片皮肤区域里分离手部看到HSI分布块图后立刻确定H应该在0到30度、S在0.2到0.6左右比单纯试阈值快很多。3.6 用Matplotlib输出多子图的完整脚本完整的脚本结构就是先生成六张S-I网格图再生成一条色相条最后用Matplotlib把所有图拼到一张画布上。如果你不想用Matplotlib也可以用Pillow手动拼接from PIL import Image blocks [] for hue in hues: blocks.append(generate_si_grid(hue)) # 拼接成3行2列 row_images [] for row in range(2): row_img Image.new(RGB, (blocks[row * 2].width blocks[row * 2 1].width, blocks[row * 2].height), black) row_img.paste(blocks[row * 2], (0, 0)) row_img.paste(blocks[row * 2 1], (blocks[row * 2].width, 0)) row_images.append(row_img) final Image.new(RGB, (row_images[0].width, row_images[0].height row_images[1].height), black) final.paste(row_images[0], (0, 0)) final.paste(row_images[1], (0, row_images[0].height)) final.save(hsi_blocks_combined.png)这样生成的是一张干净的大图方便直接贴进文档或演示PPT里。4. 常见问题与排查技巧实录4.1 颜色发灰发暗先把取值范围理清楚第一次复现时我生成的色块图整体偏灰偏暗尤其是饱和度高的区域完全没有鲜亮感。排查后发现问题出在我把RGB值直接从0到255带入转换公式而公式期望输入是0到1。比如R255被当成255.0参与计算I直接变成几百S被压缩到接近0颜色自然全是灰的。正确的做法是先做归一化arr np.asarray(img, dtypenp.float32) / 255.0然后所有计算在0到1之间进行最后再乘255转回uint8。这个坑几乎每次手写色彩转换都会踩一遍我的习惯是一开始就写成工具函数统一处理归一化和反归一化避免每个调用点重复转换。4.2 色块边缘出现马赛克纹路用Pillow画矩形色块时如果矩形参数设置不对块与块之间会出现1像素宽的缝隙或者重叠看起来像马赛克纹路。原因通常是rectagle函数把右边界和下边界当成开区间处理或者你用了(x0, y0, x0 block, y0 block)导致每个块多画了一个像素到下一格。稳妥的写法是draw.rectangle([x0, y0, x0 block - 1, y0 block - 1], fillcolor)这个细节虽然小但生成大图时非常影响观感尤其是旁边有固定网格线的时候1像素的缝隙会被无限放大。4.3 HSI转换结果与别人的代码不一致网上很多资料会把HSI和HSV混着写甚至有的文章把HSI的I直接写成max(R,G,B)那其实是HSV的V不是标准HSI。当你发现自己的色块图和别人的图差别很大时先别怀疑数学先去确认对方用的到底是HSI、HSV还是HSL。还有一个更隐蔽的问题OpenCV里的cvtColor函数可以转HSV但返回的H范围是0到179S和V范围是0到255和标准HSV范围完全不一样。如果你拿OpenCV的结果和纯数学公式的结果对比数值必然对不上。解决办法就是明确自己的目标是“数学演示”还是“工程应用”分别适配不同的范围。4.4 生成大图太慢纯循环改成NumPy向量化如果直接用两层循环遍历每一块再调用hsi_to_rgb的标量版本生成一张16×16的图还可以接受但要生成360个色相条、每个色相条又有很多像素时速度就会慢到无法忍受。我最初生成一张360×16的色相条花了十几秒改成向量化后不到一秒。其实向量化并不复杂核心就是让NumPy一次性处理所有网格点用meshgrid生成所有S和I的组合把整个数组传给转换函数最后用Pillow从数组创建图像。NumPy的广播机制会自动逐元素计算省掉Python循环的开销。这是做图像处理时的基础素养建议尽早养成“能向量化就不要写循环”的习惯。4.5 中文乱码与图像导出格式选择Matplotlib默认字体不含中文字符如果你在标题里写“色相”“饱和度”这类中文画出来就是方块。解决办法是显式指定字体import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False导出格式方面色块图建议保存成PNG因为PNG是无损压缩色块边缘清晰。JPG在这种色块边界分明的图里会出现压缩伪影影响观感。如果图特别大可以把dpi降到120到150文件体积就会小很多。为了让你排查起来更快我把常见问题整理成了速查表现象可能原因解决办法整体颜色发灰RGB没有归一化到0-1先除以255再做转换色相条颜色不对忘记区分B和G的关系加上B G时H 360 - theta色块间有线缝rectangle边界写成开区间用x0 block - 1结果和OpenCV不一致空间定义或取值范围不同确认HSI/HSV/HSLH范围0-179或0-360生成速度慢Python双层循环改用NumPy向量化计算出图太快但颜色溢出逆变换结果没有clip在0到1区间内clip后再转uint85. 实操心得与扩展方向5.1 我踩过的坑与记住的规律把整个HSI块可视化做完以后有几个规律记得特别牢。第一HSI空间不是一个“矩形盒子”而是双锥体。所以当你生成S-I网格图时S1且I特别大或者特别小的区域会超出色域颜色被clip成白色或黑色形成整齐的边界平台。这不是代码错误而是理论模型本身决定的。如果某天有人拿着一张全部填满的矩形色块图告诉你那是HSI他大概率用的是HSV或HSL。第二S0那一列或一行一定和H无关所有色相下都呈现同一条灰色渐变。这是因为饱和度为零时颜色退化到灰色轴。同理I0那一行必然是纯黑。这两条都是判断转换代码是否正确的“黄金锚点”。第三色相条里H0和H360首尾相接颜色都是红色。如果你发现首尾颜色不一致比如360度变成了蓝色那一定是角度取模写错了。我自己的代码习惯是把所有转换函数单独放到一个模块里统一输入输出范围并在函数注释里写明“输入H是0到360、S和I是0到1输出RGB是0到1”。这样时隔几个月再看代码不用重新推公式也能快速改。5.2 继续往下做HSI阈值分割、颜色匹配与3D渲染HSI可视化做出来之后很自然的延伸方向是颜色阈值分割。比如要检测一张图中的红色区域就可以在HSI空间设定H为0到30度、S大于0.3、I大于0.2然后生成掩码。和在RGB空间直接切通道相比HSI的阈值语义更接近人眼调起来直觉很多。另一个方向是做3D渲染。如果把H、S、I当作三个空间轴每个坐标点放置一个小立方体就能在三维软件里画出HSI双锥体。这个用到的是同样的离散化和映射思路本质上就是把我这里的二维色块图扩展到三维体素化。当然渲染门槛会高一些但底层逻辑是一样的。有个小技巧可以分享在做颜色匹配时与其直接比较RGB欧氏距离不如先转成HSI只比较H和S忽略I。这样对光照变化更鲁棒。比如两张同样的物体在不同亮度下拍摄RGB距离可能很大但H和S差别很小用HSI做匹配会更稳。我个人的体会是像HSI块可视化这种看似“只是为了好看”的小代码其实特别适合用来检验自己对色彩模型的理解。因为在画图的过程中你必须把每个公式、每个取值范围、每个色域边界都弄明白否则生成的图就一定有破绽。它不像调参数那么依赖经验更考验基础概念是否扎实。如果你也想把某个知识点的抽象概念落到纸面上强烈建议找一个小而具体的可视化角度做一遍那种“真的懂了”的感觉很值得体验。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号