Skip to content

Commit

Permalink
Implement block out area (#18)
Browse files Browse the repository at this point in the history
Add `-b, --block-out` CLI option
  • Loading branch information
jihchi authored Aug 7, 2021
1 parent 6bc7631 commit a04b03b
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 32 deletions.
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();
}

0 comments on commit a04b03b

Please sign in to comment.