# RGB to YUV to RGB

1. Removing the *4 and /4 scaling, here is what I get when testing from 0 to 255:

Testing round trip...
16777216 colors tested
14133458 colors differed

Testing from 16 to 235 gives:

Testing round trip...
10648000 colors tested
8980712 colors differed
2. That's exactly what you expect to see. Like I said, roughly six RGB colors map to each legal YUV color when using 8 bit limited range YUV.

Consider this one dimensional list of 12 numbers:

Code:
`0 1 2 3 4 5 6 7 8 9 10 11`
Divide each by 6 then truncate the result (for example 11 / 6 = 1.8333... truncated to 1):

Code:
`0 0 0 0 0 0 1 1 1 1 1 1`
They map to only two different values. Now convert them back by multiplying by 6:

Code:
`0 0 0 0 0 0 6 6 6 6 6 6`
Again they map to only two different values, 0 and 6. Five out of every six are inaccurate.

That's what's happening in the conversion of 8 bit RGB to 8 bit limited range YUV, except that it's happening in three dimensions, not one.
3. Agreed there will be some errors, but we still don't know for sure the magnitude of those errors. A few people have made educated guesses.
4. I worked it out in PureBasic. The errors are <= 17, or about 6.6% (17/255).

Code:
```;UPDATED 2/7/2017

OpenConsole()

;BT.709 CONSTANTS
;#Kr = 0.2126: #Kg = 0.7152: #Kb = 0.0722

;REC 601 CONSTANTS
;#KR = 0.299: #KG = 0.587: #KB = 0.114

;NTSC CONSTANTS
;#KR = 3: #KG = 6: #KB = 1

;BT.2020 CONSTANTS
#Kr= 0.2627 : #Kg= 0.678 : #Kb= 0.0593

error = 0
#err = 17

Global Rd.w,Gd.w,Bd.w,R1.w,G1.w,B1.w
Global U.b, V.b, Y.a

Procedure rgb_to_yuv()
Y = #Kr*R1 + #Kg*G1 + #Kb*B1
U = (B1*#Kb - Y) / 2
V = (R1*#Kr - Y) / 2
EndProcedure

Procedure yuv_to_rgb()
Ut.w = U * 2
Vt.w = V * 2
Rd= ((Y+Vt)/#Kr) + 0.5
Gd= (-(Y + Vt + Ut)/#Kg) + 0.5
Bd= ((Y + Ut)/#Kb) + 0.5

If Rd > 255: Rd = 255
ElseIf Rd < 0: Rd = 0
ElseIf Gd > 255: Gd = 255
ElseIf Gd < 0: Gd = 0
ElseIf Bd > 255: Bd = 255
ElseIf Bd < 0: Bd = 0
EndIf
EndProcedure

For R1 = 0 To 255
For G1 = 0 To 255
For B1 = 0 To 255

rgb_to_yuv(): yuv_to_rgb()

If R1-Rd > #err Or Rd-R1 > #err Or G1-Gd > #err Or Gd-G1 > #err Or B1-Bd > #err Or Bd-B1 > #err
error + 1
EndIf

Next
Next
Next

PrintN("Errors: "+Str(error))

Repeat
Delay(100)
Until Inkey() <> ""```
5. The errors drop to <= 9 if BT.601 coefficients are used, or about 3.5% (9/255).
6. Originally Posted by chris319
Agreed there will be some errors, but we still don't know for sure the magnitude of those errors. A few people have made educated guesses.
Of course we know what the errors are. All you have to do is print out the before and after RGB values. In my program the difference is never more than 2 units on each subchannel. The vast majority only differ by 1.
7. Originally Posted by jagabo
In my program the difference is never more than 2 units on each subchannel. The vast majority only differ by 1.
Your program uses 16-bit ints for Y, U and V and gets the errors down to zero. The errors creep in when going from 16 to 8 bits for Y, U and V. As you said,

The issue is that you can't go from 8 bit integer RGB to 8 bit integer YUV and back to 8 bit integer RGB without errors.
Why don't you write this program out in compilable form using 8 bits for Y, U and V, including a main() function? I'm having trouble coverting your C. I can add the part that evaluates the magnitude of the errors.
8. Originally Posted by chris319
Originally Posted by jagabo
In my program the difference is never more than 2 units on each subchannel. The vast majority only differ by 1.
Your program uses 16-bit ints for Y, U and V and gets the errors down to zero.
Just because it uses16 bit shorts to hold the YUV values doesn't mean the YUV values are 16 bit. It uses only 10 of the 16 bits (ie, the values are always between 0 and 1023). If you remove the *4 and /4 (or change them to *1 and /1) then it only uses 8 of the 16 bits.

