From 1d5aebcfdc312c52c9aa2c330ce37390b2671e66 Mon Sep 17 00:00:00 2001 From: Arnaud Robin Date: Mon, 31 Mar 2025 00:23:57 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(frontend)=20add=20mustache=20effects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added functionality to display mustache effects on detected faces. Enhanced the EffectsConfiguration component to toggle these effects and updated localization files for this effects. --- src/frontend/public/assets/mustache.png | Bin 0 -> 27860 bytes .../components/blur/FaceLandmarksProcessor.ts | 115 +++++++++++------- .../rooms/livekit/components/blur/index.ts | 3 +- .../effects/EffectsConfiguration.tsx | 68 ++++++++--- src/frontend/src/locales/de/rooms.json | 12 +- src/frontend/src/locales/en/rooms.json | 12 +- src/frontend/src/locales/fr/rooms.json | 12 +- src/frontend/src/locales/nl/rooms.json | 14 ++- 8 files changed, 160 insertions(+), 76 deletions(-) create mode 100644 src/frontend/public/assets/mustache.png diff --git a/src/frontend/public/assets/mustache.png b/src/frontend/public/assets/mustache.png new file mode 100644 index 0000000000000000000000000000000000000000..09956bd4046428c32474ed68ceeb2fd47e8ad65c GIT binary patch literal 27860 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 Y?z6GDQdGP<`p*hbS55vdxr}-EKe7*2RsaA1 literal 0 HcmV?d00001 diff --git a/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts b/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts index 6661c487..462ce624 100644 --- a/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts +++ b/src/frontend/src/features/rooms/livekit/components/blur/FaceLandmarksProcessor.ts @@ -46,20 +46,25 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface { type: ProcessorType - // Glasses image element + // Effect images glassesImage?: HTMLImageElement + mustacheImage?: HTMLImageElement constructor(opts: BackgroundOptions) { this.name = 'face_landmarks' this.options = opts this.type = ProcessorType.FACE_LANDMARKS - this._initGlassesImage() + this._initEffectImages() } - private _initGlassesImage() { + private _initEffectImages() { 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.mustacheImage = new Image() + this.mustacheImage.src = '/assets/mustache.png' + this.mustacheImage.crossOrigin = 'anonymous' } 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() { // Draw the original video frame at the canvas size this.outputCanvasCtx!.drawImage( @@ -185,49 +237,20 @@ export class FaceLandmarksProcessor implements BackgroundProcessorInterface { this.outputCanvasCtx!.lineWidth = 2 for (const face of this.faceLandmarkerResult.faceLandmarks) { - // Find eye landmarks (indices 33 and 263 are the left and right eye centers) - const leftEye = face[33] - const rightEye = face[263] + // Find eye landmarks (indices 33 and 263 are the left and right eye corners) + const leftEye = face[468] + const rightEye = face[473] - if (leftEye && rightEye) { - // Calculate glasses position and size - const eyeDistance = Math.sqrt( - Math.pow(rightEye.x - leftEye.x, 2) + - Math.pow(rightEye.y - leftEye.y, 2) - ) - - // Scale glasses based on eye distance - const glassesWidth = eyeDistance * PROCESSING_WIDTH * 2.5 // Adjust multiplier as needed - const glassesHeight = glassesWidth * 0.7 // Adjust aspect ratio as needed - - // Calculate center position between eyes - const centerX = (leftEye.x + rightEye.x) / 2 - 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() + // Find mouth landmarks for mustache (indices 0 and 17 are the left and right corners of the mouth) + const leftMoustache = face[92] + const rightMoustache = face[322] + + if (leftEye && rightEye && this.options.showGlasses) { + this.drawEffect(leftEye, rightEye, this.glassesImage!, 2.5, 0.7) + } + + if (leftMoustache && rightMoustache && this.options.showMustache) { + this.drawEffect(leftMoustache, rightMoustache, this.mustacheImage!, 1.5, 0.5) } } } diff --git a/src/frontend/src/features/rooms/livekit/components/blur/index.ts b/src/frontend/src/features/rooms/livekit/components/blur/index.ts index 61220587..e7f27414 100644 --- a/src/frontend/src/features/rooms/livekit/components/blur/index.ts +++ b/src/frontend/src/features/rooms/livekit/components/blur/index.ts @@ -8,7 +8,8 @@ import { FaceLandmarksProcessor } from './FaceLandmarksProcessor' export type BackgroundOptions = { blurRadius?: number imagePath?: string - showFaceLandmarks?: boolean + showGlasses?: boolean + showMustache?: boolean } export interface ProcessorSerialized { diff --git a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx index 306f16c0..10e19722 100644 --- a/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx +++ b/src/frontend/src/features/rooms/livekit/components/effects/EffectsConfiguration.tsx @@ -5,17 +5,17 @@ import { BackgroundProcessorFactory, BackgroundProcessorInterface, ProcessorType, + BackgroundOptions } from '../blur' import { css } from '@/styled-system/css' import { Text, P, ToggleButton, H } from '@/primitives' import { styled } from '@/styled-system/jsx' -import { BackgroundOptions } from '@livekit/track-processors' import { BlurOn } from '@/components/icons/BlurOn' import { BlurOnStrong } from '@/components/icons/BlurOnStrong' import { useTrackToggle } from '@livekit/components-react' import { Loader } from '@/primitives/Loader' import { useSyncAfterDelay } from '@/hooks/useSyncAfterDelay' -import { RiProhibited2Line, RiUserVoiceLine } from '@remixicon/react' +import { RiProhibited2Line, RiGlassesLine, RiEmotionLine } from '@remixicon/react' import { useHasFaceLandmarksAccess } from '../../hooks/useHasFaceLandmarksAccess' enum BlurRadius { @@ -141,9 +141,36 @@ export const EffectsConfiguration = ({ } 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'}`) } + 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 (
- await toggleEffect(ProcessorType.FACE_LANDMARKS, { - blurRadius: 0, - }) - } - isSelected={isSelected(ProcessorType.FACE_LANDMARKS, { - blurRadius: 0, - })} - data-attr="toggle-face-landmarks" + onChange={async () => await toggleFaceLandmarkEffect('glasses')} + isSelected={getFaceLandmarksOptions().showGlasses} + data-attr="toggle-glasses" > - + + + await toggleFaceLandmarkEffect('mustache')} + isSelected={getFaceLandmarksOptions().showMustache} + data-attr="toggle-mustache" + > +
diff --git a/src/frontend/src/locales/de/rooms.json b/src/frontend/src/locales/de/rooms.json index afd2198c..2a672fd7 100644 --- a/src/frontend/src/locales/de/rooms.json +++ b/src/frontend/src/locales/de/rooms.json @@ -151,9 +151,15 @@ "clear": "Virtuellen Hintergrund deaktivieren" }, "faceLandmarks": { - "title": "Gesichtsmerkmale", - "apply": "Gesichtsmerkmale aktivieren", - "clear": "Gesichtsmerkmale deaktivieren" + "title": "Visuelle Effekte", + "glasses": { + "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." }, diff --git a/src/frontend/src/locales/en/rooms.json b/src/frontend/src/locales/en/rooms.json index b94e9c70..23fc147d 100644 --- a/src/frontend/src/locales/en/rooms.json +++ b/src/frontend/src/locales/en/rooms.json @@ -150,9 +150,15 @@ "clear": "Disable virtual background" }, "faceLandmarks": { - "title": "Face landmarks", - "apply": "Enable face landmarks", - "clear": "Disable face landmarks" + "title": "Visual Effects", + "glasses": { + "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." }, diff --git a/src/frontend/src/locales/fr/rooms.json b/src/frontend/src/locales/fr/rooms.json index 553de0c3..0508179c 100644 --- a/src/frontend/src/locales/fr/rooms.json +++ b/src/frontend/src/locales/fr/rooms.json @@ -150,9 +150,15 @@ "clear": "Désactiver l'arrière-plan virtuel" }, "faceLandmarks": { - "title": "Points du visage", - "apply": "Activer les points du visage", - "clear": "Désactiver les points du visage" + "title": "Effets visuels", + "glasses": { + "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é." }, diff --git a/src/frontend/src/locales/nl/rooms.json b/src/frontend/src/locales/nl/rooms.json index 7494dde7..da56ae02 100644 --- a/src/frontend/src/locales/nl/rooms.json +++ b/src/frontend/src/locales/nl/rooms.json @@ -150,12 +150,14 @@ "clear": "Virtuele achtergrond uitschakelen" }, "faceLandmarks": { - "title": "Gezichtskenmerken", - "apply": "Gezichtskenmerken inschakelen", - "clear": "Gezichtskenmerken uitschakelen", - "tooltip": { - "apply": "Gezichtskenmerken inschakelen", - "clear": "Gezichtskenmerken uitschakelen" + "title": "Visuele effecten", + "glasses": { + "apply": "Bril toevoegen", + "clear": "Bril verwijderen" + }, + "mustache": { + "apply": "Snor toevoegen", + "clear": "Snor verwijderen" } }, "experimental": "Experimentele functie. Een v2 komt eraan voor volledige browserondersteuning en verbeterde kwaliteit."