Skip to content

Commit e1daaf9

Browse files
kosrkSlesarew
andauthored
feat: qr_reader_pc crate: opencv video input and output (#873)
* 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 <[email protected]>
1 parent e5dd21d commit e1daaf9

File tree

6 files changed

+285
-73
lines changed

6 files changed

+285
-73
lines changed

rust/qr_reader_pc/Cargo.toml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
[package]
22
name = "qr_reader_pc"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
authors = ["vera"]
55
edition = "2018"
66

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

99
[dependencies]
1010
hex = "0.4.3"
11-
rscam = "0.5.5"
12-
image = "0.23.14"
13-
raptorq = "1.6.4"
1411
qr_reader_phone = {path = "../qr_reader_phone"}
15-
quircs = "0.10.0"
1612
anyhow = "1.0.42"
13+
image = "0.23.14"
14+
quircs = "0.10.0"
15+
16+
[dependencies.opencv]
17+
version = "0.60"
18+
default-features = false
19+
features = ["clang-runtime", "videoio", "imgproc", "highgui"]

rust/qr_reader_pc/readme.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# QR reader crate for PC
2+
3+
QR reader crate for PC is a utility to capture (via webcam) QR codes from Signer mobile app
4+
and extracting data from it.
5+
It prints a string with decoded QR message in HEX format on display (and to file "decoded_output.txt").
6+
7+
## Getting Started
8+
9+
### Dependencies
10+
11+
The main requirement is the OpenCV. You can check this manuals: https://crates.io/crates/opencv and https://docs.opencv.org.
12+
13+
#### Arch Linux:
14+
15+
OpenCV package in Arch is suitable for this crate. It requires some dependencies.
16+
17+
* `pacman -S clang qt5-base opencv`
18+
19+
#### Other Linux systems:
20+
21+
* For Debian/Ubuntu also you need: `clang` and `libclang-dev`
22+
* For Gentoo/Fedora also you need: `clang`
23+
* 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.\
24+
Use this manual: https://docs.opencv.org/4.5.3/d7/d9f/tutorial_linux_install.html
25+
26+
### Executing program
27+
28+
* Run the program: `cargo run` + arguments
29+
* Press any key to stop
30+
31+
#### Arguments
32+
33+
* `d` | `-d` | `--device` : set index of camera (from list of available cameras)
34+
* `l` | `-l` | `--list` : get a list of available camera indexes
35+
* `h` | `-h` | `--help` : refers to this manual
36+
37+
Camera resolution is hardcoded (640x480).
38+
39+
#### Examples
40+
41+
* `cargo run d 0` (camera index = 0)
42+
* `cargo run l`
43+
44+

rust/qr_reader_pc/src/lib.rs

Lines changed: 182 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,61 @@
1-
use rscam::{Camera, Config};
2-
use std::io::Write;
3-
use image::{GenericImageView, Pixel, Luma, ImageBuffer, GrayImage};
4-
use quircs;
5-
use hex;
6-
use qr_reader_phone::process_payload::{process_decoded_payload, Ready, InProgress};
1+
#![deny(missing_docs)]
2+
3+
//! # QR reader crate for PC
4+
//!
5+
//! `qr_reader_pc` is a utility to capture (via webcam) QR codes from Signer
6+
//! and extracting data from it.
7+
78
use anyhow::anyhow;
9+
use qr_reader_phone::process_payload::{process_decoded_payload, InProgress, Ready};
10+
use image::{Luma, GrayImage, ImageBuffer};
811

9-
const WIDTH: u32 = 640;
10-
const HEIGHT: u32 = 480;
12+
use opencv::{
13+
highgui,
14+
prelude::*,
15+
Result,
16+
videoio,
17+
videoio::{CAP_PROP_FRAME_HEIGHT, CAP_PROP_FRAME_WIDTH,},
18+
imgproc::{COLOR_BGR2GRAY, cvt_color,},
19+
};
1120

12-
pub fn run_with_camera() -> anyhow::Result<String> {
21+
// Default camera settings
22+
const DEFAULT_WIDTH: u32 = 640;
23+
const DEFAULT_HEIGHT: u32 = 480;
24+
const MAX_CAMERA_INDEX: i32 = 6;
25+
const SKIPPED_FRAMES_QTY: u32 = 10;
1326

14-
let mut camera = match Camera::new("/dev/video0") {
15-
Ok(x) => x,
16-
Err(e) => return Err(anyhow!("Error opening camera. {}", e)),
17-
};
27+
/// Structure for storing camera settings.
28+
#[derive(Debug)]
29+
pub struct CameraSettings {
30+
index: Option<i32>,
31+
}
1832

19-
match camera.start(&Config {
20-
interval: (1, 30), // 30 fps.
21-
resolution: (WIDTH, HEIGHT),
22-
format: b"MJPG",
23-
..Default::default()
24-
}) {
25-
Ok(_) => (),
26-
Err(e) => return Err(anyhow!("Error starting camera. {}", e)),
27-
};
33+
/// Main cycle of video capture.
34+
/// Returns a string with decoded QR message in HEX format or error.
35+
///
36+
/// # Arguments
37+
///
38+
/// * `camera_settings` - CameraSettings struct that holds the camera parameters
39+
pub fn run_with_camera(camera_settings: CameraSettings) -> anyhow::Result<String> {
2840

41+
let camera_index = match camera_settings.index {
42+
Some(index) => index,
43+
None => return Err(anyhow!("There is no camera index.")),
44+
};
45+
46+
let window = "video capture";
47+
highgui::named_window(window, 1)?;
48+
49+
let mut camera = create_camera(camera_index, DEFAULT_WIDTH, DEFAULT_HEIGHT)?;
50+
skip_frames(&mut camera); // clearing old frames if they are in the camera buffer
51+
2952
let mut out = Ready::NotYet(InProgress::None);
3053
let mut line = String::new();
3154

3255
loop {
3356
match out {
3457
Ready::NotYet(decoding) => {
35-
out = match camera_capture(&camera) {
58+
out = match camera_capture(&mut camera, window) {
3659
Ok(img) => process_qr_image (&img, decoding)?,
3760
Err(_) => Ready::NotYet(decoding),
3861
};
@@ -46,59 +69,156 @@ pub fn run_with_camera() -> anyhow::Result<String> {
4669
break;
4770
},
4871
}
72+
73+
if highgui::wait_key(10)? > 0 {
74+
println!("Exit");
75+
break;
76+
};
4977
}
5078
Ok(line)
5179
}
5280

81+
fn create_camera(camera_index: i32, width: u32, height: u32) -> anyhow::Result<videoio::VideoCapture>
82+
{
83+
#[cfg(ocvrs_opencv_branch_32)]
84+
let mut camera = videoio::VideoCapture::new_default(camera_index)?;
85+
#[cfg(not(ocvrs_opencv_branch_32))]
86+
let mut camera = videoio::VideoCapture::new(camera_index, videoio::CAP_ANY)?;
87+
88+
match videoio::VideoCapture::is_opened(&camera) {
89+
Ok(opened) if opened => {
90+
camera.set(CAP_PROP_FRAME_WIDTH, width.into())?;
91+
camera.set(CAP_PROP_FRAME_HEIGHT, height.into())?;
92+
},
93+
Ok(_) => return Err(anyhow!("Camera already opened.")),
94+
Err(e) => return Err(anyhow!("Can`t open camera. {}", e)),
95+
};
96+
97+
let mut frame = Mat::default();
5398

54-
fn camera_capture(camera: &Camera) -> anyhow::Result<ImageBuffer<Luma<u8>, Vec<u8>>> {
55-
let frame = match camera.capture() {
56-
Ok(x) => x,
57-
Err(e) => return Err(anyhow!("Error with camera capture. {}", e)),
99+
match camera.read(&mut frame) {
100+
Ok(_) if frame.size()?.width > 0 => Ok(camera),
101+
Ok(_) => Err(anyhow!("Zero frame size.")),
102+
Err(e) => Err(anyhow!("Can`t read camera. {}", e)),
103+
}
104+
}
105+
106+
fn camera_capture(camera: &mut videoio::VideoCapture, window: &str) -> Result<GrayImage> {
107+
let mut frame = Mat::default();
108+
camera.read(&mut frame)?;
109+
110+
if frame.size()?.width > 0 {
111+
highgui::imshow(window, &frame)?;
58112
};
59-
let mut captured_data: Vec<u8> = Vec::new();
60-
match captured_data.write_all(&frame[..]) {
61-
Ok(_) => (),
62-
Err(e) => return Err(anyhow!("Error writing data from camera into buffer. {}", e)),
113+
114+
let mut image: GrayImage = ImageBuffer::new(DEFAULT_WIDTH, DEFAULT_HEIGHT);
115+
let mut ocv_gray_image = Mat::default();
116+
117+
cvt_color(&frame, &mut ocv_gray_image, COLOR_BGR2GRAY, 0)?;
118+
119+
for y in 0..ocv_gray_image.rows() {
120+
for x in 0..ocv_gray_image.cols() {
121+
let pixel : Luma<u8> = Luma([*ocv_gray_image.at_2d(y,x)?]);
122+
image.put_pixel(x as u32, y as u32, pixel);
123+
};
63124
};
64-
match image::load_from_memory(&captured_data[..]) {
65-
Ok(a) => {
66-
let mut gray_img: GrayImage = ImageBuffer::new(WIDTH, HEIGHT);
67-
for y in 0..HEIGHT {
68-
for x in 0..WIDTH {
69-
let new_pixel = a.get_pixel(x, y).to_luma();
70-
gray_img.put_pixel(x, y, new_pixel);
71-
}
72-
}
73-
// println!("got gray img");
74-
Ok(gray_img)
75-
},
76-
Err(e) => return Err(anyhow!("Error loading data from buffer. {}", e)),
77-
}
125+
126+
Ok(image)
78127
}
79128

80-
fn process_qr_image (img: &ImageBuffer<Luma<u8>, Vec<u8>>, decoding: InProgress) -> anyhow::Result<Ready> {
129+
/// Function for decoding QR grayscale image.
130+
/// Returns a string with decoded QR message in HEX format or error.
131+
///
132+
/// # Arguments
133+
///
134+
/// * `image` - Grayscale image containing QR and background
135+
/// * `decoding` - Stores accumulated payload data for animated QR.
136+
pub fn process_qr_image(image: &GrayImage, decoding: InProgress,) -> anyhow::Result<Ready> {
81137
let mut qr_decoder = quircs::Quirc::new();
82-
let codes = qr_decoder.identify(img.width() as usize, img.height() as usize, img);
138+
let codes = qr_decoder.identify(image.width() as usize, image.height() as usize, image);
139+
83140
match codes.last() {
84-
Some(x) => {
85-
if let Ok(code) = x {
86-
match code.decode() {
87-
Ok(decoded) => {
88-
process_decoded_payload(decoded.payload, decoding)
89-
},
90-
Err(_) => {
91-
// println!("Error with this scan: {}", e);
92-
Ok(Ready::NotYet(decoding))
93-
}
141+
Some(Ok(code)) => {
142+
match code.decode() {
143+
Ok(decoded) => {
144+
process_decoded_payload(decoded.payload, decoding)
145+
},
146+
Err(_) => {
147+
Ok(Ready::NotYet(decoding))
94148
}
95149
}
96-
else {Ok(Ready::NotYet(decoding))}
97-
},
98-
None => {
99-
// println!("no qr in this scan");
100-
Ok(Ready::NotYet(decoding))
101150
},
151+
Some(_) => Ok(Ready::NotYet(decoding)),
152+
None => Ok(Ready::NotYet(decoding)),
153+
}
154+
}
155+
156+
fn print_list_of_cameras() {
157+
let mut indexes: Vec<i32> = vec![];
158+
for dev_port in 0..=MAX_CAMERA_INDEX {
159+
if create_camera(dev_port, DEFAULT_WIDTH, DEFAULT_HEIGHT).is_ok() {
160+
indexes.push(dev_port);
161+
};
162+
};
163+
println!("\nList of available devices:");
164+
for index in indexes {
165+
println!("Camera index: {}", index);
102166
}
103167
}
104168

169+
fn skip_frames(camera: &mut videoio::VideoCapture) {
170+
for _x in 0..SKIPPED_FRAMES_QTY {
171+
if let Ok(false) | Err(_) = camera.grab() {
172+
break;
173+
}
174+
}
175+
}
176+
177+
/// The program's argument parser.
178+
/// The parser initializes the CameraSettings structure with program`s arguments
179+
/// (described in the readme.md file).
180+
pub fn arg_parser(arguments: Vec<String>) -> anyhow::Result<CameraSettings> {
181+
let mut args = arguments.into_iter();
182+
args.next(); // skip program name
183+
184+
let mut settings = CameraSettings {
185+
index: None,
186+
};
187+
188+
while let Some(arg) = args.next() {
189+
let par = match args.next() {
190+
Some(x) => x,
191+
None => String::from(""),
192+
};
193+
194+
match &arg[..] {
195+
"d" | "-d" | "--device" => match par.trim().parse() {
196+
Ok(index) => settings.index = Some(index),
197+
Err(e) => return Err(anyhow!("Camera index parsing error: {}", e)),
198+
},
199+
"h" | "-h" | "--help" => println!("Please read readme.md file."),
200+
"l" | "-l" | "--list" => print_list_of_cameras(),
201+
_ => return Err(anyhow!("Argument parsing error.")),
202+
};
203+
}
204+
205+
match settings.index {
206+
Some(_) => Ok(settings),
207+
None => Err(anyhow!("Need to provide camera index. Please read readme.md file.")),
208+
}
209+
}
210+
211+
#[cfg(test)]
212+
mod tests {
213+
use super::*;
214+
215+
#[test]
216+
fn get_camera_index() {
217+
let arguments: Vec<String> = vec!(
218+
String::from("program_name"),
219+
String::from("d"),
220+
String::from("0"));
221+
let result = arg_parser(arguments).unwrap();
222+
assert_eq!(result.index, Some(0));
223+
}
224+
}

rust/qr_reader_pc/src/main.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
use qr_reader_pc::run_with_camera;
1+
use qr_reader_pc::{arg_parser, run_with_camera};
2+
use std::env;
23

3-
fn main() {
4-
match run_with_camera() {
5-
Ok(line) => println!("Success! {}", line),
6-
Err(e) => println!("Error. {}", e),
4+
5+
fn main() -> Result<(), String> {
6+
7+
let arguments = env::args().collect();
8+
9+
let camera_settings = match arg_parser(arguments) {
10+
Ok(x) => x,
11+
Err(e) => return Err(format!("{}", e)),
12+
};
13+
14+
match run_with_camera(camera_settings) {
15+
Ok(line) => println!("Result HEX: {}", line),
16+
Err(e) => return Err(format!("QR reading error. {}", e)),
717
}
8-
}
918

19+
Ok(())
20+
}

0 commit comments

Comments
 (0)