Why opencv videowriter is so slow? - c++

Hi stackoverflow community,
I have a tricky problem and I need your help to understand what is going on here.
My program captures frames from a video grabber card (Blackmagic) which just works fine so far, at the same time I display the captured images with opencv (cv::imshow) which works good as well (But pretty cpu wasting).
The captured images are supposed to be stored on the disk as well, for this I put the captured Frames (cv::Mat) on a stack, to finally write them async with opencv:
cv::VideoWriter videoWriter(path, cv::CAP_FFMPEG, fourcc, fps, *size);
videoWriter.set(cv::VIDEOWRITER_PROP_QUALITY, 100);
int id = metaDataWriter.insertNow(path);
while (this->isRunning) {
while (!this->stackFrames.empty()) {
cv:Mat m = this->stackFrames.pop();
videoWriter << m;
}
}
videoWriter.release();
This code is running in an additional thread and will be stopped from outside.
The code is working so far, but it is sometimes pretty slow, which means my stack size increases and my system runs out of ram and get killed by the OS.
Currently it is running on my developing system:
Ubuntu 18.04.05
OpenCV 4.4.0 compiled with Cuda
Intel i7 10. generation 32GB RAM, GPU Nvidia p620, M.2 SSD
Depending on the codec (fourcc) this produces a high CPU load. So far I used mainly "MJPG", "x264". Sometimes even MJPG turns one core of the CPU to 100% load, and my stack raises until the programs run out of run. After a restart, sometimes, this problem is fixed, and it seems the load is distributed over all cores.
Regarding to the Intel Doc. for my CPU, it has integrated hardware encoding/decoding for several codecs. But I guess opencv is not using them. Opencv even uses its own ffmpeg and not the one of my system. Here is my build command of opencv:
cmake -D CMAKE_BUILD_TYPE=RELEASE \
-D CMAKE_INSTALL_PREFIX=/usr/local \
-D WITH_TBB=ON \
-D WITH_CUDA=ON \
-D BUILD_opencv_cudacodec=OFF \
-D ENABLE_FAST_MATH=1 \
-D CUDA_FAST_MATH=1 \
-D WITH_CUBLAS=1 \
-D WITH_V4L=ON \
-D WITH_QT=OFF \
-D WITH_OPENGL=ON \
-D WITH_GSTREAMER=ON \
-D OPENCV_GENERATE_PKGCONFIG=ON \
-D OPENCV_ENABLE_NONFREE=ON \
-D WITH_FFMPEG=1 \
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \
-D WITH_CUDNN=ON \
-D OPENCV_DNN_CUDA=ON \
-D CUDA_ARCH_BIN=6.1 ..
I just started development with linux and C++, before I was working with Java/Maven, so the use of cmake is still a work in progress, pls go easy on me.
Basically my question is, how can I make the video encoding/writing faster, use the hardware acceleration at best?
Or if you think there is something else fishy, pls let me know.
BR Michael

