|  |  |  | @ -1,29 +1,68 @@ | 
			
		
	
		
			
				
					|  |  |  |  | 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}; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | fn main() { | 
			
		
	
		
			
				
					|  |  |  |  |     open_and_reduce_img(); | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | /// Folder the camera images are uploaded to
 | 
			
		
	
		
			
				
					|  |  |  |  | const FOLDER_WITH_NEW_IMAGES: &str = "/dev/shm/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_IMAGES : &str = "../2/"; | 
			
		
	
		
			
				
					|  |  |  |  | /// Image cropping config: left top X
 | 
			
		
	
		
			
				
					|  |  |  |  | const CROP_X : u32 = 1872; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | const FOLDER_OUTPUT : &str = "birds"; | 
			
		
	
		
			
				
					|  |  |  |  | /// 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 = 500; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | fn open_and_reduce_img() { | 
			
		
	
		
			
				
					|  |  |  |  |     env_logger::init_from_env("LOGGING"); | 
			
		
	
		
			
				
					|  |  |  |  | fn main() { | 
			
		
	
		
			
				
					|  |  |  |  |     env_logger::init(); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     let commit_file = PathBuf::from(FOLDER_WITH_NEW_IMAGES).join(COMMIT_FILENAME); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     info!("Bird finder starts"); | 
			
		
	
		
			
				
					|  |  |  |  |     loop { | 
			
		
	
		
			
				
					|  |  |  |  |         if !commit_file.exists() { | 
			
		
	
		
			
				
					|  |  |  |  |             debug!("No commit file, wait."); | 
			
		
	
		
			
				
					|  |  |  |  |             std::thread::sleep(Duration::from_secs(10)); | 
			
		
	
		
			
				
					|  |  |  |  |             continue; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     let paths = std::fs::read_dir(FOLDER_WITH_IMAGES).unwrap(); | 
			
		
	
		
			
				
					|  |  |  |  |         if let Err(e) = process_camera_pics() { | 
			
		
	
		
			
				
					|  |  |  |  |             error!("Error processing pics: {e}"); | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         let _ = std::fs::remove_dir_all(FOLDER_WITH_NEW_IMAGES); | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | 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")?; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     let mut crops = vec![]; | 
			
		
	
		
			
				
					|  |  |  |  |     let mut thumbs = vec![]; | 
			
		
	
	
		
			
				
					|  |  |  | @ -33,16 +72,20 @@ fn open_and_reduce_img() { | 
			
		
	
		
			
				
					|  |  |  |  |     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() { | 
			
		
	
		
			
				
					|  |  |  |  |         if !meta.is_file() || !dir_entry.file_name().to_string_lossy().ends_with(".jpg") { | 
			
		
	
		
			
				
					|  |  |  |  |             continue; | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         timestamps.push(meta.ctime()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         debug!("{}", dir_entry.path().display()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         let img = image::open(dir_entry.path()).unwrap(); | 
			
		
	
		
			
				
					|  |  |  |  |         let crop = img.crop_imm(1872, 1329, 1000, 750); | 
			
		
	
		
			
				
					|  |  |  |  |         let Ok(img) = image::open(dir_entry.path()) else { | 
			
		
	
		
			
				
					|  |  |  |  |             continue; | 
			
		
	
		
			
				
					|  |  |  |  |         }; | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         // File loaded fine, assume it's OK
 | 
			
		
	
		
			
				
					|  |  |  |  |         timestamps.push(meta.ctime()); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         let crop = img.crop_imm(CROP_X, CROP_Y, CROP_W, CROP_H); | 
			
		
	
		
			
				
					|  |  |  |  |         let thumb = crop.thumbnail(THUMB_W as u32, THUMB_H as u32); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |         crops.push(crop); | 
			
		
	
	
		
			
				
					|  |  |  | @ -86,12 +129,15 @@ fn open_and_reduce_img() { | 
			
		
	
		
			
				
					|  |  |  |  |         if deviation > THRESHOLD_DEVIATION { | 
			
		
	
		
			
				
					|  |  |  |  |             info!("LIKELY BIRD!!!! in picture #{thumb_num}"); | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |             let datetime = parse_unix_ts(timestamps[thumb_num]).format("%Y-%m-%d_%H-%M-%S"); | 
			
		
	
		
			
				
					|  |  |  |  |             let path = format!("{}/{}.jpg", FOLDER_OUTPUT, datetime); | 
			
		
	
		
			
				
					|  |  |  |  |             let datetime_str = parse_unix_ts(timestamps[thumb_num]).format("%Y-%m-%d_%H-%M-%S"); | 
			
		
	
		
			
				
					|  |  |  |  |             let path = format!("{}/{}.jpg", FOLDER_WITH_BIRD_IMAGES, datetime_str); | 
			
		
	
		
			
				
					|  |  |  |  |             let mut pb = 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 pb.exists() { | 
			
		
	
		
			
				
					|  |  |  |  |                 pb.set_file_name(format!("{}-{cnt}.jpg", datetime)); | 
			
		
	
		
			
				
					|  |  |  |  |                 pb.set_file_name(format!("{}-{cnt}.jpg", datetime_str)); | 
			
		
	
		
			
				
					|  |  |  |  |                 cnt += 1; | 
			
		
	
		
			
				
					|  |  |  |  |             } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  |  | @ -101,9 +147,7 @@ fn open_and_reduce_img() { | 
			
		
	
		
			
				
					|  |  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  |     // crop.save(format!("{}/{}", FOLDER_OUTPUT, dir_entry.file_name().to_string_lossy())).unwrap();
 | 
			
		
	
		
			
				
					|  |  |  |  |     // thumb.save(format!("{}/th-{}", FOLDER_OUTPUT, dir_entry.file_name().to_string_lossy())).unwrap();
 | 
			
		
	
		
			
				
					|  |  |  |  |     Ok(()) | 
			
		
	
		
			
				
					|  |  |  |  | } | 
			
		
	
		
			
				
					|  |  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |  | fn parse_unix_ts(ts: i64) -> chrono::DateTime<Utc> { | 
			
		
	
	
		
			
				
					|  |  |  | 
 |