photon_rs/
channels.rs

1//! Channel manipulation.
2
3use image::Pixel as OtherPixel;
4
5use image::{GenericImage, GenericImageView};
6
7use crate::helpers;
8use crate::iter::ImageIterator;
9use crate::{PhotonImage, Rgb};
10use palette::{FromColor, IntoColor};
11use palette::{Hue, Lab, Lch, Saturate, Shade, Srgb, Srgba};
12
13#[cfg(feature = "enable_wasm")]
14use wasm_bindgen::prelude::*;
15
16/// Alter a select channel by incrementing or decrementing its value by a constant.
17///
18/// # Arguments
19/// * `img` - A PhotonImage.
20/// * `channel` - The channel you wish to alter, it should be either 0, 1 or 2,
21/// representing R, G, or B respectively. (O=Red, 1=Green, 2=Blue)
22/// * `amount` - The amount to increment/decrement the channel's value by for that pixel.
23/// A positive value will increment/decrement the channel's value, a negative value will decrement the channel's value.
24///
25/// ## Example
26///
27/// ```no_run
28/// // For example, to increase the Red channel for all pixels by 10:
29/// use photon_rs::channels::alter_channel;
30/// use photon_rs::native::{open_image};
31///
32/// let mut img = open_image("img.jpg").expect("File should open");
33/// alter_channel(&mut img, 0_usize, 10_i16);
34/// ```
35///
36/// Adds a constant to a select R, G, or B channel's value.
37///
38/// ### Decrease a channel's value
39/// // For example, to decrease the Green channel for all pixels by 20:
40/// ```no_run
41/// use photon_rs::channels::alter_channel;
42/// use photon_rs::native::open_image;
43///
44/// let mut img = open_image("img.jpg").expect("File should open");
45/// alter_channel(&mut img, 1_usize, -20_i16);
46/// ```
47/// **Note**: Note the use of a minus symbol when decreasing the channel.
48#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
49pub fn alter_channel(img: &mut PhotonImage, channel: usize, amt: i16) {
50    if channel > 2 {
51        panic!("Invalid channel index passed. Channel must be 0, 1, or 2 (Red=0, Green=1, Blue=2)");
52    }
53    if amt > 255 {
54        panic!("Amount to increment/decrement should be between -255 and 255");
55    }
56    let end = img.raw_pixels.len();
57
58    for i in (channel..end).step_by(4) {
59        let inc_val: i16 = img.raw_pixels[i] as i16 + amt;
60        img.raw_pixels[i] = inc_val.clamp(0, 255) as u8;
61    }
62}
63
64/// Increment or decrement every pixel's Red channel by a constant.
65///
66/// # Arguments
67/// * `img` - A PhotonImage. See the PhotonImage struct for details.
68/// * `amt` - The amount to increment or decrement the channel's value by for that pixel.
69///
70/// # Example
71///
72/// ```no_run
73/// // For example, to increase the Red channel for all pixels by 10:
74/// use photon_rs::channels::alter_red_channel;
75/// use photon_rs::native::open_image;
76///
77/// let mut img = open_image("img.jpg").expect("File should open");
78/// alter_red_channel(&mut img, 10_i16);
79/// ```
80#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
81pub fn alter_red_channel(photon_image: &mut PhotonImage, amt: i16) {
82    alter_channel(photon_image, 0, amt)
83}
84
85/// Increment or decrement every pixel's Green channel by a constant.
86///
87/// # Arguments
88/// * `img` - A PhotonImage.
89/// * `amt` - The amount to increment/decrement the channel's value by for that pixel.
90///
91/// # Example
92///
93/// ```no_run
94/// // For example, to increase the Green channel for all pixels by 20:
95/// use photon_rs::channels::alter_green_channel;
96/// use photon_rs::native::open_image;
97///
98/// let mut img = open_image("img.jpg").expect("File should open");
99/// alter_green_channel(&mut img, 20_i16);
100/// ```
101#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
102pub fn alter_green_channel(img: &mut PhotonImage, amt: i16) {
103    alter_channel(img, 1, amt)
104}
105
106/// Increment or decrement every pixel's Blue channel by a constant.
107///
108/// # Arguments
109/// * `img` - A PhotonImage.
110/// * `amt` - The amount to increment or decrement the channel's value by for that pixel.
111///
112/// # Example
113///
114/// ```no_run
115/// // For example, to increase the Blue channel for all pixels by 10:
116/// use photon_rs::channels::alter_blue_channel;
117/// use photon_rs::native::open_image;
118///
119/// let mut img = open_image("img.jpg").expect("File should open");
120/// alter_blue_channel(&mut img, 10_i16);
121/// ```
122#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
123pub fn alter_blue_channel(img: &mut PhotonImage, amt: i16) {
124    alter_channel(img, 2, amt)
125}
126
127/// Increment/decrement two channels' values simultaneously by adding an amt to each channel per pixel.
128///
129/// # Arguments
130/// * `img` - A PhotonImage.
131/// * `channel1` - A usize from 0 to 2 that represents either the R, G or B channels.
132/// * `amt1` - The amount to increment/decrement the channel's value by for that pixel.
133/// * `channel2` -A usize from 0 to 2 that represents either the R, G or B channels.
134/// * `amt2` - The amount to increment/decrement the channel's value by for that pixel.
135///
136/// # Example
137///
138/// ```no_run
139/// // For example, to increase the values of the Red and Blue channels per pixel:
140/// use photon_rs::channels::alter_two_channels;
141/// use photon_rs::native::open_image;
142///
143/// let mut img = open_image("img.jpg").expect("File should open");
144/// alter_two_channels(&mut img, 0_usize, 10_i16, 2_usize, 20_i16);
145/// ```
146#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
147pub fn alter_two_channels(
148    img: &mut PhotonImage,
149    channel1: usize,
150    amt1: i16,
151    channel2: usize,
152    amt2: i16,
153) {
154    if channel1 > 2 {
155        panic!("Invalid channel index passed. Channel1 must be equal to 0, 1, or 2.");
156    }
157    if channel2 > 2 {
158        panic!("Invalid channel index passed. Channel2 must be equal to 0, 1, or 2");
159    }
160    if amt1 > 255 {
161        panic!("Amount to inc/dec channel by should be between -255 and 255");
162    }
163    if amt2 > 255 {
164        panic!("Amount to inc/dec channel by should be between -255 and 255");
165    }
166    let end = img.raw_pixels.len();
167
168    for i in (0..end).step_by(4) {
169        let inc_val1: i16 = img.raw_pixels[i + channel1] as i16 + amt1;
170        let inc_val2: i16 = img.raw_pixels[i + channel2] as i16 + amt2;
171
172        img.raw_pixels[i + channel1] = inc_val1.clamp(0, 255) as u8;
173        img.raw_pixels[i + channel2] = inc_val2.clamp(0, 255) as u8;
174    }
175}
176
177/// Increment all 3 channels' values by adding an amt to each channel per pixel.
178///
179/// # Arguments
180/// * `img` - A PhotonImage.
181/// * `r_amt` - The amount to increment/decrement the Red channel by.
182/// * `g_amt` - The amount to increment/decrement the Green channel by.
183/// * `b_amt` - The amount to increment/decrement the Blue channel by.
184///
185/// # Example
186///
187/// ```no_run
188/// // For example, to increase the values of the Red channel by 10, the Green channel by 20,
189/// // and the Blue channel by 50:
190/// use photon_rs::channels::alter_channels;
191/// use photon_rs::native::open_image;
192///
193/// let mut img = open_image("img.jpg").expect("File should open");
194/// alter_channels(&mut img, 10_i16, 20_i16, 50_i16);
195/// ```
196#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
197pub fn alter_channels(img: &mut PhotonImage, r_amt: i16, g_amt: i16, b_amt: i16) {
198    if r_amt > 255 {
199        panic!("Invalid r_amt passed. Amount to inc/dec channel by should be between -255 and 255");
200    }
201    if g_amt > 255 {
202        panic!("Invalid g_amt passed. Amount to inc/dec channel by should be between -255 and 255");
203    }
204    if b_amt > 255 {
205        panic!("Invalid b_amt passed. Amount to inc/dec channel by should be between -255 and 255");
206    }
207    let end = img.raw_pixels.len();
208
209    for i in (0..end).step_by(4) {
210        let r_val: i16 = img.raw_pixels[i] as i16 + r_amt;
211        let g_val: i16 = img.raw_pixels[i + 1] as i16 + g_amt;
212        let b_val: i16 = img.raw_pixels[i + 2] as i16 + b_amt;
213
214        img.raw_pixels[i] = r_val.clamp(0, 255) as u8;
215        img.raw_pixels[i + 1] = g_val.clamp(0, 255) as u8;
216        img.raw_pixels[i + 2] = b_val.clamp(0, 255) as u8;
217    }
218}
219
220/// Set a certain channel to zero, thus removing the channel's influence in the pixels' final rendered colour.
221///
222/// # Arguments
223/// * `img` - A PhotonImage.
224/// * `channel` - The channel to be removed; must be a usize from 0 to 2, with 0 representing Red, 1 representing Green, and 2 representing Blue.
225/// * `min_filter` - Minimum filter. Value between 0 and 255. Only remove the channel if the current pixel's channel value is less than this minimum filter. To completely
226/// remove the channel, set this value to 255, to leave the channel as is, set to 0, and to set a channel to zero for a pixel whose red value is greater than 50,
227/// then channel would be 0 and min_filter would be 50.
228///
229/// # Example
230///
231/// ```no_run
232/// // For example, to remove the Red channel with a min_filter of 100:
233/// use photon_rs::channels::remove_channel;
234/// use photon_rs::native::open_image;
235///
236/// let mut img = open_image("img.jpg").expect("File should open");
237/// remove_channel(&mut img, 0_usize, 100_u8);
238/// ```
239#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
240pub fn remove_channel(img: &mut PhotonImage, channel: usize, min_filter: u8) {
241    if channel > 2 {
242        panic!("Invalid channel index passed. Channel must be equal to 0, 1, or 2.");
243    }
244    let end = img.raw_pixels.len();
245    for i in (channel..end).step_by(4) {
246        if img.raw_pixels[i] < min_filter {
247            img.raw_pixels[i] = 0;
248        };
249    }
250}
251
252/// Remove the Red channel's influence in an image.
253///
254/// # Arguments
255/// * `img` - A PhotonImage.
256/// * `min_filter` - Only remove the channel if the current pixel's channel value is less than this minimum filter.
257///
258/// # Example
259///
260/// ```no_run
261/// // For example, to remove the red channel for red channel pixel values less than 50:
262/// use photon_rs::channels::remove_red_channel;
263/// use photon_rs::native::open_image;
264///
265/// let mut img = open_image("img.jpg").expect("File should open");
266/// remove_red_channel(&mut img, 50_u8);
267/// ```
268#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
269pub fn remove_red_channel(img: &mut PhotonImage, min_filter: u8) {
270    remove_channel(img, 0, min_filter)
271}
272
273/// Remove the Green channel's influence in an image.
274///
275/// # Arguments
276/// * `img` - A PhotonImage.
277/// * `min_filter` - Only remove the channel if the current pixel's channel value is less than this minimum filter.
278///
279/// # Example
280///
281/// ```no_run
282/// // For example, to remove the green channel for green channel pixel values less than 50:
283/// use photon_rs::channels::remove_green_channel;
284/// use photon_rs::native::open_image;
285///
286/// let mut img = open_image("img.jpg").expect("File should open");
287/// remove_green_channel(&mut img, 50_u8);
288/// ```
289#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
290pub fn remove_green_channel(img: &mut PhotonImage, min_filter: u8) {
291    remove_channel(img, 1, min_filter)
292}
293
294/// Remove the Blue channel's influence in an image.
295///
296/// # Arguments
297/// * `img` - A PhotonImage.
298/// * `min_filter` - Only remove the channel if the current pixel's channel value is less than this minimum filter.
299///
300/// # Example
301///
302/// ```no_run
303/// // For example, to remove the blue channel for blue channel pixel values less than 50:
304/// use photon_rs::channels::remove_blue_channel;
305/// use photon_rs::native::open_image;
306///
307/// let mut img = open_image("img.jpg").expect("File should open");
308/// remove_blue_channel(&mut img, 50_u8);
309/// ```
310#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
311pub fn remove_blue_channel(img: &mut PhotonImage, min_filter: u8) {
312    remove_channel(img, 2, min_filter)
313}
314
315/// Swap two channels.
316///
317/// # Arguments
318/// * `img` - A PhotonImage.
319/// * `channel1` - An index from 0 to 2, representing the Red, Green or Blue channels respectively. Red would be represented by 0, Green by 1, and Blue by 2.
320/// * `channel2` - An index from 0 to 2, representing the Red, Green or Blue channels respectively. Same as above.
321///
322/// # Example
323///
324/// ```no_run
325/// // For example, to swap the values of the Red channel with the values of the Blue channel:
326/// use photon_rs::channels::swap_channels;
327/// use photon_rs::native::open_image;
328///
329/// let mut img = open_image("img.jpg").expect("File should open");
330/// swap_channels(&mut img, 0_usize, 2_usize);
331/// ```
332#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
333pub fn swap_channels(img: &mut PhotonImage, mut channel1: usize, mut channel2: usize) {
334    if channel1 > 2 {
335        panic!("Invalid channel index passed. Channel1 must be equal to 0, 1, or 2.");
336    }
337    if channel2 > 2 {
338        panic!("Invalid channel index passed. Channel2 must be equal to 0, 1, or 2.");
339    }
340    let end = img.raw_pixels.len();
341
342    if channel1 > channel2 {
343        std::mem::swap(&mut channel1, &mut channel2);
344    }
345
346    for i in (channel1..end).step_by(4) {
347        let difference = channel2 - channel1;
348
349        img.raw_pixels.swap(i, i + difference);
350    }
351}
352
353/// Invert RGB value of an image.
354///
355/// # Arguments
356/// * `photon_image` - A DynamicImage that contains a view into the image.
357/// # Example
358///
359/// ```no_run
360/// use photon_rs::channels::invert;
361/// use photon_rs::native::open_image;
362///
363/// let mut img = open_image("img.jpg").expect("File should open");
364/// invert(&mut img);
365/// ```
366#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
367pub fn invert(photon_image: &mut PhotonImage) {
368    let end = photon_image.get_raw_pixels().len();
369
370    for i in (0..end).step_by(4) {
371        let r_val = photon_image.raw_pixels[i];
372        let g_val = photon_image.raw_pixels[i + 1];
373        let b_val = photon_image.raw_pixels[i + 2];
374
375        photon_image.raw_pixels[i] = 255 - r_val;
376        photon_image.raw_pixels[i + 1] = 255 - g_val;
377        photon_image.raw_pixels[i + 2] = 255 - b_val;
378    }
379}
380
381/// Selective hue rotation.
382///
383/// Only rotate the hue of a pixel if its RGB values are within a specified range.
384/// This function only rotates a pixel's hue to another  if it is visually similar to the colour specified.
385/// For example, if a user wishes all pixels that are blue to be changed to red, they can selectively specify  only the blue pixels to be changed.
386/// # Arguments
387/// * `img` - A PhotonImage.
388/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
389/// * `degrees` - The amount of degrees to hue rotate by.
390///
391/// # Example
392///
393/// ```no_run
394/// // For example, to only rotate the pixels that are of RGB value RGB{20, 40, 60}:
395/// use photon_rs::Rgb;
396/// use photon_rs::channels::selective_hue_rotate;
397/// use photon_rs::native::open_image;
398///
399/// let ref_color = Rgb::new(20_u8, 40_u8, 60_u8);
400/// let mut img = open_image("img.jpg").expect("File should open");
401/// selective_hue_rotate(&mut img, ref_color, 180_f32);
402/// ```
403#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
404pub fn selective_hue_rotate(
405    photon_image: &mut PhotonImage,
406    ref_color: Rgb,
407    degrees: f32,
408) {
409    let img = helpers::dyn_image_from_raw(photon_image);
410    let (width, height) = img.dimensions();
411
412    let mut img = img.to_rgba8();
413    for (x, y) in ImageIterator::new(width, height) {
414        let px = img.get_pixel(x, y);
415
416        // Reference colour to compare the current pixel's colour to
417        let lab: Lab = Srgb::new(
418            ref_color.r as f32 / 255.0,
419            ref_color.g as f32 / 255.0,
420            ref_color.b as f32 / 255.0,
421        )
422        .into_color();
423        let channels = px.channels();
424        // Convert the current pixel's colour to the l*a*b colour space
425        let r_val: f32 = channels[0] as f32 / 255.0;
426        let g_val: f32 = channels[1] as f32 / 255.0;
427        let b_val: f32 = channels[2] as f32 / 255.0;
428
429        let px_lab: Lab = Srgb::new(r_val, g_val, b_val).into_color();
430
431        let sim = color_sim(lab, px_lab);
432        if sim > 0 && sim < 40 {
433            let px_data = img.get_pixel(x, y).channels();
434            let color = Srgba::new(
435                px_data[0] as f32,
436                px_data[1] as f32,
437                px_data[2] as f32,
438                255.0,
439            );
440            let hue_rotated_color = Lch::from_color(color).shift_hue(degrees);
441
442            let final_color: Srgba =
443                Srgba::from_linear(hue_rotated_color.into_color()).into_format();
444
445            let components = final_color.into_components();
446
447            img.put_pixel(
448                x,
449                y,
450                image::Rgba([
451                    (components.0 * 255.0) as u8,
452                    (components.1 * 255.0) as u8,
453                    (components.2 * 255.0) as u8,
454                    255,
455                ]),
456            );
457        }
458    }
459
460    photon_image.raw_pixels = img.to_vec();
461}
462
463/// Selectively change pixel colours which are similar to the reference colour provided.
464///
465/// Similarity between two colours is calculated via the CIE76 formula.
466/// Only changes the color of a pixel if its similarity to the reference colour is within the range in the algorithm.
467/// For example, with this function, a user can change the color of all blue pixels by mixing them with red by 10%.
468/// # Arguments
469/// * `photon_image` - A PhotonImage.
470/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
471/// * `new_color` - The `RGB` value of the new color (to be mixed with the matched pixels)
472/// * `fraction` - The amount of mixing the new colour with the matched pixels
473///
474/// # Example
475///
476/// ```no_run
477/// // For example, to only change the color of pixels that are similar to the RGB value RGB{200, 120, 30} by mixing RGB{30, 120, 200} with 25%:
478/// use photon_rs::Rgb;
479/// use photon_rs::channels::selective_color_convert;
480/// use photon_rs::native::open_image;
481///
482/// let ref_color = Rgb::new(200, 120, 30);
483/// let new_color = Rgb::new(30, 120, 200);
484/// let mut img = open_image("img.jpg").expect("File should open");
485/// selective_color_convert(&mut img, ref_color, new_color, 0.25);
486/// ```
487#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
488pub fn selective_color_convert(
489    photon_image: &mut PhotonImage,
490    ref_color: Rgb,
491    new_color: Rgb,
492    fraction: f32,
493) {
494    let buffer = photon_image.raw_pixels.as_mut_slice();
495
496    // Reference colour to compare the current pixel's colour to
497    let ref_lab: Lab = Srgb::new(
498        ref_color.r as f32 / 255.0,
499        ref_color.g as f32 / 255.0,
500        ref_color.b as f32 / 255.0,
501    )
502    .into_color();
503
504    for px in buffer.chunks_mut(4) {
505        let px_lab: Lab = Srgb::new(
506            px[0] as f32 / 255.0,
507            px[1] as f32 / 255.0,
508            px[2] as f32 / 255.0,
509        )
510        .into_color();
511        let sim = color_sim(ref_lab, px_lab);
512
513        if sim > 0 && sim < 40 {
514            px[0] = ((px[0] as f32) + fraction * ((new_color.r as f32) - (px[0] as f32)))
515                .clamp(0.0, 255.0) as u8;
516            px[1] = ((px[1] as f32) + fraction * ((new_color.g as f32) - (px[1] as f32)))
517                .clamp(0.0, 255.0) as u8;
518            px[2] = ((px[2] as f32) + fraction * ((new_color.b as f32) - (px[2] as f32)))
519                .clamp(0.0, 255.0) as u8;
520        }
521    }
522}
523
524// pub fn correct(img: &DynamicImage, mode: &'static str, colour_space: &'static str, amt: f32) -> DynamicImage {
525//     let mut img  = img.to_rgb();
526
527//     let (width, height) = img.dimensions();
528
529//         for x in 0..width {
530//             for y in 0..height {
531//                 let px_data = img.get_pixel(x, y).data;
532
533//                 let colour_to_cspace;
534//                 if colour_space == "hsv" {
535//                     colour_to_cspace: Hsv = Srgb::from_raw(&px_data).into_format();
536//                 }
537//                 else if colour_space == "hsl" {
538//                     colour_to_cspace = Hsl::from(color);
539//                 }
540//                 else {
541//                     colour_to_cspace = Lch::from(color);
542//                 }
543
544//                 let new_color  = match mode {
545//                     // Match a single value
546//                     "desaturate" => colour_to_cspace.desaturate(amt),
547//                     "saturate" => colour_to_cspace.saturate(amt),
548//                     "lighten" => colour_to_cspace.lighten(amt),
549//                     "darken" => colour_to_cspace.darken(amt),
550//                     _ => colour_to_cspace.saturate(amt),
551//                 };
552
553//                 img.put_pixel(x, y, image::Rgb {
554//                     data: Srgb::from_linear(new_color.into()).into_format().into_raw()
555//                 });
556//             }
557//         }
558
559//     let dynimage = image::ImageRgb8(img);
560//     dynimage
561// }
562
563/// Selectively lighten an image.
564///
565/// Only lighten the hue of a pixel if its colour matches or is similar to the RGB colour specified.
566/// For example, if a user wishes all pixels that are blue to be lightened, they can selectively specify  only the blue pixels to be changed.
567/// # Arguments
568/// * `img` - A PhotonImage.
569/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
570/// * `amt` - The level from 0 to 1 to lighten the hue by. Increasing by 10% would have an `amt` of 0.1
571///
572/// # Example
573///
574/// ```no_run
575/// // For example, to only lighten the pixels that are of or similar to RGB value RGB{20, 40, 60}:
576/// use photon_rs::Rgb;
577/// use photon_rs::channels::selective_lighten;
578/// use photon_rs::native::open_image;
579///
580/// let ref_color = Rgb::new(20_u8, 40_u8, 60_u8);
581/// let mut img = open_image("img.jpg").expect("File should open");
582/// selective_lighten(&mut img, ref_color, 0.2_f32);
583/// ```
584#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
585pub fn selective_lighten(img: &mut PhotonImage, ref_color: Rgb, amt: f32) {
586    selective(img, "lighten", ref_color, amt)
587}
588
589/// Selectively desaturate pixel colours which are similar to the reference colour provided.
590///
591/// Similarity between two colours is calculated via the CIE76 formula.
592/// Only desaturates the hue of a pixel if its similarity to the reference colour is within the range in the algorithm.
593/// For example, if a user wishes all pixels that are blue to be desaturated by 0.1, they can selectively specify  only the blue pixels to be changed.
594/// # Arguments
595/// * `img` - A PhotonImage.
596/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
597/// * `amt` - The amount of desaturate the colour by.
598///
599/// # Example
600///
601/// ```no_run
602/// // For example, to only desaturate the pixels that are similar to the RGB value RGB{20, 40, 60}:
603/// use photon_rs::Rgb;
604/// use photon_rs::channels::selective_desaturate;
605/// use photon_rs::native::open_image;
606///
607/// let ref_color = Rgb::new(20_u8, 40_u8, 60_u8);
608/// let mut img = open_image("img.jpg").expect("File should open");
609/// selective_desaturate(&mut img, ref_color, 0.1_f32);
610/// ```
611#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
612pub fn selective_desaturate(img: &mut PhotonImage, ref_color: Rgb, amt: f32) {
613    selective(img, "desaturate", ref_color, amt)
614}
615
616/// Selectively saturate pixel colours which are similar to the reference colour provided.
617///
618/// Similarity between two colours is calculated via the CIE76 formula.
619/// Only saturates the hue of a pixel if its similarity to the reference colour is within the range in the algorithm.
620/// For example, if a user wishes all pixels that are blue to have an increase in saturation by 10%, they can selectively specify only the blue pixels to be changed.
621/// # Arguments
622/// * `img` - A PhotonImage.
623/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
624/// * `amt` - The amount of saturate the colour by.
625///
626/// # Example
627///
628/// ```no_run
629/// // For example, to only increase the saturation of pixels that are similar to the RGB value RGB{20, 40, 60}:
630/// use photon_rs::Rgb;
631/// use photon_rs::channels::selective_saturate;
632/// use photon_rs::native::open_image;
633///
634/// let ref_color = Rgb::new(20_u8, 40_u8, 60_u8);
635/// let mut img = open_image("img.jpg").expect("File should open");
636/// selective_saturate(&mut img, ref_color, 0.1_f32);
637/// ```
638#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
639pub fn selective_saturate(img: &mut PhotonImage, ref_color: Rgb, amt: f32) {
640    selective(img, "saturate", ref_color, amt);
641}
642
643fn selective(
644    photon_image: &mut PhotonImage,
645    mode: &'static str,
646    ref_color: Rgb,
647    amt: f32,
648) {
649    let img = helpers::dyn_image_from_raw(photon_image);
650    let (width, height) = img.dimensions();
651    let mut img = img.to_rgba8();
652
653    for (x, y) in ImageIterator::new(width, height) {
654        let px = img.get_pixel(x, y);
655
656        // Reference colour to compare the current pixel's colour to
657        let lab: Lab = Srgb::new(
658            ref_color.r as f32 / 255.0,
659            ref_color.g as f32 / 255.0,
660            ref_color.b as f32 / 255.0,
661        )
662        .into_color();
663        let channels = px.channels();
664        // Convert the current pixel's colour to the l*a*b colour space
665        let r_val: f32 = channels[0] as f32 / 255.0;
666        let g_val: f32 = channels[1] as f32 / 255.0;
667        let b_val: f32 = channels[2] as f32 / 255.0;
668
669        let px_lab: Lab = Srgb::new(r_val, g_val, b_val).into_color();
670
671        let sim = color_sim(lab, px_lab);
672        if sim > 0 && sim < 40 {
673            let px_data = img.get_pixel(x, y).channels();
674            let lch_colour: Lch = Srgb::new(px_data[0], px_data[1], px_data[2])
675                .into_format()
676                .into_linear()
677                .into_color();
678
679            let new_color = match mode {
680                // Match a single value
681                "desaturate" => lch_colour.desaturate(amt),
682                "saturate" => lch_colour.saturate(amt),
683                "lighten" => lch_colour.lighten(amt),
684                "darken" => lch_colour.darken(amt),
685                _ => lch_colour.saturate(amt),
686            };
687
688            // let final_color: Srgba = Srgba::from_linear(new_color.into_color());
689            let final_color = Srgba::from_color(new_color);
690
691            let components = final_color.into_components();
692
693            img.put_pixel(
694                x,
695                y,
696                image::Rgba([
697                    (components.0 * 255.0) as u8,
698                    (components.1 * 255.0) as u8,
699                    (components.2 * 255.0) as u8,
700                    255,
701                ]),
702            );
703        }
704    }
705
706    photon_image.raw_pixels = img.to_vec();
707}
708
709/// Selectively changes a pixel to greyscale if it is *not* visually similar or close to the colour specified.
710/// Only changes the colour of a pixel if its RGB values are within a specified range.
711///
712/// (Similarity between two colours is calculated via the CIE76 formula.)
713/// For example, if a user wishes all pixels that are *NOT* blue to be displayed in greyscale, they can selectively specify only the blue pixels to be
714/// kept in the photo.
715/// # Arguments
716/// * `img` - A PhotonImage.
717/// * `ref_color` - The `RGB` value of the reference color (to be compared to)
718///
719/// # Example
720///
721/// ```no_run
722/// // For example, to greyscale all pixels that are *not* visually similar to the RGB colour RGB{20, 40, 60}:
723/// use photon_rs::Rgb;
724/// use photon_rs::channels::selective_greyscale;
725/// use photon_rs::native::open_image;
726///
727/// let ref_color = Rgb::new(20_u8, 40_u8, 60_u8);
728/// let mut img = open_image("img.jpg").expect("File should open");
729/// selective_greyscale(img, ref_color);
730/// ```
731#[cfg_attr(feature = "enable_wasm", wasm_bindgen)]
732pub fn selective_greyscale(mut photon_image: PhotonImage, ref_color: Rgb) {
733    let mut img = helpers::dyn_image_from_raw(&photon_image);
734
735    for (x, y) in ImageIterator::new(img.width(), img.height()) {
736        let mut px = img.get_pixel(x, y);
737
738        // Reference colour to compare the current pixel's colour to
739        let lab: Lab = Srgb::new(
740            ref_color.r as f32 / 255.0,
741            ref_color.g as f32 / 255.0,
742            ref_color.b as f32 / 255.0,
743        )
744        .into_color();
745        let channels = px.channels();
746        // Convert the current pixel's colour to the l*a*b colour space
747        let r_val: f32 = channels[0] as f32 / 255.0;
748        let g_val: f32 = channels[1] as f32 / 255.0;
749        let b_val: f32 = channels[2] as f32 / 255.0;
750
751        let px_lab: Lab = Srgb::new(r_val, g_val, b_val).into_color();
752
753        let sim = color_sim(lab, px_lab);
754        if sim > 30 {
755            let avg = channels[0] as f32 * 0.3
756                + channels[1] as f32 * 0.59
757                + channels[2] as f32 * 0.11;
758            px = image::Rgba([avg as u8, avg as u8, avg as u8, 255]);
759        }
760        img.put_pixel(x, y, px);
761    }
762
763    let raw_pixels = img.into_bytes();
764    photon_image.raw_pixels = raw_pixels;
765}
766
767/// Get the similarity of two colours in the l*a*b colour space using the CIE76 formula.
768pub fn color_sim(lab1: Lab, lab2: Lab) -> i64 {
769    let l_comp = lab2.l - lab1.l;
770    let a_comp = lab2.a - lab1.a;
771    let b_comp = lab2.b - lab1.b;
772
773    let l_comp_sq = l_comp.powf(2.0);
774    let a_comp_sq = a_comp.powf(2.0);
775    let b_comp_sq = b_comp.powf(2.0);
776
777    let total = l_comp_sq + a_comp_sq + b_comp_sq;
778    (total as f64).sqrt() as i64 + 1
779}