1use crate::{
4 shader::{BindGroupLayoutBuilder, ShaderCompiler, ShaderSource},
5 GpuDevice, Result,
6};
7use bytemuck::{Pod, Zeroable};
8use once_cell::sync::OnceCell;
9use wgpu::{BindGroup, BindGroupLayout, ComputePipeline};
10
11use super::utils;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ColorSpace {
16 BT601,
18 BT709,
20 BT2020,
22}
23
24impl ColorSpace {
25 fn to_format_id(self) -> u32 {
26 match self {
27 Self::BT601 => 0,
28 Self::BT709 => 1,
29 Self::BT2020 => 2,
30 }
31 }
32}
33
34#[repr(C)]
35#[derive(Copy, Clone, Pod, Zeroable)]
36struct ConversionParams {
37 width: u32,
38 height: u32,
39 stride: u32,
40 format: u32,
41}
42
43pub struct ColorSpaceConversion;
45
46impl ColorSpaceConversion {
47 #[allow(clippy::too_many_arguments)]
62 pub fn rgb_to_yuv(
63 device: &GpuDevice,
64 input: &[u8],
65 output: &mut [u8],
66 width: u32,
67 height: u32,
68 color_space: ColorSpace,
69 ) -> Result<()> {
70 utils::validate_dimensions(width, height)?;
71 utils::validate_buffer_size(input, width, height, 4)?;
72 utils::validate_buffer_size(output, width, height, 4)?;
73
74 let pipeline = Self::get_rgb_to_yuv_pipeline(device)?;
75 let layout = Self::get_bind_group_layout(device)?;
76
77 Self::execute_conversion(
78 device,
79 pipeline,
80 layout,
81 input,
82 output,
83 width,
84 height,
85 color_space,
86 )
87 }
88
89 #[allow(clippy::too_many_arguments)]
104 pub fn yuv_to_rgb(
105 device: &GpuDevice,
106 input: &[u8],
107 output: &mut [u8],
108 width: u32,
109 height: u32,
110 color_space: ColorSpace,
111 ) -> Result<()> {
112 utils::validate_dimensions(width, height)?;
113 utils::validate_buffer_size(input, width, height, 4)?;
114 utils::validate_buffer_size(output, width, height, 4)?;
115
116 let pipeline = Self::get_yuv_to_rgb_pipeline(device)?;
117 let layout = Self::get_bind_group_layout(device)?;
118
119 Self::execute_conversion(
120 device,
121 pipeline,
122 layout,
123 input,
124 output,
125 width,
126 height,
127 color_space,
128 )
129 }
130
131 #[allow(clippy::too_many_arguments)]
132 fn execute_conversion(
133 device: &GpuDevice,
134 pipeline: &ComputePipeline,
135 layout: &BindGroupLayout,
136 input: &[u8],
137 output: &mut [u8],
138 width: u32,
139 height: u32,
140 color_space: ColorSpace,
141 ) -> Result<()> {
142 let input_buffer = utils::create_storage_buffer(device, input.len() as u64)?;
144 let output_buffer = utils::create_storage_buffer(device, output.len() as u64)?;
145
146 device.queue().write_buffer(input_buffer.buffer(), 0, input);
148
149 let params = ConversionParams {
151 width,
152 height,
153 stride: width,
154 format: color_space.to_format_id(),
155 };
156 let params_bytes = bytemuck::bytes_of(¶ms);
157 let params_buffer = utils::create_uniform_buffer(device, params_bytes)?;
158
159 let compiler = ShaderCompiler::new(device);
161 let bind_group = compiler.create_bind_group(
162 "ColorSpace Bind Group",
163 layout,
164 &[
165 wgpu::BindGroupEntry {
166 binding: 0,
167 resource: input_buffer.buffer().as_entire_binding(),
168 },
169 wgpu::BindGroupEntry {
170 binding: 1,
171 resource: output_buffer.buffer().as_entire_binding(),
172 },
173 wgpu::BindGroupEntry {
174 binding: 2,
175 resource: params_buffer.buffer().as_entire_binding(),
176 },
177 ],
178 );
179
180 Self::dispatch_compute(device, pipeline, &bind_group, width, height)?;
182
183 let readback_buffer = utils::create_readback_buffer(device, output.len() as u64)?;
185 let mut encoder = device
186 .device()
187 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
188 label: Some("ColorSpace Copy Encoder"),
189 });
190
191 output_buffer.copy_to(&mut encoder, &readback_buffer, 0, 0, output.len() as u64)?;
192
193 device.queue().submit(Some(encoder.finish()));
194 device.wait();
195
196 let result = readback_buffer.read(device, 0, output.len() as u64)?;
197 output.copy_from_slice(&result);
198
199 Ok(())
200 }
201
202 fn dispatch_compute(
203 device: &GpuDevice,
204 pipeline: &ComputePipeline,
205 bind_group: &BindGroup,
206 width: u32,
207 height: u32,
208 ) -> Result<()> {
209 let mut encoder = device
210 .device()
211 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
212 label: Some("ColorSpace Compute Encoder"),
213 });
214
215 {
216 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
217 label: Some("ColorSpace Compute Pass"),
218 timestamp_writes: None,
219 });
220
221 compute_pass.set_pipeline(pipeline);
222 compute_pass.set_bind_group(0, bind_group, &[]);
223
224 let (dispatch_x, dispatch_y) = utils::calculate_dispatch_size(width, height, (16, 16));
225 compute_pass.dispatch_workgroups(dispatch_x, dispatch_y, 1);
226 }
227
228 device.queue().submit(Some(encoder.finish()));
229 Ok(())
230 }
231
232 fn get_bind_group_layout(device: &GpuDevice) -> Result<&'static BindGroupLayout> {
233 static LAYOUT: OnceCell<BindGroupLayout> = OnceCell::new();
234
235 Ok(LAYOUT.get_or_init(|| {
236 let compiler = ShaderCompiler::new(device);
237 let entries = BindGroupLayoutBuilder::new()
238 .add_storage_buffer_read_only(0) .add_storage_buffer(1) .add_uniform_buffer(2) .build();
242
243 compiler.create_bind_group_layout("ColorSpace Bind Group Layout", &entries)
244 }))
245 }
246
247 fn init_pipeline(
248 device: &GpuDevice,
249 name: &str,
250 entry_point: &str,
251 ) -> std::result::Result<ComputePipeline, String> {
252 let compiler = ShaderCompiler::new(device);
253 let shader = compiler
254 .compile(
255 "ColorSpace Shader",
256 ShaderSource::Embedded(crate::shader::embedded::COLORSPACE_SHADER),
257 )
258 .map_err(|e| format!("Failed to compile colorspace shader: {e}"))?;
259
260 let layout = Self::get_bind_group_layout(device)
261 .map_err(|e| format!("Failed to create bind group layout: {e}"))?;
262
263 compiler
264 .create_pipeline(name, &shader, entry_point, layout)
265 .map_err(|e| format!("Failed to create pipeline: {e}"))
266 }
267
268 fn get_rgb_to_yuv_pipeline(device: &GpuDevice) -> Result<&'static ComputePipeline> {
269 static PIPELINE: OnceCell<std::result::Result<ComputePipeline, String>> = OnceCell::new();
270
271 PIPELINE
272 .get_or_init(|| {
273 ColorSpaceConversion::init_pipeline(
274 device,
275 "RGB to YUV Pipeline",
276 "rgb_to_yuv_main",
277 )
278 })
279 .as_ref()
280 .map_err(|e| crate::GpuError::PipelineCreation(e.clone()))
281 }
282
283 fn get_yuv_to_rgb_pipeline(device: &GpuDevice) -> Result<&'static ComputePipeline> {
284 static PIPELINE: OnceCell<std::result::Result<ComputePipeline, String>> = OnceCell::new();
285
286 PIPELINE
287 .get_or_init(|| {
288 ColorSpaceConversion::init_pipeline(
289 device,
290 "YUV to RGB Pipeline",
291 "yuv_to_rgb_main",
292 )
293 })
294 .as_ref()
295 .map_err(|e| crate::GpuError::PipelineCreation(e.clone()))
296 }
297}
298
299impl ColorSpaceConversion {
304 #[must_use]
319 pub fn rgb_to_hsv(data: &[u8], width: u32, height: u32) -> Vec<u8> {
320 let pixel_count = (width as usize) * (height as usize);
321 let mut out = vec![0u8; pixel_count * 4];
322
323 for i in 0..pixel_count {
324 let base = i * 4;
325 if base + 3 >= data.len() {
326 break;
327 }
328 let r = f64::from(data[base]) / 255.0;
329 let g = f64::from(data[base + 1]) / 255.0;
330 let b = f64::from(data[base + 2]) / 255.0;
331 let alpha = data[base + 3];
332
333 let max = r.max(g).max(b);
334 let min = r.min(g).min(b);
335 let delta = max - min;
336
337 let v = max;
338 let s = if max > 0.0 { delta / max } else { 0.0 };
339
340 let h = if delta < 1e-10 {
341 0.0_f64
342 } else if (max - r).abs() < 1e-10 {
343 let sector = (g - b) / delta;
344 let sector = sector - (sector / 6.0).floor() * 6.0;
346 60.0 * sector
347 } else if (max - g).abs() < 1e-10 {
348 60.0 * ((b - r) / delta + 2.0)
349 } else {
350 60.0 * ((r - g) / delta + 4.0)
351 };
352 let h = if h < 0.0 { h + 360.0 } else { h };
353
354 out[base] = (h / 360.0 * 255.0).clamp(0.0, 255.0).round() as u8;
355 out[base + 1] = (s * 255.0).clamp(0.0, 255.0).round() as u8;
356 out[base + 2] = (v * 255.0).clamp(0.0, 255.0).round() as u8;
357 out[base + 3] = alpha;
358 }
359 out
360 }
361
362 #[must_use]
369 pub fn hsv_to_rgb(data: &[u8], width: u32, height: u32) -> Vec<u8> {
370 let pixel_count = (width as usize) * (height as usize);
371 let mut out = vec![0u8; pixel_count * 4];
372
373 for i in 0..pixel_count {
374 let base = i * 4;
375 if base + 3 >= data.len() {
376 break;
377 }
378 let h = f64::from(data[base]) * 360.0 / 255.0; let s = f64::from(data[base + 1]) / 255.0; let v = f64::from(data[base + 2]) / 255.0; let alpha = data[base + 3];
382
383 let c = v * s;
384 let h_prime = h / 60.0;
385 let h_mod2 = h_prime - (h_prime / 2.0).floor() * 2.0;
387 let x = c * (1.0 - (h_mod2 - 1.0).abs());
388 let m = v - c;
389
390 let sector = (h_prime as u32) % 6;
391 let (r1, g1, b1) = match sector {
392 0 => (c, x, 0.0),
393 1 => (x, c, 0.0),
394 2 => (0.0, c, x),
395 3 => (0.0, x, c),
396 4 => (x, 0.0, c),
397 _ => (c, 0.0, x),
398 };
399
400 out[base] = ((r1 + m) * 255.0).clamp(0.0, 255.0).round() as u8;
401 out[base + 1] = ((g1 + m) * 255.0).clamp(0.0, 255.0).round() as u8;
402 out[base + 2] = ((b1 + m) * 255.0).clamp(0.0, 255.0).round() as u8;
403 out[base + 3] = alpha;
404 }
405 out
406 }
407
408 #[must_use]
417 pub fn rgb_to_lab(data: &[u8], width: u32, height: u32) -> Vec<u8> {
418 const XN: f64 = 0.95047;
420 const YN: f64 = 1.00000;
421 const ZN: f64 = 1.08883;
422
423 let pixel_count = (width as usize) * (height as usize);
424 let mut out = vec![0u8; pixel_count * 4];
425
426 for i in 0..pixel_count {
427 let base = i * 4;
428 if base + 3 >= data.len() {
429 break;
430 }
431 let r_lin = Self::srgb_channel_to_linear(f64::from(data[base]) / 255.0);
432 let g_lin = Self::srgb_channel_to_linear(f64::from(data[base + 1]) / 255.0);
433 let b_lin = Self::srgb_channel_to_linear(f64::from(data[base + 2]) / 255.0);
434 let alpha = data[base + 3];
435
436 let x = 0.4124564 * r_lin + 0.3575761 * g_lin + 0.1804375 * b_lin;
438 let y = 0.2126729 * r_lin + 0.7151522 * g_lin + 0.0721750 * b_lin;
439 let z = 0.0193339 * r_lin + 0.1191920 * g_lin + 0.9503041 * b_lin;
440
441 let fx = Self::lab_f(x / XN);
443 let fy = Self::lab_f(y / YN);
444 let fz = Self::lab_f(z / ZN);
445
446 let l_star = 116.0 * fy - 16.0;
447 let a_star = 500.0 * (fx - fy);
448 let b_star = 200.0 * (fy - fz);
449
450 out[base] = (l_star * 255.0 / 100.0).clamp(0.0, 255.0).round() as u8;
452 out[base + 1] = (a_star + 128.0).clamp(0.0, 255.0).round() as u8;
453 out[base + 2] = (b_star + 128.0).clamp(0.0, 255.0).round() as u8;
454 out[base + 3] = alpha;
455 }
456 out
457 }
458
459 #[must_use]
464 pub fn lab_to_rgb(data: &[u8], width: u32, height: u32) -> Vec<u8> {
465 const XN: f64 = 0.95047;
466 const YN: f64 = 1.00000;
467 const ZN: f64 = 1.08883;
468
469 let pixel_count = (width as usize) * (height as usize);
470 let mut out = vec![0u8; pixel_count * 4];
471
472 for i in 0..pixel_count {
473 let base = i * 4;
474 if base + 3 >= data.len() {
475 break;
476 }
477 let l_star = f64::from(data[base]) * 100.0 / 255.0;
478 let a_star = f64::from(data[base + 1]) - 128.0;
479 let b_star = f64::from(data[base + 2]) - 128.0;
480 let alpha = data[base + 3];
481
482 let fy = (l_star + 16.0) / 116.0;
484 let fx = a_star / 500.0 + fy;
485 let fz = fy - b_star / 200.0;
486
487 let x = Self::lab_f_inv(fx) * XN;
488 let y = Self::lab_f_inv(fy) * YN;
489 let z = Self::lab_f_inv(fz) * ZN;
490
491 let r_lin = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z;
493 let g_lin = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z;
494 let b_lin = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z;
495
496 let r_srgb = Self::linear_channel_to_srgb(r_lin);
498 let g_srgb = Self::linear_channel_to_srgb(g_lin);
499 let b_srgb = Self::linear_channel_to_srgb(b_lin);
500
501 out[base] = (r_srgb * 255.0).clamp(0.0, 255.0).round() as u8;
502 out[base + 1] = (g_srgb * 255.0).clamp(0.0, 255.0).round() as u8;
503 out[base + 2] = (b_srgb * 255.0).clamp(0.0, 255.0).round() as u8;
504 out[base + 3] = alpha;
505 }
506 out
507 }
508
509 #[must_use]
514 pub fn srgb_to_linear(data: &[u8], width: u32, height: u32) -> Vec<u8> {
515 let pixel_count = (width as usize) * (height as usize);
516 let mut out = vec![0u8; pixel_count * 4];
517
518 for i in 0..pixel_count {
519 let base = i * 4;
520 if base + 3 >= data.len() {
521 break;
522 }
523 for ch in 0..3 {
524 let c = f64::from(data[base + ch]) / 255.0;
525 let lin = Self::srgb_channel_to_linear(c);
526 out[base + ch] = (lin * 255.0).clamp(0.0, 255.0).round() as u8;
527 }
528 out[base + 3] = data[base + 3];
529 }
530 out
531 }
532
533 #[must_use]
537 pub fn linear_to_srgb(data: &[u8], width: u32, height: u32) -> Vec<u8> {
538 let pixel_count = (width as usize) * (height as usize);
539 let mut out = vec![0u8; pixel_count * 4];
540
541 for i in 0..pixel_count {
542 let base = i * 4;
543 if base + 3 >= data.len() {
544 break;
545 }
546 for ch in 0..3 {
547 let c = f64::from(data[base + ch]) / 255.0;
548 let enc = Self::linear_channel_to_srgb(c);
549 out[base + ch] = (enc * 255.0).clamp(0.0, 255.0).round() as u8;
550 }
551 out[base + 3] = data[base + 3];
552 }
553 out
554 }
555
556 #[inline]
562 fn srgb_channel_to_linear(c: f64) -> f64 {
563 if c <= 0.04045 {
564 c / 12.92
565 } else {
566 ((c + 0.055) / 1.055).powf(2.4)
567 }
568 }
569
570 #[inline]
574 fn linear_channel_to_srgb(c: f64) -> f64 {
575 let c = c.clamp(0.0, 1.0);
576 if c <= 0.0031308 {
577 c * 12.92
578 } else {
579 1.055 * c.powf(1.0 / 2.4) - 0.055
580 }
581 }
582
583 #[inline]
587 fn lab_f(t: f64) -> f64 {
588 if t > 0.008_856 {
590 t.cbrt()
591 } else {
592 7.787 * t + 16.0 / 116.0
593 }
594 }
595
596 #[inline]
598 fn lab_f_inv(t: f64) -> f64 {
599 const DELTA: f64 = 6.0 / 29.0;
601 if t > DELTA {
602 t * t * t
603 } else {
604 3.0 * DELTA * DELTA * (t - 16.0 / 116.0)
605 }
606 }
607}
608
609#[must_use]
618pub fn bt601_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
619 let r = f64::from(r);
620 let g = f64::from(g);
621 let b = f64::from(b);
622 let y = 16.0 + (65.481 * r + 128.553 * g + 24.966 * b) / 255.0;
623 let cb = 128.0 + (-37.797 * r - 74.203 * g + 112.0 * b) / 255.0;
624 let cr = 128.0 + (112.0 * r - 93.786 * g - 18.214 * b) / 255.0;
625 (
626 y.round().clamp(0.0, 255.0) as u8,
627 cb.round().clamp(0.0, 255.0) as u8,
628 cr.round().clamp(0.0, 255.0) as u8,
629 )
630}
631
632#[must_use]
636pub fn bt601_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
637 let y = f64::from(y) - 16.0;
638 let cb = f64::from(cb) - 128.0;
639 let cr = f64::from(cr) - 128.0;
640 let r = 255.0 * (1.164 * y + 1.596 * cr) / 255.0;
641 let g = 255.0 * (1.164 * y - 0.392 * cb - 0.813 * cr) / 255.0;
642 let b = 255.0 * (1.164 * y + 2.017 * cb) / 255.0;
643 (
644 r.round().clamp(0.0, 255.0) as u8,
645 g.round().clamp(0.0, 255.0) as u8,
646 b.round().clamp(0.0, 255.0) as u8,
647 )
648}
649
650#[must_use]
655pub fn bt709_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
656 let r_n = f64::from(r) / 255.0;
659 let g_n = f64::from(g) / 255.0;
660 let b_n = f64::from(b) / 255.0;
661 let y = 16.0 + 219.0 * (0.2126 * r_n + 0.7152 * g_n + 0.0722 * b_n);
662 let cb = 128.0 + 224.0 * (-0.2126 / 1.8556 * r_n - 0.7152 / 1.8556 * g_n + 0.5 * b_n);
663 let cr = 128.0 + 224.0 * (0.5 * r_n - 0.7152 / 1.5748 * g_n - 0.0722 / 1.5748 * b_n);
664 (
665 y.round().clamp(0.0, 255.0) as u8,
666 cb.round().clamp(0.0, 255.0) as u8,
667 cr.round().clamp(0.0, 255.0) as u8,
668 )
669}
670
671#[must_use]
675pub fn bt709_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
676 let y_n = (f64::from(y) - 16.0) / 219.0;
677 let cb_n = (f64::from(cb) - 128.0) / 224.0;
678 let cr_n = (f64::from(cr) - 128.0) / 224.0;
679 let r = y_n + 1.5748 * cr_n;
680 let g = y_n - 0.2126 / 0.7152 * 1.5748 * cr_n - 0.0722 / 0.7152 * 1.8556 * cb_n;
681 let b = y_n + 1.8556 * cb_n;
682 (
683 (r * 255.0).round().clamp(0.0, 255.0) as u8,
684 (g * 255.0).round().clamp(0.0, 255.0) as u8,
685 (b * 255.0).round().clamp(0.0, 255.0) as u8,
686 )
687}
688
689#[must_use]
697pub fn bt2020_rgb_to_ycbcr(r: u8, g: u8, b: u8) -> (u8, u8, u8) {
698 let r_n = f64::from(r) / 255.0;
699 let g_n = f64::from(g) / 255.0;
700 let b_n = f64::from(b) / 255.0;
701 let kr = 0.2627_f64;
703 let kb = 0.0593_f64;
704 let kg = 1.0 - kr - kb; let y = 16.0 + 219.0 * (kr * r_n + kg * g_n + kb * b_n);
706 let cb = 128.0
707 + 224.0 * ((-kr / (2.0 * (1.0 - kb))) * r_n + (-kg / (2.0 * (1.0 - kb))) * g_n + 0.5 * b_n);
708 let cr = 128.0
709 + 224.0 * (0.5 * r_n + (-kg / (2.0 * (1.0 - kr))) * g_n + (-kb / (2.0 * (1.0 - kr))) * b_n);
710 (
711 y.round().clamp(0.0, 255.0) as u8,
712 cb.round().clamp(0.0, 255.0) as u8,
713 cr.round().clamp(0.0, 255.0) as u8,
714 )
715}
716
717#[must_use]
721pub fn bt2020_ycbcr_to_rgb(y: u8, cb: u8, cr: u8) -> (u8, u8, u8) {
722 let y_n = (f64::from(y) - 16.0) / 219.0;
723 let cb_n = (f64::from(cb) - 128.0) / 224.0;
724 let cr_n = (f64::from(cr) - 128.0) / 224.0;
725 let kr = 0.2627_f64;
727 let kb = 0.0593_f64;
728 let kg = 1.0 - kr - kb;
729 let r_cr = 2.0 * (1.0 - kr); let b_cb = 2.0 * (1.0 - kb); let g_cr = -2.0 * kr * (1.0 - kr) / kg;
732 let g_cb = -2.0 * kb * (1.0 - kb) / kg;
733 let r = y_n + r_cr * cr_n;
734 let g = y_n + g_cr * cr_n + g_cb * cb_n;
735 let b = y_n + b_cb * cb_n;
736 (
737 (r * 255.0).round().clamp(0.0, 255.0) as u8,
738 (g * 255.0).round().clamp(0.0, 255.0) as u8,
739 (b * 255.0).round().clamp(0.0, 255.0) as u8,
740 )
741}
742
743#[must_use]
748pub fn pq_oetf(l: f64) -> f64 {
749 const M1: f64 = 0.159_301_758_5;
751 const M2: f64 = 78.843_75;
752 const C1: f64 = 0.835_937_5;
753 const C2: f64 = 18.851_563;
754 const C3: f64 = 18.687_5;
755 let l_m1 = l.abs().powf(M1);
756 ((C1 + C2 * l_m1) / (1.0 + C3 * l_m1)).powf(M2)
757}
758
759#[must_use]
764pub fn pq_eotf(e: f64) -> f64 {
765 const M1: f64 = 0.159_301_758_5;
766 const M2: f64 = 78.843_75;
767 const C1: f64 = 0.835_937_5;
768 const C2: f64 = 18.851_563;
769 const C3: f64 = 18.687_5;
770 let e_m2 = e.abs().powf(1.0 / M2);
771 let num = (e_m2 - C1).max(0.0);
772 let den = C2 - C3 * e_m2;
773 (num / den).powf(1.0 / M1)
774}
775
776#[must_use]
781pub fn hlg_oetf(l: f64) -> f64 {
782 const A: f64 = 0.178_832_77;
783 const B: f64 = 0.284_668_92;
784 const C: f64 = 0.559_910_73;
785 if l <= 1.0 / 12.0 {
786 (3.0 * l).sqrt()
787 } else {
788 A * (12.0 * l - B).ln() + C
789 }
790}
791
792#[must_use]
797pub fn hlg_eotf(e: f64) -> f64 {
798 const A: f64 = 0.178_832_77;
799 const B: f64 = 0.284_668_92;
800 const C: f64 = 0.559_910_73;
801 if e <= 0.5 {
802 e * e / 3.0
803 } else {
804 ((e - C) / A).exp() / 12.0 + B / 12.0
805 }
806}
807
808#[cfg(test)]
813mod tests {
814 use super::*;
815
816 fn approx_eq(a: u8, b: u8, tol: u8, label: &str) {
818 assert!(
819 (a as i32 - b as i32).unsigned_abs() as u8 <= tol,
820 "{label}: got {a}, expected ~{b} (tol={tol})"
821 );
822 }
823
824 #[test]
827 fn test_bt601_white_rgb_to_ycbcr() {
828 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 255, 255);
830 approx_eq(y, 235, 2, "Y for white");
831 approx_eq(cb, 128, 2, "Cb for white");
832 approx_eq(cr, 128, 2, "Cr for white");
833 }
834
835 #[test]
836 fn test_bt601_black_rgb_to_ycbcr() {
837 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 0);
839 approx_eq(y, 16, 2, "Y for black");
840 approx_eq(cb, 128, 2, "Cb for black");
841 approx_eq(cr, 128, 2, "Cr for black");
842 }
843
844 #[test]
845 fn test_bt601_red_rgb_to_ycbcr() {
846 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 0, 0);
848 approx_eq(y, 82, 3, "Y for red");
849 approx_eq(cb, 90, 4, "Cb for red");
850 approx_eq(cr, 240, 4, "Cr for red");
851 }
852
853 #[test]
854 fn test_bt601_green_rgb_to_ycbcr() {
855 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 255, 0);
857 approx_eq(y, 145, 3, "Y for green");
858 approx_eq(cb, 54, 4, "Cb for green");
859 approx_eq(cr, 34, 4, "Cr for green");
860 }
861
862 #[test]
863 fn test_bt601_blue_rgb_to_ycbcr() {
864 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 255);
866 approx_eq(y, 41, 3, "Y for blue");
867 approx_eq(cb, 240, 4, "Cb for blue");
868 approx_eq(cr, 110, 4, "Cr for blue");
869 }
870
871 #[test]
872 fn test_bt601_roundtrip_white() {
873 let (y, cb, cr) = bt601_rgb_to_ycbcr(255, 255, 255);
874 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
875 approx_eq(r, 255, 3, "R roundtrip white");
876 approx_eq(g, 255, 3, "G roundtrip white");
877 approx_eq(b, 255, 3, "B roundtrip white");
878 }
879
880 #[test]
881 fn test_bt601_roundtrip_black() {
882 let (y, cb, cr) = bt601_rgb_to_ycbcr(0, 0, 0);
883 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
884 approx_eq(r, 0, 3, "R roundtrip black");
885 approx_eq(g, 0, 3, "G roundtrip black");
886 approx_eq(b, 0, 3, "B roundtrip black");
887 }
888
889 #[test]
890 fn test_bt601_roundtrip_grey128() {
891 let (y, cb, cr) = bt601_rgb_to_ycbcr(128, 128, 128);
892 let (r, g, b) = bt601_ycbcr_to_rgb(y, cb, cr);
893 approx_eq(r, 128, 4, "R roundtrip grey");
894 approx_eq(g, 128, 4, "G roundtrip grey");
895 approx_eq(b, 128, 4, "B roundtrip grey");
896 }
897
898 #[test]
901 fn test_bt709_white_rgb_to_ycbcr() {
902 let (y, cb, cr) = bt709_rgb_to_ycbcr(255, 255, 255);
903 approx_eq(y, 235, 2, "Y for white BT.709");
904 approx_eq(cb, 128, 2, "Cb for white BT.709");
905 approx_eq(cr, 128, 2, "Cr for white BT.709");
906 }
907
908 #[test]
909 fn test_bt709_black_rgb_to_ycbcr() {
910 let (y, cb, cr) = bt709_rgb_to_ycbcr(0, 0, 0);
911 approx_eq(y, 16, 2, "Y for black BT.709");
912 approx_eq(cb, 128, 2, "Cb for black BT.709");
913 approx_eq(cr, 128, 2, "Cr for black BT.709");
914 }
915
916 #[test]
917 fn test_bt709_red_rgb_to_ycbcr() {
918 let (y, _cb, _cr) = bt709_rgb_to_ycbcr(255, 0, 0);
920 approx_eq(y, 63, 3, "Y for red BT.709");
922 }
923
924 #[test]
925 fn test_bt709_roundtrip_white() {
926 let (y, cb, cr) = bt709_rgb_to_ycbcr(255, 255, 255);
927 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
928 approx_eq(r, 255, 4, "R roundtrip white BT.709");
929 approx_eq(g, 255, 4, "G roundtrip white BT.709");
930 approx_eq(b, 255, 4, "B roundtrip white BT.709");
931 }
932
933 #[test]
934 fn test_bt709_roundtrip_black() {
935 let (y, cb, cr) = bt709_rgb_to_ycbcr(0, 0, 0);
936 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
937 approx_eq(r, 0, 4, "R roundtrip black BT.709");
938 approx_eq(g, 0, 4, "G roundtrip black BT.709");
939 approx_eq(b, 0, 4, "B roundtrip black BT.709");
940 }
941
942 #[test]
943 fn test_bt709_roundtrip_colour() {
944 let (y, cb, cr) = bt709_rgb_to_ycbcr(100, 150, 200);
946 let (r, g, b) = bt709_ycbcr_to_rgb(y, cb, cr);
947 approx_eq(r, 100, 5, "R roundtrip colour BT.709");
948 approx_eq(g, 150, 5, "G roundtrip colour BT.709");
949 approx_eq(b, 200, 5, "B roundtrip colour BT.709");
950 }
951
952 #[test]
955 fn test_bt2020_white_rgb_to_ycbcr() {
956 let (y, cb, cr) = bt2020_rgb_to_ycbcr(255, 255, 255);
957 approx_eq(y, 235, 2, "Y for white BT.2020");
958 approx_eq(cb, 128, 2, "Cb for white BT.2020");
959 approx_eq(cr, 128, 2, "Cr for white BT.2020");
960 }
961
962 #[test]
963 fn test_bt2020_black_rgb_to_ycbcr() {
964 let (y, cb, cr) = bt2020_rgb_to_ycbcr(0, 0, 0);
965 approx_eq(y, 16, 2, "Y for black BT.2020");
966 approx_eq(cb, 128, 2, "Cb for black BT.2020");
967 approx_eq(cr, 128, 2, "Cr for black BT.2020");
968 }
969
970 #[test]
971 fn test_bt2020_red_luma() {
972 let (y, _, _) = bt2020_rgb_to_ycbcr(255, 0, 0);
974 approx_eq(y, 74, 3, "Y for red BT.2020");
975 }
976
977 #[test]
978 fn test_bt2020_roundtrip_white() {
979 let (y, cb, cr) = bt2020_rgb_to_ycbcr(255, 255, 255);
980 let (r, g, b) = bt2020_ycbcr_to_rgb(y, cb, cr);
981 approx_eq(r, 255, 4, "R roundtrip white BT.2020");
982 approx_eq(g, 255, 4, "G roundtrip white BT.2020");
983 approx_eq(b, 255, 4, "B roundtrip white BT.2020");
984 }
985
986 #[test]
987 fn test_bt2020_roundtrip_colour() {
988 let (y, cb, cr) = bt2020_rgb_to_ycbcr(100, 150, 200);
989 let (r, g, b) = bt2020_ycbcr_to_rgb(y, cb, cr);
990 approx_eq(r, 100, 5, "R roundtrip colour BT.2020");
991 approx_eq(g, 150, 5, "G roundtrip colour BT.2020");
992 approx_eq(b, 200, 5, "B roundtrip colour BT.2020");
993 }
994
995 #[test]
998 fn test_pq_oetf_zero() {
999 let v = pq_oetf(0.0);
1001 assert!(v.abs() < 1e-6, "pq_oetf(0) = {v}");
1002 }
1003
1004 #[test]
1005 fn test_pq_oetf_one() {
1006 let v = pq_oetf(1.0);
1008 assert!((v - 1.0).abs() < 1e-4, "pq_oetf(1) = {v}");
1009 }
1010
1011 #[test]
1012 fn test_pq_roundtrip() {
1013 for nits_norm in [0.0, 0.01, 0.1, 0.5, 0.9, 1.0_f64] {
1014 let encoded = pq_oetf(nits_norm);
1015 let decoded = pq_eotf(encoded);
1016 assert!(
1017 (decoded - nits_norm).abs() < 1e-5,
1018 "PQ roundtrip failed at {nits_norm}: got {decoded}"
1019 );
1020 }
1021 }
1022
1023 #[test]
1024 fn test_hlg_oetf_zero() {
1025 let v = hlg_oetf(0.0);
1026 assert!(v.abs() < 1e-6, "hlg_oetf(0) = {v}");
1027 }
1028
1029 #[test]
1030 fn test_hlg_oetf_range() {
1031 for i in 0..=20 {
1033 let l = i as f64 / 20.0;
1034 let e = hlg_oetf(l);
1035 assert!((0.0..=1.0).contains(&e), "hlg_oetf({l}) = {e} out of [0,1]");
1036 }
1037 }
1038
1039 #[test]
1040 fn test_hlg_roundtrip() {
1041 for l in [0.0, 0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 1.0_f64] {
1042 let encoded = hlg_oetf(l);
1043 let decoded = hlg_eotf(encoded);
1044 assert!(
1045 (decoded - l).abs() < 1e-6,
1046 "HLG roundtrip failed at {l}: got {decoded}"
1047 );
1048 }
1049 }
1050
1051 #[test]
1056 fn test_bt601_cpu_vs_reference_batch() {
1057 let colours = [
1058 (255u8, 0u8, 0u8), (0u8, 255u8, 0u8), (0u8, 0u8, 255u8), (255u8, 255u8, 0u8), (128u8, 128u8, 128u8), ];
1064 let expected_y: &[u8] = &[82, 145, 41, 210, 126];
1066 for (i, ((r, g, b), &ey)) in colours.iter().zip(expected_y.iter()).enumerate() {
1067 let (y, _, _) = bt601_rgb_to_ycbcr(*r, *g, *b);
1068 assert!(
1069 (y as i32 - ey as i32).unsigned_abs() <= 3,
1070 "BT.601 Y mismatch for colour {i}: got {y}, expected ~{ey}"
1071 );
1072 }
1073 }
1074
1075 #[test]
1078 fn test_bt2020_vs_bt601_luma_differ_for_red() {
1079 let (y601, _, _) = bt601_rgb_to_ycbcr(255, 0, 0);
1080 let (y2020, _, _) = bt2020_rgb_to_ycbcr(255, 0, 0);
1081 assert_ne!(y601, y2020, "BT.601 and BT.2020 Y for red must differ");
1083 }
1084
1085 #[test]
1088 fn test_grey_axis_chroma_neutral_all_standards() {
1089 for v in [0u8, 64, 128, 192, 255] {
1090 let (_, cb601, cr601) = bt601_rgb_to_ycbcr(v, v, v);
1091 let (_, cb709, cr709) = bt709_rgb_to_ycbcr(v, v, v);
1092 let (_, cb2020, cr2020) = bt2020_rgb_to_ycbcr(v, v, v);
1093 approx_eq(cb601, 128, 2, &format!("Cb BT.601 grey {v}"));
1094 approx_eq(cr601, 128, 2, &format!("Cr BT.601 grey {v}"));
1095 approx_eq(cb709, 128, 2, &format!("Cb BT.709 grey {v}"));
1096 approx_eq(cr709, 128, 2, &format!("Cr BT.709 grey {v}"));
1097 approx_eq(cb2020, 128, 2, &format!("Cb BT.2020 grey {v}"));
1098 approx_eq(cr2020, 128, 2, &format!("Cr BT.2020 grey {v}"));
1099 }
1100 }
1101
1102 #[test]
1107 fn test_bt601_reference_vectors() {
1108 let cases: &[((u8, u8, u8), (u8, u8, u8))] = &[
1110 ((255, 0, 0), (82, 90, 240)), ((0, 255, 0), (145, 54, 34)), ((0, 0, 255), (41, 240, 110)), ((255, 255, 255), (235, 128, 128)), ((0, 0, 0), (16, 128, 128)), ((128, 128, 128), (126, 128, 128)), ];
1117 for &((r, g, b), (ey, ecb, ecr)) in cases {
1118 let (y, cb, cr) = bt601_rgb_to_ycbcr(r, g, b);
1119 approx_eq(y, ey, 3, &format!("Y for ({r},{g},{b}) BT.601"));
1120 approx_eq(cb, ecb, 4, &format!("Cb for ({r},{g},{b}) BT.601"));
1121 approx_eq(cr, ecr, 4, &format!("Cr for ({r},{g},{b}) BT.601"));
1122 }
1123 }
1124
1125 #[test]
1127 fn test_bt709_reference_vectors() {
1128 let cases: &[((u8, u8, u8), (u8, u8, u8))] = &[
1130 ((255, 255, 255), (235, 128, 128)), ((0, 0, 0), (16, 128, 128)), ((255, 0, 0), (63, 102, 240)), ((0, 255, 0), (173, 42, 26)), ((0, 0, 255), (32, 240, 118)), ];
1136 for &((r, g, b), (ey, ecb, ecr)) in cases {
1137 let (y, cb, cr) = bt709_rgb_to_ycbcr(r, g, b);
1138 approx_eq(y, ey, 4, &format!("Y for ({r},{g},{b}) BT.709"));
1139 approx_eq(cb, ecb, 5, &format!("Cb for ({r},{g},{b}) BT.709"));
1140 approx_eq(cr, ecr, 5, &format!("Cr for ({r},{g},{b}) BT.709"));
1141 }
1142 }
1143
1144 #[test]
1147 fn test_bt601_vs_bt709_differ_for_primaries() {
1148 let test_colours = [(255u8, 0, 0), (0, 255, 0), (0, 0, 255)];
1149 for (r, g, b) in test_colours {
1150 let (y601, _, _) = bt601_rgb_to_ycbcr(r, g, b);
1151 let (y709, _, _) = bt709_rgb_to_ycbcr(r, g, b);
1152 assert_ne!(
1153 y601, y709,
1154 "BT.601 and BT.709 Y should differ for ({r},{g},{b})"
1155 );
1156 }
1157 }
1158
1159 #[test]
1161 fn test_bt601_deterministic() {
1162 let (y1, cb1, cr1) = bt601_rgb_to_ycbcr(100, 150, 200);
1163 let (y2, cb2, cr2) = bt601_rgb_to_ycbcr(100, 150, 200);
1164 assert_eq!(y1, y2);
1165 assert_eq!(cb1, cb2);
1166 assert_eq!(cr1, cr2);
1167 }
1168
1169 #[test]
1171 fn test_bt709_batch_roundtrip_within_tolerance() {
1172 let colours = [
1173 (10u8, 20u8, 30u8),
1174 (200, 100, 50),
1175 (64, 128, 192),
1176 (0, 255, 128),
1177 (255, 128, 0),
1178 (77, 77, 77),
1179 ];
1180 for (r, g, b) in colours {
1181 let (y, cb, cr) = bt709_rgb_to_ycbcr(r, g, b);
1182 let (ro, go, bo) = bt709_ycbcr_to_rgb(y, cb, cr);
1183 let dr = (r as i32 - ro as i32).unsigned_abs();
1184 let dg = (g as i32 - go as i32).unsigned_abs();
1185 let db = (b as i32 - bo as i32).unsigned_abs();
1186 assert!(
1187 dr <= 5 && dg <= 5 && db <= 5,
1188 "BT.709 roundtrip ({r},{g},{b}) → ({ro},{go},{bo}): diff=({dr},{dg},{db})"
1189 );
1190 }
1191 }
1192
1193 fn rgba_pixel(r: u8, g: u8, b: u8) -> Vec<u8> {
1197 vec![r, g, b, 255u8]
1198 }
1199
1200 #[test]
1202 fn test_rgb_to_hsv_red() {
1203 let data = rgba_pixel(255, 0, 0);
1204 let out = ColorSpaceConversion::rgb_to_hsv(&data, 1, 1);
1205 assert!(out[0] <= 2, "H for pure red should be ~0, got {}", out[0]);
1207 let diff_s = (out[1] as i32 - 255).unsigned_abs();
1209 assert!(diff_s <= 2, "S for pure red should be ~255, got {}", out[1]);
1210 let diff_v = (out[2] as i32 - 255).unsigned_abs();
1212 assert!(diff_v <= 2, "V for pure red should be ~255, got {}", out[2]);
1213 assert_eq!(out[3], 255);
1215 }
1216
1217 #[test]
1219 fn test_hsv_round_trip() {
1220 let test_colours: &[(u8, u8, u8)] = &[
1221 (255, 0, 0),
1222 (0, 255, 0),
1223 (0, 0, 255),
1224 (128, 64, 192),
1225 (200, 150, 100),
1226 ];
1227 for &(r, g, b) in test_colours {
1228 let data = rgba_pixel(r, g, b);
1229 let hsv = ColorSpaceConversion::rgb_to_hsv(&data, 1, 1);
1230 let rgb = ColorSpaceConversion::hsv_to_rgb(&hsv, 1, 1);
1231 let dr = (r as i32 - rgb[0] as i32).unsigned_abs();
1232 let dg = (g as i32 - rgb[1] as i32).unsigned_abs();
1233 let db = (b as i32 - rgb[2] as i32).unsigned_abs();
1234 assert!(
1235 dr <= 2 && dg <= 2 && db <= 2,
1236 "HSV round-trip ({r},{g},{b}) → ({},{},{}) diff=({dr},{dg},{db})",
1237 rgb[0],
1238 rgb[1],
1239 rgb[2]
1240 );
1241 }
1242 }
1243
1244 #[test]
1250 fn test_rgb_to_lab_gray() {
1251 let data = rgba_pixel(127, 127, 127);
1252 let out = ColorSpaceConversion::rgb_to_lab(&data, 1, 1);
1253
1254 let l_decoded = f64::from(out[0]) * 100.0 / 255.0;
1256 assert!(
1257 (l_decoded - 50.0).abs() < 4.0,
1258 "L* for mid-grey should be ~50, got {l_decoded:.2}"
1259 );
1260
1261 let a_decoded = f64::from(out[1]) - 128.0;
1263 assert!(
1264 a_decoded.abs() < 4.0,
1265 "a* for grey should be ~0, got {a_decoded:.2}"
1266 );
1267
1268 let b_decoded = f64::from(out[2]) - 128.0;
1270 assert!(
1271 b_decoded.abs() < 4.0,
1272 "b* for grey should be ~0, got {b_decoded:.2}"
1273 );
1274 }
1275
1276 #[test]
1280 fn test_srgb_linear_round_trip() {
1281 let test_values: &[u8] = &[0, 10, 30, 64, 100, 128, 180, 200, 230, 255];
1282 for &v in test_values {
1283 let data = vec![v, v, v, 255u8];
1284 let linear = ColorSpaceConversion::srgb_to_linear(&data, 1, 1);
1286 let recovered = ColorSpaceConversion::linear_to_srgb(&linear, 1, 1);
1288 let diff = (v as i32 - recovered[0] as i32).unsigned_abs();
1289 assert!(
1290 diff <= 3,
1291 "sRGB↔Linear round-trip failed for v={v}: recovered={}, diff={diff}",
1292 recovered[0]
1293 );
1294 }
1295 }
1296
1297 #[test]
1299 fn test_srgb_to_linear_monotone() {
1300 let mut prev = 0u8;
1301 for v in 1u8..=255 {
1302 let data = vec![v, v, v, 255u8];
1303 let lin = ColorSpaceConversion::srgb_to_linear(&data, 1, 1);
1304 assert!(
1305 lin[0] >= prev,
1306 "sRGB→Linear not monotone at v={v}: prev={prev}, got={}",
1307 lin[0]
1308 );
1309 prev = lin[0];
1310 }
1311 }
1312}