2

this is something that has always bugged me when I look at code around the web and in so much of the literature: why do we multiply by 255 and not 256?

sometimes you'll see something like this:

float input = some_function(); // returns 0.0 to 1.0
byte output = input * 255.0;

(i'm assuming that there's an implicit floor going on during the type conversion).

am i not correct in thinking that this is clearly wrong?

consider this:

  • what range of input gives an output of 0 ? (0 -> 1/255], right?
  • what range of input gives an output of 1 ? (1/255 -> 2/255], great!
  • what range of input gives an output of 255 ? only 1.0 does. any significantly smaller value of input will return a lower output.

this means that input is not evently mapped onto the output range.

ok. so you might think: ok use a better rounding function:

byte output = round(input * 255.0);

where round() is the usual mathematical rounding to zero decimal places. but this is still wrong. ask the same questions:

  • what range of input gives an output of 0 ? (0 -> 0.5/255]
  • what range of input gives an output of 1 ? (0.5/255 -> 1.5/255], twice as much as for 0 !
  • what range of input gives an output of 255 ? (254.5/255 -> 1.0), again half as much as for 1

so in this case the input range isn't evenly mapped either!

IMHO. the right way to do this mapping is this:

byte output = min(255, input * 256.0);

again:

  • what range of input gives an output of 0 ? (0 -> 1/256]
  • what range of input gives an output of 1 ? (1/256 -> 2/256]
  • what range of input gives an output of 255 ? (255/256 -> 1.0)

all those ranges are the same size and constitute 1/256th of the input.

i guess my question is this: am i right in considering this a bug, and if so, why is this so prevalent in code?

edit: it looks like i need to clarify. i'm not talking about random numbers here or probability. and i'm not talking about colors or hardware at all. i'm talking about converting a float in the range [0,1] evenly to a byte [0,255] so each range in the input that corresponds to each value in the output is the same size.

