Support alpha blending 64-bit RGB onto things.
authorCarl Hetherington <cth@carlh.net>
Thu, 28 Sep 2023 20:18:34 +0000 (22:18 +0200)
committerCarl Hetherington <cth@carlh.net>
Fri, 29 Sep 2023 19:22:54 +0000 (21:22 +0200)
run/tests
src/lib/image.cc
test/image_test.cc

index 42d3634fbba35a80171af31604b129ce51321646..f45beb78dff72342b3f04e458986cc013669aa8a 100755 (executable)
--- a/run/tests
+++ b/run/tests
@@ -3,7 +3,7 @@
 # e.g. --run_tests=foo
 set -e
 
-PRIVATE_GIT="a057b086149254d1ea70a4fd7efcd88791335dcd"
+PRIVATE_GIT="26097a7868332dec4175328cb9a9a793fe6d287f"
 
 if [ "$1" == "--check" ]; then
        shift 1
index 02be984a889707cf12fbbf8e83f01e27c8e07d86..578f6ca567d2bfb73b541ebf2260891a7b53fe68 100644 (file)
@@ -619,7 +619,7 @@ Image::make_black ()
 void
 Image::make_transparent ()
 {
-       if (_pixel_format != AV_PIX_FMT_BGRA && _pixel_format != AV_PIX_FMT_RGBA) {
+       if (_pixel_format != AV_PIX_FMT_BGRA && _pixel_format != AV_PIX_FMT_RGBA && _pixel_format != AV_PIX_FMT_RGBA64BE) {
                throw PixelFormatError ("make_transparent()", _pixel_format);
        }
 
@@ -654,112 +654,120 @@ struct OtherParams
        uint8_t* line_pointer(int y) const {
                return data[0] + y * stride[0];
        }
+
+       float alpha_divisor() const {
+               return pow(2, bpp * 2) - 1;
+       }
 };
 
 
-static
+template <class OtherType>
 void
-alpha_blend_onto_rgb24(TargetParams const& target, OtherParams const& other, int red, int blue)
+alpha_blend_onto_rgb24(TargetParams const& target, OtherParams const& other, int red, int blue, std::function<float (OtherType*)> get, int value_divisor)
 {
        /* Going onto RGB24.  First byte is red, second green, third blue */
+       auto const alpha_divisor = other.alpha_divisor();
        for (int ty = target.start_y, oy = other.start_y; ty < target.size.height && oy < other.size.height; ++ty, ++oy) {
-               uint8_t* tp = target.line_pointer(ty);
-               uint8_t* op = other.line_pointer(oy);
+               auto tp = target.line_pointer(ty);
+               auto op = reinterpret_cast<OtherType*>(other.line_pointer(oy));
                for (int tx = target.start_x, ox = other.start_x; tx < target.size.width && ox < other.size.width; ++tx, ++ox) {
-                       float const alpha = float (op[3]) / 255;
-                       tp[0] = op[red] * alpha + tp[0] * (1 - alpha);
-                       tp[1] = op[1] * alpha + tp[1] * (1 - alpha);
-                       tp[2] = op[blue] * alpha + tp[2] * (1 - alpha);
+                       float const alpha = get(op + 3) / alpha_divisor;
+                       tp[0] = (get(op + red) / value_divisor) * alpha + tp[0] * (1 - alpha);
+                       tp[1] = (get(op + 1) / value_divisor) * alpha + tp[1] * (1 - alpha);
+                       tp[2] = (get(op + blue) / value_divisor) * alpha + tp[2] * (1 - alpha);
 
                        tp += target.bpp;
-                       op += other.bpp;
+                       op += other.bpp / sizeof(OtherType);
                }
        }
 }
 
 
-static
+template <class OtherType>
 void
-alpha_blend_onto_bgra(TargetParams const& target, OtherParams const& other, int red, int blue)
+alpha_blend_onto_bgra(TargetParams const& target, OtherParams const& other, int red, int blue, std::function<float (OtherType*)> get, int value_divisor)
 {
+       auto const alpha_divisor = other.alpha_divisor();
        for (int ty = target.start_y, oy = other.start_y; ty < target.size.height && oy < other.size.height; ++ty, ++oy) {
-               uint8_t* tp = target.line_pointer(ty);
-               uint8_t* op = other.line_pointer(oy);
+               auto tp = target.line_pointer(ty);
+               auto op = reinterpret_cast<OtherType*>(other.line_pointer(oy));
                for (int tx = target.start_x, ox = other.start_x; tx < target.size.width && ox < other.size.width; ++tx, ++ox) {
-                       float const alpha = float (op[3]) / 255;
-                       tp[0] = op[blue] * alpha + tp[0] * (1 - alpha);
-                       tp[1] = op[1] * alpha + tp[1] * (1 - alpha);
-                       tp[2] = op[red] * alpha + tp[2] * (1 - alpha);
-                       tp[3] = op[3] * alpha + tp[3] * (1 - alpha);
+                       float const alpha = get(op + 3) / alpha_divisor;
+                       tp[0] = (get(op + blue) / value_divisor) * alpha + tp[0] * (1 - alpha);
+                       tp[1] = (get(op + 1) / value_divisor) * alpha + tp[1] * (1 - alpha);
+                       tp[2] = (get(op + red) / value_divisor) * alpha + tp[2] * (1 - alpha);
+                       tp[3] = (get(op + 3) / value_divisor) * alpha + tp[3] * (1 - alpha);
 
                        tp += target.bpp;
-                       op += other.bpp;
+                       op += other.bpp / sizeof(OtherType);
                }
        }
 }
 
 
-static
+template <class OtherType>
 void
-alpha_blend_onto_rgba(TargetParams const& target, OtherParams const& other, int red, int blue)
+alpha_blend_onto_rgba(TargetParams const& target, OtherParams const& other, int red, int blue, std::function<float (OtherType*)> get, int value_divisor)
 {
+       auto const alpha_divisor = other.alpha_divisor();
        for (int ty = target.start_y, oy = other.start_y; ty < target.size.height && oy < other.size.height; ++ty, ++oy) {
-               uint8_t* tp = target.line_pointer(ty);
-               uint8_t* op = other.line_pointer(oy);
+               auto tp = target.line_pointer(ty);
+               auto op = reinterpret_cast<OtherType*>(other.line_pointer(oy));
                for (int tx = target.start_x, ox = other.start_x; tx < target.size.width && ox < other.size.width; ++tx, ++ox) {
-                       float const alpha = float (op[3]) / 255;
-                       tp[0] = op[red] * alpha + tp[0] * (1 - alpha);
-                       tp[1] = op[1] * alpha + tp[1] * (1 - alpha);
-                       tp[2] = op[blue] * alpha + tp[2] * (1 - alpha);
-                       tp[3] = op[3] * alpha + tp[3] * (1 - alpha);
+                       float const alpha = get(op + 3) / alpha_divisor;
+                       tp[0] = (get(op + red) / value_divisor) * alpha + tp[0] * (1 - alpha);
+                       tp[1] = (get(op + 1) / value_divisor) * alpha + tp[1] * (1 - alpha);
+                       tp[2] = (get(op + blue) / value_divisor) * alpha + tp[2] * (1 - alpha);
+                       tp[3] = (get(op + 3) / value_divisor) * alpha + tp[3] * (1 - alpha);
 
                        tp += target.bpp;
-                       op += other.bpp;
+                       op += other.bpp / sizeof(OtherType);
                }
        }
 }
 
 
-static
+template <class OtherType>
 void
-alpha_blend_onto_rgb48le(TargetParams const& target, OtherParams const& other, int red, int blue)
+alpha_blend_onto_rgb48le(TargetParams const& target, OtherParams const& other, int red, int blue, std::function<float (OtherType*)> get, int value_scale)
 {
+       auto const alpha_divisor = other.alpha_divisor();
        for (int ty = target.start_y, oy = other.start_y; ty < target.size.height && oy < other.size.height; ++ty, ++oy) {
-               uint8_t* tp = target.line_pointer(ty);
-               uint8_t* op = other.line_pointer(oy);
+               auto tp = reinterpret_cast<uint16_t*>(target.line_pointer(ty));
+               auto op = reinterpret_cast<OtherType*>(other.line_pointer(oy));
                for (int tx = target.start_x, ox = other.start_x; tx < target.size.width && ox < other.size.width; ++tx, ++ox) {
-                       float const alpha = float (op[3]) / 255;
-                       /* Blend high bytes */
-                       tp[1] = op[red] * alpha + tp[1] * (1 - alpha);
-                       tp[3] = op[1] * alpha + tp[3] * (1 - alpha);
-                       tp[5] = op[blue] * alpha + tp[5] * (1 - alpha);
+                       float const alpha = get(op + 3) / alpha_divisor;
+                       tp[0] = get(op + red) * value_scale * alpha + tp[0] * (1 - alpha);
+                       tp[1] = get(op + 1) * value_scale * alpha + tp[1] * (1 - alpha);
+                       tp[2] = get(op + blue) * value_scale * alpha + tp[2] * (1 - alpha);
 
-                       tp += target.bpp;
-                       op += other.bpp;
+                       tp += target.bpp / 2;
+                       op += other.bpp / sizeof(OtherType);
                }
        }
 }
 
 
-static
+template <class OtherType>
 void
-alpha_blend_onto_xyz12le(TargetParams const& target, OtherParams const& other, int red, int blue)
+alpha_blend_onto_xyz12le(TargetParams const& target, OtherParams const& other, int red, int blue, std::function<float (OtherType*)> get, int value_divisor)
 {
+       auto const alpha_divisor = other.alpha_divisor();
        auto conv = dcp::ColourConversion::srgb_to_xyz();
        double fast_matrix[9];
        dcp::combined_rgb_to_xyz(conv, fast_matrix);
        auto lut_in = conv.in()->lut(0, 1, 8, false);
        auto lut_out = conv.out()->lut(0, 1, 16, true);
        for (int ty = target.start_y, oy = other.start_y; ty < target.size.height && oy < other.size.height; ++ty, ++oy) {
-               uint16_t* tp = reinterpret_cast<uint16_t*>(target.data[0] + ty * target.stride[0] + target.start_x * target.bpp);
-               uint8_t* op = other.data[0] + oy * other.stride[0];
+               auto tp = reinterpret_cast<uint16_t*>(target.data[0] + ty * target.stride[0] + target.start_x * target.bpp);
+               auto op = reinterpret_cast<OtherType*>(other.data[0] + oy * other.stride[0]);
                for (int tx = target.start_x, ox = other.start_x; tx < target.size.width && ox < other.size.width; ++tx, ++ox) {
-                       float const alpha = float (op[3]) / 255;
+                       float const alpha = get(op + 3) / alpha_divisor;
 
                        /* Convert sRGB to XYZ; op is BGRA.  First, input gamma LUT */
-                       double const r = lut_in[op[red]];
-                       double const g = lut_in[op[1]];
-                       double const b = lut_in[op[blue]];
+                       double const r = lut_in[get(op + red) / value_divisor];
+                       double const g = lut_in[get(op + 1) / value_divisor];
+                       double const b = lut_in[get(op + blue) / value_divisor];
 
                        /* RGB to XYZ, including Bradford transform and DCI companding */
                        double const x = max(0.0, min(1.0, r * fast_matrix[0] + g * fast_matrix[1] + b * fast_matrix[2]));
@@ -772,7 +780,7 @@ alpha_blend_onto_xyz12le(TargetParams const& target, OtherParams const& other, i
                        tp[2] = lrint(lut_out[lrint(z * 65535)] * 65535) * alpha + tp[2] * (1 - alpha);
 
                        tp += target.bpp / 2;
-                       op += other.bpp;
+                       op += other.bpp / sizeof(OtherType);
                }
        }
 }
@@ -890,8 +898,12 @@ alpha_blend_onto_yuv422p10le(TargetParams const& target, OtherParams const& othe
 void
 Image::alpha_blend (shared_ptr<const Image> other, Position<int> position)
 {
-       /* We're blending RGBA or BGRA images */
-       DCPOMATIC_ASSERT (other->pixel_format() == AV_PIX_FMT_BGRA || other->pixel_format() == AV_PIX_FMT_RGBA);
+       DCPOMATIC_ASSERT(
+               other->pixel_format() == AV_PIX_FMT_BGRA ||
+               other->pixel_format() == AV_PIX_FMT_RGBA ||
+               other->pixel_format() == AV_PIX_FMT_RGBA64BE
+               );
+
        int const blue = other->pixel_format() == AV_PIX_FMT_BGRA ? 0 : 2;
        int const red = other->pixel_format() == AV_PIX_FMT_BGRA ? 2 : 0;
 
@@ -926,29 +938,57 @@ Image::alpha_blend (shared_ptr<const Image> other, Position<int> position)
                other->size(),
                other->data(),
                other->stride(),
-               4
+               other->pixel_format() == AV_PIX_FMT_RGBA64BE ? 8 : 4
+       };
+
+       auto byteswap = [](uint16_t* p) {
+               return (*p >> 8) | ((*p & 0xff) << 8);
+       };
+
+       auto pass = [](uint8_t* p) {
+               return *p;
        };
 
        switch (_pixel_format) {
        case AV_PIX_FMT_RGB24:
                target_params.bpp = 3;
-               alpha_blend_onto_rgb24(target_params, other_params, red, blue);
+               if (other->pixel_format() == AV_PIX_FMT_RGBA64BE) {
+                       alpha_blend_onto_rgb24<uint16_t>(target_params, other_params, red, blue, byteswap, 256);
+               } else {
+                       alpha_blend_onto_rgb24<uint8_t>(target_params, other_params, red, blue, pass, 1);
+               }
                break;
        case AV_PIX_FMT_BGRA:
                target_params.bpp = 4;
-               alpha_blend_onto_bgra(target_params, other_params, red, blue);
+               if (other->pixel_format() == AV_PIX_FMT_RGBA64BE) {
+                       alpha_blend_onto_bgra<uint16_t>(target_params, other_params, red, blue, byteswap, 256);
+               } else {
+                       alpha_blend_onto_bgra<uint8_t>(target_params, other_params, red, blue, pass, 1);
+               }
                break;
        case AV_PIX_FMT_RGBA:
                target_params.bpp = 4;
-               alpha_blend_onto_rgba(target_params, other_params, red, blue);
+               if (other->pixel_format() == AV_PIX_FMT_RGBA64BE) {
+                       alpha_blend_onto_rgba<uint16_t>(target_params, other_params, red, blue, byteswap, 256);
+               } else {
+                       alpha_blend_onto_rgba<uint8_t>(target_params, other_params, red, blue, pass, 1);
+               }
                break;
        case AV_PIX_FMT_RGB48LE:
                target_params.bpp = 6;
-               alpha_blend_onto_rgb48le(target_params, other_params, red, blue);
+               if (other->pixel_format() == AV_PIX_FMT_RGBA64BE) {
+                       alpha_blend_onto_rgb48le<uint16_t>(target_params, other_params, red, blue, byteswap, 1);
+               } else {
+                       alpha_blend_onto_rgb48le<uint8_t>(target_params, other_params, red, blue, pass, 256);
+               }
                break;
        case AV_PIX_FMT_XYZ12LE:
                target_params.bpp = 6;
-               alpha_blend_onto_xyz12le(target_params, other_params, red, blue);
+               if (other->pixel_format() == AV_PIX_FMT_RGBA64BE) {
+                       alpha_blend_onto_xyz12le<uint16_t>(target_params, other_params, red, blue, byteswap, 256);
+               } else {
+                       alpha_blend_onto_xyz12le<uint8_t>(target_params, other_params, red, blue, pass, 1);
+               }
                break;
        case AV_PIX_FMT_YUV420P:
        {
index fb11d209aa3a3f9c30acd77057deab58b69d7878..77ee7d8982c7a0c0d5a1ada46e66012f10a920a4 100644 (file)
@@ -184,8 +184,52 @@ alpha_blend_test_bgra_onto(AVPixelFormat format, string suffix)
 
        auto save = background->convert_pixel_format (dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false);
 
-       write_image (save, "build/test/image_test_" + suffix + ".png");
-       check_image ("build/test/image_test_" + suffix + ".png", TestPaths::private_data() / ("image_test_" + suffix + ".png"));
+       write_image(save, "build/test/image_test_bgra_" + suffix + ".png");
+       check_image("build/test/image_test_bgra_" + suffix + ".png", TestPaths::private_data() / ("image_test_bgra_" + suffix + ".png"));
+}
+
+
+static
+void
+alpha_blend_test_rgba64be_onto(AVPixelFormat format, string suffix)
+{
+       auto proxy = make_shared<FFmpegImageProxy>(TestPaths::private_data() / "prophet_frame.tiff");
+       auto raw = proxy->image(Image::Alignment::PADDED).image;
+       auto background = raw->convert_pixel_format (dcp::YUVToRGB::REC709, format, Image::Alignment::PADDED, false);
+
+       auto overlay = make_shared<Image>(AV_PIX_FMT_RGBA64BE, dcp::Size(431, 891), Image::Alignment::PADDED);
+       overlay->make_transparent();
+
+       for (int y = 0; y < 128; ++y) {
+               auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+               for (int x = 0; x < 128; ++x) {
+                       p[x * 4 + 2] = 65535;
+                       p[x * 4 + 3] = 65535;
+               }
+       }
+
+       for (int y = 128; y < 256; ++y) {
+               auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+               for (int x = 0; x < 128; ++x) {
+                       p[x * 4 + 1] = 65535;
+                       p[x * 4 + 3] = 65535;
+               }
+       }
+
+       for (int y = 256; y < 384; ++y) {
+               auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + y * overlay->stride()[0]);
+               for (int x = 0; x < 128; ++x) {
+                       p[x * 4] = 65535;
+                       p[x * 4 + 3] = 65535;
+               }
+       }
+
+       background->alpha_blend(overlay, Position<int>(13, 17));
+
+       auto save = background->convert_pixel_format(dcp::YUVToRGB::REC709, AV_PIX_FMT_RGB24, Image::Alignment::COMPACT, false);
+
+       write_image(save, "build/test/image_test_rgba64_" + suffix + ".png");
+       check_image("build/test/image_test_rgba64_" + suffix + ".png", TestPaths::private_data() / ("image_test_rgba64_" + suffix + ".png"));
 }
 
 
@@ -199,11 +243,19 @@ BOOST_AUTO_TEST_CASE (alpha_blend_test)
        alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV420P, "yuv420p");
        alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV420P10, "yuv420p10");
        alpha_blend_test_bgra_onto(AV_PIX_FMT_YUV422P10LE, "yuv422p10le");
+
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGB24, "rgb24");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_BGRA, "bgra");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGBA, "rgba");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_RGB48LE, "rgb48le");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV420P, "yuv420p");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV420P10, "yuv420p10");
+       alpha_blend_test_rgba64be_onto(AV_PIX_FMT_YUV422P10LE, "yuv422p10le");
 }
 
 
