NiceLeeのBlog 用爱发电 bilibili~

Python 提取视频截图作预览图

2021-01-06
nIceLee

阅读:


现在要对一个视频文件提取关键帧等信息用来做预览图。
图中应当包含 文件名、大小、分辨率、视频时长以及8张截图。
拟选用opencv(主要) + PIL(用于图片添加中文)来加以实现。

成果预览

先放截图。

常见的坑

库的安装问题

原始的源在国内不太行,需要使用镜像:

清华:https://pypi.tuna.tsinghua.edu.cn/simple
阿里云:http://mirrors.aliyun.com/pypi/simple/
中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple/
华中理工大学:http://pypi.hustunique.com/
山东理工大学:http://pypi.sdutlinux.org/ 
豆瓣:http://pypi.douban.com/simple/

图片加入中文乱码问题

本来直接调用opencv的方法,但是不支持中文字符,只能cv转为PIL再加入字符再转为cv

def imgAddText(img, text, xOffset = 0, yOffset = 0):
    #cv2.putText(图像,需要添加字符串,需要绘制的坐标,字体类型,字号,字体颜色,字体粗细)
    img2 = cv2.putText(img, text, (xOffset, yOffset), cv2.LINE_AA, 0.7, (249, 249, 249), 2)
    return img2   
    
def imgAddTextUTF8(img, text, xOffset = 0, yOffset = 0):
    img_cv2_RGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # cv2和PIL中颜色的hex码的储存顺序不同
    img_PIL = Image.fromarray(img_cv2_RGB)
    
    draw = ImageDraw.Draw(img_PIL)    
    font = ImageFont.truetype("simhei.ttf", 20, encoding="utf-8") # 参数1:字体文件路径,参数2:字体大小
    draw.text((xOffset, yOffset), text, (0, 0, 0), font=font) # 参数1:打印坐标,参数2:文本,参数3:字体颜色,参数4:字体
 
    # PIL图片转cv2 图片
    img_cv2_textAdded = cv2.cvtColor(np.array(img_PIL), cv2.COLOR_RGB2BGR)
    return img_cv2_textAdded

截图时的效率问题

网上搜了一下,大多是逐帧读取并计数,文件很大的时候需要几十分钟甚至以小时计。low B代码如下:

#...省略
cutTimes = [10,600,1200, 2400] #在第10, 600,1200,2400秒进行截图
cutFrames = [int(info["rate"] * x) + 1  for x in cutTimes ] 
print("cutFrames:", cutFrames)
cap = cv2.VideoCapture(file_path)
frame_count = 1
cut_cnt = 0
while(True):
    ret, frame = cap.read()
    if ret:
        if frame_count == cutFrames[cut_cnt]:
            t_frames.append( (cutTimes[cut_cnt], frame) )
            cut_cnt += 1
            print("开始截取视频第:" + str(cut_cnt) + " 帧")
        frame_count += 1
        if cut_cnt >= len(cutFrames):
            break
        #cv2.waitKey(0)
    else:
        print("所有帧都已经保存完成")
        break

但是,实际上可以人为设置读取帧的位置,没必要一帧一帧的读:

#...省略
cutTimes = [10,600,1200, 2400] #在第10, 600,1200,2400秒进行截图
cutFrames = [int(info["rate"] * x) + 1  for x in cutTimes ] 
print("cutFrames:", cutFrames)
cap = cv2.VideoCapture(file_path)
cut_cnt = 0
for cutFrame in cutFrames:
    cap.set(cv2.CAP_PROP_POS_FRAMES,cutFrame -1)
    ret, frame = cap.read()
    if ret:
        t_frames.append( (cutTimes[cut_cnt], frame) )
        cut_cnt += 1
        print("截取视频第:" + str(cut_cnt) + " 帧")
    else:
        break

相关库的安装

pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple
pip install pillow  -i https://pypi.tuna.tsinghua.edu.cn/simple

相关代码

默认是根据时长均匀抽取 8张截图,将每张截图重新等比拉伸为宽480的图像,并且打上时间戳。
相关间隔参数可以在mp4toJpg方法里改改。
因为是自用,也就没怎么封装了。

