前言
最近在了解一个医学图像的项目,自己一个人开始调研并进行初步的可行性验证。所以重新开始写一些代码,选用的技术方案是python语言 + opencv图像处理库做一些图像上的处理。
在过程中碰到一些具体的技术问题,做一个记录。
一些小技巧
图层相关操作
数组array是Numpy库中最基本对象,而OPENCV通过cv2.imread函数(默认)读取进来的图像为
w
i
d
t
h
∗
h
e
i
g
t
∗
c
h
a
n
n
e
l
s
width * heigt * channels
width∗heigt∗channels
的一个三维数组,这个三维数组就是通过np.array这个结构来保存的。
-
数组切片
图像操作中的一个常用的操作就是获取RGB中的其中一个图层,这里需要注意的一点是,虽然我们经常说RGB,但是通过cv2.imread函数读取进来的图层顺序是B(Blue), G(Green), R(Red)。所以获取一个图层单独数据的代码可以是:
img = cv2.imread(src) img_blue = img[:, :, 0:1] img_green = img[:, :, 1:2] img_red = img[:, :, 2:3] print(img_blue.shape)
通过python语言中的切片操作来获取某一个图层的数据,这样获得的数据结构是一个
w i d t h ∗ h e i g t ∗ 1 width*heigt*1 width∗heigt∗1
的三维数组。 -
数组降维
opencv中很多函数的处理对象都是二值化图像,而二值化函数
cv2.threshold(img, threshold_value, 255, cv2.THRESH_BINARY)
的处理对象只能是一个二维数组,上述切片操作获得图层数据后,还需要进行降维操作,可通过代码:
img_red_2D = img_red[:, :, 0] print(img_red_2D.shape)
来获得一个二维数组,这样代码的输出结果就是
w i d t h ∗ h e i g t width*heigt width∗heigt
的二维数组。 -
和cv2.cvtColor的关系
在opencv中,cv2.imread函数可以加上参数:
img = cv2.imread(src, cv2.COLOR_BGR2GRAY)
来确定通过什么样的方式来加载图像。opencv会根据原始图像是否有多个图层来进行计算,具体计算规则可以参考opencv的官方文档。
就具体问题而言,我接触到的项目中,原始图像是分成三个文件,每个文件保存一个图层的数据,我的处理方式就是通过多个对象来保存数据:
blue = cv2.imread(src_blue, cv2.COLOR_BGR2GRAY) green = cv2.imread(src_green, cv2.COLOR_BGR2GRAY) red = cv2.imread(src_red, cv2.COLOR_BGR2GRAY)
当然,也可以不加参数直接加载单层图像,opencv会自动将其扩展为3个通道:
img = cv2.imread(src_blue)
通过调试来观察其区别:
img的结构(自动扩展):
blue的结构(单图层结构):
可以看出,对于单图层,如果不指定加载方式,opencv会自动将单图层的数据复制三份。这样加载后再通过切片降维后得到的数据是一样的。
-
图层合并 & 数组升维
我的项目中,原始图像是单通道图像,所以我是通过指定cv2.COLOR_BGR2GRAY的方式加载,得到的是一个二维数组,而最终需要展示出一个RGB图像,所以需要对这些数据进行合并。
我的合并代码如下:
def combineMutiChannel(blue, green, red): img_combine = np.zeros((blue.shape[0], blue.shape[1], 3), dtype="uint8") img_combine[:, :, 0:1] = blue[:, :, np.newaxis] img_combine[:, :, 1:2] = green[:, :, np.newaxis] img_combine[:, :, 2:3] = red[:, :, np.newaxis] return img_combine
通过blue[:, :, np.newaxis]将二维数组补充一个新的维度,再通过切片操作进行赋值。
这里有一个需要注意的点,如果是通过np.zeros/np.ones等函数创建的数组,最好是显式的指定数据格式,因为RGB的数据都是UINT8,而np有时候得到的数据会是float64等数据类型,这样在opencv的很多操作中都会报错。
OPENCV图像操作
在使用opencv的过程中,用到了几个基本的图像处理函数,做个记录,用于后续备查。
-
掩膜 & 连通域
掩膜操作是图像处理中非常常见的一个操作:相当于使用一个和目标区域相同大小的布尔值矩阵,如果为true,就将这个像素点取出
我在项目中的用处主要是找连通域,在opencv的找连通域函数connectedComponentsWithStats中,会返回一个和目标图像尺寸相同的标记图像,用不同的数字来标记不同的连通域。
代码如下:
def filterConnectZone(src_img): # 膨胀操作 kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) bin_clo = cv2.dilate(src_img, kernel2, iterations=1) num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(bin_clo, connectivity=8) # 查看各个返回值 # 连通域数量 # print('num_labels = ', num_labels) # # 连通域的信息:对应各个轮廓的x、y、width、height和面积 # print('stats = ', stats) # # 连通域的中心点 # print('centroids = ', centroids) # # 每一个像素的标签1、2、3.。。,同一个连通域的标签是一致的 # print('labels = ', labels) # 过滤连通区域 # 面积小于 100 # 不是圆形,1 到x和到y的直径不一致,2 面积和圆形面积的比值 # output = np.zeros((dst_blue.shape[0], dst_blue.shape[1], 1), np.uint8) output = np.zeros((dst_blue.shape[0], dst_blue.shape[1]), np.uint8) for i in range(1, num_labels): mask = labels == i output[:, :, 0][mask] = np.random.randint(0, 255) output[:, :, 1][mask] = np.random.randint(0, 255) output[:, :, 2][mask] = np.random.randint(0, 255)
上述代码中,通过mask = labels == i获得一个和labels 尺寸相同的掩膜:mask。
对掩膜的使用就是:
output[:, :, 0][mask] = np.random.randint(0, 255) x = output[:, :, 0][mask]
相当于就是在output的第一个图层上套一个mask掩膜,输出是一个数组,这个数组就是mask中为true的位置所对应的像素点,并将这些像素点赋值为一个随机数。使用调试模式可以看到x为:
-
获取感兴趣区域
操作很简单,直接通过切片获得一个区域即可:
roi = img_combine_final[roi_LT_y: roi_RB_y, roi_LT_x: roi_RB_x]
需要注意的是,y坐标在前,x坐标在后。各位可以想一下x, y分别代表的含义就清楚了。
-
寻找图层间的重叠区域
我的方案是:使用cv2.bitwise_and求交集,再使用cv2.bitwise_or计算并集,再使用连通域求交并比,来判断重叠区域有多大。
dst_green = cv2.bitwise_and(output_blue, dst_green) dst_red = cv2.bitwise_and(output_blue, dst_red) dst_green = cv2.bitwise_or(output_blue, dst_green) dst_red = cv2.bitwise_or(output_blue, dst_red)