|
|
|
@ -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}"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|