Skip to content

Initial setup for cv-sift. #40

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

Draft
wants to merge 21 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3efc488
Initial commit. Setup cv-sift crate.
aalekhpatel07 Aug 28, 2021
196e7f2
Update LICENSE template.
aalekhpatel07 Aug 28, 2021
ae8073d
Add simple utility to play around with images. Test the code using a …
aalekhpatel07 Aug 30, 2021
f90c0b2
Add dummy driver to test the cv-sift implementation.
aalekhpatel07 Aug 30, 2021
431d719
Add some utils and test for image pyramid generation.
aalekhpatel07 Sep 5, 2021
787b328
Make image generic over numeric primitives, remove dependency of imgp…
aalekhpatel07 Sep 5, 2021
bb415ac
Add method to generate scale space pyramid and add tests and sample o…
aalekhpatel07 Sep 5, 2021
c2e53ea
Sample driver to construct scale_space_pyramid.
aalekhpatel07 Sep 5, 2021
a930a19
Add method to generate scale space pyramid for difference of gaussians.
aalekhpatel07 Sep 6, 2021
ad93e61
Add sample output of difference of gaussians scale space.
aalekhpatel07 Sep 6, 2021
1fea231
WIP
aalekhpatel07 Oct 28, 2021
c01542a
Merge branch 'rust-cv:main' into main
aalekhpatel07 Jun 21, 2023
9fdff9a
Clean up.
aalekhpatel07 Jun 21, 2023
8d5e5bd
Revert "WIP"
aalekhpatel07 Jun 21, 2023
15b6dab
Rewrite the basics.
aalekhpatel07 Jun 21, 2023
87aee49
Introduce test fixture data.
aalekhpatel07 Jun 21, 2023
173bfcf
Remove generated data from res
aalekhpatel07 Jun 21, 2023
7c0fe49
Add subtraction and some tests for it.
aalekhpatel07 Jun 26, 2023
1895455
Remove LD_LIBRARY_PATH from .cargo/config.toml
aalekhpatel07 Jun 26, 2023
c3a4359
Bump nalgebra and itertools.
aalekhpatel07 Jun 26, 2023
27f4226
Refactor and update docs.
aalekhpatel07 Jun 26, 2023
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Cargo.lock

# MacOS
.DS_Store
.idea/
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"cv-pinhole",
"cv-optimize",
"cv-sfm",
"cv-sift",
"akaze",
"eight-point",
"lambda-twist",
Expand Down
2 changes: 2 additions & 0 deletions cv-sift/.cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[env]
RUST_LOG="trace"
1 change: 1 addition & 0 deletions cv-sift/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
testdata
27 changes: 27 additions & 0 deletions cv-sift/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "cv-sift"
version = "0.1.0"
edition = "2018"
authors = ["Aalekh Patel <[email protected]>"]
description = "SIFT feature extraction algorithm for computer vision"
keywords = ["keypoint", "descriptor", "vision", "sfm", "slam", "sift"]
categories = ["computer-vision", "science::robotics"]
repository = "https://github.com/rust-cv/cv"
documentation = "https://docs.rs/cv-sift/"
license = "MIT"
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# cv = { version = "0.6.0", path="../cv" }
image = { version = "0.24.6" }
nalgebra = "0.32.2"
num = "0.4"
itertools = "0.11.0"
tracing = "0.1.37"
thiserror = "1.0.40"

[dev-dependencies]
test-case = "3.1.0"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
21 changes: 21 additions & 0 deletions cv-sift/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2021 Aalekh Patel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
5 changes: 5 additions & 0 deletions cv-sift/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SIFT

