diff --git a/rust/qr_reader_pc/Cargo.toml b/rust/qr_reader_pc/Cargo.toml index 181fc05b65..fe8b662493 100644 --- a/rust/qr_reader_pc/Cargo.toml +++ b/rust/qr_reader_pc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "qr_reader_pc" -version = "0.1.0" +version = "0.2.0" authors = ["vera"] edition = "2018" @@ -8,9 +8,12 @@ edition = "2018" [dependencies] hex = "0.4.3" -rscam = "0.5.5" -image = "0.23.14" -raptorq = "1.6.4" qr_reader_phone = {path = "../qr_reader_phone"} -quircs = "0.10.0" anyhow = "1.0.42" +image = "0.23.14" +quircs = "0.10.0" + +[dependencies.opencv] +version = "0.60" +default-features = false +features = ["clang-runtime", "videoio", "imgproc", "highgui"] diff --git a/rust/qr_reader_pc/readme.md b/rust/qr_reader_pc/readme.md new file mode 100644 index 0000000000..17aa2b6563 --- /dev/null +++ b/rust/qr_reader_pc/readme.md @@ -0,0 +1,44 @@ +# QR reader crate for PC + +QR reader crate for PC is a utility to capture (via webcam) QR codes from Signer mobile app +and extracting data from it. +It prints a string with decoded QR message in HEX format on display (and to file "decoded_output.txt"). + +## Getting Started + +### Dependencies + +The main requirement is the OpenCV. You can check this manuals: https://crates.io/crates/opencv and https://docs.opencv.org. + +#### Arch Linux: + +OpenCV package in Arch is suitable for this crate. It requires some dependencies. + +* `pacman -S clang qt5-base opencv` + +#### Other Linux systems: + +* For Debian/Ubuntu also you need: `clang` and `libclang-dev` +* For Gentoo/Fedora also you need: `clang` +* It is preferable to build latest version of opencv+opencv_contrib from source. OpenCV package from the system repository may not contain the necessary libraries.\ +Use this manual: https://docs.opencv.org/4.5.3/d7/d9f/tutorial_linux_install.html + +### Executing program + +* Run the program: `cargo run` + arguments +* Press any key to stop + +#### Arguments + +* `d` | `-d` | `--device` : set index of camera (from list of available cameras) +* `l` | `-l` | `--list` : get a list of available camera indexes +* `h` | `-h` | `--help` : refers to this manual + +Camera resolution is hardcoded (640x480). + +#### Examples + +* `cargo run d 0` (camera index = 0) +* `cargo run l` + + diff --git a/rust/qr_reader_pc/src/lib.rs b/rust/qr_reader_pc/src/lib.rs index bd1dfe446f..ae588c2c7b 100644 --- a/rust/qr_reader_pc/src/lib.rs +++ b/rust/qr_reader_pc/src/lib.rs @@ -1,38 +1,61 @@ -use rscam::{Camera, Config}; -use std::io::Write; -use image::{GenericImageView, Pixel, Luma, ImageBuffer, GrayImage}; -use quircs; -use hex; -use qr_reader_phone::process_payload::{process_decoded_payload, Ready, InProgress}; +#![deny(missing_docs)] + +//! # QR reader crate for PC +//! +//! `qr_reader_pc` is a utility to capture (via webcam) QR codes from Signer +//! and extracting data from it. + use anyhow::anyhow; +use qr_reader_phone::process_payload::{process_decoded_payload, InProgress, Ready}; +use image::{Luma, GrayImage, ImageBuffer}; -const WIDTH: u32 = 640; -const HEIGHT: u32 = 480; +use opencv::{ + highgui, + prelude::*, + Result, + videoio, + videoio::{CAP_PROP_FRAME_HEIGHT, CAP_PROP_FRAME_WIDTH,}, + imgproc::{COLOR_BGR2GRAY, cvt_color,}, +}; -pub fn run_with_camera() -> anyhow::Result { +// Default camera settings +const DEFAULT_WIDTH: u32 = 640; +const DEFAULT_HEIGHT: u32 = 480; +const MAX_CAMERA_INDEX: i32 = 6; +const SKIPPED_FRAMES_QTY: u32 = 10; - let mut camera = match Camera::new("/dev/video0") { - Ok(x) => x, - Err(e) => return Err(anyhow!("Error opening camera. {}", e)), - }; +/// Structure for storing camera settings. +#[derive(Debug)] +pub struct CameraSettings { + index: Option, +} - match camera.start(&Config { - interval: (1, 30), // 30 fps. - resolution: (WIDTH, HEIGHT), - format: b"MJPG", - ..Default::default() - }) { - Ok(_) => (), - Err(e) => return Err(anyhow!("Error starting camera. {}", e)), - }; +/// Main cycle of video capture. +/// Returns a string with decoded QR message in HEX format or error. +/// +/// # Arguments +/// +/// * `camera_settings` - CameraSettings struct that holds the camera parameters +pub fn run_with_camera(camera_settings: CameraSettings) -> anyhow::Result { + let camera_index = match camera_settings.index { + Some(index) => index, + None => return Err(anyhow!("There is no camera index.")), + }; + + let window = "video capture"; + highgui::named_window(window, 1)?; + + let mut camera = create_camera(camera_index, DEFAULT_WIDTH, DEFAULT_HEIGHT)?; + skip_frames(&mut camera); // clearing old frames if they are in the camera buffer + let mut out = Ready::NotYet(InProgress::None); let mut line = String::new(); loop { match out { Ready::NotYet(decoding) => { - out = match camera_capture(&camera) { + out = match camera_capture(&mut camera, window) { Ok(img) => process_qr_image (&img, decoding)?, Err(_) => Ready::NotYet(decoding), }; @@ -46,59 +69,156 @@ pub fn run_with_camera() -> anyhow::Result { break; }, } + + if highgui::wait_key(10)? > 0 { + println!("Exit"); + break; + }; } Ok(line) } +fn create_camera(camera_index: i32, width: u32, height: u32) -> anyhow::Result +{ + #[cfg(ocvrs_opencv_branch_32)] + let mut camera = videoio::VideoCapture::new_default(camera_index)?; + #[cfg(not(ocvrs_opencv_branch_32))] + let mut camera = videoio::VideoCapture::new(camera_index, videoio::CAP_ANY)?; + + match videoio::VideoCapture::is_opened(&camera) { + Ok(opened) if opened => { + camera.set(CAP_PROP_FRAME_WIDTH, width.into())?; + camera.set(CAP_PROP_FRAME_HEIGHT, height.into())?; + }, + Ok(_) => return Err(anyhow!("Camera already opened.")), + Err(e) => return Err(anyhow!("Can`t open camera. {}", e)), + }; + + let mut frame = Mat::default(); -fn camera_capture(camera: &Camera) -> anyhow::Result, Vec>> { - let frame = match camera.capture() { - Ok(x) => x, - Err(e) => return Err(anyhow!("Error with camera capture. {}", e)), + match camera.read(&mut frame) { + Ok(_) if frame.size()?.width > 0 => Ok(camera), + Ok(_) => Err(anyhow!("Zero frame size.")), + Err(e) => Err(anyhow!("Can`t read camera. {}", e)), + } +} + +fn camera_capture(camera: &mut videoio::VideoCapture, window: &str) -> Result { + let mut frame = Mat::default(); + camera.read(&mut frame)?; + + if frame.size()?.width > 0 { + highgui::imshow(window, &frame)?; }; - let mut captured_data: Vec = Vec::new(); - match captured_data.write_all(&frame[..]) { - Ok(_) => (), - Err(e) => return Err(anyhow!("Error writing data from camera into buffer. {}", e)), + + let mut image: GrayImage = ImageBuffer::new(DEFAULT_WIDTH, DEFAULT_HEIGHT); + let mut ocv_gray_image = Mat::default(); + + cvt_color(&frame, &mut ocv_gray_image, COLOR_BGR2GRAY, 0)?; + + for y in 0..ocv_gray_image.rows() { + for x in 0..ocv_gray_image.cols() { + let pixel : Luma = Luma([*ocv_gray_image.at_2d(y,x)?]); + image.put_pixel(x as u32, y as u32, pixel); + }; }; - match image::load_from_memory(&captured_data[..]) { - Ok(a) => { - let mut gray_img: GrayImage = ImageBuffer::new(WIDTH, HEIGHT); - for y in 0..HEIGHT { - for x in 0..WIDTH { - let new_pixel = a.get_pixel(x, y).to_luma(); - gray_img.put_pixel(x, y, new_pixel); - } - } -// println!("got gray img"); - Ok(gray_img) - }, - Err(e) => return Err(anyhow!("Error loading data from buffer. {}", e)), - } + + Ok(image) } -fn process_qr_image (img: &ImageBuffer, Vec>, decoding: InProgress) -> anyhow::Result { +/// Function for decoding QR grayscale image. +/// Returns a string with decoded QR message in HEX format or error. +/// +/// # Arguments +/// +/// * `image` - Grayscale image containing QR and background +/// * `decoding` - Stores accumulated payload data for animated QR. +pub fn process_qr_image(image: &GrayImage, decoding: InProgress,) -> anyhow::Result { let mut qr_decoder = quircs::Quirc::new(); - let codes = qr_decoder.identify(img.width() as usize, img.height() as usize, img); + let codes = qr_decoder.identify(image.width() as usize, image.height() as usize, image); + match codes.last() { - Some(x) => { - if let Ok(code) = x { - match code.decode() { - Ok(decoded) => { - process_decoded_payload(decoded.payload, decoding) - }, - Err(_) => { -// println!("Error with this scan: {}", e); - Ok(Ready::NotYet(decoding)) - } + Some(Ok(code)) => { + match code.decode() { + Ok(decoded) => { + process_decoded_payload(decoded.payload, decoding) + }, + Err(_) => { + Ok(Ready::NotYet(decoding)) } } - else {Ok(Ready::NotYet(decoding))} - }, - None => { -// println!("no qr in this scan"); - Ok(Ready::NotYet(decoding)) }, + Some(_) => Ok(Ready::NotYet(decoding)), + None => Ok(Ready::NotYet(decoding)), + } +} + +fn print_list_of_cameras() { + let mut indexes: Vec = vec![]; + for dev_port in 0..=MAX_CAMERA_INDEX { + if create_camera(dev_port, DEFAULT_WIDTH, DEFAULT_HEIGHT).is_ok() { + indexes.push(dev_port); + }; + }; + println!("\nList of available devices:"); + for index in indexes { + println!("Camera index: {}", index); } } +fn skip_frames(camera: &mut videoio::VideoCapture) { + for _x in 0..SKIPPED_FRAMES_QTY { + if let Ok(false) | Err(_) = camera.grab() { + break; + } + } +} + +/// The program's argument parser. +/// The parser initializes the CameraSettings structure with program`s arguments +/// (described in the readme.md file). +pub fn arg_parser(arguments: Vec) -> anyhow::Result { + let mut args = arguments.into_iter(); + args.next(); // skip program name + + let mut settings = CameraSettings { + index: None, + }; + + while let Some(arg) = args.next() { + let par = match args.next() { + Some(x) => x, + None => String::from(""), + }; + + match &arg[..] { + "d" | "-d" | "--device" => match par.trim().parse() { + Ok(index) => settings.index = Some(index), + Err(e) => return Err(anyhow!("Camera index parsing error: {}", e)), + }, + "h" | "-h" | "--help" => println!("Please read readme.md file."), + "l" | "-l" | "--list" => print_list_of_cameras(), + _ => return Err(anyhow!("Argument parsing error.")), + }; + } + + match settings.index { + Some(_) => Ok(settings), + None => Err(anyhow!("Need to provide camera index. Please read readme.md file.")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_camera_index() { + let arguments: Vec = vec!( + String::from("program_name"), + String::from("d"), + String::from("0")); + let result = arg_parser(arguments).unwrap(); + assert_eq!(result.index, Some(0)); + } +} diff --git a/rust/qr_reader_pc/src/main.rs b/rust/qr_reader_pc/src/main.rs index 95aef25b1e..4bc6ab9f66 100644 --- a/rust/qr_reader_pc/src/main.rs +++ b/rust/qr_reader_pc/src/main.rs @@ -1,9 +1,20 @@ -use qr_reader_pc::run_with_camera; +use qr_reader_pc::{arg_parser, run_with_camera}; +use std::env; -fn main() { - match run_with_camera() { - Ok(line) => println!("Success! {}", line), - Err(e) => println!("Error. {}", e), + +fn main() -> Result<(), String> { + + let arguments = env::args().collect(); + + let camera_settings = match arg_parser(arguments) { + Ok(x) => x, + Err(e) => return Err(format!("{}", e)), + }; + + match run_with_camera(camera_settings) { + Ok(line) => println!("Result HEX: {}", line), + Err(e) => return Err(format!("QR reading error. {}", e)), } -} + Ok(()) +} diff --git a/rust/qr_reader_pc/tests/integration_test.rs b/rust/qr_reader_pc/tests/integration_test.rs new file mode 100644 index 0000000000..03c5864df1 --- /dev/null +++ b/rust/qr_reader_pc/tests/integration_test.rs @@ -0,0 +1,34 @@ +use qr_reader_pc::{process_qr_image}; +use image::{open}; +use qr_reader_phone::process_payload::{InProgress, Ready}; + +#[test] +fn check_single_qr_hex() -> Result<(), String> { + let correct_result = String::from("01d43593c715fdd31c61141abd0\ + 4a99fd6822c8558854ccde39a5684e7a56da27d82750682cdb4208cd7c\ + 13bf399b097dad0a8064c45e79a8bc50978f6a8a5db0775bcb4c335897\ + 8ca625496e056f2e7ddf724cf0040e5ff106d06f54efbd95389"); + + let gray_img = match open("./tests/test_qr_1.jpg") { + Ok(x) => x.into_luma8(), + Err(_) => return Err(String::from("File reading error.")), + }; + + let mut result = String::new(); + + match process_qr_image(&gray_img, InProgress::None) { + Ok(x) => match x { + Ready::Yes(a) => result.push_str(&hex::encode(&a)), + Ready::NotYet(_) => return Err(String::from("Waiting animated QR.")), + }, + Err(_) => return Err(String::from("QR image processing error.")), + }; + + if result != correct_result { + println!("Correct result: {}", correct_result); + println!("Decoding result: {}", result); + Err(String::from("Incorrect result")) + } else { + Ok(()) + } +} diff --git a/rust/qr_reader_pc/tests/test_qr_1.jpg b/rust/qr_reader_pc/tests/test_qr_1.jpg new file mode 100644 index 0000000000..b8fad67d9f Binary files /dev/null and b/rust/qr_reader_pc/tests/test_qr_1.jpg differ