Skip to content
Open
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 78 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,80 @@
# Glance

Glance aims to be a modular computer vision library written in Rust.

## Features

### Image Manipulation
- [x] Create and save images
- [x] Load images from files
- [x] Display images (cross platform)
- [x] Draw shapes
- [ ] Draw text

### Point Operations
- [x] Pixel-wise operations
- [x] Color adjustments (brightness, contrast, gamma)
- [ ] Color space conversions
- [x] RGBA to Grayscale
- [ ] RGBA to HSV
- [ ] RGBA to YUV
- [x] Thresholding
- [x] Histogram equalization
- [ ] Adaptive histogram equalization (CLAHE)

### Linear Filters
- [x] Gaussian blur
- [x] Box blur
- [x] Sobel filter
- [x] Laplacian filter
- [ ] Unsharp masking
- [ ] Frequency domain filtering (FFT-based)

### Non-linear Filters
- [x] Median filter
- [ ] Bilateral filter
- [ ] Noise reduction filters
- [x] Morphological operations
- [x] Erosion
- [x] Dilation

### Detection & Recognition
- [ ] Edge detection (Canny, Harris corner detection)
- [ ] Feature detection and description (SIFT, ORB, FAST)
- [ ] Template matching
- [ ] Contour detection and analysis
- [ ] Blob detection
- [ ] Hough transforms (lines, circles)

### Geometric Transformations
- [ ] Scaling, rotation, translation
- [ ] Perspective transformation
- [ ] Image warping and rectification
- [ ] Image registration and alignment

### Segmentation
- [ ] Watershed segmentation
- [ ] Region growing
- [ ] K-means clustering for segmentation
- [ ] Graph-based segmentation

### Analysis & Metrics
- [ ] Histogram computation and analysis
- [ ] Image statistics (mean, variance, entropy)
- [ ] Image quality metrics (PSNR, SSIM)
- [ ] Connected component analysis

### Advanced Processing
- [ ] Image pyramids (Gaussian, Laplacian)
- [ ] Integral images
- [ ] Distance transforms
- [ ] Convex hull computation

### I/O & Utilities
- [ ] Video frame extraction
- [ ] Batch processing utilities
- [ ] Memory-efficient streaming for large images
- [ ] Zero-copy operations where possible

### Performance & Optimization
- [ ] SIMD optimizations
- [x] Multi-threading support (currently using Rayon)
49 changes: 46 additions & 3 deletions glance-core/src/img/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ where
Ok(())
}

/// Fills the image with the specified color.
pub fn fill(mut self, color: P) -> Self {
self.data.fill(color);
self
}

/// Opens an [`Image`] instance and displays it in a window.
pub fn display(&self, title: &str) -> Result<()> {
let (width, height) = self.dimensions();
Expand Down Expand Up @@ -129,6 +135,19 @@ where
Ok(())
}

/// Vertically stacks two images of the same width.
pub fn vstack(mut self, other: &Self) -> Result<Self> {
if self.width != other.width {
return Err(CoreError::InvalidData(
"Images must have the same width to stack vertically".to_string(),
));
}

self.height += other.height;
self.data.extend(other.data.clone());
Ok(self)
}

/// Returns a reference to the pixel data at the specified position.
/// Returns an error if the position is out of bounds.
pub fn get_pixel(&self, position: (usize, usize)) -> Result<&P> {
Expand Down Expand Up @@ -177,9 +196,11 @@ where
}

