Repository: kornia/kornia-rs
Pull request #240: implement fast feature detector
Author: login: edgarriba
Created: 2025-02-24T14:55:35Z | Merged: 2025-04-01T00:39:27Z

![Screenshot from 2025-04-01 02-12-32](https://github.com/user-attachments/assets/432f8f7d-c2a8-4f45-ad56-8e75fcd27585)



Diff:
diff --git a/crates/kornia-imgproc/benches/bench_features.rs b/crates/kornia-imgproc/benches/bench_features.rs
index c9a6c95ac..3edc0143f 100644
--- a/crates/kornia-imgproc/benches/bench_features.rs
+++ b/crates/kornia-imgproc/benches/bench_features.rs
@@ -1,8 +1,37 @@
 use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
 use kornia_image::Image;
-use kornia_imgproc::features::{dog_response, HarrisResponse};
+use kornia_imgproc::{
+    color::gray_from_rgb_u8, features::*, interpolation::InterpolationMode, resize::resize_fast,
+};
+use kornia_io::functional as io;
 use rand::Rng;
 
+fn bench_fast_corner_detect(c: &mut Criterion) {
+    let mut group = c.benchmark_group("FastCornerDetect");
+
+    let img_rgb8 = io::read_image_any_rgb8("/home/edgar/Downloads/kodim08_grayscale.png").unwrap();
+
+    let new_size = [1920, 1080].into();
+    let mut img_resized = Image::from_size_val(new_size, 0).unwrap();
+    resize_fast(&img_rgb8, &mut img_resized, InterpolationMode::Bilinear).unwrap();
+
+    let mut img_gray8 = Image::from_size_val(new_size, 0).unwrap();
+    gray_from_rgb_u8(&img_resized, &mut img_gray8).unwrap();
+
+    let parameter_string = format!("{}x{}", new_size.width, new_size.height);
+
+    group.bench_with_input(
+        BenchmarkId::new("fast_native_cpu", &parameter_string),
+        &(img_gray8),
+        |b, i| {
+            let src = i.clone();
+            b.iter(|| {
+                let _res = black_box(fast_feature_detector(&src, 60, 9)).unwrap();
+            })
+        },
+    );
+}
+
 fn bench_harris_response(c: &mut Criterion) {
     let mut group = c.benchmark_group("Features");
     let mut rng = rand::thread_rng();
@@ -71,7 +100,7 @@ fn bench_dog_response(c: &mut Criterion) {
 criterion_group!(
     name = benches;
     config = Criterion::default().warm_up_time(std::time::Duration::new(10, 0));
-    targets = bench_harris_response, bench_dog_response
+    targets = bench_harris_response, bench_dog_response, bench_fast_corner_detect
 );
 criterion_main!(benches);
 
diff --git a/crates/kornia-imgproc/src/features/fast.rs b/crates/kornia-imgproc/src/features/fast.rs
new file mode 100644
index 000000000..cd6921e6c
--- /dev/null
+++ b/crates/kornia-imgproc/src/features/fast.rs
@@ -0,0 +1,162 @@
+use kornia_image::{Image, ImageError};
+use rayon::prelude::*;
+
+/// Fast feature detector
+///
+/// # Arguments
+///
+/// * `src` - The source image as Gray8 image.
+/// * `threshold` - The threshold for the fast feature detector.
+/// * `arc_length` - The total number of consecutive pixels in the Bresenham circle that must be brighter or darker than the center pixel.
+///
+/// # Returns
+///
+/// A vector containing the coordinates of the detected keypoints.
+pub fn fast_feature_detector(
+    src: &Image<u8, 1>,
+    threshold: u8,
+    arc_length: u8,
+) -> Result<Vec<[i32; 2]>, ImageError> {
+    let (cols, rows) = (src.cols() as i32, src.rows() as i32);
+
+    // Precompute the offsets for the Bresenham circle
+    let offsets = [
+        -3 * cols,     // 1
+        -3 * cols + 1, // 2
+        -2 * cols + 2, // 3
+        -cols + 3,     // 4
+        cols + 3,      // 5
+        cols + 3,      // 6
+        2 * cols + 2,  // 7
+        3 * cols + 1,  // 8
+        3 * cols,      // 9
+        3 * cols - 1,  // 10
+        2 * cols - 2,  // 11
+        cols - 3,      // 12
+        -cols - 3,     // 13
+        -2 * cols - 2, // 14
+        -3 * cols - 1, // 15
+        -3 * cols,     // 16
+    ];
+
+    // Process rows in parallel
+    let keypoints = (3..rows - 3)
+        .into_par_iter()
+        .flat_map(|y| {
+            let row_start_idx = y * cols;
+            let mut row_keypoints = Vec::new();
+
+            for x in 3..cols - 3 {
+                if is_fast_corner(
+                    src.as_slice(),
+                    row_start_idx + x,
+                    offsets,
+                    threshold,
+                    arc_length,
+                ) {
+                    row_keypoints.push([x, y]);
+                }
+            }
+
+            row_keypoints
+        })
+        .collect();
+
+    Ok(keypoints)
+}
+
+fn is_fast_corner(
+    src: &[u8],
+    pixel_idx: i32,
+    offsets: [i32; 16],
+    threshold: u8,
+    arc_length: u8,
+) -> bool {
+    let center_pixel = unsafe { *src.get_unchecked(pixel_idx as usize) };
+    let lower_threshold = &center_pixel.saturating_sub(threshold);
+    let upper_threshold = &center_pixel.saturating_add(threshold);
+
+    // Helper to get pixel value efficiently with unchecked access
+    let get_pixel_from_offset =
+        |off_idx: usize| unsafe { src.get_unchecked((pixel_idx + offsets[off_idx]) as usize) };
+
+    // Fast rejection test - check if at least 3 of the 4 high-speed test points are different enough
+    let p1 = get_pixel_from_offset(0);
+    let p5 = get_pixel_from_offset(4);
+    let p9 = get_pixel_from_offset(8);
+    let p13 = get_pixel_from_offset(12);
+
+    let m0 = !(p5 >= lower_threshold || p1 >= lower_threshold && p9 >= lower_threshold);
+    let m1 = !(p5 <= upper_threshold || p1 <= upper_threshold && p9 <= upper_threshold);
+
+    if !m0 && !m1 {
+        return false;
+    }
+
+    // check the remaining pixels
+    let p2 = get_pixel_from_offset(1);
+    let p3 = get_pixel_from_offset(2);
+    let p4 = get_pixel_from_offset(3);
+    let p6 = get_pixel_from_offset(5);
+    let p7 = get_pixel_from_offset(6);
+    let p8 = get_pixel_from_offset(7);
+    let p10 = get_pixel_from_offset(9);
+    let p11 = get_pixel_from_offset(10);
+    let p12 = get_pixel_from_offset(11);
+    let p14 = get_pixel_from_offset(13);
+    let p15 = get_pixel_from_offset(14);
+    let p16 = get_pixel_from_offset(15);
+    let pixels = [
+        p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, p15, p16,
+    ];
+
+    let mut consecutive_brighter = 0u8;
+    let mut consecutive_darker = 0u8;
+
+    for pixel in pixels {
+        if pixel > upper_threshold {
+            consecutive_brighter += 1;
+            consecutive_darker = 0;
+        } else if pixel < lower_threshold {
+            consecutive_darker += 1;
+            consecutive_brighter = 0;
+        } else {
+            consecutive_brighter = 0;
+            consecutive_darker = 0;
+        }
+
+        if consecutive_brighter >= arc_length || consecutive_darker >= arc_length {
+            return true;
+        }
+    }
+
+    false
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use kornia_image::Image;
+
+    #[test]
+    fn test_fast_feature_detector() -> Result<(), ImageError> {
+        #[rustfmt::skip]
+        let img = Image::new(
+            [7, 7].into(),
+            vec![
+                50,  50,  50,  50,  50,  50,  50,
+                50,  50,  50,  50,  50,  50,  50,
+                50,  50,  50, 200,  50,  50,  50,
+                50,  50, 200, 200, 200,  50,  50,
+                50,  50,  50, 200,  50,  50,  50,
+                50,  50,  50,  50,  50,  50,  50,
+                50,  50,  50,  50,  50,  50,  50,
+            ],
+        )?;
+        let expected_keypoints = vec![[3, 3]];
+        let keypoints = fast_feature_detector(&img, 100, 9)?;
+        assert_eq!(keypoints.len(), expected_keypoints.len());
+        assert_eq!(keypoints, expected_keypoints);
+        Ok(())
+    }
+}
diff --git a/crates/kornia-imgproc/src/features/mod.rs b/crates/kornia-imgproc/src/features/mod.rs
new file mode 100644
index 000000000..f7e4c29b3
--- /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 000000000..32c4b9a15
--- /dev/null
+++ b/examples/fast_detector/Cargo.toml
@@ -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]
+argh = { workspace = true }
+ctrlc = { workspace = true }
+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 000000000..7b187e032
--- /dev/null
+++ b/examples/fast_detector/README.md
@@ -0,0 +1,18 @@
+An example showing how to use detect FAST features on an image.
+
+```bash
+Usage: fast_detector [OPTIONS] --image-path <IMAGE_PATH>
+
+Options:
+  -i, --image-path <IMAGE_PATH>
+  -t, --threshold <THRESHOLD>    [default: 10]
+  -a, --arc-length <ARC_LENGTH>  [default: 5]
+  -h, --help                     Print help
+```
+
+Example:
+
+```bash
+cargo run --bin fast_detector --release -- --image-path ./data/fast_detector/test.jpg
+```
+![Screenshot from 2025-04-01 02-12-32](https://github.com/user-attachments/assets/8e81073b-d246-4dc4-b64e-d7595c66ffd3)
diff --git a/examples/fast_detector/src/main.rs b/examples/fast_detector/src/main.rs
new file mode 100644
index 000000000..d2e2d3d68
--- /dev/null
+++ b/examples/fast_detector/src/main.rs
@@ -0,0 +1,62 @@
+use argh::FromArgs;
+use std::path::PathBuf;
+
+use kornia::{image::Image, imgproc, io::functional as F};
+
+/// Detect FAST features on an image.
+#[derive(FromArgs)]
+struct Args {
+    /// path to the image to detect FAST features on
+    #[argh(option)]
+    image_path: PathBuf,
+
+    /// threshold for the FAST detector
+    #[argh(option, default = "10")]
+    threshold: u8,
+
+    /// arc length for the FAST detector
+    #[argh(option, default = "5")]
+    arc_length: u8,
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let args: Args = argh::from_env();
+
+    // start the recording stream
+    let rec = rerun::RecordingStreamBuilder::new("Kornia Fast Detector App").spawn()?;
+
+    // read the image
+    let img_rgb8 = F::read_image_any_rgb8(args.image_path)?;
+
+    // convert to grayscale
+    let mut img_gray8 = Image::from_size_val(img_rgb8.size(), 0u8)?;
+    imgproc::color::gray_from_rgb_u8(&img_rgb8, &mut img_gray8)?;
+
+    // detect the fast features
+    let keypoints =
+        imgproc::features::fast_feature_detector(&img_gray8, args.threshold, args.arc_length)?;
+    println!("Found {} keypoints", keypoints.len());
+
+    // log the image
+    rec.log_static(
+        "image",
+        &rerun::Image::from_elements(
+            img_rgb8.as_slice(),
+            img_rgb8.size().into(),
+            rerun::ColorModel::RGB,
+        ),
+    )?;
+
+    // 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]]),
+    )?;
+
+    Ok(())
+}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request implements a FAST feature detector using the Kornia library and integrates it into an example application, complete with documentation, dependency configuration, and benchmarks.  
- Introduces a FAST corner detection algorithm in the image processing crate  
- Adds an example binary with CLI for feature detection and its accompanying README  
- Updates benchmarks to include the new FAST feature detector

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| examples/fast_detector/src/main.rs | Example CLI app demonstrating FAST feature detection |
| examples/fast_detector/README.md | Documentation for using the FAST detector example |
| examples/fast_detector/Cargo.toml | Cargo configuration for the example, adding dependencies |
| crates/kornia-imgproc/src/features/mod.rs | Module re-exporting the FAST detection functionality |
| crates/kornia-imgproc/src/features/fast.rs | Implementation of the FAST feature detector algorithm |
| crates/kornia-imgproc/benches/bench_features.rs | Benchmark setup including the new FAST detection benchmark |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:25:47Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This pull request introduces a new fast feature detector using the FAST algorithm with a corresponding example binary, documentation, and benchmarks to demonstrate its performance.  
- Added a command-line tool to process images and detect FAST features.  
- Implemented the FAST detector in the kornia-imgproc crate along with unit tests and benchmark routines.  
- Updated Cargo configuration and README for usage instructions.

### Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                                   | Description                                                                        |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| examples/fast_detector/src/main.rs                     | New CLI application to detect FAST features on a given image.                      |
| examples/fast_detector/README.md                       | Added documentation with usage examples and screenshots.                           |
| examples/fast_detector/Cargo.toml                      | Cargo package configuration for the fast_detector example.                         |
| crates/kornia-imgproc/src/features/mod.rs               | Registered the new FAST feature detector module.                                   |
| crates/kornia-imgproc/src/features/fast.rs              | Implemented the FAST feature detection algorithm and unit tests.                   |
| crates/kornia-imgproc/benches/bench_features.rs         | Added benchmark for the FAST detector alongside existing feature benchmarks.       |
</details>



<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**crates/kornia-imgproc/src/features/fast.rs:29**
* The offset at index 5 duplicates the offset at index 4 (both 'cols + 3'). Confirm if this duplication is intentional for the Bresenham circle; if not, update to the correct offset value.
```
        cols + 3,      // 6
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-01T00:29:59Z

Review comments:
diff_hunk: @@ -0,0 +1,162 @@
+use kornia_image::{Image, ImageError};
+use rayon::prelude::*;
+
+/// Fast feature detector
+///
+/// # Arguments
+///
+/// * `src` - The source image as Gray8 image.
+/// * `threshold` - The threshold for the fast feature detector.
+/// * `arc_length` - The total number of consecutive pixels in the Bresenham circle that must be brighter or darker than the center pixel.
+///
+/// # Returns
+///
+/// A vector containing the coordinates of the detected keypoints.
+pub fn fast_feature_detector(
+    src: &Image<u8, 1>,
+    threshold: u8,
+    arc_length: u8,
+) -> Result<Vec<[i32; 2]>, ImageError> {
+    let (cols, rows) = (src.cols() as i32, src.rows() as i32);
+
+    // Precompute the offsets for the Bresenham circle
+    let offsets = [
+        -3 * rows,     // 1
+        -3 * rows + 1, // 2
+        -2 * rows + 2, // 3
+        -rows + 3,     // 4
+        rows + 3,      // 5
+        rows + 3,      // 6
+        2 * rows + 2,  // 7
+        3 * rows + 1,  // 8
+        3 * rows,      // 9
+        3 * rows - 1,  // 10
+        2 * rows - 2,  // 11
+        rows - 3,      // 12
+        -rows - 3,     // 13
+        -2 * rows - 2, // 14
+        -3 * rows - 1, // 15
+        -3 * rows,     // 16
path: crates/kornia-imgproc/src/features/fast.rs
user: login: Copilot
body: The offsets for the Bresenham circle are computed using 'rows' as the stride; however, the image data is stored in row-major order with a stride equal to 'cols'. Consider replacing 'rows' with 'cols' in the offsets calculations to correctly index neighboring pixels.
```suggestion
        -3 * cols,     // 1
        -3 * cols + 1, // 2
        -2 * cols + 2, // 3
        -cols + 3,     // 4
        cols + 3,      // 5
        cols + 3,      // 6
        2 * cols + 2,  // 7
        3 * cols + 1,  // 8
        3 * cols,      // 9
        3 * cols - 1,  // 10
        2 * cols - 2,  // 11
        cols - 3,      // 12
        -cols - 3,     // 13
        -2 * cols - 2, // 14
        -3 * cols - 1, // 15
        -3 * cols,     // 16
```
created_at: 2025-04-01T00:25:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,37 @@
 use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
 use kornia_image::Image;
-use kornia_imgproc::features::{dog_response, HarrisResponse};
+use kornia_imgproc::{
+    color::gray_from_rgb_u8, features::*, interpolation::InterpolationMode, resize::resize_fast,
+};
+use kornia_io::functional as io;
 use rand::Rng;
 
+fn bench_fast_corner_detect(c: &mut Criterion) {
+    let mut group = c.benchmark_group("FastCornerDetect");
+
+    let img_rgb8 = io::read_image_any_rgb8("/home/edgar/Downloads/kodim08_grayscale.png").unwrap();
path: crates/kornia-imgproc/benches/bench_features.rs
user: login: Copilot
body: The benchmark uses a hardcoded absolute file path, which may not be available on all systems. Consider using a relative or configurable path for improved portability.
```suggestion
use std::env;

fn bench_fast_corner_detect(c: &mut Criterion) {
    let mut group = c.benchmark_group("FastCornerDetect");

    let img_path = env::var("IMAGE_PATH").unwrap_or_else(|_| "relative/path/to/kodim08_grayscale.png".to_string());
    let img_rgb8 = io::read_image_any_rgb8(&img_path).unwrap();
```
created_at: 2025-04-01T00:29:59Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-01T00:29:29Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=kornia_kornia-rs&pullRequest=240) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=kornia_kornia-rs&pullRequest=240&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=kornia_kornia-rs&pullRequest=240&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=kornia_kornia-rs&pullRequest=240&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=kornia_kornia-rs&pullRequest=240&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=kornia_kornia-rs&pullRequest=240&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=kornia_kornia-rs&pullRequest=240)


author_association: NONE