PR #25839 opened by nmschulte
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25839
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/25839.patch

> - `-mct` option for j2kenc; `0`: off (default), `1`: on, `-1`: auto
> - only supported for RGB-like pixel formats (by nature)
> - FATE round-trip tests for various MCT options

https://lists.ffmpeg.org/archives/list/[email protected]/thread/BNJKJAQ6RU6VQUGL434EGCELHDKCXWC2/


>From 15c0e84e761035fc5cebaead26a040cf61577c6a Mon Sep 17 00:00:00 2001
From: Nathan Schulte <[email protected]>
Date: Sun, 18 Oct 2026 11:01:17 -0500
Subject: [PATCH] avcodec/j2kenc: add support for MCT (RCT/ICT)

-mct option for j2kenc; 0: off (default), 1: on, -1: auto
only supported for RGB-like pixel formats (by nature)
FATE round-trip tests for various MCT options
---
 Changelog                                 |  2 +
 doc/encoders.texi                         | 19 ++++++
 libavcodec/j2kenc.c                       | 71 ++++++++++++++++++++++-
 tests/fate/jpeg2000.mak                   | 34 +++++++++++
 tests/ref/fate/jpeg2000enc-mct-auto       |  8 +++
 tests/ref/fate/jpeg2000enc-mct-off        |  8 +++
 tests/ref/fate/jpeg2000enc-mct-on-rgb-ict |  8 +++
 tests/ref/fate/jpeg2000enc-mct-on-rgb-rct |  8 +++
 8 files changed, 157 insertions(+), 1 deletion(-)
 create mode 100644 tests/ref/fate/jpeg2000enc-mct-auto
 create mode 100644 tests/ref/fate/jpeg2000enc-mct-off
 create mode 100644 tests/ref/fate/jpeg2000enc-mct-on-rgb-ict
 create mode 100644 tests/ref/fate/jpeg2000enc-mct-on-rgb-rct

diff --git a/Changelog b/Changelog
index 32e7c8bfe74..a9db12c3216 100644
--- a/Changelog
+++ b/Changelog
@@ -19,6 +19,8 @@ version <next>:
 - Panasonic RW2 decoder
 - xHE-AAC encoding via libfdk-xhe-aac
 - jpegapp bitstream filter
+- JPEG-2000 encoder MCT (RCT/ICT) support for RGB-like pixel formats with new
+  option -mct (default off)
 
 
 version 9.0:
diff --git a/doc/encoders.texi b/doc/encoders.texi
index 2c173ac916d..92626ab5070 100644
--- a/doc/encoders.texi
+++ b/doc/encoders.texi
@@ -1617,6 +1617,25 @@ Enable this to add SOP marker at the start of each 
packet. Disabled by default.
 @item eph @var{boolean}
 Enable this to add EPH marker at the end of each packet header. Disabled by 
default.
 
+@item mct @var{integer}
+Multiple Component Transformation.
+@table @option
+@item -1
+Automatically enable MCT for RGB-like pixel formats with at least three 
components.
+@item 0
+Disable MCT (default).
+@item 1
+Enable MCT. Applies the reversible colour transform (RCT) when using the
+5/3 DWT (@option{pred=dwt53}) or the irreversible colour transform (ICT)
+when using the 9/7 DWT. Only valid for RGB-like pixel formats with at least
+three components (and without chroma subsampling). Alpha (or any extra 
components)
+is left untouched.
+@end table
+
+When enabled, the encoder both applies the transform and signals it in the
+COD marker. Enabling MCT is recommended for RGB content as it usually
+improves compression.
+
 @item prog @var{integer}
 Sets the progression order to be used by the encoder.
 Possible values are:
diff --git a/libavcodec/j2kenc.c b/libavcodec/j2kenc.c
index da777580a4a..e08799ec96c 100644
--- a/libavcodec/j2kenc.c
+++ b/libavcodec/j2kenc.c
@@ -145,6 +145,7 @@ typedef struct {
     int prog;
     int nlayers;
     char *lr_str;
+    int mct; //< multiple component transform (RGB-like): -1: auto, 0: off 
(default), 1: on
 } Jpeg2000EncoderContext;
 
 
