DAVIS数据集里的蒙太奇图像(来自于:DAVIS挑战赛)
当我们进入一个新的领域,最难的事情往往是入门和上手操作。在深度学习领域,第一件事(通常也是最关键的)就是处理数据,所以我们在写Python代码时,需要一个更有组织的方法来加载和使用图像数据。
本文的目的是在你有一个数据集后,实现一个可以直接用在Keras上的图像处理流程,它虽然基础,但是很容易扩展。我们的示例数据是DAVIS 2019挑战赛的数据集,本方法也可以用在其他图像数据集上(例如Berkeley DeepDrive 100K, nuScenes 3D Detection, Google Image Captioning等),而且其中大部分代码都可以不加修改的用在任何有监督学习的数据集上。
本文主要包含以下几个部分:
- 数据追踪
- 使用生成器(Generators)来处理数据
- 集成到一个类里
追踪数据
追踪的意思并不是说担心数据会丢失,只是我们需要一个更有组织的方法去处理他们。
如果我们只有独立的图片文件,那么只需要一个这些图片名的列表,可以用os库来生成特定文件夹下所有文件的列表。
代码语言:javascript复制import os
path = '/path/tp/directory/of/images'
if os.path.exists(path):
root, folders, files = next(os.walk(path))
# (assuming we have > 42 images) we can get the full path for a file by:
filepath = os.path.join(root, files[42])
上图中,files是文件夹下所有可访问的图片列表(存储的是文件名),os.path.exists(path)检查路径是否可以访问,os.walk(path)返回一个迭代访问文件夹的生成器,os.path.join(path1, path2, ...)用来组合文件路径(在本文中,是文件夹的路径后接文件名)以生成一个路径的字符串(注意‘/’并不是必须的)。
如果要处理的是视频,那么代码会复杂一点(取决于视频信息的存储方式)。不同于存储所有图片的列表,我们将会存储一个键值对,关键字是视频的名称,对应的值是视频所对应的图片。在DAVIS数据集中,图片是基于视频分放在不同的文件夹,所以可以很容易得到视频的列表(以及对应图片的列表)。
代码语言:javascript复制import os
path = '/path/tp/directory/of/video/folders'
if os.path.exists(path):
_, folders, _ = next(os.walk(path)) # (we can use `_` to ignore an output)
# folders is now a list of the video folder names
videos = {}
for video_foldername in folders:
video_path = os.path.join(path, video_foldername)
if os.path.exists(video_path):
_, _, images = next(os.walk(video_path))
videos[video_foldername] = images
至于我们为什么要按视频名称对图片分类,是因为有时候我们需要单个视频源的图片。此外,验证集的划分也需要根据视频进行划分,如果训练集和验证集中有来自于同一个视频的图片,那验证集的得分就没有意义(类似于'数据泄露')。
我们可以用同一套代码去加载输入图片或者输出掩码。
注意:有一个很重要的事情我忽略了,就是把图片以及相应的掩码对应起来,这个需要特别注意一下。
加载图片
在有了想要加载图片的路径后,有很多图像处理的Python库可以使用:matplotlib, scikit-image, opencv, pillow, imageio等,这里只列出了一小部分。它们的调用代码非常简单,每种库的使用方法也非常相似:
代码语言:javascript复制filename = '/path/to/image/file/'
# using matplotlib
import matplotlib.pyplot as plt
img = plt.imread(filename)
# using scikit-image
import skimage.io as skio
img = skio.imread(filename)
# using opencv
import cv2
cv2.imread(filename)
# using pillow (PIL fork)
from PIL import Image
img = Image.open(filename)
# using imageio
import imageio
img = imageio.imread(filename)
Python中不同的方法读入一张图片
在这里推荐的是前三种方法(matplotlib, scikit-image, 或者opencv),因为他们的返回值是一个ndarray数组。(如果使用其他库的话,你需要自己手动的把返回值转换成ndarray形式)
编写脚本时,检查一下图片是否加载的正确,只需要用matplotlib画出图片就行:plt.imshow(img).
加载实例的掩码
虽然我们可以直接使用上面的代码像图片一样加载输出掩码,但还是需要对它们进行一些预处理才能最终用来训练。最主要的问题就是需要对图片进行独热(one-hot)编码。通常来说onehot都是单通道的(偶尔会有三通道),但是需要编码成onehot存储在三维数组中。有很多现成的代码可以用(在StackOverflow, GitHub或者Kaggle的讨论板块,你可以很容易的找到这些代码),但是我觉得这块值得你亲自动手写一写。
用生成器(Generators)来处理大量数据
在深度学习中,我们通常会处理非常大的数据集(通常是几百GB或者TB的量级)。大部分的时间,我们不会把所有数据都加载到内存里(尽管有时候内存是够的,我们也不会把短时间内不会用到的数据常驻在内存中),因此我们需要用生成器的方法去分批次的加载少量数据。
可以用下面的方法:
代码语言:javascript复制for x, y in generator():
# do something with a batch (x,y), e.g. plotting
虽然看起来只是一个简单的Python循环,但是在循环之外生成器却做了一些特别的处理。通常的for循环会创建一个数据列表,并在首次使用时就加载所有的数据,然后再具体的使用每一个元素。但是生成器的循环不会如此粗暴,它会在请求数据的时候预加载下一个元素,在任何时候,只有很少量的数据会存在内存中。
以上就是我们的目标,那么实际操作中怎样实现一个生成器呢?
一个例子:斐波那契数列
让我们回来看一个简单的例子:生成一个无限长的斐波那契数列。斐波那契数的生成规则是新的数是它前面两个数的和。如果想打印输出一组无限的斐波那契数列,代码如下:
代码语言:javascript复制prev = curr = 1
print(prev)
print(curr)
while True:
prev, curr = curr, prev curr
print(curr)
不使用生成器来输出一组斐波那契数列
为了实现一个生成器的代码,我们写了一个函数实现了相同的功能,但并没有返回prev或者curr,而是会yield下一个数。它工作的原理,是调用一个带yield返回值的函数,并不会像return一样把控制权返回给调用者,而是会缓存下来,以期在未来的某些时候会继续使用。所有的局部变量都会保存下来,下次调用时会从它上次结束的地方继续执行。
代码语言:javascript复制def fibonacci():
prev = curr = 1
yield prev # we "return" 1
yield curr # next time the generator is called, we "return" 1 here
while True:
prev, curr = curr, prev curr
yield curr # we stop executing here and "return" curr
# if the generator keeps getting called, we'll continue executing from here
斐波那契生成器函数
事实上,上面的Python代码里有很多“魔法”的地方,但在本文我并不打算深入的讲解它们,如果你对它们有兴趣,可以通过原文查看Jeff Knupp写的关于生成器的文章。
数据生成器
在Keras中,数据生成器的常用方法可以这样写:
代码语言:javascript复制def generate_data():
# initialize variables as needed
# may need to shuffle dataset
while True:
# may need to detect if we've gone through the entire dataset and reshuffle
next_input = # load next inputs into memory
next_output = # load next ground-truth outputs into memory
# perform data augmentation as needed
# do any other preprocessing steps
yield next_input, next_output
数据生成器的写法
在需要数据的地方传入DAVIS数据集:
代码语言:javascript复制from random import shuffle
def generate_data():
# initialize variables as needed
images = []
for image_list in videos_input.values():
images.extend(image_list)
masks = []
for mask_list in videos_output.values():
masks.extend(mask_list)
i = 0
# may need to shuffle dataset
shuffle(images)
while True:
# may need to detect if we've gone through the entire dataset and reshuffle
if i >= len(images):
shuffle(images)
i = 0
# grab next inputs and outputs
next_input = load_image_from_file(images[i])
next_output = load_mask_from_file(masks[i])
# perform data augmentation as needed
# do any other preprocessing steps
yield next_input, next_output
i = 1
DAVIS数据生成器示例
使用生成器
在有了所需的数据生成器后,可以像上面的方法那样在自己的循环中调用(例如打印出输入图片和输出掩码进行对比),但是在Keras中训练模型时,并不一定非要这样做。Keras中,Model和Sequential类有多种调用方法,你可以把所有的数据作为参数传入fit(), predict(), 和evaluate() ,同时也提供了以生成器作为参数的版本,fit_generator(), predict_generator(), 和 evaluate_generator()
代码语言:javascript复制# option 1: go through the data using a for-loop
for x, y in generate_data():
import matplotlib.pyplot as plt
plt.imshow(x)
plt.show()
# option 2: pass the generator into keras and have it generate whatever data it needs
model.fit_generator(generate_data())
使用生成器的方法
集成到类里
为了流程化处理,最好的办法是把所有的数据预处理都放在一个类里(这个类在所有的数据集中都可以用)
代码语言:javascript复制import os
from random import shuffle
class DAVISDataPreprocessor(object):
def __init__(self, path_to_input_videos, path_to_output_videos):
if not os.path.exists(path_to_input_videos) or not os.path.exists(path_to_output_videos):
raise ValueError('path(s) doesn't exist!')
# extract the filenames of the images (input) by video
self.input_root, input_folders, _ = next(os.walk(path_to_input_videos))
self.videos_input = {}
for video_foldername in input_folders:
video_path = os.path.join(self.input_root, video_foldername)
if os.path.exists(video_path):
_, _, images = next(os.walk(video_path))
self.videos_input[video_foldername] = images
# repeat for output images
self.output_root, output_folders, _ = next(os.walk(path_to_output_videos))
self.videos_output = {}
for video_foldername in input_folders:
video_path = os.path.join(self.input_root, video_foldername)
if os.path.exists(video_path):
_, _, images = next(os.walk(video_path))
self.videos_input[video_foldername] = images
def load_image_from_file(self, filename):
filepath = os.path.join(self.input_root, filename)
return plt.imread(filepath)
def load_mask_from_file(self, filename):
filepath = os.path.join(self.output_root, filename)
raw_mask = plt.imread(filepath)
# do further processing on `raw_mask`
return final_mask
def generate_data(self):
# initialize variables as needed
images = []
for image_list in self.videos_input.values():
images.extend(image_list)
masks = []
for mask_list in self.videos_output.values():
masks.extend(mask_list)
i = 0
# may need to shuffle dataset
shuffle(images)
while True:
# may need to detect if we've gone through the entire dataset and reshuffle
if i >= len(images):
shuffle(images)
i = 0
# grab next inputs and outputs
next_input = self.load_image_from_file(images[i])
next_output = self.load_mask_from_file(masks[i])
# perform data augmentation as needed
# do any other preprocessing steps
yield next_input, next_output
i = 1
FAVIS预处理类
集成完后,使用起来也很简便:
代码语言:javascript复制path_to_inputs = '/path/to/input/video/folders/'
path_to_outputs = 'path/to/output/video/folders/'
davis_data = DAVISDataPreprocessor(path_to_inputs, path_to_outputs)
# use with keras directly
model.fit_generator(davis_data.generate_data())
# or, use with for-loop
for x, y in davis_data.generate_data():
# do stuff with (x, y)
预处理类的使用方法
希望这个简短的教程能让你对正在处理的大量数据有一个更好的把握(也有可能你的数据量并不大)。但是,还是有很多事情本文并没有覆盖到,这些都需要你自己去把它完善,比如说:
- 对于每一个实例实现一个存储和访问输出的类(可以考虑实例编号);
- 还可以增加一些其他需要的预处理方法(归一化,独热编码,尺度/扩展,增强等等);
- 将输入图片与它的掩码匹配对应起来
- 训练集和验证集的划分(基于视频数据)
- 参数化generate_data()方法(难道你总是需要做随机?)