Should you normalize RGB values by 255 or 256?

(30fps.net)

78 points | by pplanu 2 hours ago

11 comments

  • herf 1 hour ago
    I'll argue for the +0.5 solution. First, I don't like half-sized intervals at the edges, and second, a 255-based representation is typically a SDR (not HDR) image.

    RGB values represent luminances against some adapted state, and a "zero" in a daylit scene is not "zero luminance" - it's just about 0.001x as bright as the brightest point - it's millions of photons, way more than zero. In a sense our eyes experience contrast on a sliding scale, and there is no absolute zero in the system. For example, broadcast systems historically used 16-235 as their luminance range for SDR. I think any argument that says "we must have zero" is going to have a bias, but I don't think zero is needed for most things.

    • amavect 40 minutes ago
      I agree. Additionally, both 0.0 and 1.0 don't really exist for dithered signals, so a byte should map to [0.5, 255.5] before division by 256. This also solves the signed integer asymmetry, as a signed byte maps to [-127.5, 127.5] before division by 128. I wonder if audio DSP folks have done this already.
    • yxhuvud 1 hour ago
      Both solutions add 0.5, the difference is where in the process it happens.
    • themafia 17 minutes ago
      > In a sense our eyes experience contrast on a sliding scale

      There's a whole visual center to check the amount of incoming light and adjust your pupils for you. It's intentionally reactive.

      > and there is no absolute zero in the system.

      There maybe is. I think we call that "blind."

      > broadcast systems historically used 16-235 as their luminance range for SDR

      Mostly because it was a fully analog system and these all translate down to signal voltage. Jokingly NTSC used to be referred to as "Never Twice the Same Color" due to being a compromise bolted onto the side of an already compromised system.

  • dgently7 11 minutes ago
    "Let’s say you’re writing an image processing program. The program takes in an image, converts it to floating point, does some processing and finally saves the modified pixels to disk as 8-bit colors. "

    excuse to argue about the best way aside, if this is the goal you should not be rolling your own image file reading. you should use openimageio. idk what approach it takes in its internal conversion to float, but that library is more likely to have the right answer than you trying to roll it yourself given its the library used internally by tons of professional image manipulation software...

  • dudu24 1 hour ago
    If you have a ruler and it goes to 12 inches, you should normalize by the length L and not by 13, the number of points on the ruler.
    • lacedeconstruct 1 hour ago
      yes but >> 8 is so much faster
      • xigoi 15 minutes ago
        You don’t divide a float by 256 by shifting it right eight bits; that would yield complete garbage. You subtract 8 from the exponent, then check if you got an underflow.
      • StilesCrisis 24 minutes ago
        It's just multiplication. Floating multiply is extraordinarily fast.
        • lacedeconstruct 17 minutes ago
          The difference between 20 cycles and 1 clock cycle in a hot loop is very noticeable
      • dist-epoch 49 minutes ago
        Only in micro-benchmarks.

        For real usage, today's CPUs are limited by memory bandwidth.

        • lacedeconstruct 39 minutes ago
          What are you talking about in a hot loop in my software renderer this is like 10x faster

              // color4_t result = {
              //     .r = (src.r * src.a + dst.r * inv_alpha) * INV_255,
              //     .g = (src.g * src.a + dst.g * inv_alpha) * INV_255,
              //     .b = (src.b * src.a + dst.b * inv_alpha) * INV_255,
              //     .a = src.a + (dst.a * inv_alpha) * INV_255
              // };
          
              // 1/256 but much faster
              color4_t result = {
                  .r = (src.r * src.a + dst.r * inv_alpha) >> 8,
                  .g = (src.g * src.a + dst.g * inv_alpha) >> 8,
                  .b = (src.b * src.a + dst.b * inv_alpha) >> 8,
                  .a = src.a + ((dst.a * inv_alpha) >> 8)
              };
          • dist-epoch 39 minutes ago
            Because you are working in the cache.

            Also, you should use SIMD.

            • lacedeconstruct 31 minutes ago
              > Also, you should use SIMD. ironically no clang is better at auto vectorizing
        • szundi 47 minutes ago
          [dead]
    • groundzeros2015 1 hour ago
      I’m dumb. Doesn’t 0 start at the beginning?
  • theyeenzbeanz 1 hour ago
    Should always be 0-255 as that fits an unsigned byte.
    • crazygringo 1 hour ago
      That's not what the article is about.
    • Retr0id 1 hour ago
      > assume that in both cases the output values are clamped before the final typecast
  • crazygringo 1 hour ago
    Advice for anyone on mobile: read in landscape mode if you want to be able to see the division by 256 version code example at the start.

    The HTML/CSS is bad that lets it completely overflow the right edge of the page instead of wrapping.

    I re-read this post three times in total confusion before I figured out the most important piece was off-screen entirely.

  • atilimcetin 33 minutes ago
    Interesting article. I tend to use

    - min(floor(x*256),255) (from float to uint8)

    - i/255 (from uint8 to float)

    Basically a mix of the 2 approaches mentioned in the article.

    For all integers between [0,255], if I do uint8 -> float -> uint8 conversion, I will get the same result. That's the important part for me.

    • vitorsr 14 minutes ago
      This is what I do for the former:

          floor( nextafter( 256, 255 ) * value )
      • atilimcetin 11 minutes ago
        Oh very nice idea to get rid of the min operator.
  • Sesse__ 36 minutes ago
    You should multiply by 255.0, optionally add a dither (triangular is okay), and then let the FPU round using its default IEEE 754 round-to-nearest-ties-to-nearest-even mode. None of this crazy 0.5 stuff. :-)
  • Retr0id 1 hour ago
    Both of these assume a linear transfer function, which is rarely the case.
  • dist-epoch 53 minutes ago
    A similar issue exists in the audio world, for example 16-bit integer audio is between [-32768, 32767] (non-symmetric), but floating point audio is [-1.0, 1.0].
    • adzm 46 minutes ago
      note that floating point audio very often exceeds [-1.0, 1.0] within the pipeline, just to be tamed at the very end of the mix to fit within those bounds. this is pretty much why every modern DAW uses floating point these days.
  • DigitallyFidget 1 hour ago
    255 gives 0-255, which gives you a zero value. 256 is 1-256, you lose the option of setting 0.
    • crazygringo 1 hour ago
      That's not what the article is about.
  • ctdinjeu8 34 minutes ago
    Both. 255 for each color and the last 1 as the alpha for each channel.

    Why not??? Fight me