如何使用cv2.VideoWriter输出经过x265压缩的视频

13

我正在对一段45分钟1.2GB的视频进行渲染,该视频有800,000个大小为1344x756的帧,并且视频格式为mp4。我试图输出一个使用x265压缩的视频,但问题是当我使用cv2.VideoWriter时,10分钟视频的输出大小超过了2GB,这不是我的意图,所以我在Mac OSX和ubuntu 18上尝试了以下方法:

codec = cv2.VideoWriter_fourcc(*'HEVC')
out = cv2.VideoWriter('output.mp4', 'HEVC', fps, (width, height))

我得到的只是运行时警告:

OpenCV: FFMPEG: tag 0x43564548/'HEVC' is not found (format 'mp4 / MP4 (MPEG-4 Part 14)')'

我想要实现的并不是最高质量的输出,而是尽可能达到良好的质量并且文件大小最小化。


不要使用ffmpeg作为子进程,并通过管道传输视频数据,而应该使用PyAV。它是ffmpeg的适当封装器。将numpy矩阵放入视频文件的示例:https://github.com/PyAV-Org/PyAV/blob/main/examples/numpy/generate_video.py - Christoph Rackwitz
1个回答

22
据我所知,OpenCV VideoWriter目前尚不支持HEVC编码。
我建议您使用FFmpeg作为子进程,并将渲染的帧管道传输到ffmpegstdin输入流中。
您可以使用像ffmpeg-python这样的Python绑定,或者使用Python subprocess来执行ffmpeg
cv2.VideoWriter相比,使用ffmpeg可以更好地控制视频编码参数(cv2.VideoWriter设计了简单易用的灵活性)。
下面是一段示例代码,它渲染50个帧并将帧流传输到ffmpeg,以HEVC视频编解码器编码MP4视频文件:
import cv2
import numpy as np
import subprocess as sp
import shlex

width, height, n_frames, fps = 1344, 756, 50, 25  # 50 frames, resolution 1344x756, and 25 fps

output_filename = 'output.mp4'

# Open ffmpeg application as sub-process
# FFmpeg input PIPE: RAW images in BGR color format
# FFmpeg output MP4 file encoded with HEVC codec.
# Arguments list:
# -y                   Overwrite output file without asking
# -s {width}x{height}  Input resolution width x height (1344x756)
# -pixel_format bgr24  Input frame color format is BGR with 8 bits per color component
# -f rawvideo          Input format: raw video
# -r {fps}             Frame rate: fps (25fps)
# -i pipe:             ffmpeg input is a PIPE
# -vcodec libx265      Video codec: H.265 (HEVC)
# -pix_fmt yuv420p     Output video color space YUV420 (saving space compared to YUV444)
# -crf 24              Constant quality encoding (lower value for higher quality and larger output file).
# {output_filename}    Output file name: output_filename (output.mp4)
process = sp.Popen(shlex.split(f'ffmpeg -y -s {width}x{height} -pixel_format bgr24 -f rawvideo -r {fps} -i pipe: -vcodec libx265 -pix_fmt yuv420p -crf 24 {output_filename}'), stdin=sp.PIPE)