At 10 bits the round trip is 100 percent lossless. At 8 bits about 5/6 of the RGB values differ from the original. Never by more that 2 units of R, G, and/or B.

Originally Posted by chris319
The issue is that you can't go from 8 bit integer RGB to 8 bit integer YUV and back to 8 bit integer RGB without errors.
Why don't you write this program out in compilable form using 8 bits for Y, U and V, including a main() function? I'm having trouble coverting your C. I can add the part that evaluates the magnitude of the errors.
Because it's already written. All you have to do is remove the *4 and /4.
9. Here is a program written in PureBasic which analyzes the errors in RGB/YUV round-trip conversions. It uses the avisynth code described here: http://avisynth.nl/index.php/Color_conversions

The maximum error for each of the R, G and B channels is +/- 1 for any set of coefficients (601, 709, 2020, NTSC). This is not a count of the errors; it is the maximum deviation from the original RGB values.

Good enough for me.

Code:
```OpenConsole()

;BT.709 CONSTANTS
#Kr = 0.2126: #Kg = 0.7152: #Kb = 0.0722

;REC 601 CONSTANTS
;The Cb And Cr samples are unsigned
;#KR = 0.299: #KG = 0.587: #KB = 0.114

;NTSC CONSTANTS
;#KR = 0.30: #KG = 0.59: #KB = 0.11

;BT.2020 CONSTANTS
;#Kr= 0.2627 : #Kg= 0.678 : #Kb= 0.0593

Maxrederror = 0
Maxgreenerror = 0
Maxblueerror = 0

Global Rd.a,Gd.a,Bd.a,R1.a,G1.a,B1.a
Global U.a, V.a, Y.a

Procedure rgb_to_yuv()
;avisynth code
;http://avisynth.nl/index.php/Color_conversions
Y = #Kr*R1 + #Kg*G1 + #Kb*b1
V = 0.5*R1 - 0.5*G1*#Kg/(1-#Kr) - 0.5*b1*#Kb/(1-#Kr) + 128
U = -0.5*R1*#Kr/(1-#Kb) - 0.5*G1*#Kg/(1-#Kb) + 0.5*B1 + 128
EndProcedure

Procedure yuv_to_rgb()
;avisynth code
;http://avisynth.nl/index.php/Color_conversions
Rd = y + 2*(v-128)*(1-#Kr)
Gd = y - 2*(u-128)*(1-#Kb)*#Kb/#Kg - 2*(v-128)*(1-#Kr)*#Kr/#Kg
Bd = y + 2*(u-128)*(1-#Kb)
EndProcedure

G1=0:B1=0
For R1 = 1 To 254
rgb_to_yuv(): yuv_to_rgb()
If Rd-R1 > maxRedError: maxRedError = Rd-R1:EndIf
If R1-Rd > maxRedError: maxRedError = R1-Rd:EndIf
Next
PrintN("Max red error: "+Str(maxRedError))
PrintN(StrF(maxredError*100 / 235,2)+"%")
Print(Chr(10))

R1=0:B1=0
For G1 = 1 To 254
rgb_to_yuv(): yuv_to_rgb()
If Gd-G1 > maxGreenError: maxGreenError = Gd-G1:EndIf
If G1-Gd > maxGreenError: maxGreenError = G1-Gd:EndIf
Next
PrintN("Max green error: "+Str(maxGreenError))
PrintN(StrF(maxgreenError*100 / 235,2)+"%")
Print(Chr(10))

R1=0:G1=0
For B1 = 1 To 254
rgb_to_yuv(): yuv_to_rgb()
If Bd-B1 > maxBlueError: maxBlueError = Bd-B1:EndIf
If B1-Bd > maxBlueError: maxBlueError = B1-Bd:EndIf
Next
PrintN("Max blue error: "+Str(maxBlueError))
PrintN(StrF(maxblueError*100 / 235,2)+"%")

Repeat
Delay(100)
Until Inkey() <> ""```
10. You only tested pure reds, pure greens, and pure blues. Only 763 of the >16M colors.
11. Good catch. Take a look at this. Here are the results:

