一、数据的获取方法
二、图片清洗
三、数据标签
四、数据增强
五、数据加载

一、数据的获取方法

1.公共数据集,比如ImageNet。
2.外包平台,阿里/百度/京东微工/Amazon等,花钱包人拍摄。
3.自己拍摄
4.去网络上,比如百度等浏览器,专业拍摄网站,视频平台等,但需要爬虫提高效率。
以下是常用的爬虫工具介绍
1.
https://github.com/QianyanTech/Image-Downloader 一分钟1k张图片,可以设置多个关键词,以","分割。还可以把关键词写入.txt文件中,逐一个爬取。
2.
视频爬取工具,使用方式:./lux https://www.bilibili.com/bangumi/play/ep706633?spm_id_from=333.337.0.0,可以爬去b站,爱奇异,腾讯等国内,以youtube等国外视频平台,它也可以一次爬取多个,可以写脚本。它其实还可以爬照片文件等,有点像迅雷。
https://github.com/iawia002/lux
3.
Flickr国外的图片网站,很多数据集都是用这个来组建的,比图ImageNet。
https://www.flickr.com/
网站官网提供了爬虫接口,但需要申请一下,用以下工具爬取,其中接口使用教程:
https://github.com/chenusc11/flickr-crawler
用它爬取需要这几个步骤:

在这里插入图片描述
在这里插入图片描述

这些国内的摄影网站也有不少高质量图片。
其中图片连接为花瓣爬虫

在这里插入图片描述

二、图片清洗

//名称递加,格式修改jpg,去除损坏的图片

#coding:utf8

# Copyright 2019 longpeng2008. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# If you find any problem,please contact us
#
#     longpeng2008to2012@gmail.com 
#
# or create issues
# =============================================================================

import os
import sys
import cv2
import numpy as np

def listfiles(rootDir,rename=False):
    list_dirs = os.walk(rootDir)
    num = 0
    for root, dirs, files in list_dirs:
        for d in dirs:
            print os.path.join(root,d)
        for f in files:
            fileid = f.split('.')[0] 
            filepath = os.path.join(root,f)
            try:
                src = cv2.imread(filepath,1)
                print "src=",filepath,src.shape
                os.remove(filepath) #删除原来图片
                if rename:
                    cv2.imwrite(os.path.join(root,str(num)+".jpg"),src) #写入新的图片,重新命名
                    num = num + 1
                else:
                    cv2.imwrite(os.path.join(root,fileid+".jpg"),src) #写入新的图片,名字不变
            except:
                os.remove(filepath) #去除损坏图片
                continue

listfiles(sys.argv[1],rename=True)

删除完全相同和相似度高的文件,通过缩放,对比来决定。
待改进的地方
1.相似度计算改进,基于MSE距离,leveshtein距离,DNN特征相似度
2.遍历方式的改进,基于文件物理大小,图像尺寸,文件名字,进行预先排序,搜索一定的深度或最近邻图片。因为视频图片一般排在一起。

#coding:utf8

# Copyright 2019 longpeng2008. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# If you find any problem,please contact us
#
#     longpeng2008to2012@gmail.com 
#
# or create issues
# =============================================================================
#coding:utf8
import os
import sys
import hashlib

# 获取文件的md5 
def get_md5(file):
    file = open(file,'rb')
    md5 = hashlib.md5(file.read())
    file.close()
    md5_values = md5.hexdigest()
    return md5_values


from collections import Counter
import numpy as np
import cv2
# 计算图像相似度
def compare_image(image1,image2,mode='same'):
    ## 比较是否完全相同
    if mode == 'same':
        assert(image1.shape == image2.shape)
        diff = (image1 == image2).astype(np.int)
        if cv2.countNonZero(diff) == image1.shape[0]*image1.shape[1]:
            return 1.0

    ## 比较是否基于阈值
    elif mode == 'abs':
        assert(image1.shape == image2.shape)
        diff = np.sum(np.abs((image1.astype(np.float) - image2.astype(np.float))))
        return diff / (image1.shape[0]*image1.shape[1])

    return 0
         
# 单文件夹去重
def remove_by_md5_singledir(file_dir): 
    file_list = os.listdir(file_dir)
    md5_list =[]
    print("去重前图像数量:"+str(len(file_list)))
    for filepath in file_list:
        filemd5 = get_md5(os.path.join(file_dir,filepath))
        if filemd5 not in md5_list:
            md5_list.append(filemd5)
        else:
            os.remove(os.path.join(file_dir,filepath))

    print("去重后图像数量:"+str(len(os.listdir(file_dir))))


