From 28026fe2cf3f668448f02bffa0f8c9829847582b Mon Sep 17 00:00:00 2001 From: Lazizbek Ergashev Date: Wed, 12 Aug 2026 15:07:57 +0500 Subject: [PATCH] videoio: fix FFmpeg VideoCapture ignoring mid-stream pixel format change (cherry picked from commit f3c9241611cfb46dd17de2b3e3f25f6fe36b71f8) --- modules/videoio/src/cap_ffmpeg_impl.hpp | 8 +++- modules/videoio/test/test_ffmpeg.cpp | 62 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/modules/videoio/src/cap_ffmpeg_impl.hpp b/modules/videoio/src/cap_ffmpeg_impl.hpp index fc4479e07c..6a8543999a 100644 --- a/modules/videoio/src/cap_ffmpeg_impl.hpp +++ b/modules/videoio/src/cap_ffmpeg_impl.hpp @@ -582,6 +582,7 @@ struct CvCapture_FFMPEG AVPacket packet; Image_FFMPEG frame; struct SwsContext *img_convert_ctx; + AVPixelFormat img_convert_ctx_format; int64_t frame_number, first_frame_number; @@ -648,6 +649,7 @@ void CvCapture_FFMPEG::init() memset(&packet, 0, sizeof(packet)); av_init_packet(&packet); img_convert_ctx = 0; + img_convert_ctx_format = AV_PIX_FMT_NONE; avcodec = 0; context = 0; @@ -689,6 +691,7 @@ void CvCapture_FFMPEG::close() { sws_freeContext(img_convert_ctx); img_convert_ctx = 0; + img_convert_ctx_format = AV_PIX_FMT_NONE; } if( picture ) @@ -1922,7 +1925,8 @@ bool CvCapture_FFMPEG::retrieveFrame(int flag, unsigned char** data, int* step, if( img_convert_ctx == NULL || frame.width != video_st->CV_FFMPEG_CODEC_FIELD->width || frame.height != video_st->CV_FFMPEG_CODEC_FIELD->height || - frame.data == NULL ) + frame.data == NULL || + (AVPixelFormat)sw_picture->format != img_convert_ctx_format ) { #if LIBSWSCALE_BUILD >= CALC_FFMPEG_VERSION(6, 4, 100) int buffer_width = video_st->CV_FFMPEG_CODEC_FIELD->width; @@ -2000,6 +2004,8 @@ bool CvCapture_FFMPEG::retrieveFrame(int flag, unsigned char** data, int* step, return false; } + img_convert_ctx_format = (AVPixelFormat)sw_picture->format; + #if USE_AV_FRAME_GET_BUFFER av_frame_unref(&rgb_picture); rgb_picture.format = result_format; diff --git a/modules/videoio/test/test_ffmpeg.cpp b/modules/videoio/test/test_ffmpeg.cpp index a6ae33a6b3..e42901fe2d 100644 --- a/modules/videoio/test/test_ffmpeg.cpp +++ b/modules/videoio/test/test_ffmpeg.cpp @@ -3,6 +3,7 @@ // of this distribution and at http://opencv.org/license.html. #include "test_precomp.hpp" +#include "opencv2/imgcodecs.hpp" using namespace std; @@ -1188,4 +1189,65 @@ TEST(videoio_ffmpeg, seek_with_negative_dts) } } +// MJPEG stream whose chroma subsampling changes mid-stream at constant +// frame size must not reuse a conversion context built for the previous frame. +// related issue: https://github.com/opencv/opencv/issues/29699 +TEST(videoio_ffmpeg, mjpeg_pixel_format_change) +{ + if (!videoio_registry::hasBackend(CAP_FFMPEG)) + throw SkipTestException("FFmpeg backend was not found"); + + Mat frame0(64, 64, CV_8UC3, Scalar(0, 0, 255)); + Mat frame1(64, 64, CV_8UC3); + for (int y = 0; y < frame1.rows; y++) + for (int x = 0; x < frame1.cols; x++) + frame1.at(y, x) = Vec3b((uchar)(x * 4), (uchar)(y * 4), (uchar)((x + y) * 2)); + + vector buf0, buf1; + ASSERT_TRUE(imencode(".jpg", frame0, buf0, + {IMWRITE_JPEG_SAMPLING_FACTOR, IMWRITE_JPEG_SAMPLING_FACTOR_422, IMWRITE_JPEG_QUALITY, 100})); + ASSERT_TRUE(imencode(".jpg", frame1, buf1, + {IMWRITE_JPEG_SAMPLING_FACTOR, IMWRITE_JPEG_SAMPLING_FACTOR_420, IMWRITE_JPEG_QUALITY, 100})); + + const string filename = tempfile("test_pixfmt_change.mjpeg"); + std::ofstream file(filename.c_str(), ios::out | ios::trunc | std::ios::binary); + file.write(reinterpret_cast(buf0.data()), buf0.size()); + file.write(reinterpret_cast(buf1.data()), buf1.size()); + file.close(); + + VideoCapture cap(filename, CAP_FFMPEG); + ASSERT_TRUE(cap.isOpened()); + + Mat read0, read1; + ASSERT_TRUE(cap.read(read0)); + ASSERT_TRUE(cap.read(read1)); + + Mat decodedRef0 = imdecode(buf0, IMREAD_COLOR); + Mat decodedRef1 = imdecode(buf1, IMREAD_COLOR); + ASSERT_FALSE(decodedRef0.empty()); + ASSERT_FALSE(decodedRef1.empty()); + ASSERT_EQ(read0.size(), decodedRef0.size()); + ASSERT_EQ(read1.size(), decodedRef1.size()); + + // JPEG re-encoding is not bit exact, but a correctly decoded frame stays + // within a couple of intensity levels per channel; a stale conversion + // context (the bug) produces gross corruption of tens of levels. + const double maeThreshold = 5.0; + + Mat diff0, diff1; + absdiff(read0, decodedRef0, diff0); + absdiff(read1, decodedRef1, diff1); + Scalar mae0 = mean(diff0); + Scalar mae1 = mean(diff1); + for (int c = 0; c < 3; c++) + { + EXPECT_LE(mae0[c], maeThreshold) + << "First frame (channel " << c << ") decoded incorrectly"; + EXPECT_LE(mae1[c], maeThreshold) + << "Second frame (channel " << c << ") decoded incorrectly after pixel format change mid-stream"; + } + + remove(filename.c_str()); +} + }} // namespace