Skip to content

QR Code Detection Implementation and Add Tests #324

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ kornia-imgproc = { path = "crates/kornia-imgproc", version = "0.1.9-rc.2" }
kornia-3d = { path = "crates/kornia-3d", version = "0.1.9-rc.2" }
kornia = { path = "crates/kornia", version = "0.1.9-rc.2" }
kornia-linalg = { path = "crates/kornia-linalg", version = "0.1.9-rc.2" }
kornia-qr = { path = "crates/kornia-qr", version = "0.1.9-rc.2" }
kernels = { path = "crates/kernels", version = "0.1.9-rc.2" }

# dev dependencies for workspace
Expand Down
25 changes: 25 additions & 0 deletions crates/kornia-qr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "kornia-qr"
description = "QR code detection and decoding for kornia"
version = "0.1.9-rc.2"
edition = "2021"
authors.workspace = true
homepage.workspace = true
license.workspace = true
publish = true
repository.workspace = true
rust-version.workspace = true

[dependencies]
kornia-tensor = { workspace = true }
kornia-image = { workspace = true }
kornia-imgproc = { workspace = true }
image = "0.24"
rqrr = "0.9"
anyhow = "1.0"
thiserror = { workspace = true }
num-traits = { workspace = true }

[dev-dependencies]
kornia-io = { workspace = true }
criterion = { workspace = true }
247 changes: 247 additions & 0 deletions crates/kornia-qr/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
//! QR code detection and decoding for kornia.
//!
//! This crate provides functionality to detect and decode QR codes in images.
//! It uses the `rqrr` crate for actual QR decoding and integrates with
//! kornia's tensor operations for image processing.

use anyhow::Result;
use image::{GrayImage, ImageBuffer};
use kornia_image::Image;
use kornia_tensor::{CpuAllocator, Tensor, TensorError};
use rqrr::PreparedImage;
use thiserror::Error;

