diff --git a/Cargo.toml b/Cargo.toml index 90b6278..953ccd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,22 +23,24 @@ bench = false [dependencies] nalgebra = { version = "0.32.1", default-features = false, features = [ "std" ] } -image = { version = "0.24.5", default-features = false } -imageproc = { version = "0.23.0", default-features = false } +image = { version = "0.25.1", default-features = false } +imageproc = { version = "0.24.0", default-features = false } rand = "0.8.5" similarity-least-squares = "0.2.0" rand_xoshiro = "0.6.0" derive_builder = "0.12.0" +pixelutil-image = "0.1" +thiserror = "1.0" [dev-dependencies] -image = "0.24.5" -imageproc = "0.23.0" +image = "0.25.1" +imageproc = "0.24.0" criterion = "0.4.0" approx = "0.5.0" rstest = "0.17.0" clap = { version = "4.1.8", features = ["derive"] } anyhow = "1.0" -rusttype = { version = "0.9.3", default-features = false } +ab_glyph = "0.2.23" [dev-dependencies.cargo-husky] version = "1.5.0" diff --git a/benches/multiscaler.rs b/benches/multiscaler.rs index 8b6198f..22ee911 100644 --- a/benches/multiscaler.rs +++ b/benches/multiscaler.rs @@ -43,11 +43,7 @@ pub fn bench_run(c: &mut Criterion) { group.sample_size(10000); for size in SIZES.iter().map(|s| Size::from(*s)) { - let ms = Multiscaler::builder() - .min_size(100) - .max_size(size.height) - .build() - .unwrap(); + let ms = Multiscaler::new(100, size.height, 0.1, 1.1).unwrap(); let id = BenchmarkId::from_parameter(size); diff --git a/examples/detect-faces/args.rs b/examples/detect-faces/args.rs index 637f071..978cd82 100644 --- a/examples/detect-faces/args.rs +++ b/examples/detect-faces/args.rs @@ -75,17 +75,13 @@ impl Args { pub fn init(&self, image: &DynamicImage) -> Result<(DetectMultiscale, LocalizePerturbate)> { Ok(( DetectMultiscale::builder() - .multiscaler( - Multiscaler::builder() - .min_size(self.min_size) - .scale_factor(self.scale_factor) - .shift_factor(self.shift_factor) - .max_size( - self.max_size - .unwrap_or_else(|| image.height().min(image.width())), - ) - .build()?, - ) + .multiscaler(Multiscaler::new( + self.min_size, + self.max_size + .unwrap_or_else(|| image.height().min(image.width())), + self.scale_factor, + self.shift_factor, + )?) .clusterizer(Clusterizer { intersection_threshold: self.intersection_threshold, score_threshold: self.score_threshold, @@ -104,4 +100,4 @@ impl Args { pub fn parse() -> Args { Args::parse() -} \ No newline at end of file +} diff --git a/examples/detect-faces/main.rs b/examples/detect-faces/main.rs index bfbca0a..fe1486c 100644 --- a/examples/detect-faces/main.rs +++ b/examples/detect-faces/main.rs @@ -12,17 +12,19 @@ mod utils; use rand::SeedableRng; use rand_xoshiro::Xoroshiro128PlusPlus; -use anyhow::{anyhow, Context, Result}; +use ab_glyph::FontRef; +use anyhow::{Context, Result}; use face::Face; use shape::Shape5; use utils::{draw_face, print_faces_data}; -use rusttype::{Font, Scale}; - fn main() -> Result<()> { let args = args::parse(); + let font = FontRef::try_from_slice(include_bytes!("../../assets/DejaVuSansDigits.ttf")) + .expect("Failed to load font."); + let image = image::open(&args.input).context("Failed to load image file.")?; let (detector, localizer, shaper) = args.load_models()?; @@ -59,23 +61,11 @@ fn main() -> Result<()> { let mut rgb = image.into_rgb8(); - let height = 12.0; - let scale = Scale { - x: height, - y: height, - }; - let font = load_font()?; - for face in faces.iter() { - draw_face(&mut rgb, &face, &font, scale); + draw_face(&mut rgb, &face, &font, 12.0); } rgb.save(args.output).context("Cannot write output image")?; Ok(()) } - -fn load_font<'a>() -> Result> { - Font::try_from_bytes(include_bytes!("../../assets/DejaVuSansDigits.ttf")) - .ok_or(anyhow!("Cannot load font.")) -} diff --git a/examples/detect-faces/shape.rs b/examples/detect-faces/shape.rs index 52978e2..cca5903 100644 --- a/examples/detect-faces/shape.rs +++ b/examples/detect-faces/shape.rs @@ -51,8 +51,8 @@ impl Shape5 { let rh = rs / 2.0; ( - Square::new((l.x - lh) as i64, (l.y - lh) as i64, ls as u32), - Square::new((r.x - rh) as i64, (r.y - rh) as i64, rs as u32), + Square::new((l.x - lh) as i32, (l.y - lh) as i32, ls as u32), + Square::new((r.x - rh) as i32, (r.y - rh) as i32, rs as u32), ) } } \ No newline at end of file diff --git a/examples/detect-faces/utils.rs b/examples/detect-faces/utils.rs index fc9acb5..753acd5 100644 --- a/examples/detect-faces/utils.rs +++ b/examples/detect-faces/utils.rs @@ -1,10 +1,10 @@ +use ab_glyph::FontRef; use image::{Rgb, RgbImage}; use imageproc::drawing; -use rusttype::{Font, Scale}; use crate::face::Face; -pub fn draw_face(image: &mut RgbImage, face: &Face, font: &Font, scale: Scale) { +pub fn draw_face(image: &mut RgbImage, face: &Face, font: &FontRef<'_>, scale: f32) { drawing::draw_hollow_rect_mut(image, face.region.into(), Rgb([0, 0, 255])); let color = Rgb([0, 255, 0]); diff --git a/examples/print-init-shape.rs b/examples/print-init-shape.rs index c42b19d..cdbb23c 100644 --- a/examples/print-init-shape.rs +++ b/examples/print-init-shape.rs @@ -1,7 +1,7 @@ use std::{fs::File, io::BufReader}; use std::path::PathBuf; -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result}; use clap::Parser; use image::{Rgb, RgbImage}; use imageproc::{ @@ -11,7 +11,8 @@ use imageproc::{ rect::Rect, }; use pico_detect::Shaper; -use rusttype::{Font, Scale}; +use ab_glyph::FontRef; + #[derive(Parser, Debug)] #[command(author, version, about = "Print init points from shaper model.")] @@ -25,6 +26,7 @@ struct Args { fn main() -> Result<()> { let args = Args::parse(); + let font = FontRef::try_from_slice(include_bytes!("../assets/DejaVuSansDigits.ttf")).expect("Failed to load font."); let file = BufReader::new(File::open(&args.model_path).context("Failed to open model file.")?); let shaper = Shaper::load(file).context("Error during model loading.")?; @@ -50,8 +52,7 @@ fn main() -> Result<()> { let mut image = RgbImage::new(rect.width() + padding * 2, rect.height() + padding * 2); let color = Rgb::from([0u8, 255u8, 0u8]); - let scale = Scale { x: 20.0, y: 20.0 }; - let font = load_font()?; + let scale = 20.0; let radius = 5; for (i, point) in points.iter().enumerate() { @@ -75,8 +76,3 @@ fn main() -> Result<()> { Ok(()) } - -fn load_font<'a>() -> Result> { - Font::try_from_bytes(include_bytes!("../assets/DejaVuSansDigits.ttf")) - .ok_or(anyhow!("Cannot load font.")) -} diff --git a/examples/run-pico-model.rs b/examples/run-pico-model.rs index 165f883..e339fa2 100644 --- a/examples/run-pico-model.rs +++ b/examples/run-pico-model.rs @@ -25,10 +25,10 @@ struct Args { image_path: PathBuf, #[arg(long, default_value_t = 0)] - top: i64, + top: i32, #[arg(long, default_value_t = 0)] - left: i64, + left: i32, #[arg(long)] size: Option, @@ -60,14 +60,14 @@ fn main() -> Result<()> { ModelType::Localizer => { let localizer = Localizer::load(file)?; let point = localizer.localize(&image, square.into()); - println!("{},{}", point.x as i64, point.y as i64); + println!("{},{}", point.x as i32, point.y as i32); } ModelType::Shaper => { let shaper = Shaper::load(file)?; let shape = shaper.shape(&image, square.into()); println!("i,x,y"); for (i, point) in shape.iter().enumerate() { - println!("{},{},{}", i, point.x as i64, point.y as i64); + println!("{},{},{}", i, point.x as i32, point.y as i32); } } } diff --git a/src/detect/multiscale.rs b/src/detect/multiscale.rs index 695e3ea..ec7c848 100644 --- a/src/detect/multiscale.rs +++ b/src/detect/multiscale.rs @@ -1,51 +1,58 @@ -use derive_builder::Builder; use imageproc::rect::Rect; +use thiserror::Error; use crate::geometry::Square; -#[derive(Copy, Clone, Debug, Builder, PartialEq)] -#[builder(build_fn(validate = "Multiscaler::validate"))] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct Multiscaler { min_size: u32, max_size: u32, - #[builder(default = "0.1")] shift_factor: f32, - #[builder(default = "1.1")] scale_factor: f32, } +#[derive(Debug, Error)] +pub enum MultiscalerError { + #[error("`min_size` should be non zero")] + MinSizeIsZero, + #[error("`max_size` should be greater than `min_size`")] + MaxSizeLessThanMinSize, + #[error("`shift_factor` should be in `(0, 1]` range")] + ShiftFactorOutOfRange, + #[error("`scale_factor` should be greater than 1")] + ScaleFactorLessThanOne, +} + impl Multiscaler { #[inline] - pub fn validate(builder: &MultiscalerBuilder) -> Result<(), String> { - if let Some(value) = builder.min_size { - if value == 0 { - return Err("`min_size` should be non zero".into()); - } + pub fn new( + min_size: u32, + max_size: u32, + shift_factor: f32, + scale_factor: f32, + ) -> Result { + if min_size == 0 { + return Err(MultiscalerError::MinSizeIsZero); } - if let Some((min_size, max_size)) = builder.min_size.zip(builder.max_size) { - if min_size > max_size { - return Err("`max_size` should be greater than `min_size`".into()); - } + if min_size > max_size { + return Err(MultiscalerError::MaxSizeLessThanMinSize); } - if let Some(value) = builder.shift_factor { - if !(0.0..=1.0).contains(&value) { - return Err("`shift_factor` should be in `(0, 1]` range".into()); - } + if !(0.0..=1.0).contains(&shift_factor) { + return Err(MultiscalerError::ShiftFactorOutOfRange); } - if let Some(value) = builder.scale_factor { - if value < 1.0 { - return Err("`scale_factor` should be greater than `1.0`".into()); - } - }; - - Ok(()) - } + if scale_factor < 1.0 { + return Err(MultiscalerError::ScaleFactorLessThanOne); + } - pub fn builder() -> MultiscalerBuilder { - Default::default() + Ok(Self { + min_size, + max_size, + shift_factor, + scale_factor, + }) } pub fn min_size(&self) -> u32 { @@ -122,7 +129,7 @@ pub fn multiscale( for y in (start_y..=end_y).step_by(step) { for x in (start_x..=end_x).step_by(step) { - f(Square::new(x as i64, y as i64, size)) + f(Square::new(x, y, size)) } } size = (sizef * scale_factor) as u32; @@ -135,14 +142,7 @@ mod tests { #[test] fn test_multiscale_run() { - let ms = Multiscaler::builder() - .min_size(1) - .max_size(4) - .scale_factor(2.0) - .shift_factor(1.0) - .build() - .unwrap(); - + let ms = Multiscaler::new(1, 4, 1.0, 2.0).unwrap(); ms.run(Rect::at(0, 0).of_size(4, 4), |s| println!("{:?}", s)); } } diff --git a/src/geometry/square.rs b/src/geometry/square.rs index 252239d..c82f999 100644 --- a/src/geometry/square.rs +++ b/src/geometry/square.rs @@ -4,14 +4,14 @@ use crate::traits::region::Region; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Square { - pub(crate) left: i64, - pub(crate) top: i64, + pub(crate) left: i32, + pub(crate) top: i32, pub(crate) size: u32, } impl Square { #[inline] - pub fn at(x: i64, y: i64) -> Self { + pub fn at(x: i32, y: i32) -> Self { Self { left: x, top: y, @@ -39,7 +39,7 @@ impl Square { } #[inline] - pub fn new(left: i64, top: i64, size: u32) -> Self { + pub fn new(left: i32, top: i32, size: u32) -> Self { Self { left, top, size } } @@ -51,12 +51,12 @@ impl Square { impl Region for Square { #[inline] - fn left(&self) -> i64 { + fn left(&self) -> i32 { self.left } #[inline] - fn top(&self) -> i64 { + fn top(&self) -> i32 { self.top } @@ -76,14 +76,14 @@ impl Region for Square { } } -impl From<(i64, i64, u32)> for Square { - fn from(value: (i64, i64, u32)) -> Self { +impl From<(i32, i32, u32)> for Square { + fn from(value: (i32, i32, u32)) -> Self { Self::new(value.0, value.1, value.2) } } impl From for Rect { fn from(value: Square) -> Self { - Self::at(value.left as i32, value.top as i32).of_size(value.size, value.size) + Self::at(value.left, value.top).of_size(value.size, value.size) } } diff --git a/src/geometry/target.rs b/src/geometry/target.rs index 484bfe2..9c18914 100644 --- a/src/geometry/target.rs +++ b/src/geometry/target.rs @@ -43,13 +43,13 @@ impl Target { impl Region for Target { #[inline] - fn left(&self) -> i64 { - (self.point.x - self.size / 2.0) as i64 + fn left(&self) -> i32 { + (self.point.x - self.size / 2.0) as i32 } #[inline] - fn top(&self) -> i64 { - (self.point.y - self.size / 2.0) as i64 + fn top(&self) -> i32 { + (self.point.y - self.size / 2.0) as i32 } #[inline] @@ -68,15 +68,15 @@ impl Region for Target { } #[inline] - fn center(&self) -> Point2 { - Point2::new(self.x() as i64, self.y() as i64) + fn center(&self) -> Point2 { + Point2::new(self.x() as i32, self.y() as i32) } } impl From for Rect { #[inline] fn from(value: Target) -> Self { - Self::at(value.left() as i32, value.top() as i32).of_size(value.width(), value.height()) + Self::at(value.left(), value.top()).of_size(value.width(), value.height()) } } diff --git a/src/imageutils.rs b/src/imageutils.rs deleted file mode 100644 index bdc0315..0000000 --- a/src/imageutils.rs +++ /dev/null @@ -1,66 +0,0 @@ -use image::{GenericImageView, Luma}; -use nalgebra::Point2; - -#[inline] -pub fn get_nearest_pixel_i64(image: &I, x: i64, y: i64) -> I::Pixel -where - I: GenericImageView, -{ - let (x0, y0, w, h) = image_bounds_as_i64(image); - - let x1 = x0 + w - 1; - let y1 = y0 + h - 1; - - #[allow(clippy::manual_clamp)] - unsafe { image.unsafe_get_pixel(x.max(x0).min(x1) as u32, y.max(y0).min(y1) as u32) } -} - -#[inline] -pub fn get_nearest_luma_by_point(image: &I, point: Point2) -> T -where - I: GenericImageView>, -{ - get_nearest_pixel_i64(image, point.x, point.y).0[0] -} - -#[inline] -pub fn get_luma_by_point_f32(image: &I, point: Point2) -> Option -where - I: GenericImageView>, -{ - get_pixel_i64(image, point.x as i64, point.y as i64).map(|p| p.0[0]) -} - -#[inline] -pub fn get_luma_by_point(image: &I, point: Point2) -> Option -where - I: GenericImageView>, -{ - get_pixel_i64(image, point.x, point.y).map(|p| p.0[0]) -} - -#[inline] -pub fn image_bounds_as_i64(image: &I) -> (i64, i64, i64, i64) -where - I: GenericImageView, -{ - let (ix, iy, iw, ih) = image.bounds(); - (ix as i64, iy as i64, iw as i64, ih as i64) -} - -#[inline] -pub fn in_bounds_i64(image: &I, x: i64, y: i64) -> bool -where - I: GenericImageView, -{ - let (ix, iy, iw, ih) = image_bounds_as_i64(image); - x >= ix && x < ix + iw && y >= iy && y < iy + ih -} - -#[inline] -pub fn get_pixel_i64(image: &I, x: i64, y: i64) -> Option -where - I: GenericImageView, -{ - in_bounds_i64(image, x, y).then(|| unsafe { image.unsafe_get_pixel(x as u32, y as u32) }) -} diff --git a/src/lib.rs b/src/lib.rs index ec145b6..ad0a319 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,6 @@ extern crate derive_builder; extern crate approx; mod geometry; -mod imageutils; mod nodes; mod traits; diff --git a/src/localize/localizer/mod.rs b/src/localize/localizer/mod.rs index cfe8aac..563bdbf 100644 --- a/src/localize/localizer/mod.rs +++ b/src/localize/localizer/mod.rs @@ -52,7 +52,7 @@ impl Localizer { for stage in self.stages.iter() { let mut translation = Translation2::identity(); - let p = Point2::new(point.x as i64, point.y as i64); + let p = unsafe { point.coords.try_cast::().unwrap_unchecked() }.into(); let s = size as u32; for (codes, preds) in stage.iter() { diff --git a/src/nodes/comparison.rs b/src/nodes/comparison.rs index e292a64..b3f8cb8 100644 --- a/src/nodes/comparison.rs +++ b/src/nodes/comparison.rs @@ -1,7 +1,7 @@ use image::{GenericImageView, Luma}; use nalgebra::Point2; -use crate::imageutils::get_nearest_luma_by_point; +use pixelutil_image::clamp_pixel_unchecked; #[derive(Debug, PartialEq, Clone, Copy, Default)] pub struct ComparisonNode(pub Point2, pub Point2); @@ -46,40 +46,40 @@ impl ComparisonNode { pub fn bintest>>( &self, image: &I, - point: Point2, + point: Point2, size: u32, ) -> bool { let p0 = transform(point, size, self.0.cast()); let p1 = transform(point, size, self.1.cast()); - let lum0 = get_nearest_luma_by_point(image, p0); - let lum1 = get_nearest_luma_by_point(image, p1); + let lum0 = unsafe { clamp_pixel_unchecked(image, p0.x, p0.y) }.0[0]; + let lum1 = unsafe { clamp_pixel_unchecked(image, p1.x, p1.y) }.0[0]; lum0 > lum1 } } #[allow(dead_code)] -const SCALE: i64 = u8::MAX as i64 + 1; +const SCALE: i32 = u8::MAX as i32 + 1; const SHIFT: i32 = 8; #[allow(dead_code)] #[inline] -fn na_transform(i: Point2, s: u32, n: Point2) -> Point2 { - (i * SCALE + n.coords * (s as i64)) / SCALE +fn na_transform(i: Point2, s: u32, n: Point2) -> Point2 { + (i * SCALE + n.coords * (s as i32)) / SCALE } #[inline] -fn transform(i: Point2, s: u32, n: Point2) -> Point2 { - let (x, y) = original_transform(i.x, i.y, s, n.x, n.y); +fn transform(i: Point2, s: u32, n: Point2) -> Point2 { + let (x, y) = original_transform(i.x, i.y, s as i32, n.x, n.y); Point2::new(x, y) } #[allow(dead_code)] #[inline] -fn original_transform(ix: i64, iy: i64, s: u32, nx: i64, ny: i64) -> (i64, i64) { - let x = ((ix << SHIFT) + nx * (s as i64)) >> SHIFT; - let y = ((iy << SHIFT) + ny * (s as i64)) >> SHIFT; +fn original_transform(ix: i32, iy: i32, s: i32, nx: i32, ny: i32) -> (i32, i32) { + let x = ((ix << SHIFT) + nx * s) >> SHIFT; + let y = ((iy << SHIFT) + ny * s) >> SHIFT; (x, y) } diff --git a/src/shape/forest.rs b/src/shape/forest.rs index b9ea10d..3d309ae 100644 --- a/src/shape/forest.rs +++ b/src/shape/forest.rs @@ -3,8 +3,7 @@ use std::io::{Error, Read}; use image::{GenericImageView, Luma}; use nalgebra::{Affine2, Point2, SimilarityMatrix2}; - -use crate::imageutils::get_luma_by_point_f32; +use pixelutil_image::get_pixel; use super::delta::ShaperDelta; use super::tree::ShaperTree; @@ -53,8 +52,9 @@ impl ShaperForest { let point = unsafe { shape.get_unchecked(delta.anchor()) }; let point = point + transform_to_shape.transform_vector(delta.value()); let point = transform_to_image * point; + let point = unsafe { point.coords.try_cast::().unwrap_unchecked() }; - get_luma_by_point_f32(image, point).unwrap_or(0u8) + get_pixel(image, point.x, point.y).map(|p| p.0[0]).unwrap_or(0u8) }) .collect() } diff --git a/src/traits/region.rs b/src/traits/region.rs index 6111ec6..cbd9b69 100644 --- a/src/traits/region.rs +++ b/src/traits/region.rs @@ -1,19 +1,19 @@ use nalgebra::Point2; pub trait Region { - fn left(&self) -> i64; - fn top(&self) -> i64; + fn left(&self) -> i32; + fn top(&self) -> i32; fn width(&self) -> u32; fn height(&self) -> u32; #[inline] - fn right(&self) -> i64 { - self.left() + (self.width() - 1) as i64 + fn right(&self) -> i32 { + self.left() + (self.width() - 1) as i32 } #[inline] - fn bottom(&self) -> i64 { - self.top() + (self.height() - 1) as i64 + fn bottom(&self) -> i32 { + self.top() + (self.height() - 1) as i32 } #[inline] @@ -22,20 +22,20 @@ pub trait Region { } #[inline] - fn contains(&self, x: i64, y: i64) -> bool { + fn contains(&self, x: i32, y: i32) -> bool { self.left() <= x && self.top() <= y && self.right() >= x && self.bottom() >= y } #[inline] - fn center(&self) -> Point2 { + fn center(&self) -> Point2 { Point2::new( - self.left() + (self.width() / 2 + 1) as i64, - self.top() + (self.height() / 2 + 1) as i64, + self.left() + (self.width() / 2 + 1) as i32, + self.top() + (self.height() / 2 + 1) as i32, ) } #[inline] - fn top_left(&self) -> Point2 { + fn top_left(&self) -> Point2 { Point2::new(self.left(), self.top()) } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1196ffb..1c2b3df 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -98,13 +98,7 @@ pub fn shaper_case(test_image: GrayImage) -> (GrayImage, Square, Vec #[fixture] pub fn multiscaler(test_image: GrayImage) -> Multiscaler { - Multiscaler::builder() - .min_size(100) - .max_size(test_image.width()) - .shift_factor(0.05) - .scale_factor(1.1) - .build() - .unwrap() + Multiscaler::new(100, test_image.width(), 0.05, 1.1).unwrap() } #[fixture]