├── README.md ├── imgs ├── anime_face_detection.png ├── large_str_for_face.png ├── small_face.png └── spiderman.png ├── install.py └── scripts └── face_crop_img2img.py /README.md: -------------------------------------------------------------------------------- 1 | # face_crop_img2img 2 | 3 | ## Overview 4 | #### AUTOMATIC1111 UI custom script 5 | #### Separate the area near the face from the rest of the face and img2img with different "Denoising Strength" settings 6 | 7 | ## Example 8 | 9 | #### img2img for Small Face Picture 10 |  11 | 12 | #### Large Denoising Strength(0.7) for Face 13 |  14 | 15 | #### Prompt for Face( face close up, spiderman ) 16 |  17 | 18 | #### Anime Face Detection 19 |  20 | 21 | ## Installation 22 | - Use the Extensions tab of the webui to [Install from URL] 23 | 24 | ## Usage 25 | - Go to img2img and load your base image 26 | - Choose "face crop img2img" from the scripts select 27 | - Adjust "Face Denoising Strength" 28 | - Generate 29 | 30 | ## Options 31 | - "Face Detection Method" ... Use YuNet for realistic images and Yolov5_anime for animated images. 32 | - "Max Crop Size" ... Maximum size of the face to be cropped. If the detected face is large, ignore it. 33 | - "Face Denoising Strength" ... "Denoising Strength" applied to the face 34 | - "Face Area Magnification" ... Size recognized as around the face. Magnification from the area that the face detection method considers to be a face. 35 | - "Enable Face Prompt" ... Enable individual prompt to face 36 | -------------------------------------------------------------------------------- /imgs/anime_face_detection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s9roll7/face_crop_img2img/08760602871e4947e7f1abee554f9548783b7fbb/imgs/anime_face_detection.png -------------------------------------------------------------------------------- /imgs/large_str_for_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s9roll7/face_crop_img2img/08760602871e4947e7f1abee554f9548783b7fbb/imgs/large_str_for_face.png -------------------------------------------------------------------------------- /imgs/small_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s9roll7/face_crop_img2img/08760602871e4947e7f1abee554f9548783b7fbb/imgs/small_face.png -------------------------------------------------------------------------------- /imgs/spiderman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s9roll7/face_crop_img2img/08760602871e4947e7f1abee554f9548783b7fbb/imgs/spiderman.png -------------------------------------------------------------------------------- /install.py: -------------------------------------------------------------------------------- 1 | import launch 2 | 3 | if not launch.is_installed("ipython"): 4 | launch.run_pip("install ipython", "requirements for face crop img2img") 5 | 6 | if not launch.is_installed("seaborn"): 7 | launch.run_pip("install ""seaborn>=0.11.0""", "requirements for face crop img2img") 8 | 9 | -------------------------------------------------------------------------------- /scripts/face_crop_img2img.py: -------------------------------------------------------------------------------- 1 | import modules.scripts as scripts 2 | import gradio as gr 3 | import os 4 | import torch 5 | 6 | from modules.processing import process_images 7 | from modules.paths import models_path 8 | from modules.textual_inversion import autocrop 9 | import cv2 10 | import copy 11 | import numpy as np 12 | from PIL import Image 13 | import time 14 | import requests 15 | 16 | 17 | def get_my_dir(): 18 | if os.path.isdir("extensions/face_crop_img2img"): 19 | return "extensions/face_crop_img2img" 20 | return scripts.basedir() 21 | 22 | def x_ceiling(value, step): 23 | return -(-value // step) * step 24 | 25 | def resize_img(img, w, h): 26 | if img.shape[0] + img.shape[1] < h + w: 27 | interpolation = interpolation=cv2.INTER_CUBIC 28 | else: 29 | interpolation = interpolation=cv2.INTER_AREA 30 | 31 | return cv2.resize(img, (w, h), interpolation=interpolation) 32 | 33 | def download_and_cache_models(dirname): 34 | download_url = 'https://github.com/zymk9/yolov5_anime/blob/8b50add22dbd8224904221be3173390f56046794/weights/yolov5s_anime.pt?raw=true' 35 | model_file_name = 'yolov5s_anime.pt' 36 | 37 | if not os.path.exists(dirname): 38 | os.makedirs(dirname) 39 | 40 | cache_file = os.path.join(dirname, model_file_name) 41 | if not os.path.exists(cache_file): 42 | print(f"downloading face detection model from '{download_url}' to '{cache_file}'") 43 | response = requests.get(download_url) 44 | with open(cache_file, "wb") as f: 45 | f.write(response.content) 46 | 47 | if os.path.exists(cache_file): 48 | return cache_file 49 | return None 50 | 51 | 52 | class Script(scripts.Script): 53 | anime_face_detector = None 54 | face_detector = None 55 | mask_file = "face_crop_img2img_mask.png" 56 | mask_image = None 57 | 58 | # The title of the script. This is what will be displayed in the dropdown menu. 59 | def title(self): 60 | return "face crop img2img" 61 | 62 | 63 | # Determines when the script should be shown in the dropdown menu via the 64 | # returned value. As an example: 65 | # is_img2img is True if the current tab is img2img, and False if it is txt2img. 66 | # Thus, return is_img2img to only show the script on the img2img tab. 67 | 68 | def show(self, is_img2img): 69 | return is_img2img 70 | 71 | # How the script's is displayed in the UI. See https://gradio.app/docs/#components 72 | # for the different UI components you can use and how to create them. 73 | # Most UI components can return a value, such as a boolean for a checkbox. 74 | # The returned values are passed to the run method as parameters. 75 | 76 | def ui(self, is_img2img): 77 | face_detection_method = gr.Dropdown(choices=["YuNet","Yolov5_anime"], value="YuNet" ,label="Face Detection Method") 78 | gr.HTML(value="
\ 79 | If loading of the Yolov5_anime model fails, check\ 80 | [this] solution.\ 81 |
") 82 | max_crop = gr.Slider(minimum=0, maximum=2048, step=1, value=1024, label="Max Crop Size") 83 | face_denoising_strength = gr.Slider(minimum=0.00, maximum=1.00, step=0.01, value=0.5, label="Face Denoising Strength") 84 | face_area = gr.Slider(minimum=1.00, maximum=3.00, step=0.01, value=1.5, label="Face Area Magnification") 85 | 86 | with gr.Column(): 87 | enable_face_prompt = gr.Checkbox(False, label="Enable Face Prompt") 88 | face_prompt = gr.Textbox(label="Face Prompt", show_label=False, lines=2, 89 | placeholder="Prompt for Face", 90 | value = "face close up," 91 | ) 92 | 93 | return [face_detection_method, max_crop, face_denoising_strength,face_area,enable_face_prompt,face_prompt] 94 | 95 | 96 | def detect_face(self, img_array): 97 | if not self.face_detector: 98 | dnn_model_path = autocrop.download_and_cache_models(os.path.join(models_path, "opencv")) 99 | self.face_detector = cv2.FaceDetectorYN.create(dnn_model_path, "", (0, 0)) 100 | 101 | self.face_detector.setInputSize((img_array.shape[1], img_array.shape[0])) 102 | _, result = self.face_detector.detect(img_array) 103 | return result 104 | 105 | def detect_anime_face(self, img_array): 106 | if not self.anime_face_detector: 107 | anime_model_path = download_and_cache_models(os.path.join(models_path, "yolov5_anime")) 108 | 109 | if not os.path.isfile(anime_model_path): 110 | print( "WARNING!! " + anime_model_path + " not found.") 111 | print( "use YuNet instead.") 112 | return self.detect_face(img_array) 113 | 114 | self.anime_face_detector = torch.hub.load('ultralytics/yolov5', 'custom', path=anime_model_path) 115 | 116 | result = self.anime_face_detector(img_array) 117 | #models.common.Detections 118 | faces = [] 119 | for x_c, y_c, w, h, _, _ in result.xywh[0].tolist(): 120 | faces.append( [ x_c - w/2 , y_c - h/2, w, h ] ) 121 | 122 | return faces 123 | 124 | def get_mask(self): 125 | def create_mask( output, x_rate, y_rate, k_size ): 126 | img = np.zeros((512, 512, 3)) 127 | img = cv2.ellipse(img, ((256, 256), (int(512 * x_rate), int(512 * y_rate)), 0), (255, 255, 255), thickness=-1) 128 | img = cv2.GaussianBlur(img, (k_size, k_size), 0) 129 | cv2.imwrite(output, img) 130 | 131 | if self.mask_image is None: 132 | mask_file_path = os.path.join( get_my_dir() , self.mask_file) 133 | if not os.path.isfile(mask_file_path): 134 | create_mask( mask_file_path, 0.9, 0.9, 91) 135 | 136 | m = cv2.imread( mask_file_path )[:,:,0] 137 | m = m[:, :, np.newaxis] 138 | self.mask_image = m / 255 139 | 140 | return self.mask_image 141 | 142 | 143 | 144 | # This is where the additional processing is implemented. The parameters include 145 | # self, the model object "p" (a StableDiffusionProcessing class, see 146 | # processing.py), and the parameters returned by the ui method. 147 | # Custom functions can be defined here, and additional libraries can be imported 148 | # to be used in processing. The return value should be a Processed object, which is 149 | # what is returned by the process_images method. 150 | 151 | def run(self, p, face_detection_method, max_crop, face_denoising_strength, face_area, enable_face_prompt, face_prompt): 152 | 153 | def img_crop( img, face_coords,face_area,max_crop): 154 | img_array = np.array(img) 155 | face_imgs =[] 156 | new_coords = [] 157 | 158 | for face in face_coords: 159 | x = int(face[0]) 160 | y = int(face[1]) 161 | w = int(face[2]) 162 | h = int(face[3]) 163 | print([x,y,w,h]) 164 | 165 | if max(w,h) > max_crop: 166 | print("ignore big face") 167 | continue 168 | 169 | cx = x + int(w/2) 170 | cy = y + int(h/2) 171 | 172 | x = cx - int(w*face_area / 2) 173 | x = x if x > 0 else 0 174 | w = cx + int(w*face_area / 2) - x 175 | w = w if x+w < img.width else img.width - x 176 | 177 | y = cy - int(h*face_area / 2) 178 | y = y if y > 0 else 0 179 | h = cy + int(h*face_area / 2) - y 180 | h = h if y+h < img.height else img.height - y 181 | 182 | print([x,y,w,h]) 183 | 184 | face_imgs.append( img_array[y: y+h, x: x+w] ) 185 | new_coords.append( [x,y,w,h] ) 186 | 187 | resized = [] 188 | for face_img in face_imgs: 189 | if face_img.shape[1] < face_img.shape[0]: 190 | re_w = 512 191 | re_h = int(x_ceiling( (512 / face_img.shape[1]) * face_img.shape[0] , 64)) 192 | else: 193 | re_w = int(x_ceiling( (512 / face_img.shape[0]) * face_img.shape[1] , 64)) 194 | re_h = 512 195 | face_img = resize_img(face_img, re_w, re_h) 196 | resized.append( Image.fromarray(face_img)) 197 | 198 | return resized, new_coords 199 | 200 | 201 | def merge_face(img, face_img, face_coord, base_img_size, mask): 202 | x_rate = img.width / base_img_size[0] 203 | y_rate = img.height / base_img_size[1] 204 | 205 | img_array = np.array(img) 206 | x = int(face_coord[0] * x_rate) 207 | y = int(face_coord[1] * y_rate) 208 | w = int(face_coord[2] * x_rate) 209 | h = int(face_coord[3] * y_rate) 210 | 211 | face_array = np.array(face_img) 212 | face_array = resize_img(face_array, w, h) 213 | 214 | mask = resize_img(mask, w, h) 215 | if mask.ndim == 2: 216 | mask = mask[:, :, np.newaxis] 217 | 218 | bg = img_array[y: y+h, x: x+w] 219 | 220 | img_array[y: y+h, x: x+w] = mask * face_array + (1-mask)*bg 221 | 222 | return Image.fromarray(img_array) 223 | 224 | def detect_face(img, mask, face_detection_method): 225 | img_array = np.array(img) 226 | 227 | if mask is not None: 228 | mask_array = np.array(mask)/255 229 | 230 | if mask_array.ndim == 2: 231 | mask_array = mask_array[:, :, np.newaxis] 232 | 233 | img_array = mask_array * img_array 234 | img_array = img_array.astype(np.uint8) 235 | 236 | # image without alpha 237 | img_array = img_array[:,:,:3] 238 | 239 | if face_detection_method == "YuNet": 240 | return self.detect_face(img_array) 241 | elif face_detection_method == "Yolov5_anime": 242 | return self.detect_anime_face(img_array) 243 | 244 | def save_image(img, dir_path): 245 | filename = "/" + "face_crop_img2img_" + time.strftime("%Y%m%d-%H%M%S") + ".png" 246 | cv2.imwrite(dir_path + filename, np.array(img)[:, :, ::-1]) 247 | 248 | ### face detect in base img 249 | base_img = p.init_images[0] 250 | 251 | if base_img is None: 252 | print("p.init_images[0] is None") 253 | return process_images(p) 254 | 255 | base_img_size = (base_img.width, base_img.height) 256 | 257 | face_coords = detect_face(base_img, p.image_mask,face_detection_method) 258 | 259 | if face_coords is None or len(face_coords) == 0: 260 | print("no face detected") 261 | return process_images(p) 262 | 263 | print(face_coords) 264 | face_imgs, new_coords = img_crop(base_img, face_coords, face_area, max_crop) 265 | 266 | if not face_imgs: 267 | return process_images(p) 268 | 269 | face_p = copy.copy(p) 270 | 271 | ### img2img base img 272 | proc = process_images(p) 273 | 274 | 275 | ### img2img for each face 276 | face_img2img_results = [] 277 | 278 | for face, coord in zip(face_imgs, new_coords): 279 | # cv2.imwrite("scripts/face.png", np.array(face)[:, :, ::-1]) 280 | face_p.init_images = [face] 281 | face_p.width = face.width 282 | face_p.height = face.height 283 | face_p.denoising_strength = face_denoising_strength 284 | 285 | if enable_face_prompt: 286 | face_p.prompt = face_prompt 287 | else: 288 | face_p.prompt = "close-up face ," + face_p.prompt 289 | 290 | if p.image_mask is not None: 291 | x,y,w,h = coord 292 | face_p.image_mask = Image.fromarray( np.array(p.image_mask)[y: y+h, x: x+w] ) 293 | 294 | face_proc = process_images(face_p) 295 | face_img2img_results.append((face_proc.images[0], coord)) 296 | 297 | ### merge faces 298 | bg = proc.images[0] 299 | mask = self.get_mask() 300 | 301 | for face_img, coord in face_img2img_results: 302 | bg = merge_face(bg, face_img, coord, base_img_size, mask) 303 | 304 | save_image(bg, p.outpath_samples) 305 | 306 | proc.images[0] = bg 307 | 308 | return proc 309 | --------------------------------------------------------------------------------