# 单文件夹去重
def remove_by_pixel_singledir(file_dir,mode,th=5.0): 
    file_list = os.listdir(file_dir)
    print("去重前图像数量:"+str(len(file_list)))
    for i in range(0,len(file_list)):
        if i < len(file_list)-1:
            imagei = cv2.imread(os.path.join(file_dir,file_list[i]),0)
            imagei = cv2.resize(imagei,(128,128),interpolation=cv2.INTER_NEAREST)
            print("testing image "+os.path.join(file_dir,file_list[i]))
            for j in range(i+1,len(file_list)):
                if j < len(file_list):
                    while j < len(file_list):
                        imagej = cv2.imread(os.path.join(file_dir,file_list[j]),0)
                        imagej = cv2.resize(imagej,(128,128),interpolation=cv2.INTER_NEAREST)
                        similarity = compare_image(imagei,imagej,mode=mode)
                        print("simi="+str(similarity))
                        if similarity >= 1.0 and mode == 'same':
                            os.remove(os.path.join(file_dir,file_list[j]))
                            print('删除'+os.path.join(file_dir,file_list[j]))
                            file_list.pop(j)
                        elif similarity < th and mode == 'abs':
                            os.remove(os.path.join(file_dir,file_list[j]))
                            print('删除'+os.path.join(file_dir,file_list[j]))
                            file_list.pop(j)
                        else:
                            break

    print("去重后图像数量:"+str(len(os.listdir(file_dir))))

# 多文件夹去重
def remove_by_md5_multidir(file_list): 
    md5_list =[]
    print("去重前图像数量:"+str(len(file_list)))
    for filepath in file_list:
        filemd5 = get_md5(filepath)
        file_id = filepath.split('/')[-1]
        file_dir = filepath[0:len(filepath)-len(file_id)]
        if filemd5 not in md5_list:
            md5_list.append(filemd5)
        else:
            os.remove(filepath)
    print("去重后图像数量:"+str(len(md5_list)))

if __name__ == '__main__':
    file_dir = sys.argv[1]
    #remove_by_md5_singledir(file_dir)
    remove_by_pixel_singledir(file_dir,mode='abs',th=10)

    '''
    file_dir1 = sys.argv[1]
    file_list1 = os.listdir(file_dir1)
    file_list1 = [ os.path.join(file_dir1,x) for x in file_list1 ]
    file_dir2 = sys.argv[2]
    file_list2 = os.listdir(file_dir2)
    file_list2 = [ os.path.join(file_dir2,x) for x in file_list2 ]
    remove_by_md5_multidir(file_list1+file_list2)
    '''

//打乱数据集合

#coding:utf8

# Copyright 2019 longpeng2008. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# If you find any problem,please contact us
#
#     longpeng2008to2012@gmail.com 
#
# or create issues
# =============================================================================

import sys
import random

#file_in 输入txt文件
#file_out 输出txt文件

def shuffle(file_in,file_out):
    fin = open(file_in,'r')
    fout = open(file_out,'w')

    lines = fin.readlines()
    random.shuffle(lines)
    for line in lines:
        fout.write(line)

shuffle(sys.argv[1],sys.argv[2])

//分配数据集比例

#coding:utf8

# Copyright 2019 longpeng2008. All Rights Reserved.
# Licensed under the Apache License, Version 2.0 (the "License");
# If you find any problem,please contact us
#
#     longpeng2008to2012@gmail.com 
#
# or create issues
# =============================================================================

import sys
#valratio:验证集比例
def splittrain_val(fileall,valratio=0.1):
    fileids = fileall.split('.')
    fileid = fileids[len(fileids)-2]
    f=open(fileall);
    ftrain=open(fileid+"_train.txt",'w');
    fval=open(fileid+"_val.txt",'w');
    count = 0
    if valratio == 0 or valratio >= 1:
        valratio = 0.1
    
    interval = (int)(1.0 / valratio)
    while 1:
        line = f.readline()
        if line:
            count = count + 1
            if count % interval == 0:
                fval.write(line)
            else:
                ftrain.write(line)
        else:
            break

splittrain_val(sys.argv[1],0.1)

更多参考:https://github.com/longpeng2008/yousan.ai.git

三、数据标签

笔者另外两个博客整理的在线网页标注工具:
roboflow标注工具使用
https://blog.csdn.net/weixin_43466192/article/details/154242167?spm=1001.2014.3001.5501
label-studio标注工具使用:
https://blog.csdn.net/weixin_43466192/article/details/154153170?spm=1001.2014.3001.5501

labelme的官网,但现在无法注册新账号了,也不能在线标注了,只能下载可执行程序:http://labelme2.csail.mit.edu/Release3.0/
源码:https://github.com/wkentaro/labelme

命令行安装

conda install pyqt
conda install labelme

它生成的是一个json文件,需要用命令把json文件转化成图片:

labelme_json_to_dataset 1.json -o 1

其中第一个连接是对众多标注工具的收集索引:https://awesomeopensource.com/project/jsbroks/awesome-dataset-tools
在这里插入图片描述

百度维护的智能标注:
有时需要精细标注,甚至扣图标注,需要智能化工具辅助。

老版维护github,有可执行工具release:https://github.com/PaddleCV-SIG/EISeg
新版本维护github,有编译说明:https://github.com/PaddlePaddle/PaddleSeg/tree/release/2.6/EISeg
它有以下支持:

在这里插入图片描述
在这里插入图片描述

如果没有工具合适,需要自己开发智能标注工具,用到的算法是Polygon-RNN和Pologon-RNN++,他们是基于RNN的半监督式交互工具。
这个算法的维护地址:https://github.com/fidler-lab/polyrnn-pp-pytorch

在这里插入图片描述

还有基于GCN的标注工具他不像上面RNN只能标注多边形,GCN可以以曲线的方式进行标注:
在这里插入图片描述

四、数据增强

不同的模型对不同的增强方法比较敏感。
在这里插入图片描述
分为两大类:
单样本变换:
1.几何变换:翻转,旋转,裁减,缩放,仿射。其中翻转,旋转方向敏感类不可用。
2.像素变换:噪声,模糊,颜色扰动,对比度扰动,擦除。这几个也可能会起副作用。
多样本变换,SMOT算法:
这个算法用在图片上,有以下两种:
1.SamplePariring,随机抽两张照片,都进行单样本操作,然后两张图片叠加在一起,label用其中一个图片的标签。实验显示,这样对训练有增强帮助。
2.Mixup,Facebook提出,它不仅像SamplePariring对图像插值,对lable也进行插值。实验证明对图像,语言,GAN问题都有效。
图片插值操作如下:
在这里插入图片描述
另外,这只有一个网球,可以多放几个:
在这里插入图片描述
综合使用几何颜色等变换的工具:
https://github.com/aleju/imgaug
它覆盖了主流的数据增强算法。
pip install imgaug
其中包含数学变换,几何变换,图像融合,模糊操作,颜色与亮度,尺度变换,卷积变换(池化等),艺术增强(卡通化等,用的少),分割增强,天气增强(加上下雨)
基本使用方法,以下代码官网有案例:
它还可以概率控制,对一个Bath中的图像,做不同的操作;对于列表操作中的顺序,是否随机顺序。

import imgaug.augmenters as iaa
seq = iaa.Sequential([
    iaa.Crop(px=(1, 16), keep_size=False),
    iaa.Fliplr(0.5),
    iaa.GaussianBlur(sigma=(0, 3.0))
])
for batch_idx in range(100):
    images = load_batch(batch_idx)
    images_aug = seq(images=images)  # done by the library 输入必须是tensor 格式是[N,H,W,C]
    train_on_images(images_aug)

目标检测分为四类:关键点检测 分类 目标检测 分割。
前面的操作都适用于分类,但label中有对目标的矩形框标注的时候,变换目标的位置时,矩形框的位置也应该有变换,它也提供了函数,在变换目标的时候,对lable中的矩形框标签进行转换。
通过draw_on_image画关键点,通过BoundingBoxesOnImage变换矩形框,如下,如果裁减变换后,原来的框超出了边界,还需要超出部分裁掉,对分割也有专门的操作
在这里插入图片描述

在这里插入图片描述

在Pytorch下,数据增强的方法:

data_transforms = {
    'train': transforms.Compose([
        transforms.RandomSizedCrop(48),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
        transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5])
    ]),
    'val': transforms.Compose([
        transforms.Scale(image_size),
        transforms.CenterCrop(crop_size),
        transforms.ToTensor(),
        transforms.Normalize([0.5,0.5,0.5], [0.5,0.5,0.5]) #一般从0开始构建都是0.5,加载特定数据集需要根据官网标准
    ]),
}

数据预先处理不会改变原图,只是每次batch都执行预处理操作。这里有个问题,它其实花费时间,会阻塞住后面的流程,因此需要开启多线程,GPU每次来CPU拿数据,CPU都已经处理好了。如下num_workers=4。一般在CPU做预处理,如果是大图可以在GPU。

dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)
    #transform_train是一个可调用的对象,调用它就把下面函数依次调用
    #Compose()组装流水线
transform_train = transforms.Compose([
    transforms.RandomResizedCrop(32, scale=(0.8, 1.0)),  # 随机裁剪的图片占原面积的80%100%,将裁减后的图片缩放回32×32输出
    transforms.RandomHorizontalFlip(),                   # 随机水平翻转
    transforms.ToTensor(),                               # 转为Tensor
    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) # 归一化
])

transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
])

在pytorch中,有这些接口
在这里插入图片描述

transform = T.RandomOrder([
    T.RandomHorizontalFlip(),
    T.RandomVerticalFlip(),
    T.RandomRotation(30)
])

# 假设你有一个图像 img
transformed_img = transform(img)

当你希望从一组变换中随机选择一个来应用于图像时,可以使用 RandomChoice

transform = T.RandomChoice([
    T.RandomHorizontalFlip(),
    T.RandomVerticalFlip(),
    T.RandomRotation(30)
])

# 假设你有一个图像 img
transformed_img = transform(img)
RandomResizedCrop(32, scale=(0.8, 1.0), ratio=(3/4, 4/3))

其中scale表示裁减的面积在原面积的0.8~1.0之间,宽/高比在3/4到4/3之间。如果出现了内容的拉伸畸变会更加增强泛化能力。

RandomApple表示随机使用这些操作,RandomChoice表示随机使用某些操作。
但这些只能在目标分类中使用,一但label中含有矩形框位置,那么它们没有对label中的矩形框位置做对应的变换,label就失去意义,比如目标检测和目标分割,目标检测有其专门的库,可以对图像转换时,同步对标签转换。
torchvision.transforms 的确主要面向分类,但是目标检测 / 分割的数据增强 PyTorch 生态里有成熟库支持,不需要你自己写
主流有:Albumentations、Detectron2、MMDetection、YOLOv5/8/11,全部支持 bbox + mask + keypoints,如果是目标检测 / 分割任务,首先推荐Albumentations。
import albumentations as A
也可以使用pytorch的自定义目标增强函数,如下:
使用transforms.functional函数。
在这里插入图片描述
google的AutoAugment,它有约 16 种操作,例如:
Rotate(旋转)
Translate(平移)
Shear(错切)
Color(颜色变换)
Sharpness(锐化)
Contrast(对比度)
Brightness(亮度)
Solarize(曝光反转)
Invert(反转颜色)
Posterize(减少颜色 bit)
Cutout(遮挡增强)
基本都是对像素的操作,他使用了强化学习,RNN机制,随机选择一些增强组合,然后监看训练结果,来调整选择的策略,达到最佳策略选择。官方实验证明,效果明显:
CIFAR-10 +2.7%
ImageNet +1.5%

transform = transforms.Compose([
    transforms.RandomResizedCrop(224),
    transforms.RandomHorizontalFlip(),
    transforms.AutoAugment(policy=AutoAugmentPolicy.IMAGENET),
    transforms.ToTensor(),
    transforms.Normalize((0.485, 0.456, 0.406),
                         (0.229, 0.224, 0.225)),
])

AutoAugment是google论文实现的高级增强技术,它假设你已经做了基本增强了,比如裁减和缩放。
AutoAugmentPolicy.CIFAR10 小图像(32×32)、颜色类别明显,不是只适用于CIFAR10,而是小图
AutoAugmentPolicy.IMAGENET 大图像(≥224×224)
AutoAugmentPolicy.SVHN 数字、街景文字图像

五、数据加载

pytorch支持多种方式,XML JSON XML等,也可以自定义加载类。
ImageFolder格式,他用文件夹名称来作为label,不需要专门的label,是目前最流像的格式之一,也是pytorch的默认标准格式之一,。ImageFolder()会自动给每个图片打上label。一般自建数据集合都用这种方式,但它只有类别,没有具体的标注框,一次不适合目标检测,只适合图片中只含有目标的任务。
目录结构如下:
其中classA会被自动映射为label 0 , classB映射成 1,依次。

dataset_root/
├── train/
│   ├── classA/
│   │   ├── img1.jpg
│   │   ├── img2.jpg
│   ├── classB/
│       ├── img3.jpg
│       ├── img4.jpg
├── val/
│   ├── classA/
│   │   ├── img5.jpg
│   │   ├── img6.jpg
│   ├── classB/
│       ├── img7.jpg
│       ├── img8.jpg

pytorch加载它的方式如下:

# 定义数据增强(或预处理)
data_transforms = {
    'train': transforms.Compose([
        transforms.RandomResizedCrop(224),
        transforms.RandomHorizontalFlip(),
        transforms.ToTensor(),
    ]),
    'val': transforms.Compose([
        transforms.Resize(256),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
    ])
}

# 分别加载 train 和 val 数据
train_dataset = datasets.ImageFolder(
    root=os.path.join(data_dir, 'train'),
    transform=data_transforms['train']
)
val_dataset = datasets.ImageFolder(
    root=os.path.join(data_dir, 'val'),
    transform=data_transforms['val']
)

# 分别创建 DataLoader
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)
val_loader   = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4)

# 放入字典中(方便统一访问)
image_datasets = {'train': train_dataset, 'val': val_dataset}
dataloaders = {'train': train_loader, 'val': val_loader}

用推导式更简洁:

    ## 使用torchvision的dataset ImageFolder接口读取数据
    image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
                                              data_transforms[x]) for x in ['train', 'val']}

    ## 创建数据指针,设置batch大小,shuffle,多进程数量
    dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x],
                                                 batch_size=64,
                                                 shuffle=True,
                                                 num_workers=4) for x in ['train', 'val']}
    ## 获得数据集大小
    dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}

ImageFolder主要用在分类任务,有时用在不在乎标签,只要读到数据的生成式任务如DCGAN。
但它是每个batch都去打开对应的.peg / .png文件,每一张图片:打开文件 → 解码 jpeg/png → 转成 numpy → ToTensor花菲大量时间,IO读取慢,但GPU训练快,就会跟不上。

H5文件结构,它把文件全部打开,以二进制形式存储在一个连续的硬盘内存块中,读取速度更快。它专门为海量数据使用,比如几万~几百万张。
1.顺序 IO(Sequential I/O)远快于文件系统多次随机 IO
2.H5 可以预先存成numpy float32,不需要再解码 JPEG/PNG
将ImageFolder格式转化为H5文件:

import os
import h5py
import torch
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from tqdm import tqdm

# 输入 ImageFolder 路径
root = "./data"

# 输出 h5 文件路径
h5_path = "./dataset.h5"

# 设置图像尺寸(如果你想保持原图,可删掉 Resize)
transform = transforms.Compose([
    transforms.Resize((128, 128)),
    transforms.ToTensor()
])

# 用 ImageFolder 加载数据
dataset = datasets.ImageFolder(root=root, transform=transform)
loader = DataLoader(dataset, batch_size=1, shuffle=False)

N = len(dataset)
C, H, W = dataset[0][0].shape

print(f"共 {N} 张图片,尺寸 = {C}×{H}×{W}")

# 创建 H5 文件
with h5py.File(h5_path, "w") as h5f:
    img_storage = h5f.create_dataset(
        "images",
        shape=(N, C, H, W),
        dtype="float32"
    )
    label_storage = h5f.create_dataset(
        "labels",
        shape=(N,),
        dtype="int"
    )

    idx = 0

    for imgs, labels in tqdm(loader):
        img_storage[idx] = imgs[0].numpy()
        label_storage[idx] = int(labels[0])
        idx += 1

print(f"H5 保存完成:{h5_path}")

如何加载H5文件:

class H5Dataset(torch.utils.data.Dataset):
    def __init__(self, h5_path):
        self.h5 = h5py.File(h5_path, 'r')
        self.images = self.h5['images']
        self.labels = self.h5['labels']
        self.length = len(self.images)

    def __len__(self):
        return self.length

    def __getitem__(self, idx):
        img = torch.tensor(self.images[idx])
        label = int(self.labels[idx])
        return img, label

dataset = H5Dataset("./dataset.h5")
loader = DataLoader(dataset, batch_size=64, shuffle=True)

pickle是python的一种文件形式,也可以把图片保存成这样一种形式,但是当读取时,一次性只能把文件全部读完,因此小的数据集合才使用,如CIFAR10等。

加载.mat格式文件

import scipy.io as sio
class SIDDMatDataset(Dataset):
    def __init__(self, mat_file, transform=None):
        self.mat = sio.loadmat(mat_file)
        self.noisy = self.mat['Noisy']
        self.clean = self.mat['Gt']
        self.transform = transform

    def __len__(self):
        return self.noisy.shape[0]

    def __getitem__(self, idx):
        noisy = self.noisy[idx]
        clean = self.clean[idx]

        noisy = torch.from_numpy(noisy).permute(2,0,1).float() / 255.
        clean = torch.from_numpy(clean).permute(2,0,1).float() / 255.

        return noisy, clean

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