Added visualisation for face alignment setting
This commit is contained in:
parent
5cbec95e74
commit
b780dbad3e
4 changed files with 148 additions and 57 deletions
43
.github/workflows/docker-image.yml
vendored
43
.github/workflows/docker-image.yml
vendored
|
|
@ -1,43 +0,0 @@
|
||||||
# name: Build and Publish image to Docker Hub
|
|
||||||
# on:
|
|
||||||
# push:
|
|
||||||
# branches:
|
|
||||||
# - master
|
|
||||||
# tags:
|
|
||||||
# - 'v*'
|
|
||||||
|
|
||||||
# jobs:
|
|
||||||
# publish_images:
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - name: checkout
|
|
||||||
# uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# - name: Extract metadata (tags, labels) for Docker
|
|
||||||
# id: meta
|
|
||||||
# uses: docker/metadata-action@v5
|
|
||||||
# with:
|
|
||||||
# images: arnaudcayrol/immich-selfie-timelapse
|
|
||||||
# tags: |
|
|
||||||
# type=ref,event=branch
|
|
||||||
# type=ref,event=tag
|
|
||||||
# type=semver,pattern={{version}}
|
|
||||||
# type=semver,pattern={{major}}.{{minor}}
|
|
||||||
# type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
# - name: Set up Docker Buildx
|
|
||||||
# uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
# - name: Login to Docker Hub
|
|
||||||
# uses: docker/login-action@v3
|
|
||||||
# with:
|
|
||||||
# username: arnaudcayrol
|
|
||||||
# password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
# - name: Build and push
|
|
||||||
# uses: docker/build-push-action@v5
|
|
||||||
# with:
|
|
||||||
# context: .
|
|
||||||
# push: true
|
|
||||||
# tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
# labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { DEFAULT_CONFIG, TIMING } from '../constants.js';
|
import { DEFAULT_CONFIG, TIMING } from '../constants.js';
|
||||||
import { handleError } from '../errorHandler.js';
|
import { handleError } from '../errorHandler.js';
|
||||||
import EyeIndicator from './visual/EyeIndicator.svelte';
|
import EyeIndicator from './visual/EyeIndicator.svelte';
|
||||||
|
import AlignmentIndicator from './visual/AlignmentIndicator.svelte';
|
||||||
|
|
||||||
let { disabled = false } = $props();
|
let { disabled = false } = $props();
|
||||||
|
|
||||||
|
|
@ -314,17 +315,12 @@
|
||||||
|
|
||||||
<!-- Alignment Section -->
|
<!-- Alignment Section -->
|
||||||
<div class="setting-section">
|
<div class="setting-section">
|
||||||
<div class="section-header">
|
|
||||||
<span class="section-title">Face Alignment</span>
|
|
||||||
<span class="setting-hint" style="font-style: italic; color: var(--text-secondary);">Always enabled</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="setting-row sub-setting">
|
<div class="setting-row sub-setting">
|
||||||
<label for="eye-distance">
|
<label for="eye-distance">
|
||||||
<span class="setting-label">Eye Distance</span>
|
<span class="setting-label">Eye Distance</span>
|
||||||
<span class="setting-hint">Distance between eyes as % of image width (larger = zoom in)</span>
|
<span class="setting-hint">Distance between eyes as % of image width (larger = zoom in)</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="setting-control">
|
<div class="setting-control-with-visual">
|
||||||
<input
|
<input
|
||||||
id="eye-distance"
|
id="eye-distance"
|
||||||
type="range"
|
type="range"
|
||||||
|
|
@ -334,6 +330,9 @@
|
||||||
step="0.01"
|
step="0.01"
|
||||||
/>
|
/>
|
||||||
<span class="value">{(config.processing.alignment.eye_distance * 100).toFixed(0)}%</span>
|
<span class="value">{(config.processing.alignment.eye_distance * 100).toFixed(0)}%</span>
|
||||||
|
<div class="inline-visual-indicator">
|
||||||
|
<AlignmentIndicator eyeDistance={config.processing.alignment.eye_distance} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -740,11 +739,11 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 0.25rem 0.5rem;
|
padding: 0.5rem;
|
||||||
background: #0f0f0f;
|
background: #0f0f0f;
|
||||||
border: 1px solid #333;
|
border: 1px solid #333;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
min-width: 60px;
|
width: 96px;
|
||||||
height: 32px;
|
height: 96px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
137
frontend/src/lib/components/visual/AlignmentIndicator.svelte
Normal file
137
frontend/src/lib/components/visual/AlignmentIndicator.svelte
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Visual indicator for alignment settings showing head position and scale.
|
||||||
|
* Displays an anatomically proportioned face with eyes positioned according to eye_distance.
|
||||||
|
*
|
||||||
|
* The eye_distance parameter controls zoom level:
|
||||||
|
* - Smaller values (0.1): zoomed out, head appears smaller, shows more neck
|
||||||
|
* - Larger values (0.5): zoomed in, head appears larger, tighter crop
|
||||||
|
*
|
||||||
|
* Proportions match the processing pipeline (alignment.rs):
|
||||||
|
* - Eye positions calculated with FACE_CENTER_BELOW_EYES = 0.5
|
||||||
|
* - Face center (hairline-to-chin midpoint + neck) positioned at frame center
|
||||||
|
* - This creates a vertical offset showing more neck than forehead
|
||||||
|
* - Inter-pupillary distance (IPD) is ~37% of face width
|
||||||
|
*/
|
||||||
|
let { eyeDistance = 0.3 } = $props();
|
||||||
|
|
||||||
|
// Frame dimensions (square output image)
|
||||||
|
const frameSize = 120;
|
||||||
|
|
||||||
|
// Calculate eye positions using the same formulas as alignment.rs
|
||||||
|
const FACE_CENTER_BELOW_EYES = 0.5;
|
||||||
|
|
||||||
|
const leftEyeX = $derived((1.0 - eyeDistance) / 2.0);
|
||||||
|
const rightEyeX = $derived(1.0 - leftEyeX);
|
||||||
|
const eyeY = $derived(0.5 - eyeDistance * FACE_CENTER_BELOW_EYES);
|
||||||
|
|
||||||
|
// Convert to pixel coordinates
|
||||||
|
const leftEyePos = $derived({ x: leftEyeX * frameSize, y: eyeY * frameSize });
|
||||||
|
const rightEyePos = $derived({ x: rightEyeX * frameSize, y: eyeY * frameSize });
|
||||||
|
|
||||||
|
// Calculate eye distance in pixels
|
||||||
|
const eyeDistancePx = $derived(eyeDistance * frameSize);
|
||||||
|
|
||||||
|
// Calculate eye center point
|
||||||
|
const eyeCenterX = $derived((leftEyePos.x + rightEyePos.x) / 2);
|
||||||
|
const eyeCenterY = $derived(eyeY * frameSize);
|
||||||
|
|
||||||
|
// Face proportions based on inter-pupillary distance (IPD)
|
||||||
|
// Per crop_utils.rs, IPD is ~0.37 of face width
|
||||||
|
// Face center (hairline-to-chin midpoint + neck) is 0.5 * IPD below eyes
|
||||||
|
|
||||||
|
// Anatomical proportions relative to IPD:
|
||||||
|
// - Eyes to top of head (forehead): ~0.5 IPD
|
||||||
|
// - Eyes to face center: 0.5 IPD (by definition)
|
||||||
|
// - Face center to bottom (lower face + neck): ~0.5 IPD
|
||||||
|
// Total head height: ~1.5 IPD
|
||||||
|
const foreheadHeight = $derived(eyeDistancePx * 0.5);
|
||||||
|
const lowerFaceHeight = $derived(eyeDistancePx * 1.0);
|
||||||
|
|
||||||
|
// Head ellipse dimensions (head is taller than wide)
|
||||||
|
const headCenter = $derived({
|
||||||
|
x: eyeCenterX,
|
||||||
|
y: eyeCenterY + (lowerFaceHeight - foreheadHeight) / 2
|
||||||
|
});
|
||||||
|
const headWidth = $derived(eyeDistancePx * 1.35); // Width relative to IPD
|
||||||
|
const headHeight = $derived(eyeDistancePx * 1.85); // Height > width (ratio ~1.37)
|
||||||
|
|
||||||
|
// Eye rendering parameters (scale with IPD)
|
||||||
|
const eyeRadius = $derived(Math.max(2.5, eyeDistancePx * 0.12));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 {frameSize} {frameSize}"
|
||||||
|
class="alignment-indicator"
|
||||||
|
role="img"
|
||||||
|
aria-label="Face alignment indicator"
|
||||||
|
>
|
||||||
|
<!-- Background frame -->
|
||||||
|
<rect
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width={frameSize}
|
||||||
|
height={frameSize}
|
||||||
|
fill="rgba(0, 0, 0, 0.8)"
|
||||||
|
stroke="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Head ellipse (face shape) -->
|
||||||
|
<ellipse
|
||||||
|
cx={headCenter.x}
|
||||||
|
cy={headCenter.y}
|
||||||
|
rx={headWidth}
|
||||||
|
ry={headHeight}
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
class="head"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Left eye -->
|
||||||
|
<circle
|
||||||
|
cx={leftEyePos.x}
|
||||||
|
cy={leftEyePos.y}
|
||||||
|
r={eyeRadius}
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
class="eye"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Right eye -->
|
||||||
|
<circle
|
||||||
|
cx={rightEyePos.x}
|
||||||
|
cy={rightEyePos.y}
|
||||||
|
r={eyeRadius}
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
class="eye"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Simple mouth curve (positioned between eyes and chin) -->
|
||||||
|
<path
|
||||||
|
d="M {eyeCenterX - eyeDistancePx * 0.35} {eyeCenterY + eyeDistancePx * 0.65}
|
||||||
|
Q {eyeCenterX} {eyeCenterY + eyeDistancePx * 0.75},
|
||||||
|
{eyeCenterX + eyeDistancePx * 0.35} {eyeCenterY + eyeDistancePx * 0.65}"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
class="mouth"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.alignment-indicator {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head,
|
||||||
|
.eye,
|
||||||
|
.mouth {
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -19,9 +19,7 @@
|
||||||
|
|
||||||
// Calculate eye height based on openness
|
// Calculate eye height based on openness
|
||||||
const eyeHeight = $derived(minEyeHeight + normalizedOpenness * (maxEyeHeight - minEyeHeight));
|
const eyeHeight = $derived(minEyeHeight + normalizedOpenness * (maxEyeHeight - minEyeHeight));
|
||||||
|
const pupilRadius = 2;
|
||||||
const irisRadius = 12;
|
|
||||||
const pupilRadius = 3;
|
|
||||||
|
|
||||||
// Create almond-shaped eye outline using a path
|
// Create almond-shaped eye outline using a path
|
||||||
// The path creates a pointed eye shape (yes, this is a Skyrim reference.)
|
// The path creates a pointed eye shape (yes, this is a Skyrim reference.)
|
||||||
|
|
@ -60,7 +58,7 @@
|
||||||
<circle
|
<circle
|
||||||
cx={centerX}
|
cx={centerX}
|
||||||
cy={centerY}
|
cy={centerY}
|
||||||
r={Math.min(irisRadius, eyeHeight / 2 - 3)}
|
r={eyeHeight / 2}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="white"
|
stroke="white"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
|
|
@ -71,7 +69,7 @@
|
||||||
<circle
|
<circle
|
||||||
cx={centerX}
|
cx={centerX}
|
||||||
cy={centerY}
|
cy={centerY}
|
||||||
r={Math.min(pupilRadius, eyeHeight / 2 - 8)}
|
r={Math.max(0, Math.min(pupilRadius, eyeHeight / 2 - 8))}
|
||||||
fill="white"
|
fill="white"
|
||||||
class="pupil"
|
class="pupil"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue