Moved cropping and debug visualization in the face_processing module.
This commit is contained in:
parent
95a935ee1f
commit
211ebaf63f
4 changed files with 158 additions and 131 deletions
69
src/face_processing/crop.rs
Normal file
69
src/face_processing/crop.rs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
//! Face cropping operations.
|
||||
//!
|
||||
//! Extracts and resizes face regions from images using bounding box data.
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::immich_api::FaceData;
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, GenericImageView};
|
||||
|
||||
/// Crop and resize the face from an image using bounding box.
|
||||
/// Returns (cropped_full_res, resized_final) for intermediate saving.
|
||||
///
|
||||
/// This is a simplified version that just uses the bounding box.
|
||||
/// A full implementation would use facial landmarks for alignment.
|
||||
pub fn crop_face_with_intermediate(
|
||||
img: &DynamicImage,
|
||||
face_data: &FaceData,
|
||||
output_size: u32,
|
||||
) -> Result<(DynamicImage, DynamicImage)> {
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
|
||||
// Convert normalized bounding box to pixel coordinates
|
||||
let x1 = (face_data.bounding_box_x1 * img_width as f32) as u32;
|
||||
let y1 = (face_data.bounding_box_y1 * img_height as f32) as u32;
|
||||
let x2 = (face_data.bounding_box_x2 * img_width as f32) as u32;
|
||||
let y2 = (face_data.bounding_box_y2 * img_height as f32) as u32;
|
||||
|
||||
let face_width = x2.saturating_sub(x1);
|
||||
let face_height = y2.saturating_sub(y1);
|
||||
|
||||
if face_width == 0 || face_height == 0 {
|
||||
return Err(Error::ImageProcessing(
|
||||
"Invalid face bounding box".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Expand the crop area to include some context around the face
|
||||
// and make it square for consistent output
|
||||
let face_size = face_width.max(face_height);
|
||||
let padding = face_size / 2; // 50% padding on each side
|
||||
let crop_size = face_size + padding * 2;
|
||||
|
||||
// Calculate center of face
|
||||
let center_x = (x1 + x2) / 2;
|
||||
let center_y = (y1 + y2) / 2;
|
||||
|
||||
// Calculate crop bounds, clamped to image dimensions
|
||||
let crop_x1 = center_x
|
||||
.saturating_sub(crop_size / 2)
|
||||
.min(img_width.saturating_sub(crop_size));
|
||||
let crop_y1 = center_y
|
||||
.saturating_sub(crop_size / 2)
|
||||
.min(img_height.saturating_sub(crop_size));
|
||||
|
||||
// Ensure we don't exceed image bounds
|
||||
let actual_crop_size = crop_size.min(img_width - crop_x1).min(img_height - crop_y1);
|
||||
|
||||
if actual_crop_size < 10 {
|
||||
return Err(Error::ImageProcessing("Crop area too small".to_string()));
|
||||
}
|
||||
|
||||
// Crop the face region (full resolution)
|
||||
let cropped = img.crop_imm(crop_x1, crop_y1, actual_crop_size, actual_crop_size);
|
||||
|
||||
// Resize to output size
|
||||
let resized = cropped.resize_exact(output_size, output_size, FilterType::Lanczos3);
|
||||
|
||||
Ok((cropped, resized))
|
||||
}
|
||||
83
src/face_processing/debug.rs
Normal file
83
src/face_processing/debug.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! Debug visualization functions for face processing.
|
||||
//!
|
||||
//! These functions create annotated images showing the processing steps,
|
||||
//! useful for debugging and understanding the pipeline behavior.
|
||||
|
||||
use crate::immich_api::FaceData;
|
||||
use image::{DynamicImage, GenericImageView, Rgb};
|
||||
use imageproc::drawing::{draw_hollow_rect_mut, draw_line_segment_mut};
|
||||
use imageproc::rect::Rect;
|
||||
|
||||
/// Draw debug visualization showing bounding box and crop region.
|
||||
/// - Red rectangle: face bounding box from Immich
|
||||
/// - Green rectangle: expanded crop region used for processing
|
||||
/// - Red crosshair: center of the face bounding box
|
||||
pub fn draw_crop_debug(img: &DynamicImage, face_data: &FaceData) -> DynamicImage {
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
let mut rgb_img = img.to_rgb8();
|
||||
|
||||
// Face bounding box in pixel coordinates
|
||||
let bbox_x1 = (face_data.bounding_box_x1 * img_width as f32) as i32;
|
||||
let bbox_y1 = (face_data.bounding_box_y1 * img_height as f32) as i32;
|
||||
let bbox_x2 = (face_data.bounding_box_x2 * img_width as f32) as i32;
|
||||
let bbox_y2 = (face_data.bounding_box_y2 * img_height as f32) as i32;
|
||||
|
||||
let face_width = (bbox_x2 - bbox_x1) as u32;
|
||||
let face_height = (bbox_y2 - bbox_y1) as u32;
|
||||
|
||||
// Calculate crop region (same logic as crop_face_with_intermediate in job/mod.rs)
|
||||
let face_size = face_width.max(face_height);
|
||||
let padding = face_size / 2;
|
||||
let crop_size = face_size + padding * 2;
|
||||
|
||||
let center_x = (bbox_x1 + bbox_x2) / 2;
|
||||
let center_y = (bbox_y1 + bbox_y2) / 2;
|
||||
|
||||
let crop_x1 = (center_x - crop_size as i32 / 2).max(0) as u32;
|
||||
let crop_y1 = (center_y - crop_size as i32 / 2).max(0) as u32;
|
||||
let crop_x1 = crop_x1.min(img_width.saturating_sub(crop_size));
|
||||
let crop_y1 = crop_y1.min(img_height.saturating_sub(crop_size));
|
||||
let actual_crop_size = crop_size.min(img_width - crop_x1).min(img_height - crop_y1);
|
||||
|
||||
// Colors
|
||||
let red = Rgb([255u8, 0, 0]);
|
||||
let green = Rgb([0u8, 255, 0]);
|
||||
|
||||
// Draw face bounding box (red) - draw multiple times for thickness
|
||||
for offset in 0..3i32 {
|
||||
let rect = Rect::at(bbox_x1 - offset, bbox_y1 - offset)
|
||||
.of_size(face_width + offset as u32 * 2, face_height + offset as u32 * 2);
|
||||
draw_hollow_rect_mut(&mut rgb_img, rect, red);
|
||||
}
|
||||
|
||||
// Draw crop region (green) - draw multiple times for thickness
|
||||
for offset in 0..3i32 {
|
||||
let rect = Rect::at(crop_x1 as i32 - offset, crop_y1 as i32 - offset)
|
||||
.of_size(
|
||||
actual_crop_size + offset as u32 * 2,
|
||||
actual_crop_size + offset as u32 * 2,
|
||||
);
|
||||
draw_hollow_rect_mut(&mut rgb_img, rect, green);
|
||||
}
|
||||
|
||||
// Draw crosshair at face center
|
||||
let cross_size = 20i32;
|
||||
draw_line_segment_mut(
|
||||
&mut rgb_img,
|
||||
((center_x - cross_size) as f32, center_y as f32),
|
||||
((center_x + cross_size) as f32, center_y as f32),
|
||||
red,
|
||||
);
|
||||
draw_line_segment_mut(
|
||||
&mut rgb_img,
|
||||
(center_x as f32, (center_y - cross_size) as f32),
|
||||
(center_x as f32, (center_y + cross_size) as f32),
|
||||
red,
|
||||
);
|
||||
|
||||
DynamicImage::ImageRgb8(rgb_img)
|
||||
}
|
||||
|
||||
// Future debug visualization functions:
|
||||
// - draw_landmarks_debug: Face with 68-point landmarks drawn
|
||||
// - draw_alignment_debug: Before/after alignment visualization
|
||||
|
|
@ -3,8 +3,11 @@
|
|||
//! This module handles face detection, landmark detection,
|
||||
//! alignment, and image transformation.
|
||||
|
||||
mod crop;
|
||||
pub mod debug;
|
||||
mod types;
|
||||
|
||||
pub use crop::*;
|
||||
pub use types::*;
|
||||
|
||||
// TODO: Implement these modules
|
||||
|
|
|
|||
134
src/job/mod.rs
134
src/job/mod.rs
|
|
@ -8,15 +8,14 @@
|
|||
|
||||
use crate::config::Config;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::face_processing::crop_face_with_intermediate;
|
||||
use crate::face_processing::debug::draw_crop_debug;
|
||||
use crate::face_processing::{AssetResult, BoundingBox, ProcessedFace};
|
||||
use crate::immich_api::{Asset, FaceData, ImmichClient};
|
||||
use crate::video::compile_timelapse;
|
||||
use crate::web::{AppState, JobStatus, Progress};
|
||||
|
||||
use image::imageops::FilterType;
|
||||
use image::{DynamicImage, GenericImageView, ImageFormat, Rgb};
|
||||
use imageproc::drawing::{draw_hollow_rect_mut, draw_line_segment_mut};
|
||||
use imageproc::rect::Rect;
|
||||
use image::ImageFormat;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
|
@ -545,133 +544,6 @@ async fn process_single_asset(
|
|||
})
|
||||
}
|
||||
|
||||
/// Crop and resize the face from an image using bounding box.
|
||||
/// Returns (cropped_full_res, resized_final) for intermediate saving.
|
||||
///
|
||||
/// This is a simplified version that just uses the bounding box.
|
||||
/// A full implementation would use facial landmarks for alignment.
|
||||
fn crop_face_with_intermediate(
|
||||
img: &DynamicImage,
|
||||
face_data: &FaceData,
|
||||
output_size: u32,
|
||||
) -> Result<(DynamicImage, DynamicImage)> {
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
|
||||
// Convert normalized bounding box to pixel coordinates
|
||||
let x1 = (face_data.bounding_box_x1 * img_width as f32) as u32;
|
||||
let y1 = (face_data.bounding_box_y1 * img_height as f32) as u32;
|
||||
let x2 = (face_data.bounding_box_x2 * img_width as f32) as u32;
|
||||
let y2 = (face_data.bounding_box_y2 * img_height as f32) as u32;
|
||||
|
||||
let face_width = x2.saturating_sub(x1);
|
||||
let face_height = y2.saturating_sub(y1);
|
||||
|
||||
if face_width == 0 || face_height == 0 {
|
||||
return Err(Error::ImageProcessing(
|
||||
"Invalid face bounding box".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// Expand the crop area to include some context around the face
|
||||
// and make it square for consistent output
|
||||
let face_size = face_width.max(face_height);
|
||||
let padding = face_size / 2; // 50% padding on each side
|
||||
let crop_size = face_size + padding * 2;
|
||||
|
||||
// Calculate center of face
|
||||
let center_x = (x1 + x2) / 2;
|
||||
let center_y = (y1 + y2) / 2;
|
||||
|
||||
// Calculate crop bounds, clamped to image dimensions
|
||||
let crop_x1 = center_x
|
||||
.saturating_sub(crop_size / 2)
|
||||
.min(img_width.saturating_sub(crop_size));
|
||||
let crop_y1 = center_y
|
||||
.saturating_sub(crop_size / 2)
|
||||
.min(img_height.saturating_sub(crop_size));
|
||||
|
||||
// Ensure we don't exceed image bounds
|
||||
let actual_crop_size = crop_size.min(img_width - crop_x1).min(img_height - crop_y1);
|
||||
|
||||
if actual_crop_size < 10 {
|
||||
return Err(Error::ImageProcessing("Crop area too small".to_string()));
|
||||
}
|
||||
|
||||
// Crop the face region (full resolution)
|
||||
let cropped = img.crop_imm(crop_x1, crop_y1, actual_crop_size, actual_crop_size);
|
||||
|
||||
// Resize to output size
|
||||
let resized = cropped.resize_exact(output_size, output_size, FilterType::Lanczos3);
|
||||
|
||||
Ok((cropped, resized))
|
||||
}
|
||||
|
||||
/// Draw debug visualization showing bounding box and crop region.
|
||||
/// - Red rectangle: face bounding box from Immich
|
||||
/// - Green rectangle: expanded crop region used for processing
|
||||
fn draw_crop_debug(img: &DynamicImage, face_data: &FaceData) -> DynamicImage {
|
||||
let (img_width, img_height) = img.dimensions();
|
||||
let mut rgb_img = img.to_rgb8();
|
||||
|
||||
// Face bounding box in pixel coordinates
|
||||
let bbox_x1 = (face_data.bounding_box_x1 * img_width as f32) as i32;
|
||||
let bbox_y1 = (face_data.bounding_box_y1 * img_height as f32) as i32;
|
||||
let bbox_x2 = (face_data.bounding_box_x2 * img_width as f32) as i32;
|
||||
let bbox_y2 = (face_data.bounding_box_y2 * img_height as f32) as i32;
|
||||
|
||||
let face_width = (bbox_x2 - bbox_x1) as u32;
|
||||
let face_height = (bbox_y2 - bbox_y1) as u32;
|
||||
|
||||
// Calculate crop region (same logic as crop_face_with_intermediate)
|
||||
let face_size = face_width.max(face_height);
|
||||
let padding = face_size / 2;
|
||||
let crop_size = face_size + padding * 2;
|
||||
|
||||
let center_x = (bbox_x1 + bbox_x2) / 2;
|
||||
let center_y = (bbox_y1 + bbox_y2) / 2;
|
||||
|
||||
let crop_x1 = (center_x - crop_size as i32 / 2).max(0) as u32;
|
||||
let crop_y1 = (center_y - crop_size as i32 / 2).max(0) as u32;
|
||||
let crop_x1 = crop_x1.min(img_width.saturating_sub(crop_size));
|
||||
let crop_y1 = crop_y1.min(img_height.saturating_sub(crop_size));
|
||||
let actual_crop_size = crop_size.min(img_width - crop_x1).min(img_height - crop_y1);
|
||||
|
||||
// Colors
|
||||
let red = Rgb([255u8, 0, 0]);
|
||||
let green = Rgb([0u8, 255, 0]);
|
||||
|
||||
// Draw face bounding box (red) - draw multiple times for thickness
|
||||
for offset in 0..3i32 {
|
||||
let rect = Rect::at(bbox_x1 - offset, bbox_y1 - offset)
|
||||
.of_size(face_width + offset as u32 * 2, face_height + offset as u32 * 2);
|
||||
draw_hollow_rect_mut(&mut rgb_img, rect, red);
|
||||
}
|
||||
|
||||
// Draw crop region (green) - draw multiple times for thickness
|
||||
for offset in 0..3i32 {
|
||||
let rect = Rect::at(crop_x1 as i32 - offset, crop_y1 as i32 - offset)
|
||||
.of_size(actual_crop_size + offset as u32 * 2, actual_crop_size + offset as u32 * 2);
|
||||
draw_hollow_rect_mut(&mut rgb_img, rect, green);
|
||||
}
|
||||
|
||||
// Draw crosshair at face center
|
||||
let cross_size = 20i32;
|
||||
draw_line_segment_mut(
|
||||
&mut rgb_img,
|
||||
((center_x - cross_size) as f32, center_y as f32),
|
||||
((center_x + cross_size) as f32, center_y as f32),
|
||||
red,
|
||||
);
|
||||
draw_line_segment_mut(
|
||||
&mut rgb_img,
|
||||
(center_x as f32, (center_y - cross_size) as f32),
|
||||
(center_x as f32, (center_y + cross_size) as f32),
|
||||
red,
|
||||
);
|
||||
|
||||
DynamicImage::ImageRgb8(rgb_img)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue