# HSL->RGB

The text below is based on a SUB2r ISP Excel workbook. That workbook provides a sort of playground where you can try various input data and see the final results, both “precise” and “optimized”1), as well as the discrepancy between the two methods.

## Preface

Much is written and is available on the color space conversion from HSL to RGB (for example this Wikipedia article). However, not only this is not meant to be used with integer arithmetics, but is also very costly due to division operations. And once you realize that a 4K video running at 60FPS needs to do this conversion half a billion times a second(!) you see why this needs to be heavily optimized The below article details the way to perform this conversion without the use of division operations with high enough precision as to satisfy the imaging pipeline quality requirements for the SUB2r camera based on Artix-7 100T FPGA.

## Setup and constraints

The HSL pixels used in SUB2r's imaging pipeline have the following data width components:

• $H$ (hue) is a signed 14-bit field that maps into $[-180°..+180°]$ range
• $S$ (saturation) is an 8-bit unsigned value which maps into $[0\%..100\%]$ range
• $L$ (luminosity) is a 9-bit unsigned value that maps into $[0\%..100\%]$ range, very much like the $saturation$ above

The output of this conversion is an $RGB$ triplet 24-bits wide with 8 bits unsigned value per color channel in range $[0..255]$

## Division-less division

Of course it is mathematically impossible to divide without performing a division. Yet any division by $x$ in math is equivalent to a multiplication by an inverse $^1/_x$ value and provided there exists a trick to “cheaply” calculate the value of $^1/_x$ with sufficient precision it is possible to optimize divisions for a predefined value of a divisor.

Observing that for any value of $x$ the following is true: $\frac{1}{x} = \frac{1*N}{x*N}$ and choosing $N$ such that $x*N$ is a whole power of $2$ we have an optimization where a division is replaced by a pair of a multiplication followed by a (super cheap!) bit-shift operation by $Z$ bits: $\frac{C}{x} = C*\frac{1}{x} = C*\frac{1*N}{x*N}\Bigg|_{\{x*N=2^Z\}} = C*\frac{N}{2^Z} = [(C*N) \gg Z]$ The value $Z$ depends on the needed precision and, of course, the higher the $Z$ the less precision loss there will be in the end.

For example, to divide by $60$ using a “complementary” precision of $14$ bit: $60*N = 2^{14} \implies N = \frac{2^{14}}{60} = 273.06(6) \approx 273$ $\frac{C}{60} \approx C*\frac{273}{2^{14}}$

The last part, losing the 14 LSB, doesn't have to be done right away and with the goal of preserving as much precision as possible this operation can be postponed until later time (just need to remember that the “real” value is now augmented by that $2^{14}$ factor and adjust accordingly).

## Precise calculations

As a refresher this is how Wikipedia lists the way to convert HSL to RGB:

Given a color with hue $H ∈ [0°, 360°]$, saturation $S_{HSL} ∈ [0, 1]$, and lightness $L ∈ [0, 1]$, we first find chroma: $C = (1 - \left\vert 2 L - 1 \right\vert) \times S_{HSL}$

Then we can find a point $(R_1, G_1, B_1)$ along the bottom three faces of the RGB cube, with the same hue and chroma as our color (using the intermediate value $X$ for the second largest component of this color): $H^\prime = \frac{H}{60^\circ}$ $X = C \times (1 - |H^\prime \bmod 2 - 1|)$ $(R_1, G_1, B_1) = \begin{cases} (0, 0, 0) &\text{if } H \text{ is undefined} \\ (C, X, 0) &\text{if } 0 \leq H^\prime \leq 1 \\ (X, C, 0) &\text{if } 1 \leq H^\prime \leq 2 \\ (0, C, X) &\text{if } 2 \leq H^\prime \leq 3 \\ (0, X, C) &\text{if } 3 \leq H^\prime \leq 4 \\ (X, 0, C) &\text{if } 4 \leq H^\prime \leq 5 \\ (C, 0, X) &\text{if } 5 \leq H^\prime \leq 6 \end{cases}$

Overlap (when $H^\prime$ is an integer) occurs because two ways to calculate the value are equivalent: $X = 0$ or $X = C$, as appropriate.

Finally, we can find $R, G, B$ by adding the same amount to each component, to match lightness:

$m=L-C/2$ $(R,G,B)=(R_1+m,G_1+m,B_1+m)$

## Step-by-step algorithm (8x3 bit channel, 24-bit RGB)

Armed with the above information we can now compile the necessary sequence of calculations and format it into an easy-to-use table:

