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.

  • yxhuvud 1 hour ago

    Both solutions add 0.5, the difference is where in the process it happens.

  • amavect 1 hour 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.

  • themafia 41 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.

Nuthen 14 minutes ago

That was a fun article to read of something I haven't had to think about in a while. It brought to mind moments in game development of having pixel art needing to be drawn on an integer value despite the game logic using floating point math. I tried something similar to the +0.5 in places so that it wouldn't look as bad (especially when there's a moving camera, which also needed to be truncated..).

I also enjoyed the 2002 article by Jonathan Blow [1] that's linked at the bottom. The visualization from the first article helped a lot once this started to go more in-depth.

[1] https://web.archive.org/web/20240706043551/https://number-no...

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

    • dist-epoch 1 hour ago

      Only in micro-benchmarks.

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

      • lacedeconstruct 1 hour 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 1 hour ago

          Because you are working in the cache.

          Also, you should use SIMD.

          • lacedeconstruct 55 minutes ago

            > Also, you should use SIMD. ironically no clang is better at auto vectorizing

    • StilesCrisis 47 minutes ago

      It's just multiplication. Floating multiply is extraordinarily fast.

      • lacedeconstruct 41 minutes ago

        The difference between 20 cycles and 1 clock cycle in a hot loop is very noticeable

        • Sesse__ 19 minutes ago

          Useful, then, that you can start several vectorized floating-point muls each cycle. (E.g., most modern x86 are 3/0.5 cycles for vmulps. No 20 cycles in sight.)

        • Tuna-Fish 7 minutes ago

          FP Division by constant is optimized by a compiler into a multiply. Graphics processing typically happens on the GPU these days, and on all recent GPUs FPMUL belongs to the class of lowest-latency operations. That is, there are no other instructions that complete faster.

    • xigoi 39 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.

  • groundzeros2015 1 hour ago

    I’m dumb. Doesn’t 0 start at the beginning?

atilimcetin 57 minutes ago

Interesting article. I tend to use

- i = min(floor(f * 256), 255) (from float to uint8)

- f = 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.

edit: I wonder what's the maximum jitter amount that I can introduce to the float and get the same uint8 value. And also these 0->0.0 and 255->1.0 should map properly.

With my approach at the top, maximum jitter that I can introduce is ~1/65280.

But as the article mentioned, this is the approach:

- i = floor(f * 255 + 0.5)

- f = i / 255

with maximum jitter margin of ~1/510.

  • vitorsr 37 minutes ago

    This is what I do for the former:

        floor( nextafter( 256, 255 ) * value )
    • atilimcetin 35 minutes ago

      Oh very nice idea to get rid of the min operator.

  • AgentME 5 minutes ago

    It's worth pointing out that the article explicitly calls out your first mixed technique:

    > Finally, one should never mix the encode and decode steps of the two quantizers. That’s just broken code. It’s an easy mistake to make, though.

Retr0id 1 hour ago

Both of these assume a linear transfer function, which is rarely the case.

  • leni536 20 minutes ago

    Basically never for 8-bit color channels.

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.

theyeenzbeanz 1 hour ago

Should always be 0-255 as that fits an unsigned byte.

  • Retr0id 1 hour ago

    > assume that in both cases the output values are clamped before the final typecast

  • crazygringo 1 hour ago

    That's not what the article is about.

Sesse__ 59 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. :-)

dist-epoch 1 hour 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 1 hour 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 58 minutes ago

Both. 255 for each color and the last 1 as the alpha for each channel.

Why not??? Fight me

dgently7 35 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...

  • pixelesque 13 minutes ago

    If you're a beginner, or just want something which works quickly, sure.

    However OIIO is far from perfect in all situations (having had to debug and fix issues with its mip-map generation filtering code in the past), so don't always assume that just because there's a mature open source library out there doing something that it's always perfect.