8

In the RGB color model, color values are represented in the 0-255 range. 0 is not a dummy value, so in the 0-255 range, there are 256 colors.

When using HSL (or something similar), I'd expect 50% to correspond to rgb(127,127,127) because 50% of 256 colors is 128, and 128 corresponds to 127 in the 0-indexed 0-255 range. However, that's not the case. 50% gets converted to 128. Why is that?

I came across (only) this post Why the 50% gray's RGB value is 128 instead of 127 in Photoshop?, but the answers are talking about what/how, and not why.

The straightforward method to calculate 50% would be simply to take the half of 255 (max value) and round it (to get an integer), and that seems to be what the common converters doing, but isn't this wrong?

A better approach would be to calculate the range size, take its 50%, and then get the corresponding value in the 0-indexed range

( ((255-0) + 1) * 0.5 ) - 1 + 0 = 127
( ((rangeMax-rangeMin) + 1) * percentage ) - 1 + rangeMin = 127

Not doing this would imply that, from the computer's perspective, 0 is ignored and there are 255 colors, not 256. So 50% of 255 is 127.5, and it gets rounded to 128. Is that the case? If so, that means the percentage values are not exact.

Here's an image to make the case easier to follow:

Colors from 0 to 255


Okay, I noticed this image isn't really explanatory after HolyBlackCat's comments. The 50% is direction dependent. If we're going from 0 to 255, 50% corresponds to 127, but from 255 to 0, it's 128. This is a problem (when the range size is even). Maybe the rouding (up) is there to fix this and make the calculation direction independent. I suppose it's arbitrary at this point whether to round up or down.

1

7 Answers 7

22

Because it is neither... So, someone made a choice.

Probably the choice was made by an ex-grocery store owner, who they were used to rounding numbers up, so the client did not complain that they got their gray number rounded down.

It does not matter. It is a convention. Try rounding a number, for example in Excel and you will get a round number up when the decimal place is .5.

You are focusing on 50% gray, but I am not sure you realize that by changing a scale of 256 values to a 100-value system you have tremendous rounding problems.

1% is 2.55 values. It would make sense you round up the 1% value to a 3 on the hexadecimal scale. With the value of 2% aka 5.1, you probably won't. Let's see how this scale increments. This will be a boring post.

• Column A=% • Column B=0-255 • Column C=Exact values • Column D=Rounded Down • Column E=Rounded Up • Column F=Problem

enter image description here

  • We do not even have 100 options, but 101 options.

  • There are mixed patterns when rounding up or down 4-5, 5-4

My point is that these conversions of scales are not exact, so we simply use a given convention. A grocery store owner thinking about the customer.


A comment you made:

The common approach is to use 255

In programming, the first entry on a sequence of numbers is almost always 0, not 1, so, the maximum value is in fact 255 of a total of 256 options, being 0 the first value.

Why use 255 (the range max) in the calculation? It causes 0 to be ignored.

On the contrary, if we have a total of 256 options (8 bits), when we include 0, the maximum value that can be assigned to those maximum number of options is 255.

If we ignore 0 (which we are not) we would start counting on 1, so the maximum value would be 256. But 0 is so important, meaning we have black.


A late edition-addition.

I was watching the image you posted. You actually acknowledge that the 50% is not where you marked it.

enter image description here

And I think part of the confusion about if 50% is different depending on where you start. It is not. It is still on 127.5. But if you want to split your atom... I mean your scale, you would get two "incomplete" scales and a free neutron... I mean gap, that you obliterate from existence.

enter image description here

But that is not how we round numbers, normally.

6
  • But my point is why use 255 (the range max) in the calculation? It causes 0 to be ignored. That seems like a lazy approach. To get the percentage value, the range size should be used. Let's say the range 150-405 were used to represent 256 colors (150 included and is black). If one takes the 50% of 405 (the range max), it's 202.5 and wrong. The adjusted version (similar to 0-255) would be (405-150)*0.5+150 and gives 277.5 which would get rounded up to 278 (similar to 128) and is wrong. Again, the inital color/value (0 and 150) is ignored. The range size is 256 (405-150+1), and 50% would be 277.
    – akinuri
    Commented Aug 26, 2023 at 10:27
  • After reading the replies, it just seems that the common approach is to use 255 (the range max) in the calculation, and I get that. It's short, simple and easy. However, it's not technically correct. I guess my question is two fold. Why it's done like that, and why we're fine with it (why not fix it)? (My comments are rhetorical. I wanted to expand on my thoughts. I don't expect a reply.)
    – akinuri
    Commented Aug 26, 2023 at 10:27
  • Using a maximum value of 255 is in fact counting 0 as the initial value. Giving a total of 256. I am editing my answer to complement this topic.
    – Rafael
    Commented Aug 26, 2023 at 14:41
  • 1
    @TheEvilMetal You're exactly right – but exactly 50% black/white is the "weird hanging middle value". If there isn't one, then there isn't one.
    – wizzwizz4
    Commented Aug 28, 2023 at 13:47
  • 2
    @akinuri You seem to be asking all over the place why we don't use 256. The reason is because it's the number of values, not the max value. 256 is the total number of indices, if you divide that by 2, you get 128. What is the value of the 1st index? 0. What is the value of the 128th index? 127. It's not a mistake or an oversight, 127 and 128 are both equally viable because 256 is an even number. Commented Aug 28, 2023 at 16:04
12

On a 0–255 scale, 50% is 127.5; this is rounded to 128 by convention. (Some do it because of the half-to-even rounding strategy; I do it because #808080 looks nicer than #7F7F7F.)

Let's consider a simpler example: instead of 8-bit colour, let's have three-value colour. The values are [0, 1, 2]; the middle is 1.

Black, grey, white. The middle is grey.

Consider 11-value colour. Black is 0, white is 10: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. The middle is 5.

But what if there an even number of possibilities; say, 2-bit colour? [0, 1, 2, 3]… what's the middle value? It's somewhere between 1 and 2. We can formalise this intuition of the "middle value" using the arithmetic mean, giving us a "grey point" of 1.5.

Black, dark grey, light grey, white. Neither grey is the 50% grey.

Scale up to 8-bit colour, with possible values [0, 1, 2, …, 254, 255]. The sum is 255×256÷2, so the mean is (255×256÷2)÷256 = 255÷2 = 127.5; but this isn't one of our colours, so we have to pick a different one. The higher the colour depth, the closer this gets to the "middle value".

Of course, perceptual 50% grey is not the same as 127.5 brightness, but that's another story.

4
  • "On a 0–255 scale, 50% is 127.5" 50% of 255? So the range size (256) is not used? This seems wrong to me... Can you check this code/calculation and see if there's anything wrong with it? I've compared my calculation to what I believe the common converters do (round(max*percentage)). I used your examples, and common converters (unexpectedHalf) seem to give wrong results when the range size is even.
    – akinuri
    Commented Aug 26, 2023 at 11:05
  • 2
    @akinuri There is more than one kind of rounding, as some examples: round up, round down, round towards zero, round away from zero (think of negative numbers), and banker's rounding. Commented Aug 26, 2023 at 18:44
  • @akinuri Removing the Math.round call in line 21 of your example makes the "default" column match the procedure described in this answer.
    – wizzwizz4
    Commented Aug 26, 2023 at 23:22
  • 6
    @akinuri wizzwizz4 is right. If you want to find the middle value, what matters is the difference between the bottom value and the top value (which is 255), not the number of values in the range (which is 256). If you take the number of values in the range and divide that by 2, what you get is not the middle value, but rather the number of values in each of the two halves of the range. Commented Aug 28, 2023 at 1:59
3

I'm not a mathematics expert and can't quite explain why, but I think there must be a flaw in your theory... I guess because in Math 0-indexed is kind of the default(?)

A good way to test a formula like yours is to use it on very basic numbers.

So let's imagine a colour system with just three values: White, Gray & Black (0, 1, 2).

Clearly 1 should be at the 50% mark:

2 * 0.5  = 1

Whereas running that through your formula the result makes no sense:

( ((2-0) + 1) * 0.5 ) - 1 + 0 = 0.5
5
  • 1
    It does make sense. It just needs rounding. Since there are 256 (an even number) colors and the example percent value is 50% (half of an even number), the calculation did not need rounding. If the situation/variables change, the calculation might need updating. Also, your 2 * 0.5 = 1 misses the point. You have three colors and you want the 50% (the middle). You shouldn't divide 2, but instead 3... From a more abstract perspective, 50% means 50% of the range size. Where the range starts and ends does/should not matter.
    – akinuri
    Commented Aug 25, 2023 at 15:19
  • 4
    @akinuri On the contrary: because there are an even number of colours, half of them will be on one side of the "grey point" and half of them will be on the other side.
    – wizzwizz4
    Commented Aug 25, 2023 at 21:44
  • @akinuri So you are saying: "3 * 0.5 = 1" ? Or are you saying the middlepoint of white and black is not gray? Commented Aug 26, 2023 at 0:50
  • 1) 50% of 1-100 (100) is 50. The last value of the left values. 0 is not included. 2) 50% of 0-100 (101) is 50.5. This is more like "a little more after the 50th value" since the 0 is also a valid value and is included. 50.5 needs to be shifted by 1 because 0 is included. So it's 49.5. Now you can floor, ceil or round this depending on whatever. I've already added a comment on wizzwizz4's answer, so I don't want to repeat it here.
    – akinuri
    Commented Aug 26, 2023 at 11:18
  • And to specifically address the case in the answer and the replies, my wording in "It just needs rounding." was incomplete/misleading. You should read "rounding" as "adjusting". In the code example I added to wizzwizz4's answer, I adjust the result depending on color scale being even or odd: floor if even, round if odd. So, see the code please.
    – akinuri
    Commented Aug 26, 2023 at 11:31