-------- old - look up answer on bottom --------
Thank #Micka for the many advises, I found the right thing on the way.
Using cudacodec::VideoWriter is not that easy, after compiling I was not able to use it because of this error, and even if I can make it run, the deployment PC does not have a nvidia GPU.
Since I am going to use PCs with AMD CPUs as well, I can't use the cv::CAP_INTEL_MFX for the api-reference parameter of the cv::VideoWriter.
But there is also the cv::CAP_OPENCV_MJPEG, which works fine for the MJPG codec (not all video container are supported, I use .avi, sadly .mkv was not working with this configuration). If the user does not use MJPG as a codec I use cv::CAP_ANY, and opencv decides what is to use.
So,
cv::VideoWriter videoWriter(path, cv::CAP_OPENCV_MJPEG, fourcc, fps, *size);
works pretty well, even on my old system.
Unfortunately I never changed the api-reference parameter before, only from ffmpeg to gstreamer, I read in the doc of opencv only the last line "cv::CAP_FFMPEG or cv::CAP_GSTREAMER." and I did not see that there is an "e.g." before...
Thank you #Micka to make me read again.
P.S. for my performance problem with cv::imshow I changed from
cv::namedWindow(WINDOW_NAME, cv::WINDOW_NORMAL);
to
cv::namedWindow(WINDOW_NAME, cv::WINDOW_OPENGL);
Which obviously uses OpenGL, and does a better job. Also changing from cv::Mat to cv::UMat can speed up the performance, see here
-------------- EDIT better solution ----------------
Since I still had problems with the OpenCV VideoWriter for some systems, I was looking for another solution. Now I write the frames with FFMPEG.
For FFMPEG I can use the GPU or CPU depending on the codec I use.
If FFMPEG is installed via snapd (Ubuntu 18.04) it comes with cuda enabled by default:
sudo snap install ffmpeg --devmode
(--devmode is optional, but I had problems writing files on specific location, this was the only way for me to fix it)
And here is my code:
//this string is automatically created in my program, depending on user input and the parameters of the input frames
string ffmpegCommand = "ffmpeg -y -f rawvideo -vcodec rawvideo -framerate 50 -pix_fmt bgr24 -s 1920x1080 -i - -c:v h264_nvenc -crf 14 -maxrate:v 10M -r 50 myVideoFile.mkv";
FILE *pipeout = popen(ffmpegCommand.data(), "w");
int id = metaDataWriter.insertNow(path);
//loop will be stopped from another thread
while (this->isRunning) {
//this->frames is a stack with cv::Mat elements in the right order
//it is filled by another thread
while (!this->frames.empty()) {
cv::Mat mat = frames.front();
frames.pop();
fwrite(mat.data, 1, s, pipeout);
}
}
fflush(pipeout);
pclose(pipeout);
So a file (pipeout) is used to write the mat.data to ffmpeg, ffmpeg itself is doing the encoding and file writing. To the parameters:
-y = Overwrite output files without asking
-f = format, in this case used for input rawvideo
-vcodec = codec for input which is rawvideo as well, because the used cv::Mat.data has no compression/codec
-framerate = the input framerate I receive from my grabber card/OpenCv
-pix_fmt = the format of my raw data, in this case bgr24, so 8 bit each channel, because I use a regular OpenCV bgr cv::Mat
-s = size of each frame, in my case 1920x1080
-i = input, in this case we read from the stdinput you can see it here "-", so the file (pipeout) is captured by ffmpeg
-c:v = output codec, so this is to encode the video, here h264_nvenc is used, which is a GPU codec
-r = frame output rate, also 50 in this case myVideoFile.mkv = this is just the name of the file which is produced by ffmpeg, you can change this file and path
Additional parameters for higher quality: -crf 14 -maxrate:v 10M
This works very good for me and uses my hardware acceleration of the GPU or with another codec in charge the CPU.
I hope this helps other developers as well.

Related

cImg save_video ignores fps

I am using cImg to generate images in a CImgList.
when I save that stack to video by calling the save_video method on the stack, it ignores the fps and the output seems to be always 25fps. I opened the file in different players (VLC, windows movie,...) and its always 25fps.
cImg is using ffmpeg to create the video. I'm not specifying any codec so i assume the default mpeg2 is used (based on what VLC tells me).
I also have no specfic settings for cImg.
I have a fixed amount of images of 500 and it always produces around 20 seconds which is 25fps.
What do I need to do to output it to for example 60fps?
I fixed it but not sure if it's a bug in cImg or just me not knowing how to use it properly but from what I found is that cImg doesn't pass the --framerate parameter to ffmpeg, only the -r parameter.
I updated the code to include the framerate parameter and it does seem to work. This is the updated cImg code in the save_ffmpeg_external function:
cimg_snprintf(command,command._width,
"\"%s\" -framerate %u -v -8 -y -i \"%s_%%6d.ppm\" -pix_fmt yuv420p -vcodec %s -b %uk -r %u \"%s\"",
cimg::ffmpeg_path(),
fps,
CImg<charT>::string(filename_tmp)._system_strescape().data(),
_codec,bitrate,fps,
CImg<charT>::string(filename)._system_strescape().data());

Facebook Graph API - Getting error 2207026 when trying to upload video

