Skip to content

Commit

Permalink
v0.1.0: added upload to Telegam
Browse files Browse the repository at this point in the history
  • Loading branch information
vitali2y committed Mar 3, 2024
1 parent 4d2bc23 commit 6e99a17
Show file tree
Hide file tree
Showing 8 changed files with 205 additions and 57 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
/opencv
/output
Cargo.lock
config/aicam.toml
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
name = "aicam"
version = "0.1.0"
edition = "2021"
description = "AI face detection app based on opencv and libfacedetection libs"
description = "AI face detection & notification app (opencv + libfacedetection)"
authors = ["Vi+ <[email protected]>"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
Expand All @@ -13,10 +13,13 @@ path = "src/main.rs"

[dependencies]
libfacedetection = { path = "./libfacedetection-rs/libfacedetection-rs" }
thiserror = "1.0.57"
opencv = "0.88.8"
chrono = "0.4.34"
serde = "1.0.197"
toml = "0.8.10"
argh = "0.1.12"
once_cell = "1.19.0"
log = "0.4.21"
env_logger = "0.11.2"
reqwest = { version = "0.11.24", features = ["blocking", "json", "multipart"] }
tokio = { version = "1.36.0", features = ["full"] }
28 changes: 21 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Based on `opencv` and `libfacedetection` libs

<hr/>

- [aicam](#aicam)
- [`AI`-based face detection app for video surveillance and notification](#ai-based-face-detection-app-for-video-surveillance-and-notification)
- [1. General](#1-general)
- [2. OpenCV lib](#2-opencv-lib)
- [2.1 Getting sources](#21-getting-sources)
Expand All @@ -27,8 +29,9 @@ Based on `opencv` and `libfacedetection` libs

### 1. General

TBD
This application performs face recognition (usually in motion detection) followed by notification by uploading captured images to your `Telegram` group.

**IMPORTANT:** please do not forget to configure both `token` and `channel` params under `telegram` section of your `config/aicam.toml` config file.

### 2. OpenCV lib

Expand Down Expand Up @@ -61,16 +64,27 @@ TBD
Cloning into 'libfacedetection-rs'...
~...~
➜ aicam_libfacedetection git:(master) ✗ export LD_LIBRARY_PATH=./dist/x86_64:$LD_LIBRARY_PATH
➜ aicam_libfacedetection git:(master) ✗ mold -run cargo r # or, just: cargo run
➜ aicam_libfacedetection git:(master) ✗ RUST_LOG=debug mold -run cargo r # or, just: RUST_LOG=debug cargo run
Updating crates.io index
~...~
Finished dev [unoptimized + debuginfo] target(s) in 25.70s
Running `target/debug/aicam_libfacedetection`
Available camera:
Width: 640
Height: 480
Face: Face { confidence: 63, x: 390, y: 4, width: 231, height: 193, landmarks: [(472, 45), (555, 20), (534, 79), (511, 132), (579, 110)] }
Saving to ./aicam_20240225094529.jpg
aicam 0.1.0
AI face detection & notification app (opencv + libfacedetection)
[2024-03-03T13:50:53Z DEBUG aicam] camera /dev/video0 (640x480) is running...
[2024-03-03T13:50:56Z DEBUG aicam] face: Face { confidence: 94, x: 220, y: 240, width: 262, height: 223, landmarks: [(271, 398), (378, 410), (296, 480), (269, 524), (360, 534)] }
[2024-03-03T13:50:56Z DEBUG aicam] saving to ./output/aicam_20240303155056.jpg
[2024-03-03T13:50:56Z DEBUG aicam::upload] uploading ./output/aicam_20240303155056.jpg
[2024-03-03T13:50:56Z DEBUG reqwest::connect] starting new connection: https://api.telegram.org/
[2024-03-03T13:50:57Z DEBUG aicam] saving to ./output/aicam_20240303155056.jpg
[2024-03-03T13:50:57Z DEBUG aicam::upload] uploading ./output/aicam_20240303155056.jpg
[2024-03-03T13:50:57Z DEBUG reqwest::connect] starting new connection: https://api.telegram.org/
[2024-03-03T13:50:58Z DEBUG aicam] saving to ./output/aicam_20240303155057.jpg
[2024-03-03T13:50:58Z DEBUG aicam::upload] uploading ./output/aicam_20240303155057.jpg
[2024-03-03T13:50:58Z DEBUG reqwest::connect] starting new connection: https://api.telegram.org/
[2024-03-03T13:51:00Z DEBUG aicam] saving to ./output/aicam_20240303155059.jpg
[2024-03-03T13:51:00Z DEBUG aicam::upload] uploading ./output/aicam_20240303155059.jpg
[2024-03-03T13:51:00Z DEBUG reqwest::connect] starting new connection: https://api.telegram.org/
~...~
```

Expand Down
9 changes: 7 additions & 2 deletions config/aicam.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
[general]
device = "/dev/video0"
confidence = 30 # 50
device_name = "Cam #1"
confidence = 30
post_frames_amount = 3
output_dir = "./output"
debug = true

[telegram]
token = "TT"
channel = "CC"
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ pub static SETTINGS: Lazy<Settings> = Lazy::new(|| match Settings::new() {
});

pub mod settings;
pub mod upload;
157 changes: 113 additions & 44 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@

use chrono::Local;
use libfacedetection::facedetect_cnn;
use log::debug;
use opencv::{prelude::MatTraitConst, prelude::*, videoio::VideoCapture};
use std::{error::Error, process::exit};
use std::{error::Error, process::exit, thread, time::Duration};

use aicam::{CliArgs, SETTINGS};
use aicam::{upload::upload_image, CliArgs, SETTINGS};

fn main() -> Result<(), Box<dyn Error>> {
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
env_logger::init();
println!(
"{} {}\n{}",
env!("CARGO_PKG_NAME"),
Expand All @@ -21,73 +24,139 @@ fn main() -> Result<(), Box<dyn Error>> {
if args.version {
println!(env!("CARGO_PKG_VERSION"));
} else {
if let Err(err) = run_app() {
if let Err(err) = run_app().await {
println!("{err:?}");
exit(1);
}
}
Ok(())
}

fn run_app() -> Result<(), Box<dyn Error>> {
async fn run_app() -> Result<(), Box<dyn Error>> {
fn get_path() -> String {
format!(
"{}/{}_{}.jpg",
SETTINGS.general.output_dir,
env!("CARGO_PKG_NAME"),
Local::now().format("%Y%m%d%H%M%S"),
)
}

let mut dev = VideoCapture::from_file(&SETTINGS.general.device, opencv::videoio::CAP_ANY)
.expect("Unable to open camera");
.expect("unable to open camera");

let width = dev
.get(opencv::videoio::VideoCaptureProperties::CAP_PROP_FRAME_WIDTH as i32)
.expect("Unable to get camera width") as i32;
.expect("unable to get camera width") as i32;
let height = dev
.get(opencv::videoio::VideoCaptureProperties::CAP_PROP_FRAME_HEIGHT as i32)
.expect("Unable to get camera height") as i32;
println!(
"Camera {} ({width}x{height}) is running...",
.expect("unable to get camera height") as i32;
debug!(
"camera {} ({width}x{height}) is running...",
SETTINGS.general.device
);

let post_frames_amount = SETTINGS.general.post_frames_amount;
let mut post_frames_counter = post_frames_amount;
let mut is_post_frames_activated = false;
loop {
let is_ready = dev.grab().expect("Unable to get camera status");
let is_ready = dev.grab().expect("unable to get camera status");
if !is_ready {
is_post_frames_activated = false;
continue;
}

let mut img = Mat::default();
dev.retrieve(&mut img, 0)
.expect("Unable to get frame from camera");
.expect("unable to get frame from camera");

let faces = facedetect_cnn(
img.ptr(0).unwrap(),
img.cols(),
img.rows(),
img.mat_step().get(0) as u32,
)
.expect("Failed to detect faces");
for face in faces.faces {
if face.confidence > SETTINGS.general.confidence {
if SETTINGS.general.debug {
println!("Face: {:?}", face);
}
let ts = Local::now().format("%Y%m%d%H%M%S");
let path = format!(
"{}/{}_{}.jpg",
SETTINGS.general.output_dir,
env!("CARGO_PKG_NAME"),
ts,
);
if SETTINGS.general.debug {
println!("Saving to {}", path);
};
match opencv::imgcodecs::imwrite(&path, &img, &opencv::core::Vector::default()) {
Ok(rslt) => {
if !rslt {
println!(
"Oops, error happened (how about \"{}\" dir?)",
SETTINGS.general.output_dir,
);
return Err(Box::new(std::fmt::Error));
let mut path = get_path();
if is_post_frames_activated {
post_frames_counter -= 1;
let one_sec = Duration::from_millis(1000);
thread::sleep(one_sec);

debug!("saving to {}", path);
match opencv::imgcodecs::imwrite(&path, &img, &opencv::core::Vector::default()) {
Ok(rslt) => {
if rslt {
match upload_image(
format!(
"{} ({})",
&SETTINGS.general.device_name,
Local::now().format("%Y-%m-%d %H:%M:%S")
),
path,
)
.await
{
Ok(_) => {}
Err(err) => {
debug!("{}", err);
return Err(err);
}
}
} else {
debug!(
"oops, error happened (how about \"{}\" dir?)",
SETTINGS.general.output_dir,
);
return Err(Box::new(std::fmt::Error));
}
Err(err) => {
return Err(Box::new(err));
}
Err(err) => {
return Err(Box::new(err));
}
}
if post_frames_counter == 0 {
is_post_frames_activated = false;
post_frames_counter = post_frames_amount;
}
} else {
let faces = facedetect_cnn(
img.ptr(0).unwrap(),
img.cols(),
img.rows(),
img.mat_step().get(0) as u32,
)
.expect("failed to detect faces");
for face in faces.faces {
if face.confidence > SETTINGS.general.confidence {
debug!("face: {:?}", face);
path = get_path();
debug!("saving to {}", path);
match opencv::imgcodecs::imwrite(&path, &img, &opencv::core::Vector::default())
{
Ok(rslt) => {
if rslt {
is_post_frames_activated = true;
match upload_image(
format!(
"{} ({})",
&SETTINGS.general.device_name,
Local::now().format("%Y-%m-%d %H:%M:%S")
),
path,
)
.await
{
Ok(_) => {}
Err(err) => {
debug!("{}", err);
return Err(err);
}
}
} else {
debug!(
"oops, error happened (how about \"{}\" dir?)",
SETTINGS.general.output_dir,
);
return Err(Box::new(std::fmt::Error));
}
}
Err(err) => {
return Err(Box::new(err));
}
}
}
}
Expand Down
12 changes: 10 additions & 2 deletions src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,30 @@ use toml;
#[derive(Clone, Debug, Deserialize)]
pub struct General {
pub device: String,
pub device_name: String,
pub confidence: u16,
pub post_frames_amount: u16,
pub output_dir: String,
pub debug: bool,
}

#[derive(Clone, Debug, Deserialize)]
pub struct Telegram {
pub token: String,
pub channel: String,
}

#[derive(Clone, Debug, Deserialize)]
pub struct Settings {
pub general: General,
pub telegram: Telegram,
}

impl Settings {
pub fn new() -> Result<Self, toml::de::Error> {
let cfg_file = format!("config/{}.toml", env!("CARGO_PKG_NAME"));
toml::from_str(
&fs::read_to_string(cfg_file.clone())
.unwrap_or_else(|_| panic!("Config {cfg_file} file is absent")),
.unwrap_or_else(|_| panic!("config {cfg_file} file is absent")),
)
}
}
47 changes: 47 additions & 0 deletions src/upload.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use log::debug;
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
multipart::Form,
};
use std::{error::Error, fs::File, io::Read};

use crate::SETTINGS;

pub async fn upload_image(caption: String, file_path: String) -> Result<(), Box<dyn Error>> {
let url = format!(
"https://api.telegram.org/bot{}/sendPhoto",
SETTINGS.telegram.token
);
debug!("uploading {}", file_path);
let mut file = File::open(file_path)?;
let mut contents = vec![];
file.read_to_end(&mut contents)?;

let client = reqwest::Client::new();
let part = reqwest::multipart::Part::bytes(contents).file_name("filename.filetype");
let form = Form::new()
.text("chat_id", SETTINGS.telegram.clone().channel)
.text("caption", caption)
.part("photo", part);

let mut headers: HeaderMap = HeaderMap::new();
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("multipart/form-data"),
);

let response = client
.post(&url)
.headers(headers.clone())
.multipart(form)
.send()
.await?;
if response.status().is_success() {
Ok(())
} else {
Err(Box::new(std::io::Error::new(
std::io::ErrorKind::Other,
format!("failed to upload picture: {}", response.status()),
)))
}
}

0 comments on commit 6e99a17

Please sign in to comment.