impl Image<Rgba> {
/// Min-max normalizes the pixel data in the image.
/// The alpha channel is not modified.
pub fn normalize(&self) -> Self {
// Find the maximum value in the pixel data for each channel
let (max_r, max_g, max_b, max_a) = self
let (max_r, max_g, max_b, _max_a) = self
.par_pixels()
.map(|pixel| (pixel.r, pixel.g, pixel.b, pixel.a))
.reduce(
Expand All @@ -189,7 +210,7 @@ impl Image<Rgba> {
},
);

let (min_r, min_g, min_b, min_a) = self
let (min_r, min_g, min_b, _min_a) = self
.par_pixels()
.map(|pixel| (pixel.r, pixel.g, pixel.b, pixel.a))
.reduce(
Expand All @@ -206,7 +227,7 @@ impl Image<Rgba> {
r: (pixel.r - min_r) / (max_r - min_r),
g: (pixel.g - min_g) / (max_g - min_g),
b: (pixel.b - min_b) / (max_b - min_b),
a: (pixel.a - min_a) / (max_a - min_a),
a: pixel.a,
})
.collect();

Expand All @@ -216,9 +237,21 @@ impl Image<Rgba> {
data: normalized,
}
}

/// Applies a function to each pixel in the image, returning a new image.
/// The alpha channel is not modified.
pub fn apply<F>(mut self, f: F) -> Self
where
F: Fn(f32) -> f32 + Sync + Send,
{
self.par_pixels_mut()
.for_each(|pixel| *pixel = pixel.apply(&f));
self
}
}

impl Image<Luma> {
/// Min-max normalizes the pixel data in the image.
pub fn normalize(&self) -> Self {
// Find the maximum value in the pixel data for each channel
let max_l = self
Expand All @@ -244,4 +277,14 @@ impl Image<Luma> {
data: normalized,
}
}

/// Applies a function to each pixel in the image, returning a new image.
pub fn apply<F>(mut self, f: F) -> Self
where
F: Fn(f32) -> f32 + Sync + Send,
{
self.par_pixels_mut()
.for_each(|pixel| *pixel = pixel.apply(&f));
self
}
}
10 changes: 10 additions & 0 deletions glance-core/src/img/pixel/luma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,13 @@ impl Pixel for Luma {
[l, l, l, 255]
}
}

impl Luma {
pub fn apply<F>(mut self, f: F) -> Self
where
F: Fn(f32) -> f32,
{
self.l = f(self.l);
self
}
}
13 changes: 13 additions & 0 deletions glance-core/src/img/pixel/rgba.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,16 @@ impl From<[u8; 4]> for Rgba {
}
}
}

impl Rgba {
pub fn apply<F>(mut self, f: F) -> Self
where
F: Fn(f32) -> f32,
{
self.r = f(self.r);
self.g = f(self.g);
self.b = f(self.b);
self.a = f(self.a);
self
}
}
60 changes: 60 additions & 0 deletions glance-imgproc/src/affine_transformations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use glance_core::img::{Image, pixel::Luma};
use rayon::iter::{IndexedParallelIterator, ParallelIterator};

pub trait AffineTransformationsExtLuma {
/// Applies an affine transformation to the image.
fn affine_transform(self, matrix: [[f32; 3]; 3]) -> Image<Luma>;
fn rotate(self, angle: f32) -> Image<Luma>;
fn scale(self, scale: (f32, f32)) -> Image<Luma>;
fn translate(self, offset: (f32, f32)) -> Image<Luma>;
}

impl AffineTransformationsExtLuma for Image<Luma> {
fn affine_transform(self, matrix: [[f32; 3]; 3]) -> Image<Luma> {
let (width, height) = self.dimensions();
let new_data = self
.par_pixels()
.enumerate()
.map(|(idx, _pixel)| {
let (x, y) = (idx % width, idx / width);
let new_x =
(matrix[0][0] * x as f32 + matrix[0][1] * y as f32 + matrix[0][2]) as usize;
let new_y =
(matrix[1][0] * x as f32 + matrix[1][1] * y as f32 + matrix[1][2]) as usize;

if new_x < width && new_y < height && new_x > 0 && new_y > 0 {
return self.get_pixel((new_x, new_y)).unwrap().clone();
}

Luma { l: 0.0 }
})
.collect::<Vec<Luma>>();

Image::from_data(width, height, new_data).unwrap()
}

fn rotate(self, angle: f32) -> Image<Luma> {
let cos_angle = angle.cos();
let sin_angle = angle.sin();

let matrix = [
[cos_angle, -sin_angle, 0.0],
[sin_angle, cos_angle, 0.0],
[0.0, 0.0, 1.0],
];

self.affine_transform(matrix)
}

fn scale(self, scale: (f32, f32)) -> Image<Luma> {
let matrix = [[scale.0, 0.0, 0.0], [0.0, scale.1, 0.0], [0.0, 0.0, 1.0]];

self.affine_transform(matrix)
}

fn translate(self, offset: (f32, f32)) -> Image<Luma> {
let matrix = [[1.0, 0.0, offset.0], [0.0, 1.0, offset.1], [0.0, 0.0, 1.0]];

self.affine_transform(matrix)
}
}
106 changes: 106 additions & 0 deletions glance-imgproc/src/kernels.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
use glance_core::img::{Image, pixel::Luma};

