Allow RGB24/BGR24/RGB0/BGR0/RGBA/BGRA input. Convert to
yuvj444p/yuvj422p/yuvj420p with JPEG or BT.709 full-range matrix.

Co-authored-by: Cursor <[email protected]>
Signed-off-by: Reinhard Nißl <[email protected]>
---
 Changelog             |   1 +
 doc/encoders.texi     |  17 ++++
 libavcodec/mjpegenc.c | 214 +++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 230 insertions(+), 2 deletions(-)

diff --git a/Changelog b/Changelog
index ed98fe8580..0c6341e570 100644
--- a/Changelog
+++ b/Changelog
@@ -16,6 +16,7 @@ version <next>:
 - DSD (dsd_msbf) encoder
 
 - jpegapp bitstream filter
+- MJPEG encoder packed RGB input
 
 version 9.0:
 - Extend AMF Color Converter (vf_vpp_amf) HDR capabilities
diff --git a/doc/encoders.texi b/doc/encoders.texi
index a3ce15fcff..b908dd8bd8 100644
--- a/doc/encoders.texi
+++ b/doc/encoders.texi
@@ -1266,8 +1266,25 @@ Use the default huffman tables. This is the default 
strategy.
 Compute and use optimal huffman tables.
 
 @end table
+
+@item quality
+JPEG quality @code{1}--@code{100}. @code{0} (default) keeps @option{-q:v}.
+
+@item matrix
+RGB to YCbCr matrix when the encoder receives packed RGB
+(@code{rgb24}/@code{rgb0}/@code{bgr0}/...). Default @samp{jpeg} is BT.601
+full range (ITU T.871). @samp{bt709} selects BT.709 full range.
+
+@item chroma
+Chroma subsampling for RGB input: @samp{444} (default), @samp{422}, or
+@samp{420}. YUV input still follows @option{-pix_fmt}.
+
 @end table
 
+@example
+-c:v mjpeg -quality 95 -matrix jpeg -chroma 444
+@end example
+
 @anchor{wavpackenc}
 @section wavpack
 
diff --git a/libavcodec/mjpegenc.c b/libavcodec/mjpegenc.c
index ee70e17bcd..970840243b 100644
--- a/libavcodec/mjpegenc.c
+++ b/libavcodec/mjpegenc.c
@@ -32,7 +32,11 @@
 
 #include "config_components.h"
 
+#include "libavutil/common.h"
+#include "libavutil/frame.h"
+#include "libavutil/imgutils.h"
 #include "libavutil/mem.h"
+#include "libavutil/pixdesc.h"
 
 #include "avcodec.h"
 #include "codec_internal.h"
@@ -63,9 +67,25 @@ typedef struct MJpegHuffmanCode {
  * Note that when using slice threading only the main thread's
  * MPVEncContext is followed by a MjpegContext; the other threads
  * can access this shared context via MPVEncContext.mjpeg. */
+enum MJPEGColorMatrix {
+    MJPEG_MATRIX_JPEG = 0, /* ITU T.871 / BT.601 full range */
+    MJPEG_MATRIX_BT709,
+};
+
+enum MJPEGChroma {
+    MJPEG_CHROMA_444 = 0,
+    MJPEG_CHROMA_422,
+    MJPEG_CHROMA_420,
+};
+
 typedef struct MJPEGEncContext {
     MPVMainEncContext mpeg;
     MJpegContext   mjpeg;
+    int quality;
+    int color_matrix;
+    int chroma;
+    enum AVPixelFormat src_pix_fmt;
+    AVFrame *yuv_frame;
 } MJPEGEncContext;
 
 static av_cold void init_uni_ac_vlc(const uint8_t huff_size_ac[256],
@@ -303,9 +323,124 @@ static int alloc_huffman(MJPEGEncContext *const m2)
     return 0;
 }
 
+static int mjpeg_pix_fmt_is_rgb(enum AVPixelFormat fmt)
+{
+    switch (fmt) {
+    case AV_PIX_FMT_RGB24:
+    case AV_PIX_FMT_BGR24:
+    case AV_PIX_FMT_RGB0:
+    case AV_PIX_FMT_BGR0:
+    case AV_PIX_FMT_RGBA:
+    case AV_PIX_FMT_BGRA:
+        return 1;
+    default:
+        return 0;
+    }
+}
+
+static enum AVPixelFormat mjpeg_yuv_pix_fmt(int chroma)
+{
+    switch (chroma) {
+    case MJPEG_CHROMA_420:
+        return AV_PIX_FMT_YUVJ420P;
+    case MJPEG_CHROMA_422:
+        return AV_PIX_FMT_YUVJ422P;
+    default:
+        return AV_PIX_FMT_YUVJ444P;
+    }
+}
+
+static void mjpeg_unpack_rgb(enum AVPixelFormat fmt, const uint8_t *p,
+                             int *r, int *g, int *b)
+{
+    switch (fmt) {
+    case AV_PIX_FMT_BGR24:
+    case AV_PIX_FMT_BGR0:
+    case AV_PIX_FMT_BGRA:
+        *b = p[0];
+        *g = p[1];
+        *r = p[2];
+        break;
+    default:
+        *r = p[0];
+        *g = p[1];
+        *b = p[2];
+        break;
+    }
+}
+
+static void mjpeg_rgb_to_ycbcr(int matrix, int r, int g, int b,
+                               int *y, int *cb, int *cr)
+{
+    if (matrix == MJPEG_MATRIX_BT709) {
+        *y  = ( 54 * r + 183 * g +  18 * b + 128) >> 8;
+        *cb = ((-30 * r -  98 * g + 128 * b + 128) >> 8) + 128;
+        *cr = ((128 * r - 116 * g -  12 * b + 128) >> 8) + 128;
+    } else {
+        /* JPEG / BT.601 full range (ITU T.871) */
+        *y  = ( 77 * r + 150 * g +  29 * b + 128) >> 8;
+        *cb = ((-43 * r -  85 * g + 128 * b + 128) >> 8) + 128;
+        *cr = ((128 * r - 107 * g -  21 * b + 128) >> 8) + 128;
+    }
+    *y  = av_clip_uint8(*y);
+    *cb = av_clip_uint8(*cb);
+    *cr = av_clip_uint8(*cr);
+}
+
+static int mjpeg_convert_rgb_frame(AVCodecContext *avctx, AVFrame *dst,
+                                   const AVFrame *src)
+{
+    MJPEGEncContext *const m2 = avctx->priv_data;
+    const int width  = src->width;
+    const int height = src->height;
+    const int step   = (src->format == AV_PIX_FMT_RGB24 ||
+                        src->format == AV_PIX_FMT_BGR24) ? 3 : 4;
+    const int chroma = m2->chroma;
+    int x, y;
+
+    for (y = 0; y < height; y++) {
+        const uint8_t *sp = src->data[0] + y * src->linesize[0];
+        uint8_t *yp = dst->data[0] + y * dst->linesize[0];
+        uint8_t *up, *vp;
+        int uv_y = (chroma == MJPEG_CHROMA_420) ? (y >> 1) : y;
+
+        up = dst->data[1] + uv_y * dst->linesize[1];
+        vp = dst->data[2] + uv_y * dst->linesize[2];
+
+        for (x = 0; x < width; x++) {
+            int r, g, b, Y, Cb, Cr;
+            mjpeg_unpack_rgb(src->format, sp + x * step, &r, &g, &b);
+            mjpeg_rgb_to_ycbcr(m2->color_matrix, r, g, b, &Y, &Cb, &Cr);
+            yp[x] = Y;
+
+            if (chroma == MJPEG_CHROMA_444) {
+                up[x] = Cb;
+                vp[x] = Cr;
+            } else if (chroma == MJPEG_CHROMA_422) {
+                if (!(x & 1)) {
+                    up[x >> 1] = Cb;
+                    vp[x >> 1] = Cr;
+                } else {
+                    up[x >> 1] = (up[x >> 1] + Cb + 1) >> 1;
+                    vp[x >> 1] = (vp[x >> 1] + Cr + 1) >> 1;
+                }
+            } else if (!((x | y) & 1)) {
+                up[x >> 1] = Cb;
+                vp[x >> 1] = Cr;
+            } else if (!(y & 1)) {
+                up[x >> 1] = (up[x >> 1] + Cb + 1) >> 1;
+                vp[x >> 1] = (vp[x >> 1] + Cr + 1) >> 1;
+            }
+        }
+    }
+
+    return 0;
+}
+
 static av_cold int mjpeg_encode_close(AVCodecContext *avctx)
 {
     MJPEGEncContext *const mjpeg = avctx->priv_data;
+    av_frame_free(&mjpeg->yuv_frame);
     av_freep(&mjpeg->mjpeg.huff_buffer);
     ff_mpv_encode_end(avctx);
     return 0;
@@ -539,6 +674,29 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     m2->mpeg.encode_picture_header = mjpeg_amv_encode_picture_header;
     // May be overridden below
     s->encode_mb                   = mjpeg_encode_mb;
+    m2->src_pix_fmt                = avctx->pix_fmt;
+
+    if (m2->quality > 0) {
+        int q = m2->quality >= 100 ? 1
+                                   : av_clip((100 - m2->quality + 2) / 3, 1, 
31);
+        avctx->flags |= AV_CODEC_FLAG_QSCALE;
+        avctx->global_quality = q * FF_QP2LAMBDA;
+    }
+
+    if (avctx->color_range == AVCOL_RANGE_UNSPECIFIED)
+        avctx->color_range = AVCOL_RANGE_JPEG;
+
+    if (mjpeg_pix_fmt_is_rgb(avctx->pix_fmt)) {
+        avctx->pix_fmt = mjpeg_yuv_pix_fmt(m2->chroma);
+        if (avctx->colorspace == AVCOL_SPC_UNSPECIFIED)
+            avctx->colorspace = m2->color_matrix == MJPEG_MATRIX_BT709
+                                ? AVCOL_SPC_BT709 : AVCOL_SPC_BT470BG;
+        av_log(avctx, AV_LOG_VERBOSE,
+               "mjpeg RGB %s -> %s matrix=%s\n",
+               av_get_pix_fmt_name(m2->src_pix_fmt),
+               av_get_pix_fmt_name(avctx->pix_fmt),
+               m2->color_matrix == MJPEG_MATRIX_BT709 ? "bt709" : "jpeg");
+    }
 
     if (s->mpv_flags & FF_MPV_FLAG_QP_RD) {
         // Used to produce garbage with MJPEG.
@@ -593,6 +751,20 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     if (ret < 0)
         return ret;
 
+    if (mjpeg_pix_fmt_is_rgb(m2->src_pix_fmt)) {
+        m2->yuv_frame = av_frame_alloc();
+        if (!m2->yuv_frame)
+            return AVERROR(ENOMEM);
+        m2->yuv_frame->format = avctx->pix_fmt;
+        m2->yuv_frame->width  = avctx->width;
+        m2->yuv_frame->height = avctx->height;
+        m2->yuv_frame->color_range = AVCOL_RANGE_JPEG;
+        m2->yuv_frame->colorspace  = avctx->colorspace;
+        ret = av_frame_get_buffer(m2->yuv_frame, 32);
+        if (ret < 0)
+            return ret;
+    }
+
     // Buffers start out empty.
     m->huff_ncode = 0;
 
@@ -608,6 +780,31 @@ static av_cold int mjpeg_encode_init(AVCodecContext *avctx)
     return 0;
 }
 
+#if CONFIG_MJPEG_ENCODER
+static int mjpeg_encode_frame(AVCodecContext *avctx, AVPacket *pkt,
+                              const AVFrame *frame, int *got_packet)
+{
+    MJPEGEncContext *const m2 = avctx->priv_data;
+    const AVFrame *src = frame;
+    int ret;
+
+    if (frame && m2->yuv_frame) {
+        ret = av_frame_copy_props(m2->yuv_frame, frame);
+        if (ret < 0)
+            return ret;
+        m2->yuv_frame->color_range = AVCOL_RANGE_JPEG;
+        m2->yuv_frame->colorspace  = m2->color_matrix == MJPEG_MATRIX_BT709
+                                     ? AVCOL_SPC_BT709 : AVCOL_SPC_BT470BG;
+        ret = mjpeg_convert_rgb_frame(avctx, m2->yuv_frame, frame);
+        if (ret < 0)
+            return ret;
+        src = m2->yuv_frame;
+    }
+
+    return ff_mpv_encode_picture(avctx, pkt, src, got_packet);
+}
+#endif
+
 #if CONFIG_AMV_ENCODER
 // maximum over s->mjpeg_vsample[i]
 #define V_MAX 2
@@ -644,6 +841,7 @@ static int amv_encode_picture(AVCodecContext *avctx, 
AVPacket *pkt,
 #endif
 
 #define OFFSET(x) offsetof(MJPEGEncContext, mjpeg.x)
+#define OFFSET_PRIV(x) offsetof(MJPEGEncContext, x)
 #define VE AV_OPT_FLAG_VIDEO_PARAM | AV_OPT_FLAG_ENCODING_PARAM
 static const AVOption options[] = {
 #define AMV_OPTIONS_OFFSET 4
@@ -651,6 +849,15 @@ static const AVOption options[] = {
     { "default", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_DEFAULT }, 
INT_MIN, INT_MAX, VE, .unit = "huffman" },
     { "optimal", NULL, 0, AV_OPT_TYPE_CONST, { .i64 = HUFFMAN_TABLE_OPTIMAL }, 
INT_MIN, INT_MAX, VE, .unit = "huffman" },
 { "force_duplicated_matrix", "Always write luma and chroma matrix for mjpeg, 
useful for rtp streaming.", OFFSET(force_duplicated_matrix), AV_OPT_TYPE_BOOL, 
{.i64 = 0 }, 0, 1, VE },
+{ "quality", "JPEG quality 1-100 (0 = use -q:v). 95 ≈ -q:v 2", 
OFFSET_PRIV(quality), AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 100, VE },
+{ "matrix", "RGB→YCbCr matrix (RGB input only)", OFFSET_PRIV(color_matrix), 
AV_OPT_TYPE_INT, { .i64 = MJPEG_MATRIX_JPEG }, 0, MJPEG_MATRIX_BT709, VE, .unit 
= "matrix" },
+    { "jpeg",  "BT.601 full range (ITU T.871)", 0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_MATRIX_JPEG },  0, 0, VE, .unit = "matrix" },
+    { "bt601", "Same as jpeg",                   0, AV_OPT_TYPE_CONST, { .i64 
= MJPEG_MATRIX_JPEG },  0, 0, VE, .unit = "matrix" },
+    { "bt709", "BT.709 full range",              0, AV_OPT_TYPE_CONST, { .i64 
= MJPEG_MATRIX_BT709 }, 0, 0, VE, .unit = "matrix" },
+{ "chroma", "Chroma subsampling for RGB input", OFFSET_PRIV(chroma), 
AV_OPT_TYPE_INT, { .i64 = MJPEG_CHROMA_444 }, 0, MJPEG_CHROMA_420, VE, .unit = 
"chroma" },
+    { "444", "4:4:4 (no chroma subsample)", 0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_444 }, 0, 0, VE, .unit = "chroma" },
+    { "422", "4:2:2",                       0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_422 }, 0, 0, VE, .unit = "chroma" },
+    { "420", "4:2:0",                       0, AV_OPT_TYPE_CONST, { .i64 = 
MJPEG_CHROMA_420 }, 0, 0, VE, .unit = "chroma" },
 FF_MPV_COMMON_OPTS
 { NULL},
 };
