vectorizer/actions/
common.rs

1//! This module provides utilities for image processing, focusing on color analysis
2//! and transparency detection within images. It includes functions to:
3//!
4//! - Find an unused color in an image for tasks like color-based masking.
5//! - Check if a specific color is present within an image.
6//! - Determine whether an image should be keyed, which is useful for processes like
7//!   adding a green screen effect for later removal.
8//!
9//! # Constants
10//! - `NUM_UNUSED_COLOR_ITERATIONS`: Number of attempts to generate random unused colors.
11//! - `KEYING_THRESHOLD`: Proportion of transparent pixels required to trigger keying.
12//!
13//! # Functions
14//! - `find_unused_color_in_image`: Identifies a color not present in the provided image.
15//! - `color_exists_in_image`: Checks for the presence of a specific color in an image.
16//! - `should_key_image`: Determines if an image requires keying based on its transparency.
17//!
18//! These tools are particularly useful in workflows involving image processing, masking,
19//! or creating visual effects.
20
21mod private
22{
23  use crate::{ commands::InputOutput, svg::SvgFile, * };
24  use std::io::Write;
25  use actions::{ Error, Result };
26  use std::collections::HashMap;
27  use fastrand::Rng;
28  use palette::{ color_difference::EuclideanDistance, IntoColor, Lab, Srgb };
29  use visioncortex::Color;
30
31  const NUM_UNUSED_COLOR_ITERATIONS: usize = 6;
32  const KEYING_THRESHOLD: f32 = 0.2;
33
34  /// Find a color that is not present in a image.
35  /// First tries to check a list of `special` colors, and the a list of randomly generated colors
36  pub fn find_unused_color_in_image( img : &visioncortex::ColorImage ) -> Result < visioncortex::Color > 
37  {
38    let special_colors = IntoIterator::into_iter
39    ([
40        visioncortex::Color::new( 255, 0, 0 ),
41        visioncortex::Color::new( 0, 255, 0 ),
42        visioncortex::Color::new( 0, 0, 255 ),
43        visioncortex::Color::new( 255, 255, 0 ),
44        visioncortex::Color::new( 0, 255, 255 ),
45        visioncortex::Color::new( 255, 0, 255 ),
46    ]);
47    let rng = Rng::new();
48    let random_colors = ( 0..NUM_UNUSED_COLOR_ITERATIONS )
49    .map( | _ | visioncortex::Color::new( rng.u8( ..) , rng.u8( .. ), rng.u8( .. ) ) );
50
51    for color in special_colors.chain( random_colors ) 
52    {
53      if !color_exists_in_image( img, color ) 
54      {
55        return Ok( color );
56      }
57    }
58    Err
59    ( 
60      Error::KeyColorError
61    )
62  }
63
64  /// Checks if `color` is present in `img`
65  pub fn color_exists_in_image( img : &visioncortex::ColorImage, color : visioncortex::Color ) -> bool 
66  {
67    for y in 0..img.height 
68    {
69      for x in 0..img.width 
70      {
71        let pixel_color = img.get_pixel( x, y );
72        if pixel_color.r == color.r && 
73           pixel_color.g == color.g && 
74           pixel_color.b == color.b 
75        {
76          return true;
77        }
78      }
79    }
80    false
81  }
82
83  /// Checks if the image should be keyed (like adding a green screen, to cut it out later).
84  ///
85  /// This function determines whether an image requires keying based on its transparency. It scans several
86  /// horizontal lines across the image to count the number of transparent pixels. If the proportion of transparent
87  /// pixels exceeds a predefined threshold, the function returns `true`, indicating that the image should be keyed.
88  ///
89  /// # Arguments
90  /// * `img` - A reference to a `visioncortex::ColorImage` representing the input image.
91  ///
92  /// # Returns
93  /// * `bool` - `true` if the image should be keyed, `false` otherwise.
94  pub fn should_key_image( img : &visioncortex::ColorImage ) -> bool 
95  {
96    let ( width, height ) = ( img.width, img.height );
97    if width == 0 || height == 0 
98    {
99      return false;
100    }
101
102    // Check for transparency at several scanlines in y direction to know if the image needs to be keyed.
103    // Should be keyed if the total amount of transparent pixels is bigger than the threshold
104    let threshold = ( ( width * 2 ) as f32 * KEYING_THRESHOLD ) as usize;
105    let mut num_transparent_pixels = 0;
106    let y_positions = 
107    [
108      0,
109      height / 4,
110      height / 2,
111      3 * height / 4,
112      height - 1,
113    ];
114
115    for y in y_positions 
116    {
117      for x in 0..width 
118      {
119        if img.get_pixel( x, y ).a == 0 
120        {
121          num_transparent_pixels += 1;
122        }
123        if num_transparent_pixels >= threshold 
124        {
125          return true;
126        }
127      }
128    }
129
130    false
131  }
132  
133
134  /// Return the background color of the image
135  pub fn background_color( img : &visioncortex::ColorImage, mask : u8 ) -> Option< [ u8; 3 ] > 
136  {
137    let mut unique_colors = HashMap::new();
138
139    for y in [ 0, img.height - 1 ]
140    {
141      for x in 0..img.width
142      {
143        let c = img.get_pixel( x, y );
144        if c.a > 0
145        {
146          unique_colors.entry( [ c.r & mask, c.g & mask, c.b & mask ] )
147          .and_modify( | v | *v += 1 )
148          .or_insert( 1 );
149        }
150      }
151    }
152
153    for x in [ 0, img.width - 1 ]
154    {
155      for y in 0..img.height
156      {
157        let c = img.get_pixel( x, y );
158        if c.a > 0
159        {
160          unique_colors.entry( [ c.r & mask, c.g & mask, c.b & mask ] )
161          .and_modify( | v | *v += 1 )
162          .or_insert( 1 );
163        }
164      }
165    }
166
167    let mut colors : Vec< ( [ u8; 3 ], u32 ) > = unique_colors.into_iter().collect();
168    colors.sort_unstable_by_key( | ( _, count ) | *count );
169
170
171    colors.last().map( | ( col, _ ) | *col )
172  }
173
174  /// Return the Euclid distance between two colors in CIELAB color space
175  pub fn euclid_difference( c1 : Color, c2 : Color ) -> f32
176  {
177    let c1 =  Srgb::from( [ c1.r, c1.g, c1.b ] ).into_linear::< f32 >();
178    let c2 =  Srgb::from( [ c2.r, c2.g, c2.b ] ).into_linear::< f32 >();
179    let lab_c1 : Lab = c1.into_color();
180    let lab_c2 : Lab = c2.into_color();
181    lab_c1.distance_squared( lab_c2 )
182  }
183
184  /// Read image from the disk
185  pub fn read_image( io : &InputOutput ) -> Result< image::DynamicImage >
186  {
187    let img = image::open( &io.input ).
188    map_err( | e | Error::ImageError( e ) )?;
189
190    Ok( img )
191  }
192
193  /// Writes an SVG file to the specified output path.
194  ///
195  /// This function takes an SVG file and writes it to the specified output path on disk.
196  ///
197  /// # Arguments
198  /// * `io` - A reference to the `InputOutput` struct containing the input/output paths.
199  /// * `svg` - A reference to the `SvgFile` that will be written to disk.
200  ///
201  /// # Returns
202  /// * `Result<()>` - An empty result if the operation is successful, or an error if the writing fails.
203  ///
204  /// # Errors
205  /// This function will return an error if the file writing operation fails.
206  pub fn write_svg( io : &InputOutput, svg : &SvgFile ) -> Result< () >
207  {
208    let mut output_path = 
209    match io.output 
210    {
211      Some( ref o ) => o.clone(),
212      None =>  io.input.clone()
213    };
214    output_path.set_extension( "svg" );
215    let mut out = std::fs::File::create( output_path )
216    .map_err( | e | Error::IOError( e ) )?;
217
218    write!( &mut out, "{}", svg ).unwrap();
219    Ok( () )
220  }
221}
222
223
224crate::mod_interface!
225{
226
227  orphan use
228  {
229    find_unused_color_in_image,
230    color_exists_in_image,
231    should_key_image,
232    background_color,
233    euclid_difference,
234    read_image,
235    write_svg
236  };
237}