-/** Test Image::alpha_blend when the "base" image is XYZ12LE */
-BOOST_AUTO_TEST_CASE (alpha_blend_test_onto_xyz)
+/** Test Image::alpha_blend when blending RGBA onto XYZ12LE */
+BOOST_AUTO_TEST_CASE(alpha_blend_test_rgba_onto_xyz)
 {
        Image xyz(AV_PIX_FMT_XYZ12LE, dcp::Size(50, 50), Image::Alignment::PADDED);
        xyz.make_black();
@@ -239,6 +291,57 @@ BOOST_AUTO_TEST_CASE (alpha_blend_test_onto_xyz)
 }
 
 
+/** Test Image::alpha_blend when blending RGBA64BE onto XYZ12LE */
+BOOST_AUTO_TEST_CASE(alpha_blend_test_rgba64be_onto_xyz)
+{
+       Image xyz(AV_PIX_FMT_XYZ12LE, dcp::Size(50, 50), Image::Alignment::PADDED);
+       xyz.make_black();
+
+       auto overlay = make_shared<Image>(AV_PIX_FMT_RGBA64BE, dcp::Size(8, 8), Image::Alignment::PADDED);
+       for (int y = 0; y < 8; ++y) {
+               auto p = reinterpret_cast<uint16_t*>(overlay->data()[0] + (y * overlay->stride()[0]));
+               for (int x = 0; x < 8; ++x) {
+                       *p++ = 65535;
+                       *p++ = 0;
+                       *p++ = 0;
+                       *p++ = 65535;
+               }
+       }
+
+       xyz.alpha_blend(overlay, Position<int>(4, 4));
+
+       for (int y = 0; y < 50; ++y) {
+               auto p = reinterpret_cast<uint16_t*>(xyz.data()[0]) + (y * xyz.stride()[0] / 2);
+               for (int x = 0; x < 50; ++x) {
+                       if (4 <= x && x < 12 && 4 <= y && y < 12) {
+                               BOOST_REQUIRE_EQUAL(p[0], 45078U);
+                               BOOST_REQUIRE_EQUAL(p[1], 34939U);
+                               BOOST_REQUIRE_EQUAL(p[2], 13892U);
+                       } else {
+                               BOOST_REQUIRE_EQUAL(p[0], 0U);
+                               BOOST_REQUIRE_EQUAL(p[1], 0U);
+                               BOOST_REQUIRE_EQUAL(p[2], 0U);
+                       }
+                       p += 3;
+               }
+       }
+}
+
+
+BOOST_AUTO_TEST_CASE(alpha_blend_text)
+{
+       Image target(AV_PIX_FMT_RGB24, dcp::Size(1998, 1080), Image::Alignment::PADDED);
+       target.make_black();
+
+       FFmpegImageProxy subtitle_proxy(TestPaths::private_data() / "16-bit-sub.png");
+       auto subtitle = subtitle_proxy.image(Image::Alignment::COMPACT);
+
+       target.alpha_blend(subtitle.image, Position<int>(0, 0));
+       write_image(make_shared<Image>(target), "build/test/alpha_blend_text.png");
+       check_image("build/test/alpha_blend_text.png", TestPaths::private_data() / "16-bit-sub-blended.png");
+}
+
+
 /** Test merge (list<PositionImage>) with a single image */
 BOOST_AUTO_TEST_CASE (merge_test1)
 {