1 /**
2 Module contains color format convertion operations.
3 
4 $(DL Module contains:
5     $(DD 
6             $(LINK2 #rgb2gray,rgb2gray)
7             $(LINK2 #gray2rgb,gray2rgb)
8             $(LINK2 #rgb2hsv,rgb2hsv)
9             $(LINK2 #hsv2rgb,hsv2rgb)
10             $(LINK2 #rgb2yuv,rgb2yuv)
11             $(LINK2 #yuv2rgb,yuv2rgb)
12     )
13 )
14 
15 Copyright: Copyright Relja Ljubobratovic 2016.
16 
17 Authors: Relja Ljubobratovic
18 
19 License: $(LINK3 http://www.boost.org/LICENSE_1_0.txt, Boost Software License - Version 1.0).
20 */
21 
22 module dcv.imgproc.color;
23 
24 /*
25 TODO: redesign functions - one function to iterate, separated format convertions as template alias. 
26 Consider grouping color convertion routines into one function.
27 
28 v0.1 norm:
29 rgb2gray vice versa (done)
30 hsv2rgb -||-
31 hls2rgb -||-
32 lab2rgb -||-
33 luv2rgb -||-
34 luv2rgb -||-
35 bayer2rgb -||-
36 */
37 import std.traits : isFloatingPoint, isNumeric;
38 
39 import mir.math.common : fastmath;
40 
41 import mir.ndslice.slice;
42 import mir.ndslice.topology;
43 import mir.ndslice.algorithm;
44 import mir.ndslice.allocation;
45 
46 import dcv.core.utils;
47 
48 /**
49 RGB to Grayscale convertion strategy.
50 */
51 enum Rgb2GrayConvertion
52 {
53     MEAN, /// Mean the RGB values and assign to gray.
54     LUMINANCE_PRESERVE /// Use luminance preservation (0.2126R + 0.715G + 0.0722B). 
55 }
56 
57 /**
58 Convert RGB image to grayscale.
59 
60 Params:
61     input = Input image. Should have 3 channels, represented as R, G and B
62         respectively in that order.
63     prealloc = Pre-allocated buffer, where grayscale image will be copied. Default
64     argument is an empty slice, where new data is allocated and returned. If given 
65     slice is not of corresponding shape(range.shape[0], range.shape[1]), it is 
66     discarded and allocated anew.
67     conv = Convertion strategy - mean, or luminance preservation.
68 
69 Returns:
70     Returns grayscale version of the given RGB image, of the same size.
71 
72 Note:
73     Input and pre-allocated slices' strides must be identical.
74 */
75 Slice!(SliceKind.contiguous, [2], V*) rgb2gray(V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [2], V*) prealloc = emptySlice!([2], V),
76         Rgb2GrayConvertion conv = Rgb2GrayConvertion.LUMINANCE_PRESERVE) pure nothrow
77 {
78     return rgbbgr2gray!(false, V)(input, prealloc, conv);
79 }
80 
81 unittest
82 {
83     import std.math : approxEqual;
84 
85     auto rgb = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3].sliced(2, 2, 3);
86 
87     auto gray = rgb.rgb2gray;
88     assert(gray.flattened == [0, 1, 2, 3]);
89 }
90 
91 /**
92 Convert BGR image to grayscale.
93 
94 Same as rgb2gray, but follows swapped channels if luminance preservation
95 is chosen as convertion strategy.
96 
97 Params:
98     input = Input image. Should have 3 channels, represented as B, G and R
99         respectively in that order.
100     prealloc = Pre-allocated range, where grayscale image will be copied. Default
101     argument is an empty slice, where new data is allocated and returned. If given 
102     slice is not of corresponding shape(range.shape[0], range.shape[1]), it is 
103     discarded and allocated anew.
104     conv = Convertion strategy - mean, or luminance preservation.
105 
106 Returns:
107     Returns grayscale version of the given BGR image, of the same size.
108 
109 Note:
110     Input and pre-allocated slices' strides must be identical.
111 */
112 Slice!(SliceKind.contiguous, [2], V*) bgr2gray(V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [2], V*) prealloc = emptySlice!([2], V),
113         Rgb2GrayConvertion conv = Rgb2GrayConvertion.LUMINANCE_PRESERVE) pure nothrow
114 {
115     return rgbbgr2gray!(true, V)(input, prealloc, conv);
116 }
117 
118 unittest
119 {
120     import std.math : approxEqual;
121 
122     auto rgb = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3].sliced(2, 2, 3);
123 
124     auto gray = rgb.bgr2gray;
125     assert(gray.flattened == [0, 1, 2, 3]);
126 }
127 
128 private Slice!(SliceKind.contiguous, [2], V*) rgbbgr2gray(bool isBGR, V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [2], V*) prealloc = emptySlice!([2], V),
129         Rgb2GrayConvertion conv = Rgb2GrayConvertion.LUMINANCE_PRESERVE) pure nothrow
130 in
131 {
132     assert(!input.empty, "Input image is empty.");
133 }
134 body
135 {
136     if (prealloc.shape != input.shape[0 .. 2])
137         prealloc = uninitializedSlice!V(input.shape[0 .. 2]);
138 
139     auto rgb = staticPack!3(input);
140 
141     assert(rgb.strides == prealloc.strides,
142             "Input image and pre-allocated buffer strides are not identical.");
143 
144     auto pack = zip!true(rgb, prealloc);
145     alias PT = DeepElementType!(typeof(pack));
146 
147     if (conv == Rgb2GrayConvertion.MEAN)
148         pack.each!(rgb2grayImplMean!PT);
149     else
150         static if (isBGR)
151             pack.each!(bgr2grayImplLuminance!(PT));
152         else
153             pack.each!(rgb2grayImplLuminance!(PT));
154 
155     return prealloc;
156 }
157 
158 /**
159 Convert gray image to RGB.
160 
161 Uses grayscale value and assigns it's value
162 to each of three channels for the RGB image version.
163 
164 Params:
165     input = Grayscale image, to be converted to the RGB.
166     prealloc = Pre-allocated range, where RGB image will be copied. Default
167     argument is an empty slice, where new data is allocated and returned. If given 
168     slice is not of corresponding shape(range.shape[0], range.shape[1], 3), it is 
169     discarded and allocated anew.
170 
171 Returns:
172     Returns RGB version of the given grayscale image.
173 
174 Note:
175     Input and pre-allocated slices' strides must be identical.
176 */
177 Slice!(SliceKind.contiguous, [3], V*) gray2rgb(V)(Slice!(SliceKind.contiguous, [2], V*) input, Slice!(SliceKind.contiguous, [3], V*) prealloc = emptySlice!([3], V)) pure nothrow
178 {
179     Slice!(SliceKind.contiguous, [2], V[3]*) rgb;
180     if (input.shape != prealloc.shape[0 .. 2])
181     {
182         rgb = uninitializedSlice!(V[3])(input.length!0, input.length!1);
183     }
184     else
185     {
186         rgb = staticPack!3(prealloc);
187     }
188 
189     assert(rgb.strides == input.strides,
190             "Input and pre-allocated slices' strides are not identical.");
191 
192     auto pack = zip!true(input, rgb);
193     alias PT = DeepElementType!(typeof(pack));
194 
195     pack.each!(gray2rgbImpl!PT);
196 
197     return staticUnpack!3(rgb);
198 }
199 
200 unittest
201 {
202     import std.math : approxEqual;
203 
204     auto gray = [0, 1, 2, 3].sliced(2, 2);
205 
206     auto rgb = gray.gray2rgb;
207 
208     assert(rgb.flattened == [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3]);
209 }
210 
211 /**
212 Convert RGB image to HSV color format.
213 
214 If HSV is represented as floating point, H is 
215 represented as 0-360 (degrees), S and V are 0.0-1.0.
216 If is integral, S, and V are 0-100.
217 
218 Depending on the RGB (input) type, values are treated in the
219 algorithm to be ranged as 0-255 for ubyte, 0-65535 for ushort, 
220 and 0-1 for floating point types.
221 
222 Params:
223     input = RGB image, which gets converted to HSV.
224     prealloc = Pre-allocated range, where HSV image will be copied. Default
225     argument is an empty slice, where new data is allocated and returned. If given 
226     slice is not of corresponding shape(range.shape[0], range.shape[1], 3), it is 
227     discarded and allocated anew.
228 
229 Returns:
230     Returns HSV verion of the given RGB image.
231 
232 Note:
233     Input and pre-allocated slices' strides must be identical.
234 */
235 Slice!(SliceKind.contiguous, [3], R*) rgb2hsv(R, V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [3], R*) prealloc = emptySlice!([3], R)) pure nothrow
236         if (isNumeric!R && isNumeric!V)
237 in
238 {
239     static assert(R.max >= 360, "Invalid output type for HSV (R.max >= 360)");
240     assert(input.length!2 == 3, "Invalid channel count.");
241 }
242 body
243 {
244     if (prealloc.shape != input.shape)
245         prealloc = uninitializedSlice!R(input.shape);
246 
247     assert(input.strides == prealloc.strides,
248             "Input image and pre-allocated buffer strides are not identical.");
249 
250     auto pack = zip!true(input.staticPack!3, prealloc.staticPack!3);
251     pack.each!(rgb2hsvImpl!(DeepElementType!(typeof(pack))));
252 
253     return prealloc;
254 }
255 
256 unittest
257 {
258     // value comparison based on results from http://www.rapidtables.com/convert/color/rgb-to-hsv.htm
259     auto rgb2hsvTest(RGBType, HSVType)(RGBType[] rgb, HSVType[] expectedHSV)
260     {
261         import std.algorithm.comparison : equal;
262         import std.array : array;
263         import std.math : approxEqual;
264 
265         assert(rgb.sliced(1, 1, 3).rgb2hsv!HSVType.flattened.array.equal!approxEqual(expectedHSV));
266     }
267 
268     rgb2hsvTest(cast(ubyte[])[255, 0, 0], cast(ushort[])[0, 100, 100]);
269     rgb2hsvTest(cast(ubyte[])[255, 0, 0], cast(float[])[0, 1.0f, 1.0f]); // test float result
270 
271     // test same input values as above for 16-bit and 32-bit images
272     rgb2hsvTest(cast(ushort[])[ushort.max, 0, 0], cast(ushort[])[0, 100, 100]);
273     rgb2hsvTest(cast(float[])[1.0f, 0, 0], cast(ushort[])[0, 100, 100]);
274 
275     rgb2hsvTest(cast(ubyte[])[0, 255, 0], cast(ushort[])[120, 100, 100]);
276     rgb2hsvTest(cast(ubyte[])[0, 0, 255], cast(ushort[])[240, 100, 100]);
277     rgb2hsvTest(cast(ubyte[])[122, 158, 200], cast(float[])[212, 0.39, 0.784]);
278 }
279 
280 /**
281 Convert HSV image to RGB color format.
282 
283 If HSV is represented in floating point, H is 0-360 degrees, S and V is 0.0-1.0. 
284 If it's of integral type, S and V values are in 0-100 range.
285 
286 Output range values are based on the output type cast - ubyte will
287 range RGB values to be 0-255, ushort 0-65535, and floating types
288 0.0-1.0. Other types are not supported.
289 
290 Params:
291     input = HSV image, which gets converted to RGB.
292     prealloc = Pre-allocated range, where RGB image will be copied. Default
293     argument is an empty slice, where new data is allocated and returned. If given 
294     slice is not of corresponding shape(range.shape[0], range.shape[1], 3), it is 
295     discarded and allocated anew.
296 
297 Returns:
298     Returns RGB verion of the given HSV image.
299 
300 Note:
301     Input and pre-allocated slices' strides must be identical.
302 */
303 Slice!(SliceKind.contiguous, [3], R*) hsv2rgb(R, V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [3], R*) prealloc = emptySlice!([3], R)) pure nothrow
304         if (isNumeric!R && isNumeric!V)
305 in
306 {
307     assert(input.length!2 == 3, "Invalid channel count.");
308 }
309 body
310 {
311     if (prealloc.shape != input.shape)
312         prealloc = uninitializedSlice!R(input.shape);
313 
314     assert(input.strides == prealloc.strides,
315             "Input image and pre-allocated buffer strides are not identical.");
316 
317     auto pack = zip!true(input.staticPack!3, prealloc.staticPack!3);
318     pack.each!(hsv2rgbImpl!(DeepElementType!(typeof(pack))));
319 
320     return prealloc;
321 }
322 
323 unittest
324 {
325     // value comparison based on results from http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
326     auto hsv2rgbTest(HSVType, RGBType)(HSVType[] hsv, RGBType[] expectedRgb)
327     {
328         import std.algorithm.comparison : equal;
329         import std.array : array;
330         import std.math : approxEqual;
331 
332         assert(hsv.sliced(1, 1, 3).hsv2rgb!RGBType.flattened.array.equal!approxEqual(expectedRgb));
333     }
334 
335     import std.random : uniform;
336 
337     foreach (i; 0 .. 10)
338     {
339         // test any value with value of 0, should give rgb [0, 0, 0]
340         hsv2rgbTest(cast(ushort[])[uniform(0, 359), uniform(0, 99), 0], cast(ubyte[])[0, 0, 0]);
341     }
342 
343     hsv2rgbTest(cast(ushort[])[0, 0, 100], cast(ubyte[])[255, 255, 255]);
344     hsv2rgbTest(cast(ushort[])[150, 50, 100], cast(ubyte[])[128, 255, 191]);
345     hsv2rgbTest(cast(ushort[])[150, 50, 80], cast(ubyte[])[102, 204, 153]);
346 
347     hsv2rgbTest(cast(float[])[0.0f, 0.0f, 1.0f], cast(ubyte[])[255, 255, 255]);
348     hsv2rgbTest(cast(float[])[150.0f, 0.5f, 1.0f], cast(ubyte[])[127, 255, 191]);
349     hsv2rgbTest(cast(float[])[150.0f, 0.5f, 0.8f], cast(ubyte[])[102, 204, 153]);
350 
351     hsv2rgbTest(cast(ushort[])[0, 0, 100], cast(ushort[])[65535, 65535, 65535]);
352     hsv2rgbTest(cast(ushort[])[150, 50, 100], cast(ushort[])[32896, 65535, 49087]);
353     hsv2rgbTest(cast(ushort[])[150, 50, 80], cast(ushort[])[26214, 52428, 39321]);
354 
355     hsv2rgbTest(cast(float[])[0.0f, 0.0f, 1.0f], cast(float[])[1.0f, 1.0f, 1.0f]);
356     hsv2rgbTest(cast(float[])[150.0f, 0.5f, 1.0f], cast(float[])[0.5f, 1.0f, 0.75f]);
357     hsv2rgbTest(cast(float[])[150.0f, 0.5f, 0.8f], cast(float[])[0.4f, 0.8f, 0.6f]);
358 }
359 
360 /**
361 Convert RGB image format to YUV.
362 
363 YUV images in dcv are organized in the same buffer plane
364 where quantity of luma and chroma values are the same (as in
365 YUV444 format).
366 
367 Params:
368     input = Input RGB image.
369     prealloc = Optional pre-allocated buffer. If given, has to be
370         of same shape as input image, otherwise gets reallocated.
371 
372 Returns:
373     Resulting YUV image slice.
374 
375 Note:
376     Input and pre-allocated slices' strides must be identical.
377 */
378 Slice!(SliceKind.contiguous, [3], V*) rgb2yuv(V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [3], V*) prealloc = emptySlice!([3], V)) pure nothrow
379 in
380 {
381     assert(input.length!2 == 3, "Invalid channel count.");
382 }
383 body
384 {
385     if (prealloc.shape != input.shape)
386         prealloc = uninitializedSlice!V(input.shape);
387 
388     assert(input.strides == prealloc.strides,
389             "Input image and pre-allocated buffer strides are not identical.");
390 
391     auto p = zip!true(input, prealloc).pack!1;
392     p.each!(rgb2yuvImpl!(V, DeepElementType!(typeof(p))));
393 
394     return prealloc;
395 }
396 
397 /**
398 Convert YUV image to RGB.
399 
400 As in rgb2yuv conversion, YUV format is considered to have 
401 same amount of luma and chroma.
402 
403 Params:
404     input = Input YUV image.
405     prealloc = Optional pre-allocated buffer. If given, has to be
406         of same shape as input image, otherwise gets reallocated.
407 
408 Returns:
409     Resulting RGB image slice.
410 
411 Note:
412     Input and pre-allocated slices' strides must be identical.
413 */
414 Slice!(SliceKind.contiguous, [3], V*) yuv2rgb(V)(Slice!(SliceKind.contiguous, [3], V*) input, Slice!(SliceKind.contiguous, [3], V*) prealloc = emptySlice!([3], V)) pure nothrow
415 in
416 {
417     assert(input.length!2 == 3, "Invalid channel count.");
418 }
419 body
420 {
421     import mir.ndslice.allocation;
422     if (prealloc.shape != input.shape)
423         prealloc = uninitializedSlice!V(input.shape);
424 
425     assert(input.strides == prealloc.strides,
426             "Input image and pre-allocated buffer strides are not identical.");
427 
428     auto p = zip!true(input, prealloc).pack!1;
429     p.each!(yuv2rgbImpl!(V, DeepElementType!(typeof(p))));
430 
431     return prealloc;
432 }
433 
434 unittest
435 {
436     // test rgb to yuv conversion
437     auto rgb2yuvTest(Type)(Type[] rgb, Type[] expectedYuv)
438     {
439         import std.algorithm.comparison : equal;
440         import std.array : array;
441         import std.math : approxEqual;
442 
443         assert(rgb.sliced(1, 1, 3).rgb2yuv.flattened.array.equal!approxEqual(expectedYuv));
444     }
445 
446     rgb2yuvTest(cast(ubyte[])[0, 0, 0], cast(ubyte[])[16, 128, 128]);
447     rgb2yuvTest(cast(ubyte[])[255, 0, 0], cast(ubyte[])[82, 90, 240]);
448     rgb2yuvTest(cast(ubyte[])[0, 255, 0], cast(ubyte[])[144, 54, 34]);
449     rgb2yuvTest(cast(ubyte[])[0, 0, 255], cast(ubyte[])[41, 240, 110]);
450 }
451 
452 unittest
453 {
454     // test yuv to rgb conversion
455     auto yuv2rgbTest(Type)(Type[] yuv, Type[] expectedRgb)
456     {
457         import std.algorithm.comparison : equal;
458         import std.array : array;
459         import std.math : approxEqual;
460 
461         assert(yuv.sliced(1, 1, 3).yuv2rgb.flattened.array.equal!approxEqual(expectedRgb));
462     }
463 
464     yuv2rgbTest(cast(ubyte[])[16, 128, 128], cast(ubyte[])[0, 0, 0]);
465     yuv2rgbTest(cast(ubyte[])[150, 54, 125], cast(ubyte[])[151, 187, 7]);
466     yuv2rgbTest(cast(ubyte[])[144, 54, 34], cast(ubyte[])[0, 255, 0]);
467     yuv2rgbTest(cast(ubyte[])[41, 240, 110], cast(ubyte[])[0, 0, 255]);
468 }
469 
470 pure @nogc nothrow @fastmath:
471 
472 void rgb2grayImplMean(P)(P pack)
473 {
474     alias V = typeof(pack.b);
475     pack.b = cast(V)((pack.a[0] + pack.a[1] + pack.a[2]) / 3);
476 }
477 
478 void rgb2grayImplLuminance(RGBGRAY)(RGBGRAY pack)
479 {
480     alias V = typeof(pack.b);
481     pack.b = cast(V)(
482             cast(float)pack.a[0] * 0.212642529f +
483             cast(float)pack.a[1] * 0.715143029f +
484             cast(float)pack.a[2] * 0.072214443f
485             );
486 }
487 
488 void bgr2grayImplLuminance(RGBGRAY)(RGBGRAY pack)
489 {
490     alias V = typeof(pack.b);
491     pack.b = cast(V)(
492             cast(float)pack.a[2] * 0.212642529f +
493             cast(float)pack.a[1] * 0.715143029f +
494             cast(float)pack.a[0] * 0.072214443f
495             );
496 }
497 
498 void gray2rgbImpl(GRAYRGB)(GRAYRGB pack)
499 {
500     auto v = pack.a;
501     pack.b[0] = v;
502     pack.b[1] = v;
503     pack.b[2] = v;
504 }
505 
506 void rgb2hsvImpl(RGBHSV)(RGBHSV pack)
507 {
508     import mir.math.common;
509 
510     alias V = typeof(pack.a[0]);
511     alias R = typeof(pack.b[0]);
512 
513     static if (is(V == ubyte))
514     {
515         auto r = cast(float)(pack.a[0]) * (1.0f / 255.0f);
516         auto g = cast(float)(pack.a[1]) * (1.0f / 255.0f);
517         auto b = cast(float)(pack.a[2]) * (1.0f / 255.0f);
518     }
519     else static if (is(V == ushort))
520     {
521         auto r = cast(float)(pack.a[0]) * (1.0f / 65535.0f);
522         auto g = cast(float)(pack.a[1]) * (1.0f / 65535.0f);
523         auto b = cast(float)(pack.a[2]) * (1.0f / 65535.0f);
524     }
525     else static if (isFloatingPoint!V)
526     {
527         // assumes a value range 0-1
528         auto r = cast(float)(pack.a[0]);
529         auto g = cast(float)(pack.a[1]);
530         auto b = cast(float)(pack.a[2]);
531     }
532     else
533     {
534         static assert(0, "Invalid RGB input type: " ~ V.stringof);
535     }
536 
537     auto cmax = fmax(r, fmax(g, b));
538     auto cmin = fmin(r, fmin(g, b));
539     auto cdelta = cmax - cmin; // TODO: compute min and max in a lockstep
540 
541     auto h = cast(R)((cdelta == 0) ? 0 : (cmax == r) ? 60.0f * ((g - b) / cdelta) : (cmax == g)
542             ? 60.0f * ((b - r) / cdelta + 2) : 60.0f * ((r - g) / cdelta + 4));
543 
544     if (h < 0)
545         h += 360;
546 
547     static if (isFloatingPoint!R)
548     {
549         auto s = cast(R)(cmax == 0 ? 0 : cdelta / cmax);
550         auto v = cast(R)(cmax);
551     }
552     else
553     {
554         auto s = cast(R)(100.0f * (cmax == 0 ? 0 : cdelta / cmax));
555         auto v = cast(R)(100.0f * cmax);
556     }
557 
558     pack.b[0] = h;
559     pack.b[1] = s;
560     pack.b[2] = v;
561 }
562 
563 void hsv2rgbImpl(HSVRGB)(HSVRGB pack)
564 {
565     alias V = typeof(pack.a[0]);
566     alias R = typeof(pack.b[0]);
567 
568     static if (isFloatingPoint!V)
569     {
570         auto h = pack.a[0];
571         auto s = pack.a[1];
572         auto v = pack.a[2];
573     }
574     else
575     {
576         float h = cast(float)pack.a[0];
577         float s = cast(float)pack.a[1] * 0.01f;
578         float v = cast(float)pack.a[2] * 0.01f;
579     }
580 
581     if (s <= 0)
582     {
583         static if (isFloatingPoint!R)
584         {
585             pack.b[0] = cast(R)v;
586             pack.b[1] = cast(R)v;
587             pack.b[2] = cast(R)v;
588         }
589         else
590         {
591             pack.b[0] = cast(R)(v * R.max);
592             pack.b[1] = cast(R)(v * R.max);
593             pack.b[2] = cast(R)(v * R.max);
594         }
595         return;
596     }
597 
598     if (v <= 0.0f)
599     {
600         pack.b[0] = cast(R)0;
601         pack.b[1] = cast(R)0;
602         pack.b[2] = cast(R)0;
603         return;
604     }
605 
606     if (h >= 360.0f)
607         h = 0.0f;
608     else
609         h /= 60.0;
610 
611     auto hh = cast(int)h;
612     auto ff = h - float(hh);
613 
614     auto p = v * (1.0f - s);
615     auto q = v * (1.0f - (s * ff));
616     auto t = v * (1.0f - (s * (1.0f - ff)));
617 
618     float r = void;
619     float g = void;
620     float b = void;
621 
622     switch (hh)
623     {
624     case 0:
625         r = v;
626         g = t;
627         b = p;
628         break;
629     case 1:
630         r = q;
631         g = v;
632         b = p;
633         break;
634     case 2:
635         r = p;
636         g = v;
637         b = t;
638         break;
639     case 3:
640         r = p;
641         g = q;
642         b = v;
643         break;
644     case 4:
645         r = t;
646         g = p;
647         b = v;
648         break;
649     case 5:
650     default:
651         r = v;
652         g = p;
653         b = q;
654         break;
655     }
656 
657     static if (isFloatingPoint!R)
658     {
659         pack.b[0] = r;
660         pack.b[1] = g;
661         pack.b[2] = b;
662     }
663     else
664     {
665         pack.b[0] = cast(R)(r * R.max);
666         pack.b[1] = cast(R)(g * R.max);
667         pack.b[2] = cast(R)(b * R.max);
668     }
669 }
670 
671 void rgb2yuvImpl(V, RGBYUV)(RGBYUV pack)
672 {
673     static if (isFloatingPoint!V)
674     {
675         auto r = cast(int)pack[0].a;
676         auto g = cast(int)pack[1].a;
677         auto b = cast(int)pack[2].a;
678         pack[0].b = clip!V((r * .257) + (g * .504) + (b * .098) + 16);
679         pack[1].b = clip!V((r * .439) + (g * .368) + (b * .071) + 128);
680         pack[2].b = clip!V(-(r * .148) - (g * .291) + (b * .439) + 128);
681     }
682     else
683     {
684         auto r = pack[0].a;
685         auto g = pack[1].a;
686         auto b = pack[2].a;
687         pack[0].b = clip!V(((66 * (r) + 129 * (g) + 25 * (b) + 128) >> 8) + 16);
688         pack[1].b = clip!V(((-38 * (r) - 74 * (g) + 112 * (b) + 128) >> 8) + 128);
689         pack[2].b = clip!V(((112 * (r) - 94 * (g) - 18 * (b) + 128) >> 8) + 128);
690     }
691 }
692 
693 void yuv2rgbImpl(V, YUVRGB)(YUVRGB pack)
694 {
695     auto y = cast(int)(pack[0].a) - 16;
696     auto u = cast(int)(pack[1].a) - 128;
697     auto v = cast(int)(pack[2].a) - 128;
698     static if (isFloatingPoint!V)
699     {
700         pack[0].b = clip!V(y + 1.4075 * v);
701         pack[1].b = clip!V(y - 0.3455 * u - (0.7169 * v));
702         pack[2].b = clip!V(y + 1.7790 * u);
703     }
704     else
705     {
706         pack[0].b = clip!V((298 * y + 409 * v + 128) >> 8);
707         pack[1].b = clip!V((298 * y - 100 * u - 208 * v + 128) >> 8);
708         pack[2].b = clip!V((298 * y + 516 * u + 128) >> 8);
709     }
710 }
711