使用颜色空间进行图像分割

原文地址:https://realpython.com/python-opencv-color-spaces/

这可能是一个深度学习和大数据的时代,在这个时代,复杂的算法通过显示数百万幅图像来分析图像,但是颜色空间对于图像分析仍然非常有用。简单的方法仍然是强大的。

在本文中,您将学习如何使用OpenCV基于Python中的颜色从图像中简单地分割对象。OpenCV是一个流行的计算机视觉库,用c/c++编写,带有Python绑定,提供了操作颜色空间的简单方法。

虽然你不需要已经熟悉OpenCV或本文中使用的其他助手包,但我们假设你至少对Python中的编码有了基本的了解。

什么是颜色空间?

在最常见的颜色空间RGB(红、绿、蓝)中,颜色以其红、绿、蓝三种成分表示。在更专业的术语中,RGB将颜色描述为三个成分的元组。每个组件可以取0到255之间的值,其中元组(0,0,0)表示黑色,(255,255,255)表示白色。

RGB被认为是一个附加颜色空间,颜色可以想象为由大量的红色、蓝色和绿色光线照射到黑色背景上而产生,以下是RGB颜色的一些例子:

RGB是五种主要颜色空间模型之一,每种模型都有许多分支。有这么多颜色空间,因为不同的颜色空间对于不同的目的是有用的。

在印刷领域,CMYK非常有用,因为它描述了从白色背景产生颜色所需的颜色组合。RGB中的0元组是黑色的,而CMYK中的0元组是白色的。我们的打印机包含青色、品红色、黄色和黑色墨盒。

在某些类型的医疗领域,装有染色组织样本的载玻片被扫描并保存为图像。它们可以在HED空间中进行分析,HED空间是应用于原始组织的染色类型——苏木精、曙红和DAB——饱和度的表示。

HSVHSL是色调、饱和度和亮度的描述,对于识别图像中的对比度特别有用。这些颜色空间经常用于软件和网页设计中的颜色选择工具。

实际上,颜色是一个连续的现象,意味着有无限多的颜色。然而,颜色空间通过离散结构(固定数量的整数数值)来表示颜色,这是可以接受的,因为人眼和感知也是有限的。颜色空间完全能够代表我们能够区分的所有颜色。

既然我们理解了颜色空间的概念,我们可以继续在OpenCV中使用它们。

使用颜色空间进行简单分割

为了演示颜色空间分割技术,我们在real-Python材料库中提供了一个尼莫鱼图像数据集,供您下载和玩耍。小丑鱼很容易被它们明亮的橙色识别,所以它们是好的分割候选。让我们看看在一张图片中找到尼莫鱼有多精确。

你需要遵循的关键Python包是NumPy—Python中最重要的科学计算包,matplolib—绘图库,当然还有OpenCV。

颜色空间和使用opencv读取图像

首先,你需要设置你的环境。本文将假设您的系统上安装了Python 3.x。请注意,虽然OpenCV的当前版本是3.x,但是要导入的包的名称仍然是cv2,通过pip3 install opencv-python命令进行安装(没有pip3,用pip也行)。

>>> import cv2

成功导入OpenCV后,您可以查看OpenCV提供的所有颜色空间转换,并将它们全部保存到变量中:

>>> flags = [i for i in dir(cv2) if i.startswith('COLOR_')]

根据您的OpenCV版本,标志的列表和数量可能略有不同,但是不管怎样,会有很多标志!查看您有多少个可用的标志:

>>> len(flags)
258
>>> flags[40]
'COLOR_BGR2RGB'

COLOR_后面的第一个字符表示原始颜色空间,2后面的字符表示目标颜色空间。此标志表示从BGR(蓝色、绿色、红色)到RGB的转换。正如你所看到的,这两个颜色空间非常相似,只有第一个和最后一个通道交换。

你需要matplotlib.pyplot来查看图像,需要NumPy来处理一些图像。如果尚未安装Matplotlib或NumPy,则在尝试导入之前,您需要pip3安装Matplotlib和pip3安装NumPy:

>>> import matplotlib.pyplot as plt
>>> import numpy as np

现在,您可以加载和检查图像了。请注意,如果您是从命令行或终端工作,您的图像将出现在弹出窗口中。如果你在Jupyter笔记本或类似的东西上工作,它们会简单地显示在下面。不管您的设置如何,您都应该看到show()命令生成的图像:

>>> nemo = cv2.imread('./images/nemo0.jpg')
>>> plt.imshow(nemo)
>>> plt.show()

嘿,尼莫……还是多莉?你会注意到,蓝色和红色的频道似乎已经混在一起了。事实上,默认情况下,OpenCV读取BGR格式的图像。您可以使用cvtColor (图像、标志)和我们在上面看到的标志来解决这个问题:

>>> nemo = cv2.cvtColor(nemo, cv2.COLOR_BGR2RGB)
>>> plt.imshow(nemo)
>>> plt.show()

现在尼莫看起来更像他自己。

在RGB颜色空间可视化小丑鱼

HSV是按颜色分割颜色空间的一个很好的选择,但是为了了解原因,让我们通过可视化其像素的颜色分布来比较RGB和HSV颜色空间中的图像。3D图很好地显示了这一点,每个轴代表颜色空间中的一个通道。如果您想知道如何制作3D绘图,请查看下面部分: 要绘制该图,您还需要几个Matplotlib库:

>>> from mpl_toolkits.mplot3d import Axes3D
>>> from matplotlib import cm
>>> from matplotlib import colors

这些库提供了绘图所需的功能。您希望根据每个像素的组件将每个像素放置在其位置,并根据其颜色对其进行着色。cv2.split()在这里非常方便;它将图像分割成其分量通道。这几行代码分割图像并设置3D绘图:

>>> r, g, b = cv2.split(nemo)
>>> fig = plt.figure()
>>> axis = fig.add_subplot(1, 1, 1, projection="3d")

既然已经设置了绘图,就需要设置像素颜色。为了根据每个像素的真实颜色为其上色,需要进行一些整形和归一化。它看起来很凌乱,但实际上你需要将图像中每个像素对应的颜色展平成一个列表并归一化,这样它们就可以传递到Matplotlib scatter()的facecolors参数。

归一化只是指根据facecolors参数的要求,将颜色范围从0-255缩小到0-1。最后,facecolors想要一个列表,而不是一个NumPy数组:

>>> pixel_colors = nemo.reshape((np.shape(nemo)[0]*np.shape(nemo)[1], 3))
>>> norm = colors.Normalize(vmin=-1.,vmax=1.)
>>> norm.autoscale(pixel_colors)
>>> pixel_colors = norm(pixel_colors).tolist()

现在,我们已经准备好绘制所有组件:每个轴的像素位置及其对应的颜色,按照facecolors期望的格式。您可以构建散点图并查看它:

>>> axis.scatter(r.flatten(), g.flatten(), b.flatten(), facecolors=pixel_colors, marker=".")
>>> axis.set_xlabel("Red")
>>> axis.set_ylabel("Green")
>>> axis.set_zlabel("Blue")
>>> plt.show()

从这个图中,你可以看到图像的橙色部分跨越了几乎整个范围的红色、绿色和蓝色值。由于Nemo的一部分延伸到整个情节,根据RGB值的范围在RGB空间分割Nemo并不容易。

在HSV颜色空间可视化小丑鱼

我们在RGB空间看到尼莫,所以现在让我们在HSV空间看到他并进行比较。 正如上面简要提到的,HSV代表色调、饱和度和值(或亮度),是一个圆柱色空间。颜色或色调被建模为围绕中心垂直轴旋转的角度尺寸,这表示值通道。值从暗(底部为0 )到亮(顶部为0 )。第三个轴“饱和度”定义了色调的深浅,从垂直轴上的最不饱和到离中心最远的最饱和:

要将图像从RGB转换为HSV,可以使用cvtColor():

>>> hsv_nemo = cv2.cvtColor(nemo, cv2.COLOR_RGB2HSV)

现在,HSV_Nemo在HSV中存储了尼莫的表示。使用与上面相同的技术,我们可以查看HSV中的图像图,HSV中显示图像的代码与RGB中的代码相同。请注意,您使用相同的pixel_colors变量为像素着色,因为Matplotlib希望这些值以RGB为单位:

>>> h, s, v = cv2.split(hsv_nemo)
>>> fig = plt.figure()
>>> axis = fig.add_subplot(1, 1, 1, projection="3d")

>>> axis.scatter(h.flatten(), s.flatten(), v.flatten(), facecolors=pixel_colors, marker=".")
>>> axis.set_xlabel("Hue")
>>> axis.set_ylabel("Saturation")
>>> axis.set_zlabel("Value")
>>> plt.show()

在HSV空间中,尼莫的橙色更加本地化,视觉上也更加分离。橙子的饱和度和价值确实有所不同,但它们大多位于色调轴上的小范围内。这是可用于分段的关键点。

选取范围

让我们根据一系列简单的橙色来判断尼莫的阈值。你可以通过观察上面的图或者在线使用颜色挑选应用程序来选择范围,比如这个RGB到HSV工具。这里选择的色板是浅橙色和深橙色,几乎是红色:

>>> light_orange = (1, 190, 200)
>>> dark_orange = (18, 255, 255)

在Python中显示颜色的一个简单方法是制作所需颜色的小正方形图像,并在Matplotlib中绘制。matplotlib只解释RGB中的颜色,但是为主要颜色空间提供了方便的转换功能,以便我们可以在其他颜色空间绘制图像:

>>> from matplotlib.colors import hsv_to_rgb

然后,构建小的10x10x3正方形,填充相应的颜色。您可以使用NumPy轻松地用颜色填充正方形:

>>> lo_square = np.full((10, 10, 3), light_orange, dtype=np.uint8) / 255.0
>>> do_square = np.full((10, 10, 3), dark_orange, dtype=np.uint8) / 255.0

最后,通过将它们转换为RGB进行查看,您可以将它们绘制在一起:

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(hsv_to_rgb(do_square))
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(hsv_to_rgb(lo_square))
>>> plt.show()

产生这些图像,用选择的颜色填充:

一旦你获得了合适的颜色范围,你可以使用cv2.inrange()来尝试阈值Nemo,inRange()采用三个参数:图像、较低范围和较高范围。它返回图像大小的二进制掩码(ndarray为1和0),其中值1表示范围内的值,零值表示范围外的值:

>>> mask = cv2.inRange(hsv_nemo, light_orange, dark_orange)

要在原始图像的顶部加上遮罩,可以使用cv2.bittage_and(),使遮罩中的对应值为1:

>>> result = cv2.bitwise_and(nemo, nemo, mask=mask)

要查看到底做了什么,让我们查看遮罩和顶部带有遮罩的原始图像:

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(mask, cmap="gray")
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(result)
>>> plt.show()

这已经很好地捕捉了鱼的橙色部分。唯一的问题是尼莫也有白色条纹……幸运的是,添加第二个寻找白色的遮罩与你已经用橙色做的非常相似:

>>> light_white = (0, 0, 200)
>>> dark_white = (145, 60, 255)

一旦指定了颜色范围,就可以查看您选择的颜色:

>>> lw_square = np.full((10, 10, 3), light_white, dtype=np.uint8) / 255.0
>>> dw_square = np.full((10, 10, 3), dark_white, dtype=np.uint8) / 255.0

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(hsv_to_rgb(lw_square))
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(hsv_to_rgb(dw_square))
>>> plt.show()

我在这里选择的上限是非常蓝的白色,因为白色在阴影中有蓝色的色彩。让我们制作第二个遮罩,看看它是否捕捉到尼莫的条纹。您可以像构建第一个遮罩一样构建第二个遮罩:

>>> mask_white = cv2.inRange(hsv_nemo, light_white, dark_white)
>>> result_white = cv2.bitwise_and(nemo, nemo, mask=mask_white)

>>> plt.subplot(1, 2, 1)
>>> plt.imshow(mask_white, cmap="gray")
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(result_white)
>>> plt.show()

不错!现在你可以组合这些遮罩了。将两个遮罩加在一起,无论哪里有橙色或白色,都会产生1个值,这正是所需要的。让我们一起添加遮罩并绘制结果:

>>> final_mask = mask + mask_white

>>> final_result = cv2.bitwise_and(nemo, nemo, mask=final_mask)
>>> plt.subplot(1, 2, 1)
>>> plt.imshow(final_mask, cmap="gray")
>>> plt.subplot(1, 2, 2)
>>> plt.imshow(final_result)
>>> plt.show()

本质上,你已经在HSV颜色空间中粗略地分割了Nemo。你会注意到分割边界上有一些杂散像素,如果你喜欢,你可以使用高斯模糊来清理小的错误检测。

高斯模糊是一种图像过滤器,它使用一种叫做高斯的函数来变换图像中的每个像素。它具有平滑图像噪声和减少细节的效果。以下是对我们的图像应用模糊的情况:

>>> blur = cv2.GaussianBlur(final_result, (7, 7), 0)
>>> plt.imshow(blur)
>>> plt.show()

这个分割是否可以泛化到小丑鱼的亲属

为了好玩,让我们看看这种分割技术推广到其他小丑鱼图像的效果如何。在这个资料库中,有六张谷歌尼莫鱼图片可供公众使用。图像在子目录中,索引为nemoi.jpg,其中I是0-5的索引。首先,将尼莫的所有亲戚载入一个列表:

path = "./images/nemo"

nemos_friends = []
for i in range(6):
   friend = cv2.cvtColor(cv2.imread(path + str(i) + ".jpg"), cv2.COLOR_BGR2RGB)
   nemos_friends.append(friend)

你可以将上面用来分割一条鱼的所有代码组合成一个函数,该函数将图像作为输入并返回分割的图像。如下所示:

def segment_fish(image):
    ''' Attempts to segment the clownfish out of the provided image '''

    # Convert the image into HSV
    hsv_image = cv2.cvtColor(image, cv2.COLOR_RGB2HSV)

    # Set the orange range
    light_orange = (1, 190, 200)
    dark_orange = (18, 255, 255)

    # Apply the orange mask 
    mask = cv2.inRange(hsv_image, light_orange, dark_orange)

    # Set a white range
    light_white = (0, 0, 200)
    dark_white = (145, 60, 255)

    # Apply the white mask
    mask_white = cv2.inRange(hsv_image, light_white, dark_white)

    # Combine the two masks
    final_mask = mask + mask_white
    result = cv2.bitwise_and(image, image, mask=final_mask)

    # Clean up the segmentation using a blur
    blur = cv2.GaussianBlur(result, (7, 7), 0)
    return blur

有了这个有用的功能,你可以分割所有的鱼:

results = [segment_fish(friend) for friend in nemos_friends]

让我们通过在一个循环中绘制结果来查看所有结果:

for i in range(1, 6):
    plt.subplot(1, 2, 1)
    plt.imshow(nemos_friends[i])
    plt.subplot(1, 2, 2)
    plt.imshow(results[i])
    plt.show()

总的来说,这种简单的分割方法已经成功地找到了尼莫的大多数亲戚。然而,很明显,用特定的光照和背景分割一条Nemo鱼未必能很好地推广到分割所有Nemo鱼。

总结

在本教程中,您已经看到了几个不同的颜色空间,一幅图像是如何分布在RGB和HSV颜色空间中的,以及如何使用OpenCV在颜色空间之间进行转换和分割范围。

总之,您已经了解了如何使用OpenCV中的颜色空间来执行图像中的对象分割,并希望看到它在执行其他任务方面的潜力。在控制照明和背景的情况下,例如在实验环境中或者在更均匀的数据集上,这种分割技术简单、快速、可靠。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大数据杂谈

【Excel系列】Excel数据分析:时间序列预测

移动平均 18.1 移动平均工具的功能 “移动平均”分析工具可以基于特定的过去某段时期中变量的平均值,对未来值进行预测。移动平均值提供了由所有历史数据的简单的平...

5659
来自专栏人工智能的秘密

算法教程:能够体现文本语义关系的关键词提取算法

关键词提取能让我们快速地了解一篇文章。在信息爆炸的时代,能够有效提取文本的关键词,对于快速、及时、高效地获取信息是非常有帮助的。本文介绍一种能够体现文本语义关系...

3700
来自专栏数说工作室

面板数据与Eviews操作指南(下)

三、动态面板数据及Eviews实现 (1)动态面板数据简介 在现实社会中,很多经济关系是动态的,有时需要引入滞后项去解释这些经济关系。动态面板数据模型,即面板数...

5177
来自专栏数说工作室

面板数据与Eviews操作指南(下)

面板数据与Eviews操作指南(下) 三、动态面板数据及Eviews实现 (1)动态面板数据简介 在现实社会中,很多经济关系是动态的,有时需要引入滞后项去解释这...

3918
来自专栏数据科学学习手札

(数据科学学习手札38)ggplot2基本图形简述

  上一篇中我们介绍了ggplot2的基本语法规则,为了生成各种复杂的叠加图层,需要了解ggplot2中一些基本的几何图形的构造规则,本文便就常见的基础几何图形...

3612
来自专栏tkokof 的技术,小趣及杂念

数学笔记(三)之镜面矩阵

  镜面变换在游戏中并不少见,相关资料网上也俯拾即是,不过自己总是感觉略显生疏,在此简单一记,算作是加深印象吧~

1041
来自专栏清墨_iOS分享

OpenGL ES-3D图形变换知识

1632
来自专栏一心无二用,本人只专注于基础图像算法的实现与优化。

图像纹理合成及纹理传输算法学习(附源码)。

    有2到3年没有逛CodeProject了,上班一时无聊,就翻翻这个比较有名的国外网站,在其Articles » Multimedia » General...

4348
来自专栏Android群英传

贝塞尔曲线开发的艺术

2492
来自专栏落影的专栏

GPUImage详细解析(十二)Sobel边界检测

前言 卷积运算是一个看似复杂的概念,今天来揭开这个神秘的面纱。 卷积矩阵:卷积矩阵是一个由权重数据组成的矩阵,中心像素周围像素的亮度乘以这些权重然后再相加就能...

4235

扫码关注云+社区

领取腾讯云代金券