@@ -690,14 +897,17 @@ const FFCodec ff_mjpeg_encoder = {
     .p.id           = AV_CODEC_ID_MJPEG,
     .priv_data_size = sizeof(MJPEGEncContext),
     .init           = mjpeg_encode_init,
-    FF_CODEC_ENCODE_CB(ff_mpv_encode_picture),
+    FF_CODEC_ENCODE_CB(mjpeg_encode_frame),
     .close          = mjpeg_encode_close,
     .p.capabilities = AV_CODEC_CAP_DR1 |
                       AV_CODEC_CAP_SLICE_THREADS | AV_CODEC_CAP_FRAME_THREADS |
                       AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE,
     .caps_internal  = FF_CODEC_CAP_INIT_CLEANUP | FF_CODEC_CAP_ICC_PROFILES,
     CODEC_PIXFMTS(AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, 
AV_PIX_FMT_YUVJ444P,
-                  AV_PIX_FMT_YUV420P,  AV_PIX_FMT_YUV422P,  
AV_PIX_FMT_YUV444P),
+                  AV_PIX_FMT_YUV420P,  AV_PIX_FMT_YUV422P,  AV_PIX_FMT_YUV444P,
+                  AV_PIX_FMT_RGB24, AV_PIX_FMT_BGR24,
+                  AV_PIX_FMT_RGB0,  AV_PIX_FMT_BGR0,
+                  AV_PIX_FMT_RGBA,  AV_PIX_FMT_BGRA),
     .p.priv_class   = &mjpeg_class,
     .p.profiles     = NULL_IF_CONFIG_SMALL(ff_mjpeg_profiles),
     .get_supported_config = mjpeg_get_supported_config,
-- 
2.43.0

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]

Reply via email to