Implementation of [SIFT](https://docs.opencv.org/master/da/df5/tutorial_py_sift_intro.html), one of the first breakthroughs in Computer Vision that introduced feature descriptors and keypoints to us common folks.

## WIP
27 changes: 27 additions & 0 deletions cv-sift/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@


#[derive(Debug, Clone, Copy)]
pub struct SIFTConfig {
pub sigma: f64,
pub num_intervals: usize,
pub assumed_blur: f64,
pub image_border_width: usize
}


impl Default for SIFTConfig {
fn default() -> Self {
SIFTConfig {
sigma: 1.6,
num_intervals: 3,
assumed_blur: 0.5,
image_border_width: 5
}
}
}

impl SIFTConfig {
pub fn new() -> Self {
SIFTConfig::default()
}
}
68 changes: 68 additions & 0 deletions cv-sift/src/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use image::{DynamicImage};
use crate::ImageRgb32F;
use crate::errors::{Result, SIFTError};


pub fn try_get_rgb_32f(img: &DynamicImage) -> Result<ImageRgb32F> {
match img {

DynamicImage::ImageRgb8(p)
=> {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| pixel.0.iter().cloned().map(|v| v as f32)).collect::<Vec<_>>()
)
.unwrap())
},
DynamicImage::ImageRgb32F(p)
=> {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| pixel.0.iter().cloned()).collect::<Vec<_>>()
)
.unwrap())
},
DynamicImage::ImageRgba32F(p)
=> {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| pixel.0.iter().cloned()).collect::<Vec<_>>()
)
.unwrap())
},
DynamicImage::ImageLuma16(p) => {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| [pixel.0[0] as f32; 3]).collect::<Vec<_>>()
).unwrap())
}
DynamicImage::ImageLumaA16(p) => {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| [pixel.0[0] as f32; 3]).collect::<Vec<_>>()
).unwrap())
}
DynamicImage::ImageLuma8(p) => {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| [pixel.0[0] as f32; 3]).collect::<Vec<_>>()
).unwrap())
}
DynamicImage::ImageLumaA8(p) => {
Ok(ImageRgb32F::from_raw(
p.width(),
p.height(),
p.pixels().into_iter().flat_map(|pixel| [pixel.0[0] as f32; 3]).collect::<Vec<_>>()
).unwrap())
}
_ => {
Err(SIFTError::Unsupported("ImageRgb32F image can only be one of ImageLuma8, ImageLumaA8, ImageLuma16, ImageLumaA16, ImageImageRgb32F, ImageRgba32F.".to_string()))
}
}
}
9 changes: 9 additions & 0 deletions cv-sift/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use thiserror::Error;

#[derive(Debug, Error)]
pub enum SIFTError {
#[error("{0}")]
Unsupported(String),
}

pub type Result<T, E = SIFTError> = std::result::Result<T, E>;
38 changes: 38 additions & 0 deletions cv-sift/src/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use crate::ImageRgb32F;
use image::Pixel;

/// How close do we want two floats to be
/// before they're considered equal.
pub const TOLERANCE: f32 = 1e-8;

pub trait ImageExt {
fn is_zero(&self) -> bool;
fn is_same_as(&self, other: &Self) -> bool;
}

impl ImageExt for ImageRgb32F {
fn is_zero(&self) -> bool {
self
.pixels()
.all(
|pixel|
pixel
.channels()
.iter()
.find(|p| (**p).abs() > TOLERANCE)
.is_none()
)
}

fn is_same_as(&self, other: &Self) -> bool {

for (own_pixel, other_pixel) in self.pixels().zip(other.pixels()) {
for (self_p, other_p) in own_pixel.channels().iter().zip(other_pixel.channels().iter()) {
if (*self_p - *other_p).abs() > TOLERANCE {
return false;
}
}
}
true
}
}
12 changes: 12 additions & 0 deletions cv-sift/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
pub mod config;
pub mod pyramid;
// Expose all utils.
pub mod utils;
pub mod conversion;
// #[cfg(test)]
pub mod ext;
mod errors;

pub type ImageRgb32F = image::ImageBuffer<image::Rgb<f32>, Vec<f32>>;

pub use errors::*;
107 changes: 107 additions & 0 deletions cv-sift/src/pyramid/base_image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use std::convert::TryInto;
use image::{DynamicImage, Rgb};
use tracing::{
trace,
debug
};
use image::imageops;
use crate::ImageRgb32F;
use crate::errors::Result;
use image::Pixel;
use crate::conversion::try_get_rgb_32f;



