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

[wip] implement fast feature detector #240

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
73 changes: 73 additions & 0 deletions crates/kornia-imgproc/src/features/fast.rs
Original file line number Diff line number Diff line change
@@ -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<u8, 1>,
threshold: u8,
) -> Result<Vec<[usize; 2]>, 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
}
5 changes: 5 additions & 0 deletions crates/kornia-imgproc/src/features/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
mod responses;
pub use responses::*;

mod fast;
pub use fast::*;
13 changes: 13 additions & 0 deletions examples/fast_detector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "fast_detector"
version = "0.1.0"
authors = ["Edgar Riba <edgar.riba@gmail.com>"]
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 }
21 changes: 21 additions & 0 deletions examples/fast_detector/README.md
Original file line number Diff line number Diff line change
@@ -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 <CAMERA_ID> [default: 0]
-f, --fps <FPS> [default: 30]
-d, --duration <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)
125 changes: 125 additions & 0 deletions examples/fast_detector/src/main.rs
Original file line number Diff line number Diff line change
@@ -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<u64>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
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::<Vec<_>>();

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(())
}
Loading