Colors tested: 16387064

Max red error: 1
0.39%

Max green error: 1
0.39%

Max blue error: 1
0.39%

Code:
```OpenConsole()

;BT.709 CONSTANTS
#Kr = 0.2126: #Kg = 0.7152: #Kb = 0.0722

;REC 601 CONSTANTS
;The Cb And Cr samples are unsigned
;#KR = 0.299: #KG = 0.587: #KB = 0.114

;NTSC CONSTANTS
;#KR = 0.30: #KG = 0.59: #KB = 0.11

;BT.2020 CONSTANTS
;#Kr= 0.2627 : #Kg= 0.678 : #Kb= 0.0593

Maxrederror = 0
Maxgreenerror = 0
Maxblueerror = 0

Global Rd.a,Gd.a,Bd.a,R1.a,G1.a,B1.a
Global U.a, V.a, Y.a

Procedure rgb_to_yuv()
;avisynth code
;http://avisynth.nl/index.php/Color_conversions
Y = #Kr*R1 + #Kg*G1 + #Kb*b1
V = 0.5*R1 - 0.5*G1*#Kg/(1-#Kr) - 0.5*b1*#Kb/(1-#Kr) + 128
U = -0.5*R1*#Kr/(1-#Kb) - 0.5*G1*#Kg/(1-#Kb) + 0.5*B1 + 128
EndProcedure

Procedure yuv_to_rgb()
;avisynth code
;http://avisynth.nl/index.php/Color_conversions
Rd = y + 2*(v-128)*(1-#Kr)
Gd = y - 2*(u-128)*(1-#Kb)*#Kb/#Kg - 2*(v-128)*(1-#Kr)*#Kr/#Kg
Bd = y + 2*(u-128)*(1-#Kb)
EndProcedure

R1=1:G1=1:B1=1:count = 0

For Rct = 1 To 254
For Gct = 1 To 254
For Bct = 1 To 254

R1 = Rct: G1 = Gct: B1 = Bct
rgb_to_yuv(): yuv_to_rgb()

If Rd-R1 > maxRedError: maxRedError = Rd-R1:EndIf
If R1-Rd > maxRedError: maxRedError = R1-Rd:EndIf

If Gd-G1 > maxGreenError: maxGreenError = Gd-G1:EndIf
If G1-Gd > maxGreenError: maxGreenError = G1-Gd:EndIf

If Bd-B1 > maxBlueError: maxBlueError = Bd-B1:EndIf
If B1-Bd > maxBlueError: maxBlueError = B1-Bd:EndIf

count + 1

Next
Next
Next

PrintN("Colors tested: " + Str(count)+Chr(10))

PrintN("Max red error: "+Str(maxRedError))
PrintN(StrF(maxredError*100 / 254,2)+"%")
Print(Chr(10))

PrintN("Max green error: "+Str(maxGreenError))
PrintN(StrF(maxgreenError*100 / 254,2)+"%")
Print(Chr(10))

PrintN("Max blue error: "+Str(maxBlueError))
PrintN(StrF(maxblueError*100 / 254,2)+"%")

Repeat: Delay(100): Until Inkey() <> ""```
12. I'm not real familiar with Visual Basic but shouldn't you explicitly cast you variables as integers?
13. Originally Posted by jagabo
I'm not real familiar with Visual Basic but shouldn't you explicitly cast you variables as integers?
PureBasic, not Visual Basic.

PureBasic is variable challenged. The only unsigned variables available are 8-bit bytes and 16-bit shorts. All other variables are signed. By default, unless otherwise declared, all variables are 64 bit signed because I'm using the 64-bit version of PB. So maxrederror, maxgreenerror, maxblue error are 64 bits signed. This won't affect the tally of errors, however.

In the lines beginning with "Global" we have Rd.a, etc. The ".a" declares the variable Rd as an unsigned 8-bit byte. The "a" stands for "ascii" as in "ascii character" in the minds of the PureBasic developers. Many PureBasic users have clamored for a full range of unsigned variables but the PB developers can't figure it out.

PB does some behind-the-scenes rounding when going from float to int. What I should do is rewrite this program in Pascal.
14. Same thing in Pascal, same result: +/- 1

