看一眼就知道你的BMI:基于Keras与迁移学习的人脸预测系统

大数据文摘出品

编译:橡树_Hiangsug

让机器学习基于面部照片预测BMI不仅是一个有趣的项目,这种预测方法也有望成为未来医疗诊断的实用工具。

本文介绍了一个通过面部图像预测BMI(身体质量指数)的神经网络项目。该项目由另一个基于人脸输入对人的年龄 和性别进行分类的项目修改而来,借用了原项目训练模型的权重与通过网络摄像头探测用户面部的脚本。

训练数据集

本项目所采用的训练数据集为用户正面拍摄的4000张人脸图像及通过用户身高体重计算的BMI指数(BMI=体重/身高的平方,单位:kg/㎡)。尽管在此我们无法共享项目的训练数据集,但诸如此类的数据可以在许多地方在线获取。

图形预处理

在训练模型前,我们需要将数据集中的每一图像裁剪至用户的面部,而这一操作可以通过基于Python的dlib库的程序脚本实现。Dlib可用于检测图像中的人脸并为其添加边框线,另外还可以根据要求为截取的面部训练数据拓展为包含原图背景的区域。经实验表明,当边框距离(margin)设为20%并拓展图片大小40%(高度和宽度各20%)时模型的验证效果最佳。

以下便是以Bill Murray的裁剪图像(并不属于训练数据集)举例展示的拓展边框效果,以及在不同添加边框参数下模型在验证集上的最小平均绝对误差值(MAE)表。

原始图像

不同拓展边框下的图像

不同拓展边框下的验证集最小MAE数值表

实验结果表明,尽管无法借拓展边框在20%-50%范围的MAE微小差异判定经其训练的模型优劣,但在其拓展至最少20%下的模型性能显著优于dilb原始剪切图像,这可能是由于拓展边框内包含了较原始剪切图像更多的如额头上部、耳朵和脖子等有助于模型预测BMI值的特征信息。


import os
import cv2
import dlib
from matplotlib import pyplot as plt
import numpy as np
import config


detector = dlib.get_frontal_face_detector()




def crop_faces():
bad_crop_count = 0
if not os.path.exists(config.CROPPED_IMGS_DIR):
os.makedirs(config.CROPPED_IMGS_DIR)
print ‘Cropping faces and saving to %s’ % config.CROPPED_IMGS_DIR
good_cropped_images = []
good_cropped_img_file_names = []
detected_cropped_images = []
original_images_detected = []
for file_name in sorted(os.listdir(config.ORIGINAL_IMGS_DIR)):
np_img = cv2.imread(os.path.join(config.ORIGINAL_IMGS_DIR,file_name))
detected = detector(np_img, 1)
img_h, img_w, _ = np.shape(np_img)
original_images_detected.append(np_img)


if len(detected) != 1:
bad_crop_count += 1
continue


d = detected[0]
x1, y1, x2, y2, w, h = d.left(), d.top(), d.right() + 1, d.bottom() + 1, d.width(), d.height()
xw1 = int(x1 – config.MARGIN * w)
yw1 = int(y1 – config.MARGIN * h)
xw2 = int(x2 + config.MARGIN * w)
yw2 = int(y2 + config.MARGIN * h)
cropped_img = crop_image_to_dimensions(np_img, xw1, yw1, xw2, yw2)
norm_file_path = ‘%s/%s’ % (config.CROPPED_IMGS_DIR, file_name)
cv2.imwrite(norm_file_path, cropped_img)


good_cropped_img_file_names.append(file_name)


# save info of good cropped images
with open(config.ORIGINAL_IMGS_INFO_FILE, ‘r’) as f:
column_headers = f.read().splitlines()[0]
all_imgs_info = f.read().splitlines()[1:]
cropped_imgs_info = [l for l in all_imgs_info if l.split(‘,’)[-1] in good_cropped_img_file_names]


with open(config.CROPPED_IMGS_INFO_FILE, ‘w’) as f:
f.write(‘%s\n’ % column_headers)
for l in cropped_imgs_info:
f.write(‘%s\n’ % l)