3

For this application by itself, there isn't a very compelling reason why it should be 128 rather than 127. Both are basically equally wrong, but it doesn't really matter: 50% grey isn't a very meaningful notion anyway, since it refers to a generally nonlinear colour space. E.g. sRGB(128,128,128) actually has a brightness (luminance) of

((128255 + 0.055)/1.055)2.4 ≈ 21.6%,

nowhere near 50%. Whereas true 50% luminance (for a properly calibrated display) occurs instead near sRGB(188,188,188), but that is perceived as lighter than half as bright as full white. The sRGB "half" value does look a lot more half-bright-ish, but it's clear that the human perception doesn't exactly follow sRGB either, and whether 128 or 127 is closer to perceived half-bright can't be said generally.

Comparison chart for perceived and actual brightness

So in that light (pun not intended), it's indeed quite arbitrary to pick 128 rather than 127.

Nevertheless it's not an entirely ad-hoc convention to pick the higher of the two possible rounding values. Whilst brightness is an "unsigned" type of signal, many other signals (e.g. audio) are symmetric around zero, and even pixel values are often processed as signed quantities (with zero as middle grey), in particular for machine learning applications.

Signed values are generally represented in the two's complement system. That means the signed 8-bit value zero has a bit pattern of 00000000, which is the same as the unsigned 8-bit value 128 with the most significant bit flipped. The bit pattern 10000000 corresponds to -128 and 01111111 to +127. So, the standard representation of signed integer values has an asymmetry built in, making the highest positive value slighty smaller than the flipped lowest-negative one. Another interpretation of this for the unsigned case is that the true full range is in fact 0-256, but the brightest value can't actually be represented. That is kind of reasonable for brightness, since the brightest representable value is anyway just an arbitrary cutoff, whereas 0 corresponds at least conceptually to no light at all, a notion fixed by fundamental physics.

0
0

Each component has a value from 0 to 255. 0 equals 0%, 255 equals 100%. 1 equals (100 / 255)%. The number that would be exactly 50% is 127.7, and since the values are integers, it must be rounded. Either to 127 or to 128. People usually use the "round to even rule" where a number exactly between two integers is rounded to an even number, so it's 128.

Since 255 = 3 * 5 * 17, you can get 1/3, 1/5, 1/15, 1/17, 1/51, 1/85 and 1/255 exact. 1/3 = 85, 1/5 = 51, 1/15 = 17, 1/17 = 15, 1/51 = 5, 1/85 = 3, and 1/255 = 1. For example 0% = 0/5 = 0, 20% = 1/5 = 51, 40% = 2/5 = 102, 60% = 153, 80% = 204, 100% = 255.

3
  • 2
    Your math is really strange. How 255/2 = 127.7? How 100/255 is useful? 100/255 = 0.392156862745098
    – Rafael
    Commented Aug 26, 2023 at 0:20
  • 1
    @Rafael, note the percent sign in "100/255 %". One step is 100/255 percent, or 0.39 percent, or 0.0039 of the total. No idea where the 127.7 comes from, though.
    – ilkkachu
    Commented Aug 26, 2023 at 8:00
  • 127.7 is probably a typo or an artifact of premature rounding, 127.5 is the exact value. Commented Aug 28, 2023 at 6:43
0

Color value quantization

If color values were ranged from 0 to 100%, there would be 100 steps of 1/100th of a unit, the first starting from 0 and the last starting from 99. The point value of 100% would need to be (and is) included as the top of the 99th percentile range - because there is no unit range of [100 -101) if 100% is an upper limit.

In terms of number theory, each percentile represents a value range of [n, n+1) where n is an integer, except for the last percentile where the end point (100) in included in the range [99-100].

