From 1b5dcd4c4f35c0fc5513c5a0107d2518e378227b Mon Sep 17 00:00:00 2001 From: Dorival Pedroso Date: Mon, 17 Nov 2025 15:22:46 +1000 Subject: [PATCH] Impl draw_rgb_or_rgba function in Image --- .vscode/settings.json | 1 + examples/README.md | 2 + figures/integ_image_with_rgb.svg | 401 ++++++++++++++++++++++++++++++ figures/integ_image_with_rgba.svg | 401 ++++++++++++++++++++++++++++++ src/conversions.rs | 33 ++- src/image.rs | 25 +- tests/test_image.rs | 90 +++++++ 7 files changed, 951 insertions(+), 2 deletions(-) create mode 100644 figures/integ_image_with_rgb.svg create mode 100644 figures/integ_image_with_rgba.svg diff --git a/.vscode/settings.json b/.vscode/settings.json index d53e8ef..518e87c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -76,6 +76,7 @@ "polyline", "pyplot", "rarrow", + "rgba", "roundtooth", "rstride", "savefig", diff --git a/examples/README.md b/examples/README.md index 03eea59..a11494b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -95,6 +95,8 @@ Some output of integration tests are shown below. ## Image [test_image.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image.rs) +[test_image_with_rgb.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image_with_rgb.rs) +[test_image_with_rgba.rs](https://github.com/cpmech/plotpy/tree/main/tests/test_image_with_rgba.rs) ![image](https://raw.githubusercontent.com/cpmech/plotpy/main/figures/integ_image_1.svg) diff --git a/figures/integ_image_with_rgb.svg b/figures/integ_image_with_rgb.svg new file mode 100644 index 0000000..804cd07 --- /dev/null +++ b/figures/integ_image_with_rgb.svg @@ -0,0 +1,401 @@ + + + + + + + + 2025-11-17T15:09:35.644389 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/figures/integ_image_with_rgba.svg b/figures/integ_image_with_rgba.svg new file mode 100644 index 0000000..21874b4 --- /dev/null +++ b/figures/integ_image_with_rgba.svg @@ -0,0 +1,401 @@ + + + + + + + + 2025-11-17T15:10:14.912436 + image/svg+xml + + + Matplotlib v3.6.3, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/conversions.rs b/src/conversions.rs index c8923ff..500c081 100644 --- a/src/conversions.rs +++ b/src/conversions.rs @@ -56,6 +56,26 @@ where write!(buf, "]\n").unwrap(); } +/// Generates a 3-deep nested Python list +pub(crate) fn generate_nested_list_3(buf: &mut String, name: &str, data: &Vec>>) +where + T: std::fmt::Display + Num, +{ + write!(buf, "{}=[", name).unwrap(); + for row in data.into_iter() { + write!(buf, "[").unwrap(); + for val in row.into_iter() { + write!(buf, "[",).unwrap(); + for v in val.into_iter() { + write!(buf, "{},", v).unwrap(); + } + write!(buf, "],").unwrap(); + } + write!(buf, "],").unwrap(); + } + write!(buf, "]\n").unwrap(); +} + /// Converts a matrix to a 2D NumPy array pub(crate) fn matrix_to_array<'a, T, U>(buf: &mut String, name: &str, matrix: &'a T) where @@ -78,7 +98,7 @@ where #[cfg(test)] mod tests { - use super::{generate_list, generate_list_quoted, generate_nested_list, matrix_to_array, vector_to_array}; + use super::*; #[test] fn generate_list_works() { @@ -139,6 +159,17 @@ mod tests { assert_eq!(buf, "a=[[1,2,3,],[4,5,],[6,7,8,9,],]\n"); } + #[test] + fn generate_nested_list_3_works() { + let mut buf = String::new(); + let a = vec![ + vec![vec![1.0, 0.0, 0.0, 1.0], vec![0.0, 1.0, 0.0, 1.0]], // Row 0: Red, Green + vec![vec![0.0, 0.0, 1.0, 1.0], vec![1.0, 1.0, 1.0, 0.5]], // Row 1: Blue, White (semi-transparent) + ]; + generate_nested_list_3(&mut buf, "a", &a); + assert_eq!(buf, "a=[[[1,0,0,1,],[0,1,0,1,],],[[0,0,1,1,],[1,1,1,0.5,],],]\n"); + } + #[test] fn matrix_to_array_works() { let mut buf = String::new(); diff --git a/src/image.rs b/src/image.rs index 4e82385..b3d91f5 100644 --- a/src/image.rs +++ b/src/image.rs @@ -1,4 +1,4 @@ -use super::{matrix_to_array, AsMatrix, GraphMaker}; +use super::{generate_nested_list_3, matrix_to_array, AsMatrix, GraphMaker}; use num_traits::Num; use std::fmt::Write; @@ -55,6 +55,12 @@ impl Image { } /// (imshow) Displays data as an image + /// + /// # Arguments + /// + /// * `data` - 2D matrix-like data structure + /// + /// See pub fn draw<'a, T, U>(&mut self, data: &'a T) where T: AsMatrix<'a, U>, @@ -65,6 +71,23 @@ impl Image { write!(&mut self.buffer, "plt.imshow(data{})\n", &opt).unwrap(); } + /// (imshow) Displays data as an image with RGB or RGB(A) values + /// + /// # Arguments + /// + /// * `data` - 3D vector with shape (height, width, 3) for RGB or (height, width, 4) for RGBA + /// The inner-most vector contains the color channels. + /// + /// See + pub fn draw_rgb_or_rgba(&mut self, data: &Vec>>) + where + T: std::fmt::Display + Num, + { + generate_nested_list_3(&mut self.buffer, "data", data); + let opt = self.options(); + write!(&mut self.buffer, "plt.imshow(data{})\n", &opt).unwrap(); + } + /// Sets the colormap index /// /// Options: diff --git a/tests/test_image.rs b/tests/test_image.rs index 3b303c6..b5ea5b1 100644 --- a/tests/test_image.rs +++ b/tests/test_image.rs @@ -37,3 +37,93 @@ fn test_image_1() -> Result<(), StrError> { assert!(c > 420 && c < 500); Ok(()) } + +#[test] +fn test_image_with_rgb() -> Result<(), StrError> { + let data = vec![ + // --- Row 0 --- + vec![ + vec![1.0, 0.0, 0.0], // Pixel 0,0: Red + vec![0.0, 1.0, 0.0], // Pixel 0,1: Green + vec![0.0, 0.0, 1.0], // Pixel 0,2: Blue + ], + // --- Row 1 --- + vec![ + vec![1.0, 1.0, 0.0], // Pixel 1,0: Yellow + vec![1.0, 0.0, 1.0], // Pixel 1,1: Magenta + vec![0.0, 1.0, 1.0], // Pixel 1,2: Cyan + ], + // --- Row 2 --- + vec![ + vec![0.5, 0.5, 0.5], // Pixel 2,0: Gray + vec![1.0, 1.0, 1.0], // Pixel 2,1: White + vec![0.0, 0.0, 0.0], // Pixel 2,2: Black + ], + ]; + + // image plot and options + let mut img = Image::new(); + img.set_colormap_name("terrain") + .set_extra("alpha=0.8") + .draw_rgb_or_rgba(&data); + + let mut plot = Plot::new(); + plot.add(&img); + + // save figure + let path = Path::new(OUT_DIR).join("integ_image_with_rgb.svg"); + plot.set_show_errors(true).save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let c = lines_iter.count(); + assert!(c > 400 && c < 430); + Ok(()) +} + +#[test] +fn test_image_with_rgba() -> Result<(), StrError> { + let data = vec![ + // --- Row 0 --- + vec![ + vec![1.0, 0.0, 0.0, 0.5], // Pixel 0,0: Red + vec![0.0, 1.0, 0.0, 0.5], // Pixel 0,1: Green + vec![0.0, 0.0, 1.0, 0.5], // Pixel 0,2: Blue + ], + // --- Row 1 --- + vec![ + vec![1.0, 1.0, 0.0, 0.8], // Pixel 1,0: Yellow + vec![1.0, 0.0, 1.0, 0.8], // Pixel 1,1: Magenta + vec![0.0, 1.0, 1.0, 0.8], // Pixel 1,2: Cyan + ], + // --- Row 2 --- + vec![ + vec![0.5, 0.5, 0.5, 0.2], // Pixel 2,0: Gray + vec![1.0, 1.0, 1.0, 0.2], // Pixel 2,1: White + vec![0.0, 0.0, 0.0, 0.2], // Pixel 2,2: Black + ], + ]; + + // image plot and options + let mut img = Image::new(); + img.set_colormap_name("terrain") + .set_extra("alpha=0.8") + .draw_rgb_or_rgba(&data); + + let mut plot = Plot::new(); + plot.add(&img); + + // save figure + let path = Path::new(OUT_DIR).join("integ_image_with_rgba.svg"); + plot.set_show_errors(true).save(&path)?; + + // check number of lines + let file = File::open(path).map_err(|_| "cannot open file")?; + let buffered = BufReader::new(file); + let lines_iter = buffered.lines(); + let c = lines_iter.count(); + assert!(c > 400 && c < 430); + Ok(()) +}