print ‘Cropped %d images and saved in %s – info in %s’ % (len(original_images_detected), config.CROPPED_IMGS_DIR, config.CROPPED_IMGS_INFO_FILE)
print ‘Error detecting face in %d images – info in Data/unnormalized.txt’ % bad_crop_count
return good_cropped_images






# image cropping function taken from:
# https://stackoverflow.com/questions/15589517/how-to-crop-an-image-in-opencv-using-python
def crop_image_to_dimensions(img, x1, y1, x2, y2):
if x1 < 0 or y1 < 0 or x2 > img.shape[1] or y2 > img.shape[0]:
img, x1, x2, y1, y2 = pad_img_to_fit_bbox(img, x1, x2, y1, y2)
return img[y1:y2, x1:x2, :]


def pad_img_to_fit_bbox(img, x1, x2, y1, y2):
img = cv2.copyMakeBorder(img, – min(0, y1), max(y2 – img.shape[0], 0),
-min(0, x1), max(x2 – img.shape[1], 0), cv2.BORDER_REPLICATE)
y2 += -min(0, y1)
y1 += -min(0, y1)
x2 += -min(0, x1)
x1 += -min(0, x1)
return img, x1, x2, y1, y2


if __name__ == ‘__main__’:
crop_faces()

图像增强

为了对训练数据集合理利用,图像训练集在每一个epoch中都经过了增强处理,增加了每张原始训练图像在训练网络时的次数。这一步骤通过调取图像增强库Augmentor实现,对图像进行了动态的旋转、翻转及扭曲不同部分的分辨率的操作,也对图像的对比度和亮度进行了修改。

未经增强的原始图像

随机增强的图像


from keras.preprocessing.image import ImageDataGenerator
import pandas as pd
import Augmentor
from PIL import Image
import random
import numpy as np
import matplotlib.pyplot as plt
import math
import config




def plot_imgs_from_generator(generator, number_imgs_to_show=9):
print (‘Plotting images…’)
n_rows_cols = int(math.ceil(math.sqrt(number_imgs_to_show)))
plot_index = 1
x_batch, _ = next(generator)
while plot_index <= number_imgs_to_show:
plt.subplot(n_rows_cols, n_rows_cols, plot_index)
plt.imshow(x_batch[plot_index-1])
plot_index += 1
plt.show()




def augment_image(np_img):
p = Augmentor.Pipeline()
p.rotate(probability=1, max_left_rotation=5, max_right_rotation=5)
p.flip_left_right(probability=0.5)
p.random_distortion(probability=0.25, grid_width=2, grid_height=2, magnitude=8)
p.random_color(probability=1, min_factor=0.8, max_factor=1.2)
p.random_contrast(probability=.5, min_factor=0.8, max_factor=1.2)
p.random_brightness(probability=1, min_factor=0.5, max_factor=1.5)


image = [Image.fromarray(np_img.astype(‘uint8’))]
for operation in p.operations:
r = round(random.uniform(0, 1), 1)
if r <= operation.probability:
image = operation.perform_operation(image)
image = [np.array(i).astype(‘float64’) for i in image]
return image[0]


image_processor = ImageDataGenerator(
rescale=1./255,
preprocessing_function=augment_image)


# subtract validation size from training data
with open(config.CROPPED_IMGS_INFO_FILE) as f:
for i, _ in enumerate(f):
pass
training_n = i – config.VALIDATION_SIZE


train_df=pd.read_csv(config.CROPPED_IMGS_INFO_FILE, nrows=training_n)


train_generator=image_processor.flow_from_dataframe(
dataframe=train_df,
directory=config.CROPPED_IMGS_DIR,
x_col=‘name’,
y_col=‘bmi’,
class_mode=‘other’,
color_mode=‘rgb’,
target_size=(config.RESNET50_DEFAULT_IMG_WIDTH,config.RESNET50_DEFAULT_IMG_WIDTH),

batch_size=config.TRAIN_BATCH_SIZE)

模型建立

模型基于Keras ResNet50建立。ResNet50是目前常用于面部图像识别的优秀模型,其中从年龄和性别识别项目中经过广泛训练的年龄分类器生成的权重可用于迁移学习。

其他的一些网络架构也在面部识别任务中大放异彩,未来也可以尝试探索以上架构建立的BMI预测模型。