/// Error type for QR code detection and decoding operations.
#[derive(Error, Debug)]
pub enum QrError {
/// Error converting tensor to image.
#[error("Failed to convert tensor to image: {0}")]
TensorConversionError(#[from] TensorError),

/// Error in QR decoding.
#[error("Failed to decode QR code: {0}")]
DecodingError(String),

/// Error in image processing.
#[error("Image processing error: {0}")]
ImageProcessingError(String),

/// No QR codes found in the image.
#[error("No QR codes found in the image")]
NoQrCodesFound,
}

/// Result type for QR corners, representing the four corners of a detected QR code.
pub type QrCorners = [[f32; 2]; 4];

/// Information about a detected QR code.
pub struct QrDetection {
/// The decoded content of the QR code.
pub content: String,
/// The corner points of the QR code in the image.
pub corners: QrCorners,
/// The extracted and straightened grayscale image of the QR code.
pub straightened: GrayImage,
/// Error correction level of the QR code.
pub ecc_level: u8,
/// Masking pattern used in the QR code.
pub mask: u8,
/// Version of the QR code.
pub version: u8,
}

/// QR code detector and decoder.
///
/// Provides methods to detect and decode QR codes in images represented as kornia tensors.
pub struct QrDetector;

impl QrDetector {
/// Detects and decodes QR codes in an input image tensor.
///
/// # Arguments
///
/// * `image` - Input image tensor in HWC format (height, width, channels).
///
/// # Returns
///
/// A vector of detected QR codes with their content, corner points, and other metadata.
///
/// # Errors
///
/// Returns an error if conversion, detection, or decoding fails.
pub fn detect_and_decode(
image: &Tensor<u8, 3, CpuAllocator>,
) -> Result<Vec<QrDetection>, QrError> {
// Convert the tensor to an image format that rqrr can process
let grayscale = Self::tensor_to_grayscale(image)?;

// Prepare the image for QR detection
let mut prepared = PreparedImage::prepare(grayscale.clone());

// Detect QR codes
let grids = prepared.detect_grids();

if grids.is_empty() {
return Err(QrError::NoQrCodesFound);
}

// Process detected grids
let mut detections = Vec::with_capacity(grids.len());
for grid in grids {
// Extract corners
let bounds = grid.bounds;
let corners = [
[bounds[0].x as f32, bounds[0].y as f32],
[bounds[1].x as f32, bounds[1].y as f32],
[bounds[2].x as f32, bounds[2].y as f32],
[bounds[3].x as f32, bounds[3].y as f32],
];

// Decode QR content
let (meta, content) = match grid.decode() {
Ok(result) => result,
Err(e) => return Err(QrError::DecodingError(e.to_string())),
};

// Create detection result
detections.push(QrDetection {
content,
corners,
straightened: Self::extract_qr_image(&grayscale, &corners),
ecc_level: meta.ecc_level as u8,
mask: meta.mask as u8,
version: 1, // Default to version 1
});
}

Ok(detections)
}

/// Converts a kornia tensor to a grayscale image.
///
/// # Arguments
///
/// * `tensor` - Input image tensor in HWC format.
///
/// # Returns
///
/// A grayscale image.
///
/// # Errors
///
/// Returns an error if the conversion fails.
fn tensor_to_grayscale(tensor: &Tensor<u8, 3, CpuAllocator>) -> Result<GrayImage, QrError> {
let (height, width, channels) = (tensor.shape[0], tensor.shape[1], tensor.shape[2]);

// If the image is already grayscale, just convert directly
if channels == 1 {
let data = tensor.as_slice().to_vec();
return Ok(
ImageBuffer::from_raw(width as u32, height as u32, data).ok_or_else(|| {
QrError::ImageProcessingError("Failed to create image buffer".to_string())
})?,
);
}

// For RGB or RGBA images, convert to grayscale
let mut gray_data = Vec::with_capacity(height * width);
let data = tensor.as_slice();

for y in 0..height {
for x in 0..width {
let offset = (y * width + x) * channels;

// Simple RGB to grayscale conversion (average method)
let mut sum = 0;
let mut count = 0;

// Sum up available channels (typically R, G, B)
for c in 0..std::cmp::min(3, channels) {
sum += data[offset + c] as u32;
count += 1;
}

// Calculate average and add to grayscale data
let gray_value = if count > 0 { (sum / count) as u8 } else { 0 };
gray_data.push(gray_value);
}
}

Ok(
ImageBuffer::from_raw(width as u32, height as u32, gray_data).ok_or_else(|| {
QrError::ImageProcessingError("Failed to create grayscale image buffer".to_string())
})?,
)
}

/// Extracts a straightened image of the QR code region.
///
/// This function performs a perspective transform to obtain a straightened view of the QR code.
/// Note: This is a simplified version; in a production environment, you'd want to use
/// kornia's perspective warping functionality once fully implemented.
///
/// # Arguments
///
/// * `image` - The grayscale input image.
/// * `corners` - The four corners of the QR code.
///
/// # Returns
///
/// A straightened image of the QR code.
fn extract_qr_image(_image: &GrayImage, _corners: &QrCorners) -> GrayImage {
// This is a simplified placeholder implementation
// In a complete implementation, use kornia's warp_perspective or similar function

// For now, we just create a small empty image
// In real implementation, this would contain the warped QR code region
let size = 100; // Fixed size for placeholder
ImageBuffer::new(size, size)
}
}

/// Trait extension for Image to enable QR code detection.
pub trait QrDetectionExt<T, const C: usize> {
/// Detects and decodes QR codes in the image.
///
/// # Returns
///
/// A vector of detected QR codes with their content, corner points, and other metadata.
///
/// # Errors
///
/// Returns an error if conversion, detection, or decoding fails.
fn detect_qr_codes(&self) -> Result<Vec<QrDetection>, QrError>;
}

impl QrDetectionExt<u8, 3> for Image<u8, 3> {
fn detect_qr_codes(&self) -> Result<Vec<QrDetection>, QrError> {
QrDetector::detect_and_decode(self)
}
}

impl QrDetectionExt<u8, 1> for Image<u8, 1> {
fn detect_qr_codes(&self) -> Result<Vec<QrDetection>, QrError> {
// First convert grayscale image to a 3-channel tensor
// where all 3 channels have the same grayscale value
let (height, width) = (self.shape[0], self.shape[1]);
let mut rgb_data = Vec::with_capacity(height * width * 3);

for pixel in self.as_slice() {
rgb_data.push(*pixel);
rgb_data.push(*pixel);
rgb_data.push(*pixel);
}

let rgb_tensor = Tensor::<u8, 3, CpuAllocator>::from_shape_vec(
[height, width, 3],
rgb_data,
CpuAllocator,
)?;

QrDetector::detect_and_decode(&rgb_tensor)
}
}

// Include the tests module
#[cfg(test)]
mod tests;
84 changes: 84 additions & 0 deletions crates/kornia-qr/src/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#[cfg(test)]
mod tests {

use crate::{QrDetectionExt, QrDetector};
use kornia_image::Image;
use kornia_io::functional as F;
use kornia_tensor::{CpuAllocator, Tensor};
use std::path::Path;

// Test QR code detection with a real image
#[test]
fn test_qr_detection() {
// Load the test image with a QR code
let test_img_path = Path::new("tests/data/qr/kornia.png");

// Skip the test if the test image doesn't exist
if !test_img_path.exists() {
println!("Test image not found: {:?}", test_img_path);
return;
}

// Read the test image
let image: Image<u8, 3> = match F::read_image_any_rgb8(test_img_path) {
Ok(img) => img,
Err(e) => {
println!("Failed to read test image: {}", e);
return;
}
};

// Detect QR codes
let detections = match image.detect_qr_codes() {
Ok(dets) => dets,
Err(e) => {
println!("QR detection failed: {}", e);
return;
}
};

// Check that at least one QR code was detected
assert!(!detections.is_empty(), "No QR codes were detected");

// Check the content of the first QR code
let expected_content = "https://kornia.org";
assert_eq!(detections[0].content, expected_content);
}

// Test the grayscale conversion logic
#[test]
fn test_tensor_to_grayscale() {
// Create a simple RGB tensor
let width = 4;
let height = 4;
let channels = 3;

// Create RGB data where each pixel is (r, g, b) = (100, 150, 200)
let mut rgb_data = Vec::with_capacity(width * height * channels);
for _ in 0..(width * height) {
rgb_data.push(100); // R
rgb_data.push(150); // G
rgb_data.push(200); // B
}

// Create a tensor
let tensor = Tensor::<u8, 3, CpuAllocator>::from_shape_vec(
[height, width, channels],
rgb_data,
CpuAllocator,
)
.unwrap();

// Convert to grayscale
let grayscale = QrDetector::tensor_to_grayscale(&tensor).unwrap();

// Check dimensions
assert_eq!(grayscale.width(), width as u32);
assert_eq!(grayscale.height(), height as u32);

// For RGB (100, 150, 200), the average is 150
for pixel in grayscale.pixels() {
assert_eq!(pixel[0], 150);
}
}
}
1 change: 1 addition & 0 deletions crates/kornia/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ kornia-imgproc.workspace = true
kornia-io = { workspace = true, features = [] }
kornia-3d = { workspace = true }
kornia-icp = { workspace = true }
kornia-qr = { workspace = true }

[lib]
doctest = false
3 changes: 3 additions & 0 deletions crates/kornia/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ pub use kornia_3d as k3d;

#[doc(inline)]
pub use kornia_icp as icp;

#[doc(inline)]
pub use kornia_qr as qr;
10 changes: 10 additions & 0 deletions examples/qr_detector/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "qr_detector"
version = "0.1.0"
edition = "2021"

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

[dependencies]
kornia = { path = "../../crates/kornia" }
anyhow = "1.0"
Loading