Skip to content

fix #26276: cv::VideoWriter fails writing colorless images #27153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: 4.x
Choose a base branch
from

Conversation

03kiko
Copy link

@03kiko 03kiko commented Mar 25, 2025

When writing grayscale images (isColor=false), cv::VideoWriter failed as FFmpeg backend requires input frames to be in a grayscale format. Unlike other backends, FFmpeg doesn't perform this conversion internally and expects the input to be pre-converted. To fix this, I inserted a check in the CvVideoWriter_FFMPEG_proxy::write to convert input frames to grayscale when the input has more than 1 channel. If this is true, then the input is converted to a gray image using cv::cvtColor (with cv::COLOR_BGR2GRAY).
Additionally, as suggested in the issue comments, I have correctly propagated the return value of CvVideoWriter_FFMPEG::writeFrame back to the CvVideoWriter_FFMPEG_proxy::write. This return value wasn't being used, and writeFrame was always returning false since the FFmeg's return code AVERROR(EAGAIN) was mistakenly being treated as an error. Now it's handled as a signal for additional input (current input was successfully written, and a new frame should be sent. See FFmpeg documentation for avcodec_receive_packet). A warning is displayed if CvVideoWriter_FFMPEG::writeFrame returns false. Alternatively, this could be propagated back up to the user, making cv::VideoWriter::write a boolean.
Finally, I added a test case to verify if the grayscale conversion is being done correctly.

Fixes #26276

Pull Request Readiness Checklist

See details at https://github.com/opencv/opencv/wiki/How_to_contribute#making-a-good-pull-request

  • I agree to contribute to the project under Apache 2 License.
  • To the best of my knowledge, the proposed patch is not based on a code under GPL or another license that is incompatible with OpenCV
  • The PR is proposed to the proper branch
  • There is a reference to the original bug report and related work
  • There is accuracy test, performance test and test data in opencv_extra repository, if applicable
    Patch to opencv_extra has the same branch name.
  • The feature is well documented and sample code can be built with the project CMake

@03kiko 03kiko force-pushed the fix-videowriter-writing-colorless-images-26276 branch 4 times, most recently from 2f81a81 to aea7f0f Compare March 26, 2025 20:14
The internal return values of the FFmpeg library are now correctly
handled in the FFmpeg backend. The writeFrame function now considers
that the FFmpeg return code AVERROR(EAGAIN) indicates the encoder needs
more input, so a new frame must be sent instead of treating it as a
real error. Additionally, the return value of
CvVideoWriter_FFMPEG::writeFrame is now properly propagated back to the
write function in the cap_ffmpeg.cpp file, which displays a warning
when writeFrame returns false. Additionally, a conversion of the input
to grayscale was implemented before entering FFmpeg backend, as the
encoder expects grayscale frames when isColor=false. This requires
converting from CV_8UC3 to grayscale (a simple color conversion is done
here using the cvtColor function). Finally, a new test case has been
implemented to verify that the grayscale conversion is correctly
applied.
@03kiko 03kiko force-pushed the fix-videowriter-writing-colorless-images-26276 branch from aea7f0f to 30a5ad1 Compare March 26, 2025 23:49
@asmorkalov
Copy link
Contributor

@mshabunin @opencv-alalek Could you take a look?

@asmorkalov asmorkalov added this to the 4.12.0 milestone Mar 31, 2025
@mshabunin
Copy link
Contributor

If I remember correctly there might be issues with building FFmpeg plugin on Windows because of dependency on OpenCV.

@asmorkalov
Copy link
Contributor

Looks like VideoWriter behaviour is not consistent here:

@03kiko
Copy link
Author

03kiko commented Apr 1, 2025

@asmorkalov @mshabunin @opencv-alalek

Thank you for the feedback. I agree that consistency across backends is important, and I think the best approach is to convert data/input to the desired format, as done in MS media foundation and Android.

Key changes:

FFmpeg:

  • Instead of what I previously suggested in my commit, we should perform the needed conversions in writeFrame() in cap_ffmpeg_impl.hpp when checking parameters. (See conversion logic below).
  • Also, since the only supported formats are CV_8UC1/CV_8UC3/CV_16UC1, we should add a verification for that in write() at cap_ffmpeg.cpp and do a warning in case of non-supported formats.
  • In writeFrame(), instead of receiving only the number of channels (cn), we receive the input depth/type to make the conversions easier.

GStreamer:

  • Implement similar conversion logic.

