├── .gitignore ├── README.md ├── images ├── exampleB.jpg ├── exampleH.jpg └── exampleV.jpg ├── install.py └── scripts ├── batch_face_swap.py ├── bfs_utils.py ├── face_detect.py ├── haarcascade_frontalface_default.xml └── sd_helpers.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.pyc 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # !!! This project is no longer being maintained !!! 2 | ## I recommend trying these extensions instead https://github.com/Bing-su/adetailer and/or https://github.com/Gourieff/sd-webui-reactor 3 | 4 | ## Batch Face Swap extension for https://github.com/AUTOMATIC1111/stable-diffusion-webui 5 | Automaticaly detects faces and replaces them. 6 | 7 | ![preview](https://user-images.githubusercontent.com/46696708/236370022-1e243f62-a59c-437a-a841-d9a3d37778aa.png) 8 | 9 | ## Installation 10 | ### Automatic: 11 | 1. In the WebUI go to `Extensions`. 12 | 2. Open `Available` tab and click `Load from:` button. 13 | 3. Find `Batch Face Swap` and click `Install`. 14 | 4. Apply and restart UI 15 | ### Manual: 16 | 1. Use `git clone https://github.com/kex0/batch-face-swap.git` from your SD web UI `/extensions` folder. 17 | 2. Open `requirements_versions.txt` in the main SD web UI folder and add `mediapipe`. 18 | 3. Start or reload SD web UI. 19 | 20 | ## txt2img Guide 21 | 1. Expand the `Batch Face Swap` tab in the lower left corner. 22 |
23 | Image 24 | 25 |
26 | 2. Click the checkbox to enable it. 27 |
28 | Image 29 | 30 |
31 | 3. Click `Generate` 32 | 33 | ## img2img Guide 34 | 1. Expand the `Batch Face Swap` tab in the lower left corner. 35 |
36 | Image 37 | 38 |
39 | 2. Click the checkbox to enable it. 40 |
41 | Image 42 | 43 |
44 | 3. You can process either 1 image at a time by uploading your image at the top of the page. 45 |
46 | Image 47 | 48 |
49 | Or you can give it path to a folder containing your images. 50 |
51 | Image 52 | 53 |
54 | 4. Click `Generate` 55 | 56 | Override options only affect face generation so for example in `txt2img` you can generate the initial image with one prompt and face swap with another. Or generate the initial image with one model and faceswap with another. 57 |
58 | Example 59 | 60 | Left 'young woman in red dress' using `chilloutMix` 61 | Right 'Emma Watson in red dress' using `realisticVision` 62 | 63 |
64 | 65 | ![chrome_XSjamNtABV](https://user-images.githubusercontent.com/46696708/236360114-bd902f03-73cb-4836-a647-27f5371e3197.png) 66 | 67 | ## Example 68 | ![example](https://user-images.githubusercontent.com/46696708/211818536-7d3bd06e-f6b1-40e9-854e-9cb44be3b2f8.png) 69 | 70 | Prompt: 71 | ```ShellSession 72 | detailed closeup photo of Emma Watson, 35mm, dslr 73 | Negative prompt: (painting:1.3), (concept art:1.2), artstation, sketch, illustration, drawing, blender, octane, 3d, render, blur, smooth, low-res, grain, cartoon, watermark, text, out of focus 74 | ``` 75 | -------------------------------------------------------------------------------- /images/exampleB.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kex0/batch-face-swap/942ebae6937d9ad7964f285a790e7721f712b9ec/images/exampleB.jpg -------------------------------------------------------------------------------- /images/exampleH.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kex0/batch-face-swap/942ebae6937d9ad7964f285a790e7721f712b9ec/images/exampleH.jpg -------------------------------------------------------------------------------- /images/exampleV.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kex0/batch-face-swap/942ebae6937d9ad7964f285a790e7721f712b9ec/images/exampleV.jpg -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import launch 3 | 4 | if not launch.is_installed("mediapipe") or not launch.is_installed("mediapipe-silicon"): 5 | name = "Batch Face Swap" 6 | if platform.system() == "Darwin" and platform.machine == "arm64": 7 | # MacOS 8 | launch.run_pip("install mediapipe-silicon", "requirements for Batch Face Swap for MacOS") 9 | else: 10 | launch.run_pip("install mediapipe", "requirements for Batch Face Swap") 11 | -------------------------------------------------------------------------------- /scripts/batch_face_swap.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | cwd = os.getcwd() 5 | utils_dir = os.path.join(cwd, 'extensions', 'batch-face-swap', 'scripts') 6 | sys.path.extend([utils_dir]) 7 | 8 | from bfs_utils import * 9 | from face_detect import * 10 | from sd_helpers import renderImg2Img, renderTxt2Img 11 | 12 | import modules.scripts as scripts 13 | import gradio as gr 14 | import time 15 | import random 16 | 17 | from modules import images, masking, generation_parameters_copypaste, script_callbacks 18 | from modules.processing import process_images, create_infotext, Processed 19 | from modules.shared import opts, cmd_opts, state 20 | import modules.shared as shared 21 | import modules.sd_samplers 22 | 23 | import cv2 24 | import numpy as np 25 | from PIL import Image, ImageOps, ImageDraw, ImageFilter, UnidentifiedImageError 26 | import math 27 | 28 | def apply_checkpoint(x): 29 | info = modules.sd_models.get_closet_checkpoint_match(x) 30 | if info is None: 31 | raise RuntimeError(f"Unknown checkpoint: {x}") 32 | modules.sd_models.reload_model_weights(shared.sd_model, info) 33 | 34 | def findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file, totalNumberOfFaces, singleMaskPerImage, countFaces, maskWidth, maskHeight, skip): 35 | rejected = 0 36 | masks = [] 37 | faces_info = [] 38 | imageOriginal = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) 39 | heightOriginal = height 40 | widthOriginal = width 41 | 42 | # Calculate the size of each small image 43 | small_width = width if onlyHorizontal else math.ceil(width / divider) 44 | small_height = height if onlyVertical else math.ceil(height / divider) 45 | 46 | # Divide the large image into a list of small images 47 | small_images = [] 48 | for i in range(0, height, small_height): 49 | for j in range(0, width, small_width): 50 | small_images.append(image.crop((j, i, j + small_width, i + small_height))) 51 | 52 | # Process each small image 53 | processed_images = [] 54 | facesInImage = 0 55 | 56 | for i, small_image in enumerate(small_images): 57 | small_image_index = i 58 | small_image = cv2.cvtColor(np.array(small_image), cv2.COLOR_RGB2BGR) 59 | 60 | faces = [] 61 | 62 | if facecfg.faceMode == FaceMode.ORIGINAL: 63 | landmarks = [] 64 | landmarks = getFacialLandmarks(small_image, facecfg) 65 | numberOfFaces = int(len(landmarks)) 66 | totalNumberOfFaces += numberOfFaces 67 | 68 | if countFaces: 69 | continue 70 | 71 | faces = [] 72 | for landmark in landmarks: 73 | face_info = {} 74 | convexhull = cv2.convexHull(landmark) 75 | faces.append(convexhull) 76 | faces_info.append(computeFaceInfo(landmark, onlyHorizontal, divider, small_width, small_height, small_image_index)) 77 | 78 | elif facecfg.faceMode == FaceMode.YUNET: 79 | known_face_rects = [] 80 | 81 | # first find the faces the old way, since OpenCV is BAD at faces near the camera 82 | # save the convex hulls, but also getting bounding boxes so OpenCV can skip those 83 | landmarks = getFacialLandmarks(small_image, facecfg) 84 | for landmark in landmarks: 85 | face_info = {} 86 | convexhull = cv2.convexHull(landmark) 87 | faces.append(convexhull) 88 | bounds = cv2.boundingRect(convexhull) 89 | known_face_rects.append(list(bounds)) # convert tuple to array for consistency 90 | 91 | faces_info.append(computeFaceInfo(landmark, onlyHorizontal, divider, small_width, small_height, small_image_index)) 92 | 93 | faceRects = getFaceRectanglesYuNet(small_image, known_face_rects) 94 | 95 | for rect in faceRects: 96 | landmarkHull, face_info = getFacialLandmarkConvexHull(image, rect, onlyHorizontal, divider, small_width, small_height, small_image_index, facecfg) 97 | if landmarkHull is not None: 98 | faces.append(landmarkHull) 99 | faces_info.append(face_info) 100 | else: 101 | rejected += 1 102 | 103 | numberOfFaces = int(len(faces)) 104 | totalNumberOfFaces += numberOfFaces 105 | if countFaces: 106 | continue 107 | 108 | else: 109 | # use OpenCV2 multi-scale face detector to find all the faces 110 | 111 | known_face_rects = [] 112 | 113 | # first find the faces the old way, since OpenCV is BAD at faces near the camera 114 | # save the convex hulls, but also getting bounding boxes so OpenCV can skip those 115 | landmarks = getFacialLandmarks(small_image, facecfg) 116 | for landmark in landmarks: 117 | face_info = {} 118 | convexhull = cv2.convexHull(landmark) 119 | faces.append(convexhull) 120 | bounds = cv2.boundingRect(convexhull) 121 | known_face_rects.append(list(bounds)) # convert tuple to array for consistency 122 | 123 | faces_info.append(computeFaceInfo(landmark, onlyHorizontal, divider, small_width, small_height, small_image_index)) 124 | 125 | faceRects = getFaceRectangles(small_image, known_face_rects, facecfg) 126 | 127 | for rect in faceRects: 128 | landmarkHull, face_info = getFacialLandmarkConvexHull(image, rect, onlyHorizontal, divider, small_width, small_height, small_image_index, facecfg) 129 | if landmarkHull is not None: 130 | faces.append(landmarkHull) 131 | faces_info.append(face_info) 132 | else: 133 | rejected += 1 134 | 135 | numberOfFaces = int(len(faces)) 136 | totalNumberOfFaces += numberOfFaces 137 | if countFaces: 138 | continue 139 | 140 | 141 | if len(faces) == 0: 142 | small_image[:] = (0, 0, 0) 143 | 144 | if numberOfFaces > 0: 145 | facesInImage += numberOfFaces 146 | if facesInImage == 0 and i == len(small_images) - 1: 147 | skip = 1 148 | 149 | for i in range(len(faces)): 150 | processed_images = [] 151 | for k in range(len(small_images)): 152 | mask = np.zeros((small_height, small_width), np.uint8) 153 | if k == small_image_index: 154 | small_image = cv2.fillConvexPoly(mask, faces[i], 255) 155 | processed_image = Image.fromarray(small_image) 156 | processed_images.append(processed_image) 157 | else: 158 | processed_image = Image.fromarray(mask) 159 | processed_images.append(processed_image) 160 | 161 | # Create a new image with the same size as the original large image 162 | new_image = Image.new('RGB', (width, height)) 163 | 164 | # Paste the processed small images into the new image 165 | if onlyHorizontal == True: 166 | for i, processed_image in enumerate(processed_images): 167 | x = (i // divider) * small_width 168 | y = (i % divider) * small_height 169 | new_image.paste(processed_image, (x, y)) 170 | else: 171 | for i, processed_image in enumerate(processed_images): 172 | x = (i % divider) * small_width 173 | y = (i // divider) * small_height 174 | new_image.paste(processed_image, (x, y)) 175 | masks.append(new_image) 176 | 177 | if countFaces: 178 | return totalNumberOfFaces 179 | 180 | if file != None: 181 | if FaceDetectDevelopment: 182 | print(f"Found {facesInImage} face(s) in {str(file)} (rejected {rejected} from OpenCV)") 183 | else: 184 | print(f"Found {facesInImage} face(s) in {str(file)}") 185 | # else: 186 | # print(f"Found {facesInImage} face(s)") 187 | 188 | binary_masks = [] 189 | for i, mask in enumerate(masks): 190 | gray_image = mask.convert('L') 191 | numpy_array = np.array(gray_image) 192 | binary_mask = cv2.threshold(numpy_array, 200, 255, cv2.THRESH_BINARY)[1] 193 | if maskWidth != 100 or maskHeight != 100: 194 | binary_mask = maskResize(binary_mask, maskWidth, maskHeight) 195 | binary_masks.append(binary_mask) 196 | 197 | # try: 198 | # kernel = np.ones((int(math.ceil(0.011*height)),int(math.ceil(0.011*height))),'uint8') 199 | # dilated = cv2.dilate(binary_mask,kernel,iterations=1) 200 | # kernel = np.ones((int(math.ceil(0.0045*height)),int(math.ceil(0.0025*height))),'uint8') 201 | # dilated = cv2.dilate(dilated,kernel,iterations=1,anchor=(1, -1)) 202 | # kernel = np.ones((int(math.ceil(0.014*height)),int(math.ceil(0.0025*height))),'uint8') 203 | # dilated = cv2.dilate(dilated,kernel,iterations=1,anchor=(-1, 1)) 204 | # mask = dilated 205 | # except cv2.error: 206 | # mask = dilated 207 | 208 | if singleMaskPerImage and len(binary_masks) > 0: 209 | result = [] 210 | h, w = binary_masks[0].shape 211 | result = np.full((h,w), 0, dtype=np.uint8) 212 | for mask in binary_masks: 213 | result = cv2.add(result, mask) 214 | masks = [ result ] 215 | return masks, totalNumberOfFaces, faces_info, skip 216 | else: 217 | masks = binary_masks 218 | return masks, totalNumberOfFaces, faces_info, skip 219 | 220 | # generate debug image 221 | def faceDebug(p, masks, image, finishedImages, invertMask, forced_filename, output_path, info): 222 | generatedImages = [] 223 | paste_to = [] 224 | imageOriginal = image 225 | overlay_image = image 226 | 227 | for n, mask in enumerate(masks): 228 | mask = Image.fromarray(masks[n]) 229 | if invertMask: 230 | mask = ImageOps.invert(mask) 231 | 232 | image_masked = Image.new('RGBa', (image.width, image.height)) 233 | image_masked.paste(overlay_image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(mask.convert('L'))) 234 | 235 | overlay_image = image_masked.convert('RGBA') 236 | 237 | debugsave(overlay_image) 238 | 239 | def faceSwap(p, masks, image, finishedImages, invertMask, forced_filename, output_path, info, selectedTab,mainTab, geninfo, faces_info, rotation_threshold, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height): 240 | 241 | bfs_prompt = bfs_prompt if overridePrompt else p.prompt 242 | bfs_nprompt = bfs_nprompt if overridePrompt else p.negative_prompt 243 | original_model = modules.sd_models.select_checkpoint() 244 | if overrideModel: 245 | apply_checkpoint(sd_model) 246 | sd_sampler = sd_sampler if overrideSampler else p.sampler_name 247 | batch_size = 1 if mainTab == "txt2img" else p.batch_size 248 | n_iter = 1 if mainTab == "txt2img" else p.n_iter 249 | denoising_strength = 0.5 if overrideDenoising else denoising_strength 250 | seed = int(random.randrange(4294967294)) if overrideSeed else p.seed 251 | steps = steps if overrideSteps else p.steps 252 | cfg_scale = cfg_scale if overrideCfgScale else p.cfg_scale 253 | bfs_width = bfs_width if overrideSize else p.width 254 | bfs_height = bfs_height if overrideSize else p.height 255 | 256 | # automatically adjust mask_blur based on the size of the image but don't make it higher than 30 257 | mask_blur = np.clip(int(math.ceil(0.01*image.height if image.height > image.width else 0.01*image.width)), None, 30) if overrideMaskBlur else mask_blur 258 | 259 | # automatically adjust inpaint_full_res_padding based on the size of the image 260 | inpaint_full_res_padding = int(math.ceil(0.03*image.height if image.height > image.width else 0.03*image.width)) if overridePadding else inpaint_full_res_padding 261 | 262 | inpainting_full_res = 1 263 | inpainting_fill = 1 264 | 265 | wasGrid = p.do_not_save_grid 266 | wasReturnGrid = opts.return_grid 267 | opts.return_grid = False 268 | p.do_not_save_grid = True 269 | p.do_not_save_samples = True 270 | index = 0 271 | generatedImages = [] 272 | paste_to = [] 273 | imageOriginal = image 274 | overlay_image = image 275 | 276 | for n, mask in enumerate(masks): 277 | rotate = False 278 | mask = Image.fromarray(masks[n]) 279 | if invertMask: 280 | image_mask = ImageOps.invert(mask) 281 | else: 282 | image_masked = Image.new('RGBa', (image.width, image.height)) 283 | image_masked.paste(overlay_image.convert("RGBA").convert("RGBa"), mask=ImageOps.invert(mask.convert('L'))) 284 | 285 | overlay_image = image_masked.convert('RGBA') 286 | 287 | crop_region = masking.get_crop_region(np.array(mask), inpaint_full_res_padding) 288 | crop_region = masking.expand_crop_region(crop_region, p.width, p.height, mask.width, mask.height) 289 | x1, y1, x2, y2 = crop_region 290 | paste_to.append((x1, y1, x2-x1, y2-y1)) 291 | 292 | for i in range(len(faces_info)): 293 | try: 294 | pixel_color = mask.getpixel((faces_info[i]["center"][0],faces_info[i]["center"][1])) 295 | except IndexError: 296 | pixel_color = 0 297 | if pixel_color == 255: 298 | index = i 299 | break 300 | 301 | mask = mask.crop(crop_region) 302 | image_mask = images.resize_image(2, mask, p.width, p.height) 303 | 304 | image = image.crop(crop_region) 305 | image = images.resize_image(2, image, p.width, p.height) 306 | image_cropped = image 307 | 308 | rotation_threshold = rotation_threshold 309 | if 90+rotation_threshold > faces_info[index]["angle"] and 90-rotation_threshold < faces_info[index]["angle"]: 310 | pass 311 | else: 312 | angle_difference = (90-int(faces_info[index]["angle"]) + 360) % 360 313 | image = image.rotate(angle_difference, expand=True) 314 | image_mask = image_mask.rotate(angle_difference, expand=True) 315 | rotate = True 316 | 317 | if geninfo != "": 318 | bfs_prompt = str(geninfo.get("Prompt")) 319 | bfs_nprompt = str(geninfo.get("Negative prompt")) 320 | sd_sampler = str(geninfo.get("Sampler")) 321 | cfg_scale = float(geninfo.get("CFG scale")) 322 | bfs_width = int(geninfo.get("Size-1")) 323 | bfs_height = int(geninfo.get("Size-2")) 324 | 325 | proc = renderImg2Img( 326 | bfs_prompt, 327 | bfs_nprompt, 328 | sd_sampler, 329 | steps, 330 | cfg_scale, 331 | seed, 332 | bfs_width, 333 | bfs_height, 334 | image, 335 | image_mask, 336 | batch_size, 337 | n_iter, 338 | denoising_strength, 339 | mask_blur, 340 | inpainting_fill, 341 | inpainting_full_res, 342 | inpaint_full_res_padding, 343 | do_not_save_samples = True, 344 | ) 345 | apply_checkpoint(original_model.title) 346 | 347 | if rotate: 348 | for i in range(len(proc.images)): 349 | image_copy = image_cropped.copy() 350 | proc.images[i] = proc.images[i].rotate(int(faces_info[index]["angle"])-90) 351 | w1, h1 = image_cropped.size 352 | w2, h2 = proc.images[i].size 353 | x = (w1 - w2) // 2 354 | y = (h1 - h2) // 2 355 | image_copy.paste(proc.images[i], (x, y)) 356 | proc.images[i] = image_copy 357 | generatedImages.append(proc.images) 358 | image = imageOriginal 359 | 360 | for j in range(n_iter * batch_size): 361 | if not invertMask: 362 | image = imageOriginal 363 | for k in range(len(generatedImages)): 364 | mask = Image.fromarray(masks[k]) 365 | mask = mask.filter(ImageFilter.GaussianBlur(mask_blur)) 366 | image = apply_overlay(generatedImages[k][j], paste_to[k], image, mask) 367 | else: 368 | image = proc.images[j] 369 | 370 | info = infotext(p) 371 | 372 | final_forced_filename = forced_filename+"_"+str(j+1) if forced_filename != None and (batch_size > 1 or n_iter > 1) else forced_filename 373 | if opts.samples_format != "png" and image.mode != 'RGB': 374 | image = image.convert('RGB') 375 | images.save_image(image, output_path if output_path !="" else opts.outdir_img2img_samples, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=final_forced_filename) 376 | 377 | finishedImages.append(image) 378 | 379 | p.do_not_save_samples = False 380 | p.do_not_save_grid = wasGrid 381 | opts.return_grid = wasReturnGrid 382 | 383 | return finishedImages 384 | 385 | def generateImages(p, facecfg, input_image, input_path, searchSubdir, viewResults, divider, howSplit, saveMask, output_path, saveToOriginalFolder, onlyMask, saveNoFace, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab,mainTab, loadGenParams, rotation_threshold): 386 | suffix = '' 387 | info = infotext(p) 388 | if selectedTab == "generateMasksTab": 389 | finishedImages = [] 390 | wasCountFaces = False 391 | totalNumberOfFaces = 0 392 | allFiles = [] 393 | geninfo = "" 394 | 395 | onlyHorizontal = ("Horizontal" in howSplit) 396 | onlyVertical = ("Vertical" in howSplit) 397 | 398 | # if neither path nor image, we're done 399 | if input_path == '' and input_image is None: 400 | return finishedImages 401 | 402 | # flag whether we're processing a directory or a specified image 403 | # (the code after this supports multiple images in an array, but the UI only allows a single image) 404 | usingFilenames = (input_path != '') 405 | if usingFilenames: 406 | allFiles = listFiles(input_path, searchSubdir, allFiles) 407 | else: 408 | allFiles += input_image 409 | 410 | start_time = time.thread_time() 411 | 412 | if countFaces: 413 | print("\nCounting faces...") 414 | for i, file in enumerate(allFiles): 415 | skip = 0 416 | image = Image.open(file) if usingFilenames else file 417 | width, height = image.size 418 | masks, totalNumberOfFaces, faces_info, skip = findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file, totalNumberOfFaces, singleMaskPerImage, countFaces, maskWidth, maskHeight, skip) 419 | 420 | if not onlyMask and countFaces: 421 | print(f"\nWill process {len(allFiles)} images, found {totalNumberOfFaces} faces, generating {p.n_iter * p.batch_size} new images for each.") 422 | state.job_count = totalNumberOfFaces * p.n_iter 423 | elif not onlyMask and not countFaces: 424 | print(f"\nWill process {len(allFiles)} images, generating {p.n_iter * p.batch_size} new images for each.") 425 | state.job_count = len(allFiles) * p.n_iter 426 | 427 | for i, file in enumerate(allFiles): 428 | if usingFilenames and keepOriginalName: 429 | forced_filename = os.path.splitext(os.path.basename(file))[0] 430 | else: 431 | forced_filename = None 432 | 433 | if usingFilenames and saveToOriginalFolder: 434 | output_path = os.path.dirname(file) 435 | 436 | if countFaces: 437 | state.job = f"{i+1} out of {totalNumberOfFaces}" 438 | totalNumberOfFaces = 0 439 | wasCountFaces = True 440 | countFaces = False 441 | else: 442 | state.job = f"{i+1} out of {len(allFiles)}" 443 | 444 | if state.skipped: 445 | state.skipped = False 446 | if state.interrupted and onlyMask: 447 | state.interrupted = False 448 | elif state.interrupted: 449 | break 450 | 451 | try: 452 | image = Image.open(file) if usingFilenames else file 453 | width, height = image.size 454 | 455 | if loadGenParams: 456 | geninfo, _ = read_info_from_image(image) 457 | geninfo = generation_parameters_copypaste.parse_generation_parameters(geninfo) 458 | 459 | except UnidentifiedImageError: 460 | print(f"\nUnable to open {file}, skipping") 461 | continue 462 | 463 | skip = 0 464 | masks, totalNumberOfFaces, faces_info, skip = findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file, totalNumberOfFaces, singleMaskPerImage, countFaces, maskWidth, maskHeight, skip) 465 | 466 | if facecfg.debugSave: 467 | faceDebug(p, masks, image, finishedImages, invertMask, forced_filename, output_path, info) 468 | 469 | # Only generate mask 470 | if onlyMask: 471 | suffix = '_mask' 472 | 473 | # Load mask 474 | for i, mask in enumerate(masks): 475 | mask = Image.fromarray(mask) 476 | 477 | # Invert mask if needed 478 | if invertMask: 479 | mask = ImageOps.invert(mask) 480 | finishedImages.append(mask) 481 | 482 | if saveMask and skip != 1: 483 | custom_save_image(p, mask, output_path, forced_filename, suffix, info) 484 | elif saveMask and skip == 1 and saveNoFace: 485 | custom_save_image(p, mask, output_path, forced_filename, suffix, info) 486 | 487 | # If face was not found but user wants to save images without face 488 | if skip == 1 and saveNoFace and not onlyMask: 489 | custom_save_image(p, image, output_path, forced_filename, suffix, info) 490 | 491 | finishedImages.append(image) 492 | state.skipped = True 493 | continue 494 | 495 | # If face was not found, just skip 496 | if skip == 1: 497 | state.skipped = True 498 | continue 499 | 500 | if not onlyMask: 501 | finishedImages = faceSwap(p, masks, image, finishedImages, invertMask, forced_filename, output_path, info, selectedTab, mainTab, geninfo, faces_info, rotation_threshold, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height,) 502 | 503 | if usingFilenames and not viewResults: 504 | finishedImages = [] 505 | 506 | if wasCountFaces == True: 507 | countFaces = True 508 | 509 | timing = time.thread_time() - start_time 510 | print(f"Found {totalNumberOfFaces} faces in {len(allFiles)} images in {timing} seconds.") 511 | 512 | # EXISTING MASKS 513 | elif selectedTab == "existingMasksTab": 514 | finishedImages = [] 515 | allImages = [] 516 | allMasks = [] 517 | searchSubdir = False 518 | 519 | if pathExisting != '' and pathMasksExisting != '': 520 | allImages = listFiles(pathExisting, searchSubdir, allImages) 521 | allMasks = listFiles(pathMasksExisting, searchSubdir, allMasks) 522 | 523 | print(f"\nWill process {len(allImages)} images, generating {p.n_iter * p.batch_size} new images for each.") 524 | state.job_count = len(allImages) * p.n_iter 525 | for i, file in enumerate(allImages): 526 | forced_filename = os.path.splitext(os.path.basename(file))[0] 527 | 528 | state.job = f"{i+1} out of {len(allImages)}" 529 | 530 | if state.skipped: 531 | state.skipped = False 532 | elif state.interrupted: 533 | break 534 | 535 | try: 536 | image = Image.open(file) 537 | width, height = image.size 538 | 539 | masks = [] 540 | masks.append(Image.open(os.path.join(pathMasksExisting, os.path.splitext(os.path.basename(file))[0])+os.path.splitext(allMasks[i])[1])) 541 | except UnidentifiedImageError: 542 | print(f"\nUnable to open {file}, skipping") 543 | continue 544 | 545 | 546 | finishedImages = faceSwap(p, masks, image, finishedImages, invertMask, forced_filename, output_pathExisting, info, selectedTab, mainTab, faces_info, rotation_threshold, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height,) 547 | 548 | if not viewResults: 549 | finishedImages = [] 550 | 551 | return finishedImages 552 | 553 | class Script(scripts.Script): 554 | def title(self): 555 | return "Batch Face Swap" 556 | 557 | def show(self, is_img2img): 558 | return scripts.AlwaysVisible 559 | 560 | def ui(self, is_img2img): 561 | def updateVisualizer(searchSubdir: bool, howSplit: str, divider: int, maskWidth: int, maskHeight: int, input_path: str, visualizationOpacity: int, faceMode: int): 562 | facecfg = FaceDetectConfig(faceMode) # this is a huge pain to patch through so don't bother 563 | allFiles = [] 564 | totalNumberOfFaces = 0 565 | 566 | usingFilenames = (input_path != '') 567 | if usingFilenames: 568 | allFiles = listFiles(input_path, searchSubdir, allFiles) 569 | 570 | if len(allFiles) > 0: 571 | file = allFiles[0] 572 | try: 573 | image = Image.open(file) 574 | maxsize = (1000, 500) 575 | image.thumbnail(maxsize,Image.ANTIALIAS) 576 | except (UnidentifiedImageError, AttributeError): 577 | allFiles = [] 578 | 579 | visualizationOpacity = (visualizationOpacity/100)*255 580 | color = "white" 581 | thickness = 5 582 | 583 | if "Both" in howSplit: 584 | onlyHorizontal = False 585 | onlyVertical = False 586 | if len(allFiles) == 0: 587 | image = Image.open("./extensions/batch-face-swap/images/exampleB.jpg") 588 | width, height = image.size 589 | 590 | # if len(masks)==0 and path != '': 591 | masks, totalNumberOfFaces, faces_info, skip = findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file=None, totalNumberOfFaces=totalNumberOfFaces, singleMaskPerImage=True, countFaces=False, maskWidth=maskWidth, maskHeight=maskHeight, skip=0) 592 | 593 | if len(masks) > 0: 594 | mask = masks[0] 595 | else: 596 | mask = np.zeros((image.height, image.width, 3), dtype=np.uint8) 597 | 598 | # mask = maskResize(mask, maskSize, height) 599 | 600 | mask = Image.fromarray(mask) 601 | redImage = Image.new("RGB", (width, height), (255, 0, 0)) 602 | 603 | mask = mask.convert("L") 604 | draw = ImageDraw.Draw(mask, "L") 605 | 606 | if divider > 1: 607 | for i in range(divider-1): 608 | start_point = (0, int((height/divider)*(i+1))) 609 | end_point = (int(width), int((height/divider)*(i+1))) 610 | draw.line([start_point, end_point], fill=color, width=thickness) 611 | 612 | for i in range(divider-1): 613 | start_point = (int((width/divider)*(i+1)), 0) 614 | end_point = (int((width/divider)*(i+1)), int(height)) 615 | draw.line([start_point, end_point], fill=color, width=thickness) 616 | 617 | image = composite(redImage, image, mask, visualizationOpacity) 618 | 619 | elif "Vertical" in howSplit: 620 | onlyHorizontal = False 621 | onlyVertical = True 622 | if len(allFiles) == 0: 623 | image = Image.open("./extensions/batch-face-swap/images/exampleV.jpg") 624 | # mask = Image.open("./extensions/batch-face-swap/images/exampleV_mask.jpg") 625 | # mask = np.array(mask) 626 | width, height = image.size 627 | 628 | # if len(masks)==0 and path != '': 629 | masks, totalNumberOfFaces, faces_info, skip = findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file=None, totalNumberOfFaces=totalNumberOfFaces, singleMaskPerImage=True, countFaces=False, maskWidth=maskWidth, maskHeight=maskHeight, skip=0) 630 | 631 | if len(masks) > 0: 632 | mask = masks[0] 633 | else: 634 | mask = np.zeros((image.height, image.width, 3), dtype=np.uint8) 635 | 636 | # mask = maskResize(mask, maskSize, height) 637 | 638 | mask = Image.fromarray(mask) 639 | redImage = Image.new("RGB", (width, height), (255, 0, 0)) 640 | 641 | mask = mask.convert("L") 642 | draw = ImageDraw.Draw(mask, "L") 643 | 644 | if divider > 1: 645 | for i in range(divider-1): 646 | start_point = (int((width/divider)*(i+1)), 0) 647 | end_point = (int((width/divider)*(i+1)), int(height)) 648 | draw.line([start_point, end_point], fill=color, width=thickness) 649 | 650 | image = composite(redImage, image, mask, visualizationOpacity) 651 | 652 | else: 653 | onlyHorizontal = True 654 | onlyVertical = False 655 | if len(allFiles) == 0: 656 | image = Image.open("./extensions/batch-face-swap/images/exampleH.jpg") 657 | width, height = image.size 658 | 659 | # if len(masks)==0 and path != '': 660 | masks, totalNumberOfFaces, faces_info, skip = findFaces(facecfg, image, width, height, divider, onlyHorizontal, onlyVertical, file=None, totalNumberOfFaces=totalNumberOfFaces, singleMaskPerImage=True, countFaces=False, maskWidth=maskWidth, maskHeight=maskHeight, skip=0) 661 | 662 | if len(masks) > 0: 663 | mask = masks[0] 664 | else: 665 | mask = np.zeros((image.height, image.width, 3), dtype=np.uint8) 666 | 667 | # mask = maskResize(mask, maskSize, height) 668 | 669 | mask = Image.fromarray(mask) 670 | redImage = Image.new("RGB", (width, height), (255, 0, 0)) 671 | 672 | mask = mask.convert("L") 673 | draw = ImageDraw.Draw(mask, "L") 674 | 675 | if divider > 1: 676 | for i in range(divider-1): 677 | start_point = (0, int((height/divider)*(i+1))) 678 | end_point = (int(width), int((height/divider)*(i+1))) 679 | draw.line([start_point, end_point], fill=color, width=thickness) 680 | 681 | image = composite(redImage, image, mask, visualizationOpacity) 682 | 683 | update = gr.Image.update(value=image) 684 | return update 685 | def switchSaveMaskInteractivity(onlyMask: bool): 686 | return gr.Checkbox.update(interactive=bool(onlyMask)) 687 | def switchSaveMask(onlyMask: bool): 688 | if onlyMask == False: 689 | return gr.Checkbox.update(value=bool(onlyMask)) 690 | def switchTipsVisibility(showTips: bool): 691 | return gr.HTML.update(visible=bool(showTips)) 692 | def switchInvertMask(invertMask: bool): 693 | return gr.Checkbox.update(value=bool(invertMask)) 694 | def switchColumnVisibility(switch_element: bool): 695 | return gr.Column.update(visible=bool(switch_element)) 696 | def switchColumnVisibilityInverted(switch_element: bool): 697 | return gr.Column.update(visible=bool(not switch_element)) 698 | def switchEnableLabel(enabled: bool): 699 | if enabled == True: 700 | return gr.Checkbox.update(label=str("Enabled ✅")) 701 | else: 702 | return gr.Checkbox.update(label=str("Disabled ❌")) 703 | 704 | with gr.Accordion("🎭 Batch Face Swap 🎭", open = False, elem_id="batch_face_swap"): 705 | with gr.Row(): 706 | enabled = gr.Checkbox(label='Disabled ❌', value=False) 707 | if not is_img2img: 708 | with gr.Column(): 709 | regen_btn = gr.Button(value="Swap 🎭", variant="primary", interactive=True) 710 | gr.HTML("

Check your output folder (this won't show results in webui)

",visible=True) 711 | else: 712 | regen_btn = gr.Button(value="Swap 🎭", variant="primary", visible=False) 713 | with gr.Accordion("♻ Overrides ♻", open = True): 714 | with gr.Box(): 715 | # Overrides 716 | with gr.Column(): 717 | with gr.Column(variant='panel'): 718 | overridePrompt = gr.Checkbox(value=False, label="""Override "Prompt" """) 719 | with gr.Column(visible=False) as override_prompt_col: 720 | bfs_prompt = gr.Textbox(label="Prompt", show_label=False, lines=2, placeholder="Prompt") 721 | bfs_nprompt = gr.Textbox(label="Negative prompt", show_label=False, lines=2, placeholder="Negative prompt") 722 | with gr.Row(): 723 | with gr.Column(): 724 | with gr.Column(variant='panel', scale=2): 725 | overrideSeed = gr.Checkbox(value=True, label="""Override "Seed" to random""", interactive=True) 726 | with gr.Column(variant='panel'): 727 | overrideSampler = gr.Checkbox(value=False, label="""Override "Sampling method" """) 728 | with gr.Column(visible=False) as override_sampler_col: 729 | available_samplers = [s.name for s in modules.sd_samplers.samplers_for_img2img] 730 | sd_sampler = gr.Dropdown(label="Sampling Method", choices=available_samplers, value="Euler a", type="value", interactive=True) 731 | with gr.Column(variant='panel'): 732 | overrideSteps = gr.Checkbox(value=False, label="""Override "Sampling steps" """, interactive=True) 733 | with gr.Column(visible=False) as override_steps_col: 734 | steps = gr.Slider(minimum=1, maximum=150, step=1 , value=30, label="Sampling Steps", interactive=True) 735 | with gr.Column(variant='panel'): 736 | overrideDenoising = gr.Checkbox(value=True, label="""Override "Denoising strength" to 0.5""") 737 | with gr.Column(visible=False) as override_denoising_col: 738 | denoising_strength = gr.Slider(minimum=0, maximum=1, step=0.01 , value=0.5, label="Denoising Strength", interactive=True) 739 | 740 | with gr.Column(): 741 | with gr.Column(variant='panel', scale=2): 742 | overrideSize = gr.Checkbox(value=False, label="""Override "Resolution" """, interactive=True) 743 | with gr.Column(visible=False) as override_size_col: 744 | with gr.Row(): 745 | bfs_width = gr.Slider(minimum=64, maximum=2048, step=4 , value=512, label="Width", interactive=True) 746 | bfs_height = gr.Slider(minimum=64, maximum=2048, step=4 , value=512, label="Height", interactive=True) 747 | with gr.Column(variant='panel'): 748 | overrideModel = gr.Checkbox(value=False, label="""Override "Stable Diffusion checkpoint" """) 749 | with gr.Column(visible=False) as override_model_col: 750 | with gr.Row(): 751 | available_models = modules.sd_models.checkpoint_tiles() 752 | sd_model = gr.Dropdown(label="SD Model", choices=available_models, value=shared.sd_model.sd_checkpoint_info.title, type="value", interactive=True) 753 | modules.ui.create_refresh_button(sd_model, modules.sd_models.list_models, lambda: {"choices": modules.sd_models.checkpoint_tiles()}, "refresh_sd_checkpoint") 754 | with gr.Column(variant='panel'): 755 | overrideCfgScale = gr.Checkbox(value=False, label="""Override "CFG Scale" """, interactive=True) 756 | with gr.Column(visible=False) as override_cfg_col: 757 | cfg_scale = gr.Slider(minimum=1, maximum=30, step=1 , value=6, label="CFG Scale", interactive=True) 758 | with gr.Column(variant='panel'): 759 | overrideMaskBlur = gr.Checkbox(value=True, label="""Override "Mask blur" to automatic""") 760 | with gr.Column(visible=False) as override_maskBlur_col: 761 | mask_blur = gr.Slider(minimum=0, maximum=64, step=1 , value=4, label="Mask Blur", interactive=True) 762 | 763 | with gr.Column(variant='panel'): 764 | with gr.Row(): 765 | overridePadding = gr.Checkbox(value=True, label="""Override "Only masked padding, pixels" to automatic""") 766 | with gr.Row(): 767 | with gr.Column(visible=False) as override_padding_col: 768 | inpaint_full_res_padding = gr.Slider(minimum=0, maximum=256, step=4 , value=32, label="Only masked padding, pixels", interactive=True) 769 | 770 | if is_img2img: 771 | # Path to images 772 | gr.HTML("

Input:

") 773 | with gr.Column(variant='panel'): 774 | htmlTip1 = gr.HTML("

'Load from subdirectories' will include all images in all subdirectories.

",visible=False) 775 | with gr.Row(): 776 | input_path = gr.Textbox(label="Images directory",placeholder=r"C:\Users\dude\Desktop\images", visible=True) 777 | output_path = gr.Textbox(label="Output directory (OPTIONAL)",placeholder=r"Leave empty to save to default directory") 778 | with gr.Row(): 779 | searchSubdir = gr.Checkbox(value=False, label="Load from subdirectories") 780 | saveToOriginalFolder = gr.Checkbox(value=False, label="Save to original folder") 781 | keepOriginalName = gr.Checkbox(value=False, label="Keep original file name (OVERWRITES FILES WITH THE SAME NAME)") 782 | loadGenParams = gr.Checkbox(value=False, label="Load generation parameters from images") 783 | 784 | else: 785 | htmlTip1 = gr.HTML("

",visible=False) 786 | input_path = gr.Textbox(label="Images directory", visible=False) 787 | output_path = gr.Textbox(label="Output directory (OPTIONAL)", visible=False) 788 | searchSubdir = gr.Checkbox(value=False, label="Load from subdirectories", visible=False) 789 | saveToOriginalFolder = gr.Checkbox(value=False, label="Save to original folder", visible=False) 790 | keepOriginalName = gr.Checkbox(value=False, label="Keep original file name (OVERWRITES FILES WITH THE SAME NAME)", visible=False) 791 | loadGenParams = gr.Checkbox(value=False, label="Load generation parameters from images", visible=False) 792 | 793 | with gr.Accordion("⚙️ Settings ⚙️", open = False): 794 | with gr.Column(variant='panel'): 795 | with gr.Tab("Generate masks") as generateMasksTab: 796 | # Face detection 797 | with gr.Column(variant='compact'): 798 | gr.HTML("

Face detection:

") 799 | with gr.Row(): 800 | faceDetectMode = gr.Dropdown(label="Detector", choices=face_mode_names, value=face_mode_names[FaceMode.DEFAULT], type="index", elem_id="z_type") 801 | minFace = gr.Slider(minimum=10, maximum=200, step=1 , value=30, label="Minimum face size in pixels") 802 | 803 | with gr.Column(variant='panel'): 804 | htmlTip2 = gr.HTML("

Activate the 'Masks only' checkbox to see how many faces do your current settings detect without generating SD image. (check console)

You can also save generated masks to disk. Only possible with 'Masks only' (if you leave path empty, it will save the masks to your default webui outputs directory)

'Single mask per image' is only recommended with 'Invert mask' or if you want to save one mask per image, not per face. If you activate it without inverting mask, and try to process an image with multiple faces, it will generate only one image for all faces, producing bad results.

'Rotation threshold', if the face is rotated at an angle higher than this value, it will be automatically rotated so it's upright before generating, producing much better results.

",visible=False) 805 | # Settings 806 | with gr.Column(variant='panel'): 807 | gr.HTML("

Settings:

") 808 | with gr.Column(variant='compact'): 809 | with gr.Row(): 810 | onlyMask = gr.Checkbox(value=False, label="Masks only", visible=True) 811 | saveMask = gr.Checkbox(value=False, label="Save masks to disk", interactive=False) 812 | with gr.Row(): 813 | invertMask = gr.Checkbox(value=False, label="Invert mask", visible=True) 814 | singleMaskPerImage = gr.Checkbox(value=False, label="Single mask per image", visible=True) 815 | with gr.Row(variant='panel'): 816 | rotation_threshold = gr.Slider(minimum=0, maximum=180, step=1, value=20, label="Rotation threshold") 817 | 818 | # Image splitter 819 | with gr.Column(variant='panel'): 820 | gr.HTML("

Image splitter:

") 821 | with gr.Column(variant='panel'): 822 | htmlTip3 = gr.HTML("

This divides image to smaller images and tries to find a face in the individual smaller images.

Useful when faces are small in relation to the size of the whole picture and are not being detected.

(may result in mask that only covers a part of a face or no detection if the division goes right through the face)

Open 'Split visualizer' to see how it works.

",visible=False) 823 | with gr.Row(): 824 | divider = gr.Slider(minimum=1, maximum=5, step=1, value=1, label="How many images to divide into") 825 | maskWidth = gr.Slider(minimum=0, maximum=300, step=1, value=100, label="Mask width") 826 | with gr.Row(): 827 | howSplit = gr.Radio(["Horizontal only ▤", "Vertical only ▥", "Both ▦"], value = "Both ▦", label = "How to divide") 828 | maskHeight = gr.Slider(minimum=0, maximum=300, step=1, value=100, label="Mask height") 829 | with gr.Accordion(label="Visualizer", open=False): 830 | exampleImage = gr.Image(value=Image.open("./extensions/batch-face-swap/images/exampleB.jpg"), label="Split visualizer", show_label=False, type="pil", visible=True).style(height=500) 831 | with gr.Row(variant='compact'): 832 | with gr.Column(variant='panel'): 833 | gr.HTML("", visible=False) 834 | with gr.Column(variant='compact'): 835 | visualizationOpacity = gr.Slider(minimum=0, maximum=100, step=1, value=75, label="Opacity") 836 | 837 | # Other 838 | with gr.Column(variant='panel'): 839 | gr.HTML("

Other:

") 840 | with gr.Column(variant='panel'): 841 | htmlTip4 = gr.HTML("

'Count faces before generating' is required to see accurate progress bar (not recommended when processing a large number of images). Because without knowing the number of faces, the webui can't know how many images it will generate. Activating it means you will search for faces twice.

",visible=False) 842 | saveNoFace = gr.Checkbox(value=True, label="Save image even if face was not found") 843 | countFaces = gr.Checkbox(value=False, label="Count faces before generating (accurate progress bar but NOT recommended)") 844 | 845 | with gr.Tab("Existing masks",) as existingMasksTab: 846 | with gr.Column(variant='panel'): 847 | htmlTip5 = gr.HTML("

Image name and it's corresponding mask must have exactly the same name (if image is called `abc.jpg` then it's mask must also be called `abc.jpg`)

",visible=False) 848 | pathExisting = gr.Textbox(label="Images directory",placeholder=r"C:\Users\dude\Desktop\images") 849 | pathMasksExisting = gr.Textbox(label="Masks directory",placeholder=r"C:\Users\dude\Desktop\masks") 850 | output_pathExisting = gr.Textbox(label="Output directory (OPTIONAL)",placeholder=r"Leave empty to save to default directory") 851 | 852 | # General 853 | with gr.Box(): 854 | with gr.Column(variant='panel'): 855 | gr.HTML("

General:

") 856 | htmlTip6 = gr.HTML("

Activate 'Show results in WebUI' checkbox to see results in the WebUI at the end (not recommended when processing a large number of images)

",visible=False) 857 | with gr.Row(): 858 | viewResults = gr.Checkbox(value=True, label="Show results in WebUI") 859 | showTips = gr.Checkbox(value=False, label="Show tips") 860 | 861 | # Face detect internals 862 | with gr.Column(variant='panel', visible = FaceDetectDevelopment): 863 | gr.HTML("

Debug internal config:

") 864 | with gr.Column(variant='panel'): 865 | with gr.Row(): 866 | debugSave = gr.Checkbox(value=False, label="Save debug images") 867 | optimizeDetect= gr.Checkbox(value=True, label="Used optimized detector") 868 | face_x_scale = gr.Slider(minimum=1 , maximum= 6, step=0.1, value=4, label="Face x-scaleX") 869 | face_y_scale = gr.Slider(minimum=1 , maximum= 6, step=0.1, value=2.5, label="Face y-scaleX") 870 | 871 | multiScale = gr.Slider(minimum=1.0, maximum=200, step=0.001, value=1.03, label="Multiscale search stepsizess") 872 | multiScale2 = gr.Slider(minimum=0.8, maximum=200, step=0.001, value=1.0 , label="Multiscale search secondary scalar") 873 | multiScale3 = gr.Slider(minimum=0.8, maximum=2.0, step=0.001, value=1.0 , label="Multiscale search tertiary scale") 874 | 875 | minNeighbors = gr.Slider(minimum=1 , maximum = 10, step=1 , value=5, label="minNeighbors") 876 | mpconfidence = gr.Slider(minimum=0.01, maximum = 2.0, step=0.01, value=0.5, label="FaceMesh confidence threshold") 877 | mpcount = gr.Slider(minimum=1, maximum = 20, step=1, value=5, label="FaceMesh maximum faces") 878 | 879 | # def retriveP(getp: bool): 880 | 881 | # getp = gr.Checkbox(value=True, label="get p", visible=False) 882 | 883 | mainTab = gr.Textbox(value=f"""{"img2img" if is_img2img else "txt2img"}""", visible=False) 884 | selectedTab = gr.Textbox(value="generateMasksTab", visible=False) 885 | generateMasksTab.select(lambda: "generateMasksTab", inputs=None, outputs=selectedTab) 886 | existingMasksTab.select(lambda: "existingMasksTab", inputs=None, outputs=selectedTab) 887 | 888 | # make sure user is in the "Inpaint upload" tab 889 | input_path.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 890 | output_path.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 891 | searchSubdir.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 892 | saveToOriginalFolder.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 893 | keepOriginalName.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 894 | loadGenParams.change(fn=None, _js="gradioApp().getElementById('mode_img2img').querySelectorAll('button')[4].click()", inputs=None, outputs=None) 895 | 896 | 897 | def regen(input_path: str, searchSubdir: bool, viewResults: bool, divider: int, howSplit: str, saveMask: bool, output_path: str, saveToOriginalFolder: bool, onlyMask: bool, saveNoFace: bool, overridePrompt: bool, bfs_prompt: str, bfs_nprompt: str, overrideSampler: bool, sd_sampler: str, overrideModel: bool, sd_model: str, overrideDenoising: bool, denoising_strength: float, overrideMaskBlur: bool, mask_blur: float, overridePadding: bool, inpaint_full_res_padding: float, overrideSeed: bool, overrideSteps: bool, steps: float, overrideCfgScale: bool, cfg_scale: float, overrideSize: bool, bfs_width: float, bfs_height: float, invertMask: bool, singleMaskPerImage: bool, countFaces: bool, maskWidth: float, maskHeight: float, keepOriginalName: bool, pathExisting: str, pathMasksExisting: str, output_pathExisting: str, selectedTab: str, mainTab: str, loadGenParams: bool, rotation_threshold: float, faceDetectMode: str, face_x_scale: float, face_y_scale: float, minFace: float, multiScale: float, multiScale2: float, multiScale3: float, minNeighbors: float, mpconfidence: float, mpcount: float, debugSave: bool, optimizeDetect: bool): 898 | try: 899 | p=original_p 900 | image = input_image 901 | except NameError: 902 | print("Make sure you generated an image first!") 903 | return 904 | 905 | facecfg = FaceDetectConfig(faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect) 906 | 907 | finishedImages = generateImages(p, facecfg, image, input_path, searchSubdir, viewResults, int(divider), howSplit, saveMask, output_path, saveToOriginalFolder, onlyMask, saveNoFace, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, mainTab, loadGenParams, rotation_threshold) 908 | 909 | regen_btn.click(fn=regen, inputs=[input_path, searchSubdir, viewResults, divider, howSplit, saveMask, output_path, saveToOriginalFolder, onlyMask, saveNoFace, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, mainTab, loadGenParams, rotation_threshold, faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect], outputs=None) 910 | 911 | enabled.change(switchEnableLabel, enabled, enabled) 912 | 913 | onlyMask.change(switchSaveMaskInteractivity, onlyMask, saveMask) 914 | onlyMask.change(switchSaveMask, onlyMask, saveMask) 915 | invertMask.change(switchInvertMask, invertMask, singleMaskPerImage) 916 | 917 | faceDetectMode.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 918 | minFace.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 919 | visualizationOpacity.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 920 | searchSubdir.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 921 | howSplit.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 922 | divider.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 923 | maskWidth.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 924 | maskHeight.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 925 | input_path.change(updateVisualizer, [searchSubdir, howSplit, divider, maskWidth, maskHeight, input_path, visualizationOpacity, faceDetectMode], exampleImage) 926 | 927 | overridePrompt.change(switchColumnVisibility, overridePrompt, override_prompt_col) 928 | overrideSize.change(switchColumnVisibility, overrideSize, override_size_col) 929 | overrideSteps.change(switchColumnVisibility, overrideSteps, override_steps_col) 930 | overrideCfgScale.change(switchColumnVisibility, overrideCfgScale, override_cfg_col) 931 | overrideSampler.change(switchColumnVisibility, overrideSampler, override_sampler_col) 932 | overrideModel.change(switchColumnVisibility, overrideModel, override_model_col) 933 | overrideDenoising.change(switchColumnVisibilityInverted, overrideDenoising, override_denoising_col) 934 | overrideMaskBlur.change(switchColumnVisibilityInverted, overrideMaskBlur, override_maskBlur_col) 935 | overridePadding.change(switchColumnVisibilityInverted, overridePadding, override_padding_col) 936 | 937 | showTips.change(switchTipsVisibility, showTips, htmlTip1) 938 | showTips.change(switchTipsVisibility, showTips, htmlTip2) 939 | showTips.change(switchTipsVisibility, showTips, htmlTip3) 940 | showTips.change(switchTipsVisibility, showTips, htmlTip4) 941 | showTips.change(switchTipsVisibility, showTips, htmlTip5) 942 | showTips.change(switchTipsVisibility, showTips, htmlTip6) 943 | 944 | return [enabled, mainTab, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, input_path, searchSubdir, divider, howSplit, saveMask, output_path, saveToOriginalFolder, viewResults, saveNoFace, onlyMask, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect, loadGenParams, rotation_threshold] 945 | 946 | def process(self, p, enabled, mainTab, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, input_path, searchSubdir, divider, howSplit, saveMask, output_path, saveToOriginalFolder, viewResults, saveNoFace, onlyMask, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect, loadGenParams, rotation_threshold): 947 | 948 | global original_p 949 | global all_images 950 | original_p = p 951 | 952 | if enabled and mainTab == "img2img": 953 | wasGrid = p.do_not_save_grid 954 | p.do_not_save_grid = True 955 | 956 | all_images = [] 957 | 958 | facecfg = FaceDetectConfig(faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect) 959 | 960 | if input_path == '': 961 | input_image = [ p.init_images[0] ] 962 | else: 963 | input_image = None 964 | 965 | finishedImages = generateImages(p, facecfg, input_image, input_path, searchSubdir, viewResults, int(divider), howSplit, saveMask, output_path, saveToOriginalFolder, onlyMask, saveNoFace, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, mainTab, loadGenParams, rotation_threshold) 966 | 967 | if not viewResults: 968 | finishedImages = [] 969 | 970 | all_images += finishedImages 971 | 972 | proc = Processed(p, all_images) 973 | 974 | # doing this to prevent starting another img2img generation 975 | p.batch_size = 1 976 | p.n_iter = 0 977 | p.init_images[0] = all_images[0] 978 | 979 | p.do_not_save_grid = wasGrid 980 | return proc 981 | else: 982 | pass 983 | 984 | def postprocess(self, p, processed, enabled, mainTab, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, input_path, searchSubdir, divider, howSplit, saveMask, output_path, saveToOriginalFolder, viewResults, saveNoFace, onlyMask, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect, loadGenParams, rotation_threshold): 985 | 986 | global all_images 987 | global input_image 988 | 989 | if input_path == '': 990 | input_image = [] 991 | input_image += processed.images 992 | 993 | if enabled and mainTab == "txt2img": 994 | 995 | wasGrid = p.do_not_save_grid 996 | p.do_not_save_grid = True 997 | 998 | 999 | all_images = [] 1000 | 1001 | facecfg = FaceDetectConfig(faceDetectMode, face_x_scale, face_y_scale, minFace, multiScale, multiScale2, multiScale3, minNeighbors, mpconfidence, mpcount, debugSave, optimizeDetect) 1002 | 1003 | 1004 | finishedImages = generateImages(p, facecfg, input_image, input_path, searchSubdir, viewResults, int(divider), howSplit, saveMask, output_path, saveToOriginalFolder, onlyMask, saveNoFace, overridePrompt, bfs_prompt, bfs_nprompt, overrideSampler, sd_sampler, overrideModel, sd_model, overrideDenoising, denoising_strength, overrideMaskBlur, mask_blur, overridePadding, inpaint_full_res_padding, overrideSeed, overrideSteps, steps, overrideCfgScale, cfg_scale, overrideSize, bfs_width, bfs_height, invertMask, singleMaskPerImage, countFaces, maskWidth, maskHeight, keepOriginalName, pathExisting, pathMasksExisting, output_pathExisting, selectedTab, mainTab, loadGenParams, rotation_threshold) 1005 | 1006 | if not viewResults: 1007 | finishedImages = [] 1008 | 1009 | all_images += finishedImages 1010 | 1011 | p.do_not_save_grid = wasGrid 1012 | 1013 | processed.images = all_images 1014 | 1015 | elif enabled and mainTab == "img2img": 1016 | processed.images = all_images 1017 | else: 1018 | pass 1019 | 1020 | # def on_ui_settings(): 1021 | # section = ('bfs', "BatchFaceSwap") 1022 | # shared.opts.add_option("bfs_override_prompt", shared.OptionInfo( 1023 | # False, "Default state of Override Prompt", gr.Checkbox, {"interactive": True}, section=section)) 1024 | # shared.opts.add_option("bfs_prompt", shared.OptionInfo( 1025 | # "", "Default Prompt", section=section)) 1026 | # shared.opts.add_option("bfs_nprompt", shared.OptionInfo( 1027 | # "", "Default Negative Prompt", section=section)) 1028 | 1029 | 1030 | # script_callbacks.on_ui_settings(on_ui_settings) -------------------------------------------------------------------------------- /scripts/bfs_utils.py: -------------------------------------------------------------------------------- 1 | from modules import images, sd_samplers 2 | from modules.shared import opts 3 | from modules.processing import create_infotext 4 | 5 | from PIL import Image, ImageOps, ImageChops 6 | 7 | import traceback 8 | import piexif 9 | import json 10 | 11 | import mediapipe as mp 12 | import numpy as np 13 | import math 14 | import cv2 15 | import sys 16 | import os 17 | 18 | def image_channels(image): 19 | return image.shape[2] if image.ndim == 3 else 1 20 | 21 | def apply_overlay(image, paste_loc, imageOriginal, mask): 22 | x, y, w, h = paste_loc 23 | base_image = Image.new('RGBA', (imageOriginal.width, imageOriginal.height)) 24 | image = images.resize_image(1, image, w, h) 25 | base_image.paste(image, (x, y)) 26 | face = base_image 27 | new_mask = ImageChops.multiply(face.getchannel("A"), mask) 28 | face.putalpha(new_mask) 29 | 30 | imageOriginal = imageOriginal.convert('RGBA') 31 | image = Image.alpha_composite(imageOriginal, face) 32 | 33 | return image 34 | 35 | def composite(image1, image2, mask, visualizationOpacity): 36 | mask_np = np.array(mask) 37 | mask_np = np.where(mask_np == 255, visualizationOpacity, 0) 38 | mask = Image.fromarray(mask_np).convert('L') 39 | image = image2.copy() 40 | image.paste(image1, None, mask) 41 | 42 | return image 43 | 44 | def maskResize(mask, maskWidth, maskHeight): 45 | maskOriginal = mask 46 | try: 47 | contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) 48 | except IndexError: 49 | return mask 50 | x,y,w,h = cv2.boundingRect(contours[0]) 51 | center_x = x + w // 2 52 | center_y = y + h // 2 53 | 54 | # Get the bounding box of the contour 55 | x,y,w,h = cv2.boundingRect(contours[0]) 56 | 57 | # Crop the image 58 | cropped = maskOriginal[y:y+h,x:x+w] 59 | 60 | # Resize the cropped image by percentage 61 | height, width = cropped.shape[:2] 62 | new_height = max(int(height * maskHeight / 100), 1) 63 | new_width = max(int(width * maskWidth / 100), 1) 64 | resized = cv2.resize(cropped, (new_width, new_height)) 65 | 66 | # Create black image with same resolution as original image 67 | black_image = Image.fromarray(np.zeros((maskOriginal.shape[0], maskOriginal.shape[1]), np.uint8)) 68 | 69 | # Paste cropped image back to original image so that center of white part on new image matches center of white part on original image 70 | height, width = resized.shape[:2] 71 | x_offset = center_x - width // 2 72 | y_offset = center_y - height // 2 73 | black_image.paste(Image.fromarray(resized), (x_offset, y_offset)) 74 | mask = np.array(black_image) 75 | 76 | return mask 77 | 78 | def listFiles(input_path, searchSubdir, allFiles): 79 | try: 80 | if searchSubdir: 81 | for root, _, files in os.walk(os.path.abspath(input_path)): 82 | for file in files: 83 | if file.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.PNG', '.JPG', '.JPEG', '.BMP')): 84 | allFiles.append(os.path.join(root, file)) 85 | else: 86 | allFiles = [os.path.join(input_path, f) for f in os.listdir(input_path) if os.path.isfile(os.path.join(input_path, f)) and f.endswith(('.png', '.jpg', '.jpeg', '.bmp', '.PNG', '.JPG', '.JPEG', '.BMP'))] 87 | except FileNotFoundError: 88 | if input_path != "": 89 | print(f'Directory "{input_path}" not found!') 90 | 91 | return allFiles 92 | 93 | def custom_save_image(p, image, output_path, forced_filename, suffix, info): 94 | if output_path != "": 95 | if opts.samples_format == "png": 96 | images.save_image(image, output_path, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 97 | elif image.mode != 'RGB': 98 | image = image.convert('RGB') 99 | images.save_image(image, output_path, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 100 | else: 101 | images.save_image(image, output_path, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 102 | 103 | elif output_path == "": 104 | if opts.samples_format == "png": 105 | images.save_image(image, opts.outdir_img2img_samples, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 106 | elif image.mode != 'RGB': 107 | image = image.convert('RGB') 108 | images.save_image(image, opts.outdir_img2img_samples, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 109 | else: 110 | images.save_image(image, opts.outdir_img2img_samples, "", p.seed, p.prompt, opts.samples_format, info=info, p=p, forced_filename=forced_filename, suffix=suffix) 111 | 112 | def debugsave(image): 113 | images.save_image(image, os.getenv("AUTO1111_DEBUGDIR", "outputs"), "", "", "", "jpg", "", None) 114 | 115 | def read_info_from_image(image): 116 | items = image.info or {} 117 | 118 | geninfo = items.pop('parameters', None) 119 | 120 | if "exif" in items: 121 | exif = piexif.load(items["exif"]) 122 | exif_comment = (exif or {}).get("Exif", {}).get(piexif.ExifIFD.UserComment, b'') 123 | try: 124 | exif_comment = piexif.helper.UserComment.load(exif_comment) 125 | except ValueError: 126 | exif_comment = exif_comment.decode('utf8', errors="ignore") 127 | 128 | if exif_comment: 129 | items['exif comment'] = exif_comment 130 | geninfo = exif_comment 131 | 132 | for field in ['jfif', 'jfif_version', 'jfif_unit', 'jfif_density', 'dpi', 'exif', 133 | 'loop', 'background', 'timestamp', 'duration']: 134 | items.pop(field, None) 135 | 136 | if items.get("Software", None) == "NovelAI": 137 | try: 138 | json_info = json.loads(items["Comment"]) 139 | sampler = sd_samplers.samplers_map.get(json_info["sampler"], "Euler a") 140 | 141 | geninfo = f"""{items["Description"]} 142 | Negative prompt: {json_info["uc"]} 143 | Steps: {json_info["steps"]}, Sampler: {sampler}, CFG scale: {json_info["scale"]}, Seed: {json_info["seed"]}, Size: {image.width}x{image.height}, Clip skip: 2, ENSD: 31337""" 144 | except Exception: 145 | print("Error parsing NovelAI image generation parameters:", file=sys.stderr) 146 | print(traceback.format_exc(), file=sys.stderr) 147 | 148 | return geninfo, items 149 | 150 | def infotext(p, iteration=0, position_in_batch=0, comments={}): 151 | if p.all_prompts == None: 152 | p.all_prompts = [p.prompt] 153 | p.all_prompts = [p.prompt] 154 | if p.all_negative_prompts == None: 155 | p.all_negative_prompts = [p.negative_prompt] 156 | p.all_negative_prompts = [p.negative_prompt] 157 | if p.all_seeds == None: 158 | p.all_seeds = [p.seed] 159 | if p.all_subseeds == None: 160 | p.all_subseeds = [p.subseed] 161 | return create_infotext(p, p.all_prompts, p.all_seeds, p.all_subseeds, comments, iteration, position_in_batch) 162 | -------------------------------------------------------------------------------- /scripts/face_detect.py: -------------------------------------------------------------------------------- 1 | FaceDetectDevelopment = False # set to True enable the development panel UI 2 | 3 | from modules import images 4 | from modules.shared import opts 5 | from modules.paths import models_path 6 | from modules.textual_inversion import autocrop 7 | 8 | from PIL import Image, ImageOps 9 | 10 | import mediapipe as mp 11 | import numpy as np 12 | import math 13 | import cv2 14 | import sys 15 | import os 16 | from enum import IntEnum 17 | 18 | class FaceMode(IntEnum): 19 | # these can be in any order, but they must range from 0..N with no gaps 20 | ORIGINAL=0, 21 | OPENCV_NORMAL=1 22 | OPENCV_SLOW=2 23 | OPENCV_SLOWEST=3 24 | YUNET=4 25 | DEVELOPMENT=5 # must be highest numbered to avoid breaking UI 26 | 27 | DEFAULT=4 # the one the UI defaults to 28 | 29 | # for the UI dropdown, we want an array that's indexed by the above numbers, and we don't want 30 | # bugs if you tweak them and don't keep the array in sync, so initialize the array explicitly 31 | # using them as indices: 32 | 33 | face_mode_init = [ 34 | (FaceMode.ORIGINAL , "Fastest (mediapipe, max 5 faces)"), 35 | (FaceMode.OPENCV_NORMAL , "Normal (OpenCV + mediapipe)"), 36 | (FaceMode.OPENCV_SLOW , "Slow (OpenCV + mediapipe)"), 37 | (FaceMode.OPENCV_SLOWEST, "Extremely slow (OpenCV + mediapipe)"), 38 | (FaceMode.YUNET, "YuNet (YuNet + mediapipe)") 39 | ] 40 | if FaceDetectDevelopment: 41 | face_mode_init.append((FaceMode.DEVELOPMENT, "Development testing")) 42 | 43 | face_mode_names = [None] * len(face_mode_init) 44 | for index,name in face_mode_init: 45 | face_mode_names[index] = name 46 | 47 | class FaceDetectConfig: 48 | 49 | def __init__(self, faceMode, face_x_scale=None, face_y_scale=0, minFace=0, multiScale=0, multiScale2=0, multiScale3=0, minNeighbors=0, mpconfidence=0, mpcount=0, debugSave=0, optimizeDetect=0): 50 | self.faceMode = faceMode 51 | self.face_x_scale = face_x_scale 52 | self.face_y_scale = face_y_scale 53 | self.minFaceSize = int(minFace) 54 | self.multiScale = multiScale 55 | self.multiScale2 = multiScale2 56 | self.multiScale3 = multiScale3 57 | self.minNeighbors = int(minNeighbors) 58 | self.mpconfidence = mpconfidence 59 | self.mpcount = mpcount 60 | self.debugSave = debugSave 61 | self.optimizeDetect = optimizeDetect 62 | 63 | # allow this function to be called with just faceMode (used by updateVisualizer) 64 | # or with an explicit list of values (from the main UI) but throw away those values 65 | # if we're not in development 66 | # 67 | # for the "just faceMode" version we could put most of these as default arguments, 68 | # but then we'd have to maintain the defaults both above and below, so easier on 69 | # development to only put the correct defaults below: 70 | 71 | if not FaceDetectDevelopment or (face_x_scale is None): 72 | # If not in development mode, override all passed-in parameters to defaults 73 | # also, if called from UpdateVisualizer, all the arguments will be default values 74 | # we use None/0 in the parameter list above so we don't have to update the default values in two places in the code 75 | self.face_x_scale=4.0 76 | self.face_y_scale=2.5 77 | self.minFace=30 78 | self.multiScale=1.03 79 | self.multiScale2=1 80 | self.multiScale3=1 81 | self.minNeighbors=5 82 | self.mpconfidence=0.5 83 | self.mpcount=5 84 | self.debugSave=False 85 | self.optimizeDetect=False 86 | 87 | if True: # disable this to alter these modes specifically 88 | if faceMode == FaceMode.OPENCV_NORMAL: 89 | self.multiScale = 1.1 90 | elif faceMode == FaceMode.OPENCV_SLOW: 91 | self.multiScale = 1.01 92 | elif faceMode == FaceMode.OPENCV_SLOWEST: 93 | self.multiScale = 1.003 94 | 95 | def getFacialLandmarks(image, facecfg): 96 | height, width, _ = image.shape 97 | mp_face_mesh = mp.solutions.face_mesh 98 | with mp_face_mesh.FaceMesh(static_image_mode=True,max_num_faces=facecfg.mpcount,min_detection_confidence=facecfg.mpconfidence) as face_mesh: 99 | height, width, _ = image.shape 100 | image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) 101 | result = face_mesh.process(image_rgb) 102 | 103 | facelandmarks = [] 104 | if result.multi_face_landmarks is not None: 105 | for facial_landmarks in result.multi_face_landmarks: 106 | landmarks = [] 107 | for i in range(0, 468): 108 | pt1 = facial_landmarks.landmark[i] 109 | x = int(pt1.x * width) 110 | y = int(pt1.y * height) 111 | landmarks.append([x, y]) 112 | #cv2.circle(image, (x, y), 2, (100,100,0), -1) 113 | #cv2.imshow("Cropped", image) 114 | facelandmarks.append(np.array(landmarks, np.int32)) 115 | 116 | return facelandmarks 117 | 118 | def computeFaceInfo(landmark, onlyHorizontal, divider, small_width, small_height, small_image_index): 119 | x_chin = landmark[152][0] 120 | y_chin = -landmark[152][1] 121 | x_forehead = landmark[10][0] 122 | y_forehead = -landmark[10][1] 123 | 124 | deltaX = x_forehead - x_chin 125 | deltaY = y_forehead - y_chin 126 | 127 | face_angle = math.atan2(deltaY, deltaX) * 180 / math.pi 128 | 129 | # compute center in global coordinates in case the image was split 130 | if onlyHorizontal == True: 131 | x = ((small_image_index // divider) * small_width ) + landmark[0][0] 132 | y = ((small_image_index % divider) * small_height) + landmark[0][1] 133 | else: 134 | x = ((small_image_index % divider) * small_width ) + landmark[0][0] 135 | y = ((small_image_index // divider) * small_height) + landmark[0][1] 136 | 137 | return { "angle": face_angle, "center": (x,y) } 138 | 139 | # try to get landmarks for a face located at rect 140 | def getFacialLandmarkConvexHull(image, rect, onlyHorizontal, divider, small_width, small_height, small_image_index, facecfg): 141 | image = np.array(image) 142 | height, width, channels = image.shape 143 | 144 | # make a subimage to hand to FaceMesh 145 | (x,y,w,h) = rect 146 | 147 | face_center_x = (x + w//2) 148 | face_center_y = (y + h//2) 149 | 150 | # the new image is just 2x the size of the face 151 | subrect_width = int(float(w) * facecfg.face_x_scale) 152 | subrect_height = int(float(h) * facecfg.face_y_scale) 153 | subrect_halfwidth = subrect_width // 2 154 | subrect_halfheight = subrect_height // 2 155 | subrect_width = subrect_halfwidth * 2; 156 | subrect_height = subrect_halfheight * 2; 157 | subrect_x_center = face_center_x 158 | subrect_y_center = face_center_y 159 | 160 | subimage = np.zeros((subrect_height, subrect_width, channels), np.uint8) 161 | 162 | # this is the coordinates of the top left of the subimage relative to the original image 163 | subrect_x0 = subrect_x_center - subrect_halfwidth 164 | subrect_y0 = subrect_y_center - subrect_halfheight 165 | 166 | # we allow room for up to 1/2 of a face adjacent 167 | crop_face_x0 = face_center_x - w 168 | crop_face_x1 = face_center_x + w 169 | crop_face_y0 = face_center_y - h 170 | crop_face_y1 = face_center_y + h 171 | 172 | crop_face_x0 = max(crop_face_x0, 0 ) 173 | crop_face_y0 = max(crop_face_y0, 0) 174 | crop_face_x1 = min(crop_face_x1, width ) 175 | crop_face_y1 = min(crop_face_y1, height) 176 | 177 | # now crop the face coordinates down to the subrect as well 178 | crop_face_x0 = max(crop_face_x0, subrect_x0) 179 | crop_face_y0 = max(crop_face_y0, subrect_y0) 180 | crop_face_x1 = min(crop_face_x1, subrect_x0 + subrect_width ); 181 | crop_face_y1 = min(crop_face_y1, subrect_y0 + subrect_height); 182 | 183 | face_image = image[crop_face_y0:crop_face_y1, crop_face_x0:crop_face_x1] 184 | 185 | # by construction the face image can't be larger than the subrect, but it can be smaller 186 | subimage[crop_face_y0-subrect_y0:crop_face_y1-subrect_y0, crop_face_x0-subrect_x0:crop_face_x1-subrect_x0] = face_image 187 | 188 | # store the face box in these coordinates for later use 189 | face_rect = (x - subrect_x0, 190 | y - subrect_y0, 191 | w, 192 | h) 193 | 194 | # final face bounding rect must overlap at least 1/4 of pixels that CV2 expected, or it's not a match 195 | min_match_area = w * h // 4; 196 | 197 | landmarks = getFacialLandmarks(subimage, facecfg) 198 | mp_face_mesh = mp.solutions.face_mesh 199 | 200 | best_hull = None 201 | best_landmark = None 202 | best_area = min_match_area 203 | 204 | for landmark in landmarks: 205 | face_info = {} 206 | convexhull = cv2.convexHull(landmark) 207 | bounds = cv2.boundingRect(convexhull) 208 | # compute intersection with face_rect 209 | x0 = max(face_rect[0], bounds[0]) 210 | y0 = max(face_rect[1], bounds[1]) 211 | x1 = min(face_rect[0] + face_rect[2], bounds[0] + bounds[2]) 212 | y1 = min(face_rect[1] + face_rect[3], bounds[1] + bounds[3]) 213 | area = (x1-x0) * (y1-y0) 214 | 215 | if area > best_area: 216 | best_area = area 217 | best_hull = convexhull 218 | best_landmark = landmark 219 | 220 | face_info = None 221 | if best_hull is not None: 222 | # translate the convex hull back into the coordinate space of the passed-in image 223 | for i in range(len(best_hull)): 224 | best_hull[i][0][0] += subrect_x0 225 | best_hull[i][0][1] += subrect_y0 226 | 227 | # compute face_info and translate it back into the coordinate space 228 | face_info = computeFaceInfo(best_landmark, onlyHorizontal, divider, small_width, small_height, small_image_index) 229 | face_info["center"] = (face_info["center"][0] + subrect_x0, face_info["center"][1] + subrect_y0) 230 | 231 | return best_hull, face_info 232 | 233 | def contractRect(r): 234 | (x0,y0,w,h) = r 235 | hw,hh = w/2, h/2 236 | xc,yc = x0+hw, y0+hh 237 | x0 = xc - hw*0.85 238 | y0 = yc - hh*0.85 239 | x1 = xc + hw*0.85 240 | y1 = yc + hh*0.85 241 | return (x0,y0,x1,y1) 242 | 243 | def rectangleListOverlap(rlist, rect): 244 | # contract rect slightly 245 | rx0,ry0,rx1,ry1 = contractRect(rect) 246 | 247 | for r in rlist: 248 | x0,y0,x1,y1 = contractRect(r) 249 | if x0 < rx1 and x1 > rx0 and y0 < ry1 and y1 > ry0: 250 | return r 251 | return None 252 | 253 | # use cv2 detectMultiScale directly 254 | def getFaceRectanglesSimple(image, known_face_rects, facecfg): 255 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 256 | face_cascade = cv2.CascadeClassifier("extensions/batch-face-swap/scripts/haarcascade_frontalface_default.xml") 257 | faces = face_cascade.detectMultiScale(gray, scaleFactor=facecfg.multiScale, minNeighbors=facecfg.minNeighbors, minSize=(facecfg.minFaceSize,facecfg.minFaceSize)) 258 | 259 | all_faces = [] 260 | for r in faces: 261 | if rectangleListOverlap(known_face_rects, r) is None: 262 | all_faces.append(r) 263 | return all_faces 264 | 265 | 266 | # use cv2 detectMultiScale at multiple scales 267 | def getFaceRectangles(image, known_face_rects, facecfg): 268 | if not facecfg.optimizeDetect: 269 | return getFaceRectanglesSimple(image, known_face_rects, facecfg) 270 | 271 | height, width, _ = image.shape 272 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 273 | face_cascade = cv2.CascadeClassifier("extensions/batch-face-swap/scripts/haarcascade_frontalface_default.xml") 274 | all_faces = [] 275 | 276 | minsize = facecfg.minFaceSize 277 | # naiveScale is the scale between successive detections 278 | naiveScale = facecfg.multiScale 279 | partition_count = int(facecfg.multiScale2+0.5) 280 | partition_count = max(partition_count, 1) 281 | effectiveScale = math.pow(naiveScale, partition_count) 282 | 283 | current = gray 284 | total_scale = 1 285 | resize_scale = naiveScale 286 | 287 | for i in range(0,partition_count): 288 | faces = face_cascade.detectMultiScale(current, scaleFactor=effectiveScale, minNeighbors=facecfg.minNeighbors, minSize=(minsize,minsize)) 289 | new_faces = [] 290 | for rorig in faces: 291 | r = [ int(rorig[0] * total_scale), int(rorig[1] * total_scale), int(rorig[2] * total_scale), int(rorig[3] * total_scale) ] 292 | if rectangleListOverlap(known_face_rects, r) is None: 293 | if rectangleListOverlap(all_faces, r) is None: 294 | new_faces.append(r) 295 | 296 | all_faces.extend(new_faces) 297 | 298 | total_scale *= resize_scale 299 | width = int(width / total_scale) 300 | height = int(height / total_scale) 301 | current = cv2.resize(gray, (width, height), interpolation=cv2.INTER_LANCZOS4) 302 | 303 | return all_faces 304 | 305 | def getFaceRectanglesYuNet(img_array, known_face_rects): 306 | new_faces = [] 307 | dnn_model_path = autocrop.download_and_cache_models(os.path.join(models_path, "opencv")) 308 | face_detector = cv2.FaceDetectorYN.create(dnn_model_path, "", (0, 0)) 309 | 310 | face_detector.setInputSize((img_array.shape[1], img_array.shape[0])) 311 | _, faces = face_detector.detect(img_array) 312 | 313 | if faces is None: 314 | return new_faces 315 | 316 | face_coords = [] 317 | for face in faces: 318 | if math.isinf(face[0]): 319 | continue 320 | x = int(face[0]) 321 | y = int(face[1]) 322 | w = int(face[2]) 323 | h = int(face[3]) 324 | if w == 0 or h == 0: 325 | print("ignore w,h = 0 face") 326 | continue 327 | 328 | face_coords.append( [ x, y, w, h ] ) 329 | 330 | for r in face_coords: 331 | if rectangleListOverlap(known_face_rects, r) is None: 332 | new_faces.append(r) 333 | 334 | return new_faces 335 | 336 | ##################################################################################################################### 337 | # 338 | # various attempts at optimizing. some of them were better, but a lot more 339 | # work is needed to get something good 340 | # 341 | # note that these are not fully compatible with the rest of the code, 342 | # as they've changed since it was written 343 | # 344 | # 345 | 346 | 347 | def getFaceRectangles4(image, facecfg): 348 | if not facecfg.optimizeDetect: 349 | return getFaceRectanglesSimple(image, facecfg) 350 | 351 | height, width, _ = image.shape 352 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 353 | face_cascade = cv2.CascadeClassifier("extensions/batch-face-swap/scripts/haarcascade_frontalface_default.xml") 354 | all_faces = [] 355 | 356 | resize_scale = facecfg.multiScale 357 | trueScale = facecfg.multiScale2 358 | overlapScale = facecfg.multiScale3 359 | 360 | size = min(width,height) 361 | minsize = facecfg.minFaceSize 362 | 363 | # run the smallest detectors from 30..60 364 | current = gray 365 | total_scale = 1.0 366 | #trueScale = trueScale * trueScale 367 | while current.shape[0] > minsize and current.shape[1] > minsize: 368 | height, width = current.shape[0], current.shape[1] 369 | maxsize = int(minsize * resize_scale * overlapScale) 370 | faces = face_cascade.detectMultiScale(current, scaleFactor=trueScale, minNeighbors=facecfg.minNeighbors, minSize=(minsize,minsize), maxSize=(maxsize,maxsize)) 371 | new_faces = [] 372 | for rorig in faces: 373 | r = [ int(rorig[0] * total_scale), int(rorig[1] * total_scale), int(rorig[2] * total_scale), int(rorig[3] * total_scale) ] 374 | 375 | # clamp detected shape back to range, this shou'dn't be necessary 376 | orig = r.copy() 377 | if r[0]+r[2] > gray.shape[1]: 378 | excess = r[0] + r[2] - gray.shape[1] 379 | r[2] -= excess//2 380 | r[0] = gray.shape[1] - r[2] 381 | if r[1]+r[3] > gray.shape[0]: 382 | excess = r[1] + r[3] - gray.shape[0] 383 | r[3] -= excess//2 384 | r[1] = gray.shape[0] - r[3] 385 | if orig[0] != r[0] or orig[1] != r[1] or orig[2] != r[2] or orig[3] != r[3]: 386 | print( "Clamped bad rect from " + str(orig) + " to " + str(r) + " for image size " + str((gray.shape[1],gray.shape[0]))) 387 | overlap = rectangleListOverlap(all_faces, r) 388 | if overlap is None: 389 | new_faces.append((r, (minsize*total_scale,maxsize*total_scale))) 390 | 391 | all_faces.extend(new_faces) 392 | 393 | width = int(width / resize_scale) 394 | height = int(height / resize_scale) 395 | current = cv2.resize(gray, (width, height), interpolation=cv2.INTER_AREA) 396 | total_scale *= resize_scale 397 | 398 | if width < 600 and height < 600: 399 | resize_scale = 10 400 | trueScale = math.pow(facecfg.multiScale2,0.5) 401 | else: 402 | trueScale = facecfg.multiScale2 403 | 404 | for i in range(0,len(all_faces)): 405 | r,_ = all_faces[i] 406 | all_faces[i] = r 407 | 408 | return all_faces 409 | 410 | # use cv2 detectMultiScale at multiple scales 411 | def getFaceRectangles3(image, facecfg): 412 | if facecfg.multiScale <= 1.1: 413 | return getFaceRectanglesSimple(image, facecfg) 414 | 415 | height, width, _ = image.shape 416 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 417 | face_cascade = cv2.CascadeClassifier("extensions/batch-face-swap/scripts/haarcascade_frontalface_default.xml") 418 | all_faces = [] 419 | 420 | size = min(width,height) 421 | 422 | while size >= facecfg.minFaceSize: 423 | maxsize = size 424 | minsize = int(size / facecfg.multiScale) 425 | 426 | # make sure there's some overlap 427 | maxsize = int(maxsize*1.1 + 1) 428 | minsiez = int(minsize/1.1 - 1) 429 | 430 | ratio = float(maxsize) / float(minsize) 431 | stepCount = (maxsize - minsize) * facecfg.multiScale2 432 | scale = math.exp(math.log(ratio) / (stepCount+1)) 433 | 434 | print( f"Compute scale factor {scale} to go from {minsize} to {maxsize} in {stepCount} steps" ) 435 | faces = face_cascade.detectMultiScale(gray, scaleFactor=scale, minNeighbors=facecfg.minNeighbors, minSize=(minsize,minsize), maxSize=(maxsize,maxsize)) 436 | new_faces = [] 437 | for r in faces: 438 | overlap = rectangleListOverlap(all_faces, r) 439 | if overlap is None: 440 | new_faces.append((r, (minsize,maxsize))) 441 | all_faces.extend(new_faces) 442 | size = minsize 443 | 444 | for i in range(0,len(all_faces)): 445 | r,_ = all_faces[i] 446 | all_faces[i] = r 447 | return all_faces 448 | 449 | # use cv2 detectMultiScale at multiple scales 450 | def getFaceRectangles2(image, facecfg): 451 | if facecfg.multiScale < 1.5: 452 | return getFaceRectanglesSimple(image, facecfg) 453 | 454 | height, width, _ = image.shape 455 | gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 456 | face_cascade = cv2.CascadeClassifier("extensions/batch-face-swap/scripts/haarcascade_frontalface_default.xml") 457 | all_faces = [] 458 | stepSize = int(facecfg.multiScale) 459 | for size in range(facecfg.minFaceSize, min(width,height), stepSize): 460 | 461 | # detect faces in the range size..stepSize 462 | minsize = size 463 | maxsize = size + stepSize+1 464 | 465 | ratio = float(maxsize) / float(size) 466 | 467 | # pick a scale factor so we take about stepSize*1.2 steps to go from size to maxsize 468 | # scale^(stepsize*1.2) = ratio 469 | # (stepsize*1.2) * log(scale) = log(ratio) 470 | # log(scale) = log(ratio) / (stepsize * 1.2) 471 | scale = math.exp(math.log(ratio) / (float(stepSize)*1.1)) 472 | 473 | faces = face_cascade.detectMultiScale(gray, scaleFactor=scale, minNeighbors=facecfg.minNeighbors, minSize=(minsize,minsize), maxSize=(maxsize,maxsize)) 474 | new_faces = [] 475 | for r in faces: 476 | overlap = rectangleListOverlap(all_faces, r) 477 | if overlap is None: 478 | new_faces.append((r, (minsize,maxsize))) 479 | #else: 480 | #(rect,size) = overlap 481 | #print( "Duplicate face: " + str(r) + " in size range " + str((minsize,maxsize)) + " overlapped with " + str(rect) + " from size range " + str(size)) 482 | all_faces.extend(new_faces) 483 | 484 | for i in range(0,len(all_faces)): 485 | r,_ = all_faces[i] 486 | all_faces[i] = r 487 | return all_faces 488 | -------------------------------------------------------------------------------- /scripts/sd_helpers.py: -------------------------------------------------------------------------------- 1 | from modules.processing import ( 2 | process_images, 3 | StableDiffusionProcessingTxt2Img, 4 | StableDiffusionProcessingImg2Img, 5 | ) 6 | import modules.shared as shared 7 | 8 | 9 | def renderTxt2Img( 10 | prompt, 11 | negative_prompt, 12 | sampler, 13 | steps, 14 | cfg_scale, 15 | seed, 16 | width, 17 | height, 18 | batch_size, 19 | n_iter, 20 | do_not_save_samples, 21 | ): 22 | processed = None 23 | p = StableDiffusionProcessingTxt2Img( 24 | sd_model=shared.sd_model, 25 | outpath_samples=shared.opts.outdir_txt2img_samples, 26 | outpath_grids=shared.opts.outdir_txt2img_grids, 27 | prompt=prompt, 28 | negative_prompt=negative_prompt, 29 | seed=seed, 30 | sampler_name=sampler, 31 | steps=steps, 32 | cfg_scale=cfg_scale, 33 | width=width, 34 | height=height, 35 | batch_size=batch_size, 36 | n_iter=n_iter, 37 | do_not_save_samples=do_not_save_samples 38 | ) 39 | processed = process_images(p) 40 | return processed 41 | 42 | 43 | def renderImg2Img( 44 | prompt, 45 | negative_prompt, 46 | sampler, 47 | steps, 48 | cfg_scale, 49 | seed, 50 | width, 51 | height, 52 | init_image, 53 | mask_image, 54 | batch_size, 55 | n_iter, 56 | inpainting_denoising_strength, 57 | inpainting_mask_blur, 58 | inpainting_fill_mode, 59 | inpainting_full_res, 60 | inpainting_padding, 61 | do_not_save_samples, 62 | ): 63 | processed = None 64 | 65 | p = StableDiffusionProcessingImg2Img( 66 | sd_model=shared.sd_model, 67 | outpath_samples=shared.opts.outdir_img2img_samples, 68 | outpath_grids=shared.opts.outdir_img2img_grids, 69 | prompt=prompt, 70 | negative_prompt=negative_prompt, 71 | seed=seed, 72 | sampler_name=sampler, 73 | n_iter=n_iter, 74 | batch_size=batch_size, 75 | steps=steps, 76 | cfg_scale=cfg_scale, 77 | width=width, 78 | height=height, 79 | init_images=[init_image], 80 | mask=mask_image, 81 | denoising_strength=inpainting_denoising_strength, 82 | mask_blur=inpainting_mask_blur, 83 | inpainting_fill=inpainting_fill_mode, 84 | inpaint_full_res=inpainting_full_res, 85 | inpaint_full_res_padding=inpainting_padding, 86 | do_not_save_samples=do_not_save_samples, 87 | ) 88 | # p.latent_mask = Image.new("RGB", (p.width, p.height), "white") 89 | 90 | processed = process_images(p) 91 | return processed 92 | --------------------------------------------------------------------------------