Skip to content

Commit

Permalink
feat: qr_reader_pc crate: opencv video input and output (#873)
Browse files Browse the repository at this point in the history
* rscam replaced by nokhwa (crossplatform video capture) crate. Added video output using minifb crate.

* Program argument processing added. Added display of the list of available devices.

* Improved parser of program arguments.

* Program argument parser improvements. Added structure for camera config storing.

* Argument parser refactoring.

* Documentation added.

* implementation with opencv

* QR decoding added. Extra code removed. Documentation updated.

* Quircs crate returned (due to problems with the opencv QR decoder). Unit and integration tests added. QR decoding function added to API.

* Readme file updated.

* Opencv version update. Extra dependencies are removed.

* Doc fix.

* fix: remove extra spaces and add line break

* fix: crate version 0.2.0

* fix: skipping frames to prevent cpaturing old frames from the camera buffer

Co-authored-by: Slesarew <33295157+Slesarew@users.noreply.github.com>
  • Loading branch information
kosrk and Slesarew authored Nov 22, 2021
1 parent e5dd21d commit e1daaf9
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 73 deletions.
13 changes: 8 additions & 5 deletions rust/qr_reader_pc/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
[package]
name = "qr_reader_pc"
version = "0.1.0"
version = "0.2.0"
authors = ["vera"]
edition = "2018"

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

[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"]
44 changes: 44 additions & 0 deletions rust/qr_reader_pc/readme.md
Original file line number Diff line number Diff line change
@@ -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`


244 changes: 182 additions & 62 deletions rust/qr_reader_pc/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<String> {
// 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<i32>,
}

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<String> {

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),
};
Expand All @@ -46,59 +69,156 @@ pub fn run_with_camera() -> anyhow::Result<String> {
break;
},
}

if highgui::wait_key(10)? > 0 {
println!("Exit");
break;
};
}
Ok(line)
}

fn create_camera(camera_index: i32, width: u32, height: u32) -> anyhow::Result<videoio::VideoCapture>
{
#[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<ImageBuffer<Luma<u8>, Vec<u8>>> {
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<GrayImage> {
let mut frame = Mat::default();
camera.read(&mut frame)?;

if frame.size()?.width > 0 {
highgui::imshow(window, &frame)?;
};
let mut captured_data: Vec<u8> = 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<u8> = 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<Luma<u8>, Vec<u8>>, decoding: InProgress) -> anyhow::Result<Ready> {
/// 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<Ready> {
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<i32> = 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<String>) -> anyhow::Result<CameraSettings> {
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<String> = vec!(
String::from("program_name"),
String::from("d"),
String::from("0"));
let result = arg_parser(arguments).unwrap();
assert_eq!(result.index, Some(0));
}
}
23 changes: 17 additions & 6 deletions rust/qr_reader_pc/src/main.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
Loading

0 comments on commit e1daaf9

Please sign in to comment.