From 1037b3653677aa1ae08b91250dadd0fdef5585ab Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Tue, 29 Nov 2022 20:14:15 +0000 Subject: [PATCH 1/4] configure the NSFW checker at install time with default on 1. Changes the --safety_checker argument to --nsfw_checker and --no-nsfw_checker. The original argument is recognized for backward compatibility. 2. The configure script asks users whether to enable the checker (default yes). Also offers users ability to select default sampler and number of generation steps. 3.Enables the pasting of the caution icon on blurred images when InvokeAI is installed into the package directory. 4. Adds documentation for the NSFW checker, including caveats about accuracy, memory requirements, and intermediate image dispaly. --- docs/features/NSFW.md | 89 +++++++++++++++++++++++++++++++++++ ldm/invoke/args.py | 7 ++- ldm/invoke/generator/base.py | 24 ++++++++-- scripts/configure_invokeai.py | 77 ++++++++++++++++++++---------- setup.py | 3 +- 5 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 docs/features/NSFW.md diff --git a/docs/features/NSFW.md b/docs/features/NSFW.md new file mode 100644 index 00000000000..8775ef7853a --- /dev/null +++ b/docs/features/NSFW.md @@ -0,0 +1,89 @@ +--- +title: The NSFW Checker +--- + +# :material-file-document: NSFW Checker + +## The NSFW ("Safety") Checker + +The Stable Diffusion image generation models will produce sexual +imagery if deliberately prompted, and will occasionally produce such +images when this is not intended. Such images are colloquially known +as "Not Safe for Work" (NSFW). This behavior is due to the nature of +the training set that Stable Diffusion was trained on, which culled +millions of "aesthetic" images from the Internet. + +You may not wish to be exposed to these images, and in some +jurisdictions it may be illegal to publicly distribute such imagery, +including mounting a publicly-available server that provides +unfiltered images to the public. Furthermore, the [Stable Diffusion +weights +License](https://github.com/invoke-ai/InvokeAI/blob/main/LICENSE-ModelWeights.txt) +forbids the model from being used to "exploit any of the +vulnerabilities of a specific group of persons." + +For these reasons Stable Diffusion offers a "safety checker," a +machine learning model trained to recognize potentially disturbing +imagery. When a potentially NSFW image is detected, the checker will +blur the image and paste a warning icon on top. The checker can be +turned on and off on the command line using `--nsfw_checker` and +`--no-nsfw_checker`. + +At installation time, InvokeAI will ask whether the checker should be +activated by default (neither argument given on the command line). The +response is stored in the InvokeAI initialization file (usually +`.invokeai` in your home directory). You can change the default at any +time by opening this file in a text editor and commenting or +uncommenting the line `--nsfw_checker`. + +## Caveats + +There are a number of caveats that you need to be aware of. + +### Accuracy + +The checker is [not perfect](https://arxiv.org/abs/2210.04610).It will +occasionally flag innocuous images (false positives), and will +frequently miss violent and gory imagery (false negatives). It rarely +fails to flag sexual imagery, but this has been known to happen. For +these reasons, the InvokeAI team prefers to refer to the software as a +"NSFW Checker" rather than "safety checker." + +### Memory Usage and Performance + +The NSFW checker consumes an additional 1.2G of GPU VRAM on top of the +3.4G of VRAM used by Stable Diffusion v1.5 (this is with +half-precision arithmetic). This means that the checker will not run +successfully on GPU cards with less than 6GB VRAM, and will reduce the +size of the images that you can produce. + +The checker also introduces a slight performance penalty. Images will +take ~1 second longer to generate when the checker is +activated. Generally this is not noticeable. + +### Intermediate Images in the Web UI + +The checker only operates on the final image produced by the Stable +Diffusion algorithm. If you are using the Web UI and have enabled the +display of intermediate images, you will briefly be exposed to a +low-resolution (mosaicized) version of the final image before it is +flagged by the checker and replaced by a fully blurred version. You +are encouraged to turn **off** intermediate image rendering when you +are using the checker. Future versions of InvokeAI will apply +additional blurring to intermediate images when the checker is active. + +### Watermarking + +InvokeAI does not apply any sort of watermark to images it +generates. However, it does write metadata into the PNG data area, +including the prompt used to generate the image and relevant parameter +settings. These fields can be examined using the `sd-metadata.py` +script that comes with the InvokeAI package. + +Note that several other Stable Diffusion distributions offer +wavelet-based "invisible" watermarking. We have experimented with the +library used to generate these watermarks and have reached the +conclusion that while the watermarking library may be adding +watermarks to PNG images, the currently available version is unable to +retrieve them successfully. If and when a functioning version of the +library becomes available, we will offer this feature as well. diff --git a/ldm/invoke/args.py b/ldm/invoke/args.py index 1e7bd48471c..0d4ae7811f6 100644 --- a/ldm/invoke/args.py +++ b/ldm/invoke/args.py @@ -461,9 +461,12 @@ def _create_arg_parser(self): default='auto', ) model_group.add_argument( + '--nsfw_checker' '--safety_checker', - action='store_true', - help='Check for and blur potentially NSFW images', + action=argparse.BooleanOptionalAction, + dest='safety_checker', + default=False, + help='Check for and blur potentially NSFW images. Use --no-nsfw_checker to disable.', ) model_group.add_argument( '--patchmatch', diff --git a/ldm/invoke/generator/base.py b/ldm/invoke/generator/base.py index ce199fefb15..ba3172e9dc8 100644 --- a/ldm/invoke/generator/base.py +++ b/ldm/invoke/generator/base.py @@ -6,6 +6,7 @@ import numpy as np import random import os +import os.path as osp import traceback from tqdm import tqdm, trange from PIL import Image, ImageFilter, ImageChops @@ -32,6 +33,7 @@ def __init__(self, model, precision): self.with_variations = [] self.use_mps_noise = False self.free_gpu_mem = None + self.caution_img = None # this is going to be overridden in img2img.py, txt2img.py and inpaint.py def get_make_image(self,prompt,**kwargs): @@ -290,13 +292,29 @@ def safety_check(self,image:Image.Image): def blur(self,input): blurry = input.filter(filter=ImageFilter.GaussianBlur(radius=32)) try: - caution = Image.open(CAUTION_IMG) - caution = caution.resize((caution.width // 2, caution.height //2)) - blurry.paste(caution,(0,0),caution) + caution = self.get_caution_img() + if caution: + blurry.paste(caution,(0,0),caution) except FileNotFoundError: pass return blurry + def get_caution_img(self): + if self.caution_img: + return self.caution_img + # Find the caution image. If we are installed in the package directory it will + # be six levels up. If we are in the repo directory it will be three levels up. + for dots in ('../../..','../../../../../..'): + caution_path = osp.join(osp.dirname(__file__),dots,CAUTION_IMG) + if osp.exists(caution_path): + path = caution_path + break + if not path: + return + caution = Image.open(path) + self.caution_img = caution.resize((caution.width // 2, caution.height //2)) + return self.caution_img + # this is a handy routine for debugging use. Given a generated sample, # convert it into a PNG image and store it at the indicated path def save_sample(self, sample, filepath): diff --git a/scripts/configure_invokeai.py b/scripts/configure_invokeai.py index fd7a009e666..8b4b2ed7879 100644 --- a/scripts/configure_invokeai.py +++ b/scripts/configure_invokeai.py @@ -70,10 +70,10 @@ def postscript(): Command-line version: python scripts/invoke.py -Remember to activate that 'invokeai' environment before running invoke.py. - -Or, if you used one of the automated installers, execute "invoke.sh" (Linux/Mac) -or "invoke.bat" (Windows) to start the script. +If you installed manually, remember to activate the 'invokeai' +environment before running invoke.py. If you installed using the +automated installation script, execute "invoke.sh" (Linux/Mac) or +"invoke.bat" (Windows) to start InvokeAI. Have fun! ''' @@ -243,10 +243,10 @@ def download_weight_datasets(models:dict, access_token:str): for mod in models.keys(): repo_id = Datasets[mod]['repo_id'] filename = Datasets[mod]['file'] - print(os.path.join(Globals.root,Model_dir,Weights_dir), file=sys.stderr) + dest = os.path.join(Globals.root,Model_dir,Weights_dir) success = hf_download_with_resume( repo_id=repo_id, - model_dir=os.path.join(Globals.root,Model_dir,Weights_dir), + model_dir=dest, model_name=filename, access_token=access_token ) @@ -494,12 +494,12 @@ def download_clipseg(): #------------------------------------- def download_safety_checker(): - print('Installing safety model for NSFW content detection...',file=sys.stderr) + print('Installing model for NSFW content detection...',file=sys.stderr) try: from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker from transformers import AutoFeatureExtractor except ModuleNotFoundError: - print('Error installing safety checker model:') + print('Error installing NSFW checker model:') print(traceback.format_exc()) return safety_model_id = "CompVis/stable-diffusion-safety-checker" @@ -520,6 +520,7 @@ def download_weights(opt:dict): return else: print('** Cannot download models because no Hugging Face access token could be found. Please re-run without --yes') + return else: choice = user_wants_to_download_weights() @@ -584,7 +585,7 @@ def select_outputs(root:str,yes_to_all:bool=False): #------------------------------------- def initialize_rootdir(root:str,yes_to_all:bool=False): - assert os.path.exists('./configs'),'Run this script from within the top level of the InvokeAI source code directory, "InvokeAI"' + assert os.path.exists('./configs'),'Run this script from within the InvokeAI source code directory, "InvokeAI" or the runtime directory "invokeai".' print(f'** INITIALIZING INVOKEAI RUNTIME DIRECTORY **') root_selected = False @@ -603,19 +604,50 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): print(f'\nYou may change the chosen directories at any time by editing the --root and --outdir options in "{Globals.initfile}",') print(f'You may also change the runtime directory by setting the environment variable INVOKEAI_ROOT.\n') + enable_safety_checker = True + default_sampler = 'k_euler_a' + default_steps = '30' # deliberately a string - see test below + + sampler_choices =['ddim','k_dpm_2_a','k_dpm_2','k_euler_a','k_euler','k_heun','k_lms','plms'] + + if not yes_to_all: + print('The NSFW (not safe for work) checker blurs out images that potentially contain sexual imagery.') + print('It can be selectively enabled at run time with --nsfw_checker, and disabled with --no-nsfw_checker.') + print('The following option will set whether the checker is enabled by default. Like other options, you can') + print(f'change this setting later by editing the file {Globals.initfile}.') + enable_safety_checker = yes_or_no('Enable the NSFW checker by default?',enable_safety_checker) + + print('\nThe next choice selects the sampler to use by default. Samplers have different speed/performance') + print('tradeoffs. If you are not sure what to select, accept the default.') + sampler = None + while sampler not in sampler_choices: + sampler = input(f'Default sampler to use? ({", ".join(sampler_choices)}) [{default_sampler}]:') or default_sampler + + print('\nThe number of denoising steps affects both the speed and quality of the images generated.') + print('Higher steps often (but not always) increases the quality of the image, but increases image') + print('generation time. This can be changed at run time. Accept the default if you are unsure.') + steps = '' + while not steps.isnumeric(): + steps = input(f'Default number of steps to use during generation? [{default_steps}]:') or default_steps + else: + sampler = default_sampler + steps = default_steps + + safety_checker = '--nsfw_checker' if enable_safety_checker else '--no-nsfw_checker' + for name in ('models','configs','embeddings'): os.makedirs(os.path.join(root,name), exist_ok=True) for src in (['configs']): dest = os.path.join(root,src) if not os.path.samefile(src,dest): shutil.copytree(src,dest,dirs_exist_ok=True) - os.makedirs(outputs, exist_ok=True) + os.makedirs(outputs, exist_ok=True) init_file = os.path.expanduser(Globals.initfile) - if not os.path.exists(init_file): - print(f'Creating the initialization file at "{init_file}".\n') - with open(init_file,'w') as f: - f.write(f'''# InvokeAI initialization file + + print(f'Creating the initialization file at "{init_file}".\n') + with open(init_file,'w') as f: + f.write(f'''# InvokeAI initialization file # This is the InvokeAI initialization file, which contains command-line default values. # Feel free to edit. If anything goes wrong, you can re-initialize this file by deleting # or renaming it and then running configure_invokeai.py again. @@ -626,23 +658,18 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): # the --outdir option controls the default location of image files. --outdir="{outputs}" +# generation arguments +{safety_checker} +--sampler={sampler} +--steps={steps} + # You may place other frequently-used startup commands here, one or more per line. # Examples: # --web --host=0.0.0.0 # --steps=20 # -Ak_euler_a -C10.0 # -''' - ) - else: - print(f'Updating the initialization file at "{init_file}".\n') - with open(init_file,'r') as infile, open(f'{init_file}.tmp','w') as outfile: - for line in infile.readlines(): - if not line.startswith('--root') and not line.startswith('--outdir'): - outfile.write(line) - outfile.write(f'--root="{root}"\n') - outfile.write(f'--outdir="{outputs}"\n') - os.replace(f'{init_file}.tmp',init_file) +''') #------------------------------------- class ProgressBar(): diff --git a/setup.py b/setup.py index 6c808c1f576..bfba829b4a6 100644 --- a/setup.py +++ b/setup.py @@ -81,6 +81,7 @@ def list_files(directory): 'scripts/preload_models.py', 'scripts/images2prompt.py','scripts/merge_embeddings.py' ], data_files=[('frontend/dist',list_files('frontend/dist')), - ('frontend/dist/assets',list_files('frontend/dist/assets')) + ('frontend/dist/assets',list_files('frontend/dist/assets')), + ('assets',['assets/caution.png']), ], ) From a863f96896fccf24bfc38ceeec9e22a0e6ee2f93 Mon Sep 17 00:00:00 2001 From: mauwii Date: Wed, 30 Nov 2022 07:19:48 +0100 Subject: [PATCH 2/4] use better fitting icon --- docs/features/NSFW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/NSFW.md b/docs/features/NSFW.md index 8775ef7853a..9a39fd09c39 100644 --- a/docs/features/NSFW.md +++ b/docs/features/NSFW.md @@ -2,7 +2,7 @@ title: The NSFW Checker --- -# :material-file-document: NSFW Checker +# :material-image-off: NSFW Checker ## The NSFW ("Safety") Checker From 483080a787a40b4af2a54aa0700416f965401f75 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 30 Nov 2022 17:15:18 +0000 Subject: [PATCH 3/4] NSFW defaults false for testing --- scripts/configure_invokeai.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/configure_invokeai.py b/scripts/configure_invokeai.py index 8b4b2ed7879..154edac64fa 100644 --- a/scripts/configure_invokeai.py +++ b/scripts/configure_invokeai.py @@ -604,9 +604,9 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): print(f'\nYou may change the chosen directories at any time by editing the --root and --outdir options in "{Globals.initfile}",') print(f'You may also change the runtime directory by setting the environment variable INVOKEAI_ROOT.\n') - enable_safety_checker = True - default_sampler = 'k_euler_a' - default_steps = '30' # deliberately a string - see test below + enable_safety_checker = False # FOR TESTING CI FAILURES ONLY + default_sampler = 'k_heun' + default_steps = '20' # deliberately a string - see test below sampler_choices =['ddim','k_dpm_2_a','k_dpm_2','k_euler_a','k_euler','k_heun','k_lms','plms'] From 808227cdd534446af7e7ad9813170be2cec71304 Mon Sep 17 00:00:00 2001 From: Lincoln Stein Date: Wed, 30 Nov 2022 18:51:12 +0000 Subject: [PATCH 4/4] set default back to nsfw active --- scripts/configure_invokeai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/configure_invokeai.py b/scripts/configure_invokeai.py index 154edac64fa..2bfefaa28c6 100644 --- a/scripts/configure_invokeai.py +++ b/scripts/configure_invokeai.py @@ -604,7 +604,7 @@ def initialize_rootdir(root:str,yes_to_all:bool=False): print(f'\nYou may change the chosen directories at any time by editing the --root and --outdir options in "{Globals.initfile}",') print(f'You may also change the runtime directory by setting the environment variable INVOKEAI_ROOT.\n') - enable_safety_checker = False # FOR TESTING CI FAILURES ONLY + enable_safety_checker = True default_sampler = 'k_heun' default_steps = '20' # deliberately a string - see test below