import cv2
import os
import numpy as np
import math
from PIL import Image, ImageDraw, ImageFont

def getInfo(file_path):
    info = {}
    filepath, filename = os.path.split(file_path)
    info["name"] = filename
    
    if os.path.exists(file_path):
        info["size"] = os.path.getsize(file_path)
        info["sizeUnit"] = sizeConvert(info["size"])
        
    cap = cv2.VideoCapture(file_path)
    if cap.isOpened():
        # get方法参数按顺序对应下表(从0开始编号)
        rate = cap.get(5)  # 帧速率
        frame_number = cap.get(7)  # 视频文件的帧数
        info["rate"] = rate
        info["duration"] = int(frame_number / rate)
        info["durationHMS"] = timeConvert(info["duration"])
        info["width"]=int(cap.get(3))
        info["height"]=int(cap.get(4))
        cap.release()
    return info

def getFrames(file_path, cutTimes):
    '''
    file_path: 文件名
    cutTimes: 抽取帧的时间数组,时间单位为s
    return [时间, 帧图像]数组
    '''
    t_frames = []
    
    info = getInfo(file_path)
    cutFrames = [int(info["rate"] * x) + 1  for x in cutTimes ] 
    print("cutFrames:", cutFrames)
    cap = cv2.VideoCapture(file_path)
    cut_cnt = 0
    for cutFrame in cutFrames:
        cap.set(cv2.CAP_PROP_POS_FRAMES,cutFrame -1)
        ret, frame = cap.read()
        if ret:
            t_frames.append( (cutTimes[cut_cnt], frame) )
            cut_cnt += 1
            print("截取视频第:" + str(cut_cnt) + " 帧")
        else:
            break
    cap.release()
    return t_frames
    
def timeConvert(seconds, str = True):
    h = seconds // 3600
    m = seconds % 3600 // 60
    s = seconds % 60
    if str:
        if h > 0:
            return '{:.0f}:{:.0f}:{:.0f}'.format(h, m, s)
        else:
            return '{:.0f}:{:.0f}s'.format(m, s)
    else:
        return h, m, s
        
def sizeConvert(size):# 单位换算
    K, M, G = 1024, 1024**2, 1024**3
    if size >= G:
        return str(size//G)+'GB'
    elif size >= M:
        return str(size//M)+'MB'
    elif size >= K:
        return str(size//K)+'KB'
    else:
        return str(size)+'Bytes'

def imgResize(img, width = 1980):
    '''
    将图片等比拉伸至宽为width
    '''
    h, w = img.shape[:2] 
    height = int(h * width / w)
    if width > w:#放大图像
        img_new = cv2.resize(img, (width, height), interpolation=cv2.INTER_CUBIC)
    else:
        img_new = cv2.resize(img, (width, height), interpolation=cv2.INTER_AREA)
    return img_new

def imgAddImg(imgDst, imgSrc, xOffset = 0, yOffset = 0, copy = False):
    if copy:
        imgDst = imgDst.copy()
    imgDst[yOffset:yOffset+imgSrc.shape[0], xOffset:xOffset+imgSrc.shape[1]] = imgSrc
    return imgDst
    
def imgAddText(img, text, xOffset = 0, yOffset = 0):
    #cv2.putText(图像,需要添加字符串,需要绘制的坐标,字体类型,字号,字体颜色,字体粗细)
    img2 = cv2.putText(img, text, (xOffset, yOffset), cv2.LINE_AA, 0.7, (249, 249, 249), 2)
    return img2   
    
def imgAddTextUTF8(img, text, xOffset = 0, yOffset = 0):
    img_cv2_RGB = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # cv2和PIL中颜色的hex码的储存顺序不同
    img_PIL = Image.fromarray(img_cv2_RGB)
    
    draw = ImageDraw.Draw(img_PIL)    
    font = ImageFont.truetype("simhei.ttf", 20, encoding="utf-8") # 参数1:字体文件路径,参数2:字体大小
    draw.text((xOffset, yOffset), text, (0, 0, 0), font=font) # 参数1:打印坐标,参数2:文本,参数3:字体颜色,参数4:字体
 
    # PIL图片转cv2 图片
    img_cv2_textAdded = cv2.cvtColor(np.array(img_PIL), cv2.COLOR_RGB2BGR)
    return img_cv2_textAdded

def imgWhite(height, width):
    height = int(height)
    width = int(width)
    img = np.zeros((height,width), dtype=np.uint8)
    #img = np.zeros((height,width,3), dtype=np.uint8)
    img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
    img[:,:,:] = 255
    return img
    
def mp4toJpg(f_video, f_img):
    #start = cv2.getTickCount()
    small_pic_width = 480
    small_pic_cnt = 8
    small_pic_per_line = 2
    small_pic_gap_x = 5
    small_pic_gap_y = 5
    small_pic_top = 120
    small_pic_bottom = 5
    small_pic_left = 5
    small_pic_right = 5
    time_reduce = 5
    
    # 获取信息
    info = getInfo(f_video)
    # 提取图像
    period = int( (info["duration"] - time_reduce) // small_pic_cnt )
    cutTimes = [ period*i for i in range(small_pic_cnt) ]
    t_imgs = getFrames(f_video, cutTimes)
    small_pic_cnt = len(t_imgs)
    
    # 对图像进行缩放,添加时间戳水印
    imgs_fixed = []
    h, w = (0,0)
    for time, img in t_imgs:
        #print(r"正在加工图片%d"%time)
        img_resize = imgResize(img, small_pic_width)
        img_add_time = imgAddText(img_resize, timeConvert(time), 10, 25)
        h, w = img_add_time.shape[:2] 
        #cv2.imwrite(str(time) + "_time.png", img_add_time)
        imgs_fixed.append(img_add_time)
    # 先画一张空白图
    height = small_pic_top + small_pic_bottom + (math.ceil(small_pic_cnt*1.0/small_pic_per_line)) *(small_pic_gap_y + h) - small_pic_gap_y
    width = small_pic_left + small_pic_right + small_pic_per_line *(small_pic_gap_x + w) - small_pic_gap_x
    img_backgroud = imgWhite(height, width)
    # print("height = %.0f, width = %.0f"%(height,width))
    # print("h = %.0f, w = %.0f"%(h,w))
    
    # 在大图上加入视频信息
    filepath, filename = os.path.split(f_video)
    img_backgroud = imgAddTextUTF8(img_backgroud, "视频名称: %s"%filename, 20, 20)
    img_backgroud = imgAddTextUTF8(img_backgroud, "视频时长: %s"%info["durationHMS"], 20, 45)
    img_backgroud = imgAddTextUTF8(img_backgroud, "视频大小: %s"%(info["sizeUnit"]), 20, 70)
    img_backgroud = imgAddTextUTF8(img_backgroud, "分辨率: %dx%d"%(info["width"],info["height"]), 20, 95)
    # 在大图上加入视频截图
    i = 0
    while i < small_pic_cnt:
        xx = i%small_pic_per_line
        yy = int(i/small_pic_per_line)
        #print(r"正在将第(%d,%d)放入背景"%(xx+1, yy+1))
        off_x = int(small_pic_left + (small_pic_gap_x + w)*xx)
        off_y = int(small_pic_top + (small_pic_gap_y + h)*yy)
        img_backgroud = imgAddImg(img_backgroud, imgs_fixed[i], off_x, off_y)
        #imgAddImg(img_backgroud, img_backgroud, 0, 0)
        i+=1
    
    #end = cv2.getTickCount()
    #time_spent = (end-start)/ cv2.getTickFrequency()/60
    #print("耗时%.1f min"%time_spent)
    cv2.imwrite(f_img, img_backgroud)
    #cv2.imshow("img", img_backgroud)    
    #cv2.waitKey(0)
    
if __name__ == '__main__':
    #getInfo(r"test.mp4")
    #getFrames(r"test.mp4", [10, 20])
    video = r"test.mp4"
    img = "test.jpg"
    mp4toJpg(video, img)

内容
隐藏