So a color value of 50% of 100, would refer to the 50th percentile range with 49 percentiles above it, and a value of 0 would refer to the first percentile range [0, 1) with 49 percentiles above it, up to but not including the 50th percentile.

The intensity of a digitised 8 bit color is similarly divided into 256 ranges, starting with [0 - 1) and ending with the inclusive range of [255-256]*. An intensity of 0 represents the start of the lower half of intensity ranges, and an intensity of 128 representing the start of the upper half of the digitized ranges, just as 0% would refer to the start of lower percentile ranges and 50% to the start of the upper half of percentile ranges.

It follows that 128 as an 8 bit value is equivalent to the 50th percentile of a [0-255] range. One source of confusion appears to be treating RGB integer values as discreet values rather than approximating discreet ranges of 1/256 of full color maxima.

Note that in HSL to RGB conversion software, the Hue, Saturation and Lightness values are treated as a members of the real number set, not as integers. User interfaces in graphics programs often only present their values as integers which leads to a whole new set of inaccuracies in forward and backward conversion results.

Output of grey scale values

  • grey scale 0 produces minimum intensity, as in black or 0% lightness,
  • grey scale 255 produces maximum intensity, as in white or 100% lightness.
  • grey scale 128 produces half intensity, as in 50% lightness.

If you compare this with real color values prior to quantization, black contains any shade whose intensity falls within the first 256th of full strength color, and white contains any shade of color whose intensity falls within the last 256th of full strength color.

It now becomes apparent that the output lightness produced is based on the bottom of the sub range of intensities represented by a grey scale tone.

Hence if you build a line of length 256 units where each unit is a sub-range of real valued grey scale or color component tones before quantization, the bottom of the 128th range (numbered from zero) is at the mid point of the line (with length 256) and as a grey tone output at 50% lightness.

Quantizing color or grey scale signals into an integer number of equally spaced levels is not without artifacts.


  • Technical note, an 8 bit analogue to digital converter will not actually output a value of 256, the maximum is encoded in the range represented by 255 by nature of the process.
6
  • RBG steps have never been centered. If they originate from an analogue device that is because ADC (Analog to Digital Converter) hardware converts input values into a numbered sub-range of the ADC's maximum analogue input value (usually a voltage on a circuit board).
    – traktor
    Commented Aug 27, 2023 at 17:57
  • @ilkkachu ... possibly more importantly, if you centered ranges on integral steps, the start and end ranges are only a half step each - which can't be accommodated in an equal step range hardware and software model.
    – traktor
    Commented Aug 27, 2023 at 18:15
  • Yeah, sorry, in effect it's just a difference between a 100-step range and 101-step range. But there's the thing, none of this changes the fact that out of an even number of buckets, there is no middle one. As you said "a color value of 50% of 100, would refer to the 50th percentile range with 49 percentiles above it", but you didn't say that it would also have 50 percentiles below it, thus making it clearly not be a middle one.
    – ilkkachu
    Commented Aug 27, 2023 at 20:04
  • Here, you included each integer value here in the upper bucket, but that choice is entirely arbitrary, you could construct the similar structure with the ranges being [0-1], (1-2], (2-3] ... (99-100] instead. And now the bucket containing 50 would be (49-50] instead, not (50-51].
    – ilkkachu
    Commented Aug 27, 2023 at 20:04
  • @ilkkachu The choice can be seen to be arbitrary due to the width of a point on a line being zero. In practice at least some hardware voltage comparators assign color signals below a reference voltage to a lower bucket and, if not below, the upper bucket. See separate updates in the answer to distinguish between creating RGB values as opposed to displaying them.
    – traktor
    Commented Aug 28, 2023 at 2:05
-3

In the range 0-255, there are 256 values. But both 0 and 255 are actual values, so the real range is -0.5 to 255.5. Halfway between 0 and 255 is (0+255)/2 = 127.5. Since you have to pick a value, and that's literally halfway between 127 and 128, it really doesn't matter which one you choose. Neither is precisely halfway in between.

2
  • 2
    Why would the real range be from -0.5 to 255.5? What would a negative brightness mean?
    – ilkkachu
    Commented Aug 26, 2023 at 17:48
  • I have a feeling that this calculation is based on significant figures, though I don't understand why it's applied in this case. Otherwise, I'm really lost why the real range is -0.5 to 255.5.
    – Andrew T.
    Commented Aug 27, 2023 at 3:47

Not the answer you're looking for? Browse other questions tagged or ask your own question.