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]
