解决YOLOv8图像分割颜色混淆问题:手把手教你修改掩膜和预测框颜色

在计算机视觉项目的实际落地过程中,可视化效果的好坏直接影响着开发者的调试效率和最终演示的观感。你是否遇到过这样的场景:使用YOLOv8进行图像分割时,模型预测出的物体掩膜(mask)和包裹它的边界框(bounding box)颜色过于接近,甚至完全一样,导致在复杂的图像背景中难以一眼区分出目标物体和它的轮廓?这种颜色混淆问题不仅让结果图看起来混乱,更可能掩盖掉一些重要的预测细节,比如多个重叠物体的边界。

对于从事安防监控、自动驾驶感知、工业质检或者医学图像分析的朋友来说,清晰的可视化是沟通和验证模型性能的关键。本文将从一个实践者的角度出发,为你提供一套完整、可操作的解决方案。我们不会停留在简单的“改个颜色”的层面,而是深入YOLOv8的绘图工具类,理解其颜色分配机制,并教你如何定制一套既美观又实用的颜色方案,确保你的分割结果图一目了然,专业感十足。

1. 理解YOLOv8的可视化核心:plotting.py 与 Colors 类

要修改预测结果的颜色,我们必须先找到控制这一切的“开关”。在YOLOv8的源码架构中,所有与结果绘制相关的逻辑都封装在 ultralytics/utils/plotting.py 这个文件里。这个文件就像一个画家的工具箱,里面装着画笔、调色板和绘制各种图形(如框、文本、掩膜)的函数。

1.1 定位颜色定义的源头

打开你的YOLOv8项目目录,找到 ultralytics/utils/plotting.py。用你喜欢的代码编辑器(如VSCode、PyCharm)打开它。在这个文件中,有一个名为 Colors 的类,它就是整个可视化系统的色彩管理中心。

这个类的设计非常巧妙,它采用了一种“按需分配、循环使用”的策略来管理颜色。让我们看看它的核心部分(代码位置可能因版本略有不同,通常在文件靠前的位置):

class Colors:
    """
    Ultralytics color palette https://ultralytics.com/ for bounding boxes and segments.
    """
    def __init__(self):
        """Initialize colors as hex = matplotlib.colors.TABLEAU_COLORS.values()."""
        hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB',
                '2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7')
        self.palette = [self.hex2rgb(f'#{c}') for c in hexs]
        self.n = len(self.palette)

    def __call__(self, i, bgr=False):
        """Converts hex color codes to rgb values."""
        c = self.palette[int(i) % self.n]
        return (c[2], c[1], c[0]) if bgr else c

    @staticmethod
    def hex2rgb(h):
        """Converts hex color code to rgb tuple."""
        return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))

