Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement block out area #18

Merged
merged 10 commits into from
Aug 7, 2021
1 change: 1 addition & 0 deletions benches/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ fn criterion_benchmark(c: &mut Criterion) {
do_not_check_dimensions: true,
detect_anti_aliased_pixels: false,
blend_factor_of_unchanged_pixels: None,
block_out_areas: None,
};

c.bench_function("1000 × 667 pixels", |b| {
Expand Down
Binary file added benches/fixtures/tiger-blockout-diff.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 benches/fixtures/yellow.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
61 changes: 52 additions & 9 deletions src/cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use getopts::{Matches, Options};
use std::collections::HashSet;
use std::env;

const VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");
Expand All @@ -12,6 +13,7 @@ const SHORT_NAME_OUTPUT_IMAGE_PATH: &str = "o";
const SHORT_NAME_THRESHOLD: &str = "t";
const SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS: &str = "d";
const SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS: &str = "a";
const SHORT_NAME_BLOCK_OUT_AREA: &str = "b";
const DEFAULT_PATH_OF_DIFF_IMAGE: &str = "diff.png";

pub enum OutputImageBase {
Expand All @@ -31,46 +33,53 @@ impl Cli {

let mut options = Options::new();

options.optflag(SHORT_NAME_HELP, "help", "print this help menu");
options.optflag(SHORT_NAME_VERSION, "version", "print the version");
options.optflag(SHORT_NAME_HELP, "help", "Print this help menu.");
options.optflag(SHORT_NAME_VERSION, "version", "Print the version.");

options.optmulti(
SHORT_NAME_BLOCK_OUT_AREA,
"block-out",
"Block-out area. Can be repeated multiple times.",
"x,y,w,h",
);

options.optflag(
SHORT_NAME_DONT_CHECK_DIMENSIONS,
"ignore-dimensions",
"don't check image dimensions",
"Do not check image dimensions.",
);

options.optflagopt(
SHORT_NAME_BLEND_FACTOR_OF_UNCHENGED_PIXELS,
"alpha",
"blending factor of unchanged pixels in the diff output. ranges from 0 for pure white to 1 for original brightness. (default: 0.1)",
"Blending factor of unchanged pixels in the diff output. Ranges from 0 for pure white to 1 for original brightness. (default: 0.1)",
"NUM"
);

options.optflagopt(
SHORT_NAME_COPY_IMAGE_AS_BASE,
"copy-image",
"copies specific image to output as base. (default: left)",
"Copies specific image to output as base. (default: left)",
"{left, right}",
);

options.optflag(
SHORT_NAME_DETECT_ANTI_ALIASED_PIXELS,
"detect-anti-aliased",
"detect anti-aliased pixels. (default: false)",
"Detects anti-aliased pixels. (default: false)",
);

options.optopt(
SHORT_NAME_OUTPUT_IMAGE_PATH,
"output",
"the file path of diff image, PNG only. (default: diff.png)",
"The file path of diff image, PNG only. (default: diff.png)",
"OUTPUT",
);

options.optopt(
SHORT_NAME_THRESHOLD,
"threshold",
"matching threshold, ranges from 0 to 1, less more precise. (default: 0.1)",
"Matching threshold, ranges from 0 to 1, less more precise. (default: 0.1)",
"NUM",
);

Expand Down Expand Up @@ -198,6 +207,40 @@ impl Cli {
.get(1)
.with_context(|| format!("the {} argument is missing", "RIGHT".magenta()).red())?;

Ok((&left_image, &right_image))
Ok((left_image, right_image))
}

pub fn get_block_out_area(&self) -> Option<HashSet<(u32, u32)>> {
self.matches
.opt_strs(SHORT_NAME_BLOCK_OUT_AREA)
.iter()
.fold(None, |acc, area| {
let area = {
let mut segments = area
.splitn(4, ',')
.map(|segment| segment.parse::<u32>().ok().unwrap_or(0));
let x = segments.next().unwrap_or(0);
let y = segments.next().unwrap_or(0);
let width = segments.next().unwrap_or(0);
let height = segments.next().unwrap_or(0);

match (x, y, width, height) {
(0, _, _, _) | (_, 0, _, _) | (_, _, 0, _) | (_, _, _, 0) => None,
(x, y, width, height) => Some((x, y, width, height)),
}
};
match area {
None => acc,
Some((x, y, width, height)) => {
let mut acc = acc.unwrap_or_default();
for i in x..=x + width {
for j in y..=y + height {
acc.insert((i, j));
}
}
Some(acc)
}
}
})
}
}
44 changes: 30 additions & 14 deletions src/diff.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use super::{antialiased, cli, yiq::YIQ};
use super::{antialiased, cli, yiq::Yiq};
use anyhow::{anyhow, Context, Result};
use colored::*;
use image::io::Reader as ImageIoReader;
use image::{GenericImageView, ImageBuffer, ImageFormat, Pixel, Rgba, RgbaImage};
use std::collections::HashSet;

const MAX_YIQ_POSSIBLE_DELTA: f32 = 35215.0;
const RED_PIXEL: Rgba<u8> = Rgba([255, 0, 0, 255]);
Expand All @@ -15,6 +16,7 @@ pub enum DiffResult {
Different(u32, u32),
OutOfBounds(u32, u32),
AntiAliased(u32, u32),
BlockedOut(u32, u32),
}

pub struct RunParams<'a> {
Expand All @@ -26,6 +28,7 @@ pub struct RunParams<'a> {
pub do_not_check_dimensions: bool,
pub detect_anti_aliased_pixels: bool,
pub blend_factor_of_unchanged_pixels: Option<f32>,
pub block_out_areas: Option<HashSet<(u32, u32)>>,
}

fn open_and_decode_image(path: &str, which: &str) -> Result<RgbaImage> {
Expand All @@ -39,12 +42,13 @@ fn open_and_decode_image(path: &str, which: &str) -> Result<RgbaImage> {
}

pub fn get_results(
left_image: &RgbaImage,
right_image: &RgbaImage,
left_image: RgbaImage,
right_image: RgbaImage,
threshold: f32,
detect_anti_aliased_pixels: bool,
blend_factor_of_unchanged_pixels: Option<f32>,
output_image_base: &Option<cli::OutputImageBase>,
block_out_areas: &Option<HashSet<(u32, u32)>>,
) -> Option<(i32, RgbaImage)> {
let (width, height) = left_image.dimensions();

Expand All @@ -54,9 +58,15 @@ pub fn get_results(

if left_pixel == right_pixel {
DiffResult::Identical(x, y)
} else if block_out_areas
.as_ref()
.and_then(|set| set.contains(&(x, y)).then(|| ()))
.is_some()
{
DiffResult::BlockedOut(x, y)
} else {
let left_pixel = YIQ::from_rgba(left_pixel);
let right_pixel = YIQ::from_rgba(right_pixel);
let left_pixel = Yiq::from_rgba(left_pixel);
let right_pixel = Yiq::from_rgba(right_pixel);
let delta = left_pixel.squared_distance(&right_pixel);

if delta.abs() > threshold {
Expand Down Expand Up @@ -90,7 +100,7 @@ pub fn get_results(
DiffResult::Identical(x, y) | DiffResult::BelowThreshold(x, y) => {
if let Some(alpha) = blend_factor_of_unchanged_pixels {
let left_pixel = left_image.get_pixel(x, y);
let yiq_y = YIQ::rgb2y(&left_pixel.to_rgb());
let yiq_y = Yiq::rgb2y(&left_pixel.to_rgb());
let rgba_a = left_pixel.channels()[3] as f32;
let color =
super::blend_semi_transparent_white(yiq_y, alpha * rgba_a / 255.0) as u8;
Expand All @@ -105,6 +115,7 @@ pub fn get_results(
DiffResult::AntiAliased(x, y) => {
output_image.put_pixel(x, y, YELLOW_PIXEL);
}
DiffResult::BlockedOut(_x, _y) => (),
}
}

Expand Down Expand Up @@ -137,12 +148,13 @@ pub fn run(params: &RunParams) -> Result<Option<i32>> {
let threshold = MAX_YIQ_POSSIBLE_DELTA * params.threshold * params.threshold;

match get_results(
&left_image,
&right_image,
left_image,
right_image,
threshold,
params.detect_anti_aliased_pixels,
params.blend_factor_of_unchanged_pixels,
&params.output_image_base,
&params.block_out_areas,
) {
Some((diffs, output_image)) => {
output_image
Expand All @@ -169,30 +181,33 @@ mod tests {
do_not_check_dimensions: true,
detect_anti_aliased_pixels: false,
blend_factor_of_unchanged_pixels: None,
block_out_areas: None,
};

#[test]
fn test_zero_width_height() {
let actual = get_results(
&RgbaImage::new(0, 0),
&RgbaImage::new(0, 0),
RgbaImage::new(0, 0),
RgbaImage::new(0, 0),
RUN_PARAMS.threshold,
RUN_PARAMS.detect_anti_aliased_pixels,
RUN_PARAMS.blend_factor_of_unchanged_pixels,
&RUN_PARAMS.output_image_base,
&RUN_PARAMS.block_out_areas,
);
assert_eq!(None, actual);
}

#[test]
fn test_1_pixel() {
let actual = get_results(
&RgbaImage::new(1, 1),
&RgbaImage::new(1, 1),
RgbaImage::new(1, 1),
RgbaImage::new(1, 1),
RUN_PARAMS.threshold,
RUN_PARAMS.detect_anti_aliased_pixels,
RUN_PARAMS.blend_factor_of_unchanged_pixels,
&RUN_PARAMS.output_image_base,
&RUN_PARAMS.block_out_areas,
);
assert_eq!(None, actual);
}
Expand All @@ -202,12 +217,13 @@ mod tests {
let mut left = RgbaImage::new(1, 1);
left.put_pixel(0, 0, YELLOW_PIXEL);
let actual = get_results(
&left,
&RgbaImage::new(1, 1),
left,
RgbaImage::new(1, 1),
RUN_PARAMS.threshold,
RUN_PARAMS.detect_anti_aliased_pixels,
RUN_PARAMS.blend_factor_of_unchanged_pixels,
&RUN_PARAMS.output_image_base,
&RUN_PARAMS.block_out_areas,
);

let mut expected_image = RgbaImage::new(1, 1);
Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ pub mod cli;
pub mod diff;
mod yiq;

use crate::yiq::Yiq;
use image::{Pixel, RgbaImage};
use std::cmp;
use yiq::*;

fn get_diagonal_neighbours(x1: u32, y1: u32, width: u32, height: u32) -> ((u32, u32), (u32, u32)) {
// (x0, y0)
Expand Down Expand Up @@ -88,7 +88,7 @@ pub fn antialiased(
}

let neighbor = left.get_pixel(x, y).to_rgb();
let delta = YIQ::delta_y(center, &neighbor);
let delta = Yiq::delta_y(center, &neighbor);

if delta == 0.0 {
zeros += 1;
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn main() -> Result<()> {
let threshold = cli.get_threshold()?;
let detect_anti_aliased_pixels = cli.detect_anti_aliased_pixels();
let blend_factor_of_unchanged_pixels = cli.blend_factor_of_unchanged_pixels()?;
let block_out_areas = cli.get_block_out_area();

diff::run(&diff::RunParams {
left,
Expand All @@ -31,6 +32,7 @@ fn main() -> Result<()> {
do_not_check_dimensions,
detect_anti_aliased_pixels,
blend_factor_of_unchanged_pixels,
block_out_areas,
})
.map(|code| {
if let Some(code) = code {
Expand Down
14 changes: 7 additions & 7 deletions src/yiq.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use image::Pixel;

#[derive(Debug, PartialEq)]
pub struct YIQ {
pub struct Yiq {
y: f32, // luminance, in range [0, 1]
i: f32, // hue of color, in range ~ [-0.5, 0.5]
q: f32, // saturation of color, in range ~ [-0.5, 0.5]
}

impl YIQ {
impl Yiq {
#[allow(clippy::many_single_char_names, clippy::excessive_precision)]
pub fn rgb2y(rgb: &image::Rgb<u8>) -> f32 {
let rgb = rgb.channels();
Expand Down Expand Up @@ -68,27 +68,27 @@ impl YIQ {

#[cfg(test)]
mod tests {
use super::YIQ;
use super::Yiq;

#[test]
fn test_from_rgb() {
let expected = YIQ {
let expected = Yiq {
y: 0.0,
i: 0.0,
q: 0.0,
};
let actual = YIQ::from_rgba(&image::Rgba([0, 0, 0, 0]));
let actual = Yiq::from_rgba(&image::Rgba([0, 0, 0, 0]));
assert_eq!(expected, actual);
}

#[test]
fn test_squared_distance_same() {
let a = YIQ {
let a = Yiq {
y: 0.5,
i: -0.1,
q: 0.1,
};
let b = YIQ {
let b = Yiq {
y: 0.5,
i: -0.1,
q: 0.1,
Expand Down
22 changes: 22 additions & 0 deletions tests/e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,3 +148,25 @@ fn test_output_image_web_page() {

temp.close().unwrap();
}

#[test]
fn test_block_out_area() {
let temp = NamedTempFile::new("tiger-blockout-diff.png").unwrap();
let mut cmd = Command::cargo_bin("dify").unwrap();
let assert = cmd
.arg(fs::canonicalize("./benches/fixtures/tiger.jpg").unwrap())
.arg(fs::canonicalize("./benches/fixtures/yellow.jpg").unwrap())
.arg("--output")
.arg(temp.path().display().to_string())
.arg("--copy-image")
.arg("left")
.arg("--block-out")
.arg("100,50,350,400");

assert.assert().failure();
temp.assert(predicate::path::eq_file(
fs::canonicalize("./benches/fixtures/tiger-blockout-diff.png").unwrap(),
));

temp.close().unwrap();
}