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}; /// 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 = 600; fn main() { env_logger::init(); let commit_file = PathBuf::from(FOLDER_WITH_NEW_IMAGES).join(COMMIT_FILENAME); loop { if !commit_file.exists() { debug!("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 { chrono::DateTime::::from_naive_utc_and_offset(NaiveDateTime::from_timestamp_opt(ts, 0) .expect("Bad timestamp"), Utc) }