├── .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 | 
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 | Image
24 |
25 |
Image
29 |
30 |
Image
37 |
38 |
Image
42 |
43 |
Image
47 |
48 |
Image
52 |
53 |
Example
59 |
60 | Left 'young woman in red dress' using `chilloutMix`
61 | Right 'Emma Watson in red dress' using `realisticVision`
62 |
63 |
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 | --------------------------------------------------------------------------------