关键点解析:

  • hexs 元组:这里定义了20种默认的十六进制颜色码(去掉了#前缀)。这就是YOLOv8的初始调色板。
  • self.palette:在初始化时,将hexs中的每个颜色码通过 hex2rgb 静态方法转换为RGB元组,存储在一个列表中。
  • __call__ 方法:这是类的“魔法方法”。当你使用 colors(i) 这样的形式调用时,它会根据索引 i(通常是类别ID)返回对应的颜色。% self.n 确保了即使类别数超过20种,颜色也会循环使用,不会索引越界。bgr 参数用于适配OpenCV的BGR颜色格式。

注意:YOLOv8在绘制时,对于同一张图片中的不同物体实例,即使它们属于同一类别,也可能为了区分而分配不同的颜色索引。但掩膜和其对应的预测框通常会使用相同的颜色索引,这就是导致颜色混淆的根本原因。我们的修改需要从调色板本身入手,让相邻的颜色在视觉上差异更大。

1.2 颜色混淆问题的成因分析

为什么会出现框和掩膜颜色相近的问题?这主要源于默认调色板的设计逻辑。hexs 中的颜色虽然丰富,但并非完全按照最大视觉区分度来排列。例如,‘FF3838’(亮红色)和 ‘FF9D97’(浅粉红色)在色相上非常接近,如果它们被先后分配给一个物体的框和掩膜,在快速浏览时确实容易混淆。

此外,当场景中存在大量同类物体时,颜色循环使用会导致距离较远的两个物体可能被分配相同的颜色,如果它们又恰好相邻,也会造成视觉干扰。

因此,我们的目标不仅仅是替换一两个颜色,而是规划一套新的色彩策略。

2. 规划你的专属颜色方案:从理论到实践

在动手修改代码前,花几分钟规划一下颜色方案是值得的。一个好的方案应该考虑以下几点:

  1. 高对比度:框的颜色和掩膜填充色需要有明显的区分度。
  2. 类别区分:不同类别的物体最好使用色相差异大的颜色。
  3. 视觉友好:避免使用过于刺眼或饱和度过高的颜色组合,长时间查看不易疲劳。
  4. 实用性:考虑到打印(灰度)或色盲用户,可以适当引入纹理或边框样式的思考。

2.1 选择与生成颜色码

对于不熟悉色彩理论的朋友,这里有几个实用的工具和方法:

方法一:使用在线配色工具

  • Coolors.co:快速生成协调的调色板,可以锁定喜欢的颜色并重新生成其余部分。
  • Adobe Color:提供基于色轮规则(互补色、类似色等)的配色方案,非常专业。
  • Color Hunt:汇集了设计师们分享的现成精美配色方案,可以直接“拿来主义”。

方法二:使用系统画图工具取色(适合快速微调) 正如原始提示中提到的,Windows自带的“画图”工具或更专业的“画图3D”工具,都提供了颜色选取器和自定义颜色面板,可以直观地选择颜色并获取其十六进制码。

方法三:程序化生成 如果你需要大量颜色且希望它们均匀分布在色相环上,可以用一小段Python代码来生成:

import colorsys

def generate_colors(num_colors, saturation=0.9, value=0.9):
    """生成一组在HSV空间均匀分布的颜色"""
    colors = []
    for i in range(num_colors):
        hue = i / num_colors  # 色相在0-1之间均匀分布
        rgb = colorsys.hsv_to_rgb(hue, saturation, value)
        # 将RGB从0-1浮点数转换为0-255整数,再转为十六进制字符串
        hex_color = ''.join(f'{int(c*255):02X}' for c in rgb)
        colors.append(hex_color)
    return colors

# 生成20种颜色
my_palette = generate_colors(20)
print(my_palette)

2.2 设计两套颜色策略

为了解决框掩膜混淆,我推荐两种实战策略:

策略A:互补色对策略 为每个类别准备一对互补色或对比色,一个用于绘制边框,一个用于填充掩膜。这需要在绘图逻辑上进行更深度的定制,但效果最清晰。

策略B:优化调色板顺序策略(更简单直接) 这是我们本文重点实施的方法。我们不改变YOLOv8“一个索引对应一个颜色”的分配逻辑,而是重新排列 hexs 调色板中颜色的顺序,让相邻索引的颜色在视觉上截然不同。这样,即使框和掩膜使用了相邻索引(虽然通常相同,但系统在某些情况下可能分配相邻索引),也能很好地区分。

例如,我们可以手动将色相差异大的颜色交错排列:红、蓝、绿、紫、黄、青……而不是默认的红、粉、橙、黄绿……

下表展示了一个优化后的调色板前10色示例,并与默认顺序进行了对比:

索引默认调色板颜色 (示例)视觉描述优化后调色板颜色 (示例)视觉描述优化思路
0FF3838亮红色FF3838亮红色保留高辨识度红色
1FF9D97浅粉红2C99A8青色跳至冷色系,形成强对比
2FF701F橙色0018EC蓝色再次跳至对比色
3FFB21D橙黄色3DDB86亮绿色使用绿色,与蓝、红均不同
4CFD231黄绿色8438FF紫色引入紫色系
548F90A荧光绿FFB21D橙黄色穿插暖色
692CC17草绿色00D4BB蓝绿色使用另一种青色
73DDB86亮绿色FF701F橙色穿插暖色
81A9334深绿色CB38FF紫红色使用紫红色
900D4BB蓝绿色92CC17草绿色补入绿色系

这个表格展示了如何通过重新排序,让相邻序号的颜色在色相环上相距更远,从而最大化视觉差异。

3. 手把手修改代码:两种安全的方法

现在,我们进入实操环节。修改核心文件有两种推荐做法,推荐使用方法二,它更安全,便于维护和团队协作。

3.1 方法一:直接修改 plotting.py 文件(简单但不易维护)

这是最直接的方法,但会改变YOLOv8库的原始文件,未来更新库时可能会被覆盖。

  1. 备份原文件:在修改前,强烈建议复制一份 plotting.py 文件,例如重命名为 plotting.py.backup。
  2. 编辑 Colors 类:找到 __init__ 方法中的 hexs 元组。
  3. 替换颜色码:将你精心准备的新颜色码元组替换进去。例如,使用我们上面规划的部分优化颜色:
hexs = ('FF3838', '2C99A8', '0018EC', '3DDB86', '8438FF', 'FFB21D', '00D4BB', 'FF701F', 'CB38FF', '92CC17',
        'FF9D97', '48F90A', 'CFD231', '1A9334', '344593', '6473FF', '520085', 'FF95C8', 'FF37C7', '00C2FF')

这里我混合了重新排序和保留部分原颜色,你可以完全自定义。

  1. 保存文件。

提示:如果你只需要修改很少的类别颜色,比如只想让“人”这个类别的框和掩膜颜色固定为深蓝色和浅蓝色,而不动其他类别,直接修改 hexs 中对应索引的颜色即可。你需要先弄清楚你的类别ID映射关系。

3.2 方法二:创建自定义工具类并猴子补丁(推荐)

这是一种更优雅、非侵入式的方法。我们不直接修改库文件,而是在自己的项目中创建一个新的模块,在运行时“替换”掉原来的 Colors 类。

  1. 在你的项目根目录下创建一个新文件,例如 custom_plotting.py。
  2. 在新文件中编写自定义的 Colors 类,并实现猴子补丁:
# custom_plotting.py
from ultralytics.utils.plotting import Colors as OriginalColors

class CustomColors:
    """自定义颜色调色板,解决颜色混淆问题"""
    def __init__(self):
        # 定义你自己的优化颜色列表
        self.custom_hexs = ('FF3838', '2C99A8', '0018EC', '3DDB86', '8438FF', 'FFB21D',
                            '00D4BB', 'FF701F', 'CB38FF', '92CC17', 'FF9D97', '48F90A',
                            'CFD231', '1A9334', '344593', '6473FF', '520085', 'FF95C8',
                            'FF37C7', '00C2FF')
        self.palette = [self.hex2rgb(f'#{c}') for c in self.custom_hexs]
        self.n = len(self.palette)

    def __call__(self, i, bgr=False):
        c = self.palette[int(i) % self.n]
        return (c[2], c[1], c[0]) if bgr else c

    @staticmethod
    def hex2rgb(h):
        return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))

# 应用猴子补丁:替换原模块中的Colors类
import ultralytics.utils.plotting as plotting_module
plotting_module.Colors = CustomColors

print("[INFO] 自定义颜色调色板已加载。")
  1. 在你的主程序或推理脚本的最开头,导入这个自定义模块:
# 你的推理脚本 main.py 或 predict.py
import custom_plotting  # 这行会执行猴子补丁,替换Colors类
from ultralytics import YOLO

# 后续的模型加载和预测代码...
model = YOLO('yolov8n-seg.pt')
results = model.predict(source='your_image.jpg', save=True)

这种方法的好处是:

  • 零污染:完全不修改YOLOv8库的源代码。
  • 可维护:所有自定义逻辑集中在自己的文件中。
  • 灵活切换:通过注释掉 import custom_plotting 这一行,即可轻松切换回默认颜色方案。

4. 验证与效果调试:确保修改生效

修改完成后,必须通过实际推理来验证效果。我们使用一个包含多类别、多物体的复杂场景图片进行测试,比只用一张简单的巴士图片更能说明问题。

4.1 执行推理命令

打开终端或命令行,切换到你的项目目录,运行预测命令。确保你的模型路径和图片路径正确。

# 基础命令,使用你的自定义猴子补丁
python your_predict_script.py

