From d2a187ceecdde5d7e4a4526531d7c0c909dcd227 Mon Sep 17 00:00:00 2001 From: Tim Kallady Date: Thu, 5 Sep 2024 16:07:45 +1000 Subject: [PATCH 1/4] flood fill algorithm and regression test --- src/drawing/fill.rs | 62 ++++++++++++++++++++++++ src/drawing/mod.rs | 3 ++ tests/data/truth/flood_filled_shape.png | Bin 0 -> 3161 bytes tests/regression.rs | 20 ++++++++ 4 files changed, 85 insertions(+) create mode 100644 src/drawing/fill.rs create mode 100644 tests/data/truth/flood_filled_shape.png diff --git a/src/drawing/fill.rs b/src/drawing/fill.rs new file mode 100644 index 00000000..bdacfe1e --- /dev/null +++ b/src/drawing/fill.rs @@ -0,0 +1,62 @@ +use image::{GenericImage, Pixel}; + +/// Equivalent to bucket tool in MS-PAINT +/// Performs 4-way flood-fill based on this algorithm: https://en.wikipedia.org/wiki/Flood_fill#Span_filling +pub fn flood_fill(image: &mut I, x: u32, y: u32, fill_with: I::Pixel) +where + I: GenericImage, +{ + let target = image.get_pixel(x, y); + + let mut stack = Vec::new(); + + stack.push((x as i32, x as i32, y as i32, 1 as i32)); + stack.push((x as i32, x as i32, y as i32 - 1, -1 as i32)); + + while !stack.is_empty() { + let (x1, x2, y, dy) = stack.pop().unwrap(); + let mut x1 = x1; + let mut x = x1; + if inside(image, x, y, target) { + while inside(image, x - 1, y, target) { + image.put_pixel(x as u32 - 1, y as u32, fill_with); + x = x - 1; + } + if x < x1 { + stack.push((x, x1 - 1, y - dy, -dy)) + } + } + while x1 <= x2 { + while inside(image, x1, y, target) { + image.put_pixel(x1 as u32, y as u32, fill_with); + x1 = x1 + 1; + } + if x1 > x { + stack.push((x, x1 - 1, y + dy, dy)) + } + if x1 - 1 > x2 { + stack.push((x2 + 1, x1 - 1, y - dy, -dy)) + } + x1 = x1 + 1; + while x1 < x2 && !inside(image, x1, y, target) { + x1 = x1 + 1 + } + x = x1 + } + } +} + +/// Determines whether (x,y) is within the image bounds and if the pixel there is equal to target_color +fn inside(image: &I, x: i32, y: i32, target_color: I::Pixel) -> bool +where + I: GenericImage, +{ + if x < 0 || y < 0 { + return false; + } + let x = x as u32; + let y = y as u32; + let (width, height) = image.dimensions(); + //TODO: Compare pixel equality without conversion to rgba + x < width && y < height && image.get_pixel(x, y).to_rgba() == target_color.to_rgba() +} diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index 4883011f..4f04f18d 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -35,6 +35,9 @@ pub use self::rect::{ mod text; pub use self::text::{draw_text, draw_text_mut, text_size}; +mod fill; +pub use self::fill::flood_fill; + // Set pixel at (x, y) to color if this point lies within image bounds, // otherwise do nothing. fn draw_if_in_bounds(canvas: &mut C, x: i32, y: i32, color: C::Pixel) diff --git a/tests/data/truth/flood_filled_shape.png b/tests/data/truth/flood_filled_shape.png new file mode 100644 index 0000000000000000000000000000000000000000..e850eaa80bf312438047f730b013757b4b69e0ce GIT binary patch literal 3161 zcmcInZ&Xv~8NXn}ASjRygHjC%QCj2}(Jrkm>!k=tsugTQ)2$8&LD=ciQpY-|+vydE z4M=D}nyHPG{`t@h&QjFVS*Gn3jHs9_#}BS>*jXq&gwAs|jQ4mn_Ils_-hk(HU-oS| zCzss!eg3`A^Lu{pLubYA9V?cvST*}@=YdMogJqxm>P97rc=o<^gDc22{rrx~IgaJa|9e?*{Au#wgC53X6K+}SKrgA%|gGgbY_4UzunaV&pw zDVqxIqeALMq0kGX;e`_nRt(=9AT2WTETY{=lbH}Yl z1i-&X1GxIb+DwOvVf1l&#;to|MvdOT++^2{@Fx+`Yc!%yKU%Elo!=SGe`oa`SZS18 zu%9y2JM{L3&Sj_HzHTV!36N%^)Zh2TXgK5#w`#pZQSR_KDOWIKVfI8I9pw~Al8ZEI znao0Q?sOc3Dw3o+xAHjrysS7FSc3B^dl{=V|H*CADytDfu^LOdPi}K=yt4KcS#c9$ zsCbraPIQywRtC@Zp4Y3MKn|V(NObX7(8CgW+$hcW4n;$w2|YtmzI~cxVCOBXR+|6t zHuo-U@$Yxj=Q!6S_LQ0`fj(6SmTl{-a*a!vp!`hvLHr=I-9TQRJ8fS+q7rMJBpM#ANLd*Q{|r1 zL<;p}jS(o^5XmbQ=j((*MX*-T0al!>vmt+mJnC-fZCT}>vyWE-3c*?*$C0bOb4_0|3RA}Z8~jSw;7Plz z20$+bl>22_kS3dq-uT1|rK25#(w82QY={HbsDMus;8IBvJtD6)H{IaY zF%Hs=?thfRbd8(rhgKF9NzWx;8>oo#kzWmDf!)T!xG`e#Cy!rT+N9M^`r;(xS(&#k;TBzVIKT2};TK?HemlW=05_ z12OFBcp||y!2r30l@`58yec>2++E^u{ zso!06FrU0bf2CnF=g3g>Zhit)-dGXUsi{?(U%oFt3%~oj#ik@OCMwP8@Zf)c3TDh6(GDH{&16XIWA}2OWBtP3Ynsrd3T!qfZCjg zO$->EDzkGAD!jYf$NdN(KG_BLBOY;X^Qw5qFlG>nu9ho$3^n@T(bO70??ORAk(DNS z+?d$#(d~XeMb+?4-i2LmQ=Oa0(bOlX=n;t0+p&paI^82GW8n1OkKwp_*<=hIU1jpk z;S#_T=MqRCHS*(JLc3zqRxnCd5rdT!n0*SVPFQS2;WNO4*wDp7C>eI%r=I9rf!118 z48gfrf4)=^RgE2s^*1<`?z1aT2e>EDdXE+TJ@^V>t~CpsBlYgqa4d8gRKKBAa^cwA zj?pk-)a!8mwI1=a+TxiW15u3Y6-T_Iz?NMpLJlXOSuOgewiDukW~#FfSIkgde!dtYa_3T(SCsa9x=-8I$n z$uNnd@eQIcMoMXH7|qC&2$~ZAF3e7+Fok&Umv5gX>nTSJ5SdZpPt)!kl(z4QlEA{q xn^H8JI~0cjYxrge7v|kF!$s6u=rAX9{w;6r$s_Om1OGBGhLYXI-lCee{{XqZSQr2R literal 0 HcmV?d00001 diff --git a/tests/regression.rs b/tests/regression.rs index a8e478df..891de260 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -23,6 +23,7 @@ use image::{ use imageproc::contrast::ThresholdType; use imageproc::definitions::Image; +use imageproc::drawing::flood_fill; use imageproc::filter::bilateral::GaussianEuclideanColorDistance; use imageproc::filter::bilateral_filter; use imageproc::kernel::{self}; @@ -748,6 +749,25 @@ fn test_draw_filled_ellipse() { compare_to_truth_image(&image, "filled_ellipse.png"); } +#[test] +fn test_draw_flood_filled_shape() { + use imageproc::drawing::{draw_hollow_ellipse_mut, flood_fill}; + + let red = Rgb([255, 0, 0]); + let green = Rgb([0, 255, 0]); + let blue = Rgb([0, 0, 255]); + let mut image = RgbImage::from_pixel(200, 200, Rgb([255, 255, 255])); + + draw_hollow_ellipse_mut(&mut image, (100, 100), 50, 50, red); + draw_hollow_ellipse_mut(&mut image, (50, 100), 40, 90, blue); + draw_hollow_ellipse_mut(&mut image, (100, 150), 80, 30, green); + draw_hollow_ellipse_mut(&mut image, (150, 150), 100, 60, blue); + + flood_fill(&mut image, 120, 120, red); + + compare_to_truth_image(&image, "flood_filled_shape.png"); +} + #[test] fn test_hough_line_detection() { use imageproc::hough::{detect_lines, draw_polar_lines, LineDetectionOptions, PolarLine}; From 3785074dcdcfa2fff560f41b591379d0165c6b43 Mon Sep 17 00:00:00 2001 From: Tim Kallady Date: Mon, 9 Sep 2024 11:33:46 +1000 Subject: [PATCH 2/4] add _mut version, change from GenericImage to Image --- src/drawing/fill.rs | 25 ++++++++++++++++++------- src/drawing/mod.rs | 2 +- tests/regression.rs | 7 ++++--- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/drawing/fill.rs b/src/drawing/fill.rs index bdacfe1e..4fcef467 100644 --- a/src/drawing/fill.rs +++ b/src/drawing/fill.rs @@ -1,12 +1,23 @@ -use image::{GenericImage, Pixel}; +use crate::definitions::Image; +use image::Pixel; /// Equivalent to bucket tool in MS-PAINT /// Performs 4-way flood-fill based on this algorithm: https://en.wikipedia.org/wiki/Flood_fill#Span_filling -pub fn flood_fill(image: &mut I, x: u32, y: u32, fill_with: I::Pixel) +pub fn flood_fill

(image: &Image

, x: u32, y: u32, fill_with: P) -> Image

where - I: GenericImage, + P: Pixel + PartialEq, { - let target = image.get_pixel(x, y); + let mut filled_image = image.clone(); + flood_fill_mut(&mut filled_image, x, y, fill_with); + filled_image +} + +#[doc=generate_mut_doc_comment!("draw_line_segment")] +pub fn flood_fill_mut

(image: &mut Image

, x: u32, y: u32, fill_with: P) +where + P: Pixel + PartialEq, +{ + let target = image.get_pixel(x, y).clone(); let mut stack = Vec::new(); @@ -47,9 +58,9 @@ where } /// Determines whether (x,y) is within the image bounds and if the pixel there is equal to target_color -fn inside(image: &I, x: i32, y: i32, target_color: I::Pixel) -> bool +fn inside

(image: &Image

, x: i32, y: i32, target_pixel: P) -> bool where - I: GenericImage, + P: Pixel + PartialEq, { if x < 0 || y < 0 { return false; @@ -58,5 +69,5 @@ where let y = y as u32; let (width, height) = image.dimensions(); //TODO: Compare pixel equality without conversion to rgba - x < width && y < height && image.get_pixel(x, y).to_rgba() == target_color.to_rgba() + x < width && y < height && *image.get_pixel(x, y) == target_pixel } diff --git a/src/drawing/mod.rs b/src/drawing/mod.rs index 4f04f18d..a56aa0fa 100644 --- a/src/drawing/mod.rs +++ b/src/drawing/mod.rs @@ -36,7 +36,7 @@ mod text; pub use self::text::{draw_text, draw_text_mut, text_size}; mod fill; -pub use self::fill::flood_fill; +pub use self::fill::{flood_fill, flood_fill_mut}; // Set pixel at (x, y) to color if this point lies within image bounds, // otherwise do nothing. diff --git a/tests/regression.rs b/tests/regression.rs index 891de260..f1534b53 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -23,7 +23,6 @@ use image::{ use imageproc::contrast::ThresholdType; use imageproc::definitions::Image; -use imageproc::drawing::flood_fill; use imageproc::filter::bilateral::GaussianEuclideanColorDistance; use imageproc::filter::bilateral_filter; use imageproc::kernel::{self}; @@ -751,7 +750,7 @@ fn test_draw_filled_ellipse() { #[test] fn test_draw_flood_filled_shape() { - use imageproc::drawing::{draw_hollow_ellipse_mut, flood_fill}; + use imageproc::drawing::{draw_hollow_ellipse_mut, flood_fill, flood_fill_mut}; let red = Rgb([255, 0, 0]); let green = Rgb([0, 255, 0]); @@ -763,8 +762,10 @@ fn test_draw_flood_filled_shape() { draw_hollow_ellipse_mut(&mut image, (100, 150), 80, 30, green); draw_hollow_ellipse_mut(&mut image, (150, 150), 100, 60, blue); - flood_fill(&mut image, 120, 120, red); + let filled_image = flood_fill(&image, 120, 120, red); + compare_to_truth_image(&filled_image, "flood_filled_shape.png"); + flood_fill_mut(&mut image, 120, 120, red); compare_to_truth_image(&image, "flood_filled_shape.png"); } From 610e71ef0b9e5a2f1161d94f43d0ba40dc237bd7 Mon Sep 17 00:00:00 2001 From: Tim Kallady Date: Wed, 18 Sep 2024 15:59:28 +1000 Subject: [PATCH 3/4] Remove redundant test and fix doc comment --- src/drawing/fill.rs | 3 +-- tests/regression.rs | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/drawing/fill.rs b/src/drawing/fill.rs index 4fcef467..f7e71bf4 100644 --- a/src/drawing/fill.rs +++ b/src/drawing/fill.rs @@ -12,7 +12,7 @@ where filled_image } -#[doc=generate_mut_doc_comment!("draw_line_segment")] +#[doc=generate_mut_doc_comment!("flood_fill")] pub fn flood_fill_mut

(image: &mut Image

, x: u32, y: u32, fill_with: P) where P: Pixel + PartialEq, @@ -68,6 +68,5 @@ where let x = x as u32; let y = y as u32; let (width, height) = image.dimensions(); - //TODO: Compare pixel equality without conversion to rgba x < width && y < height && *image.get_pixel(x, y) == target_pixel } diff --git a/tests/regression.rs b/tests/regression.rs index f1534b53..0710de04 100644 --- a/tests/regression.rs +++ b/tests/regression.rs @@ -762,9 +762,6 @@ fn test_draw_flood_filled_shape() { draw_hollow_ellipse_mut(&mut image, (100, 150), 80, 30, green); draw_hollow_ellipse_mut(&mut image, (150, 150), 100, 60, blue); - let filled_image = flood_fill(&image, 120, 120, red); - compare_to_truth_image(&filled_image, "flood_filled_shape.png"); - flood_fill_mut(&mut image, 120, 120, red); compare_to_truth_image(&image, "flood_filled_shape.png"); } From 279f2c171caf7fd0c151bc11b96579ac69e8cd26 Mon Sep 17 00:00:00 2001 From: Tim Kallady Date: Tue, 24 Sep 2024 13:30:50 +1000 Subject: [PATCH 4/4] Fix url in docs --- src/drawing/fill.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/drawing/fill.rs b/src/drawing/fill.rs index f7e71bf4..f7cb458f 100644 --- a/src/drawing/fill.rs +++ b/src/drawing/fill.rs @@ -2,7 +2,7 @@ use crate::definitions::Image; use image::Pixel; /// Equivalent to bucket tool in MS-PAINT -/// Performs 4-way flood-fill based on this algorithm: https://en.wikipedia.org/wiki/Flood_fill#Span_filling +/// Performs 4-way flood-fill based on this algorithm: pub fn flood_fill

(image: &Image

, x: u32, y: u32, fill_with: P) -> Image

where P: Pixel + PartialEq,