From de84e022eed8f477c5736a9c1b2438a337066a1f Mon Sep 17 00:00:00 2001 From: jiveabillion Date: Mon, 27 Nov 2023 12:23:44 -0500 Subject: [PATCH 1/3] Added Masking Tab Added Masking tab to allow further configuration of the masking feature. Added ability to save the masked face as a separate file with alpha transparency for easy editing in photoshop in a layer over the original --- scripts/inferencers/bisenet_mask_generator.py | 13 +- .../inferencers/vignette_mask_generator.py | 50 +++++++ scripts/reactor_faceswap.py | 133 +++++++++++++++--- scripts/reactor_swapper.py | 70 ++++----- 4 files changed, 213 insertions(+), 53 deletions(-) create mode 100644 scripts/inferencers/vignette_mask_generator.py diff --git a/scripts/inferencers/bisenet_mask_generator.py b/scripts/inferencers/bisenet_mask_generator.py index 27eb2e7..79ab9dd 100644 --- a/scripts/inferencers/bisenet_mask_generator.py +++ b/scripts/inferencers/bisenet_mask_generator.py @@ -4,16 +4,19 @@ import cv2 import modules.shared as shared import numpy as np import torch +from scripts.reactor_logger import logger from facexlib.parsing import init_parsing_model from facexlib.utils.misc import img2tensor from torchvision.transforms.functional import normalize from PIL import Image +from scripts.inferencers.vignette_mask_generator import VignetteMaskGenerator from scripts.inferencers.mask_generator import MaskGenerator -from scripts.reactor_logger import logger + class BiSeNetMaskGenerator(MaskGenerator): def __init__(self) -> None: self.mask_model = init_parsing_model(device=shared.device) + self.fallback_mask_generator = VignetteMaskGenerator() def name(self): return "BiSeNet" @@ -25,7 +28,7 @@ class BiSeNetMaskGenerator(MaskGenerator): affected_areas: List[str], mask_size: int, use_minimal_area: bool, - fallback_ratio: float = 0.25, + fallback_ratio: float = 0.10, **kwargs, ) -> np.ndarray: original_face_image = face_image @@ -59,11 +62,11 @@ class BiSeNetMaskGenerator(MaskGenerator): if w != 512 or h != 512: mask = cv2.resize(mask, dsize=(w, h)) - """if MaskGenerator.calculate_mask_coverage(mask) < fallback_ratio: - logger.info("Use fallback mask generator") + if MaskGenerator.calculate_mask_coverage(mask) < fallback_ratio: + logger.status(F"Mask coverage less than fallback ratio of {fallback_ratio}. Using vignette mask generator.") mask = self.fallback_mask_generator.generate_mask( original_face_image, face_area_on_image, use_minimal_area=True - )""" + ) return mask diff --git a/scripts/inferencers/vignette_mask_generator.py b/scripts/inferencers/vignette_mask_generator.py new file mode 100644 index 0000000..71d3204 --- /dev/null +++ b/scripts/inferencers/vignette_mask_generator.py @@ -0,0 +1,50 @@ +from typing import Tuple + +import cv2 +import numpy as np + +from scripts.inferencers.mask_generator import MaskGenerator + + +class VignetteMaskGenerator(MaskGenerator): + def name(self): + return "Vignette" + + def generate_mask( + self, + face_image: np.ndarray, + face_area_on_image: Tuple[int, int, int, int], + use_minimal_area: bool, + sigma: float = -1, + keep_safe_area: bool = False, + **kwargs, + ) -> np.ndarray: + (left, top, right, bottom) = face_area_on_image + w, h = right - left, bottom - top + mask = np.zeros((face_image.shape[0], face_image.shape[1]), dtype=np.uint8) + if use_minimal_area: + sigma = 120 if sigma == -1 else sigma + mask[top : top + h, left : left + w] = 255 + else: + sigma = 180 if sigma == -1 else sigma + h, w = face_image.shape[0], face_image.shape[1] + mask[:, :] = 255 + + Y = np.linspace(0, h, h, endpoint=False) + X = np.linspace(0, w, w, endpoint=False) + Y, X = np.meshgrid(Y, X) + Y -= h / 2 + X -= w / 2 + + gaussian = np.exp(-(X**2 + Y**2) / (2 * sigma**2)) + gaussian_mask = np.uint8(255 * gaussian.T) + if use_minimal_area: + mask[top : top + h, left : left + w] = gaussian_mask + else: + mask[:, :] = gaussian_mask + + if keep_safe_area: + mask = cv2.ellipse(mask, ((left + right) // 2, (top + bottom) // 2), (w // 2, h // 2), 0, 0, 360, 255, -1) + + mask = cv2.cvtColor(mask, cv2.COLOR_GRAY2RGB) + return mask diff --git a/scripts/reactor_faceswap.py b/scripts/reactor_faceswap.py index 0c54371..f2258ba 100644 --- a/scripts/reactor_faceswap.py +++ b/scripts/reactor_faceswap.py @@ -28,13 +28,14 @@ except: model_path = os.path.abspath("models") from scripts.reactor_logger import logger -from scripts.reactor_swapper import EnhancementOptions, swap_face, check_process_halt, reset_messaged +from scripts.reactor_swapper import EnhancementOptions,MaskOptions,MaskOption, swap_face, check_process_halt, reset_messaged from scripts.reactor_version import version_flag, app_title from scripts.console_log_patch import apply_logging_patch from scripts.reactor_helpers import make_grid, get_image_path, set_Device from scripts.reactor_globals import DEVICE, DEVICE_LIST + MODELS_PATH = None def get_models(): @@ -66,8 +67,8 @@ class FaceSwapScript(scripts.Script): img = gr.Image(type="pil") enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") save_original = gr.Checkbox(False, label="Save Original", info="Save the original image(s) made before swapping; If you use \"img2img\" - this option will affect with \"Swap in generated\" only") - mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours.") - + mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") + gr.Markdown("
") gr.Markdown("Source Image (above):") with gr.Row(): @@ -140,6 +141,24 @@ class FaceSwapScript(scripts.Script): upscaler_visibility = gr.Slider( 0, 1, 1, step=0.1, label="Upscaler Visibility (if scale = 1)" ) + with gr.Tab("Masking"): + save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") + mask_areas = gr.CheckboxGroup( + label="Mask areas", choices=["Face", "Hair", "Hat", "Neck"], type="value", value= MaskOption.DEFAULT_FACE_AREAS + ) + mask_blur = gr.Slider(label="Mask blur ", minimum=0, maximum=64, step=1, value=MaskOption.DEFAULT_MASK_BLUR,info="The number of pixels from the outer edge of the mask to blur.") + face_size = gr.Radio( + label = "Face Size", choices = [512,256,128],value=MaskOption.DEFAULT_FACE_SIZE,type="value", info="Size of the masked area. Use larger numbers if the face is expected to be large, smaller if small. Default is 512." + ) + mask_vignette_fallback_threshold = gr.Slider( + minimum=0.1, + maximum=1.0, + step=0.01, + value=MaskOption.DEFAULT_VIGNETTE_THRESHOLD, + label="Vignette fallback threshold", + info="Switch to a rectangular vignette mask when masked area is only this specified percentage of Face Size." + ) + use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") with gr.Tab("Settings"): models = get_models() with gr.Row(visible=EP_is_visible): @@ -213,7 +232,14 @@ class FaceSwapScript(scripts.Script): source_hash_check, target_hash_check, device, - mask_face + mask_face, + save_face_mask, + mask_areas, + mask_blur, + use_minimal_area, + face_size, + mask_vignette_fallback_threshold, + ] @@ -242,7 +268,17 @@ class FaceSwapScript(scripts.Script): restorer_visibility=self.face_restorer_visibility, codeformer_weight=self.codeformer_weight, ) - + @property + def mask_options(self) -> MaskOptions: + return MaskOptions( + mask_areas = self.mask_areas, + save_face_mask = self.save_face_mask, + mask_blur = self.mask_blur, + face_size = self.mask_face_size, + vignette_fallback_threshold = self.mask_vignette_fallback_threshold, + use_minimal_area = self.mask_use_minimal_area, + ) + def process( self, p: StableDiffusionProcessing, @@ -267,7 +303,14 @@ class FaceSwapScript(scripts.Script): source_hash_check, target_hash_check, device, - mask_face + mask_face, + save_face_mask, + mask_areas, + mask_blur, + mask_use_minimal_area, + mask_face_size, + mask_vignette_fallback_threshold, + ): self.enable = enable if self.enable: @@ -296,6 +339,12 @@ class FaceSwapScript(scripts.Script): self.target_hash_check = target_hash_check self.device = device self.mask_face = mask_face + self.save_face_mask = save_face_mask + self.mask_blur = mask_blur + self.mask_areas = mask_areas + self.mask_face_size = mask_face_size + self.mask_vignette_fallback_threshold = mask_vignette_fallback_threshold + self.mask_use_minimal_area = mask_use_minimal_area if self.gender_source is None or self.gender_source == "No": self.gender_source = 0 if self.gender_target is None or self.gender_target == "No": @@ -318,9 +367,10 @@ class FaceSwapScript(scripts.Script): self.target_hash_check = False set_Device(self.device) - + logger.status(f"Self: {self}") if self.source is not None: apply_logging_patch(console_logging_level) + if isinstance(p, StableDiffusionProcessingImg2Img) and self.swap_in_source: logger.status("Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index) @@ -339,7 +389,8 @@ class FaceSwapScript(scripts.Script): source_hash_check=self.source_hash_check, target_hash_check=self.target_hash_check, device=self.device, - mask_face=mask_face + mask_face=mask_face, + mask_options=self.mask_options ) p.init_images[i] = result # result_path = get_image_path(p.init_images[i], p.outpath_samples, "", p.all_seeds[i], p.all_prompts[i], "txt", p=p, suffix="-swapped") @@ -373,13 +424,14 @@ class FaceSwapScript(scripts.Script): if self.swap_in_generated: logger.status("Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index) if self.source is not None: + for i,(img,info) in enumerate(zip(orig_images, orig_infotexts)): if check_process_halt(): postprocess_run = False break if len(orig_images) > 1: logger.status("Swap in %s", i) - result, output, swapped = swap_face( + result, output, swapped, masked_faces = swap_face( self.source, img, source_faces_index=self.source_faces_index, @@ -391,7 +443,8 @@ class FaceSwapScript(scripts.Script): source_hash_check=self.source_hash_check, target_hash_check=self.target_hash_check, device=self.device, - mask_face=self.mask_face + mask_face=self.mask_face, + mask_options=self.mask_options ) if result is not None and swapped > 0: result_images.append(result) @@ -400,6 +453,14 @@ class FaceSwapScript(scripts.Script): img_path = save_image(result, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) except: logger.error("Cannot save a result image - please, check SD WebUI Settings (Saving and Paths)") + if self.mask_face and self.save_face_mask and masked_faces is not None: + result_images.append(masked_faces) + suffix = "-mask" + try: + img_path = save_image(masked_faces, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) + except: + logger.error("Cannot save a Masked Face image - please, check SD WebUI Settings (Saving and Paths)") + elif result is None: logger.error("Cannot create a result image") @@ -476,7 +537,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): with gr.Column(): img = gr.Image(type="pil") enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") - mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours.") + mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") gr.Markdown("Source Image (above):") with gr.Row(): @@ -536,6 +597,25 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): upscaler_visibility = gr.Slider( 0, 1, 1, step=0.1, label="Upscaler Visibility (if scale = 1)" ) + with gr.Tab("Masking"): + save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") + mask_areas = gr.CheckboxGroup( + label="Mask areas", choices=["Face", "Hair", "Hat", "Neck"], type="value", value= MaskOption.DEFAULT_FACE_AREAS + ) + mask_blur = gr.Slider(label="Mask blur ", minimum=0, maximum=64, step=1, value=MaskOption.DEFAULT_MASK_BLUR,info="The number of pixels from the outer edge of the mask to blur.") + face_size = gr.Radio( + label = "Face Size", choices = [512,256,128],value=MaskOption.DEFAULT_FACE_SIZE,type="value", info="Size of the masked area. Use larger numbers if the face is expected to be large, smaller if small. Default is 512." + ) + mask_vignette_fallback_threshold = gr.Slider( + minimum=0.1, + maximum=1.0, + step=0.01, + value=MaskOption.DEFAULT_VIGNETTE_THRESHOLD, + label="Vignette fallback threshold", + info="Switch to a rectangular vignette mask when masked area is only this specified percentage of Face Size." + ) + use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") + with gr.Tab("Settings"): models = get_models() with gr.Row(visible=EP_is_visible): @@ -592,7 +672,13 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): 'gender_target': gender_target, 'codeformer_weight': codeformer_weight, 'device': device, - 'mask_face':mask_face + 'mask_face':mask_face, + 'save_face_mask':save_face_mask, + 'mask_areas':mask_areas, + 'mask_blur':mask_blur, + 'mask_vignette_fallback_threshold':mask_vignette_fallback_threshold, + 'face_size':face_size, + 'use_minimal_area':use_minimal_area, } return args @@ -621,13 +707,21 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): restorer_visibility=self.face_restorer_visibility, codeformer_weight=self.codeformer_weight, ) - + @property + def mask_options(self) -> MaskOptions: + return MaskOptions( + mask_areas = self.mask_areas, + save_face_mask = self.save_face_mask, + mask_blur = self.mask_blur, + face_size = self.mask_face_size, + vignette_fallback_threshold = self.mask_vignette_fallback_threshold, + use_minimal_area = self.mask_use_minimal_area, + ) def process(self, pp: scripts_postprocessing.PostprocessedImage, **args): if args['enable']: reset_messaged() if check_process_halt(): - return - + return global MODELS_PATH self.source = args['img'] self.face_restorer_name = args['face_restorer_name'] @@ -643,6 +737,12 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): self.codeformer_weight = args['codeformer_weight'] self.device = args['device'] self.mask_face = args['mask_face'] + self.save_face_mask = args['save_face_mask'] + self.mask_areas= args['mask_areas'] + self.mask_blur= args['mask_blur'] + self.mask_vignette_fallback_threshold= args['mask_vignette_fallback_threshold'] + self.face_size= args['face_size'] + self.use_minimal_area= args['use_minimal_area'] if self.gender_source is None or self.gender_source == "No": self.gender_source = 0 if self.gender_target is None or self.gender_target == "No": @@ -681,7 +781,8 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): source_hash_check=True, target_hash_check=True, device=self.device, - mask_face=self.mask_face + mask_face=self.mask_face, + mask_options=self.mask_options ) try: pp.info["ReActor"] = True diff --git a/scripts/reactor_swapper.py b/scripts/reactor_swapper.py index 061513d..db0955b 100644 --- a/scripts/reactor_swapper.py +++ b/scripts/reactor_swapper.py @@ -1,7 +1,7 @@ import copy import os from dataclasses import dataclass -from typing import List, Union +from typing import List, Tuple, Union import cv2 import numpy as np @@ -41,7 +41,12 @@ if DEVICE == "CUDA": PROVIDERS = ["CUDAExecutionProvider"] else: PROVIDERS = ["CPUExecutionProvider"] - +class MaskOption: + DEFAULT_FACE_AREAS = ["Face"] + DEFAULT_FACE_SIZE = 512 + DEFAULT_VIGNETTE_THRESHOLD = 0.1 + DEFAULT_MASK_BLUR = 12, + DEFAULT_USE_MINIMAL_AREA = True @dataclass class EnhancementOptions: @@ -52,7 +57,16 @@ class EnhancementOptions: face_restorer: FaceRestoration = None restorer_visibility: float = 0.5 codeformer_weight: float = 0.5 + +@dataclass +class MaskOptions: + mask_areas:List[str] + save_face_mask: bool = False + mask_blur:int = 12 + face_size:int = 512 + vignette_fallback_threshold:float =0.10 + use_minimal_area:bool = True MESSAGED_STOPPED = False MESSAGED_SKIPPED = False @@ -175,7 +189,7 @@ def enhance_image(image: Image, enhancement_options: EnhancementOptions): result_image = restore_face(result_image, enhancement_options) return result_image -def enhance_image_and_mask(image: Image.Image, enhancement_options: EnhancementOptions,target_img_orig:Image.Image,entire_mask_image:Image.Image)->Image.Image: +def enhance_image_and_mask(image: Image.Image, enhancement_options: EnhancementOptions,target_img_orig:Image.Image,entire_mask_image:Image.Image)->Tuple[Image.Image,Image.Image]: result_image = image if check_process_halt(msgforced=True): @@ -183,7 +197,11 @@ def enhance_image_and_mask(image: Image.Image, enhancement_options: EnhancementO if enhancement_options.do_restore_first: + result_image = restore_face(result_image, enhancement_options) + + transparent = Image.new("RGBA",result_image.size) + masked_faces = Image.composite(result_image.convert("RGBA"),transparent,entire_mask_image) result_image = Image.composite(result_image,target_img_orig,entire_mask_image) result_image = upscale_image(result_image, enhancement_options) @@ -191,10 +209,13 @@ def enhance_image_and_mask(image: Image.Image, enhancement_options: EnhancementO result_image = upscale_image(result_image, enhancement_options) entire_mask_image = Image.fromarray(cv2.resize(np.array(entire_mask_image),result_image.size, interpolation=cv2.INTER_AREA)).convert("L") + result_image = Image.composite(result_image,target_img_orig,entire_mask_image) result_image = restore_face(result_image, enhancement_options) + transparent = Image.new("RGBA",result_image.size) + masked_faces = Image.composite(result_image.convert("RGBA"),transparent,entire_mask_image) + return result_image, masked_faces - return result_image def get_gender(face, face_index): @@ -310,7 +331,8 @@ def swap_face( source_hash_check: bool = True, target_hash_check: bool = False, device: str = "CPU", - mask_face:bool = False + mask_face:bool = False, + mask_options:Union[MaskOptions, None]= None ): global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, PROVIDERS result_image = target_img @@ -444,7 +466,7 @@ def swap_face( swapped_image = face_swapper.get(result, target_face, source_face) if mask_face: - result = apply_face_mask(swapped_image=swapped_image,target_image=result,target_face=target_face,entire_mask_image=entire_mask_image) + result = apply_face_mask(swapped_image=swapped_image,target_image=result,target_face=target_face,entire_mask_image=entire_mask_image,mask_options=mask_options) else: result = swapped_image swapped += 1 @@ -480,8 +502,8 @@ def swap_face( result_image = Image.fromarray(cv2.cvtColor(result, cv2.COLOR_BGR2RGB)) if enhancement_options is not None and swapped > 0: - if mask_face and entire_mask_image is not None: - result_image = enhance_image_and_mask(result_image, enhancement_options,Image.fromarray(target_img_orig),Image.fromarray(entire_mask_image).convert("L")) + if mask_face and entire_mask_image is not None: + result_image, masked_faces = enhance_image_and_mask(result_image, enhancement_options,Image.fromarray(target_img_orig),Image.fromarray(entire_mask_image).convert("L")) else: result_image = enhance_image(result_image, enhancement_options) elif mask_face and entire_mask_image is not None and swapped > 0: @@ -492,20 +514,19 @@ def swap_face( else: logger.status("No source face(s) found") - return result_image, output, swapped + return result_image, output, swapped,masked_faces -def apply_face_mask(swapped_image:np.ndarray,target_image:np.ndarray,target_face,entire_mask_image:np.array)->np.ndarray: - logger.status("Masking Face") +def apply_face_mask(swapped_image:np.ndarray,target_image:np.ndarray,target_face,entire_mask_image:np.array,mask_options:Union[MaskOptions,None] = None)->np.ndarray: + logger.status("Masking Face") mask_generator = BiSeNetMaskGenerator() - face = Face(target_image,Rect.from_ndarray(np.array(target_face.bbox)),1.6,512,"") + face = Face(target_image,Rect.from_ndarray(np.array(target_face.bbox)),1.6,mask_options.face_size,"") face_image = np.array(face.image) - process_face_image(face) face_area_on_image = face.face_area_on_image - mask = mask_generator.generate_mask(face_image,face_area_on_image=face_area_on_image,affected_areas=["Face"],mask_size=0,use_minimal_area=True) - mask = cv2.blur(mask, (12, 12)) - """entire_mask_image = np.zeros_like(target_image)""" + + mask = mask_generator.generate_mask(face_image,face_area_on_image=face_area_on_image,affected_areas=mask_options.mask_areas,mask_size=0,use_minimal_area=mask_options.use_minimal_area) + mask = cv2.blur(mask, (mask_options.mask_blur, mask_options.mask_blur)) larger_mask = cv2.resize(mask, dsize=(face.width, face.height)) entire_mask_image[ face.top : face.bottom, @@ -554,22 +575,7 @@ def color_generator(colors): color_iter = color_generator(colors) -def process_face_image( - face: Face, - **kwargs, - ) -> Image: - image = np.array(face.image) - overlay = image.copy() - cv2.rectangle(overlay, (0, 0), (image.shape[1], image.shape[0]), next(color_iter), -1) - l, t, r, b = face.face_area_on_image - cv2.rectangle(overlay, (l, t), (r, b), (0, 0, 0), 10) - if face.landmarks_on_image is not None: - for landmark in face.landmarks_on_image: - cv2.circle(overlay, (int(landmark.x), int(landmark.y)), 6, (0, 0, 0), 10) - alpha = 0.3 - output = cv2.addWeighted(image, 1 - alpha, overlay, alpha, 0) - - return Image.fromarray(output) + def dilate_erode(img: Image.Image, value: int) -> Image.Image: """ The dilate_erode function takes an image and a value. From 0f7f73d47ff38682e00e77d153e731be6f3a174f Mon Sep 17 00:00:00 2001 From: jiveabillion Date: Mon, 27 Nov 2023 17:13:21 -0500 Subject: [PATCH 2/3] Added Multi Face Added ability to upload multiple source face files and have it apply them all to each diffused image --- scripts/reactor_faceswap.py | 167 +++++++++++++++++++++++++++++------- scripts/reactor_swapper.py | 12 +-- 2 files changed, 141 insertions(+), 38 deletions(-) diff --git a/scripts/reactor_faceswap.py b/scripts/reactor_faceswap.py index f2258ba..981588c 100644 --- a/scripts/reactor_faceswap.py +++ b/scripts/reactor_faceswap.py @@ -1,5 +1,6 @@ import os, glob import gradio as gr +import tempfile from PIL import Image try: import torch.cuda as cuda @@ -8,7 +9,7 @@ except: EP_is_visible = False from typing import List - +from PIL import Image import modules.scripts as scripts from modules.upscaler import Upscaler, UpscalerData from modules import scripts, shared, images, scripts_postprocessing @@ -65,10 +66,11 @@ class FaceSwapScript(scripts.Script): with gr.Tab("Main"): with gr.Column(): img = gr.Image(type="pil") + face_files = gr.File(label="Multiple Source Face Files",file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") save_original = gr.Checkbox(False, label="Save Original", info="Save the original image(s) made before swapping; If you use \"img2img\" - this option will affect with \"Swap in generated\" only") mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") - + gr.Markdown("
") gr.Markdown("Source Image (above):") with gr.Row(): @@ -143,13 +145,16 @@ class FaceSwapScript(scripts.Script): ) with gr.Tab("Masking"): save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") + use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") + mask_areas = gr.CheckboxGroup( label="Mask areas", choices=["Face", "Hair", "Hat", "Neck"], type="value", value= MaskOption.DEFAULT_FACE_AREAS ) - mask_blur = gr.Slider(label="Mask blur ", minimum=0, maximum=64, step=1, value=MaskOption.DEFAULT_MASK_BLUR,info="The number of pixels from the outer edge of the mask to blur.") face_size = gr.Radio( label = "Face Size", choices = [512,256,128],value=MaskOption.DEFAULT_FACE_SIZE,type="value", info="Size of the masked area. Use larger numbers if the face is expected to be large, smaller if small. Default is 512." ) + mask_blur = gr.Slider(label="Mask blur", minimum=0, maximum=64, step=1, value=12,info="The number of pixels from the outer edge of the mask to blur.") + mask_vignette_fallback_threshold = gr.Slider( minimum=0.1, maximum=1.0, @@ -158,7 +163,6 @@ class FaceSwapScript(scripts.Script): label="Vignette fallback threshold", info="Switch to a rectangular vignette mask when masked area is only this specified percentage of Face Size." ) - use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") with gr.Tab("Settings"): models = get_models() with gr.Row(visible=EP_is_visible): @@ -239,7 +243,7 @@ class FaceSwapScript(scripts.Script): use_minimal_area, face_size, mask_vignette_fallback_threshold, - + face_files ] @@ -304,22 +308,23 @@ class FaceSwapScript(scripts.Script): target_hash_check, device, mask_face, - save_face_mask, + save_face_mask:bool, mask_areas, - mask_blur, + mask_blur:int, mask_use_minimal_area, mask_face_size, mask_vignette_fallback_threshold, - + face_files ): self.enable = enable if self.enable: - + reset_messaged() if check_process_halt(): return global MODELS_PATH + self.source = img self.face_restorer_name = face_restorer_name self.upscaler_scale = upscaler_scale @@ -345,6 +350,7 @@ class FaceSwapScript(scripts.Script): self.mask_face_size = mask_face_size self.mask_vignette_fallback_threshold = mask_vignette_fallback_threshold self.mask_use_minimal_area = mask_use_minimal_area + self.face_files = face_files if self.gender_source is None or self.gender_source == "No": self.gender_source = 0 if self.gender_target is None or self.gender_target == "No": @@ -367,9 +373,9 @@ class FaceSwapScript(scripts.Script): self.target_hash_check = False set_Device(self.device) - logger.status(f"Self: {self}") + apply_logging_patch(console_logging_level) if self.source is not None: - apply_logging_patch(console_logging_level) + if isinstance(p, StableDiffusionProcessingImg2Img) and self.swap_in_source: logger.status("Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index) @@ -401,7 +407,7 @@ class FaceSwapScript(scripts.Script): if shared.state.interrupted or shared.state.skipped: return - else: + elif self.face_files is None or len(self.face_files) == 0: logger.error("Please provide a source face") def postprocess(self, p: StableDiffusionProcessing, processed: Processed, *args): @@ -410,14 +416,15 @@ class FaceSwapScript(scripts.Script): reset_messaged() if check_process_halt(): return + postprocess_run: bool = True + orig_images : List[Image.Image] = processed.images[processed.index_of_first_image:] + orig_infotexts : List[str] = processed.infotexts[processed.index_of_first_image:] + result_images:List[Image.Image] = [] + + if self.save_original: - postprocess_run: bool = True - - orig_images : List[Image.Image] = processed.images[processed.index_of_first_image:] - orig_infotexts : List[str] = processed.infotexts[processed.index_of_first_image:] - result_images: List = processed.images # result_info: List = processed.infotexts @@ -469,7 +476,48 @@ class FaceSwapScript(scripts.Script): # fullfn = split_fullfn[0] + ".txt" # with open(fullfn, 'w', encoding="utf8") as f: # f.writelines(output) - + if self.face_files is not None and len(self.face_files) > 0: + + for i,(img,info) in enumerate(zip(orig_images, orig_infotexts)): + for j,f_img in enumerate(self.face_files): + + if check_process_halt(): + postprocess_run = False + break + if len(self.face_files) > 1: + logger.status("Swap in face file #%s", j+1) + result, output, swapped, masked_faces = swap_face( + Image.open(os.path.abspath(f_img.name)), + img, + source_faces_index=self.source_faces_index, + faces_index=self.faces_index, + model=self.model, + enhancement_options=self.enhancement_options, + gender_source=self.gender_source, + gender_target=self.gender_target, + source_hash_check=self.source_hash_check, + target_hash_check=self.target_hash_check, + device=self.device, + mask_face=self.mask_face, + mask_options=self.mask_options + ) + if result is not None and swapped > 0: + result_images.append(result) + suffix = f"-swapped-ff-{j+1}" + try: + img_path = save_image(result, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) + except: + logger.error("Cannot save a result image - please, check SD WebUI Settings (Saving and Paths)") + if self.mask_face and self.save_face_mask and masked_faces is not None: + result_images.append(masked_faces) + suffix = f"-mask-ff-{j+1}" + try: + img_path = save_image(masked_faces, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) + except: + logger.error("Cannot save a Masked Face image - please, check SD WebUI Settings (Saving and Paths)") + + elif result is None: + logger.error("Cannot create a result image") if shared.opts.return_grid and len(result_images) > 2 and postprocess_run: grid = make_grid(result_images) result_images.insert(0, grid) @@ -480,13 +528,62 @@ class FaceSwapScript(scripts.Script): processed.images = result_images # processed.infotexts = result_info - + elif self.face_files is not None and len(self.face_files) > 0: + for i,(img,info) in enumerate(zip(orig_images, orig_infotexts)): + for j,f_img in enumerate(self.face_files): + + if check_process_halt(): + postprocess_run = False + break + if len(self.face_files) > 1: + logger.status("Swap in face file #%s", j+1) + result, output, swapped, masked_faces = swap_face( + Image.open(os.path.abspath(f_img.name)), + img, + source_faces_index=self.source_faces_index, + faces_index=self.faces_index, + model=self.model, + enhancement_options=self.enhancement_options, + gender_source=self.gender_source, + gender_target=self.gender_target, + source_hash_check=self.source_hash_check, + target_hash_check=self.target_hash_check, + device=self.device, + mask_face=self.mask_face, + mask_options=self.mask_options + ) + if result is not None and swapped > 0: + result_images.append(result) + suffix = f"-swapped-ff-{j+1}" + try: + img_path = save_image(result, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) + except: + logger.error("Cannot save a result image - please, check SD WebUI Settings (Saving and Paths)") + if self.mask_face and self.save_face_mask and masked_faces is not None: + result_images.append(masked_faces) + suffix = f"-mask-ff-{j+1}" + try: + img_path = save_image(masked_faces, p.outpath_samples, "", p.all_seeds[0], p.all_prompts[0], "png",info=info, p=p, suffix=suffix) + except: + logger.error("Cannot save a Masked Face image - please, check SD WebUI Settings (Saving and Paths)") + + elif result is None: + logger.error("Cannot create a result image") + if shared.opts.return_grid and len(result_images) > 2 and postprocess_run: + grid = make_grid(result_images) + result_images.insert(0, grid) + try: + save_image(grid, p.outpath_grids, "grid", p.all_seeds[0], p.all_prompts[0], shared.opts.grid_format, info=info, short_filename=not shared.opts.grid_extended_filename, p=p, grid=True) + except: + logger.error("Cannot save a grid - please, check SD WebUI Settings (Saving and Paths)") + + processed.images = result_images def postprocess_batch(self, p, *args, **kwargs): if self.enable and not self.save_original: images = kwargs["images"] def postprocess_image(self, p, script_pp: scripts.PostprocessImageArgs, *args): - if self.enable and self.swap_in_generated and not self.save_original: + if self.enable and self.swap_in_generated and not ( self.save_original or ( self.face_files is not None and len(self.face_files) > 0)): current_job_number = shared.state.job_no + 1 job_count = shared.state.job_count @@ -498,7 +595,7 @@ class FaceSwapScript(scripts.Script): if self.source is not None: logger.status("Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index) image: Image.Image = script_pp.image - result, output, swapped = swap_face( + result, output, swapped, masked_faces = swap_face( self.source, image, source_faces_index=self.source_faces_index, @@ -510,7 +607,8 @@ class FaceSwapScript(scripts.Script): source_hash_check=self.source_hash_check, target_hash_check=self.target_hash_check, device=self.device, - mask_face=self.mask_face + mask_face=self.mask_face, + mask_options=self.mask_options ) try: pp = scripts_postprocessing.PostprocessedImage(result) @@ -525,7 +623,7 @@ class FaceSwapScript(scripts.Script): # f.writelines(output) except: logger.error("Cannot create a result image") - + class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): name = 'ReActor' @@ -536,9 +634,9 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): with gr.Tab("Main"): with gr.Column(): img = gr.Image(type="pil") + face_files = gr.File(file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") - gr.Markdown("Source Image (above):") with gr.Row(): source_faces_index = gr.Textbox( @@ -599,13 +697,16 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): ) with gr.Tab("Masking"): save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") + use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") + mask_areas = gr.CheckboxGroup( label="Mask areas", choices=["Face", "Hair", "Hat", "Neck"], type="value", value= MaskOption.DEFAULT_FACE_AREAS ) - mask_blur = gr.Slider(label="Mask blur ", minimum=0, maximum=64, step=1, value=MaskOption.DEFAULT_MASK_BLUR,info="The number of pixels from the outer edge of the mask to blur.") face_size = gr.Radio( label = "Face Size", choices = [512,256,128],value=MaskOption.DEFAULT_FACE_SIZE,type="value", info="Size of the masked area. Use larger numbers if the face is expected to be large, smaller if small. Default is 512." ) + mask_blur = gr.Slider(label="Mask blur", minimum=0, maximum=64, step=1, value=12,info="The number of pixels from the outer edge of the mask to blur.") + mask_vignette_fallback_threshold = gr.Slider( minimum=0.1, maximum=1.0, @@ -614,7 +715,6 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): label="Vignette fallback threshold", info="Switch to a rectangular vignette mask when masked area is only this specified percentage of Face Size." ) - use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") with gr.Tab("Settings"): models = get_models() @@ -654,7 +754,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): label="Console Log Level", type="index", ) - + args = { 'img': img, 'enable': enable, @@ -679,6 +779,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): 'mask_vignette_fallback_threshold':mask_vignette_fallback_threshold, 'face_size':face_size, 'use_minimal_area':use_minimal_area, + 'face_files':face_files } return args @@ -713,9 +814,9 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): mask_areas = self.mask_areas, save_face_mask = self.save_face_mask, mask_blur = self.mask_blur, - face_size = self.mask_face_size, + face_size = self.face_size, vignette_fallback_threshold = self.mask_vignette_fallback_threshold, - use_minimal_area = self.mask_use_minimal_area, + use_minimal_area = self.use_minimal_area, ) def process(self, pp: scripts_postprocessing.PostprocessedImage, **args): if args['enable']: @@ -723,6 +824,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): if check_process_halt(): return global MODELS_PATH + self.source = args['img'] self.face_restorer_name = args['face_restorer_name'] self.upscaler_scale = args['upscaler_scale'] @@ -743,6 +845,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): self.mask_vignette_fallback_threshold= args['mask_vignette_fallback_threshold'] self.face_size= args['face_size'] self.use_minimal_area= args['use_minimal_area'] + self.face_files = args['face_files'] if self.gender_source is None or self.gender_source == "No": self.gender_source = 0 if self.gender_target is None or self.gender_target == "No": @@ -764,12 +867,12 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): reset_messaged() set_Device(self.device) - + apply_logging_patch(self.console_logging_level) if self.source is not None: - apply_logging_patch(self.console_logging_level) + logger.status("Working: source face index %s, target face index %s", self.source_faces_index, self.faces_index) image: Image.Image = pp.image - result, output, swapped = swap_face( + result, output, swapped,masked_faces = swap_face( self.source, image, source_faces_index=self.source_faces_index, diff --git a/scripts/reactor_swapper.py b/scripts/reactor_swapper.py index db0955b..f094c6c 100644 --- a/scripts/reactor_swapper.py +++ b/scripts/reactor_swapper.py @@ -43,10 +43,10 @@ else: PROVIDERS = ["CPUExecutionProvider"] class MaskOption: DEFAULT_FACE_AREAS = ["Face"] - DEFAULT_FACE_SIZE = 512 - DEFAULT_VIGNETTE_THRESHOLD = 0.1 - DEFAULT_MASK_BLUR = 12, - DEFAULT_USE_MINIMAL_AREA = True + DEFAULT_FACE_SIZE:int = 512 + DEFAULT_VIGNETTE_THRESHOLD:float = 0.1 + DEFAULT_MASK_BLUR:int = 12, + DEFAULT_USE_MINIMAL_AREA:bool = True @dataclass class EnhancementOptions: @@ -63,7 +63,7 @@ class EnhancementOptions: class MaskOptions: mask_areas:List[str] save_face_mask: bool = False - mask_blur:int = 12 + mask_blur:int = 12 face_size:int = 512 vignette_fallback_threshold:float =0.10 use_minimal_area:bool = True @@ -336,7 +336,7 @@ def swap_face( ): global SOURCE_FACES, SOURCE_IMAGE_HASH, TARGET_FACES, TARGET_IMAGE_HASH, PROVIDERS result_image = target_img - + masked_faces = None PROVIDERS = ["CUDAExecutionProvider"] if device == "CUDA" else ["CPUExecutionProvider"] if check_process_halt(): From 5d0b29f2e8a7b373c5bddf2713f1d295d76ac90d Mon Sep 17 00:00:00 2001 From: jiveabillion Date: Tue, 28 Nov 2023 14:43:50 -0500 Subject: [PATCH 3/3] Multiple Source Face Files and UI change --- scripts/reactor_faceswap.py | 22 +++++++++++++--------- scripts/reactor_swapper.py | 4 +++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/scripts/reactor_faceswap.py b/scripts/reactor_faceswap.py index 981588c..a2e56ad 100644 --- a/scripts/reactor_faceswap.py +++ b/scripts/reactor_faceswap.py @@ -63,11 +63,15 @@ class FaceSwapScript(scripts.Script): def ui(self, is_img2img): with gr.Accordion(f"{app_title}", open=False): + enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") + gr.Markdown("
") with gr.Tab("Main"): + with gr.Column(): - img = gr.Image(type="pil") - face_files = gr.File(label="Multiple Source Face Files",file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") - enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") + with gr.Tab("Single Source Image"): + img = gr.Image(type="pil") + with gr.Tab("Multiple Source Images"): + face_files = gr.File(label="Multiple Source Face Files",file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") save_original = gr.Checkbox(False, label="Save Original", info="Save the original image(s) made before swapping; If you use \"img2img\" - this option will affect with \"Swap in generated\" only") mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") @@ -634,7 +638,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): with gr.Tab("Main"): with gr.Column(): img = gr.Image(type="pil") - face_files = gr.File(file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") + #face_files = gr.File(file_count="multiple",file_types=["image"],info="Upload multiple face files and each file will be processed in post processing") enable = gr.Checkbox(False, label="Enable", info=f"The Fast and Simple FaceSwap Extension - {version_flag}") mask_face = gr.Checkbox(False, label="Mask Faces", info="Attempt to mask only the faces and eliminate pixelation of the image around the contours. Additional settings in the Masking tab.") gr.Markdown("Source Image (above):") @@ -696,7 +700,7 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): 0, 1, 1, step=0.1, label="Upscaler Visibility (if scale = 1)" ) with gr.Tab("Masking"): - save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") + #save_face_mask = gr.Checkbox(False, label="Save Face Mask", info="Save the face mask as a separate image with alpha transparency.") use_minimal_area = gr.Checkbox(MaskOption.DEFAULT_USE_MINIMAL_AREA, label="Use Minimal Area", info="Use the least amount of area for the mask as possible. This is good for multiple faces that are close together or for preserving the most of the surrounding image.") mask_areas = gr.CheckboxGroup( @@ -773,13 +777,13 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): 'codeformer_weight': codeformer_weight, 'device': device, 'mask_face':mask_face, - 'save_face_mask':save_face_mask, + 'mask_areas':mask_areas, 'mask_blur':mask_blur, 'mask_vignette_fallback_threshold':mask_vignette_fallback_threshold, 'face_size':face_size, 'use_minimal_area':use_minimal_area, - 'face_files':face_files + } return args @@ -839,13 +843,13 @@ class FaceSwapScriptExtras(scripts_postprocessing.ScriptPostprocessing): self.codeformer_weight = args['codeformer_weight'] self.device = args['device'] self.mask_face = args['mask_face'] - self.save_face_mask = args['save_face_mask'] + self.save_face_mask = None self.mask_areas= args['mask_areas'] self.mask_blur= args['mask_blur'] self.mask_vignette_fallback_threshold= args['mask_vignette_fallback_threshold'] self.face_size= args['face_size'] self.use_minimal_area= args['use_minimal_area'] - self.face_files = args['face_files'] + self.face_files = None if self.gender_source is None or self.gender_source == "No": self.gender_source = 0 if self.gender_target is None or self.gender_target == "No": diff --git a/scripts/reactor_swapper.py b/scripts/reactor_swapper.py index f094c6c..b40d5d8 100644 --- a/scripts/reactor_swapper.py +++ b/scripts/reactor_swapper.py @@ -341,7 +341,9 @@ def swap_face( if check_process_halt(): return result_image, [], 0 - + if mask_options is None: + mask_options = MaskOptions() + if model is not None: if isinstance(source_img, str): # source_img is a base64 string