/// Scale the image by a factor of 2 and apply some blur.
/// # Examples
/// ```
/// use cv_sift::pyramid::generate_base_image;
/// use image::{DynamicImage};
///
/// let img = image::open("tests/fixtures/box.png").unwrap();
/// assert_eq!(img.height(), 223);
/// assert_eq!(img.width(), 324);
/// let base_img = generate_base_image(&img, 1.6, 0.5).unwrap();
/// assert_eq!(base_img.height(), 446);
/// assert_eq!(base_img.width(), 648);
///
/// ```
pub fn generate_base_image(
img: &DynamicImage,
sigma: f64,
assumed_blur: f64
) -> Result<ImageRgb32F> {
let (height, width) = (img.height(), img.width());

trace!(
height,
width,
sigma,
assumed_blur,
"Computing base image"
);

let scaled = img.resize(
width * 2,
height * 2,
imageops::FilterType::Triangle
);

trace!(
height = height * 2,
width = width * 2,
interpolation = "linear",
"Scaled image."
);

let final_sigma = {
let sigma_val = sigma * sigma - 4.0 * assumed_blur * assumed_blur;
if sigma_val > 0.01 {
sigma_val.sqrt()
} else {
0.01_f64.sqrt()
}
};

debug!(final_sigma, "Computed final_sigma for blurring.");

let blurred = scaled.blur(final_sigma as f32);
try_get_rgb_32f(&blurred)
}


/// Returns a difference of two images.
pub fn subtract(minuend: &ImageRgb32F, subtrahend: &ImageRgb32F) -> Result<ImageRgb32F> {
assert_eq!(minuend.height(), subtrahend.height());
assert_eq!(minuend.width(), subtrahend.width());

let (width, height) = (minuend.width() as u32, minuend.height() as u32);

let mut result_mat = ImageRgb32F::new(width, height);
let mut result_mat_pixels = result_mat.pixels_mut();

for (minuend_pixel, subtrahend_pixel) in minuend.pixels().zip(subtrahend.pixels()){

let output_pixel: [f32; 3] =
minuend_pixel
.channels()
.iter()
.zip(
subtrahend_pixel
.channels()
.iter()
)
.map(|(minuend_p, subtrahend_p)| {
*minuend_p - *subtrahend_p
})
.collect::<Vec<f32>>()
.try_into()
.unwrap();

let next_pixel = result_mat_pixels.next().unwrap();
*next_pixel = Rgb(output_pixel);
}
Ok(result_mat)
}


36 changes: 36 additions & 0 deletions cv-sift/src/pyramid/kernels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// List of gaussian kernels at which to blur the input image.
/// # Examples
/// ```
/// use cv_sift::pyramid::gaussian_kernels;
/// use cv_sift::utils::assert_similar;
///
/// let kernels = gaussian_kernels(1.6, 3);
/// let expected: [f64; 6] = [
/// 1.6,
/// 1.2262735,
/// 1.54500779,
/// 1.94658784,
/// 2.452547,
/// 3.09001559
/// ];
/// assert_similar(&kernels, &expected);
/// ```
pub fn gaussian_kernels(
sigma: f64,
num_intervals: usize
) -> Vec<f64> {

let images_per_octave = num_intervals + 3;
let k: f64 = (2.0_f64).powf(1.0 / num_intervals as f64);
let mut kernels: Vec<f64> = vec![0.0; images_per_octave];

kernels[0] = sigma;
for (idx, item) in kernels.iter_mut().enumerate().take(images_per_octave).skip(1) {
let sigma_previous = (k.powf(idx as f64 - 1.0)) * sigma;
let sigma_total = k * sigma_previous;
*item = (sigma_total.powf(2.0) - sigma_previous.powf(2.0)).sqrt();

}

kernels
}
8 changes: 8 additions & 0 deletions cv-sift/src/pyramid/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod base_image;
pub use base_image::*;

mod kernels;
pub use kernels::*;

mod octaves;
pub use octaves::*;
Loading