为视频转编码以及添加音频

系列仓库地址:https://github.com/xuanhao44/AnimeGANv2


在前篇的 3.3 和 4 中提到 https://www.sheniao.top/tech/191.html,由 OpenCV 的 VideoWriter 导出的视频由于专利问题,并不是原生支持 H264 编码的,而这正是能在浏览器上播放的视频编码格式。于是需要想办法转成这种编码格式。

为此尝试了 issue 里提到的方法,也就是自己编译 OpenCV,再带上 openh264 库,但是很遗憾的失败了。

这次就来尝试其他的办法给视频转编码。

0 服务器

带显卡的服务器:RTX A4000。

1 直接调用 FFMPEG

需要安装 ffmpeg:

sudo apt update
sudo apt install ffmpeg -y

部分代码:

    video_out = cv2.VideoWriter("tmp.mp4", fourcc, fps, (width, height))

    ...(省略)

    # When your video is ready, just run the following command
    # You can actually just write the command below in your terminal

    # https://snipit.io/public/snippets/43806
    # os.system("ffmpeg -i Video.mp4 -vcodec libx264 Video2.mp4")
    # os.system("ffmpeg -i tmp.mp4 -vcodec libx264 " + video_out_path + " -y")

    # https://stackoverflow.com/questions/12938581/ffmpeg-mux-video-and-audio-from-another-video-mapping-issue
    # ffmpeg -an -i tmp.mp4 -vn -i video_path -c:a copy -c:v copy video_out_path
    # os.system("ffmpeg -an -i tmp.mp4 -vn -i " + video_path + " -c:a copy -c:v copy " + video_out_path + " -y")

    # 合成大西瓜!
    os.system(
        "ffmpeg -an -i tmp.mp4 -vn -i " + video_path + " -c:a copy -c:v copy -vcodec libx264 " + video_out_path + " -y")

我在 onnx_video2anime.pyonnx_app.py 中使用了这种办法。

2 使用 PyAV

2.1 说明

PyAV 是 FFmpeg 的 Pythonic 绑定。

文档(稳定版):https://pyav.org/docs/stable/

安装:pip install --user av

2.2 代码

在经历了很长时间的寻找和测试之后,终于成功了。

下面仅展示两个更改的函数。

def process_image_alter(img, x32=True):
    h, w = img.shape[:2]
    if x32:  # resize image to multiple of 32s
        def to_32s(x):
            return 256 if x < 256 else x - x % 32

        img = cv2.resize(img, (to_32s(w), to_32s(h)))
    img = img.astype(np.float32) / 127.5 - 1.0  # 注意修改
    return img

def cvt2anime_video(video_path, output, model, onnx='model.onnx'):
    # check onnx model
    exists = os.path.isfile(onnx)
    if not exists:
        print('Model file not found:', onnx)
        return

    # 加载模型,若有 GPU, 则用 GPU 推理
    # 参考:https://zhuanlan.zhihu.com/p/645720587
    # 慎入!https://zhuanlan.zhihu.com/p/492040015
    if ort.get_device() == 'GPU':
        print('use gpu')
        providers = ['CUDAExecutionProvider', 'CPUExecutionProvider', ]
        session = ort.InferenceSession(onnx, providers=providers)
        session.set_providers(['CUDAExecutionProvider'], [{'device_id': 0}])  # gpu 0
    else:
        print('use cpu')
        providers = ['CPUExecutionProvider', ]
        session = ort.InferenceSession(onnx, providers=providers)

    video_in_name = os.path.basename(video_path)  # 只取文件名
    # 输出视频名称、路径
    video_out_name = video_in_name.rsplit('.', 1)[0] + '_' + model + '.mp4'
    video_out_path = os.path.join(output, video_out_name)

    # 载入视频
    in_container = av.open(video_path, 'r')
    in_video_stream = next(s for s in in_container.streams if s.type == 'video')
    in_audio_stream = next(s for s in in_container.streams if s.type == 'audio')

    fps = in_video_stream.base_rate  # 帧率
    width = in_video_stream.width  # 帧宽
    height = in_video_stream.height  # 帧高
    total_time_in_second = in_video_stream.duration * 1.0 * in_video_stream.time_base  # 视频总长
    total_frame = int(total_time_in_second * fps)  # 视频总帧数

    out_container = av.open(video_out_path, 'w')
    out_video_stream = out_container.add_stream("h264", rate=fps)
    out_audio_stream = out_container.add_stream(template=in_audio_stream)

    out_video_stream.width = width
    out_video_stream.height = height

    pbar = tqdm(total=total_frame, ncols=80)
    pbar.set_description(f"Making: {video_out_name}")

    for packet in in_container.demux(in_video_stream, in_audio_stream):

        _type = packet.stream.type

        for frame in packet.decode():
            if _type == 'video':
                frame = frame.to_ndarray(format="rgb24")  # 这里 frame 得到了 rgb 格式
                # https://www.zhihu.com/question/452884533 VideoCapture 读出来的图片默认是 BGR 格式,所以需要转
                # 但是这里 frame 可以指定格式,所以后面就不 cvtColor 了。

                frame = np.asarray(
                    np.expand_dims(process_image_alter(frame), 0))  # 修改原来的 process_image 函数,不用转换 cvtColor 了
                fake_img = session.run(None, {session.get_inputs()[0].name: frame})
                fake_img = post_precess(fake_img[0], (width, height))

                frame = av.VideoFrame.from_ndarray(fake_img, format="rgb24")  # 接收 rgb
                out_container.mux(out_video_stream.encode(frame))

                pbar.update(1)  # bar 跟随 video frame

            elif _type == 'audio':
                # We need to skip the "flushing" packets that `demux` generates.
                if packet.dts is None:
                    continue
                # We need to assign the packet to the new stream.
                packet.stream = out_audio_stream
                out_container.mux(packet)

    pbar.close()

    # Close the file
    out_container.close()

    return video_out_path

我在 onnx_video2anime_pyav.pyonnx_app_pyav.py 中使用了这种办法。

2.3 遇到的问题和解决办法

现象:一开始只处理 video 的时候简单套用了之前的代码,但是遇到了视频变蓝的问题。

原因:视频 RGB 和 BGR 格式错误。

参考:https://www.zhihu.com/question/452884533

解释:原来的代码中,OpenCV 的 VideoCapture.read 得到的是 BGR 格式的图片,所以在中间的处理过程中转成了 RGB 格式。

而现在 frame.to_ndarray 可以直接指定得到的图片格式,那就可以直接得到 RGB 格式:

frame = frame.to_ndarray(format="rgb24")  # 这里 frame 得到了 rgb 格式

于是后面 process_image 函数也不需要 cvtColor 来转换了。


现象:只得到音频,或者只得到视频。

最开始参考的是:https://github.com/PyAV-Org/PyAV/issues/302#issue-310229729

但是后来参考的才是对的(尽管他们很像):https://github.com/PyAV-Org/PyAV/discussions/866#discussion-3773956

原因:循环部分 packet.stream.type 的位置错误。


现象:视频音画不同步。

原因:没有为输出的视频指定和原视频一样的 fps。

补上 rate=fps

out_video_stream = out_container.add_stream("h264", rate=fps)

进度条 pbar 怎么处理:跟随 video 的 frame 的处理,在所有循环结束后关闭。

最后修改:2023 年 10 月 26 日
如果觉得我的文章对你有用,请随意赞赏