@@ -279,7 +280,7 @@ static int put_cod(Jpeg2000EncoderContext *s)
     // SGcod
     bytestream_put_byte(&s->buf, s->prog); // progression level
     bytestream_put_be16(&s->buf, s->nlayers); // num of layers
-    bytestream_put_byte(&s->buf, 0); // unspecified
+    bytestream_put_byte(&s->buf, codsty->mct); // multiple component 
transformation
     // SPcod
     bytestream_put_byte(&s->buf, codsty->nreslevels - 1); // num of decomp. 
levels
     bytestream_put_byte(&s->buf, codsty->log2_cblk_width-2); // cblk width
@@ -435,6 +436,51 @@ static int init_tiles(Jpeg2000EncoderContext *s)
     return 0;
 }
 
+static void mct_encode_rct(int *y, int *u, int *v, int n)
+{
+    int i;
+    for (i = 0; i < n; i++) {
+        int R = y[i], G = u[i], B = v[i];
+        y[i] = (R + (2 * G) + B) >> 2;
+        u[i] = B - G;
+        v[i] = R - G;
+    }
+}
+
+static void mct_encode_ict(int *y, int *u, int *v, int n)
+{
+    int i;
+    for (i = 0; i < n; i++) {
+        double R = y[i], G = u[i], B = v[i];
+        y[i] = lrint( 0.29900 * R + 0.58700 * G + 0.11400 * B);
+        u[i] = lrint(-0.16875 * R - 0.33126 * G + 0.50000 * B);
+        v[i] = lrint( 0.50000 * R - 0.41869 * G - 0.08131 * B);
+    }
+}
+
+static void apply_mct(Jpeg2000EncoderContext *s)
+{
+    int tileno;
+
+    if (!s->codsty.mct)
+        return;
+
+    for (tileno = 0; tileno < s->numXtiles * s->numYtiles; tileno++) {
+        Jpeg2000Tile *tile = s->tile + tileno;
+        Jpeg2000Component *c0 = tile->comp + 0;
+        Jpeg2000Component *c1 = tile->comp + 1;
+        Jpeg2000Component *c2 = tile->comp + 2;
+        int w = c0->coord[0][1] - c0->coord[0][0];
+        int h = c0->coord[1][1] - c0->coord[1][0];
+        int n = w * h;
+
+        if (s->codsty.transform == FF_DWT53)
+            mct_encode_rct(c0->i_data, c1->i_data, c2->i_data, n);
+        else
+            mct_encode_ict(c0->i_data, c1->i_data, c2->i_data, n);
+    }
+}
+
 #define COPY_FRAME(D, PIXEL)                                                   
                                             \
     static void copy_frame_ ##D(Jpeg2000EncoderContext *s)                     
                                             \
     {                                                                          
                                             \
@@ -1491,6 +1537,8 @@ static int encode_frame(AVCodecContext *avctx, AVPacket 
*pkt,
     else
         copy_frame_8(s);
 
+    apply_mct(s);
+
     reinit(s);
 
     if (s->format == CODEC_JP2) {
@@ -1722,6 +1770,26 @@ static av_cold int j2kenc_init(AVCodecContext *avctx)
         }
     }
 
+    s->codsty.mct = 0;
+    if (s->mct < 0) { // mct == -1; auto-on for RGB-like
+        s->codsty.mct = (desc->flags & AV_PIX_FMT_FLAG_RGB) && s->ncomponents 
>= 3;
+    } else if (s->mct == 1) {
+        if (s->ncomponents >= 3)
+            s->codsty.mct = 1;
+        else
+            av_log(avctx, AV_LOG_WARNING,
+                   "MCT requested but fewer than 3 components; disabling\n");
+    }
+    if (s->codsty.mct && (s->chroma_shift[0] || s->chroma_shift[1])) {
+        av_log(avctx, AV_LOG_ERROR, "MCT is not supported with chroma 
subsampling; disabling\n");
+        s->codsty.mct = 0;
+    }
+    if (s->codsty.mct && codsty->transform == FF_DWT53) {
+        // MCT expands chrominance range by 1 bit, and DWT can add more
+        // use extra guard bits to compensate
+        qntsty->nguardbits++;
+    }
+
     ff_thread_once(&init_static_once, init_luts);
 
     init_quantization(s);
@@ -1756,6 +1824,7 @@ static const AVOption options[] = {
     { "dwt53",         NULL,                0,                     
AV_OPT_TYPE_CONST, { .i64 = 1           }, INT_MIN, INT_MAX,       VE, .unit = 
"pred"        },
     { "sop",           "SOP marker",        OFFSET(sop),           
AV_OPT_TYPE_INT,   { .i64 = 0           }, 0,         1,           VE, },
     { "eph",           "EPH marker",        OFFSET(eph),           
AV_OPT_TYPE_INT,   { .i64 = 0           }, 0,         1,           VE, },
+    { "mct",           "Multiple component transformation", OFFSET(mct), 
AV_OPT_TYPE_INT, { .i64 = 0       }, -1,        1,           VE, .unit = "mct" 
},
     { "prog",          "Progression Order", OFFSET(prog),          
AV_OPT_TYPE_INT,   { .i64 = 0           }, JPEG2000_PGOD_LRCP,         
JPEG2000_PGOD_CPRL,           VE, .unit = "prog" },
     { "lrcp",          NULL,                0,                     
AV_OPT_TYPE_CONST,  { .i64 = JPEG2000_PGOD_LRCP }, 0,         0,           VE, 
.unit = "prog" },
     { "rlcp",          NULL,                0,                     
AV_OPT_TYPE_CONST,  { .i64 = JPEG2000_PGOD_RLCP }, 0,         0,           VE, 
.unit = "prog" },
diff --git a/tests/fate/jpeg2000.mak b/tests/fate/jpeg2000.mak
index a99b0c4e0cd..9fb7a356f27 100644
--- a/tests/fate/jpeg2000.mak
+++ b/tests/fate/jpeg2000.mak
@@ -67,3 +67,37 @@ FATE_JPEG2000DEC += $(FATE_JPEG2000DEC-yes)
 
 FATE_SAMPLES_FFMPEG-$(call FRAMECRC, IMAGE_J2K_PIPE, JPEG2000) += 
$(FATE_JPEG2000DEC)
 fate-jpeg2000dec: $(FATE_JPEG2000DEC)
+
+# JPEG 2000 encoder MCT tests.
+# RGB + irreversible 9/7 transform: lossy round trip
+FATE_JPEG2000ENC += fate-jpeg2000enc-mct-on-rgb-ict
+fate-jpeg2000enc-mct-on-rgb-ict: CMD = transcode \
+    "lavfi" "testsrc2=size=64x64:rate=1:duration=1,format=rgb24" \
+    rawvideo "-c:v jpeg2000 -mct 1 -pred dwt97int -frames:v 1" \
+    "-pix_fmt rgb24"
+
+# RGB + reversible 5/3 transform: lossless round trip
+FATE_JPEG2000ENC += fate-jpeg2000enc-mct-on-rgb-rct
+fate-jpeg2000enc-mct-on-rgb-rct: CMD = transcode \
+    "lavfi" "testsrc2=size=64x64:rate=1:duration=1,format=rgb24" \
+    rawvideo "-c:v jpeg2000 -mct 1 -pred dwt53 -frames:v 1" \
+    "-pix_fmt rgb24"
+
+# Explicitly disabled MCT must retain lossless round-trip behavior
+FATE_JPEG2000ENC += fate-jpeg2000enc-mct-off
+fate-jpeg2000enc-mct-off: CMD = transcode \
+    "lavfi" "testsrc2=size=64x64:rate=1:duration=1,format=rgb24" \
+    rawvideo "-c:v jpeg2000 -mct 0 -pred dwt53 -frames:v 1" \
+    "-pix_fmt rgb24"
+
+# Automatic MCT selection on RGB input; round trip
+FATE_JPEG2000ENC += fate-jpeg2000enc-mct-auto
+fate-jpeg2000enc-mct-auto: CMD = transcode \
+    "lavfi" "testsrc2=size=64x64:rate=1:duration=1,format=rgb24" \
+    rawvideo "-c:v jpeg2000 -mct -1 -pred dwt53 -frames:v 1" \
+    "-pix_fmt rgb24"
+
+FATE_JPEG2000ENC-$(call ALLYES, JPEG2000_ENCODER JPEG2000_DECODER  
RAWVIDEO_ENCODER RAWVIDEO_DECODER  LAVFI_INDEV) += $(FATE_JPEG2000ENC)
+
+FATE_SAMPLES_FFMPEG-$(call ALLYES, JPEG2000_ENCODER JPEG2000_DECODER 
RAWVIDEO_ENCODER RAWVIDEO_DECODER LAVFI_INDEV) += $(FATE_JPEG2000ENC-yes)
+fate-jpeg2000enc: $(FATE_JPEG2000ENC-yes)
diff --git a/tests/ref/fate/jpeg2000enc-mct-auto 
b/tests/ref/fate/jpeg2000enc-mct-auto
new file mode 100644
index 00000000000..d7601398b89
--- /dev/null
+++ b/tests/ref/fate/jpeg2000enc-mct-auto
@@ -0,0 +1,8 @@
+f831eb2c9fb1673962435a257332ced4 *tests/data/fate/jpeg2000enc-mct-auto.rawvideo
+6748 tests/data/fate/jpeg2000enc-mct-auto.rawvideo
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x64
+#sar 0: 0/1
+0,          0,          0,        1,    12288, 0xcd3a18af
diff --git a/tests/ref/fate/jpeg2000enc-mct-off 
b/tests/ref/fate/jpeg2000enc-mct-off
new file mode 100644
index 00000000000..392646eb9d6
--- /dev/null
+++ b/tests/ref/fate/jpeg2000enc-mct-off
@@ -0,0 +1,8 @@
+f7f378e60f4496b3f6854affe01af4fc *tests/data/fate/jpeg2000enc-mct-off.rawvideo
+6043 tests/data/fate/jpeg2000enc-mct-off.rawvideo
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x64
+#sar 0: 0/1
+0,          0,          0,        1,    12288, 0xcd3a18af
diff --git a/tests/ref/fate/jpeg2000enc-mct-on-rgb-ict 
b/tests/ref/fate/jpeg2000enc-mct-on-rgb-ict
new file mode 100644
index 00000000000..f55a1e01a21
--- /dev/null
+++ b/tests/ref/fate/jpeg2000enc-mct-on-rgb-ict
@@ -0,0 +1,8 @@
+ccf2a1580d61f29a8c36d941a06a2944 
*tests/data/fate/jpeg2000enc-mct-on-rgb-ict.rawvideo
+7499 tests/data/fate/jpeg2000enc-mct-on-rgb-ict.rawvideo
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x64
+#sar 0: 0/1
+0,          0,          0,        1,    12288, 0xe11a1b4d
diff --git a/tests/ref/fate/jpeg2000enc-mct-on-rgb-rct 
b/tests/ref/fate/jpeg2000enc-mct-on-rgb-rct
new file mode 100644
index 00000000000..6576b592b7e
--- /dev/null
+++ b/tests/ref/fate/jpeg2000enc-mct-on-rgb-rct
@@ -0,0 +1,8 @@
+f831eb2c9fb1673962435a257332ced4 
*tests/data/fate/jpeg2000enc-mct-on-rgb-rct.rawvideo
+6748 tests/data/fate/jpeg2000enc-mct-on-rgb-rct.rawvideo
+#tb 0: 1/25
+#media_type 0: video
+#codec_id 0: rawvideo
+#dimensions 0: 64x64
+#sar 0: 0/1
+0,          0,          0,        1,    12288, 0xcd3a18af
-- 
2.54.0

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

Reply via email to