https://github.com/wjwzy/lip_reading
项目主要针对基于视频的计算机唇读系统中唇部检测、唇读特征提取和唇语识别等关键技术进行了研究。具体来说,首先对数据进行预处理,包括对视频进行切帧和增广处理;然后采用Yolov5算法对唇部检测并截取有效区域进行后续处理;接着设计了一个新型网络用于唇部特征的提取和识别。该网络融合了3DResNet和GRU网络,能够同时利用视频数据的空间和时间信息,进而提取高效的特征获得较好的识别结果。最后,为了体现可视化和实用性,本文使用Flask框架实现了唇读系统的各个功能,可以在web端体验唇读系统的识别效果。
研究的主要内容是对中文唇语词语的识别功能,研究的问题涉及到唇部定位、数据特征提取、网络对特征的预测结果以及web端实现前后端数据交互四个方面。而最为关键的,就在于特征提取以及预测结果两个方面,重点设计可行的深度学习方案,对比各种方法来研究出本文的最佳实现手段。对于模型的鲁棒性和泛化能力,项目需要大量而又无误的数据集支持,以及采用合适该数据集的网络模型,才能保证网络拥有强有力的表现能力。
在系统功能上,用户需要登录成功才能使用唇语识别功能,项目流程如图所示。
在技术路线上,第一个模块是目标检测,该模块需要准确的找到人脸的唇部位置,并且通过预测的坐标对图像进行切割,保证唇部位于图像的最中间位置,项目实现算法为Yolov5算法,采用最小的预训练模型进行训练。在目标检测数据集的制作中,需要保证数据的完整性,并且标注的嘴唇应该位于图像的最中间位置,达到唇部定位的效果,这样可以大程度加快后续分类网络的拟合速度。
第二模块采用的是3DResNet与GRU复合式网络,通过Yolov5算法处理过的数据传入该网络中,残差结构提取特征,GRU保证时序信息的传递与保存,再通过softmax得到预测的结果。在该模块中,训练数据的数量尤为关键,所以在预处理中,使用数据增广让网络有充足的数据训练。其次,网络的结构也非常重要,残差网络ResNet是由多层网络堆叠而成,解决网络深度造成的梯度消失问题,让网络更深,提取到的图像信息特征越多越有效。而循环神经网络RNN的变种体GRU则是通过了门的控制机制,使时序信息得到很好的保留,让神经网络更加关注的是时间序列的唇部动态变化信息。
预测结果最终传入web模块,依靠html和js完成前后端的交互,实现的系统的识别功能。在web模块中,所使用的框架是Flask框架,该框架优点就是轻巧灵活,在登录功能中html直接往后端提交表单,后端只要通过数据库对表单进行校验,就可以完成登录功能。在识别功能中,通过js配合html读取本地视频,再提交到后台进行处理,处理过程首先是经过视频切帧,然后Yolov5模型对帧数图像进行唇部坐标预测,再切割图像并保存,最后通过分类网络得到预测的结果,识别的词语展示到前端页面即可完成整个功能的流程,处理的流程如图所示。
项目采用的技术为当今最主流的one-stage目标检测算法Yolo,用来辅助残差网络ResNet对视频进行唇语翻译,该目标检测算法首次应用于唇语识别中,使系统达到端到端的识别效果。其次数据集是由多帧数图像组成单一样本,存在缺帧等问题,给项目带来了一定的难度。项目还采用了python轻量级web框架Flask,通过前端html和js的配合使用来操作深度学习算法,达到视觉上的展示效果,让用户可以使用网页端操作唇语识别系统。
项目由三部分构成,唇读模块、目标检测模块和web端demo模块,由于目标检测的模型路径问题,将demo和yolov5整合到了一个目录下。
post请求将前端读取的视频先保存至本地相应目录,再做后续相关操作。
@app.route('/predict', methods=['GET', 'POST'])
def predict():
if request.method == 'POST':
try:
# 读取video文件
f = request.files['file']
# 保存前端读取的视频到uploads
basepath = args.save_video
file_path = os.path.join(basepath, secure_filename(f.filename))
f.save(file_path)
img_list = video_to_frames(file_path)
cut_img_list = cut_img(img_list)
del img_list
vocab_path = 'lip_models/vocab100.txt'
# 载入网络进行预测
result = model_predict(model, cut_img_list, vocab_path, args.device)
result = str(result[0])
print("识别结果:" + result)
return result
except Exception as e:
return "错误,无法正确识别"
return None
切帧直接存入list返回
def video_to_frames(path):
"""
输入:path(视频文件的路径)
"""
# VideoCapture视频读取类
# 抽取帧数
videoCapture = cv2.VideoCapture()
videoCapture.open(path)
# 总帧数
frames = videoCapture.get(cv2.CAP_PROP_FRAME_COUNT)
img_list = []
for i in range(int(frames)):
ret, frame = videoCapture.read()
if i % 4 == 0:
img_list.append(frame)
print("视频切帧完成!")
return img_list
重新构建一个类,将加载模型初始化,由于只有一个类别,因此classes为0,定义detect方法,传入参数为图像的list,返回结果为对应坐标的list。
class yolov5(object):
def __init__(self,
img_size = 416,
weights = 'runs/train/exp/weights/best.pt',
iou_thres = 0.45,
conf_thres = 0.25,
device = '0',
classes = 0,
agnostic_nms = False,
augment = False
):
self.imgsz = img_size
self.iou_thres = iou_thres
self.conf_thres = conf_thres
self.device = select_device(device)
self.classes = classes
self.agnostic_nms = agnostic_nms
self.augment = augment
# Initialize
set_logging()
self.half = self.device.type != 'cpu' # half precision only supported on CUDA
# Load model
self.model = attempt_load(weights, map_location=self.device) # load FP32 model
def detect(self, source):
stride = int(self.model.stride.max()) # model stride
imgsz = check_img_size(self.imgsz, s=stride) # 检查图片的大小
if self.half:
self.model.half() # to FP16
cudnn.benchmark = True # 设置True可以加速恒定图像大小的处理速度
# Run inference
if self.device.type != 'cpu':
self.model(torch.zeros(1, 3, imgsz, imgsz).to(self.device).type_as(next(self.model.parameters()))) # run once
result = []
for img0 in source:
imgsz = check_img_size(imgsz) # check img_size
img = letterbox(img0, imgsz, stride=32)[0]
# Convert
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
img = np.ascontiguousarray(img)
img = torch.from_numpy(img).to(self.device)
img = img.half() if self.half else img.float() # uint8 to fp16/32
img /= 255.0 # 0 - 255 to 0.0 - 1.0
if img.ndimension() == 3:
img = img.unsqueeze(0)
# 获取模型预测
pred = self.model(img, augment=self.augment)[0]
# 使用NMS进行预测
pred = non_max_suppression(pred, self.conf_thres, self.iou_thres, classes=self.classes,
agnostic=self.agnostic_nms)
# 过程检测
for i, det in enumerate(pred): # 遍历预测框
# 还原图像坐标值大小
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape).round()
result.append(det[0][:4].tolist())
return result
目标检测模块得到唇部坐标,然后裁剪保存至新的list中返回。
# 根据预测得到的坐标进行裁剪
def cut_img(img_list):
# 进行目标检测得到坐标点
result = yolov5_det.detect(img_list)
cut_img_list = []
for idx, image in enumerate(img_list):
labels = result[idx]
cropped = image[int(labels[1]): int(labels[3]), int(labels[0]):int(labels[2])]
cut_img_list.append(cropped)
print("嘴型检测并裁剪完成!")
return cut_img_list
def _sample(cut_img_list, bilater = True):
data = []
for img in cut_img_list:
img = img_clip(img) # 缩放并填充至112大小
if bilater and random.random() < 0.6:
# 引入双边滤波去噪
img = cv2.bilateralFilter(src=img, d=0, sigmaColor=random.randint(15, 30), sigmaSpace=15)
# 归一化,转换数据类型 并限定上下界限的大小必须为fixed_side
img = img.astype(np.float32)
# 标准化处理
img -= np.mean(img) # 减去均值
img /= np.std(img) # 除以标准差
data.append(img)
return np.array(data)
网络输出后获取最大值的下标,结合词表得到预测的唇读词语类别
def model_predict(model, cut_img_list, vocab_path, device):
model.to(device)
id2label = []
with open(vocab_path, 'r', encoding='utf-8') as f:
for word in f:
id2label.append(word.split(',')[0])
# 预处理
test_data = process(cut_img_list)
test_data = torch.tensor(padding_batch(test_data))
print("数据预处理完成!")
##############################
# 预测
##############################
pre_result = []
with torch.no_grad():
batch_inputs = test_data.to(device)
logist = model(batch_inputs)
pred = torch.argmax(logist, dim=-1).tolist()
pre_result.append(id2label[pred[0]])
return pre_result
在数据方面,项目使用的数据集是2019年“创青春·交子杯”新网银行高校金融科技挑战赛-AI算法赛道的唇语数据集,该数据集是只有图片帧的数据集,整份数据集是由9996份带标签训练样本和2504份无标签预测样本构成,标签文件为txt格式文件,一个样本文件名对应一个中文词语,存储量8GB左右。数据包含的唇语是两个字或者四个字的中文词语,它们的比例为6816:3180,总共有313个类别,除了其中的“落地生根”和“卓有成效”两个类是只有22个样本,其他类别均有32个样本。每个数据样本的帧数由2到24不等,平均帧数在8帧左右,分布比较均匀,图片内容基本都是人脸部的下半部分,如图所示。
图像色度和饱和度都没有太大差异,这样一定程度削减的干扰因素,方便网络能够学习更多的有效特征。
数据可以分为有用信息和无用信息,在这份数据里,开口状态即为有用信息,闭口则是无用信息。因此在数据处理过程,神经网络应该着重注意开口状态的特征,但唇语数据中唇形差异并不大,使得低层数的网络提取不到更多有效的特征,所以在主干网络的选取上为多层数的深度3D神经网络,以便提取更多维度的特征提供网络学习记忆。此外,时间序列的特征信息也尤为重要,仅靠3D网络往往不会有太好的效果,因此项目在分类网络结构中需要引入循环神经网络RNN,它能将最大限度的保留时序特征,非常符合网络对特征的需求。
项目对比不同的目标检测算法进行唇部检测,yolov5s、Faster-RCNN、SSD300以及Yolov3-spp这些主流目标检测算法。不同算法与不同模型对同一份数据集的效果肯定都是不一样的,通过试验不同算法在数据集中都进行了不同程度的训练以及测试,整理出了每一个算法在AP-50精度、GPU推理速度、置信度以及模型存储空间四个方面上的性能分析,如表所示。
在项目设计中,目标检测模块需要的仅仅是推理速度上的高相应需求。在参考多份资料以及通过实验结果分析,最终将算法的选择上采取Yolov5算法。
项目经过几轮测试对比,网络最终修改为ResNet内嵌一层的GRU,同样形成CNN与RNN的复合式网络,但网络并不会单纯使用3D的卷积与池化的结构,因为在特征提取的过程中,更趋向于CNN来提取像素特征,让GRU来提取时间动态变化的特征。因此在残差模块中,卷积和池化将替换成2D操作,这一定程度上能避免时间维度带来的干扰。流程如图所示。
残差模块如下图所示。
由于网络处理的数据是由多帧图像构成的动态变化多维数据,如果仅仅采用3D卷积和池化等操作,会因为下采样过程中使得时间维度数据丢失。从数据集来说,每一个样本的帧数并不恒定,而且是从2帧到24帧不等,数据帧数平均在8帧左右,由于帧数过低,时间维度上的信息本身就存在过少状态。因此当采用3D的卷积与池化后,会进一步缩减该维度上的特征数据,所以网络最终会过分依赖图像的像素两个维度来进行强行拟合,这是完全违背了神经网络的设计理念。
当残差网络从3D的残差模块更换成2D之后,此时数据中的时间维度并不会进行下采样,所以该维度特征会得到一个很好的保留。所以,为了提取时间维度的特征信息,在网络经过线性全连接之后引入一层门控循环单元GRU,让GRU通过更新门和重置门来控制时间维度特征信息的关联性。在全连接层中,通过输出的特征向量与隐藏层形成线性全连接,使网络融合了残差模块的特征信息来自适应感受向量的临界点,以此来提高网络的自适应表现能力。具体来说就是在线性全连接层中的引入自适应词语边界,不让GRU输出的隐藏层向量直接连接分类层,而是将GRU的每个时间维度的输出连接到全连接层中,在做sotfmax之后再将时间维度相加,最后使得每个时间维度的输出都能为最后的分类层做出贡献,最大化的实现了网络多维度的特征融合。
项目选择了100个词语进行研究,也就是100个类别,每个类别32份样本,一份样本平均帧数8帧左右,因此总体数据量有100×32×8=25600张图片左右。
根据标签与文件名生成词表,根据词表的下标对应各个类别。
在图片的预处理中,首先是将图片进行缩放填充至112的像素,并对所有图像进行镜像翻转的数据增强,并对所有图像进行60%概率的双边滤波去噪。如下图所示,a为原图,b是经过缩放、填充、双边滤波后的图像,可以发现相邻的像素变得更加平滑,唇部棱角变得比较立体,图像的阴影更少了,这样很大程度减少了噪声的干扰。c图是镜像翻转的增强图像,可以使数据得到扩充,将3200份样本扩充到6400份。
由于网络一直过拟合,通过观察数据集,发现这是由于数据集帧数不恒定而产生的。所以在分批次的时候,会产生同一批次帧数不一的情况,这种情况有可能使网络会被帧数的干扰而影响特征提取的效果。因此在预处理阶段,程序划分批次的时候,就需要对每个批次进行0填充再训练,也就是将批次中每个样本的帧数固定到样本最大值,批次中样本统一帧数的形式输入到网络中训练。
纯3DResnet18+预处理trick训练效果:
3DDensenet+LSTM+GRU+预处理trick训练效果:
3D+2D残差模块+GRU+预处理trick训练效果:
不同预处理手段对比:
项目最终成品是页面测试离线视频,从切帧、目标定位切割、网络分类、页面展示结果。
点击Choose按钮选择本地离线视频,选择后出现Predict按钮,点击后进行视频处理,待网络处理完响应请求,并将结果展示至页面,如下图。
唇语识别功能的测试中,会存在一个低泛化问题,这是由于数据集产生的。首先数据集中的样本是制作方采样而成,当训练出一个损失低正确率高,并且整体测试集效果比较好的模型时,对自己录制的视频进行切帧并送入网络进行分类,会出现准确率低的情况,这很明显模型对于非数据集的测试样例预测的效果并不是特别好。而模型的泛化能力想要提升,就需要对训练的数据集进行调整,例如增加一些自己的图像,增大每个类别的样本数据,以此提高模型的各项综合能力。
除了泛化问题,还存在相似词语识别混淆问题。例如样本中“技术”和“基础”两个词语,它们之间存在高度相似的动态唇形,测试结果和预想的一样,模型有时会对类似词语无法正确地分类。如下图为系统对录制的“技术”、“基础”两个词语视频识别结果。
在上图中,a图为“技术”词语,b图为“基础”词语,但是网络识别结果却张冠李戴,将两词识别相互混淆。如下图为二词唇部定位切割后的帧数图,首先从唇形上来说二者差异并不大,唯一不同的仅仅是“术”字和“础”字发音时的嚼舌情况不同。然而网络目前仅仅是针对图像的技术处理,并未涉及高层次的语义分析,因此在遇到唇形相似的词语这种情况下,根本无法正确地分辨类别。
混淆词语唇形切割:
在所使用的数据集中,类别的样本数不够很大程度上导致了这种情况产生,不仅如此,唇语识别原本是一个句子输入的过程,前一个字与后一个字的关联性也取决了识别的准确率高低。因此,在现有的基础上要想提高识别效果,首先需要从数据集出发,将过低帧数的样本进行重新采样,抽取高帧数的数据集样本,提高数据集的质量与科学性。同时还需要对网络深度进行修改,增加网络计算参数,让特征在网络中更加细化。其次,应该在网络中引入注意力机制,让网络充分提取开口的像素特征,以及时间维度上帧与帧之间的联系。最后需要设计语义分析模块,将图像转化成语义再对其特征向量做分析处理,细化字与字之间的关联特征,并将数据映射到更高的维度上做到数据再分。
针对web端操作下的唇语识别系统,本文主要是使用了两大主流深度学习算法部署到Flask框架的集成思想,对如下内容进行了研究应用:
(1)Yolov5算法对人脸进行唇部定位,采用预测的坐标对数据集进行处理,整理得到图像内容仅包含有效信息的数据集;
(2)在本文所使用的数据集中,对比了不同的目标检测算法与分类网络结构,通过实验数据分析来最终确定选用的算法和网络结构;
(3)设计3DResNet和GRU复合网络,利用2D的残差模块组成深度网进行提取特征,最后利用GRU将每个帧数映射到特征维度中,形成由批次、时序和图像像素的高维度特征信息,再经过全连接层和softmax层处理;
(4)整合两个算法到Flask框架中,这是唇语识别首次应用到web框架中,通过设计路由和URL地址,再配合视图函数对预测算法进行方法调用,让系统达到可视化效果;
(5)对视频流进行预测,充分利用3D模型的优点对唇语视频进行识别,从视频的读取到切帧,最后传入网络中对图像包含的唇语信息进行相关的解码,达到端到端的识别效果。
项目一直存在一些不足的地方,针对这些问题,能从以下方面进行优化:第一是泛化能力,项目后续应该从数据集从发,需要进行一次所有类别的数据采集,以此来扩充样本数目。同时,在网络层数上,可以适当进行加深,以此来增强网络的表现能力,让数据更加细分,模型能学习到更高维度的特征信息。第二是类别数目上,可以扩充至原数据集的313类,让项目能够识别更多的中文词语,让系统不受限于翻译的类别,让更多中文词语甚至短语能够正确的被识别。第三是响应时间里,模型有可能受电脑的硬件设备影响,也有可能是双网络的原因,导致处理时间过长,这应该是项目未来工作的重点研究对象,优化项目的各个细节,提升算法的执行能力。
此外,当前识别的仅仅是词语,真正的唇语识别应该是以句子的形式被翻译的,这就需要对视频进行语句判断,例如开口到闭口的停顿时长,是否可以利用这一点来做句子识别的突破口。在识别功能上,目前仅仅是读取离线视频识别,未来的优化方向也可以向实时视频识别进军,让整个项目更加的智能,更加人性化,攻克唇语识别的这个难题,为将来的唇语工作做出积极的贡献。