Pascal doesn't cut you any slack regarding variable types.

Code:
```const
Kr = 0.30; Kg = 0.59; Kb = 0.11;

var
Y,U,V, R1,G1,B1,Rd,Gd,Bd : byte;
maxrederror : longword = 0;
maxgreenerror : longword = 0;
maxblueerror : longword = 0;
count : longword = 0;

procedure rgb_to_yuv;
begin
(*avisynth code
http://avisynth.nl/index.php/Color_conversions*)
Y := round(Kr*R1 + Kg*G1 + Kb*B1);
V := round(0.5*R1 - 0.5*G1*Kg/(1-Kr) - 0.5*B1*Kb/(1-Kr) + 128);
U := round(-0.5*R1*Kr/(1-Kb) - 0.5*G1*Kg/(1-Kb) + 0.5*B1 + 128);
end;

procedure yuv_to_rgb;
begin
(*avisynth code
http://avisynth.nl/index.php/Color_conversions*)
Rd := round(Y + 2*(v-128)*(1-Kr));
Gd := round(Y - 2*(u-128)*(1-Kb)*Kb/Kg - 2*(v-128)*(1-Kr)*Kr/Kg);
Bd := round(Y + 2*(u-128)*(1-Kb));
end;

begin (*main program*)

for R1 := 1 to 254 do
for G1 := 1 to 254 do
for B1 := 1 to 254 do
begin

rgb_to_yuv; yuv_to_rgb; inc(count);

If Rd-R1 > maxRedError then maxrederror := Rd-R1
else if R1-Rd > maxRedError then maxrederror := R1-Rd;

If Gd-G1 > maxGreenError then maxGreenerror := Gd-G1
else if G1-Gd > maxGreenError then maxGreenerror := G1-Gd;

If Bd-B1 > maxBlueError then maxBlueerror := Bd-B1
else if B1-Bd > maxBlueError then maxBlueerror := B1-Bd;

end;

writeln('Colors tested:  ',count);
writeln('Maximum red error:   ',maxrederror);
writeln('Maximum green error: ',maxGreenerror);
writeln('Maximum blue error:  ',maxBlueerror);

end.```
15. Keep in mind that no production software would use floating point for this. They would use scaled integers or lookup tables.
16. Originally Posted by jagabo
Keep in mind that no production software would use floating point for this. They would use scaled integers or lookup tables.
The coefficients are floating point. The rest are integers. Pascal won't compile the code unless the parts containing floating point (coefficients) are explicitly either rounded or truncated.

If the video is transmitted, Y U and V are transmitted as unsigned bytes.
17. Nobody would use floating point coefficients to do this in production code that works with large amounts of data. They would use scaled integers or table lookups. Integer math is on the order of 10 times faster than floating point. For example:

Code:
`Y := round(Kr*R1 + Kg*G1 + Kb*B1);`
would be performed like this with pc.601 coefficients multiplied by 65536:

Code:
```;#KR = 0.299: #KG = 0.587: #KB = 0.114 each multiplied by 65536
Y := (19595*R1 + 38470*G1 + 7471*B1) / 65536;```
That avoids any floating point math but loses a little accuracy.
18. Avisynth actually uses scaled integer math (convert.h)
Code:
```inline void YUV2RGB(int y, int u, int v, BYTE* out)
{
const int crv = int(1.596*65536+0.5);
const int cgv = int(0.813*65536+0.5);
const int cgu = int(0.391*65536+0.5);
const int cbu = int(2.018*65536+0.5);

int scaled_y = (y - 16) * int((255.0/219.0)*65536+0.5);

out[0] = ScaledPixelClip(scaled_y + (u-128) * cbu);                 // blue
out[1] = ScaledPixelClip(scaled_y - (u-128) * cgu - (v-128) * cgv); // green
out[2] = ScaledPixelClip(scaled_y                 + (v-128) * crv); // red
}

//...

inline int RGB2YUV(int rgb)
{
const int cyb = int(0.114*219/255*65536+0.5);
const int cyg = int(0.587*219/255*65536+0.5);
const int cyr = int(0.299*219/255*65536+0.5);

// y can't overflow
int y = (cyb*(rgb&255) + cyg*((rgb>>8)&255) + cyr*((rgb>>16)&255) + 0x108000) >> 16;
int scaled_y = (y - 16) * int(255.0/219.0*65536+0.5);
int b_y = ((rgb&255) << 16) - scaled_y;
int u = ScaledPixelClip((b_y >> 10) * int(1/2.018*1024+0.5) + 0x800000);
int r_y = (rgb & 0xFF0000) - scaled_y;
int v = ScaledPixelClip((r_y >> 10) * int(1/1.596*1024+0.5) + 0x800000);
return ((y*256+u)*256+v) | (rgb & 0xff000000);
}

//...

static __inline BYTE ScaledPixelClip(int i) {
return PixelClip((i+32768) >> 16);
}```
EDIT note, I don't fully understand this code, which is one reason I was reluctant to submit my own C++ implementation.
19. raffriff: are you able to compile and execute this code? Could you add my checking code and test it for accuracy?
20. Originally Posted by chris319
raffriff: are you able to compile and execute this code?
The code snippet I posted will not compile. It is part of the AviSynth project with many unknown (to me) dependencies.

I have never done it, but compiling the complete AviSynth project is doable with some work:
http://avisynth.nl/index.php/Filter_SDK/Compile_AviSynth
21. It has this ScaledPixelClip() function which I don't understand, either.
22. Oh, now I get it. (internal.h)
Code:
```static __inline BYTE ScaledPixelClip(int i) {
return PixelClip((i+32768) >> 16);
}```
Okay, but what is PixelClip() doing?
Code:
```class _PixelClip {
enum { buffer=320 };
BYTE clip[256+buffer*2];
public:
_PixelClip() {
memset(clip, 0, buffer);
for (int i=0; i<256; ++i) clip[i+buffer] = (BYTE)i;
memset(clip+buffer+256, 255, buffer);
}
BYTE operator()(int i) { return clip[i+buffer]; }
};

extern _PixelClip PixelClip;```
It looks to me like singleton object PixelClip contains an array of 256 BYTEs corresponding to as many ints:
Code:
`for (int i=0; i<256; ++i) clip[i+buffer] = (BYTE)i;`
...so calling
Code:
`return PixelClip((i+32768) >> 16);`
in turn calls PixelClip's member function ()
Code:
`BYTE operator()(int i) { return clip[i+buffer]; }`
And instead of simply doing something like the following 'normal' clipping/typecasting function:
Code:
```static __inline BYTE PixelClip(int i) {
return (i<=0) ? (BYTE)0 : (i>=255) ? (BYTE)255 : (BYTE)i;
}```
They do all ...THAT? I don't get it. Is it supposed to be faster?