# name math excerpt from Wikipedia bits range C-like code factor2)
1 $h$ $hue(pixel)$ 14
signed
$-8192..+8191$ mapped into $-180^\circ..+180^\circ$
h = pixel.hue();
$\frac{45}{2^{11}}$
2 $s$ $sat(pixel)$ 8 $0..255$
s = pixel.sat();
$1$
3 $l$ $luma(pixel)$ 9 $0..511$ mapped into $0..255$
l = pixel.luma();
$^1/_2$
4 $H$ $(H+360^\circ) \bmod 360^\circ$ 12 $0..4096$ mapped into $0^\circ..+360^\circ$
h1 = (h >> 2);
H = (h1 > 0 ? h1 : h1 + 4096);
$\frac{45}{2^{9}}$
$S$4) $\frac{S}{255}$ $0..1$
$L$5) $\frac{S}{511}$ $0..1$
5 $L^\prime$ $1-|2L-1|$ 9 $0..510$
L_prime = 2^9 - abs(2 * L - 2^9);
$\frac{1}{2^9}$
6 $C$ $L^\prime \times S$ 9 $0..510$
C = ((L_prime * s) >> 8);
$\frac{1}{2^9}$
7 $H^\prime$ $\frac{H}{60^\circ}$ 12 $0..4095$ mapped into $0..6$
Hp = ((H * 273) >> 9);
$\frac{45}{2^{14}}$
8 $H^\prime_2$ $H^\prime \bmod 2$ 10 $0..727$
Hp2 = Hp - (91 * 2^3) * ((Hp * 90) >> 16)
$\frac{45}{2^{14}}$
9 $H^\prime_{2-1}$ $H^\prime \bmod 2 - 1$ 10
signed
$-364..+363$
Hp2m1 = Hp2 - 2^2 * 91;
$\frac{45}{2^{14}}$
11 $H^\prime_{final}$ $1-|H^\prime \bmod 2 - 1|$ 9 $0..364$ mapped into $0..1$
Hpf = 91 * 2^2 - abs(Hp2m1);
$\frac{45}{2^{14}}$
12 $X$ $C \times H^\prime_{final}$ 9 $0..510$
X = ((C * Hpf * 180) >> 16);
$\frac{1}{2^9}$
13 $(R_1,G_1,B_1)$ multi-branch 9 $0..510$
if(Hp < 364*1){
/*(C,X,0)*/
}else if(Hp < 364*2){
/*(X,C,0)*/
}//...

14 $m$ $L - C/2$ 9 $0..510$
m = l - C / 2;
$\frac{1}{2^9}$
15 $(R,G,B)$ $(R_1+m,G_1+m,B_1+m)$ 8 $0..255$
R=R1+m; G=G1+m; B=B1+m;
$1$

## Step-by-step algorithm (10x3 bit channel, 30-bit RGB)

Armed with the above information we can now compile the necessary sequence of calculations and format it into an easy-to-use table:

# name math excerpt from Wikipedia bits range C-like code factor7)
1 $H$ $hue(pixel)$ 14
signed
$-8192..+8191$ mapped into $-180^\circ..+180^\circ$
H = pixel.hue();
$\frac{180}{2^{13}}$
2 $S$ $sat(pixel)$ 9 $0..511$
S = pixel.sat();
$^1/_2$
3 $L$ $luma(pixel)$ 12 $0..4095$ mapped into $0..255$
L = pixel.luma();
$\frac{1}{2^4}$
4 $h$ $(H+360^\circ) \bmod 360^\circ$ 14 $0..16383$ mapped into $0^\circ..+360^\circ$
h = (H > 0 ? H : H + 16383);
$\frac{360}{2^{14}}$
5 $L^\prime$ $1-|2L-1|$ 12
L_prime = 2^12 - abs(2 * L - 2^12);
$\frac{1}{2^{12}}$
6 $C$ $L^\prime \times S$ 21
C = L_prime * s;
$\frac{1}{2^{21}}$
7 $H^\prime$ $\frac{H}{60^\circ}$ 23 $0..2^23-1$ mapped into $0..6$
Hp = H * 273;
$\frac{180}{2^{27}}$
8 $H^\prime_2$ $H^\prime \bmod 2$ 21
Hp2 = Hp - (91 * 2^14) * (Hp * 90) >> 27)
$\frac{180}{2^{27}}$
9 $H^\prime_{2-1}$ $H^\prime \bmod 2 - 1$ 21
signed
Hp2m1 = Hp2 - 2^13 * 91;
$\frac{180}{2^{27}}$
11 $H^\prime_{final}$ $1-|H^\prime \bmod 2 - 1|$ 21
Hpf = 91 * 2^13 - abs(Hp2m1);
$\frac{180}{2^{27}}$
12 $X$ $C \times H^\prime_{final}$ 21
X = ((C * Hpf * 180) >> 27);
$\frac{1}{2^21}$
13 $(R_1,G_1,B_1)$ multi-branch 21
if(Hp < 745472*1){
/*(C,X,0)*/
}else if(Hp < 745472*2){
/*(X,C,0)*/
}//...

14 $m$ $L - C/2$ 12
m = L - (C / 2^9) / 2;
$\frac{1}{2^{12}}$
15 $(R,G,B)$ $(R_1+m,G_1+m,B_1+m)$ 10 $0..1023$
R=(R1/2^9+m)/2^2; G=(G1/2^9+m)/2^2; B=(B1/2^9+m)/2^2;
$1$
1)
lower precision
2) , 7)
multiply by that much to get the “real” value
3)
optimized from
H = ((h > 0 ? h : h + 16384) >> 2);
4) , 5)
not needed for calculations
6)
optimized from
Hp2 = Hp % (2^3 * 273 / 3);
8)
optimized from
Hp2 = Hp % (2^14 * 273 / 3); 