pub fn sobel_x() -> Image<Luma> {
Image::from_data(
3,
3,
[-1.0, -2.0, -1.0, 0.0, 0.0, 0.0, 1.0, 2.0, 1.0]
.iter()
.map(|&l| Luma { l })
.collect(),
)
.unwrap()
}

pub fn sobel_y() -> Image<Luma> {
Image::from_data(
3,
3,
[-1.0, 0.0, 1.0, -2.0, 0.0, 2.0, -1.0, 0.0, 1.0]
.iter()
.map(|&l| Luma { l })
.collect(),
)
.unwrap()
}

pub fn laplacian_3x3() -> Image<Luma> {
Image::from_data(
3,
3,
[0.0, 1.0, 0.0, 1.0, -4.0, 1.0, 0.0, 1.0, 0.0]
.iter()
.map(|&l| Luma { l })
.collect(),
)
.unwrap()
}

pub fn box_filter(size: usize) -> Image<Luma> {
let value = 1.0 / (size * size) as f32;
let data: Vec<Luma> = vec![Luma { l: value }; size * size];

Image::from_data(size, size, data).unwrap()
}

pub fn gaussian_filter(size: usize, sigma: f32) -> Image<Luma> {
let mut data = Vec::with_capacity(size * size);
let half_size = size as f32 / 2.0;
let two_sigma_squared = 2.0 * sigma * sigma;

for y in 0..size {
for x in 0..size {
let x_diff = (x as f32 - half_size).powi(2);
let y_diff = (y as f32 - half_size).powi(2);
let value = (-(x_diff + y_diff) / two_sigma_squared).exp()
/ (std::f32::consts::PI * two_sigma_squared);
data.push(Luma { l: value });
}
}

Image::from_data(size, size, data).unwrap()
}

pub enum StructuringElementShape {
Rectangle,
Disk,
Cross,
}
/// Creates a square structuring element of given size, filled with ones.
/// Used in morphological operations. Shape has to be specified.
pub fn structuring_element(shape: StructuringElementShape, size: (usize, usize)) -> Image<Luma> {
let (width, height) = size;

match shape {
StructuringElementShape::Rectangle => {
Image::from_data(width, height, vec![Luma { l: 1.0 }; width * height]).unwrap()
}
StructuringElementShape::Disk => {
let mut data = Vec::with_capacity(width * height);
let half_width = width as f32 / 2.0;
let half_height = height as f32 / 2.0;
for y in 0..height {
for x in 0..width {
let dx = (x as f32 - half_width).powi(2);
let dy = (y as f32 - half_height).powi(2);
if dx + dy <= (half_width * half_width) {
data.push(Luma { l: 1.0 });
} else {
data.push(Luma { l: 0.0 });
}
}
}
Image::from_data(width, height, data).unwrap()
}
StructuringElementShape::Cross => {
let mut data = vec![Luma { l: 0.0 }; width * height];
for i in 0..width {
data[i + (height / 2) * width] = Luma { l: 1.0 };
}
for i in 0..height {
data[(width / 2) + i * width] = Luma { l: 1.0 };
}
Image::from_data(width, height, data).unwrap()
}
}
}
Loading