# 或者,如果你使用方法一直接修改了库文件,可以直接使用yolo命令
yolo predict model=path/to/your/yolov8n-seg.pt source='path/to/test_image.jpg' save=True

在 your_predict_script.py 中,你的代码应该类似这样:

import custom_plotting  # 确保这行在最前面
from ultralytics import YOLO
import cv2

model = YOLO('models/yolov8m-seg.pt')  # 使用分割模型
# 使用一张包含多物体、背景复杂的图片进行测试
results = model.predict(source='test_data/crowded_street.jpg',
                         save=True,
                         save_txt=False,
                         show_labels=True,
                         show_conf=True)

# 你也可以直接处理results对象,获取带标注的图片
for r in results:
    im_array = r.plot()  # 这会使用我们修改后的Colors类进行绘制
    cv2.imwrite('output/custom_color_result.jpg', im_array)

4.2 效果评估与迭代

生成结果图片后,仔细检查:

  • 不同类别的物体是否使用了易于区分的颜色?
  • 同一图片中的不同实例(尤其是相邻的同类物体)颜色是否足够不同?
  • 边界框和其内部的掩膜填充色是否避免了混淆?这是我们的核心目标。

如果效果不理想,比如某些颜色在特定背景下依然不醒目,你需要回到 custom_plotting.py 中调整 custom_hexs 列表。这是一个迭代的过程。你可以考虑:

  • 将容易混淆的颜色替换为对比度更高的颜色。
  • 针对你的特定数据集(如医学图像多是灰阶,卫星图像多有绿色)定制背景互补色。
  • 如果类别数很少(<10),可以手动为每个类别指定一对固定的框色和掩膜色,这需要更深入地重写 plot 函数中的绘图逻辑,但控制力最强。

5. 进阶技巧与注意事项

5.1 处理类别数超过调色板颜色数的情况

当检测或分割的类别超过20个时,Colors 类会通过取模运算循环使用颜色。这可能导致第21类和第1类颜色相同。如果你希望为大量类别提供更多唯一颜色,只需扩展你的 custom_hexs 元组即可。添加足够多的、视觉差异明显的颜色码。例如,使用我们之前提供的 generate_colors 函数生成50或100种颜色。

5.2 修改特定类别的颜色(固定映射)

有时,你可能希望某些重要类别(如“人”、“车”)始终使用特定的、符合行业惯例的颜色(如人用红色,车用蓝色)。这需要将类别ID固定映射到颜色索引。

你可以在自定义的 Colors 类中重写 __call__ 方法,加入一个映射字典:

class CustomColorsWithMapping:
    def __init__(self):
        self.custom_hexs = (...)  # 你的颜色列表
        self.palette = [self.hex2rgb(f'#{c}') for c in self.custom_hexs]
        self.n = len(self.palette)
        # 定义类别ID到固定颜色索引的映射 (例如 COCO数据集: 0: person, 2: car)
        self.special_mapping = {0: 0,  # 人 -> 调色板索引0 (红色)
                                2: 2}  # 车 -> 调色板索引2 (蓝色)
    def __call__(self, i, bgr=False):
        # 如果类别在特殊映射中,使用指定索引,否则使用默认的取模逻辑
        color_index = self.special_mapping.get(int(i), int(i) % self.n)
        c = self.palette[color_index]
        return (c[2], c[1], c[0]) if bgr else c

5.3 保存与分享你的配置

如果你找到了一套在多个项目上都表现优异的颜色方案,可以将 custom_hexs 列表单独保存为一个配置文件(如 color_palette.yaml 或 .json),然后在 CustomColors 类中加载它。这样便于团队统一可视化风格,也方便在不同模型间复用。

最后,修改可视化颜色虽然是一个细节,但它体现了对项目完整性和专业性的追求。清晰直观的预测结果,能让你在团队评审、客户演示或论文图表展示中占据先机。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