1.数据集介绍
本项目所使用的数据集是BCCD数据集。
数据集包含三类血细胞照片:WBC
(白细胞)、RBC
(红细胞)、Platelets
(血小板)
可以在这里下载:BCCD Object Detection Dataset
本文所使用的是2021.2.25版本的。包含847张图片,每张图像大小为416x416,图像格式为.jpg
该版本以源图像进行了图像增强,包括:
- 50% 的概率进行水平翻转
- 50% 的概率进行垂直翻转
- 以等概率选择以下其中一种 90 度的旋转方式:不旋转、顺时针旋转、逆时针旋转、上下翻转
- 随机裁剪图像,裁剪的部分占图像的 0% 到 15%
- 随机亮度调整,亮度调整范围为 -15% 到 +15%
- 随机曝光调整,曝光调整范围为 -20% 到 +20%
所以在后面阶段不需要对图像进行增强操作。
当然新版本或者其他版本不同也不影响使用,一些版本图像大小不同,一些对图像并未做预处理,后期可能需要。
如果下载的数据集内容不一致,感兴趣的可以在这里下载,https://download.csdn.net/download/qq_43616651/88496033我已经上传。
该数据集已经分好test,train,vaild文件。
train 包含801张图片与801个包含图像标签与boxes的xml文件。
test 包含36张图片与36个包含图像标签与boxes的xml文件。
valid 包含73张图片与73个包含图像标签与boxes的xml文件。
其中每张图片的标签信息存放在xml文件中,可以用浏览器打开查看。
其中xml书写是对称的写法,比如size类是size开头,size结尾,中间包含width,height,depth三个参数。
对我们最有用的就是object类,其中对于神经网络使用的则是name与bndbox,表示图像中某个位置处细胞的名称,及其标注框bndbox。
其中bndbox中包含xmin,xmax,ymin,ymax元素。这里(xmin,ymin)组成标注框的左上角点位,(xmax,ymax)组成标注框右下角的点位。
对于检测框位置的表示有两种方式,一种是上述表述,给出标注框的左上角,与右下角。一种是给出标注框的中心位置与标注框的大小(即长宽),二者可以转换,这里就不细述。
以上就是数据集的介绍。
2.xml文件的读取
为了对xml的内容读取,这里使用的库是glob和xml.etree.ElemenTree
glob
是一个用于文件匹配的 Python 标准库模块,它可以帮助你在指定目录中查找文件和文件夹,以及根据文件名的模式匹配进行筛选。
glob的用法:
#导入glob库
import glob
#1.查找特定目录下的所有文件
file_list = glob.glob('/path/to/directory/*')
#这将返回 /path/to/directory 目录下的所有文件的列表。
#2.使用通配符来筛选文件
txt_files = glob.glob('/path/to/directory/*.txt')
#这将返回 /path/to/directory 目录下所有扩展名为 .txt 的文件。
#3.查找所有子目录中的文件
all_files = glob.glob('/path/to/directory/**/*.txt', recursive=True)
#这将返回 /path/to/directory 目录及其子目录中的所有 .txt 文件。
#4.使用多个通配符:
files = glob.glob('/path/to/directory/*.[jpg,png]')
这将返回 /path/to/directory 目录下所有扩展名为 .jpg 或 .png 的文件。
"""
glob 支持的通配符包括 *(匹配任何字符或字符序列)、
?(匹配任何单个字符)、[...](匹配字符集中的任何字符)、
[!...](不匹配字符集中的任何字符)等。
"""
glob可以快速实现文件,图像的批处理操作,数据集加载,图像文件批量处理。
xml.etree.ElementTree
是 Python 标准库中用于解析和处理 XML 数据的模块。
xml.etree.ElementTree的用法:
#导入模块
import xml.etree.ElementTree as ET
#解析parse XML文档
tree = ET.parse('example.xml') # 从文件中解析XML文档
root = tree.getroot() # 获取根元素
#或者,你也可以直接从XML字符串解析:
xml_data = '<root><element>data</element></root>'
root = ET.fromstring(xml_data)
#遍历XML树,通过遍历树来访问XML文档中的元素:
for child in root:
print(child.tag, child.text)
#查找元素
#使用 find 和 findall 方法来查找元素
element = root.find('element') # 查找第一个匹配的元素
elements = root.findall('element') # 查找所有匹配的元素
#访问元素属性:
element = root.find('element')
attribute_value = element.get('attribute_name')
#创建新元素:
new_element = ET.Element('new_element')
new_element.text = 'data'
root.append(new_element)
#保存XML文档:
tree.write('new_example.xml')
#它还提供了许多其他功能,例如修改XML文档、删除元素、处理命名空间等。
基于以上库,实现读取train/test/valid数据集下的图像与xml文件,并将图像中的target信息以及bndbox从xml文件中解析出来。
以下是代码:
path = './data/train/' #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
#乱序文件调整顺序
xml_list = [] #列表申请
img_list = []
for i in xml_file:
img = i[:-3]+'jpg' #将xml文件名后三位改成jpg,就是图像的文件名
if img in img_file: #判断该文件名是不是在img_file中,如果在,那就加入img_list
img_list.append(img)
xml_list.append(i)
#以上只是读取图像文件名,xml文件名
an_file =open(xml_file[0],encoding='utf-8') #用utf-8的格式打开该文件
tree = ET.parse(an_file) #用ET来解析an_file,得到文件内容树格式
root = tree.getroot() #获取树的根目录,抓取xml中的数据,与html的爬取是一样的
#root findall 可以找目标标签,比如xml里的size,object等元素的内容
bndbox = [] #列表
for object in root.findall('object'):
cell = object.find('name').text #同理拿到object的name属性,可以得到细胞的类名
xmin = object.find('bndbox').find('xmin').text #拿到候选框的位置
ymin = object.find('bndbox').find('ymin').text
xmax = object.find('bndbox').find('xmax').text
ymax = object.find('bndbox').find('ymax').text
bndbox.append([cell,xmin,ymin,xmax,ymax])
#以上是打开xml文件,并解析文件内容,我们想要的只有object下的name,与bndbox的
# xmin,ymin,xmax,ymax,最后将这些元素通过append加入到bndbox列表中。
最后输出bndbox,可以得到:
即细胞类名和标注框的位置。
3.xml信息解析
在2中,我们将xml中的所需信息读取出来并存放在bndbox中,但是这并不能在网络使用。首先我们的任务是血细胞目标检测任务,这个任务可以主要分为细胞种类的分类或者识别,以及对于某种细胞的检测框预测。
对于分类任务,不能直接向网络直接输入类别,需要对类别编码。
这里将三类细胞,WBC,RBC,Platelets,利用字典将类别名转化为对应编码(0,1,2)。
对于检测框的真实数据,由于在标注时有些检测框位置位于边界,或者某个方向的候选框重叠等问题,需要将这些有问题的标注框进行筛选。
下面就基于2中的代码进行改写,将文件信息分类label与bbox,其中label存放细胞种类标签,而bbox存放标注框的位置。
import glob
import xml.etree.ElementTree as ET
#定义类别字典
#分为3种血细胞,标号为0,1,2
class_idx = {'WBC':0,'RBC':1,'Platelets':2}
def get_LabelFromXml(xml_file):
an_file = open(xml_file, encoding='utf-8') # 用utf-8的格式打开该文件
tree = ET.parse(an_file) # 用ET来解析an_file,得到文件内容树格式
root = tree.getroot() # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
label = []
bbox_list = []
for object in root.findall('object'):
cell = object.find('name').text # 同理拿到object的name属性,可以得到细胞的类名
cell_id = class_idx[cell] #根据字典将类名变成类序号
xmin = object.find('bndbox').find('xmin').text # 拿到候选框的位置
ymin = object.find('bndbox').find('ymin').text
xmax = object.find('bndbox').find('xmax').text
ymax = object.find('bndbox').find('ymax').text
#1 位于边界的框筛选不要
#2 边界框无大小的筛选不邀
if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
pass #或者continue
elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
pass
else:
label.append(cell_id) #保存标签名
bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)]) #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)
return label,bbox_list
#检验函数
path = './data/train/' #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
#乱序文件调整顺序
xml_list = []
img_list = []
for i in xml_file:
img = i[:-3]+'jpg' #将xml文件名后三位改成jpg,就是图像的文件名
if img in img_file: #判断该文件名是不是在img_file中,如果在,那就加入img_list
img_list.append(img)
xml_list.append(i)
#批量处理
for i in xml_list:
label1,bbox1 = get_LabelFromXml(i)
print(label1)
print(bbox1)
#所有文件的数据爬取出来
#由于每张图片的类别数量不同,后续还需特殊处理
以上就将数据集train文件的信息全部处理好了。
但是由于上述代码对检测框进行了筛选,所以不同图像中的label,bbox的大小是不同的,即每幅图中所包含的细胞数量、候选框数量时不同的,这点在进行神经网络批处理的时候存在问题。后面处理。下面是label与bbox的数据显示
4.train_dataset的制作
有人可能想问为什么我这不是已经将数据准保好了,为什么还要将数据封装成data_set。
这里给出回答,封装数据集为自定义类,主要目的是为了更好的组织和管理数据,并于深度学习框架的数据加载器(‘DataLoader’)兼容。方便对图像的批处理,比如图像增强,将数据传入cuda等。方便数据加载,预处理。
这里使用torch.utils.data中的Dataset类。我们封装数据集类基础Dataset类的属性。
例如以下代码示例:
import torch
from torch.utils.data import Dataset
from PIL import Image
class CustomDataset(Dataset):
def __init__(self, data_dir, transform=None):
self.data_dir = data_dir
self.transform = transform
self.data = [] # 数据集的样本列表,包括图像和标签
# 在构造函数中加载数据并填充到self.data中
def __len__(self):
return len(self.data)
def __getitem__(self, index):
image_path, label = self.data[index]
image = Image.open(image_path)
if self.transform:
image = self.transform(image)
return image, label
torch.utils.data.Dataset
类的常见方法和用法:
-
__init__(self, ...)
: 构造函数,用于初始化数据集的属性或参数。你可以在这里传递数据集的文件路径、转换等信息。 -
__len__(self)
: 返回数据集的样本数量,通常用于确定数据集的大小。 -
__getitem__(self, index)
: 根据给定的索引index
返回数据集中的一个样本。在这个方法中,你可以加载图像、标签,进行数据预处理等操作。 -
自定义数据加载和预处理:你可以在
__getitem__
方法中定义如何加载和处理数据。这通常包括从磁盘加载图像、对图像进行缩放、归一化、数据增强等操作。 -
数据切分:你可以将数据集划分为训练、验证和测试集,以便用于模型训练和评估。
其中可以利用transform对数据进行预处理,数据增强等。
比如:
from torchvision import transforms
# 数据预处理操作,例如缩放、归一化等
transform = transforms.Compose([
transforms.Resize((256, 256)),
transforms.ToTensor(),
])
这样,你就可以使用 data_loader
对数据进行批处理并传递给神经网络进行训练。
来看看本项目如何制作数据集data_set。
from PIL import Image
import torch
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
import xml.etree.ElementTree as ET
import glob
#定义xml提取函数
class_idx = {'WBC':0,'RBC':1,'Platelets':2}
def get_LabelFromXml(xml_file):
an_file = open(xml_file, encoding='utf-8') # 用utf-8的格式打开该文件
tree = ET.parse(an_file) # 用ET来解析an_file,得到文件内容树格式
root = tree.getroot() # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
label = []
bbox_list = []
for object in root.findall('object'):
cell = object.find('name').text # 同理拿到object的name属性,可以得到细胞的类名
cell_id = class_idx[cell] #根据字典将类名变成类序号
xmin = object.find('bndbox').find('xmin').text # 拿到候选框的位置
ymin = object.find('bndbox').find('ymin').text
xmax = object.find('bndbox').find('xmax').text
ymax = object.find('bndbox').find('ymax').text
#1 位于边界的框筛选不要
#2 边界框无大小的筛选不邀
if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
pass #或者continue
elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
pass
else:
label.append(cell_id) #保存标签名
bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)]) #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)
return label,bbox_list
#pytorch 数据增强时,resize,旋转之类的,目标位置,目标类别都需改变,
transformer = transforms.Compose([transforms.ToTensor(),])
class CellDetection(Dataset):
def __init__(self,img,xml,transformer = None):
self.img = img
self.xml = xml
self.transformer = transformer
def __getitem__(self, index):
img = self.img[index]
xml = self.xml[index]
img_open = Image.open(img)
img_tensor = self.transformer(img_open)
label, bbox = get_LabelFromXml(xml)
#将列表转换为tensor label int64 box float32
bbox_tensor = torch.as_tensor(bbox,dtype=torch.float32)
label_tensor = torch.as_tensor(label,dtype=torch.int64)
#打包表为字典
target = {}
target['boxes'] = bbox_tensor
target['labels'] = label_tensor
return img_tensor,target
def __len__(self):
return len(self.img)
#检验函数
path = './data/train/' #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
#乱序文件调整顺序
xml_list = []
img_list = []
for i in xml_file:
img = i[:-3]+'jpg' #将xml文件名后三位改成jpg,就是图像的文件名
if img in img_file: #判断该文件名是不是在img_file中,如果在,那就加入img_list
img_list.append(img)
xml_list.append(i)
train_data = CellDetection(img_list,xml_list,transformer)
#测试
print(train_data.xml[5])
从以上代码CellDetection可以看到,CellDetection类具有3个属性,img,xml以及transform。
在len中返回的时数据集的大小。而在getiem具有数据索引以及对数据进行了加载或者读取,对于img图像进行了transform,并将其与label,bbox转化为tensor张量并且是float类型。这是因为神经网络输入数据的类型是以tensor形式输入的,并且是浮点数。而我们之前3中对数据进行加载得到的都是整数int类型。所以这块需要进行转换。之后将label与bbox统一打包成target。对于该类初始化输入img文件名列表,以及xml文件名列表与自定义的tramsformer。这样就得到train_dataset。当然也可以基于此得到test数据集。
5.dataloader
到这一步就简单了。我们已经得到了train_dataset。接下来就是将数据集输入dataloader中,得到train的数据加载器。
首先还是介绍一下dataloader。
DataLoader
是 PyTorch 中的一个工具,用于从数据集中加载批次的数据以供训练。它负责多项任务,如数据分批、数据随机化、多线程加载等,以帮助你更有效地训练深度学习模型。
#导入 DataLoader:
from torch.utils.data import DataLoader
#创建 DataLoader:
data_loader = DataLoader(dataset, batch_size=32, shuffle=True)
#dataset:要加载数据的数据集对象,通常是你自己创建的 torch.utils.data.Dataset 类的实例。
#batch_size:每个批次的样本数量。
#shuffle:是否对数据进行随机洗牌,通常在训练时使用以确保模型不会对样本的顺序产生依赖。
#遍历 DataLoader:
for inputs, labels in data_loader:
# 在此处进行模型训练或其他操作
#DataLoader 还支持其他参数,如多线程数据加载、自定义批处理函数、持续加载等。
data_loader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4,collate_fn)
#num_workers:用于多线程数据加载的工作进程数量,可提高数据加载效率。
#collate_fn:自定义的数据批处理函数,用于对每个批次进行自定义处理。
上面3我们说到了不同图像中的label,bbox的大小是不同的,即每幅图中所包含的细胞数量、候选框数量时不同的。
这里需要自定义一个批处理函数。将它们合并成可以传递给神经网络的形式。
def detection_collate(x):
return list(tuple(zip(*x)))
这里我们这样定义批处理函数。这个函数的主要目的是将一批数据中的图像、标签等信息从一个列表中提取出来并进行格式化,以便传递给神经网络。
在对象检测任务中,每个样本通常包含一幅图像和与该图像相关的目标(例如,边界框和类别标签)。每个样本的结构可能会有所不同,因此需要一个函数来将它们合并成可以传递给神经网络的形式。
detection_collate
函数中的 zip(*x)
用于将一批数据进行解压缩,然后 list
函数将结果转换为列表。这有助于将批处理的数据转换为一组图像和一组标签,以便进行后续的处理。
假设 x
是一批数据,其中每个元素是一个样本,每个样本由图像和标签组成,detection_collate
的目的是将它们分别提取出来,以便进行批处理。这个函数的具体行为可能会根据数据的结构和你的需求而有所不同。
这里提一嘴,我们的train_dataset,处理需要提取的label和xml,还有img,我们向网络中输入训练的就是图像,label与xml作为真实值计算损失函数。
除此之外dataloader的其他参数,batch_size,就是每次传进网络的批次大小,shuffle表示是否对数据打乱排序。
好了以上就是从数据集到dataloader。看看完整代码。
import matplotlib.pyplot as plt
from PIL import Image
import torch
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
import xml.etree.ElementTree as ET
import glob
#定义xml提取函数
class_idx = {'WBC':0,'RBC':1,'Platelets':2}
def get_LabelFromXml(xml_file):
an_file = open(xml_file, encoding='utf-8') # 用utf-8的格式打开该文件
tree = ET.parse(an_file) # 用ET来解析an_file,得到文件内容树格式
root = tree.getroot() # 获取树的根目录,抓取xml中的数据,与html的爬取是一样的
label = []
bbox_list = []
for object in root.findall('object'):
cell = object.find('name').text # 同理拿到object的name属性,可以得到细胞的类名
cell_id = class_idx[cell] #根据字典将类名变成类序号
xmin = object.find('bndbox').find('xmin').text # 拿到候选框的位置
ymin = object.find('bndbox').find('ymin').text
xmax = object.find('bndbox').find('xmax').text
ymax = object.find('bndbox').find('ymax').text
#1 位于边界的框筛选不要
#2 边界框无大小的筛选不邀
if int(xmin)== 0 or int(xmax)== 0 or(ymin)== 0 or(ymax)== 0:
pass #或者continue
elif int(xmin) ==int(xmax)== 0 or(ymin) == (ymax)== 0:
pass
else:
label.append(cell_id) #保存标签名
bbox_list.append([int(xmin),int(ymin),int(xmax),int(ymax)]) #保存候选框位置 左上角(x1,y1)和右下角(x2,y2)
return label,bbox_list
#pytorch 数据增强时,resize,旋转之类的,目标位置,目标类别都需改变,
transformer = transforms.Compose([transforms.ToTensor(),])
class CellDetection(Dataset):
def __init__(self,img,xml,transformer = None):
self.img = img
self.xml = xml
self.transformer = transformer
def __getitem__(self, index):
img = self.img[index]
xml = self.xml[index]
img_open = Image.open(img)
img_tensor = self.transformer(img_open)
label, bbox = get_LabelFromXml(xml)
#将列表转换为tensor label int64 box float32
bbox_tensor = torch.as_tensor(bbox,dtype=torch.float32)
label_tensor = torch.as_tensor(label,dtype=torch.int64)
#打包表为字典
target = {}
target['boxes'] = bbox_tensor
target['labels'] = label_tensor
return img_tensor,target
def __len__(self):
return len(self.img)
#检验函数
path = './data/train/' #设置需要爬取的文件路径,当前文件下data下的train文件
xml_file = glob.glob(path+'*.xml') #通过glob来爬取文件中.xml结尾的文件,文件名
img_file = glob.glob(path+'*.jpg') #通过glob来爬取文件中.jpg结尾的图片,文件名
#乱序文件调整顺序
xml_list = []
img_list = []
for i in xml_file:
img = i[:-3]+'jpg' #将xml文件名后三位改成jpg,就是图像的文件名
if img in img_file: #判断该文件名是不是在img_file中,如果在,那就加入img_list
img_list.append(img)
xml_list.append(i)
train_data = CellDetection(img_list,xml_list,transformer)
#测试
print(train_data.xml[5])
#定义打包函数
def detection_collate(x):
return list(tuple(zip(*x)))
print(detection_collate(train_data[1]))
dl_train = DataLoader(train_data,batch_size = 2,shuffle=True,collate_fn=detection_collate)
#数据加载器,加入train_data,因为本项目目标检测任务,其label,与bbox的数据长度不一致,所以不能完美的输入金网络
#多物体,多目标操作
#如果不对数据打包,则不能把数据形成一个batch放进网络
img,label = next(iter(dl_train))
label1 = label[0]
print(label1)
print(img[0])
print(img[0].shape)
plt.imshow(img[0].permute(1,2,0))
#permute 将tensor变量换通道. 图片转tensor 0通道是3,1是h,2是w
plt.show()