What the conversions would look like in both backends:

cv::Mat mat(height, width, type, (void*)data, step);
if (input_pix_fmt == AV_PIX_FMT_BGR24) {
        if (type != CV_8UC3) {
            if (type == CV_16UC1) // CV_16UC1 -> CV_8UC1
            	mat.convertTo(mat, CV_8UC1, 1.0 / 256); // scale down 16-bit to 8-bit
            cv::cvtColor(mat, mat, COLOR_GRAY2BGR); // CV_8UC1 -> CV_8UC3
            data = mat.data;
            step = (int)mat.step;
            type = CV_8UC3;
        }
}
else if (input_pix_fmt == AV_PIX_FMT_GRAY8) {
	if (type != CV_8UC1) {
            if (type == CV_8UC3) // CV_8UC3 -> CV_8UC1
            	cv::cvtColor(mat, mat, COLOR_BGR2GRAY);
            else // CV_16UC1 -> CV_8UC1
            	mat.convertTo(mat, CV_8UC1, 1.0 / 256); // scale down 16-bit to 8-bit
     	    data = mat.data;
            step = (int)mat.step;
            type = CV_8UC1;
	}
}
else if (input_pix_fmt == AV_PIX_FMT_GRAY16LE) {
	if (type != CV_16UC1) {
    	    if (type == CV_8UC3)
    	    	cv::cvtColor(mat, mat, COLOR_BGR2GRAY);  // CV_8UC3 -> CV_8UC1
    	    mat.convertTo(mat, CV_16UC1);  // CV_8UC1 -> CV_16UC1
    	    data = mat.data;
            step = (int)mat.step;
            type = CV_16UC1;
	}
}
else {
	CV_LOG_WARNING(NULL, "Input data does not match selected pixel format: "
        	<< av_get_pix_fmt_name(input_pix_fmt)
                << ", depth: " << type);
        CV_Assert(false);
}

Test Cases to Add:

  • isColor=false with CV_8UC3 (already done in my PR)
  • isColor=true with CV_8UC1
  • isColor=true with CV_16UC1
  • ...

Does this approach seem reasonable? I'm ready to implement these changes.

@03kiko
Copy link
Author

03kiko commented Apr 10, 2025

Hi @asmorkalov, is there anything I should address or clarify to help move this PR forward?

In my last message, I suggested aligning the behavior of the FFmpeg and GStreamer backends with that of MS Media Foundation and Android — handling input conversion internally.

Specifically, the idea is to perform the necessary conversions inside the writeFrame() function for FFmpeg (and similarly for GStreamer). Since only three input types are supported (CV_8UC1, CV_8UC3, and CV_16UC1), these conversions should be straightforward.

I also plan to add test cases to cover these scenarios and make sure the conversions behave as expected.

Please let me know if anything is unclear or if you’d prefer a different approach. I’m ready to make any needed adjustments and move forward with the implementation.

03kiko added 3 commits May 1, 2025 21:32
Aligns FFmpeg backend behavior with MS Media Foundation and Android,
by moving the internal conversion logic to writeFrame().

This now handles all relevant format conversions:
- CV_8UC3 -> CV_8UC1 or CV_16UC1
- CV_8UC1 -> CV_8UC3 or CV_16UC1
- CV_16UC1 -> CV_8UC1 or CV_8UC3

It still corrects AVERROR(EAGAIN) handling, which was previously
misinterpreting it as an error and displays a warning when
writeFrame returns false. Adds more test cases ensuring all
possible conversions are properly tested.
@03kiko
Copy link
Author

03kiko commented May 2, 2025

Hi @asmorkalov @mshabunin @opencv-alalek,

Since I didn’t hear back, I decided to move forward and implement the changes I previously proposed. The update improves consistency between the FFmpeg backend and others like MS Media Foundation and Android.

Specifically:

  • The conversion logic has been moved into CvVideoWriter_FFMPEG::writeFrame().
  • All relevant format conversion cases are now handled.
  • Test cases were added to cover all supported conversion scenarios.
  • The fix for AVERROR(EAGAIN) handling remains, along with a warning when writeFrame returns false.

Please let me know if you’d like any changes or have further feedback.

@03kiko
Copy link
Author

03kiko commented May 9, 2025

@asmorkalov @opencv-alalek @mshabunin friendly reminder

Copy link
Contributor

@opencv-alalek opencv-alalek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent ignorance of the error is a design bug.

