Mattysaur commited on
Commit
425d1ae
·
verified ·
1 Parent(s): d0eb08d

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +683 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Faceswap App
3
- emoji: 🌖
4
- colorFrom: indigo
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: faceswap-app
3
+ emoji: 🐳
4
+ colorFrom: pink
5
+ colorTo: purple
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,683 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Mobile Face Swap</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/face-api.min.js"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ #canvas {
12
+ max-width: 100%;
13
+ height: auto;
14
+ display: block;
15
+ margin: 0 auto;
16
+ border-radius: 12px;
17
+ }
18
+
19
+ #preview {
20
+ max-width: 100%;
21
+ height: auto;
22
+ display: block;
23
+ margin: 0 auto;
24
+ border-radius: 12px;
25
+ }
26
+
27
+ .camera-container {
28
+ position: relative;
29
+ margin: 0 auto;
30
+ width: 100%;
31
+ max-width: 400px;
32
+ }
33
+
34
+ .face-landmark {
35
+ position: absolute;
36
+ width: 8px;
37
+ height: 8px;
38
+ background-color: red;
39
+ border-radius: 50%;
40
+ transform: translate(-4px, -4px);
41
+ }
42
+
43
+ .loading-spinner {
44
+ animation: spin 1s linear infinite;
45
+ }
46
+
47
+ @keyframes spin {
48
+ 0% { transform: rotate(0deg); }
49
+ 100% { transform: rotate(360deg); }
50
+ }
51
+
52
+ .tab-content {
53
+ display: none;
54
+ }
55
+
56
+ .tab-content.active {
57
+ display: block;
58
+ }
59
+
60
+ .tab-button.active {
61
+ border-bottom: 3px solid #3b82f6;
62
+ color: #3b82f6;
63
+ }
64
+
65
+ #swapResult {
66
+ transition: all 0.3s ease;
67
+ }
68
+
69
+ .face-box {
70
+ position: absolute;
71
+ border: 2px solid #3b82f6;
72
+ background-color: rgba(59, 130, 246, 0.2);
73
+ }
74
+ </style>
75
+ </head>
76
+ <body class="bg-gray-100 min-h-screen">
77
+ <div class="container mx-auto px-4 py-8 max-w-md">
78
+ <header class="text-center mb-8">
79
+ <h1 class="text-3xl font-bold text-gray-800">
80
+ <i class="fas fa-user-astronaut text-blue-500"></i> Face Swap
81
+ </h1>
82
+ <p class="text-gray-600 mt-2">Swap faces between photos in real-time</p>
83
+ </header>
84
+
85
+ <div class="bg-white rounded-xl shadow-lg p-6 mb-6">
86
+ <div class="flex border-b border-gray-200">
87
+ <button class="tab-button active px-4 py-2 font-medium text-gray-700" data-tab="camera">
88
+ <i class="fas fa-camera mr-2"></i>Camera
89
+ </button>
90
+ <button class="tab-button px-4 py-2 font-medium text-gray-700" data-tab="upload">
91
+ <i class="fas fa-upload mr-2"></i>Upload
92
+ </button>
93
+ <button class="tab-button px-4 py-2 font-medium text-gray-700" data-tab="gallery">
94
+ <i class="fas fa-images mr-2"></i>Gallery
95
+ </button>
96
+ </div>
97
+
98
+ <!-- Camera Tab -->
99
+ <div id="camera" class="tab-content active mt-4">
100
+ <div class="camera-container">
101
+ <video id="video" autoplay muted playsinline class="w-full rounded-lg hidden"></video>
102
+ <canvas id="canvas" class="hidden"></canvas>
103
+ <div id="preview-container" class="hidden">
104
+ <img id="preview" src="" alt="Preview">
105
+ <div id="face-boxes"></div>
106
+ </div>
107
+
108
+ <div id="camera-controls" class="flex justify-center mt-4 space-x-4">
109
+ <button id="startCamera" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-full">
110
+ <i class="fas fa-play mr-2"></i>Start Camera
111
+ </button>
112
+ <button id="capture" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-full hidden">
113
+ <i class="fas fa-camera mr-2"></i>Capture
114
+ </button>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Upload Tab -->
120
+ <div id="upload" class="tab-content mt-4">
121
+ <div class="text-center">
122
+ <label class="block mb-4">
123
+ <span class="text-gray-700">First Face Image</span>
124
+ <input type="file" id="image1" accept="image/*" class="mt-1 block w-full text-sm text-gray-500
125
+ file:mr-4 file:py-2 file:px-4
126
+ file:rounded-full file:border-0
127
+ file:text-sm file:font-semibold
128
+ file:bg-blue-50 file:text-blue-700
129
+ hover:file:bg-blue-100">
130
+ </label>
131
+
132
+ <label class="block mb-4">
133
+ <span class="text-gray-700">Second Face Image</span>
134
+ <input type="file" id="image2" accept="image/*" class="mt-1 block w-full text-sm text-gray-500
135
+ file:mr-4 file:py-2 file:px-4
136
+ file:rounded-full file:border-0
137
+ file:text-sm file:font-semibold
138
+ file:bg-blue-50 file:text-blue-700
139
+ hover:file:bg-blue-100">
140
+ </label>
141
+
142
+ <button id="processUpload" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-full">
143
+ <i class="fas fa-exchange-alt mr-2"></i>Swap Faces
144
+ </button>
145
+ </div>
146
+ </div>
147
+
148
+ <!-- Gallery Tab -->
149
+ <div id="gallery" class="tab-content mt-4">
150
+ <div class="text-center">
151
+ <p class="text-gray-600 mb-4">Select from sample images</p>
152
+
153
+ <div class="grid grid-cols-2 gap-4 mb-6">
154
+ <div class="cursor-pointer" onclick="selectSampleImage('sample1a', 'sample1b')">
155
+ <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=200&h=200&fit=crop" class="w-full rounded-lg">
156
+ <img src="https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=200&h=200&fit=crop" class="w-full rounded-lg mt-2">
157
+ <p class="text-sm mt-1">Sample 1</p>
158
+ </div>
159
+ <div class="cursor-pointer" onclick="selectSampleImage('sample2a', 'sample2b')">
160
+ <img src="https://images.unsplash.com/photo-1554151228-14d9def656e4?w=200&h=200&fit=crop" class="w-full rounded-lg">
161
+ <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=200&h=200&fit=crop" class="w-full rounded-lg mt-2">
162
+ <p class="text-sm mt-1">Sample 2</p>
163
+ </div>
164
+ </div>
165
+
166
+ <button id="processGallery" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-full">
167
+ <i class="fas fa-exchange-alt mr-2"></i>Swap Faces
168
+ </button>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <!-- Result Section -->
174
+ <div id="result-section" class="bg-white rounded-xl shadow-lg p-6 hidden">
175
+ <h2 class="text-xl font-bold text-gray-800 mb-4">
176
+ <i class="fas fa-magic mr-2 text-blue-500"></i>Result
177
+ </h2>
178
+
179
+ <div class="flex justify-center mb-4">
180
+ <img id="swapResult" src="" alt="Face Swap Result" class="max-w-full rounded-lg">
181
+ </div>
182
+
183
+ <div class="flex justify-center space-x-4">
184
+ <button id="saveResult" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-full">
185
+ <i class="fas fa-save mr-2"></i>Save
186
+ </button>
187
+ <button id="newSwap" class="bg-blue-500 hover:bg-blue-600 text-white px-6 py-2 rounded-full">
188
+ <i class="fas fa-redo mr-2"></i>New Swap
189
+ </button>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Loading Indicator -->
194
+ <div id="loading" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-50">
195
+ <div class="bg-white p-6 rounded-lg shadow-xl text-center">
196
+ <div class="loading-spinner inline-block text-blue-500 text-4xl mb-4">
197
+ <i class="fas fa-spinner"></i>
198
+ </div>
199
+ <p class="text-lg font-medium">Processing faces...</p>
200
+ <p class="text-sm text-gray-600 mt-2" id="loadingStatus">Detecting facial landmarks</p>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <script>
206
+ // Global variables
207
+ let videoStream;
208
+ let faceImages = {
209
+ image1: null,
210
+ image2: null
211
+ };
212
+ let modelsLoaded = false;
213
+
214
+ // DOM elements
215
+ const video = document.getElementById('video');
216
+ const canvas = document.getElementById('canvas');
217
+ const preview = document.getElementById('preview');
218
+ const previewContainer = document.getElementById('preview-container');
219
+ const faceBoxes = document.getElementById('face-boxes');
220
+ const swapResult = document.getElementById('swapResult');
221
+ const resultSection = document.getElementById('result-section');
222
+ const loading = document.getElementById('loading');
223
+ const loadingStatus = document.getElementById('loadingStatus');
224
+
225
+ // Tab switching
226
+ document.querySelectorAll('.tab-button').forEach(button => {
227
+ button.addEventListener('click', () => {
228
+ document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
229
+ document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
230
+
231
+ button.classList.add('active');
232
+ document.getElementById(button.dataset.tab).classList.add('active');
233
+
234
+ // Stop camera when switching tabs
235
+ if (button.dataset.tab !== 'camera' && videoStream) {
236
+ stopCamera();
237
+ }
238
+ });
239
+ });
240
+
241
+ // Initialize camera
242
+ document.getElementById('startCamera').addEventListener('click', async () => {
243
+ try {
244
+ const stream = await navigator.mediaDevices.getUserMedia({
245
+ video: {
246
+ width: { ideal: 640 },
247
+ height: { ideal: 480 },
248
+ facingMode: 'user'
249
+ },
250
+ audio: false
251
+ });
252
+
253
+ video.srcObject = stream;
254
+ videoStream = stream;
255
+ video.classList.remove('hidden');
256
+ document.getElementById('capture').classList.remove('hidden');
257
+ document.getElementById('startCamera').classList.add('hidden');
258
+
259
+ // Load models if not already loaded
260
+ if (!modelsLoaded) {
261
+ await loadModels();
262
+ }
263
+ } catch (err) {
264
+ alert('Could not access the camera. Please make sure you have granted camera permissions.');
265
+ console.error(err);
266
+ }
267
+ });
268
+
269
+ // Capture photo from camera
270
+ document.getElementById('capture').addEventListener('click', () => {
271
+ const context = canvas.getContext('2d');
272
+ canvas.width = video.videoWidth;
273
+ canvas.height = video.videoHeight;
274
+ context.drawImage(video, 0, 0, canvas.width, canvas.height);
275
+
276
+ // Show preview
277
+ preview.src = canvas.toDataURL('image/png');
278
+ previewContainer.classList.remove('hidden');
279
+ video.classList.add('hidden');
280
+
281
+ // Process the captured image
282
+ processImage(canvas).then(faces => {
283
+ if (faces.length > 0) {
284
+ faceImages.image1 = canvas;
285
+ showFaceBoxes(faces);
286
+ alert('Face captured! Now switch to upload tab to select the second face.');
287
+ } else {
288
+ alert('No faces detected. Please try again.');
289
+ previewContainer.classList.add('hidden');
290
+ video.classList.remove('hidden');
291
+ }
292
+ });
293
+ });
294
+
295
+ // Process uploaded images
296
+ document.getElementById('processUpload').addEventListener('click', async () => {
297
+ const image1Input = document.getElementById('image1');
298
+ const image2Input = document.getElementById('image2');
299
+
300
+ if (!image1Input.files[0] || !image2Input.files[0]) {
301
+ alert('Please select both images first');
302
+ return;
303
+ }
304
+
305
+ showLoading('Loading and processing images...');
306
+
307
+ try {
308
+ // Load models if not already loaded
309
+ if (!modelsLoaded) {
310
+ await loadModels();
311
+ }
312
+
313
+ // Process first image
314
+ const img1 = await loadImage(image1Input.files[0]);
315
+ const faces1 = await faceapi.detectAllFaces(img1).withFaceLandmarks().run();
316
+
317
+ if (faces1.length === 0) {
318
+ hideLoading();
319
+ alert('No face detected in the first image');
320
+ return;
321
+ }
322
+
323
+ // Process second image
324
+ const img2 = await loadImage(image2Input.files[0]);
325
+ const faces2 = await faceapi.detectAllFaces(img2).withFaceLandmarks().run();
326
+
327
+ if (faces2.length === 0) {
328
+ hideLoading();
329
+ alert('No face detected in the second image');
330
+ return;
331
+ }
332
+
333
+ // Perform face swap
334
+ loadingStatus.textContent = 'Swapping faces...';
335
+ const result = await swapFaces(img1, faces1[0], img2, faces2[0]);
336
+
337
+ // Show result
338
+ swapResult.src = result.toDataURL('image/png');
339
+ resultSection.classList.remove('hidden');
340
+ hideLoading();
341
+
342
+ // Scroll to result
343
+ resultSection.scrollIntoView({ behavior: 'smooth' });
344
+ } catch (err) {
345
+ hideLoading();
346
+ alert('An error occurred while processing the images');
347
+ console.error(err);
348
+ }
349
+ });
350
+
351
+ // Process gallery images
352
+ document.getElementById('processGallery').addEventListener('click', async () => {
353
+ if (!window.selectedSample1 || !window.selectedSample2) {
354
+ alert('Please select a sample pair first');
355
+ return;
356
+ }
357
+
358
+ showLoading('Processing sample images...');
359
+
360
+ try {
361
+ // Load models if not already loaded
362
+ if (!modelsLoaded) {
363
+ await loadModels();
364
+ }
365
+
366
+ // Process first image
367
+ const img1 = await loadImageFromUrl(window.selectedSample1);
368
+ const faces1 = await faceapi.detectAllFaces(img1).withFaceLandmarks().run();
369
+
370
+ if (faces1.length === 0) {
371
+ hideLoading();
372
+ alert('No face detected in the first image');
373
+ return;
374
+ }
375
+
376
+ // Process second image
377
+ const img2 = await loadImageFromUrl(window.selectedSample2);
378
+ const faces2 = await faceapi.detectAllFaces(img2).withFaceLandmarks().run();
379
+
380
+ if (faces2.length === 0) {
381
+ hideLoading();
382
+ alert('No face detected in the second image');
383
+ return;
384
+ }
385
+
386
+ // Perform face swap
387
+ loadingStatus.textContent = 'Swapping faces...';
388
+ const result = await swapFaces(img1, faces1[0], img2, faces2[0]);
389
+
390
+ // Show result
391
+ swapResult.src = result.toDataURL('image/png');
392
+ resultSection.classList.remove('hidden');
393
+ hideLoading();
394
+
395
+ // Scroll to result
396
+ resultSection.scrollIntoView({ behavior: 'smooth' });
397
+ } catch (err) {
398
+ hideLoading();
399
+ alert('An error occurred while processing the images');
400
+ console.error(err);
401
+ }
402
+ });
403
+
404
+ // Save result
405
+ document.getElementById('saveResult').addEventListener('click', () => {
406
+ if (!swapResult.src) return;
407
+
408
+ const link = document.createElement('a');
409
+ link.download = 'face-swap-result.png';
410
+ link.href = swapResult.src;
411
+ document.body.appendChild(link);
412
+ link.click();
413
+ document.body.removeChild(link);
414
+ });
415
+
416
+ // New swap
417
+ document.getElementById('newSwap').addEventListener('click', () => {
418
+ resultSection.classList.add('hidden');
419
+
420
+ // Reset camera tab
421
+ if (videoStream) {
422
+ stopCamera();
423
+ video.classList.remove('hidden');
424
+ previewContainer.classList.add('hidden');
425
+ document.getElementById('startCamera').classList.remove('hidden');
426
+ document.getElementById('capture').classList.add('hidden');
427
+ }
428
+
429
+ // Reset upload tab
430
+ document.getElementById('image1').value = '';
431
+ document.getElementById('image2').value = '';
432
+
433
+ // Reset gallery tab
434
+ window.selectedSample1 = null;
435
+ window.selectedSample2 = null;
436
+ });
437
+
438
+ // Helper functions
439
+ async function loadModels() {
440
+ showLoading('Loading face detection models...');
441
+
442
+ await faceapi.nets.tinyFaceDetector.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models');
443
+ await faceapi.nets.faceLandmark68Net.loadFromUri('https://justadudewhohacks.github.io/face-api.js/models');
444
+
445
+ modelsLoaded = true;
446
+ hideLoading();
447
+ }
448
+
449
+ function loadImage(file) {
450
+ return new Promise((resolve, reject) => {
451
+ const img = new Image();
452
+ img.onload = () => resolve(img);
453
+ img.onerror = reject;
454
+ img.src = URL.createObjectURL(file);
455
+ });
456
+ }
457
+
458
+ function loadImageFromUrl(url) {
459
+ return new Promise((resolve, reject) => {
460
+ const img = new Image();
461
+ img.crossOrigin = 'Anonymous';
462
+ img.onload = () => resolve(img);
463
+ img.onerror = reject;
464
+ img.src = url;
465
+ });
466
+ }
467
+
468
+ async function processImage(imageElement) {
469
+ showLoading('Detecting faces...');
470
+
471
+ try {
472
+ const detections = await faceapi.detectAllFaces(imageElement)
473
+ .withFaceLandmarks()
474
+ .run();
475
+
476
+ hideLoading();
477
+ return detections;
478
+ } catch (err) {
479
+ hideLoading();
480
+ console.error(err);
481
+ return [];
482
+ }
483
+ }
484
+
485
+ function showFaceBoxes(faces) {
486
+ faceBoxes.innerHTML = '';
487
+
488
+ faces.forEach(face => {
489
+ const box = face.detection.box;
490
+ const faceBox = document.createElement('div');
491
+ faceBox.className = 'face-box';
492
+ faceBox.style.width = `${box.width}px`;
493
+ faceBox.style.height = `${box.height}px`;
494
+ faceBox.style.left = `${box.x}px`;
495
+ faceBox.style.top = `${box.y}px`;
496
+ faceBoxes.appendChild(faceBox);
497
+ });
498
+ }
499
+
500
+ function stopCamera() {
501
+ if (videoStream) {
502
+ videoStream.getTracks().forEach(track => track.stop());
503
+ videoStream = null;
504
+ video.srcObject = null;
505
+ video.classList.add('hidden');
506
+ document.getElementById('capture').classList.add('hidden');
507
+ document.getElementById('startCamera').classList.remove('hidden');
508
+ previewContainer.classList.add('hidden');
509
+ }
510
+ }
511
+
512
+ function showLoading(message) {
513
+ loadingStatus.textContent = message;
514
+ loading.classList.remove('hidden');
515
+ }
516
+
517
+ function hideLoading() {
518
+ loading.classList.add('hidden');
519
+ }
520
+
521
+ // Sample image selection
522
+ window.selectSampleImage = function(img1, img2) {
523
+ window.selectedSample1 = `https://images.unsplash.com/photo-${getImageId(img1)}?w=800&h=800&fit=crop`;
524
+ window.selectedSample2 = `https://images.unsplash.com/photo-${getImageId(img2)}?w=800&h=800&fit=crop`;
525
+
526
+ // Highlight selected sample
527
+ document.querySelectorAll('#gallery .cursor-pointer').forEach(el => {
528
+ el.classList.remove('ring-2', 'ring-blue-500');
529
+ });
530
+ event.currentTarget.classList.add('ring-2', 'ring-blue-500');
531
+ };
532
+
533
+ function getImageId(img) {
534
+ const samples = {
535
+ 'sample1a': '1507003211169-0a1dd7228f2d',
536
+ 'sample1b': '1531123897727-8f129e1688ce',
537
+ 'sample2a': '1554151228-14d9def656e4',
538
+ 'sample2b': '1494790108377-be9c29b29330'
539
+ };
540
+ return samples[img];
541
+ }
542
+
543
+ // Face swap algorithm
544
+ async function swapFaces(img1, face1, img2, face2) {
545
+ // Create canvas for result
546
+ const resultCanvas = document.createElement('canvas');
547
+ const ctx = resultCanvas.getContext('2d');
548
+
549
+ // Set canvas dimensions to match first image
550
+ resultCanvas.width = img1.width;
551
+ resultCanvas.height = img1.height;
552
+
553
+ // Draw first image as background
554
+ ctx.drawImage(img1, 0, 0);
555
+
556
+ // Get facial landmarks
557
+ const landmarks1 = face1.landmarks;
558
+ const landmarks2 = face2.landmarks;
559
+
560
+ // Calculate transformation matrix from face2 to face1
561
+ const matrix = getTransformationMatrix(landmarks2, landmarks1);
562
+
563
+ // Create temporary canvas for warped face
564
+ const tempCanvas = document.createElement('canvas');
565
+ tempCanvas.width = img2.width;
566
+ tempCanvas.height = img2.height;
567
+ const tempCtx = tempCanvas.getContext('2d');
568
+ tempCtx.drawImage(img2, 0, 0);
569
+
570
+ // Warp the second face to match the first face's shape
571
+ warpFace(tempCanvas, matrix, face2.detection.box, face1.detection.box);
572
+
573
+ // Draw the warped face onto the result
574
+ ctx.drawImage(tempCanvas, 0, 0, tempCanvas.width, tempCanvas.height,
575
+ 0, 0, resultCanvas.width, resultCanvas.height);
576
+
577
+ // Blend the faces for more natural result
578
+ blendFaces(resultCanvas, face1, tempCanvas, face2);
579
+
580
+ return resultCanvas;
581
+ }
582
+
583
+ function getTransformationMatrix(sourceLandmarks, targetLandmarks) {
584
+ // This is a simplified version - a real implementation would use
585
+ // more sophisticated algorithms like affine transformation or thin plate splines
586
+
587
+ // For demo purposes, we'll just calculate an average scaling factor
588
+ const sourcePoints = sourceLandmarks.positions;
589
+ const targetPoints = targetLandmarks.positions;
590
+
591
+ let totalDistanceSource = 0;
592
+ let totalDistanceTarget = 0;
593
+
594
+ // Calculate average distances between some key points
595
+ const pairs = [
596
+ [36, 39], // left eye
597
+ [42, 45], // right eye
598
+ [48, 54], // mouth width
599
+ [27, 8] // nose to chin
600
+ ];
601
+
602
+ pairs.forEach(pair => {
603
+ const [i, j] = pair;
604
+ totalDistanceSource += distance(sourcePoints[i], sourcePoints[j]);
605
+ totalDistanceTarget += distance(targetPoints[i], targetPoints[j]);
606
+ });
607
+
608
+ const scale = totalDistanceTarget / totalDistanceSource;
609
+
610
+ // Calculate average offset
611
+ const sourceCenter = getCenter(sourceLandmarks);
612
+ const targetCenter = getCenter(targetLandmarks);
613
+
614
+ return {
615
+ scale,
616
+ translateX: targetCenter.x - sourceCenter.x * scale,
617
+ translateY: targetCenter.y - sourceCenter.y * scale
618
+ };
619
+ }
620
+
621
+ function warpFace(canvas, matrix, sourceBox, targetBox) {
622
+ const ctx = canvas.getContext('2d');
623
+
624
+ // Create a temporary canvas to hold the transformed image
625
+ const tempCanvas = document.createElement('canvas');
626
+ tempCanvas.width = canvas.width;
627
+ tempCanvas.height = canvas.height;
628
+ const tempCtx = tempCanvas.getContext('2d');
629
+
630
+ // Apply transformation
631
+ tempCtx.save();
632
+ tempCtx.translate(matrix.translateX, matrix.translateY);
633
+ tempCtx.scale(matrix.scale, matrix.scale);
634
+ tempCtx.drawImage(canvas, 0, 0);
635
+ tempCtx.restore();
636
+
637
+ // Copy back to original canvas
638
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
639
+ ctx.drawImage(tempCanvas, 0, 0);
640
+ }
641
+
642
+ function blendFaces(resultCanvas, face1, face2Canvas, face2) {
643
+ // This is a simplified blending function
644
+ // A real implementation would use more sophisticated techniques like:
645
+ // - Poisson blending
646
+ // - Feathering at the edges
647
+ // - Color correction
648
+
649
+ const ctx = resultCanvas.getContext('2d');
650
+
651
+ // For demo, we'll just draw the second face over the first with some opacity
652
+ ctx.globalCompositeOperation = 'source-atop';
653
+ ctx.globalAlpha = 0.7;
654
+ ctx.drawImage(face2Canvas, 0, 0);
655
+ ctx.globalAlpha = 1.0;
656
+ ctx.globalCompositeOperation = 'source-over';
657
+ }
658
+
659
+ function distance(p1, p2) {
660
+ return Math.sqrt(Math.pow(p2.x - p1.x, 2) + Math.pow(p2.y - p1.y, 2));
661
+ }
662
+
663
+ function getCenter(landmarks) {
664
+ const points = landmarks.positions;
665
+ let x = 0, y = 0;
666
+
667
+ points.forEach(point => {
668
+ x += point.x;
669
+ y += point.y;
670
+ });
671
+
672
+ return {
673
+ x: x / points.length,
674
+ y: y / points.length
675
+ };
676
+ }
677
+
678
+ // Initialize with some sample images
679
+ window.selectedSample1 = 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=800&h=800&fit=crop';
680
+ window.selectedSample2 = 'https://images.unsplash.com/photo-1531123897727-8f129e1688ce?w=800&h=800&fit=crop';
681
+ </script>
682
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Mattysaur/faceswap-app" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
683
+ </html>