You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
5.7 KiB
180 lines
5.7 KiB
use std::os::unix::fs::MetadataExt;
|
|
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
use anyhow::Context;
|
|
|
|
use chrono::{NaiveDateTime, Utc};
|
|
use image::{GenericImageView, Pixel};
|
|
use log::{debug, error, info, trace};
|
|
|
|
/// Folder the camera images are uploaded to
|
|
const FOLDER_WITH_NEW_IMAGES: &str = "/tmp/camera";
|
|
//const FOLDER_WITH_NEW_IMAGES: &str = "/tmp/camera";
|
|
|
|
/// File created in the uploads directory when the upload finishes.
|
|
const COMMIT_FILENAME : &str = "commit";
|
|
|
|
/// Folder where the cropped bird pics are saved
|
|
const FOLDER_WITH_BIRD_IMAGES: &str = "/backup/krmitko";
|
|
//const FOLDER_WITH_BIRD_IMAGES: &str = "/tmp/ptaci";
|
|
|
|
/// Image cropping config: left top X
|
|
const CROP_X : u32 = 1872;
|
|
|
|
/// Image cropping config: left top Y
|
|
const CROP_Y : u32 = 1329;
|
|
|
|
/// Image cropping config: crop width
|
|
const CROP_W : u32 = 1000;
|
|
|
|
/// Image cropping config: crop height
|
|
const CROP_H : u32 = 750;
|
|
|
|
/// Thumbnail width, used for bird detection (together with height creates the kernel size)
|
|
/// This is a compromise between sensitivity and susceptibility to false positives.
|
|
const THUMB_W: usize = 12;
|
|
|
|
/// Thumbnail height
|
|
const THUMB_H: usize = 8;
|
|
|
|
/// Threshold for bird detection.
|
|
/// Bird is detected if the sum of deviations from a median value in a picture exceeds this threshold.
|
|
const THRESHOLD_DEVIATION: u32 = 300;
|
|
|
|
fn main() {
|
|
env_logger::init();
|
|
|
|
let commit_file = PathBuf::from(FOLDER_WITH_NEW_IMAGES).join(COMMIT_FILENAME);
|
|
|
|
loop {
|
|
if !commit_file.exists() {
|
|
trace!("No commit file, wait.");
|
|
std::thread::sleep(Duration::from_secs(5));
|
|
continue;
|
|
}
|
|
|
|
if let Err(e) = process_camera_pics() {
|
|
error!("Error processing pics: {e}");
|
|
}
|
|
|
|
// clean up. The assumption is that the bird processing is much faster than photo capture,
|
|
// so we dont need to be super careful with the timing
|
|
if let Ok(paths) = std::fs::read_dir(FOLDER_WITH_NEW_IMAGES) {
|
|
for dir_entry in paths {
|
|
let Ok(dir_entry) = dir_entry else { continue; };
|
|
if dir_entry.path().is_file() {
|
|
let _ = std::fs::remove_file(dir_entry.path());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn process_camera_pics() -> anyhow::Result<()> {
|
|
info!("Processing camera pics");
|
|
|
|
let paths = std::fs::read_dir(FOLDER_WITH_NEW_IMAGES).context("Read folder with images")?;
|
|
|
|
|
|
struct Pic {
|
|
full_path: PathBuf,
|
|
thumb: [u8; THUMB_H * THUMB_W],
|
|
timestamp: i64,
|
|
filename: String,
|
|
}
|
|
|
|
let mut pics = vec![];
|
|
|
|
info!("Loading files");
|
|
for dir_entry in paths {
|
|
let Ok(dir_entry) = dir_entry else { continue; };
|
|
let Ok(meta) = dir_entry.metadata() else { continue; };
|
|
if !meta.is_file() || !dir_entry.file_name().to_string_lossy().ends_with(".jpg") {
|
|
continue;
|
|
}
|
|
|
|
debug!("{}", dir_entry.path().display());
|
|
|
|
// load the pic as is
|
|
let Ok(img) = image::open(dir_entry.path()) else {
|
|
continue;
|
|
};
|
|
|
|
// extract the interesting rectangle
|
|
let crop = img.crop_imm(CROP_X, CROP_Y, CROP_W, CROP_H);
|
|
|
|
// resize for change detection. This is a very small bitmap
|
|
let thumb = crop.thumbnail(THUMB_W as u32, THUMB_H as u32);
|
|
|
|
let mut pic = Pic {
|
|
full_path: dir_entry.path(),
|
|
thumb: [0u8; THUMB_H * THUMB_W],
|
|
timestamp: meta.mtime(),
|
|
filename: dir_entry.path().file_name().unwrap_or_default()
|
|
.to_string_lossy().to_string(),
|
|
};
|
|
|
|
// extract all pixels
|
|
for (x, y, val) in thumb.pixels() {
|
|
pic.thumb[(y * thumb.width() + x) as usize] = val.to_luma().0[0];
|
|
}
|
|
|
|
pics.push(pic);
|
|
}
|
|
|
|
info!("Computing digests & medians");
|
|
// transpose the thumb vecs so we can compute medians
|
|
let mut transposed = vec![];
|
|
for pic in &pics {
|
|
for (b, item) in pic.thumb.iter().enumerate() {
|
|
if transposed.len() <= b {
|
|
transposed.push(vec![]);
|
|
}
|
|
transposed[b].push(*item);
|
|
}
|
|
}
|
|
|
|
// compute medians
|
|
let mut medians = [0; THUMB_H * THUMB_W];
|
|
for (a, mut column) in transposed.into_iter().enumerate() {
|
|
column.sort();
|
|
medians[a] = column[column.len() / 2]; // approximately median ...
|
|
}
|
|
|
|
debug!("Medians: {:?}", medians);
|
|
|
|
info!("Finding deviations");
|
|
for pic in pics {
|
|
let deviation = pic.thumb.iter().zip(medians.iter())
|
|
.map(|(a, b)| (*a as i16 - *b as i16).abs() as u32).fold(0u32, |a, b| a + b);
|
|
|
|
debug!("Picture {} summary diff from median is: {deviation}", pic.filename);
|
|
|
|
if deviation > THRESHOLD_DEVIATION {
|
|
info!("LIKELY BIRD!!!! in picture {}", pic.filename);
|
|
|
|
let datetime_str = parse_unix_ts(pic.timestamp).format("%Y-%m-%d_%H-%M-%S");
|
|
let path = format!("{}/{}.jpg", FOLDER_WITH_BIRD_IMAGES, datetime_str);
|
|
let mut target_path = PathBuf::from(path);
|
|
|
|
// Ensure we do not overwrite a file if there are multiple in the same second!
|
|
// This adds -2, -3 etc. to the file name
|
|
let mut cnt = 2;
|
|
while target_path.exists() {
|
|
target_path.set_file_name(format!("{}-{cnt}.jpg", datetime_str));
|
|
cnt += 1;
|
|
}
|
|
|
|
if let Err(e) = std::fs::copy(pic.full_path, target_path) {
|
|
error!("Fail to save pic: {e}");
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_unix_ts(ts: i64) -> chrono::DateTime<Utc> {
|
|
chrono::DateTime::<Utc>::from_naive_utc_and_offset(NaiveDateTime::from_timestamp_opt(ts, 0)
|
|
.expect("Bad timestamp"), Utc)
|
|
}
|
|
|