Spongman
  • 9,665
  • 8
  • 39
  • 58
  • What about multiplying by 256.0 - ε, where ε is a very small float less than 1? – President James K. Polk Jan 20 '18 at 04:04
  • Have a look at [this answer](https://stackoverflow.com/a/5295202/1377097). Let `min = 0`, `max = 1`, `a = 0` and `b = 255`. (Assuming you're talking about the closed interval `[0,1]` and not `[0,1)`.) – beaker Jan 20 '18 at 22:19
  • @beaker no, that doesn't work, for precisely the reasons i gave in my question above. – Spongman Jan 25 '18 at 02:23
  • @James K Polk yes that would work, although it seems a little 'dirty'. i wasn't so much looking for another solution, more confirmation that 'multiply by 255' _is_ actually a bug. – Spongman Jan 25 '18 at 02:28

2 Answers2

2

You are right. Assuming that valueBetween0and1 can take values 0.0 and 1.0, the "correct" way to do it is something like

byteValue = (byte)(min(255, valueBetween0and1 * 256))

Having said that, one could also argue that the desired quality of the software can vary: does it really matter whether you get 16777216 or 16581375 colors in some throw-away plot? It is one of those "trivial" tasks which is very easy to get wrong by +1/-1. Is it worth it to spend 5 minutes trying to get the 255-th pixel intensity, or can you apply your precious attention elsewhere? It depends on the situation: (byte)(valueBetween0and1 * 255) is a pragmatic solution which is simple, cheap, close enough to the truth, and also immediately, obviously "harmless" in the sense that it definitely won't produce 256 as output. It's not a good solution if you are working on some image manipulation tool like Photoshop or if you are working on some rendering pipeline for a computer game. But it is perfectly acceptable in almost all other contexts. So, whether it is a "bug" or merely a minor improvement proposal depends on the context.

Here is a variant of your problem, which involves random number generators: Generate random numbers in specified range - various cases (int, float, inclusive, exclusive) Notice that e.g. Math.random() in Java or Random.NextDouble in C# return values greater or equal to 0, but strictly smaller than 1.0.

You want the case "Integer-B: [min, max)" (inclusive-exclusive) with min = 0 and max = 256.

If you follow the "recipe" Int-B exactly, you obtain the code:

0 + floor(random() * (256 - 0))

If you remove all the zeros, you are left with just

floor(random() * 256)

and you don't need to & with 0xFF, because you never get 256 (as long as your random number generator guarantees to never return 1).

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
  • `byteValue = 0xFF & floor(valueBetween0and1 * 256)` but this gives the output '0' for input '1'. that can't be right? – Spongman Jan 20 '18 at 01:50
  • Ah, you are right again. It does not distort the distribution in the case of random number generators that might return `1` (the `1` is mapped to `0`, and nothing bad happens). This is of course nonsense if the values aren't random and actually mean something. My fault. Will correct. Thanks. – Andrey Tyukin Jan 20 '18 at 02:10
  • I simply deleted my "alternative" solution with `0xFF & ...`, your solution was the right one right from the beginning. I hope. Unless I made some stupid +-1 error. O_o? – Andrey Tyukin Jan 20 '18 at 02:14
0

I think your question is misled. It looks like you start assuming that there is some "fairness rule" that enforces the "right way" of translation. Unfortunately in practice this is not the case. If you want just generate a random color, then you may use whatever logic fits you. But if you do actual image processing, there is no rule that says the each integer value has to be mapped on the same interval on the float value. On the contrary what you really want is a mapping between two inclusive intervals [0;1] and [0;255]. And often you don't know how many real discretization steps there will be in the [0;1] range down the line when the color is actually shown. (On modern monitors there are probable all 256 different levels for each color but on other output devices there might be significantly less choices and the total number might be not a power of 2). And the real mapping rule is that if for two colors red component values are R1 and R2 then proportion of the actual colors' red component brightness should be as close to R1:R2 as possible. And this rule automatically implies multiply by 255 when you want to map onto [0;255] and thus this is what everybody does.

Note that what you suggest is most probably introducing a bug rather than fixing a bug. For example the proportion rules actually means that you can calculate a mix of two colors R1 and R2 with mixing coefficients k1 and k2 as

Rmix = (k1*R1 + k2*R2)/(k1+k2)

Now let's try to calculate 3:1 mix of 100% Red with 100% Black (i.e. 0% Red) two ways:

  1. using [0-255] integers Rmix = (255*3+1*0)/(3+1) = 191.25 ≈ 191
  2. using [0;1] floating range and then converting it to [0-255] Rmix_float = (1.0*3 + 1*0.0)/(3+1) = 0.75 so Rmix_converted_256 = 256*0.75 = 192.

It means your "multiply by 256" logic has actually introduced inconsistency of different results depending on which scale you use for image processing. Obviously if you used "multiply by 255" logic as everyone else does, you'd get a consistent answer Rmix_converted_255 = 255*0.75 = 191.25 ≈ 191.

SergGr
  • 23,570
  • 2
  • 30
  • 51
  • ok, i think you need to re-read my question again. i'm not talking about random inputs, and i'm not (really) talking about monitors - i'm talking about converting a float [0,1] evenly into the range [0,255].in your answer, what's the size of the input range that gives an output value of '0', and what's the size of the input range that gives an output value of '255' ? – Spongman Jan 25 '18 at 02:26
  • @Spongman, and I think you need to re-read my answer. My point is that the goal is not to have "the same range mapping" the goal is to preserve the crucial quality of linearity that is the basis of any image processing. And you approach breaks linearity as I show in the example at the and of my answer. – SergGr Jan 25 '18 at 12:29
  • i don't see anything in your answer that really addresses any of the points i made in my question regarding rounding, how multiplying by 255 will only assign an epsilon-sized portion of the input to the '255' output value, which is essentially negligible size, and which means that you're effectively only fully representing the values 0-254 in the output if you multiply by 255. it's like saying 1cm = 9mm ! – Spongman Jan 28 '18 at 08:52
  • @Spongman, I glad you brought a `cm`/`mm` example. You can see that in range from `0cm` to `1cm` there are **_11_** different values in whole `mm`s from `0mm` to `10mm`. Still the proper way to convert floatig point value in `cm` into a whole value in `mm` is to do mutliplication by **_10_** and then round. Nobody multiplies by **_11_** to do that although according to your logic using **x10** "_will only assign an epsilon-sized portion of the input_" to the `10mm` output. The reasons why every sane person multiplies by 10 in `cm`=>`mm` is the same whyeverybody multiplies by `255` for colors. – SergGr Jan 28 '18 at 09:04
  • @Spongman, I'm not sure how to restate it differently. The whole point of the answer is that the goal that you declare as your paramount and still defend is not relevant to any practical application. And the goal that is really relevant leads to what everyone else does i.e. to multiplying by 255. – SergGr Jan 28 '18 at 09:11
  • @Spongman, for the sake of argument assume that your idea about bug is wrong. Could you tell me, what kind of an arguments could persuade you in this? If you come to the SO only to find proofs for your idea and disregard any arguments against it, this is what is called "belief". If this is really a scientific question, there must be some arguments/facts that if they are true - they will make you change your mind. So what are those facts/arguments? – SergGr Jan 28 '18 at 16:53
  • Trust me, I am seriously considering your answers. I am more than willing to have my mind changed about this, I just do not find your reasoning to be convincing. There are 10mm in a cm. there are 256 values in a byte. To scale from 1mm to 1cm you multiply by 10. To scale from 1bit to 1byte you multiply by 256. – Spongman Jan 29 '18 at 16:36
  • @Spongman, I don't think our mathematics is the same for the `mm`/`cm` example. There are **11** `mm` values, namely 0,1,2,3,4,5,6,7,8,9,10. **10** is the max available value and it is one less than the number of different values (because of 0). So when you multiply by 10 you effectively multiply by (number_of_values - 1). There is a distinct 0 value in both case. So the same logic applied to byte will generate (256 - 1) = 255. In reverse direction: applying your multiply-by-256 logic, you effectively say that you want to multiply be number_of_values which for `cm`/`mm` translation means 11. – SergGr Jan 29 '18 at 18:17
  • "There are 11 mm values". i don't know about that. every ruler i have looked at there are 10 1mm sections in the 1st centimeter. the 10th mm is the start of the 2nd cm. – Spongman Jan 31 '18 at 01:43
  • here's a pen that demonstrates what i'm talking about: https://codepen.io/Spongman/pen/OQPYRP?editors=0010 there's a red line up the 0'th and 255'th column (first and last). notice how ZERO values are added to the last column after multipliying the 0-1 random number by 255 even though the maximum random number returned is well within the top 1/256th of the range 0-1. – Spongman Jan 31 '18 at 02:13
  • @Spongman, I'm not sure what that example was intended to show. Whatever it was I didn't get it. I just see jumping columns of yellowish green. However there is one thing I see: it is based on random generation. As I said, if your goal is random generation of colors, you might use whatever rules fits your specific target distribution. But if you are in the image-processing business, what you are interested in is linearity which implies the `x255` transformation. – SergGr Jan 31 '18 at 19:40
  • the random number generator is linear. the example is intended to show that the '255' column receives zero hits if you scale a float number in the range 0-1 to a byte by doing `floor(value*255)`. if you have a better way of doing it, please let me know. – Spongman Jan 31 '18 at 19:44
  • @Spongman, I still don't get the point of this example. Nowhere in the actual image processing there is a part with random color generation. The code the uses the `x255` are not about random colors. So why do you think this example is of any relevance? If you want to generate random colors in bytes - just use integer random by mod 256. This is supported in almost any language in the standard library. P.S. By the way in your very first comment you said that "_i'm not talking about random inputs_" but you seem to get back to random colors again and again. – SergGr Jan 31 '18 at 19:46
  • now i think you're just trolling. read the code. the colors aren't random, the input values of the scaling function are random. i used random values in this case since it's an easy way to simply choose a bunch of numbers in the range 0-1 without having to worry about effects of quantization. for the 3rd time: the whole point of that example is that there's ZERO counts in the '255' column. which is the whole point of my original question. – Spongman Jan 31 '18 at 23:14
  • how's this? how many mm in one half a cm? 5mm, right? 5 = 10/2. so how many values in half a byte? what do you get when scaling 0.5 to a byte? i get 128 = 256/2. – Spongman Jan 31 '18 at 23:15
  • @Spongman, 1. every time you go back to the `cm`/`mm` example you never provide any counter-argument to the fact that this is an example for my case because there are **11** different `mm` values and you use multiply by **10** so obviously half is multiply by **(11-1)/2**. 2. Your code has absolutely no comments. I can believe that in your mind it conveys some idea in an obvious way. But for me it just doesn't. – SergGr Jan 31 '18 at 23:25
  • ok, maybe a slightly simpler question. how do you scale (0,1] to a byte? and why doesn't your answer _ever_ give an output value of 255? – Spongman Feb 05 '18 at 17:01
  • @Spongman, the crucial question is "how do I scale to achieve **_what goal(s)_**?". Unfortunately there is no generic scaling that preserves all the good qualities you can imagine. As I said time and again, in many practical applications the most important quality to have is [linearity](https://en.wikipedia.org/wiki/Linearity). This is is why `cm` to `mm` is multiply by 10 which is 1 less than choices count and why color transformation is multiply by 255. You have your own "important property" in mind but you failed to provide any reason why it is of any relevance to image processing. – SergGr Feb 05 '18 at 17:28
  • i don't know why you keep getting hung up on image processing like it's some special snowflake that deserves special consideration. but since you're on the topic, doesn't it concern you that if you're doing all your math in (0-1] space and then scaling the output to 24-bit color, then if you're multiplying by 255 then you're _never_ going to get 0xffffff as an output. seems weird to me. – Spongman Feb 05 '18 at 18:08
  • @Spongman, 1. as you can't have all the good properties you should decide which one to choose. And this is why the context matters. Colors are typically used for image processing. And image processing is actually quite typical in that linearity is the most commonly deserved property among different areas whereas the property you suggest AFAIK is only deserved in random processes. 2. I don't see how your example of `(0, 1]` is relevant. People use inclusive `[0;1]` range for colors and multiplying 1.0 by 255 easily produces 255 bye as an output. – SergGr Feb 05 '18 at 18:22
  • yeah, but 1.0 is the _only_ value that produces 255, whereas a 0-0.0039~ produces the value 0. hardly 'linear' to me. but that's the whole point of my question. if you're looking for linear it seems to me that the _worst_ of the 3 options is '*255'. – Spongman Feb 05 '18 at 18:27
  • @Spongman, 1. If you do rounding as everyone does, it is not **_the only_** although yes, range is smaller. You insist the "the same size of ranges" is the important thing but you didn't provide any example of why it is important for anything relevant for colors context. Could you explicitly provide such an example? 2. Please, open the wiki page on [linearity](https://en.wikipedia.org/wiki/Linearity#In_mathematics) and after reading the definition answer: could you have a scaling that has both it and your property? If yes - how? If no, which one you find really important for colors and why? – SergGr Feb 05 '18 at 18:33
  • when you say 'rounding' what kind of rounding do you mean? floor/ceil/round/banker's ? the range isn't just smaller, it's of size zero +/- epsilon. how would you scale from the range 0-1 to a two bits, which stores 4 values, multiply by 3? i'm really not concerned with colors, but since you brought up image processing it seems to me that a continuous range of inputs should map evenly to the output range. if you quantize the range 0-1 then this should result in evenly sized bands of output. width '*255' you don't - the '255' band is zero with (again, +/- epsilon). – Spongman Feb 06 '18 at 18:45
  • @Spongman Sorry, I shall give up on this discussion. AFAICS your position is that you have that property in your mind but even when explicitly asked you failed to provide the context in which that property is important. The only area that I know where where `x255` logic is typically applied is colors and image processing but you dismiss that as irrelevant and still fail to provide any other area of application. When I turn your `cm/mm` argument as actually an argument for my position, you just ignore it. And when I explicitly point it out and ask for any counter-arguments you still ignore it. – SergGr Feb 06 '18 at 23:14
  • When I ask explicit questions, you ignore them as well. The only way I can sum up is: there is no universal "best" scaling. If for some reason your task lies in a domain where the "even mapping of ranges" is the important property, than yes, multiplying by 255 is the wrong thing to do. In most of the real world contexts that property is not relevant and the really relevant property is linearity which implies the `x255` rule as the correct scaling. With that I wish you good luck and wisdom to know which domain your are working in. – SergGr Feb 06 '18 at 23:14
  • There are several ways of converting 0.0-1.0 to 0-255, and each one has individual properties, and it depend on your individual use case which properties are important. I agree that linearity is a very useful property that shouldn't be sacrificed lightly, but there are application where other properties are more important. There is no absolute "right" or "wrong", and this is a case where it depend on the context what is a bug and what isn't. – Franz D. Jun 17 '21 at 18:15