基于OpenCV批量分片高像素影像
为了更加精确的诊断和治疗,医疗影像往往是大像素(1920x1080)或超大像素图像(4k图像4096x2160)。这类图像的尺寸与深度学习实验数据常见尺寸(227x227,或32x32)有巨大的差别,不仅对网络的深度要求更高,所需的算力、内存、运算时间等成本也会更高。因此,在处理医疗图像或遥感图像这样的高像素图像时,往往都需要将其批量处理成小像素方片,并针对每一个方片进行识别或预测。
因此无需我们再人工进行处理,然而掌握高像素图像的分片技巧十分必要,我们将以数张大像素的图像为例展示批量分片高像素图像的代码与相关知识。
**星辰与病灶细胞一样,可能分布在图像的各个位置,也可能集中在图像上的某个区域,因此适合用来作为替代图。我们先以一张图像为例进行分片:
#!pip install opencv-python
import cv2
import os
import matplotlib.pyplot as plt
cv2.__version__
'4.5.5'
- 导入图像,进行查看
#设置图像所在的文件夹目录,你可以自由设置你自己的目录
PATH = r"E:\02_2022DL\HealthCareProject\image"
#使用opencv读入图像
img = cv2.imread(os.path.join(PATH,"imge01.jpg"))
#由于opencv在读取图像时,没有成功读取也不会报错
#因此在读取图像后需要检查图像是否为None
if img is None:
print("failed")
else:
print("succeed")
succeed
img.shape #高,宽
(1600, 2560, 3)
#将图像进行可视化,由于OpenCV的默认通道顺序是BGR,因此在可视化时需要转换为RGB
#对图像进行浅复制,避免在操作中误覆盖原图
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy()
plt.figure(dpi=200)
plt.imshow(imgcopy)
plt.axis("off");
imgcopy.shape #注意判断导入后的高宽顺序。我们平时说1920x1080是宽x高,现在导入后明显是高x宽
(1600, 2560, 3)
#像素值
imgcopy.shape[0] * imgcopy.shape[1]
4096000
我们的最终目标:将图像分割为数千个小型方片:
- 将一张图像分割为两半
分割的本质就是先裁剪图像、再对裁剪后的图像进行保存。例如,假设我们现在要将以下图像按中线分割为两部分,最简单的手段就是先在原始图像上裁剪出左半边的图像,保存为一个单独的文件,再在原始图像上裁剪出右半边的图像,保存为一个单独的文件。在这个过程当中,我们没有对原始图像做任何的更改,却可以得到两张新的图像。
由于图像数据都是三维矩阵,因此只要通过索引就可以轻松将图像进行裁剪。对一张图像来说,索引的顺序是从上到下、从左到右,因此,如果要取出整个图像的左侧,则需要对图像进行如下索引:
imgcopy.shape #2560
(1600, 2560, 3)
imgcopy[:,0:1280].shape #取出一半的宽,全部的高,不对通道做任何处理
(1600, 1280, 3)
#尝试将索引出的图像可视化
plt.figure(dpi=140)
plt.imshow(imgcopy[:,0:1280])
plt.axis("off");
#也可以尝试索引出图像的右侧
plt.figure(dpi=140)
plt.imshow(imgcopy[:,1280:]) #依然是全部的高,不过宽是从1280中线开始向后取
plt.axis("off");
不难发现,对图像进行截取时,只需要按照图像[高的起点:高的终点,宽的起点:宽的终点]
进行索引即可,只要我们知道图像当中的相对位置(四个坐标),就可以按照任意的起点和终点截取任何尺寸、任何形状的图片。接下来,当我们索引出想要的图像后,只需要将这张图像保存成新的图片文件即可。在这里我们要使用的是opencv中的经典方法imwrite:
cv2.imwrite(保存图像的目录+文件名,需要保存的图片对象)
例如,上述星辰图的左侧保存到之前设置好的PATH目录,则有:
PATH
'E:\\02_2022DL\\HealthCareProject\\image'
os.path.join(PATH,"star1.jpg") #确定文件名
'E:\\02_2022DL\\HealthCareProject\\image\\star1.jpg'
cv2.imwrite(os.path.join(PATH,"star2.jpg"),imgcopy[:,0:1280])
True
返回True,则说明保存成功,如果报错或返回False则保存失败。此时我们可以在PATH设定的目录下查看保存出的文件。
- 将一张图像分割为小尺寸方片
将图像分割为小尺寸方片的原理与将图像分割成两半的原理完全一致:首先,从图像中裁剪出我们需要的小尺寸方片,然后将该方片对象保存为一张新的图像。然而,一张大尺寸图像可以被分割为成千上万的小尺寸方片,因此我们需要将每个小方片一一索引出来,再一一保存:
我们必然不可能手动一一裁剪方片,因此我们需要一个循环来帮助我们进行分割。观察上图,每个方片的尺寸为50x50,左上角第一个被切分出的方片索引为图像[:50,:50]
,紧接着左数第二个方片的索引为图像[:50,50:100]
,第三个方片的索引为图像[:50,100:150]
以此类推,第一行的所有方片都可以被表示为图像[:50,x:x+50]
。可见,我们只需要在宽度上进行循环,每次让宽的起点增加50,宽的终点增加50,就可以实现第一行的截取和分割。同理,第一列的所有方片都可以被表示为图像[y:y+50,:50]
,所以只要在高度上进行循环,每次让高的起点增加50,高的终点增加50,就可以实现第一列的截取和分割。那只要同时在高和宽上进行循环,就可以实现对上图中所有方片的截取和分割。
以宽为例,我们可以有:
imgcopy.shape[1]
2560
[*range(0,imgcopy.shape[1],50)] #从0开始,到宽的最大值结束,每50个数取出一个数
[0,
50,
100,
150,
200,
250,
300,
350,
400,
450,
500,
550,
600,
650,
700,
750,
800,
850,
900,
950,
1000,
1050,
1100,
1150,
1200,
1250,
1300,
1350,
1400,
1450,
1500,
1550,
1600,
1650,
1700,
1750,
1800,
1850,
1900,
1950,
2000,
2050,
2100,
2150,
2200,
2250,
2300,
2350,
2400,
2450,
2500,
2550]
for x in range(0,imgcopy.shape[1],50): #把range中的值作为宽的起点,再给该起点加上50像素,作为终点
print("[{}:{}]".format(x,x+50))
[0:50]
[50:100]
[100:150]
[150:200]
[200:250]
[250:300]
[300:350]
[350:400]
[400:450]
[450:500]
[500:550]
[550:600]
[600:650]
[650:700]
[700:750]
[750:800]
[800:850]
[850:900]
[900:950]
[950:1000]
[1000:1050]
[1050:1100]
[1100:1150]
[1150:1200]
[1200:1250]
[1250:1300]
[1300:1350]
[1350:1400]
[1400:1450]
[1450:1500]
[1500:1550]
[1550:1600]
[1600:1650]
[1650:1700]
[1700:1750]
[1750:1800]
[1800:1850]
[1850:1900]
[1900:1950]
[1950:2000]
[2000:2050]
[2050:2100]
[2100:2150]
[2150:2200]
[2200:2250]
[2250:2300]
[2300:2350]
[2350:2400]
[2400:2450]
[2450:2500]
[2500:2550]
[2550:2600]
imgcopy[:50,2550:2600].shape
(50, 10, 3)
现在,同时对宽和高执行循环:
#每次分割前,获取完整的原始图像
imgcopy = cv2.cvtColor(img, cv2.COLOR_BGR2RGB).copy()
#获取图像的高度和宽度
imgheight = imgcopy.shape[0]
imgwidth = imgcopy.shape[1]
#确定每个方片的尺寸
patch_height = 50
patch_weight = 50
#令x为宽上的起点,y为高上的起点,开始循环
for y in range(0, imgheight, patch_height):
for x in range(0, imgwidth, patch_weight):
#首先检查:设置的方片尺寸是否大于原始图像尺寸?如果是,则直接打断循环,不进行分割
if patch_height > imgheight or patch_weight > imgwidth:
print("方片尺寸过大,超过原始尺寸")
break
#方片尺寸没有大于原始图像尺寸,则计算宽和高上的终点
y_ = y + patch_height
x_ = x + patch_weight
#接着检查,宽和高上索引的终点是否大于了原始图像尺寸?
#当起点位于图像上,但终点落在图像外的时候,我们可以规定几种选择
#舍弃所有不足规定方片尺寸的图像,或者保存这些残缺的图像
#对于病理识别来说,损失边缘的一点点信息并无大碍
#因此我们规定,只有当高和宽的终点都落在图像上时,才进行切片和保存
if imgheight >= y_ and imgwidth >= x_:
#索引出切片
patch = imgcopy[y:y_, x:x_]
#保存索引出的图像,如果想,可以在图像文件名中保存四角坐标,也可以选择只保存左上角坐标
cv2.imwrite(os.path.join(PATH,"patches"
,"universe"+"x"+str(x)+"_"+str(x_)+"y"+str(y)+"_"+ str(y_) +".jpg")
, patch)
#保存之后,在原始图像上对当前索引出的区域绘制白色边框
#注意这一操作将会在正在被切片的图像上进行
cv2.rectangle(imgcopy #要绘制长方体的对象
, (x, y), (x_, y_) #整个正方形的左上角和右下角的坐标
, (255, 255, 255) #使用的颜色
, 2 #线条的粗细,数字越大越粗
)
#循环完毕后,绘制被我们分割后的图像
plt.figure(dpi=300)
plt.imshow(imgcopy)
plt.axis("off");
现在可以检查目标文件夹,观察所有被分割的图像了。
- 批量处理大型图像
当我们有大量的大型图像需要处理时,我们首先要从文件夹中将所有的图像都读入。在这里,我们需要借助os库下著名函数listdir的力量:
os.listdir(path):读取目录中所有的文件名
只要有文件名,我们就可以在cv2.imread
当中按照文件名一一读入相应的图像。同时,对于不是图片数据的文件名,cv.imread
会读取失败,从而导致读入的对象为None。因此我们也需要对读入的对象进行检查,对于任何不是None的对象(也就是对于任意成功读取的图像),我们将其保存在列表当中:
def load_images_from_folder(folder):
"""批量读取文件夹中的图片"""
images = [] #将准备读取的图像保存在列表当中,因为arrays是不允许在循环中逐渐添加对象的
for filename in os.listdir(folder):
img = cv2.imread(os.path.join(folder,filename))
if img is not None:
images.append(img)
return images
images = load_images_from_folder(PATH)