13

I'm doing some rendering on a 45-min 1.2GB video 80,0000 frames of size 1344x756 each and the video is in the mp4 format, I'm trying to output a video with x265 compression the problem is when I'm using cv2.VideoWriter the output size for 10 minutes of the video exceeds 2GB which is not what I'm intending to end up with so I tried the following on mac osx and ubuntu 18:

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

All I'm getting is runtime warnings:

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

What I'm trying to achieve is not necessarily the highest quality output but should be a good quality and the lowest size possible.

1

1 Answer 1

23

As far as I know, OpenCV VideoWriter has no support for HEVC encoding (yet).

I recommend you using FFmpeg as sub-process, and PIPE the rendered frames to stdin input stream of ffmpeg.

You may use Python binding for ffmpeg like ffmpeg-python, or execute ffmpeg using Python subprocess.

Using ffmpeg, you have much more control over video encoding parameters compared to cv2.VideoWriter (cv2.VideoWriter is designed for simplicity on expanse of flexibility).

Here is a sample code that renders 50 frames, stream frames to ffmpeg that encodes MP4 video file with HEVC video codec:

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).

Notes:

  • 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++ example:

In Python there are multiple FFmpeg bindings that allows H.265 video encoding.
In C++, there are far less options...

We may apply similar solution with C++ (using FFmpeg sub-process).
For executing FFmpeg sub-process and opening stdin pipe, we may use _popen in Windows and popen in Linux.

Note:

  • I noticed that _popen is not as reliable as CreateProcess, and we need to wait (say one second at the end) for the output file to get closed.
    I am not sure if there is a similar issue with popen in Linux.

C++ Code sample:

#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://stackoverflow.com/a/62804585/4926757
    std::this_thread::sleep_for(std::chrono::milliseconds(1000)); // sleep for 1 second

    return 0;
}
8
  • Great Code! For anyone getting "file not found: ffmpeg...", try using import shlex and shlex.split(...) around the command string, this will split the string at spaces while preserving quoted parts of the strings like the filepath, also writing /usr/bin/ffmpeg instead of ffmpeg helps against the execution path issue. Commented Jul 1, 2020 at 15:07
  • Thank you, I updated the post for Linux support. Instead of shlex.split(...), you may also use '...'.split()
    – Rotem
    Commented Jul 1, 2020 at 16:38
  • 1
    .split() will break file paths that contain spaces if they are quoted like this '... -crf 24 "{output_filename}"', that's why i would prefer the shlex solution, but you are right that in many cases .split() will suffice. Commented Jul 1, 2020 at 20:03
  • Thank you so much! I think I am going to resort to your approach as default to write out videos! <3 Commented Feb 17, 2022 at 14:03
  • There is a chance you may get X265 encodes 0 frames. This is most likely because the threads initiated by ffmpeg aren't safely closed. It helps to add time.sleep(2) post video write out. Commented Feb 17, 2022 at 18:28