Fix image facade to safely compile
This commit is contained in:
parent
d62de8524c
commit
a5204b865b
4 changed files with 212 additions and 46 deletions
92
Cargo.lock
generated
92
Cargo.lock
generated
|
@ -22,6 +22,15 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "android_system_properties"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "1.8.0"
|
version = "1.8.0"
|
||||||
|
@ -234,6 +243,21 @@ version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
||||||
|
dependencies = [
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
"time",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clang"
|
name = "clang"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
|
@ -283,6 +307,12 @@ dependencies = [
|
||||||
"yaml-rust",
|
"yaml-rust",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "cpufeatures"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
|
@ -527,7 +557,7 @@ checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -563,6 +593,29 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.56"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "instant"
|
name = "instant"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -731,6 +784,16 @@ dependencies = [
|
||||||
"minimal-lexical",
|
"minimal-lexical",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-integer"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-traits"
|
name = "num-traits"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
@ -961,6 +1024,7 @@ name = "rust_ocr"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-std",
|
"async-std",
|
||||||
|
"chrono",
|
||||||
"config",
|
"config",
|
||||||
"derivative",
|
"derivative",
|
||||||
"fern",
|
"fern",
|
||||||
|
@ -1136,6 +1200,17 @@ dependencies = [
|
||||||
"syn 2.0.16",
|
"syn 2.0.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.1.45"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.5.11"
|
version = "0.5.11"
|
||||||
|
@ -1191,6 +1266,12 @@ version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.10.0+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
@ -1306,6 +1387,15 @@ version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows"
|
||||||
|
version = "0.48.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.48.0"
|
version = "0.48.0"
|
||||||
|
|
|
@ -7,6 +7,7 @@ edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-std = "1.12.0"
|
async-std = "1.12.0"
|
||||||
|
chrono = "0.4.24"
|
||||||
config = "0.13.3"
|
config = "0.13.3"
|
||||||
derivative = "2.2.0"
|
derivative = "2.2.0"
|
||||||
fern = "0.6.2"
|
fern = "0.6.2"
|
||||||
|
|
|
@ -1,47 +1,53 @@
|
||||||
|
use std::sync::Arc;
|
||||||
use std::{sync::Mutex, thread, collections::VecDeque};
|
use std::{sync::Mutex, thread, collections::VecDeque};
|
||||||
|
|
||||||
use config::*;
|
use config::*;
|
||||||
#[allow(unused_imports)]
|
use opencv::imgcodecs::{IMWRITE_PNG_BILEVEL, IMREAD_UNCHANGED};
|
||||||
use opencv::{videoio::{VideoCapture, VideoCaptureTrait, CAP_PROP_FOURCC,
|
use opencv::{videoio::{VideoCapture, VideoCaptureTrait, CAP_PROP_FOURCC,
|
||||||
CAP_ANY, VideoWriter, CAP_PROP_FRAME_WIDTH,
|
CAP_ANY, VideoWriter, CAP_PROP_FRAME_WIDTH,
|
||||||
CAP_PROP_FRAME_HEIGHT},
|
CAP_PROP_FRAME_HEIGHT},
|
||||||
imgproc::{THRESH_BINARY, COLOR_BGR2GRAY,threshold,cvt_color},
|
imgproc::{THRESH_BINARY, COLOR_BGR2GRAY,threshold,cvt_color},
|
||||||
core::{bitwise_and,Mat},
|
core::{bitwise_and,Rect_,Mat,MatTraitConst,no_array,Vector},
|
||||||
highgui::{select_roi,imshow},
|
highgui::{select_roi,imshow},
|
||||||
imgcodecs::{imread,imwrite,imdecode}};
|
imgcodecs::{imwrite,imread}};
|
||||||
#[allow(unused_imports)]
|
use leptess::LepTess;
|
||||||
use leptess::{leptonica::{pix_read,Pix},
|
|
||||||
tesseract::TessApi};
|
|
||||||
|
|
||||||
const FRAME_WIDTH:i32 = 800;
|
const FRAME_WIDTH:i32 = 800;
|
||||||
const FRAME_HEIGHT:i32 = 600;
|
const FRAME_HEIGHT:i32 = 600;
|
||||||
const MJPG_FOURCC_CODE:i32 = VideoWriter::fourcc('m','j','p','g').unwrap();
|
const CROP_X:&str = "crop x";
|
||||||
|
const CROP_Y:&str = "crop y";
|
||||||
|
const CROP_WIDTH:&str = "crop width";
|
||||||
|
const CROP_HEIGHT:&str = "crop height";
|
||||||
|
const THRESHOLD:&str = "threshold value";
|
||||||
|
const COMPOSITE_FRAMES:u16 = 5;
|
||||||
|
|
||||||
pub struct Camera{
|
pub struct Camera{
|
||||||
settings: Config,
|
settings: Config,
|
||||||
camera: FrameGrabber,
|
camera: FrameGrabber,
|
||||||
name: String,
|
name: String,
|
||||||
ocr: TessApi,
|
ocr: LepTess,
|
||||||
|
active: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
struct FrameGrabber{
|
struct FrameGrabber{
|
||||||
camera: VideoCapture,
|
image_queue: Arc<Mutex<VecDeque<Mat>>>,
|
||||||
image_queue: Mutex<VecDeque<Mat>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrameGrabber{
|
impl FrameGrabber{
|
||||||
fn new(camera:VideoCapture, composite_frames:i64) -> Self{
|
fn new(mut camera:VideoCapture, composite_frames:u16) -> Self{
|
||||||
let mut output = Self{
|
let output = Self{
|
||||||
camera,
|
image_queue: Arc::new(Mutex::new(VecDeque::with_capacity(composite_frames as usize)))
|
||||||
image_queue: Mutex::new(VecDeque::with_capacity(composite_frames as usize))
|
|
||||||
};
|
};
|
||||||
|
let image_queue = output.image_queue.clone();
|
||||||
thread::spawn(move||{
|
thread::spawn(move||{
|
||||||
loop {
|
loop {
|
||||||
let mut image = Mat::default();
|
let mut image = Mat::default();
|
||||||
let result = output.camera.read(&mut image);
|
let result = &camera.read(&mut image);
|
||||||
match result{
|
match result{
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
let mut image_queue = *output.image_queue.lock().unwrap();
|
let mut converted_image = Mat::default();
|
||||||
|
_ = cvt_color(&mut image, &mut converted_image, COLOR_BGR2GRAY, 0);
|
||||||
|
let mut image_queue = image_queue.lock().unwrap();
|
||||||
if image_queue.len() == image_queue.capacity(){
|
if image_queue.len() == image_queue.capacity(){
|
||||||
image_queue.pop_back();
|
image_queue.pop_back();
|
||||||
}
|
}
|
||||||
|
@ -54,60 +60,130 @@ impl FrameGrabber{
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn grab(&self, mut image:&Mat){
|
fn grab(&self) -> Mat{
|
||||||
let mut image_queue = *self.image_queue.lock().unwrap();
|
let mut image_queue = self.image_queue.lock().unwrap();
|
||||||
image = &mut image_queue.pop_front().unwrap();
|
image_queue.pop_front().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn burst(&self) -> Vec<Mat>{
|
fn burst(&self) -> Vec<Mat>{
|
||||||
let mut image_queue = *self.image_queue.lock().unwrap();
|
let image_queue = self.image_queue.lock().unwrap();
|
||||||
return image_queue.clone().into();
|
return image_queue.clone().into();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn change_composite_frames(&self, composite_frames:u16){
|
||||||
|
let mut image_queue = self.image_queue.lock().unwrap();
|
||||||
|
image_queue.resize(composite_frames as usize, Default::default());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Camera{
|
impl Camera{
|
||||||
pub fn new(camera_name:String) -> Option<Self>{
|
pub fn new(camera_name:String) -> Option<Self>{
|
||||||
let defaults = Config::builder();
|
let mut defaults = Config::builder();
|
||||||
defaults.set_default("crop x", 275);
|
defaults = defaults.set_default(CROP_X, 275).unwrap();
|
||||||
defaults.set_default("crop y", 200);
|
defaults = defaults.set_default(CROP_Y, 200).unwrap();
|
||||||
defaults.set_default("crop width", 80);
|
defaults = defaults.set_default(CROP_WIDTH, 80).unwrap();
|
||||||
defaults.set_default("crop height", 50);
|
defaults = defaults.set_default(CROP_HEIGHT, 50).unwrap();
|
||||||
defaults.set_default("threshold value", 50);
|
defaults = defaults.set_default(THRESHOLD, 50).unwrap();
|
||||||
defaults.set_default("composite frames", 5);
|
|
||||||
defaults.set_default("active", true);
|
|
||||||
let default = defaults.build().unwrap();
|
let default = defaults.build().unwrap();
|
||||||
|
|
||||||
let settings = Config::builder().add_source(default).build().unwrap();
|
let settings = Config::builder().add_source(default).build().unwrap();
|
||||||
|
|
||||||
let mut camera = VideoCapture::from_file(&camera_name, CAP_ANY).unwrap();
|
let mut camera = VideoCapture::from_file(&camera_name, CAP_ANY).unwrap();
|
||||||
camera.set(CAP_PROP_FOURCC, MJPG_FOURCC_CODE as f64);
|
_ = camera.set(CAP_PROP_FOURCC, VideoWriter::fourcc('m','j','p','g').unwrap() as f64);
|
||||||
camera.set(CAP_PROP_FRAME_WIDTH, FRAME_WIDTH as f64);
|
_ = camera.set(CAP_PROP_FRAME_WIDTH, FRAME_WIDTH as f64);
|
||||||
camera.set(CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT as f64);
|
_ = camera.set(CAP_PROP_FRAME_HEIGHT, FRAME_HEIGHT as f64);
|
||||||
if let result = camera.open_file(&camera_name, CAP_ANY){
|
match camera.open_file(&camera_name, CAP_ANY) {
|
||||||
match result {
|
Ok(_) => {},
|
||||||
Ok(_) => {},
|
Err(_) => return None
|
||||||
Err(error) => return None
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let frame_grabber = FrameGrabber::new(camera, settings.get_int("composite frames").unwrap_or(1));
|
let frame_grabber = FrameGrabber::new(camera, COMPOSITE_FRAMES);
|
||||||
|
|
||||||
let name = camera_name.split('-').last().unwrap().to_string();
|
let name = camera_name.split('-').last().unwrap().to_string();
|
||||||
|
|
||||||
let ocr = TessApi::new(Some("tessdata"), "Pro6_temp_test").unwrap();
|
let ocr = LepTess::new(Some("tessdata"), "Pro6_temp_test").unwrap();
|
||||||
|
|
||||||
Some(Self{
|
Some(Self{
|
||||||
settings,
|
settings,
|
||||||
camera: frame_grabber,
|
camera: frame_grabber,
|
||||||
name,
|
name,
|
||||||
ocr,
|
ocr,
|
||||||
|
active: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn take_picture(&mut self) -> Mat{
|
pub fn show_image(&self){
|
||||||
let mut image = Mat::default();
|
_ = imshow("Test image", &imread(&self.complete_process(),IMREAD_UNCHANGED).unwrap());
|
||||||
self.camera.grab(&mut image);
|
}
|
||||||
let mut output = Mat::default();
|
|
||||||
cvt_color(&mut image, &mut output, COLOR_BGR2GRAY, 0);
|
fn complete_process(&self) -> String{
|
||||||
return output;
|
let images = self.camera.burst();
|
||||||
|
let mut final_image = images[0].clone();
|
||||||
|
for mut image in images{
|
||||||
|
image = self.crop(image);
|
||||||
|
image = self.threshold(image);
|
||||||
|
_ = bitwise_and(&final_image.clone(), &image, &mut final_image, &no_array());
|
||||||
|
}
|
||||||
|
return self.save(final_image).unwrap_or("".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crop(&self,image:Mat) -> Mat{
|
||||||
|
let x = self.settings.get_int(&CROP_X).unwrap();
|
||||||
|
let y = self.settings.get_int(&CROP_Y).unwrap();
|
||||||
|
let width = self.settings.get_int(&CROP_WIDTH).unwrap();
|
||||||
|
let height = self.settings.get_int(&CROP_HEIGHT).unwrap();
|
||||||
|
let roi = Rect_::new(x as i32, y as i32, width as i32, height as i32);
|
||||||
|
return image.apply_1(roi).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn threshold(&self,image:Mat) -> Mat{
|
||||||
|
let mut output = image.clone();
|
||||||
|
let threshold_value = self.settings.get_int(&THRESHOLD).unwrap();
|
||||||
|
_ = threshold(&image,&mut output, threshold_value as f64, 255 as f64 ,THRESH_BINARY);
|
||||||
|
return Mat::default();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&self,image:Mat) -> Result<String,opencv::Error>{
|
||||||
|
let mut write_parameters:Vector<i32> = Vector::new();
|
||||||
|
write_parameters.push(IMWRITE_PNG_BILEVEL);
|
||||||
|
let mut filename:String = String::new();
|
||||||
|
filename.push_str(&chrono::Local::now().to_rfc3339().to_string());
|
||||||
|
filename.push_str(&self.name);
|
||||||
|
match imwrite(&filename, &image, &write_parameters){
|
||||||
|
Ok(_) => Ok(filename),
|
||||||
|
Err(error) => Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_crop(&mut self) -> Result<(),opencv::Error>{
|
||||||
|
match select_roi(&self.camera.grab(), false, false){
|
||||||
|
Err(error) => { return Err(error); }
|
||||||
|
Ok(rect) => {
|
||||||
|
let mut new_settings = Config::builder().add_source(self.settings.clone());
|
||||||
|
new_settings = new_settings.set_override(CROP_X, rect.x).unwrap();
|
||||||
|
new_settings = new_settings.set_override(CROP_Y, rect.y).unwrap();
|
||||||
|
new_settings = new_settings.set_override(CROP_WIDTH, rect.width).unwrap();
|
||||||
|
new_settings = new_settings.set_override(CROP_HEIGHT, rect.height).unwrap();
|
||||||
|
self.settings = new_settings.build().unwrap();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_threshold(&mut self, thresh:u16){
|
||||||
|
let new_settings = Config::builder().add_source(self.settings.clone()).set_override(THRESHOLD, thresh);
|
||||||
|
self.settings = new_settings.expect("Bad config settings!").build().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_composite_frames(&self, composite_frames:u16){
|
||||||
|
self.camera.change_composite_frames(composite_frames);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deactivate(&mut self){ self.active = false; }
|
||||||
|
pub fn activate(&mut self) { self.active = true; }
|
||||||
|
pub fn is_active(&self) -> bool { self.active }
|
||||||
|
|
||||||
|
pub fn parse_image(&mut self, file_location:String) -> f64{
|
||||||
|
_ = self.ocr.set_image(file_location);
|
||||||
|
return str::parse(&self.ocr.get_utf8_text().unwrap()).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod config_facade;
|
|
||||||
pub mod gpio_facade;
|
pub mod gpio_facade;
|
||||||
pub mod image_facade;
|
pub mod image_facade;
|
||||||
pub mod output_facade;
|
pub mod output_facade;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue