使用python将mask绘制到对应的图像上

在使用深度学习等方法处理计算机视觉问题而对图像进行处理的过程中,不可避免地要处理原始图像及其相应的mask。比如将mask绘制到原始图像上,将mask的轮廓绘制到原始图像上,提取mask的轮廓,或者已知mask的轮廓而将mask填充,等等。 尽管这些问题都不是复杂的问题,但使用频率比较高,而每一次对其进行处理时都会浪费时间甚至分心,而耽误真正的任务的执行。因此,本文就将在处理这些问题中的经验进行一下总结,同时也为以后的使用备忘。 当然,因为我的经验主要还是在医学图像的处理上,所以这里就以医学图像为例来进行处理。 首先我们来看这样一张原始图像:

这是一个肺部CT的一个slice,下面我们给出肺分割的mask,即从上面原始图像中分割出肺部区域的mask,如下图:

可以看到肺部mask刚好对应了原始图像中的肺部区域,两个图像相乘,即可从原始图像中提取出肺部区域。 这是已知mask之后,图像处理的几个需求: 1.从原始图像中取出mask区域; 2.将mask区域绘制到原始图像上。 其中,1比较简单,可以直接将原始图像与mask相乘即可;而2则有两种需求,一种是直接将mask像蒙版一样覆盖到原始图像上,这样虽然可以看到mask对应的区域,但绘制上去的mask会对原始图像造成影响;另一种则是将mask的轮廓绘制到原始图像上,这样即可以看到mask区域,对原始图像影响也较小。

1.在原始图像上绘制mask的轮廓

将原始mask使用OpenCV读进来mask = cv2.imread('mask.png', 0),可以看到,mask是一个二维numpy array,只有0和255两个值。显然,255对应的部分表示选中的区域,0表示未选中的区域。但是这样一个mask array却没有给出mask的轮廓;而绘制mask的轮廓,首先则要找到mask的轮廓。 找到mask的轮廓通常有这样两个包可以使用,一个就是上面用的OpenCV,还有一个是skimage里提供的,下面我们分别使用这两种工具来寻找轮廓进行绘制。

1.1 使用OpenCV寻找轮廓

OpenCV里有findContours函数,可以专门用来从mask中寻找其轮廓,如下:

def draw_mask_edge_on_image_cv2(image, mask, color=(0, 0, 255)):
coef = 255 if np.max(image) < 3 else 1
image = (image * coef).astype(np.float32)
contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
cv2.drawContours(image, contours, -1, color, 1)
cv2.imwrite(‘test.png’, image)

其中,第2和3行是用来对image进行一个前期处理,如果image的值是0~1的浮点数范围,将其map到0~255;如果直接是0~255的,则不进行处理;至于将image转化为float32格式,则是因为后面第5行对image进行操作时,需要image是float32或者uint8等几种格式,所以我们先期转好。 第4行,即使用findContours函数来从mask中寻找其轮廓(即contours),需要注意,这里传入的mask必须是二值图像;后面的两个参数则分别为mode和method,可以根据实际进行修改选用。 还需要注意的是这个函数寻找到的轮廓,即contours的格式,其为一个列表,列表的元素为一个个contour,每一个contour为一个numpy array,它是一个三维的array,其shape类似:(20, 1, 2),表示该contour一共有20个点,每个点包括x、y两个坐标,其顺序也是x, y。至于为什么是三维,我也搞不懂,应该是设计时就是这个样子吧。 第5行将image转化为OpenCV特殊的彩色图格式BGR,则是因为后面绘制轮廓时为了醒目,绘制了红色的轮廓,如果不先期将image转化为彩色图模式,则绘制的轮廓仍然为灰色,会造成误导。转换前,image的shape为335*335,转换后,则为335*335*3。 第6行是将寻找到的轮廓绘制到image上,这个drawContours函数的第1、2个参数分别为image和轮廓,第3个参数表示绘制第几个contour,设为-1,表示绘制所有的contours,然后就是轮廓的颜色,最后是轮廓的thickness,需要注意的是,如果设置为-1,则表示以填充模式绘制轮廓,又相当于将提取出的轮廓还原为原始mask了。 第7行将绘制了轮廓之后的image保存一下,以便查看效果。

1.2 使用skimage寻找轮廓

接着我们来看使用skimage包中的相应函数来寻找mask的轮廓并进行绘制,如下:

def draw_mask_edge_on_image_skimage(image, mask, color=(0, 0, 255)):
coef = 255 if np.max(image) < 3 else 1
image = (image * coef).astype(np.float32)
contours = measure.find_contours(mask, 0.5)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
for c in contours:
c = np.around(c).astype(np.int)
image[c[:, 0], c[:, 1]] = np.array(color)
cv2.imwrite(‘test.png’, image)

下面我们进行一下解释: 前几行与前类似,此处不再赘述。 第4行使用skimage.measure的find_contours函数来从mask里寻找轮廓,我不清楚mask这个是否像OpenCV一样还是需要为二值图像,不过第2个参数是level。 第5行与前类似。 第6行开始,因为skimage没有像OpenCV里那样的drawContours函数,所以需要我们自己手动去将找到的轮廓绘制到图像上。

1.3 skimage与OpenCV混用

既然skimage与OpenCV都可以从mask中寻找轮廓,而OpenCV可以将寻找到的轮廓绘制到原始图像上,那么是否可以使用skimage寻找而用OpenCV来绘制呢?当然可以,不过需要注意一些细节,即二者找到的轮廓的不同之处。代码如下:

def draw_mask_edge_on_skimage_using_cv2(image, mask, color=(0, 0, 255)):
coef = 255 if np.max(image) < 3 else 1
image = (image * coef).astype(np.float32)
contours = measure.find_contours(mask, 0.5)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
_contours = []
for c in contours:
c[:, [0, 1]] = c[:, [1, 0]]
_contours.append(np.around(np.expand_dims(c, 1)).astype(np.int))
cv2.drawContours(image, _contours, -1, color, 1)
cv2.imwrite(‘test.png’, image)

该程序的前面几行,与之前程序类似,从第6行开始,是其独特之处,即将skimage寻找到的轮廓转化为OpenCV的轮廓格式,然后用OpenCV的drawContours函数将轮廓绘制到图像上。 skimage中找到的轮廓的x, y坐标与OpenCV中的轮廓的x, y坐标刚好相反,所以首先需要对其进行交换;随后,因为skimage的轮廓坐标是二维的,而OpenCV中的则是三维的,所以需要对skimage找到的轮廓坐标进行维度扩展,在第1维上增加一个维度为1的维度。 还需要注意的是,skimage寻找到的轮廓坐标为浮点数(np.float64),而OpenCV寻找到的轮廓坐标,或者是其要求的轮廓坐标则为整数,所以这里需要有一个转换。 将其转换好之后,即可使用OpenCV的drawContours函数将其绘制出来了。

2.在原始图像上绘制mask

前面我们详细讲述了使用skimage和OpenCV将mask的轮廓绘制到原始图像上的方法,下面我们也把将mask绘制到原始图像上的方法进行一下备忘。

2.1 使用OpenCV进行绘制

如前所述,使用OpenCV的drawContours函数绘制寻找到的mask轮廓时,将最后一个参数thickness设置为-1,使用fill模式对轮廓进行填充,即将mask绘制到了原始图像上。 代码与前类似,此处不再重复。

2.2 编写程序进行绘制

在原始图像上绘制mask,即将mask绘制到原始图像上,也就是将mask中值为255的部分绘制到原始图像上,这样一想,问题变得简单。代码如下:

def draw_mask_on_image_cv2(image, mask):
coef = 255 if np.max(image) < 3 else 1
image = (image * coef).astype(np.float32)
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
image[…, 2] = np.where(mask == 255, 255, image[…, 2])
cv2.imwrite(‘test.png’, image)

前面几行代码,此前已经解释过。 唯一的不同在倒数第2行代码。该行代码将前一行转换为BGR的B、G、R三通道图像中的R通道进行了一些处理,将其中mask为255的部分设置为255,而其余部分保持不变。这样以来,mask范围中的像素点的R通道被设置为255,相当于mask范围被近似处理成了红色(虽然其他两个通道也有值,不是标准的红色,但大体上这些像素点在三通道图像上表现为红色)。

完整代码

本文的完整代码,请点击参阅GitHub上的文件