Skip to content

Improve RGB channel detection heuristics for the HT256 encoder #2029

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: htj2k-beta
Choose a base branch
from

Conversation

palemieux
Copy link

@palemieux palemieux commented May 1, 2025

Updated algorithm:

  * RGB channels are present if the either (a) the names of the channels in
  * their entirety or (b) following the "." character match one of the
  * triplets defined in {params}. Order of the channels and case of the
  * channel names are ignored.
  *
  * Example 1: "main.b", "main.g", "main.r" match
  * Example 2: "MainR", "MainG", "MainB" do not match
  * Example 3: "R", "B", "B" match
  * Example 4: "red", "green", "blue" match

@palemieux palemieux force-pushed the issues/expand-rgb-detection-heuristics branch from c5bdab3 to 710e606 Compare May 1, 2025 23:10

if (c_name == "r" || c_name.compare(0, 3, "red") == 0) { r_index = i; }
else if (c_name == "g" || c_name.compare(0, 5, "green") == 0) { g_index = i; }
else if (c_name == "b" || c_name.compare(0, 4, "blue")== 0) { b_index = i; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will consider R as a red channel, but not a channel which has a layer name with a R suffix e.g.diffuse.R?
Also, if the image happens to have both a R and a r channel, it'll pick the r channel as the red channel not R, which is a little unfortunate, since the standard name is R

Ideally it should look for R as the channel name, and only fall back to using a channel with a non-standard name, or whose name starts with a layer name, if no R channel is found.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Proposed algorithm:

  • look for R, G and B (case sensitive)
  • if not found, look for:
    • r, g and b
    • .r, .g and .b suffixes (case insensitive)
    • red, green, blue (case insensitive)

Anything else, like:

  • -red, -green and -blue suffixes (case insensitive)
  • red*, green* and blue* (case insensitive)
  • -r, -g and -b suffixes (case insensitive)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first two points seem sensible. The standard says that a red channel must be R, or a *.R. It's quite common to see .red as a suffix, and it would be worth checking for that too, as well as the full channel name

Being too generous with names may cause problems, such as redrawn.G being treated as a red channel, or blueberry.R as a blue channel.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the RGBA interface? It looks like it simply appends R, G and B to a common prefix. If so, should the encoder look for R, G and B at the end of the channel name?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NVM, it looks like the prefix in the RGBA interface is computed with prefixFromLayerName(), which ensures a . is present.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See revised algorithm.

@meshula
Copy link
Contributor

meshula commented May 3, 2025

The algorithm below captures two years of use and feedback from many contributors and is believed to be good for this purpose.

// strcasecmp is not available in the MSVC stdlib.
#ifdef _MSC_VER
#define strcasecmp _stricmp
#endif

static bool strIsComponent(const char* layerName, 
                           const char* str, 
                           const char* component)
{
    if (!str || !component)
        return false;

    // if the layername is specified, it must match the beginning of the string.
    // if layername was specified, move the string pointer past it
    if (layerName) {
        if (strncmp(layerName, str, strlen(layerName)) != 0)
            return false;
        str += strlen(layerName);
    }

    // search backwards in str for a dot. If found, move the string pointer
    // past it
    const char* dot = strrchr(str, '.');
    if (dot) {
        str = dot + 1;
    }

    // if one character is left, then do a case insensitive comparison with the
    // first character of the component.
    if (strlen(str) == 1) {
        return tolower(str[0]) == tolower(component[0]);
    }

    // The final check is to return true if a case insensitive comparison of
    // the string with the component is true.
    return strcasecmp(str, component) == 0;
}

static exr_result_t _nanoexr_rgba_decoding_initialize(
    exr_context_t exr,
    nanoexr_ImageData_t* img,
    const char* layerName,
    int partIndex, exr_chunk_info_t* cinfo, exr_decode_pipeline_t* decoder,
    int* rgba)
{
    exr_result_t rv = EXR_ERR_SUCCESS;
    rv = exr_decoding_initialize(exr, partIndex, cinfo, decoder);
    if (rv != EXR_ERR_SUCCESS) {
        fprintf(stderr, "exr_decoding_initialize failed: %s\n", 
                exr_get_default_error_message(rv));
        return rv;
    }
    int bytesPerChannel = nanoexr_getPixelTypeSize(img->pixelType);
    for (int i = 0; i < img->channelCount; ++i) {
        rgba[i] = -1;
    }
    for (int c = 0; c < decoder->channel_count; ++c) {
        int channelIndex = -1;
        decoder->channels[c].decode_to_ptr = NULL;
        if (strIsComponent(layerName, decoder->channels[c].channel_name, "red")
            || (rgba[0] == -1 && strIsComponent(layerName, decoder->channels[c].channel_name, "y"))) {
            rgba[0] = c;
            channelIndex = 0;
        }
        else if (strIsComponent(layerName, decoder->channels[c].channel_name, "green")) {
            rgba[1] = c;
            channelIndex = 1;
        }
        else if (strIsComponent(layerName, decoder->channels[c].channel_name, "blue")) {
            rgba[2] = c;
            channelIndex = 2;
        }
        else if (strIsComponent(layerName, decoder->channels[c].channel_name, "alpha")) {
            rgba[3] = c;
            channelIndex = 3;
        }
        if (channelIndex == -1 || channelIndex >= img->channelCount) {
            continue;
        }
        // calculate start of pixel data for the target channel
        decoder->channels[c].decode_to_ptr = (uint8_t*) (ptrdiff_t) (channelIndex * bytesPerChannel);
    }
    return rv;
}

@meshula
Copy link
Contributor

meshula commented May 5, 2025

The rgba detection is much more robust now! However, there is one more fly in the ointment.

The algorithm I posted takes a layer name. The point there is that if an exr image contains layers named say

albedo.{rgba}
beauty.{rgba}

one should take care to group albedo.{rgba} together, and beauty.{rgba} together. A fault that can arise if you don't also aggregate layer names is this:

albedo.{rgb} beauty.{rgba}

will yield rgba = {albedo.r, albedo.g, albedo.b, beauty.a }

or this scenario:

odd.{rb}
even.{ga}

yielding rgba={odd.r, even.g, odd.b, even.a}

@meshula
Copy link
Contributor

meshula commented May 5, 2025

Also, I was wondering about the red, grn, blu, suffixes. In the code I posted we only allow red, green, blue, and r, g, b. I've never seen grn, blu arise, and was curious if you use tools that produce them?

@palemieux
Copy link
Author

Also, I was wondering about the red, grn, blu, suffixes. In the code I posted we only allow red, green, blue, and r, g, b. I've never seen grn, blu arise, and was curious if you use tools that produce them?

This came from the DWA code.

@palemieux
Copy link
Author

The rgba detection is much more robust now! However, there is one more fly in the ointment.

To summarize: the prefix, if it exists, should match across the R, G and B channels, right?

@palemieux
Copy link
Author

@meshula I have added RGB detection heuristics tests at src/test/OpenEXRCoreTest/compression.cpp. I would appreciate your quick review. Feel free to suggest additional combinations.

palemieux added 4 commits May 7, 2025 08:56
Signed-off-by: Pierre-Anthony Lemieux <[email protected]>
Signed-off-by: Pierre-Anthony Lemieux <[email protected]>
Add unit tests

Signed-off-by: Pierre-Anthony Lemieux <[email protected]>
@palemieux palemieux force-pushed the issues/expand-rgb-detection-heuristics branch from 49e4855 to 4c98d80 Compare May 7, 2025 15:56
RGBChannelParams params[] = {
{"r", "g", "b", -1, -1, -1, NULL, 0},
{"red", "green", "blue", -1, -1, -1, NULL, 0},
{"red", "grn", "blu", -1, -1, -1, NULL, 0}};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still wonder if there's a tool that produces red, gin, blu, in other words, what's the motivation or including them?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to remove them... your call.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My opinion is that it suggests to the reader of the code that those abbreviations are expected as legal, but we don't do that anywhere else so I prefer to elide them without something to ground them.

Copy link
Contributor

@meshula meshula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This version seems robust against the combinations I can think of, thanks!

Signed-off-by: Pierre-Anthony Lemieux <[email protected]>
Copy link
Contributor

@meshula meshula left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants