Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ image = { version = "0.25", default-features = false, features = ["png"] }
name = "compare"
harness = false

[[test]]
name = "utils"
harness = false

[profile.release-with-debug]
inherits = "release"
debug = true

[features]
default = []
yuv_compare = []
33 changes: 31 additions & 2 deletions src/hybrid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,8 @@ pub fn rgba_blended_hybrid_compare(
/// Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares
/// Please mind that the RGBSimilarity-Image does _not_ contain plain RGB here
/// - The red channel contains 1. - similarity(ssim, y)
/// - The green channel contains 1. - similarity(rms, u)
/// - The blue channel contains 1. - similarity(rms, v)
/// - The green channel contains 1. - similarity(rms, u)
/// - The blue channel contains 1. - similarity(rms, v)
/// This leads to a nice visualization of color and structure differences - with structural differences (meaning gray mssim diffs) leading to red rectangles
/// and and the u and v color diffs leading to color-deviations in green, blue and cyan
/// All-black meaning no differences
Expand All @@ -179,6 +179,35 @@ pub fn rgb_hybrid_compare(first: &RgbImage, second: &RgbImage) -> Result<Similar

let first_channels = first.split_to_yuv();
let second_channels = second.split_to_yuv();

internal_yuv_hybrid_compare(&first_channels, &second_channels)
}

/// Comparing structure via MSSIM on Y channel, comparing color-diff-vectors on U and V summing the squares
/// first_channels and second_channels are arrays, each containing 3 `GrayImage`s
/// - The first GrayImage contains the Y values - similarity(ssim, y)
/// - The second GrayImage contains the U values - similarity(rms, u)
/// - The third GrayImage contains the V values - similarity(rms, v)
/// Please mind that the SimilarityImage does _not_ contain plain RGB here
/// - The red channel contains 1. - similarity(ssim, y)
/// - The green channel contains 1. - similarity(rms, u)
/// - The blue channel contains 1. - similarity(rms, v)
/// This leads to a nice visualization of color and structure differences - with structural differences (meaning gray mssim diffs) leading to red rectangles
/// and and the u and v color diffs leading to color-deviations in green, blue and cyan
/// All-black meaning no differences
#[cfg(feature = "yuv_compare")]
pub fn yuv_hybrid_compare(first_channels: &[GrayImage; 3], second_channels: &[GrayImage; 3]) -> Result<Similarity, CompareError> {
Comment thread
ForestedDev marked this conversation as resolved.
if (first_channels[0].dimensions() != second_channels[0].dimensions())
|| (first_channels[1].dimensions() != second_channels[1].dimensions()) // 5 checks needed, this ensures all channels are the same sizes.
|| (first_channels[2].dimensions() != second_channels[2].dimensions()) // First 3 ensure the channels each have the same resolution across the 2 images
|| (first_channels[0].dimensions() != first_channels[1].dimensions()) // Last 2 check if each channel is the same in the first image
|| (first_channels[1].dimensions() != first_channels[2].dimensions()) { // If all checks pass this leaves all 6 being the same
return Err(CompareError::DimensionsDiffer);
}
internal_yuv_hybrid_compare(&first_channels, &second_channels)
}