I don't see any input validation either. Just access an array with a signed int!
(EDIT there is a buffer of 320 bytes located in front of the array -- protection for out of range array access?)
23. Alright, alright, I hadda do it
Here's the Avisynth code, (using my PixelClip), called by your routine (ported to C++)
Code:
```#define WIN32_LEAN_AND_MEAN		// Exclude rarely-used stuff from Windows headers
#include <windows.h>
#include <limits.h>

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/*
*/
static __inline BYTE PixelClip(int i) {
return (i<=0) ? (BYTE)0 : (i>=255) ? (BYTE)255 : (BYTE)i;
}

/*
http://avisynth2.cvs.sourceforge.net/viewvc/avisynth2/avisynth/src/internal.h?view=markup
*/

static __inline BYTE ScaledPixelClip(int i) {
return PixelClip((i+32768) >> 16);
}

/*
http://avisynth2.cvs.sourceforge.net/viewvc/avisynth2/avisynth/src/convert/convert.h?view=markup
*/
inline void YUV2RGB(int y, int u, int v, BYTE* out)
{
const int crv = int(1.596*65536+0.5);
const int cgv = int(0.813*65536+0.5);
const int cgu = int(0.391*65536+0.5);
const int cbu = int(2.018*65536+0.5);

int scaled_y = (y - 16) * int((255.0/219.0)*65536+0.5);

out[0] = ScaledPixelClip(scaled_y + (u-128) * cbu);                 // blue
out[1] = ScaledPixelClip(scaled_y - (u-128) * cgu - (v-128) * cgv); // green
out[2] = ScaledPixelClip(scaled_y                 + (v-128) * crv); // red
}

inline int RGB2YUV(int rgb)
{
const int cyb = int(0.114*219/255*65536+0.5);
const int cyg = int(0.587*219/255*65536+0.5);
const int cyr = int(0.299*219/255*65536+0.5);

// y can't overflow
int y = (cyb*(rgb&255) + cyg*((rgb>>8)&255) + cyr*((rgb>>16)&255) + 0x108000) >> 16;
int scaled_y = (y - 16) * int(255.0/219.0*65536+0.5);
int b_y = ((rgb&255) << 16) - scaled_y;
int u = ScaledPixelClip((b_y >> 10) * int(1/2.018*1024+0.5) + 0x800000);
int r_y = (rgb & 0xFF0000) - scaled_y;
int v = ScaledPixelClip((r_y >> 10) * int(1/1.596*1024+0.5) + 0x800000);
return ((y*256+u)*256+v) | (rgb & 0xff000000);
}

/*
*/

BYTE Y,U,V, R1,G1,B1, Rd,Gd,Bd;
LONG maxRedError,maxGreenerror,maxBlueError, count;

void rgb_to_yuv()
{
int rgb = ((R1 & 0xff) << 16) + ((G1 & 0xff) << 8) + (B1 & 0xff);
int yuv = RGB2YUV(rgb);
Y = (yuv & 0xff0000) >> 16;
U = (yuv & 0xff00) >> 8;
V = (yuv & 0xff);
}

void yuv_to_rgb()
{
BYTE b[3];
YUV2RGB(Y, U, V, &b[0]);
Rd = b[2];
Gd = b[1];
Bd = b[0];
}

int main(int argc, char **argv)
{
maxRedError=0;
maxGreenerror=0;
maxBlueError=0;
count=0;

for (R1=1; R1<=254; R1++)
{
for (G1=1; G1<=254; G1++)
{
for (B1=1; B1<=254; B1++)
{
rgb_to_yuv();
yuv_to_rgb();
count++;

int t = abs(Rd-R1);
if (t > maxRedError)
maxRedError = t;

t = abs(Gd-G1);
if (t > maxGreenerror)
maxGreenerror = t;

t = abs(Bd-B1);
if (t > maxBlueError)
maxBlueError = t;
}
}
}

printf("Colors tested: %d\n", count);
printf("Maximum red error:  %d\n", maxRedError);
printf("Maximum green error:  %d\n", maxGreenerror);
printf("Maximum blue error:  %d\n", maxBlueError);
}

// (end)```
24. Note that raffriff42 program (and mine earlier) is using limited range YUV (commonly used in all consumer formats like DVD, Blu-ray, broadcast TV, etc.) whereas chris319's program is using full range YUV. Hence the larger errors in riffraff42's program.
25. Raffriff's error is only higher by 1 in the green channel (2 vs 1 in my code).

Code:
```BYTE Y,U,V, R1,G1,B1, Rd,Gd,Bd;

BYTE b[3];```
My code uses unsigned bytes. This would be hard to catch unless you've worked with PureBasic before. The ".a" declaration signifies an unsigned byte. What happens with your code if you make those bytes unsigned?

Mine was never intended to be production code -- just testing.
26. I am having no luck with the YCgCo code described here: https://www.microsoft.com/en-us/research/publication/ycocg-r-a-color-space-with-rgb-re...dynamic-range/

Do either of you spot a problem with my encoding/decoding? I am able to get a passable monochrome image from it.

Code:
```sub rgb_to_yuv ()
Rf = R1 / 255
Gf = G1 / 255
Bf = B1 / 255

Cof = Rf - Bf
Cgf = Gf - (Rf + Bf) / 2 'Gf - 1
Yf = Gf/2 + (Rf + Bf) / 4
end sub

sub yuv_to_rgb()
dim as single temp
Rf = Yf + Cof - Cgf
Gf = Yf + Cgf
Bf = Yf - Cof - Cgf
Rd = Rf * 255
Gd = Gf * 255
Bd = Bf * 255```
27. Originally Posted by chris319
What happens with your code if you make those bytes unsigned?
It works exactly the same. I didn't show the code but I verified the R, G, B, Y, U, and V primaries never exceeded their expected ranges. I used "int" because the original code I was working with could optionally test different bit depths. 8 bit, 9 bit, and 10 bit. Hence it needed to use a bigger type than "unsigned char".
28. My question was directed at raffriff

Statistics