I try to upload a video via the graph API but as the title states, I get an error that states that I use an unsupported video format.
I thought maybe the original video file doesn't match the video specifications as defined on https://developers.facebook.com/docs/instagram-api/reference/ig-user/media#video-specifications so I wrote a function to convert the file to match the specification, using ffmpeg with the following command (I tried many different ones, but this is the last one I tried)
ffmpeg -i ${tmpInFile} -vf format=yuv420p -vf scale=-2:720 -pix_fmt yuv420p -r 30 -movflags +faststart -c:v libx264 -b:v 3M -c:a aac -b:a 128k -ac 2 -ar 44100 -shortest -f mp4 ${tmpOutFile}
Unfortunately, the error persists.
Here's the complete process:
First I use await fetch('/api/convert-mp4', { method: 'POST', body: file }); to send the uploaded video file to the backend.
Next I get the blob data from the request with const blob = await request.blob();.
Then I create a temporary file with await fs.writeFile(tmpInFile, await blob.stream()).
Then I call ffmpeg with the above command mentioned above and then read the file with const buffer = await fs.readFile(tmpOutFile);.
Then I send the buffer as response body back to the client with return {status: 200,body: buffer}.
Then I get the blob data from the response with const blob = await convertVideoResponse.blob();.
Finally I convert it back into a file object with
const convertedFile = new File([blob], file.name, { type: 'video/mp4' });
This file I upload to Supabase Storage (https://supabase.com/storage), and get a publicly accessible url (which I confirmed by opening it in an incognito tab).
In the supabase dashboard I can see the video file has the correct media container (video/mp4) and the file size is small enough.
Does anyone have an idea what could be the issue?
Edit:
By changing the ffmpeg command to use h265 instead of h254 ffmpeg -i ${tmpInFile} -vf format=yuv420p -vf scale=-2:1350 -pix_fmt yuv420p -r 30 -movflags +faststart -c:v libx265 -vtag hvc1 -an -x265-params crf=25 -b:v 3M -c:a copy -c:a aac -b:a 128k -ac 2 -ar 44100 -shortest -f mp4 ${tmpOutFile} I managed to get it to work for some videos but not all, which confuses me, as I assumed that the video attributes should be the same for all videos processed by the same command.
I had the very same issue, and GOT IT TO WORK!
TL;DR
Make sure your video is not too long
More details
In the end I came up with this as an example:
ffmpeg -i input.mp4 \
-c:v libx264 -aspect 16:9 -crf 18 \
-vf "scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih),pad=1280:720:(1280-iw)/2:(720-ih)/2" \
-fpsmax 60 -preset ultrafast -c:a aac -b:a 128k -ac 1 -pix_fmt yuv420p -movflags +faststart -t 59 -y output.mp4
On some flags some details about what and why:
-c:v libx264 → You need to compress with Video codec: HEVC or H264, progressive scan, closed GOP, 4:2:0 chroma subsampling
--aspect and -vf "scale=iw*min(1280/iw\,720/ih):ih*min(1280/iw\,720/ih),pad=1280:720:(1280-iw)/2:(720-ih)/2" → As I want my video's always to be within 1280x720, padding added if needed.
fpsmax → Make sure you do not exceed the framerate of 60fps
-c:a aac -b:a 128k -ac 1 → For "AAC, 48khz sample rate maximum, 1 or 2 channels (mono or stereo)"
-t 59 → Limit to under 1 minute, as that is the max for a video!
I think the most important thing here is the -t 59 as the API only supports files up to one minute!
After this, everything worked 🎉

video capturing with V4l2 has dropped/lost frames

I have a scientific application which captures a video4Linux video stream. It's crucial that we capture each frame and no one gets lost. Unfortunately frames are missing here and there and I don't know why.
To detect dropped frames I compare the v4l2_buffer's sequence number with my own counter directly after reading a frame:
void detectDroppedFrame(v4l2_buffer* buffer) {
_frameCounter++;
auto isLastFrame = buffer->sequence == 0 && _frameCounter > 1;
if (!isLastFrame && _frameCounter != buffer->sequence+1)
{
std::cout << "\n####### WARNING! Missing frame detected!" << std::endl;
_frameCounter = buffer->sequence+1; // re-sync our counter with correct frame number from driver.
}
}
My running 1-file example gist can be found at github (based on official V4L2 capture example): https://gist.github.com/SebastianMartens/7d63f8300a0bcf0c7072a674b3ea4817
Tested with webcam on Ubuntu 18.04 virtual machine on notebook-hardware (uvcvideo driver) as well as with CSI camera on our embedded hardware running ubuntu 18.04 natively. Frames are not processed and buffers seems to be grabbed fast enough (buffer status checked with VIDIOC_QUERYBUF which shows that all buffers are in the driver's incoming queue and V4L2_BUF_FLAG_DONE flag is not set). I have lost frames with MMAP as well as with UserPtr method. Also it seems to be independent of pixelformat, image size and framerate!
To me it looks like if the camera/v4l2 driver is not able to fill available buffers fast enough but also increasing the file descriptor priority with VIDIOC_S_PRIORITY command does not help (still likely to be a thread scheduling problem?).
=> What are possible reasons why V4L2 does not forward frames (does not put them into it's outgoing queue)?
=> Is my method to detect lost frames correct? Are there other options or tools for that?
I had a similar problem when using the bttv driver. All attempts to capture at full resolution would result in dropped frames (usually around 10% of frames, often in bursts). Capturing at half resolution worked fine.
The solution that I found, though far from ideal, was to apply a load to the linux scheduler. Here is an example, using the "tvtime" program to do the capture:
#! /bin/bash
# apply a load in the background
while true; do /bin/true; done &> /dev/null &
# do the video capture
/usr/bin/tvtime "$#"
# kill the loop running in the background
pkill -P $$
This creates a loop that repeatedly runs /bin/true in the background. Almost any executable will do (I used /bin/date at first). The loop will create a heavy load, but with a multi-core system there is still plenty of headroom for other tasks.
This obviously isn't an ideal solution, but for what it's worth, it allows me to capture full-resolution video with no dropped frames. I have little desire to poke around in the driver/kernel code to find a better solution.
FYI, here are the details of my system:
OS: Ubuntu 20.04, kernel 5.4.0-42
MB: Gigabyte AB350M-D3H
CPU: AMD Ryzen 5 2400G
GPU: AMD Raven Ridge
Driver name : bttv
Card type : BT878 video (Hauppauge (bt878))
Bus info : PCI:0000:06:00.0
Driver version : 5.4.44
Capabilities : 0x85250015

FFmpeg: real-time buffer[ ] [input] too full or near too full (101% of size: 3041280 [rtbufsize parameter]) frame dropped

I use the library of ffmpeg to decode stream from [TTQ HD Camera] and encode it to a rtmp stream.
but I receive a lot of warnings like the picture below.
i try to set qmin and qmax , it seems a little better. but still not totally resolve the problem.
encoder_context->qmin = 10;
encoder_context->qmax = 51;
who knows this is why ?
[dshow # 04bfc640] real-time buffer [TTQ HD Camera] [video input] too full or near too full (101% of size: 3041280 [rtbufsize parameter])! frame dropped!
Have you tried increasing the -rtbufsize parameter to something larger than 3041280? If you have the RAM for it, try something like 2000M. It should be defined before the -i of the camera.
So something like:
ffmpeg -f dshow -video_size 1920x1080 -rtbufsize 2147.48M -framerate 30 -pixel_format bgr0 -i video=...
Note that the resolution and frame rate are just examples and you would have to fill in your values that you have used in the ffmpeg command already.

How to make .avi, .mp4 file with jpeg frames?

I'm working with IP camera, and I have got Jpeg frames and audio data (PCM) from camera.
Now, I want to create video file (both audio and video) under .avi or .mp4 format from above data.
I searched and I knew that ffmpeg library can do it. But I don't know how to using ffmpeg to do this.
Can you suggest me some sample code or the function of ffmpeg to do it?
If your objective is to write a c++ app to do this for you please disregard this answer, I'll just leave it here for future reference. If not, here's how you can do it in bash:
First, make sure your images are in a nice format, easy to handle by ffmpeg. You can copy the images to a different directory:
mkdir tmp
x=1; for i in *jpg; do counter=$(printf %03d $x); cp "$i" tmp/img"$counter".jpg; x=$(($x+1)); done
Copy your audio data to the tmp directory and encode the video. Let's say your camera took a picture every ten seconds:
cd tmp
ffmpeg -i audio.wav -f image2 -i img%03d.jpg -vcodec msmpeg4v2 -r 0.1 -intra out.avi
Where -r 0.1 indicates a framerate of 0.1 which is one frame every 10 seconds.
The possible issues here are:
Your audio/video might go slightly out of sync unless you calculate your desired framerate carefully in advance. You should be able to get the length of the audio (or video) using ffmpeg and some grep magic. Even so the sync might be an issue with longer clips.
if you have more than 999 images the %03d format will not be enough, make sure to change the 3 to the desired length of the index
The video will inherit its length from the longer of the streams, you can restrict it using the -t switch:
-t duration - Restrict the transcoded/captured video sequence to the duration specified in seconds. "hh:mm:ss[.xxx]" syntax is also supported.