The sRGB color space is non-linear. Many transformations to and from the space require the RGB components to be mapped to a linear form.
The "official" transformation for each RGB component value in the range [0,1] is:
if (C_srgb <= 0.04045)
C_lin = C_srgb / 12.92;
else
C_lin = pow((C_srgb + 0.055) / 1.055, 2.4);
This is often approximated using the "gamma 2.2" formula:
C_lin_1 = pow(C_srgb, 2.2);
This works fine, but is fairly inaccurate. The graph below uses the right-hand axis for the absolute difference:
For example, if the values are quantized to eight bits, for the sRGB component value 197/255, the linear output is 145/255 instead of 142/255. Can we do better?
In fact, if we simply change the "magic number" to 2.233333... we get better results:
C_lin_2 = pow(C_srgb, 2.233333333);
However, the "pow" functionality is either prohibitively expensive or non-existent on many platforms. So, if we limit ourselves to simple arithmetic, a good approximation I found is the cubic:
C_lin_3 = 0.012522878 * C_srgb +
0.682171111 * C_srgb * C_srgb +
0.305306011 * C_srgb * C_srgb * C_srgb;
This can be computed in HLSL using:
float3 RGB = sRGB * (sRGB * (sRGB * 0.305306011 + 0.682171111) + 0.012522878);
The reverse transformation (from linear to sRGB) is more problematic:
Again, the "official" transformation is piecewise:
if (C_lin <= 0.0031308)
C_srgb = C_lin * 12.92;
else
C_srgb = 1.055 * pow(C_lin, 1.0 / 2.4) - 0.055;
This is usually poorly approximated with the inverse of the computation of "C_lin_1":
C_srgb_1 = pow(C_lin, 0.4545454545);
In fact, the linear portion of the official graph is tiny, so an almost-perfect approximation is:
C_srgb_2 = max(1.055 * pow(C_lin, 0.416666667) - 0.055, 0);
The clamp ("max(..., 0)") is free on many platforms, but the formula does use the "pow" functionality. If we assume we only have square-root operations at our disposal, a good approximation I found was:
C_srgb_3 = 0.585122381 * sqrt(C_lin) +
0.783140355 * sqrt(sqrt(C_lin)) -
0.368262736 * sqrt(sqrt(sqrt(C_lin)));
This can be computed in HLSL using:
float3 S1 = sqrt(RGB);
float3 S2 = sqrt(S1);
float3 S3 = sqrt(S2);
float3 sRGB = 0.585122381 * S1 + 0.783140355 * S2 - 0.368262736 * S3;
An even better approximation (at the cost of an additional 'mad') is:
float3 S1 = sqrt(RGB);
float3 S2 = sqrt(S1);
float3 S3 = sqrt(S2);
float3 sRGB = 0.662002687 * S1 + 0.684122060 * S2 - 0.323583601 * S3 - 0.0225411470 * RGB;
Depending on your platform architecture, this may be faster using multiplication by a constant matrix for the final step.
Monday, 6 August 2012
Sunday, 5 August 2012
RGB/HCY in HLSL
The HCY colour space is a tractable hue/chroma/luminance scheme developed by Kuzma Shapran. It is ideal for pixel shaders, being only slightly more expensive that the HSV and HSL schemes. However, it tries to be more "meaningful" in terms of human perception.
The three components are:
The HLSL conversions are as follows:
I've folded the code into my web page on such conversions here.
The three components are:
- Hue (H) computed in the same manner as HSV and HSL;
- Chroma (C) computed as the scaled difference between the maximum unweighted RGB component and the minimum unweighted RGB component; and
- Luminance (Y) computed as the weighted sum of RGB components.
The HLSL conversions are as follows:
// The weights of RGB contributions to luminance.
// Should sum to unity.
float3 HCYwts = float3(0.299, 0.587, 0.114);
float3 HUEtoRGB(in float H)
{
float R = abs(H * 6 - 3) - 1;
float G = 2 - abs(H * 6 - 2);
float B = 2 - abs(H * 6 - 4);
return saturate(float3(R,G,B));
}
float RGBCVtoHUE(in float3 RGB, in float C, in float V)
{
float3 Delta = (V - RGB) / C;
Delta.rgb -= Delta.brg;
Delta.rgb += float3(2,4,6);
// NOTE 1
Delta.brg = step(V, RGB) * Delta.brg;
float H;
#if NO_ASM
H = max(Delta.r, max(Delta.g, Delta.b));
#else
float4 Delta4 = Delta.rgbr;
asm { max4 H, Delta4 };
#endif
return frac(H / 6);
}
float3 RGBtoHCY(in float3 RGB)
{
float3 HCY = 0;
float U, V;
#if NO_ASM
U = -min(RGB.r, min(RGB.g, RGB.b));
V = max(RGB.r, max(RGB.g, RGB.b));
#else
float4 RGB4 = RGB.rgbr;
asm { max4 U, -RGB4 };
asm { max4 V, RGB4 };
#endif
HCY.y = V + U;
HCY.z = dot(RGB, HCYwts);
if (HCY.y != 0)
{
HCY.x = RGBCVtoHUE(RGB, HCY.y, V);
float Z = dot(HUEtoRGB(HCY.x), HCYwts);
if (HCY.z > Z)
{
HCY.z = 1 - HCY.z;
Z = 1 - Z;
}
HCY.y *= Z / HCY.z;
}
return HCY;
}
float3 HCYtoRGB(in float3 HCY)
{
float RGB = HUEtoRGB(HCY.x);
float Z = dot(RGB, HCYwts);
if (HCY.z < Z)
{
HCY.y *= HCY.z / Z;
}
else if (Z < 1)
{
HCY.y *= (1 - HCY.z) / (1 - Z);
}
return (RGB - Z) * HCY.y + HCY.z;
}
I've folded the code into my web page on such conversions here.
Monday, 25 June 2012
Thursday, 21 June 2012
QR Codes
I've been looking at QR codes recently. I'm intrigued at the idea of being able to customise them by deliberately obscuring sections, in the knowledge that the contents can be reconstructed via error correction. In experimentation, I've created two images that are successfully-decoded URLs:
The latter is more impressive, in my opinion, as it is only Version 2, with low data redundancy.
The latter is more impressive, in my opinion, as it is only Version 2, with low data redundancy.
Sunday, 17 June 2012
Skeleton Alphabet 4
Here are some alternative forms of the lowercase skeletons that only use straight lines and circular arcs.
To remove the nasty corner from the bowl of 'u' on the left, below, follow the red arc for the first half but the green arc for the second.
Similarly for 'n':
The 'm' is just two 'n' letters stuck together, with a small fillet in the middle:
We make the 'l' and 't' narrower:
Open the tail of 'g' and straighten the tail of 'y':
Smooth out the head of 'f' and shorten its crossbar:
We also make the central bars of 'a' and 'e' horizontal:
Alas, the last two changes make the 'a' a little unbalanced and the 'e' a bit wide.
Never mind, we continue by making the 'k' such that the arm and leg are at right angles. Draw the leg first, then fit the arm:
Finally, the 's' needs fettling to only use circular arcs. This turns out to be very tricky. Here's one, less than satisfactory, construction:
That needs a bit more work put into it!
Thus far, the amended skeleton lowercase alphabet looks like this:
The whole set looks like this (badly kerned):
To remove the nasty corner from the bowl of 'u' on the left, below, follow the red arc for the first half but the green arc for the second.
Similarly for 'n':
The 'm' is just two 'n' letters stuck together, with a small fillet in the middle:
We make the 'l' and 't' narrower:
Open the tail of 'g' and straighten the tail of 'y':
Smooth out the head of 'f' and shorten its crossbar:
We also make the central bars of 'a' and 'e' horizontal:
Alas, the last two changes make the 'a' a little unbalanced and the 'e' a bit wide.
Never mind, we continue by making the 'k' such that the arm and leg are at right angles. Draw the leg first, then fit the arm:
Finally, the 's' needs fettling to only use circular arcs. This turns out to be very tricky. Here's one, less than satisfactory, construction:
That needs a bit more work put into it!
Thus far, the amended skeleton lowercase alphabet looks like this:
The whole set looks like this (badly kerned):
Saturday, 16 June 2012
Skeleton Alphabet 3
Now for lowercase letters. We take the unit square (blue) and create a smaller construction motif (green) of size 'x' centred at the bottom:
Ann Camp suggests the x-height to be three-fifths, but if you choose 2/π (about 0.6366) things work out nicely later on (see below).
Ascenders go to the top of the unit square, thereby making them the same height as the capitals:
Similarly, descenders go below the baseline by the same amount:
In the 'g' above, the upper bowl is a circle of diameter one-half. The lower bowl is drawn by eye, according to Ann Camp.
Here are the trivial constructions:
The 'm' and 'w' letters are constructed like two 'n' and 'v' letters glued together. Because of our strange choice of x-height, they are exactly one unit wide:
The upper portion of the 'f' is like the arc of 'r':
The tail of 'j' is a quarter circle:
The crux of 'k' is determined in a similar manner as that of the capital 'K':
The 's' is drawn by eye:
The vertical of 't' is half the height of a standard ascender:
The tail of 'y' is drawn by eye:
All this leads to the following lowercase grid, with shadow glyphs from Arial (pink), Calibri (green) and Lucida (blue):
As with the capitals, there are a few changes I would personally make, by I'll leave that until next time.
Ann Camp suggests the x-height to be three-fifths, but if you choose 2/π (about 0.6366) things work out nicely later on (see below).
Ascenders go to the top of the unit square, thereby making them the same height as the capitals:
Similarly, descenders go below the baseline by the same amount:
In the 'g' above, the upper bowl is a circle of diameter one-half. The lower bowl is drawn by eye, according to Ann Camp.
Here are the trivial constructions:
The 'm' and 'w' letters are constructed like two 'n' and 'v' letters glued together. Because of our strange choice of x-height, they are exactly one unit wide:
The upper portion of the 'f' is like the arc of 'r':
The tail of 'j' is a quarter circle:
The crux of 'k' is determined in a similar manner as that of the capital 'K':
The 's' is drawn by eye:
The vertical of 't' is half the height of a standard ascender:
The tail of 'y' is drawn by eye:
All this leads to the following lowercase grid, with shadow glyphs from Arial (pink), Calibri (green) and Lucida (blue):
As with the capitals, there are a few changes I would personally make, by I'll leave that until next time.
Subscribe to:
Posts (Atom)


















































