image processor for a bird feeder camera
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.
bird-detector/src/main.rs

156 lines
4.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};
/// 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";
/// 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 = 500;
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(10));
continue;
}
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![];
let mut timestamps = 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());
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);
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];
}
}
thumbs.push(thumb_ar);
}
info!("Computing digests & medians");
let mut transposed = vec![];
for thumb in thumbs.iter() {
for (b, item) in thumb.iter().enumerate() {
if transposed.len() <= b {
transposed.push(vec![]);
}
transposed[b].push(*item);
}
}
let mut medians = [0; THUMB_H * THUMB_W];
for (a, column) in transposed.iter().enumerate() {
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())
.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}");
if deviation > THRESHOLD_DEVIATION {
info!("LIKELY BIRD!!!! in picture #{thumb_num}");
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_str));
cnt += 1;
}
if let Err(e) = crops[thumb_num].save(pb) {
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)
}