From 4a29af12e174f2b53bf909788b7cad95837aef79 Mon Sep 17 00:00:00 2001 From: Bram Stolk Date: Sun, 16 Nov 2025 15:09:15 -0800 Subject: [PATCH] [effect] Support 1D LUT in color map filter. This adds more complete support for color mapping with .cube files. Both 1D and 3D LUTs are now supported. Tested by running the filter with both types of LUTs. It also adds const specifiers to the 3D lookup code. FIXES #1022 Signed-off-by: Bram Stolk --- src/effects/ColorMap.cpp | 218 ++++++++++++++++++++++++++++++--------- src/effects/ColorMap.h | 15 ++- 2 files changed, 185 insertions(+), 48 deletions(-) diff --git a/src/effects/ColorMap.cpp b/src/effects/ColorMap.cpp index 3587cd9d3..8c1f4915e 100644 --- a/src/effects/ColorMap.cpp +++ b/src/effects/ColorMap.cpp @@ -26,7 +26,11 @@ void ColorMap::load_cube_file() return; } - int parsed_size = 0; + // .cube files can have 1D lookups or 3D lookups. + // Depending what type of LUT we have, one of these will be set to non-zero. + int parsed_size_3d = 0; + int parsed_size_1d = 0; + std::vector parsed_data; #pragma omp critical(load_lut) @@ -45,15 +49,50 @@ void ColorMap::load_cube_file() if (line.startsWith("LUT_3D_SIZE")) { auto parts = line.split(ws_re); if (parts.size() >= 2) { - parsed_size = parts[1].toInt(); + parsed_size_3d = parts[1].toInt(); + } + break; + } + if (line.startsWith("LUT_1D_SIZE")) { + auto parts = line.split(ws_re); + if (parts.size() >= 2) { + parsed_size_1d = parts[1].toInt(); } break; } } - // 2) Read N³ lines of R G B floats - if (parsed_size > 0) { - int total = parsed_size * parsed_size * parsed_size; + // 2) Read N³ lines of R G B floats if we have a 3D LUT. + if (parsed_size_3d > 0) { + int total = parsed_size_3d * parsed_size_3d * parsed_size_3d; + parsed_data.reserve(size_t(total * 3)); + while (!in.atEnd() && int(parsed_data.size()) < total * 3) { + line = in.readLine().trimmed(); + if (line.isEmpty() || + line.startsWith("#") || + line.startsWith("TITLE") || + line.startsWith("DOMAIN")) + { + continue; + } + auto vals = line.split(ws_re); + if (vals.size() >= 3) { + // .cube file is R G B + parsed_data.push_back(vals[0].toFloat()); + parsed_data.push_back(vals[1].toFloat()); + parsed_data.push_back(vals[2].toFloat()); + } + } + if (int(parsed_data.size()) != total * 3) { + parsed_data.clear(); + parsed_size_3d = 0; + fprintf(stderr, "Unexpected sample count in the .cube file. Discarding 3D LUT.\n"); + } + } + + // 3) Read N lines of R G B floats if we have a 1D LUT. + if (parsed_size_1d > 0) { + int total = parsed_size_1d; parsed_data.reserve(size_t(total * 3)); while (!in.atEnd() && int(parsed_data.size()) < total * 3) { line = in.readLine().trimmed(); @@ -74,14 +113,20 @@ void ColorMap::load_cube_file() } if (int(parsed_data.size()) != total * 3) { parsed_data.clear(); - parsed_size = 0; + parsed_size_3d = 0; + fprintf(stderr, "Unexpected sample count in the .cube file. Discarding 1D LUT.\n"); } } } } - if (parsed_size > 0) { - lut_size = parsed_size; + if (parsed_size_3d > 0) { + lut_is_3d = true; + lut_size = parsed_size_3d; + lut_data.swap(parsed_data); + } else if (parsed_size_1d > 0) { + lut_is_3d = false; + lut_size = parsed_size_1d; lut_data.swap(parsed_data); } else { lut_data.clear(); @@ -115,6 +160,7 @@ ColorMap::ColorMap(const std::string &path, const Keyframe &iB) : lut_path(path), lut_size(0), + lut_is_3d(false), needs_refresh(true), intensity(i), intensity_r(iR), @@ -137,55 +183,65 @@ ColorMap::GetFrame(std::shared_ptr frame, int64_t frame_number) if (lut_data.empty()) return frame; + // Depending whether we have 1D or 3D LUT, we do different lookups. + if (lut_is_3d) + return GetFrame3D(frame, frame_number); + else + return GetFrame1D(frame, frame_number); +} + +std::shared_ptr +ColorMap::GetFrame3D(std::shared_ptr frame, int64_t frame_number) +{ auto image = frame->GetImage(); - int w = image->width(), h = image->height(); + const int w = image->width(), h = image->height(); unsigned char *pixels = image->bits(); - float overall = float(intensity.GetValue(frame_number)); - float tR = float(intensity_r.GetValue(frame_number)) * overall; - float tG = float(intensity_g.GetValue(frame_number)) * overall; - float tB = float(intensity_b.GetValue(frame_number)) * overall; + const float overall = float(intensity.GetValue(frame_number)); + const float tR = float(intensity_r.GetValue(frame_number)) * overall; + const float tG = float(intensity_g.GetValue(frame_number)) * overall; + const float tB = float(intensity_b.GetValue(frame_number)) * overall; - int pixel_count = w * h; + const int pixel_count = w * h; #pragma omp parallel for for (int i = 0; i < pixel_count; ++i) { - int idx = i * 4; - int A = pixels[idx + 3]; - float alpha = A / 255.0f; + const int idx = i * 4; + const int A = pixels[idx + 3]; + const float alpha = A / 255.0f; if (alpha == 0.0f) continue; // demultiply premultiplied RGBA - float R = pixels[idx + 0] / alpha; - float G = pixels[idx + 1] / alpha; - float B = pixels[idx + 2] / alpha; + const float R = pixels[idx + 0] / alpha; + const float G = pixels[idx + 1] / alpha; + const float B = pixels[idx + 2] / alpha; // normalize to [0,1] - float Rn = R * (1.0f / 255.0f); - float Gn = G * (1.0f / 255.0f); - float Bn = B * (1.0f / 255.0f); + const float Rn = R * (1.0f / 255.0f); + const float Gn = G * (1.0f / 255.0f); + const float Bn = B * (1.0f / 255.0f); // map into LUT space [0 .. size-1] - float rf = Rn * (lut_size - 1); - float gf = Gn * (lut_size - 1); - float bf = Bn * (lut_size - 1); + const float rf = Rn * (lut_size - 1); + const float gf = Gn * (lut_size - 1); + const float bf = Bn * (lut_size - 1); - int r0 = int(floor(rf)), r1 = std::min(r0 + 1, lut_size - 1); - int g0 = int(floor(gf)), g1 = std::min(g0 + 1, lut_size - 1); - int b0 = int(floor(bf)), b1 = std::min(b0 + 1, lut_size - 1); + const int r0 = int(floor(rf)), r1 = std::min(r0 + 1, lut_size - 1); + const int g0 = int(floor(gf)), g1 = std::min(g0 + 1, lut_size - 1); + const int b0 = int(floor(bf)), b1 = std::min(b0 + 1, lut_size - 1); - float dr = rf - r0; - float dg = gf - g0; - float db = bf - b0; + const float dr = rf - r0; + const float dg = gf - g0; + const float db = bf - b0; // compute base offsets with red fastest, then green, then blue - int base000 = ((b0 * lut_size + g0) * lut_size + r0) * 3; - int base100 = ((b0 * lut_size + g0) * lut_size + r1) * 3; - int base010 = ((b0 * lut_size + g1) * lut_size + r0) * 3; - int base110 = ((b0 * lut_size + g1) * lut_size + r1) * 3; - int base001 = ((b1 * lut_size + g0) * lut_size + r0) * 3; - int base101 = ((b1 * lut_size + g0) * lut_size + r1) * 3; - int base011 = ((b1 * lut_size + g1) * lut_size + r0) * 3; - int base111 = ((b1 * lut_size + g1) * lut_size + r1) * 3; + const int base000 = ((b0 * lut_size + g0) * lut_size + r0) * 3; + const int base100 = ((b0 * lut_size + g0) * lut_size + r1) * 3; + const int base010 = ((b0 * lut_size + g1) * lut_size + r0) * 3; + const int base110 = ((b0 * lut_size + g1) * lut_size + r1) * 3; + const int base001 = ((b1 * lut_size + g0) * lut_size + r0) * 3; + const int base101 = ((b1 * lut_size + g0) * lut_size + r1) * 3; + const int base011 = ((b1 * lut_size + g1) * lut_size + r0) * 3; + const int base111 = ((b1 * lut_size + g1) * lut_size + r1) * 3; // trilinear interpolation // red @@ -195,7 +251,7 @@ ColorMap::GetFrame(std::shared_ptr frame, int64_t frame_number) float c11 = lut_data[base011 + 0] * (1 - dr) + lut_data[base111 + 0] * dr; float c0 = c00 * (1 - dg) + c10 * dg; float c1 = c01 * (1 - dg) + c11 * dg; - float lr = c0 * (1 - db) + c1 * db; + const float lr = c0 * (1 - db) + c1 * db; // green c00 = lut_data[base000 + 1] * (1 - dr) + lut_data[base100 + 1] * dr; @@ -204,7 +260,7 @@ ColorMap::GetFrame(std::shared_ptr frame, int64_t frame_number) c11 = lut_data[base011 + 1] * (1 - dr) + lut_data[base111 + 1] * dr; c0 = c00 * (1 - dg) + c10 * dg; c1 = c01 * (1 - dg) + c11 * dg; - float lg = c0 * (1 - db) + c1 * db; + const float lg = c0 * (1 - db) + c1 * db; // blue c00 = lut_data[base000 + 2] * (1 - dr) + lut_data[base100 + 2] * dr; @@ -213,12 +269,12 @@ ColorMap::GetFrame(std::shared_ptr frame, int64_t frame_number) c11 = lut_data[base011 + 2] * (1 - dr) + lut_data[base111 + 2] * dr; c0 = c00 * (1 - dg) + c10 * dg; c1 = c01 * (1 - dg) + c11 * dg; - float lb = c0 * (1 - db) + c1 * db; + const float lb = c0 * (1 - db) + c1 * db; // blend per-channel, re-premultiply alpha - float outR = (lr * tR + Rn * (1 - tR)) * alpha; - float outG = (lg * tG + Gn * (1 - tG)) * alpha; - float outB = (lb * tB + Bn * (1 - tB)) * alpha; + const float outR = (lr * tR + Rn * (1 - tR)) * alpha; + const float outG = (lg * tG + Gn * (1 - tG)) * alpha; + const float outB = (lb * tB + Bn * (1 - tB)) * alpha; pixels[idx + 0] = constrain(outR * 255.0f); pixels[idx + 1] = constrain(outG * 255.0f); @@ -229,6 +285,76 @@ ColorMap::GetFrame(std::shared_ptr frame, int64_t frame_number) return frame; } +std::shared_ptr +ColorMap::GetFrame1D(std::shared_ptr frame, int64_t frame_number) +{ + auto image = frame->GetImage(); + const int w = image->width(), h = image->height(); + unsigned char *pixels = image->bits(); + + const float overall = float(intensity.GetValue(frame_number)); + const float tR = float(intensity_r.GetValue(frame_number)) * overall; + const float tG = float(intensity_g.GetValue(frame_number)) * overall; + const float tB = float(intensity_b.GetValue(frame_number)) * overall; + + const int pixel_count = w * h; + #pragma omp parallel for + for (int i = 0; i < pixel_count; ++i) { + const int idx = i * 4; + const int A = pixels[idx + 3]; + const float alpha = A / 255.0f; + if (alpha == 0.0f) continue; + + // demultiply premultiplied RGBA + const float R = pixels[idx + 0] / alpha; + const float G = pixels[idx + 1] / alpha; + const float B = pixels[idx + 2] / alpha; + + // normalize to [0,1] + const float Rn = R * (1.0f / 255.0f); + const float Gn = G * (1.0f / 255.0f); + const float Bn = B * (1.0f / 255.0f); + + // map into LUT space [0 .. size-1] + const float rf = Rn * (lut_size - 1); + const float gf = Gn * (lut_size - 1); + const float bf = Bn * (lut_size - 1); + + const int r0 = int(floor(rf)), r1 = std::min(r0 + 1, lut_size - 1); + const int g0 = int(floor(gf)), g1 = std::min(g0 + 1, lut_size - 1); + const int b0 = int(floor(bf)), b1 = std::min(b0 + 1, lut_size - 1); + + const float dr = rf - r0; + const float dg = gf - g0; + const float db = bf - b0; + + const int base_r_0 = r0 * 3; + const int base_r_1 = r1 * 3; + const int base_g_0 = g0 * 3; + const int base_g_1 = g1 * 3; + const int base_b_0 = b0 * 3; + const int base_b_1 = b1 * 3; + + // linear interpolation for red. + const float lr = lut_data[base_r_0 + 0] * (1 - dr) + lut_data[base_r_1 + 0] * dr; + // linear interpolation for grn. + const float lg = lut_data[base_g_0 + 1] * (1 - dg) + lut_data[base_g_1 + 1] * dg; + // linear interpolation for blu. + const float lb = lut_data[base_b_0 + 2] * (1 - db) + lut_data[base_b_1 + 2] * db; + + // blend per-channel, re-premultiply alpha + const float outR = (lr * tR + Rn * (1 - tR)) * alpha; + const float outG = (lg * tG + Gn * (1 - tG)) * alpha; + const float outB = (lb * tB + Bn * (1 - tB)) * alpha; + + pixels[idx + 0] = constrain(outR * 255.0f); + pixels[idx + 1] = constrain(outG * 255.0f); + pixels[idx + 2] = constrain(outB * 255.0f); + // alpha left unchanged + } + + return frame; +} std::string ColorMap::Json() const { diff --git a/src/effects/ColorMap.h b/src/effects/ColorMap.h index 91cc73868..f8edd22d3 100644 --- a/src/effects/ColorMap.h +++ b/src/effects/ColorMap.h @@ -35,8 +35,9 @@ namespace openshot { private: std::string lut_path; ///< Filesystem path to .cube LUT file - int lut_size; ///< Dimension N of the cube (LUT_3D_SIZE) - std::vector lut_data; ///< Flat array [N³ × 3] RGB lookup table + int lut_size; ///< LUT_1D_SIZE or LUT_3D_SIZE. + bool lut_is_3d; ///< When false, we have a 1D LUT. + std::vector lut_data; ///< Flat array [N³ × 3] RGB lookup table in case of 3D. bool needs_refresh; ///< Reload LUT on next frame /// Populate info fields (class_name, name, description) @@ -45,6 +46,16 @@ namespace openshot /// Parse the .cube file into lut_size & lut_data void load_cube_file(); + /// Apply effect to an existing frame using 1D LUT + std::shared_ptr + GetFrame1D(std::shared_ptr frame, + int64_t frame_number); + + /// Apply effect to an existing frame using 3D LUT + std::shared_ptr + GetFrame3D(std::shared_ptr frame, + int64_t frame_number); + public: Keyframe intensity; ///< Overall intensity 0–1 (affects all channels) Keyframe intensity_r; ///< Blend 0–1 for red channel