pub(crate) fn internal_yuv_hybrid_compare(first_channels: &[GrayImage; 3], second_channels: &[GrayImage; 3]) -> Result<Similarity, CompareError> {
let (_, mssim_result) = ssim_simple(&first_channels[0], &second_channels[0])?;
let (_, u_result) = root_mean_squared_error_simple(&first_channels[1], &second_channels[1])?;
let (_, v_result) = root_mean_squared_error_simple(&first_channels[2], &second_channels[2])?;
Expand Down
11 changes: 11 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,15 @@ mod histogram;
mod hybrid;
mod squared_error;
mod ssim;


#[cfg(not(feature="yuv_compare"))] // Tests cannot be implemented to check the functionality of this feature gate, please be mindful of this.
mod utils;

/// Provides some utilities to make yuv image management and conversion easier.
#[cfg(feature="yuv_compare")] // Exposes rgb/yuv conversions and split to yuv publicly, others were left to be pub crate or private
pub mod utils; // All exposed APIs have tests, and compilation will fail if they ever become non-public

#[doc(hidden)]
pub mod prelude {
pub use image::{GrayImage, ImageBuffer, Luma, Rgb, RgbImage};
Expand Down Expand Up @@ -226,6 +233,10 @@ pub use hybrid::rgba_hybrid_compare;
#[doc(inline)]
pub use hybrid::rgba_blended_hybrid_compare;

#[doc(inline)]
#[cfg(feature = "yuv_compare")]
pub use hybrid::yuv_hybrid_compare;

pub use hybrid::BlendInput;

#[cfg(test)]
Expand Down
17 changes: 10 additions & 7 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use image::RgbaImage;
use itertools::izip;

/// see https://www.itu.int/rec/T-REC-T.871
fn rgb_to_yuv(rgb: &[f32; 3]) -> [f32; 3] {
pub fn rgb_to_yuv(rgb: &[f32; 3]) -> [f32; 3] {
let py = 0. + (0.299 * rgb[0]) + (0.587 * rgb[1]) + (0.114 * rgb[2]);
let pu = 128. - (0.168736 * rgb[0]) - (0.331264 * rgb[1]) + (0.5 * rgb[2]);
let pv = 128. + (0.5 * rgb[0]) - (0.418688 * rgb[1]) - (0.081312 * rgb[2]);
Expand All @@ -12,7 +12,7 @@ fn rgb_to_yuv(rgb: &[f32; 3]) -> [f32; 3] {

/// see https://www.itu.int/rec/T-REC-T.871
#[allow(dead_code)]
fn yuv_to_rgb(yuv: &[f32; 3]) -> [f32; 3] {
pub fn yuv_to_rgb(yuv: &[f32; 3]) -> [f32; 3] {
let r = yuv[0] + (1.402 * (yuv[2] - 128.));
let g = yuv[0] - (0.344136 * (yuv[1] - 128.)) - (0.714136 * (yuv[2] - 128.));
let b = yuv[0] + (1.772 * (yuv[1] - 128.));
Expand Down Expand Up @@ -67,8 +67,11 @@ pub(crate) fn blend_alpha(image: &RgbaImage, color: Rgb<u8>) -> RgbImage {
buffer
}

/// Holds methods to split an RgbImage into arrays of channels
pub trait Decompose {
/// Returns an array containing 3 `GrayImage`s where each channel represents [r, g, b]
fn split_channels(&self) -> [GrayImage; 3];
/// Converts an image to yuv then returns an array containing 3 `GrayImage`s where each channel represents [y, u, v]
fn split_to_yuv(&self) -> [GrayImage; 3];
}

Expand Down Expand Up @@ -113,7 +116,7 @@ impl Decompose for RgbImage {
}
}

pub fn merge_similarity_channels(input: &[&GraySimilarityImage; 3]) -> RGBSimilarityImage {
pub(crate) fn merge_similarity_channels(input: &[&GraySimilarityImage; 3]) -> RGBSimilarityImage {
Comment thread
ForestedDev marked this conversation as resolved.
let mut output = RGBSimilarityImage::new(input[0].width(), input[0].height());
izip!(
input[0].pixels(),
Expand All @@ -128,12 +131,12 @@ pub fn merge_similarity_channels(input: &[&GraySimilarityImage; 3]) -> RGBSimila
output
}

pub struct Window {
pub(crate) struct Window {
pub top_left: (u32, u32),
pub bottom_right: (u32, u32),
}

pub struct WindowIter<'a> {
pub(crate) struct WindowIter<'a> {
current_index: u32,
window: &'a Window,
}
Expand Down Expand Up @@ -189,7 +192,7 @@ impl Window {
result
}

pub fn iter_pixels(&self) -> WindowIter {
pub fn iter_pixels(&'_ self) -> WindowIter<'_> {
WindowIter {
window: self,
current_index: 0,
Expand All @@ -204,7 +207,7 @@ impl Window {
}
}

pub fn draw_window_to_image(window: &Window, image: &mut GraySimilarityImage, val: f32) {
pub(crate) fn draw_window_to_image(window: &Window, image: &mut GraySimilarityImage, val: f32) {
window
.iter_pixels()
.for_each(|current_pixel| image.put_pixel(current_pixel.0, current_pixel.1, Luma([val])));
Expand Down
39 changes: 19 additions & 20 deletions tests/compare.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub struct CompareWorld {
comparison_result: Option<Similarity>,
comparison_result_rgb: Option<Similarity>,
comparison_result_rgba: Option<Similarity>,
comparison_result_yuv: Option<Similarity>,
}

#[given(expr = "the images {string} and {string} are loaded")]
Expand Down Expand Up @@ -129,6 +130,20 @@ fn compare_hybrid_rgb(world: &mut CompareWorld) {
);
}

#[cfg(feature = "yuv_compare")]
#[when(expr = "comparing the images using the hybrid mode as yuv")]
fn compare_hybrid_yuv(world: &mut CompareWorld) {
use image_compare::utils::Decompose;

world.comparison_result_yuv = Some(
image_compare::yuv_hybrid_compare(
&world.first.as_ref().unwrap().clone().into_rgb8().split_to_yuv(),
&world.second.as_ref().unwrap().clone().into_rgb8().split_to_yuv(),
)
.expect("Error comparing the two images!"),
);
}

#[then(expr = "the similarity score is {float}")]
fn check_result_score(world: &mut CompareWorld, score: f64) {
if world.comparison_result.is_some() {
Expand All @@ -137,6 +152,8 @@ fn check_result_score(world: &mut CompareWorld, score: f64) {
assert_eq!(world.comparison_result_rgb.as_ref().unwrap().score, score);
} else if world.comparison_result_rgba.is_some() {
assert_eq!(world.comparison_result_rgba.as_ref().unwrap().score, score);
} else if world.comparison_result_yuv.is_some() {
assert_eq!(world.comparison_result_yuv.as_ref().unwrap().score, score);
} else {
panic!("No result calculated yet")
}
Expand Down Expand Up @@ -182,31 +199,13 @@ fn check_result_image_rgba(world: &mut CompareWorld, reference: String) {
);
}

#[then(expr = "the rgb similarity image matches {string}")]
fn check_result_image_rgb(world: &mut CompareWorld, reference: String) {
let img = world
.comparison_result_rgb
.as_ref()
.unwrap()
.image
.to_color_map()
.into_rgb8();
let image_one = image::open(reference)
.expect("Could not find reference-image")
.into_rgb8();
assert_eq!(
image_compare::rgb_similarity_structure(&Algorithm::RootMeanSquared, &img, &image_one)
.expect("Could not compare")
.score,
1.0
);
}

#[tokio::main]
async fn main() {
CompareWorld::run("tests/features/structure_gray.feature").await;
CompareWorld::run("tests/features/histogram_gray.feature").await;
CompareWorld::run("tests/features/structure_rgb.feature").await;
CompareWorld::run("tests/features/hybrid_rgb.feature").await;
CompareWorld::run("tests/features/hybrid_rgba.feature").await;
#[cfg(feature = "yuv_compare")]
CompareWorld::run("tests/features/hybrid_yuv.feature").await;
}
Binary file added tests/data/colored_primitives_b_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/colored_primitives_g_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/colored_primitives_r_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/colored_primitives_u_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/colored_primitives_v_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/colored_primitives_y_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/pad_gaprao_b_channel.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/data/pad_gaprao_g_channel.png
Binary file added tests/data/pad_gaprao_lighter_b_channel.png
Binary file added tests/data/pad_gaprao_lighter_g_channel.png
Binary file added tests/data/pad_gaprao_lighter_r_channel.png
Binary file added tests/data/pad_gaprao_lighter_u_channel.png
Binary file added tests/data/pad_gaprao_lighter_v_channel.png
Binary file added tests/data/pad_gaprao_lighter_y_channel.png
Binary file added tests/data/pad_gaprao_noise_b_channel.png
Binary file added tests/data/pad_gaprao_noise_g_channel.png
Binary file added tests/data/pad_gaprao_noise_r_channel.png
Binary file added tests/data/pad_gaprao_noise_u_channel.png
Binary file added tests/data/pad_gaprao_noise_v_channel.png
Binary file added tests/data/pad_gaprao_noise_y_channel.png
Binary file added tests/data/pad_gaprao_r_channel.png
Binary file added tests/data/pad_gaprao_u_channel.png
Binary file added tests/data/pad_gaprao_v_channel.png
Binary file added tests/data/pad_gaprao_y_channel.png
35 changes: 35 additions & 0 deletions tests/features/decompose.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Feature: Decompose trait test

Scenario Outline: Compare newly split yuv channels to expected values
Given the image '<full_image>' is loaded
When comparing yuv channels
Then check split success

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Then check split success
Then the split channels match the references

then steps should be formulated in passive, but this is just a hint - I can fix this later :)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will keep that in mind for future use- was the first time i'd worked with that test runner


Examples:
| full_image |
| tests/data/pad_gaprao.png |
| tests/data/pad_gaprao_lighter.png |
| tests/data/pad_gaprao_noise.png |
| tests/data/pad_gaprao_gray_inverted.png |
| tests/data/pad_gaprao_color_filters.png |
| tests/data/colored_primitives_swapped.png |
| tests/data/colored_primitives_hybrid_compare_rgb.png |
| tests/data/colored_primitives.png |



Scenario Outline: Compare newly split yuv channels to expected values
Given the image '<full_image>' is loaded
When comparing rgb channels
Then check split success

Examples:
| full_image |
| tests/data/pad_gaprao.png |
| tests/data/pad_gaprao_lighter.png |
| tests/data/pad_gaprao_noise.png |
| tests/data/pad_gaprao_gray_inverted.png |
| tests/data/pad_gaprao_color_filters.png |
| tests/data/colored_primitives_swapped.png |
| tests/data/colored_primitives_hybrid_compare_rgb.png |
| tests/data/colored_primitives.png |
20 changes: 20 additions & 0 deletions tests/features/hybrid_yuv.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Feature: YUV image comparison using hybrid mode - MSSIM for for Y channel, RMS for U and V

Scenario: Comparing an image to the original with hybrid mode and checking the difference image
Given the images 'tests/data/colored_primitives.png' and 'tests/data/colored_primitives_swapped.png' are loaded
When comparing the images using the hybrid mode as yuv
Then the YUV similarity image matches 'tests/data/colored_primitives_hybrid_compare_rgb.png'


Scenario Outline: Comparing a modified image to the original using hybrid mode algorithm
Given the images 'tests/data/pad_gaprao.png' and '<compare_image>' are loaded
When comparing the images using the hybrid mode as yuv
Then the similarity score is <result>

Examples:
| compare_image | result |
| tests/data/pad_gaprao.png | 1.0 |
| tests/data/pad_gaprao_lighter.png | 0.9514066504143178 |
| tests/data/pad_gaprao_noise.png | 0.13009783371684705 |
| tests/data/pad_gaprao_gray_inverted.png | 3.2566565189821024e-5 |
| tests/data/pad_gaprao_color_filters.png | 0.9876923700357477 |
74 changes: 74 additions & 0 deletions tests/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
use cucumber::{given, then, when, World};
use image::DynamicImage;
use image_compare::prelude::*;
extern crate image;

#[cfg(feature = "yuv_compare")] // Do not remove, ensures rgb_to_yuv stays public not pub(crate) or private
use image_compare::utils::rgb_to_yuv;
#[cfg(feature = "yuv_compare")] // Do not remove, ensures yuv_to_rgb stays public not pub(crate) or private
use image_compare::utils::yuv_to_rgb;

// `World` is your shared, likely mutable state.
#[derive(Debug, World, Default)]
pub struct SplitWorld {
image: Option<DynamicImage>,
channel_r: Option<GrayImage>,
channel_g: Option<GrayImage>,
channel_b: Option<GrayImage>,
channel_y: Option<GrayImage>,
channel_u: Option<GrayImage>,
channel_v: Option<GrayImage>,
success: Option<bool>
}

#[cfg(feature = "yuv_compare")]
#[given(expr = "the image {string} is loaded")]
fn load_images(world: &mut SplitWorld, image: String) {
let full_image = image::open(image.clone()).expect(format!("Could not find test-image {}", image).as_str());
world.image = Some(full_image.clone());
world.channel_y = Some(image::open(image.clone().replace(".png", "_y_channel.png")).expect("Could not load y_channel").to_luma8()); // Automatically loads images
world.channel_u = Some(image::open(image.clone().replace(".png", "_u_channel.png")).expect("Could not load u_channel").to_luma8());
world.channel_v = Some(image::open(image.clone().replace(".png", "_v_channel.png")).expect("Could not load v_channel").to_luma8());
world.channel_r = Some(image::open(image.clone().replace(".png", "_r_channel.png")).expect("Could not load r_channel").to_luma8());
world.channel_g = Some(image::open(image.clone().replace(".png", "_g_channel.png")).expect("Could not load g_channel").to_luma8());
world.channel_b = Some(image::open(image.clone().replace(".png", "_b_channel.png")).expect("Could not load b_channel").to_luma8());
}

#[cfg(feature = "yuv_compare")]
#[when(expr = "comparing yuv channels")]
fn decompose_yuv(world: &mut SplitWorld) {
use image_compare::utils::Decompose;

let [channel_y, channel_u, channel_v] = world.image.as_ref().unwrap().to_rgb8().split_to_yuv();
let y_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_y, world.channel_y.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;
let u_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_u, world.channel_u.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;
let v_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_v, world.channel_v.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;

world.success = Some(y_comp && u_comp && v_comp);
}

#[cfg(feature = "yuv_compare")]
#[when(expr = "comparing rgb channels")]
fn decompose_rgb(world: &mut SplitWorld) {
use image_compare::utils::Decompose;

let [channel_r, channel_g, channel_b] = world.image.as_ref().unwrap().to_rgb8().split_channels();
let r_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_r, world.channel_r.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;
let g_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_g, world.channel_g.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;
let b_comp = image_compare::gray_similarity_structure(&Algorithm::RootMeanSquared, &channel_b, world.channel_b.as_ref().unwrap()).expect("RMS Gray compare error").score == 1.0;

world.success = Some(r_comp && g_comp && b_comp);
}

#[cfg(feature = "yuv_compare")]
#[then(expr = "check split success")]
fn check_success(world: &mut SplitWorld) {
assert!(world.success.unwrap() == true);
}


#[tokio::main]
async fn main() {
#[cfg(feature = "yuv_compare")]
SplitWorld::run("tests/features/decompose.feature").await;
}
Loading