1.问题出现
利用海康威视相机拍出来的视频是H265格式的,相比于常规的H264编码,压缩率更高,但因此如果直接用之前的方法读取,会出现无法读取的情况,如下。
可以看到,对于帧间没有改变的部分,H265编码就只保存一份,因此直接解析出来就都是空白的,只保存了当前帧中运动(不同的部分)。这样的话,采用常规的OpenCV读取视频的方式就行不通了。因此本篇博客主要介绍另一种更“专业”的视频读取方法:利用PyAV读取视频。另外也对之前的OpenCV读取视频文件代码进行了优化,效率会高很多。具体来说就是直接使用get()
函数根据索引直接获取到指定帧即可,而不再需要逐帧遍历。
2.PyAV简介与安装
简单来说,PyAV是对FFmpeg的Python封装,集成了FFmpeg的全部功能,使用起来十分方便。如果对视频处理稍微有点了解的话,应该听过FFmpeg,官网是这里,它是一个跨平台的多媒体处理工具,很多一些小的视频播放器都是基于它开发的。PyAV的官网是这里。相比于前面提到的OpenCV,它是一个专门用来处理视频的库,因此各种功能十分专业,包括转码、读取音频等。它的安装也十分简单,pip
即可。
pip install av
对,它的简写就是av
,安装好以后就可以愉快地写代码了。
3.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))
输出的帧如下。
除了输出的帧影像,还会保存输出的每一帧所对应的时间,方便其它后续应用。当然,如果仔细研究的话可以看到,这里其实还是非常简单粗暴地读取每一帧,然后选择指定帧保存。因为目前我并不知道如何根据索引来定位某一帧,因此只能用这样的办法了。如果以后了解的话,会及时更新。
4.优化版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))