目標:介紹如何對圖像數據進行預處理使訓練得到的神經網絡模型盡可能小地被無關因素所影響。但與此同時,復雜的預處理過程可能導致訓練效率的下降。為了減少預處理對於訓練速度的影響,TensorFlow 提供了多線程處理輸入數據的解決方案。
TFRecord 輸入數據格式
TensorFlow 提供了一種統一的格式來存儲數據(TFRecord)。TFRecord 文件中的數據都是通過 tf.train.Example Protocol Buffer
的格式存儲的。
tf.train.Example
的定義:
message Example {
Features features = 1;
};
message Features {
map<string, Feature> feature = 1;
};
message Features {
oneof kind {
BytesList bytes_list = 1;
FloatList float_list = 2;
Int64List int64_list = 3;
}
};
train.Example 的數據結構比較簡潔,包含了一個從屬性名稱到取值的字典。其中屬性名稱為一個字符串,屬性的取值可以為字符串(BytesList),實數列表(FloatList)或者整數列表(Int64List)。
%pylab inline
import tensorflow as tf
import numpy as np
from tensorflow.examples.tutorials.mnist import input_data
Populating the interactive namespace from numpy and matplotlib
def __int64__feature(value):
'''生成整數型的屬性'''
return tf.train.Feature(int64_list= tf.train.Int64List(value=[value]))
def __bytes__feature(value):
'''生成字符型的屬性'''
return tf.train.Feature(bytes_list= tf.train.BytesList(value=[value]))
mnist = input_data.read_data_sets('E:/datasets/mnist/data', dtype= tf.uint8, one_hot= True)
Extracting E:/datasets/mnist/data\train-images-idx3-ubyte.gz
Extracting E:/datasets/mnist/data\train-labels-idx1-ubyte.gz
Extracting E:/datasets/mnist/data\t10k-images-idx3-ubyte.gz
Extracting E:/datasets/mnist/data\t10k-labels-idx1-ubyte.gz
images = mnist.train.images
# 訓練數據所對應的正確答案,可以作為一個屬性保存在 TFRecord 中
labels = mnist.train.labels
# 訓練數據的圖像分辨率,這可以作為 Example 中的一個屬性
pixels = images.shape[1]
num_examples = mnist.train.num_examples
# 輸出 TFRecord 文件的地址
filename = 'E:/datasets/mnist/output.tfrecords'
# 創建一個 writer 來寫 TFRecord 文件
writer = tf.python_io.TFRecordWriter(filename)
for index in range(num_examples):
# 將圖片矩陣轉化為一個字符串
image_raw = images[index].tostring()
# 將一個樣例轉換為 Example Protocol Buffer,並將所有的信息寫入到這個數據結構
examle = tf.train.Example(features = tf.train.Features(feature={
'pixels': __int64__feature(pixels),
'label': __int64__feature(np.argmax(labels[index])),
'image_raw': __bytes__feature(image_raw)
}))
# 將一個 Example 寫入 TFRecord 文件
writer.write(examle.SerializeToString())
writer.close()
以上程序將 MNIST 數據集中所有的訓練數據存儲到一個 TFRecord 文件中。當數據量很大時,也可以將數據寫入到多個 TFRecord 文件。
讀取 TFRecord 文件中的數據
import tensorflow as tf
# 創建一個 reader 來讀取 TFRecord 文件中的樣例
reader = tf.TFRecordReader()
# 創建一個隊列來維護輸入文件列表
filename_queue = tf.train.string_input_producer(['E:/datasets/mnist/output.tfrecords'])
# 從文件中讀出一個樣例。也可使用 read_up_to 函數一次性讀取多個樣例
_, serialized_example = reader.read(filename_queue)
# 解析讀入的一個樣例。如果需要解析多個樣例,可以用 parse_example 函數
features = tf.parse_single_example(serialized_example,
features= {
# TensorFlow 提供兩種不同的屬性解析方法。一種是 tf.FixedLenFeature,解析結果為一個 Tensor
# 另一種方法為 tf.VarLenFeature,解析結果為 SparseTensor,用於處理稀疏數據
# 解析數據的格式需要和寫入數據的格式一致
'image_raw': tf.FixedLenFeature([], tf.string),
'pixels': tf.FixedLenFeature([], tf.int64),
'label': tf.FixedLenFeature([], tf.int64),
})
# tf.decode_raw 可以將字符串解析成圖片對應的像素數組
images = tf.decode_raw(features['image_raw'], tf.uint8)
labels = tf.cast(features['label'], tf.int32)
pixels = tf.cast(features['pixels'], tf.int32)
sess = tf.Session()
# 啟動多線程
coord = tf.train.Coordinator()
threads = tf.train.start_queue_runners(sess= sess, coord= coord)
# 每次運行可以讀取 TFRecord 文件中的一個樣例。當所有樣例都讀完之后,在此樣例中程序會再重頭讀取
for i in range(10):
image, label, pixel = sess.run([images, labels, pixels])
sess.close()
INFO:tensorflow:Error reported to Coordinator: <class 'tensorflow.python.framework.errors_impl.CancelledError'>, Run call was cancelled
INFO:tensorflow:Error reported to Coordinator: <class 'tensorflow.python.framework.errors_impl.CancelledError'>, Run call was cancelled
圖像數據處理
通過對圖像的預處理,可以盡量避免模型受到無關因素的影響。在大部分圖像識別問題中,通過圖像預處理過程可以提高模型的准確率。
圖像編碼處理
一張 RGB 色彩模式的圖片可看作一個三維矩陣,矩陣的每一個數字表示圖像上不同位置,不同顏色的亮度。然而圖像在存儲時並不是直接記錄這些矩陣中的數字,而是記錄經過壓縮編碼之后的結果。所要將一張圖像還原成一個三維矩陣,需要解碼的過程。TensorFlow 提供了對 jpeg 和 png 格式圖像的編碼/解碼函數。
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
# 讀取圖像的原始數據
image_raw_data = tf.gfile.FastGFile('E:/datasets/cat.jpg', 'rb').read() # 必須是 ‘rb’ 模式打開,否則會報錯
with tf.Session() as sess:
# 將圖像使用 jpeg 的格式解碼從而得到圖像對應的三維矩陣
# tf.image.decode_jpeg 函數對 png 格式的圖像進行解碼。解碼之后的結果為一個張量,
## 在使用它的取值之前需要明確調用運行的過程。
img_data = tf.image.decode_jpeg(image_raw_data)
# 輸出解碼之后的三維矩陣。
print(img_data.eval())
[[[162 161 140]
[162 162 138]
[161 161 137]
...,
[106 140 46]
[101 137 47]
[102 141 52]]
[[164 162 139]
[163 161 136]
[163 161 138]
...,
[104 138 43]
[102 139 46]
[108 138 50]]
[[165 163 140]
[165 163 138]
[163 161 136]
...,
[104 135 41]
[102 137 43]
[108 139 45]]
...,
[[207 200 181]
[206 199 180]
[206 199 180]
...,
[109 84 53]
[107 84 53]
[106 81 50]]
[[205 200 180]
[205 200 180]
[206 199 180]
...,
[106 83 49]
[105 82 51]
[106 81 50]]
[[205 200 180]
[205 198 179]
[205 198 179]
...,
[108 86 49]
[105 82 48]
[104 81 49]]]
打印圖片
with tf.Session() as sess:
plt.imshow(img_data.eval())
plt.show()
with tf.Session() as sess:
# 數據類型轉換為實數方便程序對圖像進行處理
img_data = tf.image.convert_image_dtype(img_data, dtype= tf.float32)
img_data = tf.image.convert_image_dtype(img_data, dtype= tf.uint8)
# 將表示一張圖片的三維矩陣重新按照 jpeg 格式編碼並存入文件中
## 打開這張圖像,可以得到和原始圖像一樣的圖像
encoded_image = tf.image.encode_jpeg(img_data)
with tf.gfile.GFile('E:/datasets/output.jpg', 'wb') as f:
f.write(encoded_image.eval())
重新調整圖片大小
一般地,網上獲取的圖片的大小是不固定的,但是神經網絡輸入節點的個數是固定的。所以在將圖像的像素作為輸入提供給神經網絡之前,需要將圖片的大小統一。圖片的大小調整有兩種方式:
- 通過算法使得新的圖片盡可能的保存原始圖像上的所有信息。
- Tensorflow 提供了四種不同的方法,並且將它們封裝在
tf.image.resize_images
函數中。
- Tensorflow 提供了四種不同的方法,並且將它們封裝在
- 裁剪或填充
保存完整信息
with tf.Session() as sess:
resized = tf.image.resize_images(img_data, [300, 300], method=0)
# TensorFlow的函數處理圖片后存儲的數據是float32格式的,需要轉換成uint8才能正確打印圖片。
print("Digital type: ", resized.dtype)
print("Digital shape: ", resized.get_shape())
cat = np.asarray(resized.eval(), dtype='uint8')
# tf.image.convert_image_dtype(rgb_image, tf.float32)
plt.imshow(cat)
plt.show()
Digital type: <dtype: 'float32'>
Digital shape: (300, 300, 3)
tf.image.resize_images
的 method
參數:
Method 取值 | 圖像大小調整算法 |
---|---|
0 | 雙線性插值法(Bilinear interpolation) |
1 | 最近鄰居法(Nearest neighbor interpolation) |
2 | 雙三次插值法(Bicubic interpolation) |
3 | 面積插值法(Area interpolation) |
裁剪和填充
tf.image.resize_image_with_crop_or_pad
函數可以調整圖像的大小。如果原始圖像的尺寸大於目標圖像這個函數會自動裁取原始圖像中居中的部分;如果目標圖像大於原始圖像,這個函數會自動在原始圖像的四周填充全 \(0\) 背景。
with tf.Session() as sess:
croped = tf.image.resize_image_with_crop_or_pad(img_data, 1000, 1000)
padded = tf.image.resize_image_with_crop_or_pad(img_data, 3000, 3000)
plt.imshow(croped.eval())
plt.show()
plt.imshow(padded.eval())
plt.show()
通過比例調整圖片大小
tf.image.central_crop
函數可以按比例裁剪圖像。其中比例取值:\((0, 1]\) 的實數。
with tf.Session() as sess:
central_cropped = tf.image.central_crop(img_data, 0.5)
plt.imshow(central_cropped.eval())
plt.show()
以上的函數都是截取或填充圖像的中間部分。使用 tf.image.crop_to_bounding_box
和 tf.image.pad_to_bounding_box
函數可以裁剪或填充給定區域的圖像。
翻轉圖片
with tf.Session() as sess:
# 上下翻轉
flipped1 = tf.image.flip_up_down(img_data)
plt.imshow(flipped1.eval())
plt.show()
# 左右翻轉
flipped2 = tf.image.flip_left_right(img_data)
plt.imshow(flipped2.eval())
plt.show()
#對角線翻轉
transposed = tf.image.transpose_image(img_data)
plt.imshow(transposed.eval())
plt.show()
在很多圖像識別問題中,圖像的翻轉不會影響識別結果。於是在訓練圖像識別的神經網絡時,可以隨機地翻轉訓練圖像,這樣訓練得到的模型可以識別不同角度的實體。因而隨機翻轉訓練圖像是一種零成本的很常用的圖像預處理方式。TensorFlow 提供了方便的 API 完成隨機圖像翻轉的過程。以下代碼實現了這個過程:
with tf.Session() as sess:
# 以一定概率上下翻轉圖片。
flipped = tf.image.random_flip_up_down(img_data)
plt.imshow(flipped.eval())
plt.show()
# 以一定概率左右翻轉圖片。
flipped = tf.image.random_flip_left_right(img_data)
plt.imshow(flipped.eval())
plt.show()
圖片色彩調整
和圖像翻轉類似,調整圖像的亮度、對比度、飽和度和色相在很多圖像識別應用中都不會影響識別結果。所以在訓練神經網絡模型時,可以隨機調整訓練圖像的這些屬性,從而使訓練得到的模型盡可能小的受到無關因素的影響。以下代碼可以完成此功能:
with tf.Session() as sess:
# 將圖片的亮度 -0.5。
adjusted = tf.image.adjust_brightness(img_data, -0.5)
plt.imshow(adjusted.eval())
plt.show()
# 將圖片的亮度 +0.5
adjusted = tf.image.adjust_brightness(img_data, 0.5)
plt.imshow(adjusted.eval())
plt.show()
# 在[-max_delta, max_delta)的范圍隨機調整圖片的亮度。
adjusted = tf.image.random_brightness(img_data, max_delta=0.5)
plt.imshow(adjusted.eval())
plt.show()
with tf.Session() as sess:
# 將圖片的對比度 -5
adjusted = tf.image.adjust_contrast(img_data, -5)
plt.imshow(adjusted.eval())
plt.show()
# 將圖片的對比度 +5
adjusted = tf.image.adjust_contrast(img_data, 5)
plt.imshow(adjusted.eval())
plt.show()
# 在[lower, upper]的范圍隨機調整圖的對比度。
lower = 7
upper = 88
adjusted = tf.image.random_contrast(img_data, lower, upper)
plt.imshow(adjusted.eval())
plt.show()
添加色相和飽和度
with tf.Session() as sess:
'''調節色相'''
adjusted = tf.image.adjust_hue(img_data, 0.1)
plt.imshow(adjusted.eval())
plt.show()
adjusted = tf.image.adjust_hue(img_data, 0.3)
plt.imshow(adjusted.eval())
plt.show()
adjusted = tf.image.adjust_hue(img_data, 0.6)
plt.imshow(adjusted.eval())
plt.show()
adjusted = tf.image.adjust_hue(img_data, 0.9)
plt.imshow(adjusted.eval())
plt.show()
# 在[-max_delta, max_delta]的范圍隨機調整圖片的色相。max_delta的取值在[0, 0.5]之間。
adjusted = tf.image.random_hue(img_data, 0.5)
plt.imshow(adjusted.eval())
plt.show()
with tf.Session() as sess:
# 將圖片的飽和度-5。
adjusted = tf.image.adjust_saturation(img_data, -5)
plt.imshow(adjusted.eval())
plt.show()
# 將圖片的飽和度+5。
adjusted = tf.image.adjust_saturation(img_data, 5)
plt.imshow(adjusted.eval())
plt.show()
# 在[lower, upper]的范圍隨機調整圖的飽和度。
#adjusted = tf.image.random_saturation(img_data, lower, upper)
with tf.Session() as sess:
# 將代表一張圖片的三維矩陣中的數字均值變為0,方差變為1。
image = img.imread('E:/datasets/cat.jpg')
adjusted = tf.image.per_image_standardization(image)
cat = np.asarray(adjusted.eval(), dtype='uint8')
plt.imshow(cat) # imshow 僅支持 uint8 格式
plt.show()
標准化處理可以使得不同的特征具有相同的尺度(Scale)。這樣,在使用梯度下降法學習參數的時候,不同特征對參數的影響程度就一樣了。tf.image.per_image_standardization(image)
,此函數的運算過程是將整幅圖片標准化(不是歸一化),加速神經網絡的訓練。主要有如下操作,\(\frac{x - mean}{adjusted\_stddev}\),其中\(x\)為圖片的 RGB 三通道像素值,\(mean\)分別為三通道像素的均值,\(adjusted\_stddev = \max \left(stddev, \frac{1.0}{\sqrt{image.NumElements()}}\right)\)。
- \(stddev\)為三通道像素的標准差,image.NumElements()
計算的是三通道各自的像素個數。
import tensorflow as tf
import matplotlib.image as img
import matplotlib.pyplot as plt
import numpy as np
sess = tf.InteractiveSession()
image = img.imread('E:/datasets/cat.jpg')
shape = tf.shape(image).eval()
h,w = shape[0],shape[1]
standardization_image = tf.image.per_image_standardization(image) #標准化
fig = plt.figure()
fig1 = plt.figure()
ax = fig.add_subplot(111)
ax.set_title('orginal image')
ax.imshow(image)
ax1 = fig1.add_subplot(311)
ax1.set_title('original hist')
ax1.hist(sess.run(tf.reshape(image,[h*w,-1])))
ax1 = fig1.add_subplot(313)
ax1.set_title('standardization hist')
ax1.hist(sess.run(tf.reshape(standardization_image,[h*w,-1])))
plt.ion()
plt.show()
添加標注框並裁減
在很多圖像識別數據集中,圖像中需要關注的物體通常會被標注框圈出來。使用 tf.image.draw_bounding_boxes
函數便可以實現。
with tf.Session() as sess:
# 將圖像縮小一些,這樣可視化能夠讓標注框更加清晰
img_data = tf.image.resize_images(img_data, [180, 267], method= 1)
# tf.image.draw_bounding_boxes 函數要求圖像矩陣中的數字為實數類型。
# tf.image.draw_bounding_boxes 函數的輸入是一個 batch 的數據,也就是多張圖像組成的四維矩陣,所以需要將解碼后的圖像加一維
batched = tf.expand_dims(tf.image.convert_image_dtype(img_data, tf.float32), 0)
# 給出每張圖像的所有標注框。一個標注框有四個數字,分別代表 [y_min, x_min, y_max, x_max]
## 注意這里給出的數字都是圖像的相對位置。比如在 180 * 267 的圖像中 [0.35, 0.47, 0.5, 0.56] 代表了從 (63, 125) 到 (90, 150)
### 的圖像。
boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
result = tf.image.draw_bounding_boxes(batched, boxes)
plt.imshow(result[0].eval())
plt.show()
和隨機翻轉圖像、隨機調整顏色類似,隨機截取圖像上有信息含量的部分也是一個提高模型健壯性(robustness)的一種方式。這樣可以使訓練得到的模型不受被識別物體的大小的影響。tf.image.sample_distorted_bounding_box
函數可以完成隨機截取圖像的過程。
with tf.Session() as sess:
boxes = tf.constant([[[0.05, 0.05, 0.9, 0.7], [0.35, 0.47, 0.5, 0.56]]])
# 可以通過提供隨機標注框的方式來告訴隨機截取圖像的算法哪些部分是 “有信息量” 的。
begin, size, bbox_for_draw = tf.image.sample_distorted_bounding_box(
tf.shape(img_data), bounding_boxes=boxes)
# 通過標注框可視化隨機截取得到的圖像
batched = tf.expand_dims(tf.image.convert_image_dtype(img_data, tf.float32), 0)
image_with_box = tf.image.draw_bounding_boxes(batched, bbox_for_draw)
plt.imshow(image_with_box[0].eval())
plt.show()
# 截取隨機出來的圖像,因為算法帶有隨機成分,所以每次的結果都可能不相同
distorted_image = tf.slice(img_data, begin, size)
plt.imshow(distorted_image.eval())
plt.show()