Add a threshold for maximum amount of padding
This commit is contained in:
parent
1cdb1d7eb0
commit
facc140619
7 changed files with 296 additions and 4 deletions
|
|
@ -45,6 +45,10 @@
|
||||||
enabled: config.processing.face_resolution.enabled,
|
enabled: config.processing.face_resolution.enabled,
|
||||||
min_size: Number(config.processing.face_resolution.min_size),
|
min_size: Number(config.processing.face_resolution.min_size),
|
||||||
},
|
},
|
||||||
|
crop: {
|
||||||
|
enabled: config.processing.crop.enabled,
|
||||||
|
max_padding_percent: Number(config.processing.crop.max_padding_percent),
|
||||||
|
},
|
||||||
brightness: {
|
brightness: {
|
||||||
enabled: config.processing.brightness.enabled,
|
enabled: config.processing.brightness.enabled,
|
||||||
min_brightness: Number(config.processing.brightness.min_brightness),
|
min_brightness: Number(config.processing.brightness.min_brightness),
|
||||||
|
|
@ -203,6 +207,38 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Face Too Close to Edge Section -->
|
||||||
|
<div class="setting-section">
|
||||||
|
<div class="section-header">
|
||||||
|
<span class="section-title">Face Too Close to Edge</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={config.processing.crop.enabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="section-hint">Faces too close to the edge of the photo are often distorted and require too much padding</div>
|
||||||
|
|
||||||
|
{#if config.processing.crop.enabled}
|
||||||
|
<div class="setting-row sub-setting">
|
||||||
|
<label for="max-padding">
|
||||||
|
<span class="setting-label">Max Padding</span>
|
||||||
|
<span class="setting-hint">Maximum allowed padding from edge replication</span>
|
||||||
|
</label>
|
||||||
|
<div class="setting-control">
|
||||||
|
<input
|
||||||
|
id="max-padding"
|
||||||
|
type="range"
|
||||||
|
bind:value={config.processing.crop.max_padding_percent}
|
||||||
|
min="5"
|
||||||
|
max="80"
|
||||||
|
step="5"
|
||||||
|
/>
|
||||||
|
<span class="value">{config.processing.crop.max_padding_percent}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Brightness Section -->
|
<!-- Brightness Section -->
|
||||||
<div class="setting-section">
|
<div class="setting-section">
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
|
|
@ -793,6 +829,12 @@
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.section-header input[type='checkbox'] {
|
.section-header input[type='checkbox'] {
|
||||||
width: 1.25rem;
|
width: 1.25rem;
|
||||||
height: 1.25rem;
|
height: 1.25rem;
|
||||||
|
|
|
||||||
|
|
@ -201,6 +201,41 @@ impl BlurConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Crop padding filter configuration.
|
||||||
|
///
|
||||||
|
/// Skips images where too much of the crop region falls outside the original
|
||||||
|
/// image bounds (filled by edge-pixel replication).
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CropConfig {
|
||||||
|
/// Whether crop padding filtering is enabled.
|
||||||
|
pub enabled: bool,
|
||||||
|
|
||||||
|
/// Maximum allowed padding percentage (0.0-100.0).
|
||||||
|
/// Images where the replicated-fill area exceeds this will be skipped.
|
||||||
|
pub max_padding_percent: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CropConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
enabled: false,
|
||||||
|
max_padding_percent: 30.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CropConfig {
|
||||||
|
/// Validate the configuration values.
|
||||||
|
pub fn validate(&self) -> Result<()> {
|
||||||
|
if self.enabled && !(0.0..=100.0).contains(&self.max_padding_percent) {
|
||||||
|
return Err(Error::Config(
|
||||||
|
"Crop max_padding_percent must be between 0.0 and 100.0".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Output/resize configuration.
|
/// Output/resize configuration.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct OutputConfig {
|
pub struct OutputConfig {
|
||||||
|
|
@ -508,6 +543,10 @@ pub struct ProcessingConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub face_resolution: FaceResolutionConfig,
|
pub face_resolution: FaceResolutionConfig,
|
||||||
|
|
||||||
|
/// Crop padding filter settings.
|
||||||
|
#[serde(default)]
|
||||||
|
pub crop: CropConfig,
|
||||||
|
|
||||||
/// Blur detection settings.
|
/// Blur detection settings.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub blur: BlurConfig,
|
pub blur: BlurConfig,
|
||||||
|
|
@ -547,6 +586,7 @@ impl Default for ProcessingConfig {
|
||||||
max_workers: 1,
|
max_workers: 1,
|
||||||
use_preview: true,
|
use_preview: true,
|
||||||
face_resolution: FaceResolutionConfig::default(),
|
face_resolution: FaceResolutionConfig::default(),
|
||||||
|
crop: CropConfig::default(),
|
||||||
blur: BlurConfig::default(),
|
blur: BlurConfig::default(),
|
||||||
brightness: BrightnessConfig::default(),
|
brightness: BrightnessConfig::default(),
|
||||||
head_pose: HeadPoseConfig::default(),
|
head_pose: HeadPoseConfig::default(),
|
||||||
|
|
@ -568,6 +608,7 @@ impl ProcessingConfig {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.face_resolution.validate()?;
|
self.face_resolution.validate()?;
|
||||||
|
self.crop.validate()?;
|
||||||
self.blur.validate()?;
|
self.blur.validate()?;
|
||||||
self.brightness.validate()?;
|
self.brightness.validate()?;
|
||||||
self.head_pose.validate()?;
|
self.head_pose.validate()?;
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,25 @@ use image::imageops::FilterType;
|
||||||
use image::{DynamicImage, GenericImageView, RgbImage};
|
use image::{DynamicImage, GenericImageView, RgbImage};
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
/// Per-edge padding fractions (0.0-1.0) relative to crop size.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct PaddingEdges {
|
||||||
|
pub top: f32,
|
||||||
|
pub bottom: f32,
|
||||||
|
pub left: f32,
|
||||||
|
pub right: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PaddingEdges {
|
||||||
|
/// Total fraction of crop area that is padding.
|
||||||
|
pub fn total_fraction(&self) -> f32 {
|
||||||
|
// Area = 1 - (1-left-right)*(1-top-bottom)
|
||||||
|
let inner_w = (1.0 - self.left - self.right).max(0.0);
|
||||||
|
let inner_h = (1.0 - self.top - self.bottom).max(0.0);
|
||||||
|
1.0 - inner_w * inner_h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Result of cropping a face from an image.
|
/// Result of cropping a face from an image.
|
||||||
pub struct CropResult {
|
pub struct CropResult {
|
||||||
/// The cropped image at full resolution.
|
/// The cropped image at full resolution.
|
||||||
|
|
@ -17,6 +36,10 @@ pub struct CropResult {
|
||||||
pub resized: DynamicImage,
|
pub resized: DynamicImage,
|
||||||
/// The face bounding box in crop coordinates.
|
/// The face bounding box in crop coordinates.
|
||||||
pub face_rect: BoundingBox,
|
pub face_rect: BoundingBox,
|
||||||
|
/// Fraction of the crop area that falls outside the original image (0.0-1.0).
|
||||||
|
pub padding_fraction: f32,
|
||||||
|
/// Per-edge padding fractions.
|
||||||
|
pub padding_edges: PaddingEdges,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Crop and resize the face from an image using bounding box.
|
/// Crop and resize the face from an image using bounding box.
|
||||||
|
|
@ -70,6 +93,25 @@ pub fn crop_face_with_intermediate(
|
||||||
let ideal_x1 = center_x as i32 - crop_size as i32 / 2;
|
let ideal_x1 = center_x as i32 - crop_size as i32 / 2;
|
||||||
let ideal_y1 = center_y as i32 - crop_size as i32 / 2;
|
let ideal_y1 = center_y as i32 - crop_size as i32 / 2;
|
||||||
|
|
||||||
|
// Calculate what fraction of the crop area falls outside the image bounds.
|
||||||
|
let overlap_x1 = (ideal_x1).max(0) as u64;
|
||||||
|
let overlap_y1 = (ideal_y1).max(0) as u64;
|
||||||
|
let overlap_x2 = ((ideal_x1 + crop_size as i32) as u64).min(img_width as u64);
|
||||||
|
let overlap_y2 = ((ideal_y1 + crop_size as i32) as u64).min(img_height as u64);
|
||||||
|
let overlap_area =
|
||||||
|
overlap_x2.saturating_sub(overlap_x1) * overlap_y2.saturating_sub(overlap_y1);
|
||||||
|
let total_area = crop_size as u64 * crop_size as u64;
|
||||||
|
let padding_fraction = 1.0 - (overlap_area as f32 / total_area as f32);
|
||||||
|
|
||||||
|
// Per-edge padding as fraction of crop size
|
||||||
|
let cs = crop_size as f32;
|
||||||
|
let padding_edges = PaddingEdges {
|
||||||
|
left: (-ideal_x1).max(0) as f32 / cs,
|
||||||
|
top: (-ideal_y1).max(0) as f32 / cs,
|
||||||
|
right: ((ideal_x1 + crop_size as i32) - img_width as i32).max(0) as f32 / cs,
|
||||||
|
bottom: ((ideal_y1 + crop_size as i32) - img_height as i32).max(0) as f32 / cs,
|
||||||
|
};
|
||||||
|
|
||||||
// Crop with replicate-fill: always keeps the face centered by extending
|
// Crop with replicate-fill: always keeps the face centered by extending
|
||||||
// edge pixels at image borders instead of shifting the crop window.
|
// edge pixels at image borders instead of shifting the crop window.
|
||||||
let cropped = crop_with_replicate_fill(img, ideal_x1, ideal_y1, crop_size);
|
let cropped = crop_with_replicate_fill(img, ideal_x1, ideal_y1, crop_size);
|
||||||
|
|
@ -89,6 +131,8 @@ pub fn crop_face_with_intermediate(
|
||||||
cropped,
|
cropped,
|
||||||
resized,
|
resized,
|
||||||
face_rect,
|
face_rect,
|
||||||
|
padding_fraction,
|
||||||
|
padding_edges,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ pub mod steps;
|
||||||
mod traits;
|
mod traits;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
pub use crop_utils::{crop_face_with_intermediate, CropResult};
|
pub use crop_utils::{crop_face_with_intermediate, CropResult, PaddingEdges};
|
||||||
pub use debug_utils::{draw_simple_text, face_rect_pixels};
|
pub use debug_utils::{draw_simple_text, face_rect_pixels};
|
||||||
pub use orientation::load_image_with_orientation;
|
pub use orientation::load_image_with_orientation;
|
||||||
pub use traits::*;
|
pub use traits::*;
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::pipeline::crop_face_with_intermediate;
|
use crate::pipeline::crop_face_with_intermediate;
|
||||||
|
use crate::pipeline::debug_utils::draw_simple_text;
|
||||||
use crate::pipeline::{
|
use crate::pipeline::{
|
||||||
computed_keys, BoundingBox, ComputedValue, PipelineContext, ProcessingStep, StepOutcome,
|
computed_keys, BoundingBox, ComputedValue, PipelineContext, ProcessingStep, StepOutcome,
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use image::{DynamicImage, Rgb};
|
||||||
|
|
||||||
/// Crops the face region from the full image and resizes it.
|
/// Crops the face region from the full image and resizes it.
|
||||||
///
|
///
|
||||||
|
|
@ -39,6 +41,30 @@ impl ProcessingStep for CropAndResizeStep {
|
||||||
// Crop returns CropResult with cropped images and face rectangle in crop coordinates
|
// Crop returns CropResult with cropped images and face rectangle in crop coordinates
|
||||||
match crop_face_with_intermediate(image, &ctx.face_data, output_size, eye_distance) {
|
match crop_face_with_intermediate(image, &ctx.face_data, output_size, eye_distance) {
|
||||||
Ok(crop_result) => {
|
Ok(crop_result) => {
|
||||||
|
// Store padding info for debug visualization
|
||||||
|
ctx.set_computed(
|
||||||
|
computed_keys::PADDING_EDGES,
|
||||||
|
ComputedValue::PaddingEdges(crop_result.padding_edges),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if too much of the crop falls outside the image
|
||||||
|
let crop_config = &config.processing.crop;
|
||||||
|
if crop_config.enabled {
|
||||||
|
let padding_pct = crop_result.padding_fraction * 100.0;
|
||||||
|
if padding_pct > crop_config.max_padding_percent {
|
||||||
|
// Set image so debug_visualize can use it
|
||||||
|
ctx.image = Some(crop_result.resized);
|
||||||
|
return StepOutcome::Skip {
|
||||||
|
ctx,
|
||||||
|
reason: "excessive_padding".to_string(),
|
||||||
|
detail: Some(format!(
|
||||||
|
"padding {:.1}% exceeds max {:.1}%",
|
||||||
|
padding_pct, crop_config.max_padding_percent
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use the pre-resized image from the crop function
|
// Use the pre-resized image from the crop function
|
||||||
let cropped_size = crop_result.cropped.width();
|
let cropped_size = crop_result.cropped.width();
|
||||||
ctx.image = Some(crop_result.resized);
|
ctx.image = Some(crop_result.resized);
|
||||||
|
|
@ -67,6 +93,117 @@ impl ProcessingStep for CropAndResizeStep {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn debug_visualize(&self, ctx: &PipelineContext, config: &Config) -> Option<DynamicImage> {
|
||||||
|
let edges = ctx
|
||||||
|
.get_computed(computed_keys::PADDING_EDGES)
|
||||||
|
.and_then(|v| v.as_padding_edges())?;
|
||||||
|
let image = ctx.image.as_ref()?;
|
||||||
|
let rgb = image.to_rgb8();
|
||||||
|
let (width, height) = (rgb.width(), rgb.height());
|
||||||
|
|
||||||
|
let mut debug_img = rgb.clone();
|
||||||
|
|
||||||
|
// Tint padded regions with a semi-transparent red overlay
|
||||||
|
let tint = |pixel: &Rgb<u8>| -> Rgb<u8> {
|
||||||
|
Rgb([
|
||||||
|
(pixel[0] as u16 / 2 + 127).min(255) as u8,
|
||||||
|
pixel[1] / 2,
|
||||||
|
pixel[2] / 2,
|
||||||
|
])
|
||||||
|
};
|
||||||
|
|
||||||
|
let left_px = (edges.left * width as f32).round() as u32;
|
||||||
|
let right_px = (edges.right * width as f32).round() as u32;
|
||||||
|
let top_px = (edges.top * height as f32).round() as u32;
|
||||||
|
let bottom_px = (edges.bottom * height as f32).round() as u32;
|
||||||
|
|
||||||
|
// Tint left edge
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..left_px.min(width) {
|
||||||
|
debug_img.put_pixel(x, y, tint(debug_img.get_pixel(x, y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tint right edge
|
||||||
|
for y in 0..height {
|
||||||
|
for x in width.saturating_sub(right_px)..width {
|
||||||
|
debug_img.put_pixel(x, y, tint(debug_img.get_pixel(x, y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tint top edge (only the non-corner part to avoid double-tinting)
|
||||||
|
for y in 0..top_px.min(height) {
|
||||||
|
for x in left_px.min(width)..width.saturating_sub(right_px) {
|
||||||
|
debug_img.put_pixel(x, y, tint(debug_img.get_pixel(x, y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Tint bottom edge (only the non-corner part)
|
||||||
|
for y in height.saturating_sub(bottom_px)..height {
|
||||||
|
for x in left_px.min(width)..width.saturating_sub(right_px) {
|
||||||
|
debug_img.put_pixel(x, y, tint(debug_img.get_pixel(x, y)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw padding percentage bar at the bottom
|
||||||
|
let total_pct = edges.total_fraction() * 100.0;
|
||||||
|
let max_pct = config.processing.crop.max_padding_percent;
|
||||||
|
|
||||||
|
let bar_height = 20u32;
|
||||||
|
let bar_y = height.saturating_sub(bar_height);
|
||||||
|
let bar_width = (width as f32 * 0.8) as u32;
|
||||||
|
let bar_x = (width - bar_width) / 2;
|
||||||
|
|
||||||
|
// Background
|
||||||
|
for y in bar_y..height {
|
||||||
|
for x in 0..width {
|
||||||
|
debug_img.put_pixel(x, y, Rgb([40, 40, 40]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bar outline
|
||||||
|
let outline_y = bar_y + 4;
|
||||||
|
let outline_height = bar_height - 8;
|
||||||
|
for x in bar_x..bar_x + bar_width {
|
||||||
|
debug_img.put_pixel(x, outline_y, Rgb([200, 200, 200]));
|
||||||
|
debug_img.put_pixel(x, outline_y + outline_height - 1, Rgb([200, 200, 200]));
|
||||||
|
}
|
||||||
|
for y in outline_y..outline_y + outline_height {
|
||||||
|
debug_img.put_pixel(bar_x, y, Rgb([200, 200, 200]));
|
||||||
|
debug_img.put_pixel(bar_x + bar_width - 1, y, Rgb([200, 200, 200]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill bar (scale: 0-50% maps to full bar)
|
||||||
|
let max_scale = 50.0_f32;
|
||||||
|
let normalized = (total_pct / max_scale).clamp(0.0, 1.0);
|
||||||
|
let fill_width = ((bar_width - 4) as f32 * normalized) as u32;
|
||||||
|
let fill_color = if total_pct > max_pct {
|
||||||
|
Rgb([255, 80, 80]) // Red - exceeds threshold
|
||||||
|
} else if total_pct > max_pct * 0.7 {
|
||||||
|
Rgb([255, 200, 80]) // Yellow - approaching threshold
|
||||||
|
} else {
|
||||||
|
Rgb([80, 255, 80]) // Green - well within threshold
|
||||||
|
};
|
||||||
|
for y in (outline_y + 2)..(outline_y + outline_height - 2) {
|
||||||
|
for x in (bar_x + 2)..(bar_x + 2 + fill_width) {
|
||||||
|
if x < width {
|
||||||
|
debug_img.put_pixel(x, y, fill_color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw threshold marker on the bar
|
||||||
|
let threshold_x = bar_x + 2 + ((bar_width - 4) as f32 * (max_pct / max_scale).clamp(0.0, 1.0)) as u32;
|
||||||
|
if threshold_x < bar_x + bar_width {
|
||||||
|
for y in outline_y..(outline_y + outline_height) {
|
||||||
|
debug_img.put_pixel(threshold_x, y, Rgb([255, 255, 255]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text label
|
||||||
|
let text = format!("{:.1}%", total_pct);
|
||||||
|
draw_simple_text(&mut debug_img, 5, bar_y + 6, &text, Rgb([255, 255, 255]));
|
||||||
|
|
||||||
|
Some(DynamicImage::ImageRgb8(debug_img))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,8 @@ pub mod computed_keys {
|
||||||
pub const HEAD_POSE: &str = "head_pose";
|
pub const HEAD_POSE: &str = "head_pose";
|
||||||
/// Face bounding box in current image coordinates computed by CropAndResizeStep.
|
/// Face bounding box in current image coordinates computed by CropAndResizeStep.
|
||||||
pub const FACE_RECT: &str = "face_rect";
|
pub const FACE_RECT: &str = "face_rect";
|
||||||
|
/// Per-edge padding fractions computed by CropAndResizeStep.
|
||||||
|
pub const PADDING_EDGES: &str = "padding_edges";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Outcome of a pipeline step execution.
|
/// Outcome of a pipeline step execution.
|
||||||
|
|
@ -63,6 +65,8 @@ pub enum ComputedValue {
|
||||||
Landmarks(Box<Landmarks>),
|
Landmarks(Box<Landmarks>),
|
||||||
/// Face bounding box in current image coordinates.
|
/// Face bounding box in current image coordinates.
|
||||||
FaceRect(BoundingBox),
|
FaceRect(BoundingBox),
|
||||||
|
/// Per-edge padding fractions from crop step.
|
||||||
|
PaddingEdges(super::PaddingEdges),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ComputedValue {
|
impl ComputedValue {
|
||||||
|
|
@ -121,6 +125,14 @@ impl ComputedValue {
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get as PaddingEdges if this is a PaddingEdges variant.
|
||||||
|
pub fn as_padding_edges(&self) -> Option<&super::PaddingEdges> {
|
||||||
|
match self {
|
||||||
|
ComputedValue::PaddingEdges(v) => Some(v),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A debug image with metadata about whether the step passed or failed.
|
/// A debug image with metadata about whether the step passed or failed.
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
//! Configuration endpoints.
|
//! Configuration endpoints.
|
||||||
|
|
||||||
use crate::config::{
|
use crate::config::{
|
||||||
AlignmentConfig, BlurConfig, BrightnessConfig, EyeFilterConfig, FaceResolutionConfig,
|
AlignmentConfig, BlurConfig, BrightnessConfig, CropConfig, EyeFilterConfig,
|
||||||
HeadPoseConfig, OutputConfig, ProcessingConfig, TimeIntervalConfig, TimestampConfig,
|
FaceResolutionConfig, HeadPoseConfig, OutputConfig, ProcessingConfig, TimeIntervalConfig,
|
||||||
VideoConfig, CONFIG_PATH,
|
TimestampConfig, VideoConfig, CONFIG_PATH,
|
||||||
};
|
};
|
||||||
use crate::web::state::AppState;
|
use crate::web::state::AppState;
|
||||||
use axum::{extract::State, http::StatusCode, response::Json};
|
use axum::{extract::State, http::StatusCode, response::Json};
|
||||||
|
|
@ -48,6 +48,7 @@ pub struct ConfigUpdateRequest {
|
||||||
pub struct ProcessingConfigUpdate {
|
pub struct ProcessingConfigUpdate {
|
||||||
pub max_workers: Option<usize>,
|
pub max_workers: Option<usize>,
|
||||||
pub face_resolution: Option<FaceResolutionConfig>,
|
pub face_resolution: Option<FaceResolutionConfig>,
|
||||||
|
pub crop: Option<CropConfig>,
|
||||||
pub blur: Option<BlurConfig>,
|
pub blur: Option<BlurConfig>,
|
||||||
pub brightness: Option<BrightnessConfig>,
|
pub brightness: Option<BrightnessConfig>,
|
||||||
pub head_pose: Option<HeadPoseConfig>,
|
pub head_pose: Option<HeadPoseConfig>,
|
||||||
|
|
@ -108,6 +109,18 @@ fn validate_processing_config(proc: &ProcessingConfigUpdate) -> Result<(), Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ref cr) = proc.crop {
|
||||||
|
if cr.enabled && !(0.0..=100.0).contains(&cr.max_padding_percent) {
|
||||||
|
return Err(ValidationError::new(
|
||||||
|
"processing.crop.max_padding_percent",
|
||||||
|
format!(
|
||||||
|
"must be between 0.0 and 100.0, got {}",
|
||||||
|
cr.max_padding_percent
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(ref bl) = proc.blur {
|
if let Some(ref bl) = proc.blur {
|
||||||
if bl.enabled && bl.min_sharpness < 0.0 {
|
if bl.enabled && bl.min_sharpness < 0.0 {
|
||||||
return Err(ValidationError::new(
|
return Err(ValidationError::new(
|
||||||
|
|
@ -268,6 +281,9 @@ pub async fn update_config(
|
||||||
if let Some(v) = proc.face_resolution {
|
if let Some(v) = proc.face_resolution {
|
||||||
config.processing.face_resolution = v;
|
config.processing.face_resolution = v;
|
||||||
}
|
}
|
||||||
|
if let Some(v) = proc.crop {
|
||||||
|
config.processing.crop = v;
|
||||||
|
}
|
||||||
if let Some(v) = proc.blur {
|
if let Some(v) = proc.blur {
|
||||||
config.processing.blur = v;
|
config.processing.blur = v;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue