最近在mooc上看了一个公开课:职场英语,用来学习在找工作时或者工作中用到的英语交流技巧,非常不错。由于自己听力不是很好,有的字幕中的单词不认识,并且想记下来便于以后学习。所以想把公开课中的字幕都记录下来,存到一个txt中,便于自己去翻译或者后续温习。基于这个需求,通过python opencv tesseract等工具,完成了视频字幕的提取。
首先,我们需要将mooc上的公开课录制下来,这里,我使用了win10自带的xbox录屏工具(快捷键win+G可以调出来),打开mooc职场英语公开课,播放时按快捷键win+Alt+R开始录制,会得到mp4格式的视频。
我录制的视频如下图所示:
有了视频之后我们就可以编写python程序来进行字幕的提取。
首先我们看一下通过python opencv来读取视频:
import cv2
#读取视频
videoCapture = cv2.VideoCapture('./test.mp4')
#读一帧
success, frame = videoCapture.read()
#连续读帧,保存成图片
cnt = 0
while success:
jpg_name = "./frame"+str(cnt)+".jpg"
cv2.imwrite(jpg_name,frame)
cnt += 1
success, frame = videoCapture.read() #获取下一帧
这样,我们会在当前路径下看到视频保存成了一张一张的图片。
既然用opencv将视频能变成一张一张的图片,我们就可以在程序的while中进行处理,将字幕提取出来。对于提取字幕,其实是一个ocr的问题,首先需要文本检测,之后进行识别。
这里我们使用一个比较简单的文本检测方法,分两步,垂直分割与水平分割。垂直分割我们考虑到字幕都是出现在视频的下方,所以简单的使用一个固定的垂直方向ratio(比如0.9)就可以,我们提取(图片高*ratio,图片高)这个范围内的一个长条子图像就可以了,比如图片高100,我们提取(90,100)这个长条图像。水平分割我们在图像二值化之后的图上做(字幕像素为255,背景像素为0),逐列检测这个长条图像的像素值,发现这一列上有的像素不为0,说明这一列包含字幕了,这样可以找到左右边界,做切分就可以。为了使后面的识别准确率高一些,我们最好不要完全按照左右边界去切,左右各留出一个小边界最好。这两部分的代码如下(我们定义了一个函数,逐步完成 灰度化->二值化->垂直切分->水平切分 操作。左右切分时的小边界代码中用的10像素。函数参数thresh_代表二值化时的分割线,小于thresh_的像素置零,大于的置255。函数参数v_cut_ratio_代表垂直切分的固定ration,如上文说的0.9。注意函数中的if代表我们考虑了没有字幕的情况,如果左右边界特别小,code中是小于20,我们认为它没有字幕,就不会进入if,从而返回一个np.zero(1)):
def caption_region_extr(src_img, thresh_, v_cut_ratio_):
#to gray
imgray = src_img
if len(src_img.shape)==3 and src_img.shape[-1]==3:
imgray = cv2.cvtColor(src_img,cv2.COLOR_BGR2GRAY)
#binary
th,img_bn = cv2.threshold(imgray,thresh_,255,cv2.THRESH_BINARY)
#vertical cut
crop_start = int(v_cut_ratio_ * img_bn.shape[0])
crop_end = img_bn.shape[0]
v_cut_img = img_bn[crop_start:crop_end,:]
#horizontal cut
h_left = 0
h_right = 0
for i in range(v_cut_img.shape[1]):
if np.any(v_cut_img[:,i]>0):
h_left = i
break
for i in range(v_cut_img.shape[1]-1,-1,-1):
if np.any(v_cut_img[:,i]>0):
h_right = i
break
h_cut_img = np.zeros(1)
if (h_right-h_left)>20:
#expand a little
h_left = max(h_left - 10,0)
h_right = min(h_right + 10, v_cut_img.shape[1]-1)
h_cut_img = v_cut_img[:,h_left:h_right+1]
return h_cut_img
经过这个函数的处理,我们得到的图像就是只包含字幕的区域(二值图像):
将这个图像送入tesseract,就可以得到正确的string,把这个string保存到txt中即可。
import pytesseract
text = pytesseract.image_to_string(cap_img)
tesseract我是下载的4.0版本,windows,地址:https://tesseract-ocr.github.io/tessdoc/4.0-with-LSTM.html#400-alpha-for-windows,下载完成之后安装,需要将安装路径加入到环境变量Path,并且需要新加入一个用户变量TESSDATA_PREFIX:
否则会出现如下错误:
安装好tesseract之后就可以在win的cmd中使用tesseract了。要在python程序中使用,还需要安装python库:pip install pytesseract
介绍完了tesseract,我们继续完善我们的python程序,由于opencv while读取一帧一帧的视频会造成字幕重复的现象,有可能我们好几帧提取的都是一行字幕,毕竟字幕变化没有帧率快的。所以我们还需要提取完图像之后增加一个去重的功能,这里就是简单的实现了一个Equal_函数来判断两幅提取的图像是不是相似的,通过图像尺寸与图像矩阵的余弦距离来进行判断,代码如下(注意之所以将图像矩阵转换为float64,就是怕矩阵展开成向量计算相似读的时候int型越界):
def Equal_(region_a_, region_b_, thresh_):
if region_a_.shape != region_b_.shape:
return False
a = region_a_.reshape(-1).astype(np.float64)
b = region_b_.reshape(-1).astype(np.float64)
a_norm = np.linalg.norm(a)
b_norm = np.linalg.norm(b)
similiarity = np.dot(a, b.T)/(a_norm * b_norm)
dist = 1. - similiarity
if dist>thresh_:
return False
else:
return True
当然,我们通过将字幕图像输入给tesseract,识别之后在string上判断是否相同也是可以的,这里我们用图像判断一次,然后再用tesseract结果判断一次,鲁棒一些。
这样,我们就会逐帧得到对应的字幕string,将其保存在一个list中,读完视频之后将list写入文件中就可以了。
这个整体过程就介绍完了,当然这个过程是很简单的,其实我们可以引入一些神经网络模型来做文本的提取与识别。这里就是简单的做水平垂直分割,后续复杂场景可以再进行引入。比如各种视频的文本提取等应用,我们可以考虑EAST或者CTPN文本检测,CRNN+CTC文本识别的方案,还有端到端的检测识别方案FOTS等。再完善一些,我们将识别出来的英文文本txt进行翻译变成其他语言,引入更加完美的方案。
本录制视频的字幕提取整体python 代码如下所示:
import cv2
import numpy as np
import pytesseract
def caption_region_extr(src_img, thresh_, v_cut_ratio_):
#to gray
imgray = src_img
if len(src_img.shape)==3 and src_img.shape[-1]==3:
imgray = cv2.cvtColor(src_img,cv2.COLOR_BGR2GRAY)
#binary
th,img_bn = cv2.threshold(imgray,thresh_,255,cv2.THRESH_BINARY)
#vertical cut
crop_start = int(v_cut_ratio_ * img_bn.shape[0])
crop_end = img_bn.shape[0]
v_cut_img = img_bn[crop_start:crop_end,:]
#horizontal cut
h_left = 0
h_right = 0
for i in range(v_cut_img.shape[1]):
if np.any(v_cut_img[:,i]>0):
h_left = i
break
for i in range(v_cut_img.shape[1]-1,-1,-1):
if np.any(v_cut_img[:,i]>0):
h_right = i
break
h_cut_img = np.zeros(1)
if (h_right-h_left)>20:
#expand a little
h_left = max(h_left - 10,0)
h_right = min(h_right + 10, v_cut_img.shape[1]-1)
h_cut_img = v_cut_img[:,h_left:h_right+1]
return h_cut_img
def Equal_(region_a_, region_b_, thresh_):
if region_a_.shape != region_b_.shape:
return False
a = region_a_.reshape(-1).astype(np.float64)
b = region_b_.reshape(-1).astype(np.float64)
a_norm = np.linalg.norm(a)
b_norm = np.linalg.norm(b)
similiarity = np.dot(a, b.T)/(a_norm * b_norm)
dist = 1. - similiarity
if dist>thresh_:
return False
else:
return True
#获得视频的格式
videoCapture = cv2.VideoCapture('./test.mp4')
#获得码率及尺寸
# fps = videoCapture.get(cv2.CAP_PROP_FPS)
# size = (int(videoCapture.get(cv2.CAP_PROP_FRAME_WIDTH)),
# int(videoCapture.get(cv2.CAP_PROP_FRAME_HEIGHT)))
# fNUMS = videoCapture.get(cv2.CAP_PROP_FRAME_COUNT)
# print(size)
# print(fps)
#读帧
success, frame = videoCapture.read()
name_cnt = 0
crop_ratio_ = 0.9
pre_cap_region = np.zeros(1)
ocr_=list()
b_reference_ = False
while success:
# cv2.imshow('windows', frame) #显示
# cv2.waitKey(int(1000/fps)) #延迟
cap_region = caption_region_extr(frame, 200, crop_ratio_)
#first caption
if (len(pre_cap_region) == 1) and (len(cap_region.shape) != 1):
pre_cap_region = cap_region
if b_reference_:
img_name = "zimu"+str(name_cnt)+".jpg"
cv2.imwrite(img_name, pre_cap_region)
name_cnt += 1
text = pytesseract.image_to_string(pre_cap_region)
ocr_.append(text)
if len(cap_region.shape) != 1:
if False == Equal_(cap_region,pre_cap_region,0.1):
if b_reference_:
img_name = "zimu"+str(name_cnt)+".jpg"
cv2.imwrite(img_name, cap_region)
name_cnt += 1
text = pytesseract.image_to_string(cap_region)
if text!=ocr_[-1]:
ocr_.append(text)
pre_cap_region = cap_region
success, frame = videoCapture.read() #获取下一帧
videoCapture.release()
with open("result.txt","w") as wf:
for line in ocr_:
wf.writelines(line+"n")
算上空格与一些注释行才100行,非常简单。