But implicit data manipulation is an evil too.
There are no safe operations, except may be COLOR_GRAY2BGR.
So required conversions should be left on the user side.

.write() should fail properly instead if input is not expected.

{
if (!encode_video) {
CV_Assert(cn == 1 && ((width > 0 && height == 1) || (width == 1 && height > 0 && step == 1)));
CV_Assert((type == CV_16UC1 || type == CV_8UC1) && ((width > 0 && height == 1) || (width == 1 && height > 0 && step == 1)));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case of encode_video = False we should accept only raw bytes arrays.
So we should have CV_8UC1 data type only.
Expected layout is 1xN or Nx1.

This change must be reverted.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out! The original logic only checked if cn == 1, so I mistakenly assumed both CV_8UC1 and CV_16UC1 were acceptable. I’ll revert this logic in the next commit.

convertedMat = inputMat;
else {
if (type == CV_16UC1) // CV_16UC1 -> CV_8UC1
inputMat.convertTo(inputMat, CV_8UC1, 1.0 / 256);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inputMat ... inputMat

"In-place" ops should not be used in general (causes extra .clone() call).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the heads-up, I’ll avoid in-place operations moving forward.

if (type == CV_16UC1) // CV_16UC1 -> CV_8UC1
inputMat.convertTo(inputMat, CV_8UC1, 1.0 / 256);
cv::cvtColor(inputMat, convertedMat, cv::COLOR_GRAY2BGR); // CV_8UC1 -> CV_8UC3
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is about other cases?

16UC3?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I had focused only on the explicitly supported types (CV_8UC1, CV_16UC1, CV_8UC3) and overlooked the possibility of unsupported inputs like CV_16UC3. I’ll address this as well.

@03kiko
Copy link
Author

03kiko commented May 17, 2025

Silent ignorance of the error is a design bug.

But implicit data manipulation is an evil too. There are no safe operations, except may be COLOR_GRAY2BGR. So required conversions should be left on the user side.

.write() should fail properly instead if input is not expected.

Given that there are no safe operations, the best approach is indeed to leave format handling to the user. Since .write() is a void function, changing its return type would require a broader change (though it could be beneficial). Instead, this will be addressed through clearer warnings:

  • One when writeFrame() fails to write the frame (already in previous commits)
  • Another explicitly reports a mismatch between expected and received input types, similar to the GStreamer backend. (Will add this in the next commit)
    This avoids silent failures and makes the frame skipping behaviour more transparent to users.

03kiko added 4 commits May 18, 2025 01:02
Fixes a minor typo in the warning message for type mismatches in the
GStreamer backend. ("... expected CV_16UC3" -> "... expected CV_16UC1")
This change is unrelated to the FFmpeg logic but improves consistency
across backends.
Replaces implicit data manipulation with a warning-based approach.
Following reviewer feedback, data conversion is now explicitly
left to the user, and no automatic transformations are performed
in writeFrame().
Since no frames should be written, simply checking if video capture
fails to open is enough. Also reduced the frames to be written from
90 to 30
Test cases are skipped when writer fails to open. Trailing whitespaces
were also removed
@03kiko
Copy link
Author

03kiko commented May 18, 2025

Hi @opencv-alalek @mshabunin,

I've pushed a new version addressing @opencv-alalek’s feedback. All required conversions should now be handled by the user. I kept the type argument in the writeFrame() function to allow for more specific warnings. I’ve also added three test cases to validate that VideoWriter fails in case of mismatched types and that appropriate warnings are logged.

However, on Win64, one test case fails: when the input is CV_8UC1 and the expected depth is CV_16UC1. Despite the mismatch, the writer still successfully writes the video, and the file can be read. I suspect this is because, in this particular case, encode_video = false, which skips the input/expected types check and directly encapsulates the frame data, or some other implicit conversion is happening, etc.

I would appreciate your input, as the other two test cases behave as expected by failing due to the mismatch. Is this platform-specific behaviour expected, or am I missing something?

Thanks!

Edit:
The CI failed on the Linux/Ubuntu 22.04 job in two videoio_ffmpeg_16bit.basic test cases. However, I tested this locally and all tests passed. Because I can't reproduce the failure on my machine, it's difficult to diagnose or fix the issue.

My System Information:
OS: Pop!_OS 22.04 LTS (Jammy)
OpenCV version: 4.12.0 (local build)
Compiler: GCC 15.1.0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

cv::VideoWriter fails writing colorless images
4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy