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}