diff --git a/crates/kornia-imgproc/src/features/fast.rs b/crates/kornia-imgproc/src/features/fast.rs new file mode 100644 index 00000000..fb0f144d --- /dev/null +++ b/crates/kornia-imgproc/src/features/fast.rs @@ -0,0 +1,73 @@ +use kornia_image::{Image, ImageError}; +use rayon::iter::IntoParallelIterator; +use rayon::iter::ParallelIterator; + +/// Fast feature detector +pub fn fast_feature_detector( + src: &Image, + threshold: u8, +) -> Result, ImageError> { + let mut keypoints = Vec::new(); + + let (cols, rows) = (src.cols(), src.rows()); + let src_data = src.as_slice(); + + for y in 3..(rows - 3) { + for x in 3..(cols - 3) { + if is_fast_corner(src_data, x, y, cols, threshold) { + keypoints.push([x, y]); + } + } + } + + Ok(keypoints) +} + +fn is_fast_corner(src: &[u8], x: usize, y: usize, cols: usize, threshold: u8) -> bool { + let current_idx = y * cols + x; + let center_pixel = src[current_idx]; + + let mut darker_count = 0; + let mut brighter_count = 0; + + let offsets = [ + (-3, 0), + (-3, 1), + (-2, 2), + (-1, 3), + (0, 3), + (1, 3), + (2, 2), + (3, 1), + (3, 0), + (3, -1), + (2, -2), + (1, -3), + (0, -3), + (-1, -3), + (-2, -2), + (-3, -1), + ]; + + for (dx, dy) in offsets.iter() { + let nx = x as isize + dx; + let ny = y as isize + dy; + + let neighbor_idx = ny * cols as isize + nx; + let neighbor_pixel = src[neighbor_idx as usize]; + + if neighbor_pixel <= center_pixel.wrapping_sub(threshold) { + darker_count += 1; + } + + if neighbor_pixel >= center_pixel.wrapping_add(threshold) { + brighter_count += 1; + } + + if darker_count >= threshold || brighter_count >= threshold { + return true; + } + } + + false +} diff --git a/crates/kornia-imgproc/src/features/mod.rs b/crates/kornia-imgproc/src/features/mod.rs new file mode 100644 index 00000000..f7e4c29b --- /dev/null +++ b/crates/kornia-imgproc/src/features/mod.rs @@ -0,0 +1,5 @@ +mod responses; +pub use responses::*; + +mod fast; +pub use fast::*; diff --git a/crates/kornia-imgproc/src/features.rs b/crates/kornia-imgproc/src/features/responses.rs similarity index 100% rename from crates/kornia-imgproc/src/features.rs rename to crates/kornia-imgproc/src/features/responses.rs diff --git a/examples/fast_detector/Cargo.toml b/examples/fast_detector/Cargo.toml new file mode 100644 index 00000000..b73328e8 --- /dev/null +++ b/examples/fast_detector/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fast_detector" +version = "0.1.0" +authors = ["Edgar Riba "] +license = "Apache-2.0" +edition = "2021" +publish = false + +[dependencies] +clap = { version = "4.5.4", features = ["derive"] } +ctrlc = "3.4.4" +kornia = { workspace = true, features = ["gstreamer"] } +rerun = { workspace = true } diff --git a/examples/fast_detector/README.md b/examples/fast_detector/README.md new file mode 100644 index 00000000..89b95027 --- /dev/null +++ b/examples/fast_detector/README.md @@ -0,0 +1,21 @@ +An example showing how to use the webcam with the `kornia_io` module with the ability to cancel the feed after a certain amount of time. This example will display the webcam feed in a [`rerun`](https://github.com/rerun-io/rerun) window. + +NOTE: This example requires the gstremer backend to be enabled. To enable the gstreamer backend, use the `gstreamer` feature flag when building the `kornia` crate and its dependencies. + +```bash +Usage: webcam [OPTIONS] + +Options: + -c, --camera-id [default: 0] + -f, --fps [default: 30] + -d, --duration + -h, --help Print help +``` + +Example: + +```bash +cargo run --bin webcam --release -- --camera-id 0 --duration 5 --fps 30 +``` + +![Screenshot from 2024-08-28 18-33-56](https://github.com/user-attachments/assets/783619e4-4867-48bc-b7d2-d32a133e4f5a) diff --git a/examples/fast_detector/src/main.rs b/examples/fast_detector/src/main.rs new file mode 100644 index 00000000..701df426 --- /dev/null +++ b/examples/fast_detector/src/main.rs @@ -0,0 +1,125 @@ +use clap::Parser; +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use kornia::{ + image::{Image, ImageSize}, + imgproc, + io::{fps_counter::FpsCounter, stream::V4L2CameraConfig}, +}; + +#[derive(Parser)] +struct Args { + #[arg(short, long, default_value = "0")] + camera_id: u32, + + #[arg(short, long, default_value = "30")] + fps: u32, + + #[arg(short, long)] + duration: Option, +} + +fn main() -> Result<(), Box> { + let args = Args::parse(); + + // start the recording stream + let rec = rerun::RecordingStreamBuilder::new("Kornia Webcapture App").spawn()?; + + // create a webcam capture object with camera id 0 + // and force the image size to 640x480 + let mut webcam = V4L2CameraConfig::new() + .with_camera_id(args.camera_id) + .with_fps(args.fps) + .with_size(ImageSize { + width: 640, + height: 480, + }) + .build()?; + + // start the background pipeline + webcam.start()?; + + // create a cancel token to stop the webcam capture + let cancel_token = Arc::new(AtomicBool::new(false)); + + // create a shared fps counter + let mut fps_counter = FpsCounter::new(); + + ctrlc::set_handler({ + let cancel_token = cancel_token.clone(); + move || { + println!("Received Ctrl-C signal. Sending cancel signal !!"); + cancel_token.store(true, Ordering::SeqCst); + } + })?; + + // we launch a timer to cancel the token after a certain duration + std::thread::spawn({ + let cancel_token = cancel_token.clone(); + move || { + if let Some(duration_secs) = args.duration { + std::thread::sleep(std::time::Duration::from_secs(duration_secs)); + println!("Sending timer cancel signal !!"); + cancel_token.store(true, Ordering::SeqCst); + } + } + }); + + // preallocate images + let mut gray = Image::from_size_val( + ImageSize { + width: 640, + height: 480, + }, + 0u8, + )?; + + // start grabbing frames from the camera + while !cancel_token.load(Ordering::SeqCst) { + let Some(img) = webcam.grab()? else { + continue; + }; + + // convert the image to grayscale + imgproc::color::gray_from_rgb_u8(&img, &mut gray)?; + + // detect the fast features + let keypoints = imgproc::features::fast_feature_detector(&gray, 10)?; + + fps_counter.update(); + println!("FPS: {}", fps_counter.fps()); + + // log the image + rec.log_static( + "image", + &rerun::Image::from_elements(img.as_slice(), img.size().into(), rerun::ColorModel::RGB), + )?; + + // log the grayscale image + // rec.log_static( + // "gray", + // &rerun::Image::from_elements(gray.as_slice(), gray.size().into(), rerun::ColorModel::L), + // )?; + + // log the keypoints + let points = keypoints + .iter() + .map(|k| (k[0] as f32, k[1] as f32)) + .collect::>(); + + rec.log_static( + "image/keypoints", + &rerun::Points2D::new(points).with_colors([[0, 0, 255]]), + )?; + } + + // NOTE: this is important to close the webcam properly, otherwise the app will hang + webcam.close()?; + + println!("Finished recording. Closing app."); + + Ok(()) +}