(frontend) add mustache effects

Added functionality to display mustache effects
on detected faces. Enhanced the EffectsConfiguration
component to toggle these effects and updated
localization files for this effects.
This commit is contained in:
Arnaud Robin
2025-03-31 00:23:57 +02:00
committed by aleb_the_flash
parent 0d6881382b
commit 1d5aebcfdc
8 changed files with 160 additions and 76 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -46,20 +46,25 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
type: ProcessorType type: ProcessorType
// Glasses image element // Effect images
glassesImage?: HTMLImageElement glassesImage?: HTMLImageElement
mustacheImage?: HTMLImageElement
constructor(opts: BackgroundOptions) { constructor(opts: BackgroundOptions) {
this.name = 'face_landmarks' this.name = 'face_landmarks'
this.options = opts this.options = opts
this.type = ProcessorType.FACE_LANDMARKS this.type = ProcessorType.FACE_LANDMARKS
this._initGlassesImage() this._initEffectImages()
} }
private _initGlassesImage() { private _initEffectImages() {
this.glassesImage = new Image() this.glassesImage = new Image()
this.glassesImage.src = '/assets/glasses.png' // You'll need to add this image to your public assets this.glassesImage.src = '/assets/glasses.png'
this.glassesImage.crossOrigin = 'anonymous' this.glassesImage.crossOrigin = 'anonymous'
this.mustacheImage = new Image()
this.mustacheImage.src = '/assets/mustache.png'
this.mustacheImage.crossOrigin = 'anonymous'
} }
static get isSupported() { static get isSupported() {
@@ -162,6 +167,53 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
) )
} }
private drawEffect(
leftPoint: { x: number; y: number },
rightPoint: { x: number; y: number },
image: HTMLImageElement,
widthScale: number,
heightScale: number
) {
// Calculate distance between points
const distance = Math.sqrt(
Math.pow(rightPoint.x - leftPoint.x, 2) +
Math.pow(rightPoint.y - leftPoint.y, 2)
)
// Scale image based on distance
const width = distance * PROCESSING_WIDTH * widthScale
const height = width * heightScale
// Calculate center position between points
const centerX = (leftPoint.x + rightPoint.x) / 2
const centerY = (leftPoint.y + rightPoint.y) / 2
// Draw image
this.outputCanvasCtx!.save()
this.outputCanvasCtx!.translate(
centerX * PROCESSING_WIDTH,
centerY * PROCESSING_HEIGHT
)
// Calculate rotation angle based on point positions
const angle = Math.atan2(
rightPoint.y - leftPoint.y,
rightPoint.x - leftPoint.x
)
this.outputCanvasCtx!.rotate(angle)
// Draw image centered at the midpoint between points
this.outputCanvasCtx!.drawImage(
image,
-width / 2,
-height / 2,
width,
height
)
this.outputCanvasCtx!.restore()
}
async drawFaceLandmarks() { async drawFaceLandmarks() {
// Draw the original video frame at the canvas size // Draw the original video frame at the canvas size
this.outputCanvasCtx!.drawImage( this.outputCanvasCtx!.drawImage(
@@ -185,49 +237,20 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface {
this.outputCanvasCtx!.lineWidth = 2 this.outputCanvasCtx!.lineWidth = 2
for (const face of this.faceLandmarkerResult.faceLandmarks) { for (const face of this.faceLandmarkerResult.faceLandmarks) {
// Find eye landmarks (indices 33 and 263 are the left and right eye centers) // Find eye landmarks (indices 33 and 263 are the left and right eye corners)
const leftEye = face[33] const leftEye = face[468]
const rightEye = face[263] const rightEye = face[473]
if (leftEye && rightEye) { // Find mouth landmarks for mustache (indices 0 and 17 are the left and right corners of the mouth)
// Calculate glasses position and size const leftMoustache = face[92]
const eyeDistance = Math.sqrt( const rightMoustache = face[322]
Math.pow(rightEye.x - leftEye.x, 2) +
Math.pow(rightEye.y - leftEye.y, 2)
)
// Scale glasses based on eye distance if (leftEye && rightEye && this.options.showGlasses) {
const glassesWidth = eyeDistance * PROCESSING_WIDTH * 2.5 // Adjust multiplier as needed this.drawEffect(leftEye, rightEye, this.glassesImage!, 2.5, 0.7)
const glassesHeight = glassesWidth * 0.7 // Adjust aspect ratio as needed }
// Calculate center position between eyes if (leftMoustache && rightMoustache && this.options.showMustache) {
const centerX = (leftEye.x + rightEye.x) / 2 this.drawEffect(leftMoustache, rightMoustache, this.mustacheImage!, 1.5, 0.5)
const centerY = (leftEye.y + rightEye.y) / 2
// Draw glasses
this.outputCanvasCtx!.save()
this.outputCanvasCtx!.translate(
centerX * PROCESSING_WIDTH,
centerY * PROCESSING_HEIGHT
)
// Calculate rotation angle based on eye positions
const angle = Math.atan2(
rightEye.y - leftEye.y,
rightEye.x - leftEye.x
)
this.outputCanvasCtx!.rotate(angle)
// Draw glasses centered at the midpoint between eyes
this.outputCanvasCtx!.drawImage(
this.glassesImage!,
-glassesWidth / 2,
-glassesHeight / 2,
glassesWidth,
glassesHeight
)
this.outputCanvasCtx!.restore()
} }
} }
} }

View File

@@ -8,7 +8,8 @@ import { FaceLandmarksProcessor } from './FaceLandmarksProcessor'
export type BackgroundOptions = { export type BackgroundOptions = {
blurRadius?: number blurRadius?: number
imagePath?: string imagePath?: string
showFaceLandmarks?: boolean showGlasses?: boolean
showMustache?: boolean
} }
export interface ProcessorSerialized { export interface ProcessorSerialized {

View File

@@ -5,17 +5,17 @@ import {
BackgroundProcessorFactory, BackgroundProcessorFactory,
BackgroundProcessorInterface, BackgroundProcessorInterface,
ProcessorType, ProcessorType,
BackgroundOptions
} from '../blur' } from '../blur'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { Text, P, ToggleButton, H } from '@/primitives' import { Text, P, ToggleButton, H } from '@/primitives'
import { styled } from '@/styled-system/jsx' import { styled } from '@/styled-system/jsx'
import { BackgroundOptions } from '@livekit/track-processors'
import { BlurOn } from '@/components/icons/BlurOn' import { BlurOn } from '@/components/icons/BlurOn'
import { BlurOnStrong } from '@/components/icons/BlurOnStrong' import { BlurOnStrong } from '@/components/icons/BlurOnStrong'
import { useTrackToggle } from '@livekit/components-react' import { useTrackToggle } from '@livekit/components-react'
import { Loader } from '@/primitives/Loader' import { Loader } from '@/primitives/Loader'
import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay' import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay'
import { RiProhibited2Line, RiUserVoiceLine } from '@remixicon/react' import { RiProhibited2Line, RiGlassesLine, RiEmotionLine } from '@remixicon/react'
import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess' import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess'
enum BlurRadius { enum BlurRadius {
@@ -141,9 +141,36 @@ export const EffectsConfiguration = ({
} }
const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => { const tooltipLabel = (type: ProcessorType, options: BackgroundOptions) => {
if (type === ProcessorType.FACE_LANDMARKS) {
const effect = options.showGlasses ? 'glasses' : 'mustache'
return t(`faceLandmarks.${effect}.${isSelected(type, options) ? 'clear' : 'apply'}`)
}
return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`) return t(`${type}.${isSelected(type, options) ? 'clear' : 'apply'}`)
} }
const getFaceLandmarksOptions = () => {
const processor = getProcessor()
if (processor?.serialize().type === ProcessorType.FACE_LANDMARKS) {
return processor.serialize().options as { showGlasses?: boolean; showMustache?: boolean }
}
return { showGlasses: false, showMustache: false }
}
const toggleFaceLandmarkEffect = async (effect: 'glasses' | 'mustache') => {
const currentOptions = getFaceLandmarksOptions()
const newOptions = {
...currentOptions,
[effect === 'glasses' ? 'showGlasses' : 'showMustache']: !currentOptions[effect === 'glasses' ? 'showGlasses' : 'showMustache']
}
if (!newOptions.showGlasses && !newOptions.showMustache) {
// If both effects are off stop the processor
await clearEffect()
} else {
await toggleEffect(ProcessorType.FACE_LANDMARKS, newOptions)
}
}
return ( return (
<div <div
className={css( className={css(
@@ -328,23 +355,36 @@ export const EffectsConfiguration = ({
<ToggleButton <ToggleButton
variant="bigSquare" variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, { aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
blurRadius: 0, showGlasses: true,
showMustache: false,
})} })}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, { tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
blurRadius: 0, showGlasses: true,
showMustache: false,
})} })}
isDisabled={processorPendingReveal} isDisabled={processorPendingReveal}
onChange={async () => onChange={async () => await toggleFaceLandmarkEffect('glasses')}
await toggleEffect(ProcessorType.FACE_LANDMARKS, { isSelected={getFaceLandmarksOptions().showGlasses}
blurRadius: 0, data-attr="toggle-glasses"
})
}
isSelected={isSelected(ProcessorType.FACE_LANDMARKS, {
blurRadius: 0,
})}
data-attr="toggle-face-landmarks"
> >
<RiUserVoiceLine /> <RiGlassesLine />
</ToggleButton>
<ToggleButton
variant="bigSquare"
aria-label={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showMustache: true,
})}
tooltip={tooltipLabel(ProcessorType.FACE_LANDMARKS, {
showGlasses: false,
showMustache: true,
})}
isDisabled={processorPendingReveal}
onChange={async () => await toggleFaceLandmarkEffect('mustache')}
isSelected={getFaceLandmarksOptions().showMustache}
data-attr="toggle-mustache"
>
<RiEmotionLine />
</ToggleButton> </ToggleButton>
</div> </div>
</div> </div>

View File

@@ -151,9 +151,15 @@
"clear": "Virtuellen Hintergrund deaktivieren" "clear": "Virtuellen Hintergrund deaktivieren"
}, },
"faceLandmarks": { "faceLandmarks": {
"title": "Gesichtsmerkmale", "title": "Visuelle Effekte",
"apply": "Gesichtsmerkmale aktivieren", "glasses": {
"clear": "Gesichtsmerkmale deaktivieren" "apply": "Brille hinzufügen",
"clear": "Brille entfernen"
},
"mustache": {
"apply": "Schnurrbart hinzufügen",
"clear": "Schnurrbart entfernen"
}
}, },
"experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität." "experimental": "Experimentelle Funktion. Eine v2 kommt für vollständige Browserunterstützung und verbesserte Qualität."
}, },

View File

@@ -150,9 +150,15 @@
"clear": "Disable virtual background" "clear": "Disable virtual background"
}, },
"faceLandmarks": { "faceLandmarks": {
"title": "Face landmarks", "title": "Visual Effects",
"apply": "Enable face landmarks", "glasses": {
"clear": "Disable face landmarks" "apply": "Add Glasses",
"clear": "Remove Glasses"
},
"mustache": {
"apply": "Add Mustache",
"clear": "Remove Mustache"
}
}, },
"experimental": "Experimental feature. A v2 is coming for full browser support and improved quality." "experimental": "Experimental feature. A v2 is coming for full browser support and improved quality."
}, },

View File

@@ -150,9 +150,15 @@
"clear": "Désactiver l'arrière-plan virtuel" "clear": "Désactiver l'arrière-plan virtuel"
}, },
"faceLandmarks": { "faceLandmarks": {
"title": "Points du visage", "title": "Effets visuels",
"apply": "Activer les points du visage", "glasses": {
"clear": "Désactiver les points du visage" "apply": "Ajouter des lunettes",
"clear": "Retirer les lunettes"
},
"mustache": {
"apply": "Ajouter une moustache",
"clear": "Retirer la moustache"
}
}, },
"experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité." "experimental": "Fonctionnalité expérimentale. Une v2 arrive pour un support complet des navigateurs et une meilleure qualité."
}, },

View File

@@ -150,12 +150,14 @@
"clear": "Virtuele achtergrond uitschakelen" "clear": "Virtuele achtergrond uitschakelen"
}, },
"faceLandmarks": { "faceLandmarks": {
"title": "Gezichtskenmerken", "title": "Visuele effecten",
"apply": "Gezichtskenmerken inschakelen", "glasses": {
"clear": "Gezichtskenmerken uitschakelen", "apply": "Bril toevoegen",
"tooltip": { "clear": "Bril verwijderen"
"apply": "Gezichtskenmerken inschakelen", },
"clear": "Gezichtskenmerken uitschakelen" "mustache": {
"apply": "Snor toevoegen",
"clear": "Snor verwijderen"
} }
}, },
"experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit." "experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."