使用PyAV读取H265(HEVC)编码的视频文件

Nov 12,2020   3974 words   15 min


在之前的这几篇博客:视频拆分程序视频内容预览工具(利用Python实现)中都介绍了如何将视频文件拆分成一帧帧的影像。一般情况下而言,也基本能满足需求,但最近出现新的需求。利用海康威视相机拍出来的视频是H265格式的,相比于常规的H264编码,压缩率更高,但因此如果直接用之前的方法读取,会出现无法读取的情况,如下。 可以看到,对于帧间没有改变的部分,H265编码就只保存一份,因此直接解析出来就都是空白的,只保存了当前帧中运动(不同的部分)。这样的话,采用常规的OpenCV读取视频的方式就行不通了。因此本篇博客主要介绍另一种更“专业”的视频读取方法:利用PyAV读取视频。另外也对之前的OpenCV读取视频文件代码进行了优化,效率会高很多。具体来说就是直接使用get()函数根据索引直接获取到指定帧即可,而不再需要逐帧遍历。

1.PyAV简介与安装

简单来说,PyAV是对FFmpeg的Python封装,集成了FFmpeg的全部功能,使用起来十分方便。如果对视频处理稍微有点了解的话,应该听过FFmpeg,官网是这里,它是一个跨平台的多媒体处理工具,很多一些小的视频播放器都是基于它开发的。PyAV的官网是这里。相比于前面提到的OpenCV,它是一个专门用来处理视频的库,因此各种功能十分专业,包括转码、读取音频等。它的安装也十分简单,pip即可。

pip install av

对,它的简写就是av,不要想歪哦。安装好以后就可以愉快地写代码了。

2.H265视频读取代码

利用PyAV读取H265视频文件的核心代码如下。

import av

if __name__ == '__main__':
    video_path = "./D01_20201011131153.mp4"
    out_path = "./frames"
    frame_interval = 500

    container = av.open(video_path)
    # 获取要提取的视频流对象
    stream = container.streams.video[0]
    fps = stream.base_rate  # 帧率
    frame_width = stream.width  # 帧宽
    frame_height = stream.height  # 帧高
    total_time_in_second = stream.duration * 1.0 * stream.time_base  # 视频总长
    total_frame = int(total_time_in_second * fps) + 1  # 视频总帧数
    iter_time = int(total_frame / frame_interval) + 1  # 需要迭代的次数
    print('Total time in second:', round(total_time_in_second, 3))

    counter = 0
    frame_indices = []
    for frame in container.decode(video=0):
        if frame.index % frame_interval == 0:
            counter += 1
            frame_indices.append(frame.index)
            print(counter, '/', iter_time)
            # to_image()函数返回的其实是PIL类型的影像,因此,这里的save()函数其实是PIL的Image类型的成员函数,和PyAV无关
            # 因此,如果需要修改什么保存相关设置的话,按照PIL的API进行
            frame.to_image().save(out_path + "/frame-%05d.jpg" % frame.index)

    fout = open(out_path + "/summary.txt", 'w')
    fout.write("Video name:" + video_path + "\n")
    fout.write("Frames in total:" + total_frame.__str__() + "\n")
    fout.write("FPS:" + fps.__str__() + "\n")
    fout.write("Seconds in total:" + round(total_time_in_second, 3).__str__() + "\n")
    fout.write("Frame width:" + frame_width.__str__() + "\n")
    fout.write("Frame height:" + frame_height.__str__() + "\n")
    fout.write("Output frame number:" + iter_time.__str__() + "\n")
    for i in range(len(frame_indices)):
        fout.write(frame_indices[i].__str__() + "\t" + round(frame_indices[i] * 1.0 / fps, 3).__str__() + "\n")
    fout.close()

    print("\n==========Summary Info==========")
    print('Frames in total:', total_frame)
    print('FPS:', fps)
    print('Frame width:', frame_width)
    print('Frame height', frame_height)
    print("Output frame number:", len(frame_indices))

输出的帧如下。 除了输出的帧影像,还会保存输出的每一帧所对应的时间,方便其它后续应用。 当然,如果仔细研究的话可以看到,这里其实还是非常简单粗暴地读取每一帧,然后选择指定帧保存。因为目前我并不知道如何根据索引来定位某一帧,因此只能用这样的办法了。如果以后了解的话,会及时更新。

3.优化版OpenCV代码

把之前循环遍历的步骤简化了,这样就不用逐帧读取,直接获取指定位置的帧内容就好了。

import cv2

if __name__ == '__main__':
    video_path = "./D01_20201011131153.mp4"
    out_path = "./frames"
    out_type = ".jpg"
    time_interval = 50

    cap = cv2.VideoCapture(video_path)
    frames = int(cap.get(7))
    fps = int(cap.get(5))
    video_width = int(cap.get(3))
    video_height = int(cap.get(4))
    frame_interval = time_interval * fps

    total_number = int(round(frames / frame_interval, 0))

    frame_indices = []
    for i in range(0, frames, frame_interval):
        cap.set(cv2.CAP_PROP_POS_FRAMES, i)
        ret, frame = cap.read()
        print('Total frame number:' + total_number.__str__(), ', complete', round((i * 1.0 / frames) * 100, 3), '%')
        cv2.imwrite(out_path + "/frame_" + i.__str__().zfill(5) + out_type, frame)
        frame_indices.append(i)
    cap.release()

    fout = open(out_path + "/summary.txt", 'w')
    fout.write("Video name:" + video_path + "\n")
    fout.write("Frames in total:" + frames.__str__() + "\n")
    fout.write("FPS:" + fps.__str__() + "\n")
    fout.write("Seconds in total:" + round(frames * 1.0 / fps, 3).__str__() + "\n")
    fout.write("Frame width:" + video_width.__str__() + "\n")
    fout.write("Frame height:" + video_height.__str__() + "\n")
    fout.write("Output frame number:" + len(frame_indices).__str__() + "\n")
    for i in range(len(frame_indices)):
        fout.write(frame_indices[i].__str__() + "\t" + round(frame_indices[i] * 1.0 / fps, 3).__str__() + "\n")
    fout.close()

    print("\n==========Summary Info==========")
    print('Frames in total:', frames)
    print('FPS:', fps)
    print('Frame width:', video_width)
    print('Frame height', video_height)
    print("Output frame number:", len(frame_indices))

最后,所有的代码都上传到了Github上,点击查看

4.参考资料

  • [1] https://pyav.org/docs/develop/index.html
  • [2] https://pyav.org/docs/develop/api/stream.html
  • [3] https://www.colabug.com/2020/0326/7170091

本文作者原创,未经许可不得转载,谢谢配合

返回顶部