# Build synthetic video frames and write them to ffmpeg input stream.
for i in range(n_frames):
    # Build synthetic image for testing ("render" a video frame).
    img = np.full((height, width, 3), 60, np.uint8)
    cv2.putText(img, str(i+1), (width//2-100*len(str(i+1)), height//2+100), cv2.FONT_HERSHEY_DUPLEX, 10, (255, 30, 30), 20)  # Blue number

    # Write raw video frame to input stream of ffmpeg sub-process.
    process.stdin.write(img.tobytes())

# Close and flush stdin
process.stdin.close()

# Wait for sub-process to finish
process.wait()

# Terminate the sub-process
process.terminate()  # Note: We don't have to terminate the sub-process (after process.wait(), the sub-process is supposed to be closed).

注:

  • ffmpeg executable must be in the execution path of the Python script.

  • For Linux, in case ffmpeg is not in the execution path, you may use the full path:

     process = sp.Popen(shlex.split(f'/usr/bin/ffmpeg -y -s {width}x{height} -pixel_format bgr24 -f rawvideo -r {fps} -i pipe: -vcodec libx265 -pix_fmt yuv420p -crf 24 {output_filename}'), stdin=sp.PIPE)
    

    (Assuming ffmpeg executable is in /usr/bin/).

  • Python 3's f-Strings syntax requires Python version 3.6 or above.


C++示例:

在Python中,有多个FFmpeg绑定可以实现H.265视频编码。
在C++中,可选项要少得多...

我们可以使用类似的解决方案来使用C++(使用FFmpeg子进程)。
为了执行FFmpeg子进程并打开stdin管道,我们可以在Windows中使用_popen,在Linux中使用popen

注意:

  • 我注意到_popen不如CreateProcess可靠,并且我们需要等待(例如在结尾处等待一秒钟)输出文件关闭。
    我不确定在Linux中是否存在类似的问题与popen

C++代码示例:

#include <stdio.h>
#include <chrono>
#include <thread>
#include "opencv2/opencv.hpp"
#include <string>

int main()
{
    // 50 frames, resolution 1344x756, and 25 fps
    int width = 1344;
    int height = 756;
    int n_frames = 50;
    int fps = 25;

    const std::string output_filename = "output.mp4"; //Example for file name with spaces: "\"output with spaces.mp4\""

    //Open ffmpeg application as sub - process
    //FFmpeg input PIPE : RAW images in BGR color format
    //FFmpeg output MP4 file encoded with HEVC codec (using libx265 encoder).
    std::string ffmpeg_cmd = std::string("ffmpeg -y -f rawvideo -r ") + std::to_string(fps) +
        " -video_size " + std::to_string(width) + "x" + std::to_string(height) +
        " -pixel_format bgr24 -i pipe: -vcodec libx265 -crf 24 -pix_fmt yuv420p " + output_filename;

    //Execute FFmpeg as sub-process, open stdin pipe (of FFmpeg sub-process) for writing.
    //In Windows we need to use _popen and in Linux popen
#ifdef _MSC_VER
    FILE* pipeout = _popen(ffmpeg_cmd.c_str(), "wb");   //Windows (ffmpeg.exe must be in the execution path)
#else
    //https://batchloaf.wordpress.com/2017/02/12/a-simple-way-to-read-and-write-audio-and-video-files-in-c-using-ffmpeg-part-2-video/
    FILE* pipeout = popen(ffmpeg_cmd.c_str(), "w");     //Linux (assume ffmpeg exist in /usr/bin/ffmpeg (and in path).
#endif

    for (int i = 0; i < n_frames; i++)
    {
        //Build synthetic image for testing ("render" a video frame):
        cv::Mat frame = cv::Mat(height, width, CV_8UC3);
        frame = cv::Scalar(60, 60, 60); //Fill background with dark gray
        cv::putText(frame, std::to_string(i+1), cv::Point(width/2 - 100*(int)(std::to_string(i+1).length()), height/2+100), cv::FONT_HERSHEY_DUPLEX, 10, cv::Scalar(255, 30, 30), 20);  // Draw a blue number
        //cv::imshow("frame", frame); cv::waitKey(1); //Show the frame for testing

        //Write width*height*3 bytes to stdin pipe of FFmpeg sub-process (assume frame data is continuous in the RAM).
        fwrite(frame.data, 1, (size_t)width*height*3, pipeout);
    }

    //Flush and close input and output pipes
    fflush(pipeout);

#ifdef _MSC_VER
    _pclose(pipeout);   //Windows
#else
    pclose(pipeout);    //Linux
#endif

    //It looks like we need to wait one more second at the end. //https://dev59.com/-2855IYBdhLWcg3wuGwM#62804585
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // sleep for 1 second

    return 0;
}

1
.split()会在文件路径包含空格的情况下将其拆分,如果像这样引用'... -crf 24 "{output_filename}"',因此我更喜欢shlex解决方案,但是在许多情况下.split()也足够。 - GlabbichRulz
非常感谢!我想我会采用你的方法作为默认方式来编写视频!<3 - Rakshit Kothari
你可能会遇到X265 encodes 0 frames的情况。这很可能是由于ffmpeg启动的线程没有被安全关闭造成的。在视频写出后添加time.sleep(2)有助于解决此问题。 - Rakshit Kothari
@RakshitKothari 当我开始使用上述方法时,我认为我们可能需要添加一些等待时间。今天我知道process.stdin.close()会将缓冲区刷新到文件中,并关闭文件。process.wait()语句实际上是在FFmpeg进程已经终止(所有线程都已安全关闭)之后执行的。根本不需要process.terminate()。你按原样执行上面的代码,是否得到了0帧? - Rotem
@rotem 没错,我就是。基本上我有一个循环,它遍历视频列表并使用您的方法进行编辑和保存。我所做的唯一修改是在 process.stdin.close() 之后调用了 del process。删除后,我只是 time.sleep(2)。这为我解决了问题。 - Rakshit Kothari
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接