diff --git a/src/main.rs b/src/main.rs index 96f6a4a..c95ae14 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,17 +4,19 @@ use std::time::Duration; use anyhow::Context; use chrono::{NaiveDateTime, Utc}; -use image::{GenericImageView, Pixel}; +use image::{DynamicImage, GenericImageView, Pixel}; use log::{debug, error, info}; /// Folder the camera images are uploaded to const FOLDER_WITH_NEW_IMAGES: &str = "/dev/shm/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; @@ -47,7 +49,7 @@ fn main() { loop { if !commit_file.exists() { debug!("No commit file, wait."); - std::thread::sleep(Duration::from_secs(10)); + std::thread::sleep(Duration::from_secs(5)); continue; } @@ -55,7 +57,16 @@ fn main() { error!("Error processing pics: {e}"); } - let _ = std::fs::remove_dir_all(FOLDER_WITH_NEW_IMAGES); + // 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()); + } + } + } } } @@ -64,9 +75,16 @@ fn process_camera_pics() -> anyhow::Result<()> { let paths = std::fs::read_dir(FOLDER_WITH_NEW_IMAGES).context("Read folder with images")?; - let mut crops = vec![]; - let mut thumbs = vec![]; - let mut timestamps = vec![]; + + struct Pic { + crop: DynamicImage, + // the actual thumb is sized to fit here, it can be a bit smaller + thumb: [u8; THUMB_H * THUMB_W], + timestamp: i64, + filename: String, + } + + let mut pics = vec![]; info!("Loading files"); for dir_entry in paths { @@ -78,32 +96,38 @@ fn process_camera_pics() -> anyhow::Result<()> { debug!("{}", dir_entry.path().display()); + // load the pic as is let Ok(img) = image::open(dir_entry.path()) else { continue; }; - // File loaded fine, assume it's OK - timestamps.push(meta.ctime()); - + // 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); - crops.push(crop); + let mut pic = Pic { + crop, + thumb: [0u8; THUMB_H * THUMB_W], + timestamp: meta.ctime(), + filename: dir_entry.path().file_name().unwrap_or_default() + .to_string_lossy().to_string(), + }; - let mut thumb_ar = [0u8; THUMB_H * THUMB_W]; - for y in 0..thumb.height() { - for x in 0..thumb.width() { - thumb_ar[(y * thumb.width() + x) as usize] = thumb.get_pixel(x, y).to_luma().0[0]; - } + // extract all pixels + for (x, y, val) in thumb.pixels() { + pic.thumb[(y * thumb.width() + x) as usize] = val.to_luma().0[0]; } - thumbs.push(thumb_ar); + pics.push(pic); } info!("Computing digests & medians"); + // transpose the thumb vecs so we can compute medians let mut transposed = vec![]; - for thumb in thumbs.iter() { - for (b, item) in thumb.iter().enumerate() { + for pic in &pics { + for (b, item) in pic.thumb.iter().enumerate() { if transposed.len() <= b { transposed.push(vec![]); } @@ -111,25 +135,26 @@ fn process_camera_pics() -> anyhow::Result<()> { } } + // compute medians let mut medians = [0; THUMB_H * THUMB_W]; - - for (a, column) in transposed.iter().enumerate() { + 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 (thumb_num, thumb) in thumbs.iter().enumerate() { - let deviation = thumb.iter().zip(medians.iter()) + 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!("Thumb {thumb_num} summary diff from median is: {deviation}"); + debug!("Picture {} summary diff from median is: {deviation}", pic.filename); if deviation > THRESHOLD_DEVIATION { - info!("LIKELY BIRD!!!! in picture #{thumb_num}"); + info!("LIKELY BIRD!!!! in picture {}", pic.filename); - let datetime_str = parse_unix_ts(timestamps[thumb_num]).format("%Y-%m-%d_%H-%M-%S"); + 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 pb = PathBuf::from(path); @@ -141,7 +166,7 @@ fn process_camera_pics() -> anyhow::Result<()> { cnt += 1; } - if let Err(e) = crops[thumb_num].save(pb) { + if let Err(e) = pic.crop.save(pb) { error!("Fail to save pic: {e}"); } }