Fixed an issue with caching. Initial resize is tied to eye distance setting.
This commit is contained in:
parent
250cbd491a
commit
4f4f64aab1
7 changed files with 24 additions and 33 deletions
|
|
@ -14,6 +14,7 @@
|
||||||
let deleting = $state(false);
|
let deleting = $state(false);
|
||||||
let compiling = $state(false);
|
let compiling = $state(false);
|
||||||
let videoExists = $state(false);
|
let videoExists = $state(false);
|
||||||
|
let cacheBust = $state(Date.now());
|
||||||
|
|
||||||
let selectedCount = $derived(selectedImages.size);
|
let selectedCount = $derived(selectedImages.size);
|
||||||
let allSelected = $derived(images.length > 0 && selectedImages.size === images.length);
|
let allSelected = $derived(images.length > 0 && selectedImages.size === images.length);
|
||||||
|
|
@ -21,6 +22,7 @@
|
||||||
async function loadImages() {
|
async function loadImages() {
|
||||||
loading = true;
|
loading = true;
|
||||||
error = null;
|
error = null;
|
||||||
|
cacheBust = Date.now();
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/output/${encodeURIComponent(folderName)}/images`);
|
const res = await fetch(`/api/output/${encodeURIComponent(folderName)}/images`);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
|
|
@ -197,7 +199,7 @@
|
||||||
<span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span>
|
<span class="checkbox">{selectedImages.has(image.filename) ? '✓' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<img
|
<img
|
||||||
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}"
|
src="/output/{encodeURIComponent(folderName)}/images/{encodeURIComponent(image.filename)}?v={cacheBust}"
|
||||||
alt={image.filename}
|
alt={image.filename}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,10 @@
|
||||||
|
|
||||||
let { folderName = null } = $props();
|
let { folderName = null } = $props();
|
||||||
|
|
||||||
|
let cacheBust = $state(Date.now());
|
||||||
let videoUrl = $derived(
|
let videoUrl = $derived(
|
||||||
folderName
|
folderName
|
||||||
? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4`
|
? `/output/${encodeURIComponent(folderName)}/${encodeURIComponent(folderName)}.mp4?v=${cacheBust}`
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
let videoError = $state(false);
|
let videoError = $state(false);
|
||||||
|
|
|
||||||
|
|
@ -316,9 +316,7 @@ pub struct AlignmentConfig {
|
||||||
|
|
||||||
impl Default for AlignmentConfig {
|
impl Default for AlignmentConfig {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self { eye_distance: 0.3 }
|
||||||
eye_distance: 0.3,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -341,8 +339,7 @@ impl AlignmentConfig {
|
||||||
}
|
}
|
||||||
if self.eye_distance < 0.1 || self.eye_distance > 0.5 {
|
if self.eye_distance < 0.1 || self.eye_distance > 0.5 {
|
||||||
return Err(Error::Config(
|
return Err(Error::Config(
|
||||||
"Alignment eye_distance should be between 0.1 and 0.5 for best results"
|
"Alignment eye_distance should be between 0.1 and 0.5 for best results".to_string(),
|
||||||
.to_string(),
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ pub fn crop_face_with_intermediate(
|
||||||
img: &DynamicImage,
|
img: &DynamicImage,
|
||||||
face_data: &FaceData,
|
face_data: &FaceData,
|
||||||
output_size: u32,
|
output_size: u32,
|
||||||
|
eye_distance: f32,
|
||||||
) -> Result<CropResult> {
|
) -> Result<CropResult> {
|
||||||
let (img_width, img_height) = img.dimensions();
|
let (img_width, img_height) = img.dimensions();
|
||||||
|
|
||||||
|
|
@ -50,20 +51,20 @@ pub fn crop_face_with_intermediate(
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand the crop area to include some context around the face
|
// Derive crop size from eye_distance so the initial crop captures
|
||||||
// and make it square for consistent output
|
// the right amount of context for the alignment step.
|
||||||
let face_size = face_width.max(face_height);
|
const IPD_RATIO: f32 = 0.37;
|
||||||
let padding = face_size / 2; // 50% padding on each side
|
const SAFETY_MARGIN: f32 = 1.30;
|
||||||
let crop_size = face_size + padding * 2;
|
|
||||||
|
let face_size = face_width.max(face_height) as f32;
|
||||||
|
let estimated_ipd = IPD_RATIO * face_width as f32;
|
||||||
|
let ideal_crop = (estimated_ipd / eye_distance) * SAFETY_MARGIN;
|
||||||
|
let crop_size = ideal_crop.max(face_size * 1.5).max(10.0).round() as u32;
|
||||||
|
|
||||||
// Calculate center of face
|
// Calculate center of face
|
||||||
let center_x = (x1 + x2) / 2;
|
let center_x = (x1 + x2) / 2;
|
||||||
let center_y = (y1 + y2) / 2;
|
let center_y = (y1 + y2) / 2;
|
||||||
|
|
||||||
if crop_size < 10 {
|
|
||||||
return Err(Error::ImageProcessing("Crop area too small".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ideal crop region centered on the face (may extend outside image bounds)
|
// Ideal crop region centered on the face (may extend outside image bounds)
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -73,15 +73,6 @@ impl ProcessingStep for AlignmentStep {
|
||||||
|
|
||||||
ctx.image = Some(aligned);
|
ctx.image = Some(aligned);
|
||||||
|
|
||||||
tracing::trace!(
|
|
||||||
"Aligned: left_eye=({:.1},{:.1}), right_eye=({:.1},{:.1}), target_size={}",
|
|
||||||
left_eye.x,
|
|
||||||
left_eye.y,
|
|
||||||
right_eye.x,
|
|
||||||
right_eye.y,
|
|
||||||
output_size
|
|
||||||
);
|
|
||||||
|
|
||||||
StepOutcome::Continue(ctx)
|
StepOutcome::Continue(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,10 +169,7 @@ fn calculate_eye_alignment_transform(
|
||||||
let left_eye_x = alignment_config.left_eye_x();
|
let left_eye_x = alignment_config.left_eye_x();
|
||||||
let left_eye_y = alignment_config.left_eye_y();
|
let left_eye_y = alignment_config.left_eye_y();
|
||||||
|
|
||||||
let left_eye_target = Point::new(
|
let left_eye_target = Point::new(output_size_f * left_eye_x, output_size_f * left_eye_y);
|
||||||
output_size_f * left_eye_x,
|
|
||||||
output_size_f * left_eye_y,
|
|
||||||
);
|
|
||||||
let right_eye_target = Point::new(
|
let right_eye_target = Point::new(
|
||||||
output_size_f * (1.0 - left_eye_x),
|
output_size_f * (1.0 - left_eye_x),
|
||||||
output_size_f * left_eye_y,
|
output_size_f * left_eye_y,
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,10 @@ impl ProcessingStep for CropAndResizeStep {
|
||||||
};
|
};
|
||||||
|
|
||||||
let output_size = config.processing.output.size;
|
let output_size = config.processing.output.size;
|
||||||
|
let eye_distance = config.processing.alignment.eye_distance;
|
||||||
|
|
||||||
// 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) {
|
match crop_face_with_intermediate(image, &ctx.face_data, output_size, eye_distance) {
|
||||||
Ok(crop_result) => {
|
Ok(crop_result) => {
|
||||||
// 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();
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,14 @@ pub fn create_router(state: AppState) -> Router {
|
||||||
)
|
)
|
||||||
.route("/api/config", get(get_config))
|
.route("/api/config", get(get_config))
|
||||||
.route("/api/config", put(update_config))
|
.route("/api/config", put(update_config))
|
||||||
// Serve output files (video, images) with cache headers
|
// Serve output files (video, images) — no-cache so browsers revalidate
|
||||||
|
// after reprocessing (filenames stay the same across runs)
|
||||||
.nest_service(
|
.nest_service(
|
||||||
"/output",
|
"/output",
|
||||||
ServiceBuilder::new()
|
ServiceBuilder::new()
|
||||||
.layer(SetResponseHeaderLayer::if_not_present(
|
.layer(SetResponseHeaderLayer::overriding(
|
||||||
header::CACHE_CONTROL,
|
header::CACHE_CONTROL,
|
||||||
header::HeaderValue::from_static("public, max-age=86400"),
|
header::HeaderValue::from_static("no-store"),
|
||||||
))
|
))
|
||||||
.service(ServeDir::new(output_dir)),
|
.service(ServeDir::new(output_dir)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue