diff --git a/scripts/CFG Auto.py b/scripts/CFG Auto.py new file mode 100644 index 0000000000000000000000000000000000000000..f151ed235ed5c057accd306c798881c9167533e1 --- /dev/null +++ b/scripts/CFG Auto.py @@ -0,0 +1,368 @@ +#CFG Scheduler for Automatic1111 Stable Diffusion web-ui +#Author: https://github.com/guzuligo/ +#Based on: https://github.com/tkalayci71/attenuate-cfg-scale +#Version: 1.81 + +from logging import PlaceHolder +import math +import os +import sys +import traceback +import copy +import numpy as np +import modules.scripts as scripts +import gradio as gr + +#from modules.processing import Processed, process_images +from modules import images,processing +from modules.processing import process_images, Processed +from modules.processing import Processed +from modules.shared import opts, cmd_opts, state +class Script(scripts.Script): + def run(self,p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle): + return self.runBasic(p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle) + + #def run(self,p,cfg,eta,dns ,loops,nSingle): + # return self.runAdvanced(p,cfg,eta,dns ,loops,nSingle) + + def show(self, is_img2img): + self.isAdvanced=False + return True + def title(self): + return "CFG Scheduling" if (self.isAdvanced) else "CFG Auto" + + def uiAdvanced(self, is_img2img): + + placeholder="The steps on which to modify, in format step:value - example: 0:10 ; 10:15" + n0 = gr.Textbox(label="CFG",placeholder=placeholder) + placeholder="You can also use functions like: 0: math.fabs(-t) ; 1: (1-t/T) ; 2:=e ;3:t*d" + n1 = gr.Textbox(label="ETA",placeholder=placeholder) + #loops + #n2 = gr.Slider(minimum=1, maximum=32, step=1, label='Loops', value=1) + n2 = gr.Slider(minimum=0, maximum=1, step=0.01, label='Target Denoising : Decay per Batch', value=0.5) + with gr.Row(): + loops=gr.Number(value=1,precision=0,label="loops") + nSingle= gr.Checkbox(label="Loop returns one") + + return [n0,n1,n2 ,loops,nSingle] + #uiBasic + def uiAuto(self, is_img2img): + self.autoOptions={"b1":"Blur First V1","b2":"Blur Last","f1":"Force at Start V1","f2":"Force Allover"} + with gr.Row(): + dns = gr.Slider(minimum=0, maximum=1, step=0.01, label='Target Denoising : Decay per Batch', value=0.25) + n0=gr.Dropdown(list(self.autoOptions.values()),value=self.autoOptions["b1"],label="Scheduler") + with gr.Row(): + n1 = gr.Slider(minimum=0, maximum=100, step=1, label='Main Strength', value=10) + n2 = gr.Slider(minimum=0, maximum=100, step=1, label='Sub- Strength', value=10) + with gr.Row(): + n3 = gr.Slider(minimum=0, maximum=100, step=1, label='Main Range', value=10) + n4 = gr.Slider(minimum=0, maximum=100, step=1, label='Sub- Range', value=10) + with gr.Row(): + loops=gr.Number(value=1,precision=0,label="loops") + nSingle= gr.Checkbox(label="Loop returns one") + return [n0,dns, n1,n2,n3,n4 ,loops,nSingle] + + def ui(self, is_img2img): + return self.uiAdvanced(is_img2img) if (self.isAdvanced) else self.uiAuto(is_img2img) + + def prepare(self,p,cfg,eta): + sampler_name=p.sampler_name + if not sampler_name: + print("Warning: sampler not specified. Using Euler a") + sampler_name="Euler a" + #if p.sampler_index in (0,1,2,7,8,10,14): + if sampler_name in ('Euler a','Euler','LMS','DPM++ 2M','DPM fast','LMS Karras','DPM++ 2M Karras'): + max_mul_count = p.steps * p.batch_size + steps_per_mul = p.batch_size + #elif p.sampler_index in (3,4,5,6,11,12,13): + elif sampler_name in ('Heun','DPM2','DPM2 a','DPM++ 2S a','DPM2 Karras','DPM2 a Karras','DPM++ 2S a Karras'): + max_mul_count = ((p.steps*2)-1) * p.batch_size + steps_per_mul = 2 * p.batch_size + #elif p.sampler_index==15: # ddim + elif sampler_name=='DDIM': # ddim + max_mul_count = fix_ddim_step_count(p.steps) + steps_per_mul = 1 + #elif p.sampler_index==16: # plms + elif sampler_name=='PLMS': # plms + max_mul_count = fix_ddim_step_count(p.steps)+1 + steps_per_mul = 1 + else: + print("Not supported sampler", p.sampler_name, p.sampler_index) + return # 9=dpm adaptive + + + + #print("it is:",n0t) + #for x in range(int(n)): + self.p=p + cfg=cfg.strip() + eta=eta.strip() + if cfg: + p.cfg_scale=Fake_float(p.cfg_scale,self.split(cfg,str(p.cfg_scale)) , max_mul_count, steps_per_mul) + #p.cfg_scale.p=p + if eta: + if (eta.find("@")==-1): + p.s_churn=p.eta =Fake_float(p.eta or 1,self.split(eta,str(p.eta)) , max_mul_count, steps_per_mul) + #print(p.s_noise) + + #Fake_float(p.s_churn or 1,self.split(eta,str(p.s_churn)), max_mul_count, steps_per_mul) + else: + eta=eta.split("@") + if eta[0].strip()!="": + p.s_churn=Fake_float(p.s_churn or 1,self.split(eta[0],str(p.s_churn)), max_mul_count, steps_per_mul) + if len(eta)>1 and eta[1].strip()!="": + p.s_noise=Fake_float(p.s_noise or 1,self.split(eta[1],str(p.s_noise)), max_mul_count, steps_per_mul) + if len(eta)>2 and eta[2].strip()!="": + p.s_tmin=Fake_float(p.s_tmin or 1,self.split(eta[2],str(p.s_tmin)), max_mul_count, steps_per_mul) + if len(eta)>3 and eta[3].strip()!="": + p.s_tmax=Fake_float(p.s_tmax or 1,self.split(eta[2],str(p.s_tmax)), max_mul_count, steps_per_mul) + + + #p.cfg_scale.p=p + # + + + + def runBasic(self,p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle): + if(n0==self.autoOptions["b1"]): + cfg=f"""0:{ns2}/2 if (t{nr1}/100 or e<(1-({nr1}+{nr2}*(100-{nr1})/100)/100)) else {ns2}/10""" + eta=f"""0:e if (e>{nr1}/100 or e<(1-({nr1}+{nr2}*(100-{nr1})/100)/100)) else {ns1}/10""" + elif(n0==self.autoOptions["f2"]): + cfg=f"""= min(40,max(0,cfg+x(t)*({ns2}-50)/2 )) """ + eta=f"""0:(1-(t%(2+ 10-.1*{nr1} ))/ (2+10-.1*{nr1}) )*{ns1}*.1 * (e*(100-{nr2})+{nr2})*.01 """ + self.cfgsib={"Scheduler":n0,'Main Strength':ns1,'Sub- Strength':ns2,'Main Range':nr1,'Sub- Range':nr2} + return self.runAdvanced(p,cfg,eta,dns ,loops,nSingle) + + + def runAdvanced(self, p, cfg,eta,dns ,loops,nSingle): + self.initSeed=p.seed + #loops=p.batch_size + loops = loops if (loops>0) else 1 + + batch_count=p.n_iter + state.job_count = loops*p.n_iter + p.denoising_strength=p.denoising_strength or (1 if (self.isAdvanced) else 0.2) + initial_denoising_strength=p.denoising_strength + p.do_not_save_grid = True + if hasattr(p,"init_images"): + original_init_image = p.init_images + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + else: + original_init_image=None + + all_images = [] + cfgsi=" loops:"+str(loops)+" terget denoising: "+str(dns)+"\nCFG: "+cfg+"\nETA: "+eta+"\n" + + p.extra_generation_params = { + "CFG Scheduler Info":cfgsi, + } + + + #if basic, add basic info as well + if (self.isAdvanced==False): + self.cfgsib.update(p.extra_generation_params) + p.extra_generation_params=self.cfgsib + + if loops>1: + processing.fix_seed(p) + #self.initDenoise=p.denoising_strength + + for n in range(batch_count): + proc=None + history = [] + p.denoising_strength=initial_denoising_strength + if (original_init_image!=None): + p.init_images=original_init_image + for loop in range(loops): + if opts.img2img_color_correction and original_init_image!=None: + p.color_corrections = initial_color_corrections + + p.batch_size = 1 + p.n_iter = 1 + self.loop=loop + self.prepare(p, cfg,eta) + proc = process_images(p) + if loop==0: + self.initInfo=proc.info + self.initSeed=proc.seed + if len(proc.images)>0: + history.append(proc.images[0]) + p.seed+=1 + p.init_images=[proc.images[0]] + #p.denoising_strength=min(max(p.denoising_strength * dns, 0.05), 1) + p.denoising_strength=initial_denoising_strength+(dns-initial_denoising_strength)*(loop+1)/(loops) + else:#interrupted + break + #print("New denoising:"+str(p.denoising_strength)+"\n" ) + all_images += history + if loops>0:#TODO:maybe this is not needed + p.seed=self.initSeed + #return proc if (loops==1 and p.batch_size==1) else Processed(p, all_images, self.initSeed, self.initInfo) + return proc if(nSingle) else Processed(p, all_images, self.initSeed, self.initInfo) + + + + + + + + def peek(self,val): + print(val) + return val + + def split(self,src,default='0'): + p=self.p + self.P=copy.copy({ + 'cfg':float(str(p.cfg_scale)), + 'd':p.denoising_strength or 1, + 'l':self.loop, + 'min':min, + 'max':max, + 'abs':abs, + 'pow':pow, + 'pi':math.pi, + 'x':self._interpolate, + 'int':int, + 'floor':math.floor, + 'peek':self.peek, + }) + + if src[0:4]=="eval": + src="0:"+src[4:] + if src[0]=="=": + src="0:"+src[1:] + + #clean up + while src[len(src)-1] in [";"," "]: + src=src[0:len(src)-1] + while src[0] in [";"," "]: + src=src[1:] + + arr0 = src.split(';')##2 + + #resort array accounting for commas in indecies + arr=[] + for j in arr0: + #print(j) + v=j.split(":") + q=v[0].split(",") + + for i in q: + arr.append(i+":"+v[1]) + + + + + arr.sort(key=self._sort) + s=[] + val=default + for j in range(p.steps+1): + i=0 + while i 0) + + + def __mul__(self,other): + return self.fake_mul(other) + + def __rmul__(self,other): + return self.fake_mul(other) + + #def __add__(self,other): + #print("ADD!") + # return self.get_fake_value(other)+other + #def __sub__(self,other): + #print("SUB!") + # return self.get_fake_value(other)-other + + + + def fake_mul(self,other): + #print("MUL!") + return self.get_fake_value(other) * other + + + def get_fake_value(self,other): + if (self.max_step_count==1): + fake_value = self.arr[0] + else: + + fake_value = self.arr[self.curstep] + self.current_mul = (self.current_mul+1) % self.max_mul_count + self.curstep = (self.current_mul) // self.steps_per_mul + self.current_step+=1#FAKE STEP + return fake_value + + + + +def fix_ddim_step_count(steps): + valid_step = 999 / (1000 // steps) + if valid_step == int(valid_step): steps=int(valid_step)+1 + if ((1000 % steps)!=0): steps +=1 + return steps diff --git a/scripts/CFG Schedule.py b/scripts/CFG Schedule.py new file mode 100644 index 0000000000000000000000000000000000000000..1815a32129950d104e03950e991b51bb3a11dd43 --- /dev/null +++ b/scripts/CFG Schedule.py @@ -0,0 +1,369 @@ +#CFG Scheduler for Automatic1111 Stable Diffusion web-ui +#Author: https://github.com/guzuligo/ +#Based on: https://github.com/tkalayci71/attenuate-cfg-scale +#Version: 1.81 + +from logging import PlaceHolder +import math +import os +import sys +import traceback +import copy +import numpy as np +import modules.scripts as scripts +import gradio as gr + +#from modules.processing import Processed, process_images +from modules import images,processing +from modules.processing import process_images, Processed +from modules.processing import Processed +from modules.shared import opts, cmd_opts, state +class Script(scripts.Script): + #def run(self,p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle): + # return self.runBasic(p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle) + + def run(self,p,cfg,eta,dns ,loops,nSingle): + return self.runAdvanced(p,cfg,eta,dns ,loops,nSingle) + + def show(self, is_img2img): + self.isAdvanced=True + return True + def title(self): + return "CFG Scheduling" if (self.isAdvanced) else "CFG Auto" + + def uiAdvanced(self, is_img2img): + + placeholder="The steps on which to modify, in format step:value - example: 0:10 ; 10:15" + n0 = gr.Textbox(label="CFG",placeholder=placeholder) + placeholder="You can also use functions like: 0: math.fabs(-t) ; 1: (1-t/T) ; 2:=e ;3:t*d" + n1 = gr.Textbox(label="ETA",placeholder=placeholder) + #loops + #n2 = gr.Slider(minimum=1, maximum=32, step=1, label='Loops', value=1) + n2 = gr.Slider(minimum=0, maximum=1, step=0.01, label='Target Denoising : Decay per Batch', value=0.5) + with gr.Row(): + loops=gr.Number(value=1,precision=0,label="loops") + nSingle= gr.Checkbox(label="Loop returns one") + + return [n0,n1,n2 ,loops,nSingle] + #uiBasic + def uiAuto(self, is_img2img): + self.autoOptions={"b1":"Blur First V1","b2":"Blur Last","f1":"Force at Start V1","f2":"Force Allover"} + with gr.Row(): + dns = gr.Slider(minimum=0, maximum=1, step=0.01, label='Target Denoising : Decay per Batch', value=0.25) + n0=gr.Dropdown(list(self.autoOptions.values()),value=self.autoOptions["b1"],label="Scheduler") + with gr.Row(): + n1 = gr.Slider(minimum=0, maximum=100, step=1, label='Main Strength', value=10) + n2 = gr.Slider(minimum=0, maximum=100, step=1, label='Sub- Strength', value=10) + with gr.Row(): + n3 = gr.Slider(minimum=0, maximum=100, step=1, label='Main Range', value=10) + n4 = gr.Slider(minimum=0, maximum=100, step=1, label='Sub- Range', value=10) + with gr.Row(): + loops=gr.Number(value=1,precision=0,label="loops") + nSingle= gr.Checkbox(label="Loop returns one") + return [n0,dns, n1,n2,n3,n4 ,loops,nSingle] + + def ui(self, is_img2img): + return self.uiAdvanced(is_img2img) if (self.isAdvanced) else self.uiAuto(is_img2img) + + def prepare(self,p,cfg,eta): + sampler_name=p.sampler_name + if not sampler_name: + print("Warning: sampler not specified. Using Euler a") + sampler_name="Euler a" + #if p.sampler_index in (0,1,2,7,8,10,14): + if sampler_name in ('Euler a','Euler','LMS','DPM++ 2M','DPM fast','LMS Karras','DPM++ 2M Karras'): + max_mul_count = p.steps * p.batch_size + steps_per_mul = p.batch_size + #elif p.sampler_index in (3,4,5,6,11,12,13): + elif sampler_name in ('Heun','DPM2','DPM2 a','DPM++ 2S a','DPM2 Karras','DPM2 a Karras','DPM++ 2S a Karras'): + max_mul_count = ((p.steps*2)-1) * p.batch_size + steps_per_mul = 2 * p.batch_size + #elif p.sampler_index==15: # ddim + elif sampler_name=='DDIM': # ddim + max_mul_count = fix_ddim_step_count(p.steps) + steps_per_mul = 1 + #elif p.sampler_index==16: # plms + elif sampler_name=='PLMS': # plms + max_mul_count = fix_ddim_step_count(p.steps)+1 + steps_per_mul = 1 + else: + print("Not supported sampler", p.sampler_name, p.sampler_index) + return # 9=dpm adaptive + + + + #print("it is:",n0t) + #for x in range(int(n)): + self.p=p + cfg=cfg.strip() + eta=eta.strip() + if cfg: + p.cfg_scale=Fake_float(p.cfg_scale,self.split(cfg,str(p.cfg_scale)) , max_mul_count, steps_per_mul) + #p.cfg_scale.p=p + if eta: + if (eta.find("@")==-1): + p.s_churn=p.eta =Fake_float(p.eta or 1,self.split(eta,str(p.eta)) , max_mul_count, steps_per_mul) + #print(p.s_noise) + + #Fake_float(p.s_churn or 1,self.split(eta,str(p.s_churn)), max_mul_count, steps_per_mul) + else: + eta=eta.split("@") + if eta[0].strip()!="": + p.s_churn=Fake_float(p.s_churn or 1,self.split(eta[0],str(p.s_churn)), max_mul_count, steps_per_mul) + if len(eta)>1 and eta[1].strip()!="": + p.s_noise=Fake_float(p.s_noise or 1,self.split(eta[1],str(p.s_noise)), max_mul_count, steps_per_mul) + if len(eta)>2 and eta[2].strip()!="": + p.s_tmin=Fake_float(p.s_tmin or 1,self.split(eta[2],str(p.s_tmin)), max_mul_count, steps_per_mul) + if len(eta)>3 and eta[3].strip()!="": + p.s_tmax=Fake_float(p.s_tmax or 1,self.split(eta[2],str(p.s_tmax)), max_mul_count, steps_per_mul) + + + #p.cfg_scale.p=p + # + + + + def runBasic(self,p,n0,dns,ns1,ns2,nr1,nr2 ,loops,nSingle): + if(n0==self.autoOptions["b1"]): + cfg=f"""0:{ns2}/2 if (t{nr1}/100 or e<(1-({nr1}+{nr2}*(100-{nr1})/100)/100)) else {ns2}/10""" + eta=f"""0:e if (e>{nr1}/100 or e<(1-({nr1}+{nr2}*(100-{nr1})/100)/100)) else {ns1}/10""" + elif(n0==self.autoOptions["f2"]): + cfg=f"""= min(40,max(0,cfg+x(t)*({ns2}-50)/2 )) """ + eta=f"""0:(1-(t%(2+ 10-.1*{nr1} ))/ (2+10-.1*{nr1}) )*{ns1}*.1 * (e*(100-{nr2})+{nr2})*.01 """ + self.cfgsib={"Scheduler":n0,'Main Strength':ns1,'Sub- Strength':ns2,'Main Range':nr1,'Sub- Range':nr2} + self.cfgsib={"Scheduler":n0,'Main Strength':ns1,'Sub- Strength':ns2,'Main Range':nr1,'Sub- Range':nr2} + return self.runAdvanced(p,cfg,eta,dns ,loops,nSingle) + + + def runAdvanced(self, p, cfg,eta,dns ,loops,nSingle): + self.initSeed=p.seed + #loops=p.batch_size + loops = loops if (loops>0) else 1 + + batch_count=p.n_iter + state.job_count = loops*p.n_iter + p.denoising_strength=p.denoising_strength or (1 if (self.isAdvanced) else 0.2) + initial_denoising_strength=p.denoising_strength + p.do_not_save_grid = True + if hasattr(p,"init_images"): + original_init_image = p.init_images + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + else: + original_init_image=None + + all_images = [] + cfgsi=" loops:"+str(loops)+" terget denoising: "+str(dns)+"\nCFG: "+cfg+"\nETA: "+eta+"\n" + + p.extra_generation_params = { + "CFG Scheduler Info":cfgsi, + } + + + #if basic, add basic info as well + if (self.isAdvanced==False): + self.cfgsib.update(p.extra_generation_params) + p.extra_generation_params=self.cfgsib + + if loops>1: + processing.fix_seed(p) + #self.initDenoise=p.denoising_strength + + for n in range(batch_count): + proc=None + history = [] + p.denoising_strength=initial_denoising_strength + if (original_init_image!=None): + p.init_images=original_init_image + for loop in range(loops): + if opts.img2img_color_correction and original_init_image!=None: + p.color_corrections = initial_color_corrections + + p.batch_size = 1 + p.n_iter = 1 + self.loop=loop + self.prepare(p, cfg,eta) + proc = process_images(p) + if loop==0: + self.initInfo=proc.info + self.initSeed=proc.seed + if len(proc.images)>0: + history.append(proc.images[0]) + p.seed+=1 + p.init_images=[proc.images[0]] + #p.denoising_strength=min(max(p.denoising_strength * dns, 0.05), 1) + p.denoising_strength=initial_denoising_strength+(dns-initial_denoising_strength)*(loop+1)/(loops) + else:#interrupted + break + #print("New denoising:"+str(p.denoising_strength)+"\n" ) + all_images += history + if loops>0:#TODO:maybe this is not needed + p.seed=self.initSeed + #return proc if (loops==1 and p.batch_size==1) else Processed(p, all_images, self.initSeed, self.initInfo) + return proc if(nSingle) else Processed(p, all_images, self.initSeed, self.initInfo) + + + + + + + + def peek(self,val): + print(val) + return val + + def split(self,src,default='0'): + p=self.p + self.P=copy.copy({ + 'cfg':float(str(p.cfg_scale)), + 'd':p.denoising_strength or 1, + 'l':self.loop, + 'min':min, + 'max':max, + 'abs':abs, + 'pow':pow, + 'pi':math.pi, + 'x':self._interpolate, + 'int':int, + 'floor':math.floor, + 'peek':self.peek, + }) + + if src[0:4]=="eval": + src="0:"+src[4:] + if src[0]=="=": + src="0:"+src[1:] + + #clean up + while src[len(src)-1] in [";"," "]: + src=src[0:len(src)-1] + while src[0] in [";"," "]: + src=src[1:] + + arr0 = src.split(';')##2 + + #resort array accounting for commas in indecies + arr=[] + for j in arr0: + #print(j) + v=j.split(":") + q=v[0].split(",") + + for i in q: + arr.append(i+":"+v[1]) + + + + + arr.sort(key=self._sort) + s=[] + val=default + for j in range(p.steps+1): + i=0 + while i 0) + + + def __mul__(self,other): + return self.fake_mul(other) + + def __rmul__(self,other): + return self.fake_mul(other) + + #def __add__(self,other): + #print("ADD!") + # return self.get_fake_value(other)+other + #def __sub__(self,other): + #print("SUB!") + # return self.get_fake_value(other)-other + + + + def fake_mul(self,other): + #print("MUL!") + return self.get_fake_value(other) * other + + + def get_fake_value(self,other): + if (self.max_step_count==1): + fake_value = self.arr[0] + else: + + fake_value = self.arr[self.curstep] + self.current_mul = (self.current_mul+1) % self.max_mul_count + self.curstep = (self.current_mul) // self.steps_per_mul + self.current_step+=1#FAKE STEP + return fake_value + + + + +def fix_ddim_step_count(steps): + valid_step = 999 / (1000 // steps) + if valid_step == int(valid_step): steps=int(valid_step)+1 + if ((1000 % steps)!=0): steps +=1 + return steps diff --git a/scripts/ContorlNet_I2I_sequence_toyxyz_V2.py b/scripts/ContorlNet_I2I_sequence_toyxyz_V2.py new file mode 100644 index 0000000000000000000000000000000000000000..4d107582707c1a4e75ea287e042eafcc60639bcc --- /dev/null +++ b/scripts/ContorlNet_I2I_sequence_toyxyz_V2.py @@ -0,0 +1,367 @@ +import copy +import os +import shutil + +import cv2 +import gradio as gr +import numpy as np +import modules.scripts as scripts + +from modules import images, processing +from modules.processing import process_images, Processed +from modules.shared import opts +from PIL import Image, ImageFilter, ImageColor, ImageOps +from pathlib import Path +from typing import List, Tuple, Iterable + + +#Returns a list of images located in the input path. For ControlNet iamges +def get_all_frames_from_path(path): + if not os.path.isdir(path): + return None + frame_list = [] + for filename in sorted(os.listdir(path)): + if filename.endswith(".jpg") or filename.endswith(".png"): + img_path = os.path.join(path, filename) + img = cv2.imread(img_path) + if img is not None: + frame_list.append(img) + frame_list.insert(0, frame_list[0]) + return frame_list + + +#Returns a list of images located in the input path. For Color iamges +def get_images_from_path(path): + if not os.path.isdir(path): + return None + images = [] + for filename in os.listdir(path): + if filename.endswith('.jpg') or filename.endswith('.png'): + img_path = os.path.join(path, filename) + img = Image.open(img_path) + images.append(img) + images.append(images[-1]) + images.insert(0, images[0]) + return images + +#Returns the number of the smallest number in the entire image sequence list. For ControlNet +def get_min_frame_num(video_list): + min_frame_num = -1 + for video in video_list: + if video is None: + continue + else: + frame_num = len(video) + print(frame_num) + if min_frame_num < 0: + min_frame_num = frame_num + elif frame_num < min_frame_num: + min_frame_num = frame_num + return min_frame_num + + +#Blende method + + +def basic(target, blend, opacity): + return target * opacity + blend * (1-opacity) + +def blender(func): + def blend(target, blend, opacity=1, *args): + res = func(target, blend, *args) + res = basic(res, blend, opacity) + return np.clip(res, 0, 1) + return blend + + +class Blend: + @classmethod + def method(cls, name): + return getattr(cls, name) + + normal = basic + + @staticmethod + @blender + def darken(target, blend, *args): + return np.minimum(target, blend) + + @staticmethod + @blender + def multiply(target, blend, *args): + return target * blend + + @staticmethod + @blender + def color_burn(target, blend, *args): + return 1 - (1-target)/blend + + @staticmethod + @blender + def linear_burn(target, blend, *args): + return target+blend-1 + + @staticmethod + @blender + def lighten(target, blend, *args): + return np.maximum(target, blend) + + @staticmethod + @blender + def screen(target, blend, *args): + return 1 - (1-target) * (1-blend) + + @staticmethod + @blender + def color_dodge(target, blend, *args): + return target/(1-blend) + + @staticmethod + @blender + def linear_dodge(target, blend, *args): + return target+blend + + @staticmethod + @blender + def overlay(target, blend, *args): + return (target>0.5) * (1-(2-2*target)*(1-blend)) +\ + (target<=0.5) * (2*target*blend) + + @staticmethod + @blender + def soft_light(target, blend, *args): + return (blend>0.5) * (1 - (1-target)*(1-(blend-0.5))) +\ + (blend<=0.5) * (target*(blend+0.5)) + + @staticmethod + @blender + def hard_light(target, blend, *args): + return (blend>0.5) * (1 - (1-target)*(2-2*blend)) +\ + (blend<=0.5) * (2*target*blend) + + @staticmethod + @blender + def vivid_light(target, blend, *args): + return (blend>0.5) * (1 - (1-target)/(2*blend-1)) +\ + (blend<=0.5) * (target/(1-2*blend)) + + @staticmethod + @blender + def linear_light(target, blend, *args): + return (blend>0.5) * (target + 2*(blend-0.5)) +\ + (blend<=0.5) * (target + 2*blend) + + @staticmethod + @blender + def pin_light(target, blend, *args): + return (blend>0.5) * np.maximum(target,2*(blend-0.5)) +\ + (blend<=0.5) * np.minimum(target,2*blend) + + @staticmethod + @blender + def difference(target, blend, *args): + return np.abs(target - blend) + + @staticmethod + @blender + def exclusion(target, blend, *args): + return 0.5 - 2*(target-0.5)*(blend-0.5) + +blend_methods = [i for i in Blend.__dict__.keys() if i[0]!='_' and i!='method'] + + + +def blend_images(base_img, blend_img, blend_method, blend_opacity, do_invert): + + img_base = np.array(base_img.convert("RGB")).astype(np.float64)/255 + + if do_invert: + img_to_blend = ImageOps.invert(blend_img.convert('RGB')) + else: + img_to_blend = blend_img + + img_to_blend = img_to_blend.resize((int(base_img.width), int(base_img.height))) + + img_to_blend = np.array(img_to_blend.convert("RGB")).astype(np.float64)/255 + + img_blended = Blend.method(blend_method)(img_to_blend, img_base, blend_opacity) + + img_blended *= 255 + + img_blended = Image.fromarray(img_blended.astype(np.uint8), mode='RGB') + + return img_blended + + +#Define UI and script properties. +class Script(scripts.Script): + + def title(self): + return "controlnet I2I sequence_toyxyz_v2" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + + ctrls_group = () + max_models = opts.data.get("control_net_max_models_num", 1) + + input_list = [] + + with gr.Group(): + with gr.Accordion("ControlNet-I2I-sequence-toyxyz", open = True): + with gr.Column(): + + feed_prev_frame = gr.Checkbox(value=False, label="Feed previous frame / Reduce flickering by feeding the previous frame image generated by Img2Img") + + use_init_img = gr.Checkbox(value=False, label="Blend color image / Blend the color image sequence with the initial Img2Img image or previous frame") + + use_TemporalNet = gr.Checkbox(value=False, label="Use TemporalNet / Using TemporalNet to reduce flicker between image sequences. Add TemporalNet in addition to the multi-controlnet you need. It should be placed at the end of the controlnet list.") + + blendmode = gr.Dropdown(blend_methods, value='normal', label='Blend mode / Choose how to blend the color image with the Previous frame or Img2Img initial image') + + opacityvalue = gr.Slider(0, 1, value=0, label="Opacity / Previous frame or Img2Img initial image + (color image * opacity)", info="Choose betwen 0 and 1") + + + for i in range(max_models): + input_path = gr.Textbox(label=f"ControlNet-{i}", placeholder="image sequence path") + input_list.append(input_path) + + tone_image_path = gr.Textbox(label=f"Color_Image / Color images to be used for Img2Img in sequence", placeholder="image sequence path") + + output_path = gr.Textbox(label=f"Output_path / Deletes the contents located in the path, and creates a new path if it does not exist", placeholder="Output path") + + ctrls_group += tuple(input_list) + (use_TemporalNet, use_init_img, opacityvalue, blendmode, feed_prev_frame, tone_image_path, output_path) + + return ctrls_group + + + + #Image Generate Definition + def run(self, p, *args): + + path = p.outpath_samples + + output_path = args[-1] # get the last argument, which is the output path + + feedprev = args[-3] + + blendm = args[-4] + + opacityval = args[-5] + + useinit = args[-6] + + usetempo = args[-7] + + + # Check whether the output path exists, if it does, delete it and create a new one. + if os.path.isdir(output_path): + for file in os.scandir(output_path): + os.remove(file.path) + else : + os.mkdir(output_path) + + #Get the number of controlnet models. + video_num = opts.data.get("control_net_max_models_num", 1) + + # Get the ControlNet image sequence list. + image_list = [get_all_frames_from_path(image) for image in args[:video_num]] + + # Get a list of color image sequences. + color_image_list = get_images_from_path(args[-2]) + + # Get the first frame + previmg = p.init_images + + tempoimg = p.init_images[0] + + #If img2img color correction is enabled in webui settings, color correction is performed based on the first frame. + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + + #Save initial img2img image + initial_image = p.init_images[0] + + # Get the total number of frames. + frame_num = get_min_frame_num(image_list) + + # image processing + if frame_num > 0: + output_image_list = [] + + for frame in range(frame_num): + copy_p = copy.copy(p) + copy_p.control_net_input_image = [] + for video in image_list: + if video is None: + continue + copy_p.control_net_input_image.append(video[frame]) + + if usetempo == True : + copy_p.control_net_input_image.append(tempoimg) + + + if color_image_list and feedprev == False: + + if frame 0 : + tempoimg = proc.images[0] + + + #Save image + if(frame>0): + images.save_image(img, output_path, f"Frame_{frame}") + copy_p.close() + + + else: + proc = process_images(p) + + return proc \ No newline at end of file diff --git a/scripts/LoopbackWave.py b/scripts/LoopbackWave.py new file mode 100644 index 0000000000000000000000000000000000000000..b38a154d8ddefbcc45c80c041eb92f26d0096635 --- /dev/null +++ b/scripts/LoopbackWave.py @@ -0,0 +1,348 @@ +import os +import platform +import numpy as np +from tqdm import trange +import math +import subprocess as sp +import string +import random +from functools import reduce +import re + +import modules.scripts as scripts +import gradio as gr + +from datetime import datetime +from modules import processing, shared, sd_samplers, images +from modules.processing import Processed +from modules.sd_samplers import samplers +from modules.shared import opts, cmd_opts, state +import subprocess + + +wave_completed_regex = r'@wave_completed\(([\-]?[0-9]*\.?[0-9]+), ?([\-]?[0-9]*\.?[0-9]+)\)' +wave_remaining_regex = r'@wave_remaining\(([\-]?[0-9]*\.?[0-9]+), ?([\-]?[0-9]*\.?[0-9]+)\)' + +def run_cmd(cmd): + cmd = list(map(lambda arg: str(arg), cmd)) + print("Executing %s" % " ".join(cmd)) + popen_params = {"stdout": sp.DEVNULL, "stderr": sp.PIPE, "stdin": sp.DEVNULL} + + if os.name == "nt": + popen_params["creationflags"] = 0x08000000 + + proc = sp.Popen(cmd, **popen_params) + out, err = proc.communicate() # proc.wait() + proc.stderr.close() + + if proc.returncode: + raise IOError(err.decode("utf8")) + + del proc + +def encode_video(input_pattern, starting_number, output_dir, fps, quality, encoding, create_segments, segment_duration, ffmpeg_path): + two_pass = (encoding == "VP9 (webm)") + alpha_channel = ("webm" in encoding) + suffix = "webm" if "webm" in encoding else "mp4" + output_location = output_dir + f".{suffix}" + + encoding_lib = { + "VP9 (webm)": "libvpx-vp9", + "VP8 (webm)": "libvpx", + "H.264 (mp4)": "libx264", + "H.265 (mp4)": "libx265", + }[encoding] + + args = [ + "-framerate", fps, + "-start_number", int(starting_number), + "-i", input_pattern, + "-c:v", encoding_lib, + "-b:v","0", + "-crf", quality, + ] + + if encoding_lib == "libvpx-vp9": + args += ["-pix_fmt", "yuva420p"] + + if(ffmpeg_path == ""): + ffmpeg_path = "ffmpeg" + if(platform.system == "Windows"): + ffmpeg_path += ".exe" + + print("\n\n") + if two_pass: + first_pass_args = args + [ + "-pass", "1", + "-an", + "-f", "null", + os.devnull + ] + + second_pass_args = args + [ + "-pass", "2", + output_location + ] + + print("Running first pass ffmpeg encoding") + + run_cmd([ffmpeg_path] + first_pass_args) + print("Running second pass ffmpeg encoding. This could take awhile...") + run_cmd([ffmpeg_path] + second_pass_args) + else: + print("Running ffmpeg encoding. This could take awhile...") + run_cmd([ffmpeg_path] + args + [output_location]) + + if(create_segments): + print("Segmenting video") + run_cmd([ffmpeg_path] + [ + "-i", output_location, + "-f", "segment", + "-segment_time", segment_duration, + "-vcodec", "copy", + "-acodec", "copy", + f"{output_dir}.%d.{suffix}" + ]) + +def set_weights(match_obj, wave_progress): + weight_0 = 0 + weight_1 = 0 + if match_obj.group(1) is not None: + weight_0 = float(match_obj.group(1)) + if match_obj.group(2) is not None: + weight_1 = float(match_obj.group(2)) + + max_weight = max(weight_0, weight_1) + min_weight = min(weight_0, weight_1) + + weight_range = max_weight - min_weight + weight = min_weight + weight_range * wave_progress + return str(weight) + + +class Script(scripts.Script): + def title(self): + return "Loopback Wave V1.4.1" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + frames = gr.Slider(minimum=1, maximum=2048, step=1, label='Frames', value=100) + frames_per_wave = gr.Slider(minimum=0, maximum=120, step=1, label='Frames Per Wave', value=20) + denoising_strength_change_amplitude = gr.Slider(minimum=0, maximum=1, step=0.01, label='Max additional denoise', value=0.6) + denoising_strength_change_offset = gr.Number(minimum=0, maximum=180, step=1, label='Wave offset (ignore this if you don\'t know what it means)', value=0) + initial_image_number = gr.Number(minimum=0, label='Initial generated image number', value=0) + + save_prompts = gr.Checkbox(label='Save prompts as text file', value=True) + prompts = gr.Textbox(label="Prompt Changes", lines=5, value="") + + save_video = gr.Checkbox(label='Save results as video', value=True) + output_dir = gr.Textbox(label="Video Name", lines=1, value="") + video_fps = gr.Slider(minimum=1, maximum=120, step=1, label='Frames per second', value=10) + video_quality = gr.Slider(minimum=0, maximum=60, step=1, label='Video Quality (crf)', value=40) + video_encoding = gr.Dropdown(label='Video encoding', value="VP9 (webm)", choices=["VP9 (webm)", "VP8 (webm)", "H.265 (mp4)", "H.264 (mp4)"]) + ffmpeg_path = gr.Textbox(label="ffmpeg binary. Only set this if it fails otherwise.", lines=1, value="") + + segment_video = gr.Checkbox(label='Cut video in to segments', value=True) + video_segment_duration = gr.Slider(minimum=10, maximum=60, step=1, label='Video Segment Duration (seconds)', value=20) + + + return [frames, denoising_strength_change_amplitude, frames_per_wave, denoising_strength_change_offset,initial_image_number, prompts, save_prompts, save_video, output_dir, video_fps, video_quality, video_encoding, ffmpeg_path, segment_video, video_segment_duration] + + def run(self, p, frames, denoising_strength_change_amplitude, frames_per_wave, denoising_strength_change_offset, initial_image_number, prompts: str,save_prompts, save_video, output_dir, video_fps, video_quality, video_encoding, ffmpeg_path, segment_video, video_segment_duration): + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Max Additional Denoise": denoising_strength_change_amplitude, + "Frames per wave": frames_per_wave, + "Wave Offset": denoising_strength_change_offset, + } + + # We save them ourselves for the sake of ffmpeg + p.do_not_save_samples = True + + changes_dict = {} + + + p.batch_size = 1 + p.n_iter = 1 + + output_images, info = None, None + initial_seed = None + initial_info = None + + grids = [] + all_images = [] + original_init_image = p.init_images + state.job_count = frames * batch_count + + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + initial_denoising_strength = p.denoising_strength + + if(output_dir==""): + output_dir = str(p.seed) + else: + output_dir = output_dir + "-" + str(p.seed) + + loopback_wave_path = os.path.join(p.outpath_samples, "loopback-wave") + loopback_wave_images_path = os.path.join(loopback_wave_path, output_dir) + + os.makedirs(loopback_wave_images_path, exist_ok=True) + + p.outpath_samples = loopback_wave_images_path + + prompts = prompts.strip() + + if save_prompts: + with open(loopback_wave_images_path + "-prompts.txt", "w") as f: + generation_settings = [ + "Generation Settings", + f"Total Frames: {frames}", + f"Frames Per Wave: {frames_per_wave}", + f"Wave Offset: {denoising_strength_change_offset}", + f"Base Denoising Strength: {initial_denoising_strength}", + f"Max Additional Denoise: {denoising_strength_change_amplitude}", + f"Initial Image Number: {initial_image_number}", + "", + "Video Encoding Settings", + f"Save Video: {save_video}" + ] + + if save_video: + generation_settings = generation_settings + [ + f"Framerate: {video_fps}", + f"Quality: {video_quality}", + f"Encoding: {video_encoding}", + f"Create Segmented Video: {segment_video}" + ] + + if segment_video: + generation_settings = generation_settings + [f"Segment Duration: {video_segment_duration}"] + + generation_settings = generation_settings + [ + "", + "Prompt Details", + "Initial Prompt:" + p.prompt, + "", + "Negative Prompt:" + p.negative_prompt, + "", + "Frame change prompts:", + prompts + ] + + + + f.write('\n'.join(generation_settings)) + + if prompts: + lines = prompts.split("\n") + for prompt_line in lines: + params = prompt_line.split("::") + if len(params) == 2: + changes_dict[params[0]] = { "prompt": params[1] } + elif len(params) == 3: + changes_dict[params[0]] = { "seed": params[1], "prompt": params[2] } + else: + raise IOError(f"Invalid input in prompt line: {prompt_line}") + + raw_prompt = p.prompt + + for n in range(batch_count): + history = [] + + # Reset to original init image at the start of each batch + p.init_images = original_init_image + + seed_state = "adding" + current_seed = p.seed + + for i in range(frames): + current_seed = p.seed + state.job = "" + + if str(i) in changes_dict: + raw_prompt = changes_dict[str(i)]["prompt"] + state.job = "New prompt: %s\n" % raw_prompt + + if "seed" in changes_dict[str(i)]: + current_seed = changes_dict[str(i)]["seed"] + + if current_seed.startswith("+"): + seed_state = "adding" + current_seed = current_seed.strip("+") + elif current_seed.startswith("-"): + seed_state = "subtracting" + current_seed = current_seed.strip("-") + else: + seed_state = "constant" + + current_seed = int(current_seed) + p.seed = current_seed + + + + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if opts.img2img_color_correction: + p.color_corrections = initial_color_corrections + + + wave_progress = float(1)/(float(frames_per_wave - 1))*float(((float(i)%float(frames_per_wave)) + ((float(1)/float(180))*denoising_strength_change_offset))) + print(wave_progress) + new_prompt = re.sub(wave_completed_regex, lambda x: set_weights(x, wave_progress), raw_prompt) + new_prompt = re.sub(wave_remaining_regex, lambda x: set_weights(x, 1 - wave_progress), new_prompt) + p.prompt = new_prompt + + print(new_prompt) + + denoising_strength_change_rate = 180/frames_per_wave + + cos = abs(math.cos(math.radians(i*denoising_strength_change_rate + denoising_strength_change_offset))) + p.denoising_strength = initial_denoising_strength + denoising_strength_change_amplitude - (cos * denoising_strength_change_amplitude) + + state.job += f"Iteration {i + 1}/{frames}, batch {n + 1}/{batch_count}. Denoising Strength: {p.denoising_strength}" + + processed = processing.process_images(p) + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + init_img = processed.images[0] + + p.init_images = [init_img] + + if seed_state == "adding": + p.seed = processed.seed + 1 + elif seed_state == "subtracting": + p.seed = processed.seed - 1 + + image_number = int(initial_image_number + i) + images.save_image(init_img, p.outpath_samples, "", processed.seed, processed.prompt, forced_filename=str(image_number)) + + history.append(init_img) + + grid = images.image_grid(history, rows=1) + if opts.grid_save: + images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + grids.append(grid) + all_images += history + + if opts.return_grid: + all_images = grids + all_images + + if save_video: + now = datetime.now() # get the current date and time + date_string = now.strftime("%Y-%m-%d") + input_pattern = os.path.join(loopback_wave_images_path, date_string,"%d.png") + encode_video(input_pattern, initial_image_number, loopback_wave_images_path, video_fps, video_quality, video_encoding, segment_video, video_segment_duration, ffmpeg_path) + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed + + diff --git a/scripts/Txt2img2img2img.py b/scripts/Txt2img2img2img.py new file mode 100644 index 0000000000000000000000000000000000000000..accbc57e50da77b56134b48c470942256aa72d2b --- /dev/null +++ b/scripts/Txt2img2img2img.py @@ -0,0 +1,291 @@ +from modules.shared import opts, cmd_opts, state +from modules.processing import Processed, StableDiffusionProcessingImg2Img, process_images, images +from modules import paths, shared, modelloader, sd_models +from modules import sd_samplers +from PIL import Image, ImageDraw +import gradio as gr +import modules.scripts as scripts +from random import randint +from skimage.util import random_noise +from gradio.processing_utils import encode_pil_to_base64 +import numpy as np +import os.path +from copy import deepcopy +def remap_range(value, minIn, MaxIn, minOut, maxOut): + if value > MaxIn: value = MaxIn; + if value < minIn: value = minIn; + if (MaxIn - minIn) == 0 : return maxOut + finalValue = ((value - minIn) / (MaxIn - minIn)) * (maxOut - minOut) + minOut; + return finalValue; + +class Script(scripts.Script): + def title(self): + return "Txt2img2img2img" + + def ui(self, is_img2img): + if is_img2img: return + + # samplers list + img2img_samplers_names = [s.name for s in sd_samplers.samplers_for_img2img] + + # models list + model_dir = "Stable-diffusion" + model_path = os.path.abspath(os.path.join(paths.models_path, model_dir)) + model_list = modelloader.load_models(model_path=model_path, command_path=shared.cmd_opts.ckpt_dir, ext_filter=[".ckpt", ".safetensors"], download_name="v1-5-pruned-emaonly.safetensors", ext_blacklist=[".vae.ckpt", ".vae.safetensors"]) + model_list = [m.split("\\")[-1] for m in model_list] + model_list.append("Same") + + t2iii_reprocess = gr.Slider(minimum=1, maximum=128, step=1, label='Number of img2img', value=1) + t2iii_steps = gr.Slider(minimum=1, maximum=120, step=1, label='img2img steps', value=24) + with gr.Row(): + t2iii_cfg_scale = gr.Slider(minimum=1, maximum=30, step=0.1, label='img2img cfg scale ', value=8.3) + t2iii_cfg_scale_end = gr.Slider(minimum=0, maximum=30, step=0.1, label='img2img cfg end scale (0=disabled) ', value=0) + with gr.Row(): + t2iii_denoising_strength = gr.Slider(minimum=0, maximum=1, step=0.01, label='img2img denoising strength ', value=0.42) + t2iii_patch_end_denoising = gr.Slider(minimum=0, maximum=1, step=0.01, label='Last img denoising (0=disabled)', value=0) + t2iii_seed_shift = gr.Slider(minimum=-1, maximum=1000000, step=1, label='img2img new seed+ (-1 for random)', value=1) + with gr.Row(): + t2iii_patch_upscale = gr.Checkbox(label='Patch upscale', value=False) + t2iii_patch_shift = gr.Checkbox(label='Patch upscale shift grid', value=True) + t2iii_save_first = gr.Checkbox(label='Save first image', value=False) + t2iii_only_last = gr.Checkbox(label='Only save last img2img', value=True) + t2iii_face_correction = gr.Checkbox(label='Face correction on all', value=False) + t2iii_face_correction_last = gr.Checkbox(label='Face correction on last', value=False) + with gr.Row(): + t2iii_model = gr.Dropdown(label="Model", choices=model_list, value="Same") + t2iii_sampler = gr.Dropdown(label="Sampler", choices=img2img_samplers_names, value="DPM++ 2M") + # with gr.Row(): + # t2iii_override_s_noise = gr.Checkbox(label='Override sampler\'s noise for last pass', value=False) + # t2iii_sampler_noise = gr.Slider(minimum=0, maximum=1, step=0.5, label='Sampler\'s noise for last pass', value=0) + with gr.Row(): + t2iii_clip = gr.Slider(minimum=0, maximum=12, step=1, label='change clip for img2img (0 = disabled)', value=0) + t2iii_noise = gr.Slider(minimum=0, maximum=0.005, step=0.0001, label='Add noise before img2img ', value=0) + with gr.Row(): + t2iii_patch_square_size = gr.Slider(minimum=64, maximum=2048, step=64, label='Patch upscale square size', value=512) + t2iii_patch_padding = gr.Slider(minimum=0, maximum=512, step=8, label='Patch upscale padding', value=128) + with gr.Row(): + t2iii_patch_border = gr.Slider(minimum=0, maximum=64, step=1, label='Patch upscale mask inner border', value=8) + t2iii_patch_mask_blur = gr.Slider(minimum=0, maximum=64 , step=1, label='Patch upscale mask blur', value=4) + with gr.Row(): + t2iii_upscale_x = gr.Slider(minimum=64, maximum=16384, step=64, label='img2img width (64 = no rescale) ', value=768) + t2iii_upscale_y = gr.Slider(minimum=64, maximum=16384, step=64, label='img2img height (64 = no rescale) ', value=960) + t2iii_2x_last = gr.Slider(minimum=1, maximum=4, step=0.1, label='resize time x size for last pass', value=1) + with gr.Row(): + t2iii_replace_prompt = gr.Checkbox(label='Replace the prompt', value=False) + t2iii_replace_negative_prompt = gr.Checkbox(label='Replace the negative prompt', value=False) + add_to_prompt = gr.Textbox(label="Add to prompt", lines=2, max_lines=2000) + add_to_negative_prompt = gr.Textbox(label="Add to prompt", lines=2, max_lines=2000) + + return [t2iii_reprocess,t2iii_steps,t2iii_cfg_scale,t2iii_seed_shift,t2iii_denoising_strength,t2iii_patch_upscale,t2iii_patch_shift,t2iii_2x_last,t2iii_save_first,t2iii_only_last,t2iii_face_correction,t2iii_face_correction_last, t2iii_model, t2iii_sampler,t2iii_clip,t2iii_noise,t2iii_patch_padding,t2iii_patch_square_size,t2iii_patch_border,t2iii_patch_mask_blur,t2iii_patch_end_denoising,t2iii_upscale_x,t2iii_upscale_y,add_to_prompt,add_to_negative_prompt,t2iii_replace_prompt,t2iii_replace_negative_prompt,t2iii_cfg_scale_end] + def run(self,p,t2iii_reprocess,t2iii_steps,t2iii_cfg_scale,t2iii_seed_shift,t2iii_denoising_strength,t2iii_patch_upscale,t2iii_patch_shift,t2iii_2x_last,t2iii_save_first,t2iii_only_last,t2iii_face_correction,t2iii_face_correction_last, t2iii_model, t2iii_sampler,t2iii_clip,t2iii_noise,t2iii_patch_padding,t2iii_patch_square_size,t2iii_patch_border,t2iii_patch_mask_blur,t2iii_patch_end_denoising,t2iii_upscale_x,t2iii_upscale_y,add_to_prompt,add_to_negative_prompt,t2iii_replace_prompt,t2iii_replace_negative_prompt,t2iii_cfg_scale_end): + def add_noise_to_image(img,seed,t2iii_noise): + img = np.array(img) + img = random_noise(img, mode='gaussian', seed=proc.seed, clip=True, var=t2iii_noise) + img = np.array(255*img, dtype = 'uint8') + img = Image.fromarray(np.array(img)) + return img + def create_mask(size, border_width): + im = Image.new('RGB', (size, size), color='white') + draw = ImageDraw.Draw(im) + draw.rectangle((0, 0, size, size), outline='black', width=border_width) + return im + def clean_model_name(model_name): + if "[" in model_name: + model_name = model_name.split(" ")[-2] + return model_name + def get_current_model_name(): + return clean_model_name(shared.opts.sd_model_checkpoint) + def is_model_loaded(wanted): + return wanted == get_current_model_name() + + img2img_samplers_names = [s.name for s in sd_samplers.samplers_for_img2img] + img2img_sampler_index = [i for i in range(len(img2img_samplers_names)) if img2img_samplers_names[i] == t2iii_sampler][0] + + + + # models paths + model_dir = "Stable-diffusion" + model_path = os.path.abspath(os.path.join(paths.models_path, model_dir)) + initial_model = get_current_model_name() + initial_model_path = os.path.abspath(os.path.join(model_path,initial_model)) + + secondary_model_name = clean_model_name(t2iii_model) + secondary_model_path = "" + if t2iii_model != "Same": + secondary_model_name = clean_model_name(t2iii_model) + secondary_model_path = os.path.abspath(os.path.join(model_path,secondary_model_name)) + + + + if p.seed == -1: p.seed = randint(0,1000000000) + + initial_CLIP = opts.data["CLIP_stop_at_last_layers"] + p.do_not_save_samples = not t2iii_save_first + initial_prompt = deepcopy(p.prompt) + initial_negative_prompt = deepcopy(p.negative_prompt) + + n_iter=p.n_iter + for j in range(n_iter): + + if t2iii_model != "Same": + if not is_model_loaded(initial_model): + print() + sd_models.load_model(sd_models.CheckpointInfo(initial_model_path)) + + p.n_iter=1 + if t2iii_clip > 0: + opts.data["CLIP_stop_at_last_layers"] = initial_CLIP + + p.prompt = initial_prompt + p.negative_prompt = initial_negative_prompt + + # PROCESS IMAGE + proc = process_images(p) + + if add_to_prompt != "" or t2iii_replace_prompt: + if t2iii_replace_prompt: + p.prompt = add_to_prompt + else: + p.prompt = initial_prompt+add_to_prompt + + if add_to_negative_prompt != "" or t2iii_replace_negative_prompt: + if t2iii_replace_negative_prompt: + p.negative_prompt = add_to_negative_prompt + else: + p.negative_prompt = initial_negative_prompt+add_to_negative_prompt + + basename = "" + extra_gen_parms = { + 'Initial steps':p.steps, + 'Initial CFG scale':p.cfg_scale, + "Initial seed": p.seed, + 'Initial sampler': p.sampler_name, + 'Reprocess amount':t2iii_reprocess + } + for i in range(t2iii_reprocess): + if t2iii_upscale_x > 64: + upscale_x = t2iii_upscale_x + else: + upscale_x = p.width + if t2iii_upscale_y > 64: + upscale_y = t2iii_upscale_y + else: + upscale_y = p.height + if t2iii_2x_last > 1 and i+1 == t2iii_reprocess: + upscale_x = int(upscale_x*t2iii_2x_last) + upscale_y = int(upscale_y*t2iii_2x_last) + if t2iii_seed_shift == -1: + reprocess_seed = randint(0,999999999) + else: + reprocess_seed = p.seed+t2iii_seed_shift*(i+1) + if t2iii_clip > 0: + opts.data["CLIP_stop_at_last_layers"] = t2iii_clip + + if state.interrupted: + if t2iii_clip > 0: + opts.data["CLIP_stop_at_last_layers"] = initial_CLIP + break + + if t2iii_model != "Same": + if not is_model_loaded(secondary_model_name): + print() + sd_models.load_model(sd_models.CheckpointInfo(secondary_model_path)) + + if i == 0: + proc_temp = proc + else: + proc_temp = proc2 + if t2iii_noise > 0 : + proc_temp.images[0] = add_noise_to_image(proc_temp.images[0],p.seed,t2iii_noise) + + img2img_processing = StableDiffusionProcessingImg2Img( + init_images=proc_temp.images, + resize_mode=0, + denoising_strength=remap_range(i,0,t2iii_reprocess-1,t2iii_denoising_strength,t2iii_patch_end_denoising) if t2iii_patch_end_denoising > 0 else t2iii_denoising_strength, + mask=None, + mask_blur=t2iii_patch_mask_blur, + inpainting_fill=1, + inpaint_full_res=False, + inpaint_full_res_padding=0, + inpainting_mask_invert=0, + sd_model=p.sd_model, + outpath_samples=p.outpath_samples, + outpath_grids=p.outpath_grids, + prompt=p.prompt, + styles=p.styles, + seed=reprocess_seed, + subseed=proc_temp.subseed, + subseed_strength=p.subseed_strength, + seed_resize_from_h=p.seed_resize_from_h, + seed_resize_from_w=p.seed_resize_from_w, + #seed_enable_extras=p.seed_enable_extras, + sampler_name=t2iii_sampler, + #sampler_index=img2img_sampler_index, + batch_size=p.batch_size, + n_iter=p.n_iter, + steps=t2iii_steps, + cfg_scale=remap_range(i,0,t2iii_reprocess-1,t2iii_cfg_scale,t2iii_cfg_scale_end) if t2iii_cfg_scale_end > 0 else t2iii_cfg_scale, + width=upscale_x, + height=upscale_y, + restore_faces=(t2iii_face_correction or (t2iii_face_correction_last and t2iii_reprocess-1 == i)) and not (t2iii_reprocess-1 == i and not t2iii_face_correction_last), + tiling=p.tiling, + do_not_save_samples=True, + do_not_save_grid=p.do_not_save_grid, + extra_generation_params=extra_gen_parms, + overlay_images=p.overlay_images, + negative_prompt=p.negative_prompt, + eta=p.eta + ) + # if t2iii_reprocess-1 == i and t2iii_override_s_noise: + # img2img_processing.s_noise = t2iii_sampler_noise + if not t2iii_patch_upscale: + proc2 = process_images(img2img_processing) + if ((t2iii_only_last and t2iii_reprocess-1 == i) or not t2iii_only_last): + images.save_image(proc2.images[0], p.outpath_samples, "", proc_temp.seed, proc2.prompt, opts.samples_format, info=proc_temp.info, p=p) + else: + proc_temp.images[0] = proc_temp.images[0].resize((upscale_x, upscale_y), Image.Resampling.LANCZOS) + width_for_patch, height_for_patch = proc_temp.images[0].size + real_square_size = int(t2iii_patch_square_size) + overlap_pass = int(real_square_size/t2iii_reprocess)*i + patch_seed = reprocess_seed + for x in range(0, width_for_patch+overlap_pass if i>0 and t2iii_patch_shift else width_for_patch, real_square_size): + for y in range(0, height_for_patch+overlap_pass if i>0 and t2iii_patch_shift else height_for_patch, real_square_size): + if ( + x-overlap_pass > width_for_patch or + y-overlap_pass > height_for_patch or + x+real_square_size-overlap_pass < 0 or + y+real_square_size-overlap_pass < 0 + ): continue + if t2iii_seed_shift == -1: + patch_seed = randint(0,999999999) + else: + patch_seed = patch_seed+t2iii_seed_shift + patch = proc_temp.images[0].crop((x-t2iii_patch_padding-overlap_pass, + y-t2iii_patch_padding-overlap_pass, + x + real_square_size + t2iii_patch_padding-overlap_pass, + y + real_square_size + t2iii_patch_padding-overlap_pass)) + img2img_processing.init_images = [patch] + img2img_processing.do_not_save_samples = True + img2img_processing.width = patch.size[0] + img2img_processing.height = patch.size[1] + img2img_processing.seed = patch_seed + mask = create_mask(patch.size[0],t2iii_patch_padding+t2iii_patch_border) + img2img_processing.image_mask = mask + proc_patch_temp = process_images(img2img_processing) + patch = proc_patch_temp.images[0] + patch = patch.crop((t2iii_patch_padding, t2iii_patch_padding, patch.size[0] - t2iii_patch_padding, patch.size[1] - t2iii_patch_padding)) + proc_temp.images[0].paste(patch, (x-overlap_pass, y-overlap_pass)) + proc2 = proc_patch_temp + proc2.images[0] = proc_temp.images[0] + images.save_image(proc2.images[0], p.outpath_samples, "", proc2.seed, proc2.prompt, opts.samples_format, info=proc2.info, p=p) + + + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + if t2iii_model != "Same": + if not is_model_loaded(initial_model): + print() + sd_models.load_model(sd_models.CheckpointInfo(initial_model_path)) + if t2iii_clip > 0: + opts.data["CLIP_stop_at_last_layers"] = initial_CLIP + return proc diff --git a/scripts/UnsharpMask.py b/scripts/UnsharpMask.py new file mode 100644 index 0000000000000000000000000000000000000000..c91ecb7cf9d14785426be6e777d64f1471a78536 --- /dev/null +++ b/scripts/UnsharpMask.py @@ -0,0 +1,54 @@ +import modules.scripts as scripts +import gradio as gr +import os +from modules import images +from modules.processing import process_images, Processed +from modules.processing import Processed +from modules.shared import opts, cmd_opts, state + +class Script(scripts.Script): + + def title(self): + return "Unsharp Mask" + +#only show in img2img tab + + def show(self, is_img2img): + return is_img2img + +#Gradio interface parameters + + def ui(self, is_img2img): + save = gr.Checkbox(False, label="Save original and effect") + umradius = gr.Slider(minimum=0.0, maximum=1000.0, step=1, value=0, label="Radius") + umpercent = gr.Slider(minimum=0.0, maximum=500.0, step=1, value=0, label="Percent") + umthreshold = gr.Slider(minimum=0.0, maximum=255.0, step=1, value=0, label="Threshold") + return [save, umradius, umpercent, umthreshold] + +#processed object + def run(self, p, save, umradius, umpercent, umthreshold): + +#perform unsharp mask operation + def unsharp_mask(im, umradius, umpercent, umthreshold): + from PIL import Image, ImageFilter + raf = im + raf = raf.filter(filter=ImageFilter.UnsharpMask(radius = umradius, percent = umpercent, threshold = umthreshold)) + return raf + +# If save is off, save with prefix filename + basename = "" + if(save): + basename += "unsharpmask_" + else: + p.do_not_save_samples = True + +#process images + proc = process_images(p) + + for i in range(len(proc.images)): + + proc.images[i] = unsharp_mask(proc.images[i], umradius, umpercent, umthreshold) + images.save_image(proc.images[i], p.outpath_samples, basename, + proc.seed + i, proc.prompt, opts.samples_format, info= proc.info, p=p) + + return proc diff --git a/scripts/__pycache__/CFG Auto.cpython-310.pyc b/scripts/__pycache__/CFG Auto.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..45bf3dc020dfecff121fa49ce2f0ce5b94093ba1 Binary files /dev/null and b/scripts/__pycache__/CFG Auto.cpython-310.pyc differ diff --git a/scripts/__pycache__/CFG Schedule.cpython-310.pyc b/scripts/__pycache__/CFG Schedule.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aab45f901e1d5c4aae6ae0f4daefbd0ea3ee5e31 Binary files /dev/null and b/scripts/__pycache__/CFG Schedule.cpython-310.pyc differ diff --git a/scripts/__pycache__/ContorlNet_I2I_sequence_toyxyz_V2.cpython-310.pyc b/scripts/__pycache__/ContorlNet_I2I_sequence_toyxyz_V2.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28a5c1168e6ef4841ba009f36ebc09ddf8c2a3b5 Binary files /dev/null and b/scripts/__pycache__/ContorlNet_I2I_sequence_toyxyz_V2.cpython-310.pyc differ diff --git a/scripts/__pycache__/LoopbackWave.cpython-310.pyc b/scripts/__pycache__/LoopbackWave.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d097e9bed99ee1e467c889604f31054ce5ad65b4 Binary files /dev/null and b/scripts/__pycache__/LoopbackWave.cpython-310.pyc differ diff --git a/scripts/__pycache__/Txt2img2img2img.cpython-310.pyc b/scripts/__pycache__/Txt2img2img2img.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98e4e0f0b8a9730564140fce4ffa315d26824d31 Binary files /dev/null and b/scripts/__pycache__/Txt2img2img2img.cpython-310.pyc differ diff --git a/scripts/__pycache__/UnsharpMask.cpython-310.pyc b/scripts/__pycache__/UnsharpMask.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49ea65ea1320b8a281a7a079000618845ebb92a2 Binary files /dev/null and b/scripts/__pycache__/UnsharpMask.cpython-310.pyc differ diff --git a/scripts/__pycache__/advanced_loopback.cpython-310.pyc b/scripts/__pycache__/advanced_loopback.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6d3ef4afc0fa7596fbced4096fe68de913f0f77a Binary files /dev/null and b/scripts/__pycache__/advanced_loopback.cpython-310.pyc differ diff --git a/scripts/__pycache__/advanced_loopback_blend.cpython-310.pyc b/scripts/__pycache__/advanced_loopback_blend.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ae57359daf177d9fb715badf521dbb557ba6bd30 Binary files /dev/null and b/scripts/__pycache__/advanced_loopback_blend.cpython-310.pyc differ diff --git a/scripts/__pycache__/advanced_seed_blending.cpython-310.pyc b/scripts/__pycache__/advanced_seed_blending.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fd221c85b87e7b69120a3049791d83d4e8afe1e6 Binary files /dev/null and b/scripts/__pycache__/advanced_seed_blending.cpython-310.pyc differ diff --git a/scripts/__pycache__/alternate_sampler_noise_schedules.cpython-310.pyc b/scripts/__pycache__/alternate_sampler_noise_schedules.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a016a61f4880244b8e7b82f863b7d3ce6d04208d Binary files /dev/null and b/scripts/__pycache__/alternate_sampler_noise_schedules.cpython-310.pyc differ diff --git a/scripts/__pycache__/block_lora.cpython-310.pyc b/scripts/__pycache__/block_lora.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af222274153e7d6e62a6e983488cdf40bd7ae1c4 Binary files /dev/null and b/scripts/__pycache__/block_lora.cpython-310.pyc differ diff --git a/scripts/__pycache__/cache_cleaner(from sd-webui-gradio-cleaner).cpython-310.pyc b/scripts/__pycache__/cache_cleaner(from sd-webui-gradio-cleaner).cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3086a52cdc77a33750bff623ce6014b3671b319 Binary files /dev/null and b/scripts/__pycache__/cache_cleaner(from sd-webui-gradio-cleaner).cpython-310.pyc differ diff --git a/scripts/__pycache__/custom_code-Copy1.cpython-310.pyc b/scripts/__pycache__/custom_code-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ac9ce5c24f6062e1c2e6416425f67952375cd03 Binary files /dev/null and b/scripts/__pycache__/custom_code-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/custom_code.cpython-310.pyc b/scripts/__pycache__/custom_code.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2d3fbdaf2145f8e2e2448ea2430e50f8084e0707 Binary files /dev/null and b/scripts/__pycache__/custom_code.cpython-310.pyc differ diff --git a/scripts/__pycache__/epiCFG_schedule_type.cpython-310.pyc b/scripts/__pycache__/epiCFG_schedule_type.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6a7cdecda1e45d67eccd7c717522cfb6c5209089 Binary files /dev/null and b/scripts/__pycache__/epiCFG_schedule_type.cpython-310.pyc differ diff --git a/scripts/__pycache__/external_masking.cpython-310.pyc b/scripts/__pycache__/external_masking.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7dca2da169c37a3788ca423757e13e06fa7396f4 Binary files /dev/null and b/scripts/__pycache__/external_masking.cpython-310.pyc differ diff --git a/scripts/__pycache__/img2imgalt-Copy1.cpython-310.pyc b/scripts/__pycache__/img2imgalt-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..975cb0734d6dec9c35e3162f6f3037d1832cdc74 Binary files /dev/null and b/scripts/__pycache__/img2imgalt-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/img2imgalt.cpython-310.pyc b/scripts/__pycache__/img2imgalt.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c02a35314407adfcd30d3ade96d74f6dc62dbbe9 Binary files /dev/null and b/scripts/__pycache__/img2imgalt.cpython-310.pyc differ diff --git a/scripts/__pycache__/loopback-Copy1.cpython-310.pyc b/scripts/__pycache__/loopback-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8769f8f417f79524fe8cbcaf2a80c32a6ffbb5ff Binary files /dev/null and b/scripts/__pycache__/loopback-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/loopback.cpython-310.pyc b/scripts/__pycache__/loopback.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef9bbcc1355452fc4b3afc7a69efd7da1f8667f9 Binary files /dev/null and b/scripts/__pycache__/loopback.cpython-310.pyc differ diff --git a/scripts/__pycache__/loopback_for_chain.cpython-310.pyc b/scripts/__pycache__/loopback_for_chain.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e91436950c72274f36f038490b4f4d41fab45f70 Binary files /dev/null and b/scripts/__pycache__/loopback_for_chain.cpython-310.pyc differ diff --git a/scripts/__pycache__/outpainting_mk_2-Copy1.cpython-310.pyc b/scripts/__pycache__/outpainting_mk_2-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d19511411781d062b461bf06e1a8826025b0d742 Binary files /dev/null and b/scripts/__pycache__/outpainting_mk_2-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/outpainting_mk_2.cpython-310.pyc b/scripts/__pycache__/outpainting_mk_2.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8b5b719b929ef9a4a94b2fcad73f35aacbf6a546 Binary files /dev/null and b/scripts/__pycache__/outpainting_mk_2.cpython-310.pyc differ diff --git a/scripts/__pycache__/poor_mans_outpainting-Copy1.cpython-310.pyc b/scripts/__pycache__/poor_mans_outpainting-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46c9ecb87d9b6952257e3a98e35edf9059bb5a1a Binary files /dev/null and b/scripts/__pycache__/poor_mans_outpainting-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/poor_mans_outpainting.cpython-310.pyc b/scripts/__pycache__/poor_mans_outpainting.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..960b057dceb5c531dd8fc8ed4ee9a820944b5f3d Binary files /dev/null and b/scripts/__pycache__/poor_mans_outpainting.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_codeformer-Copy1.cpython-310.pyc b/scripts/__pycache__/postprocessing_codeformer-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..621c65027be81e75d5c2ccc0bdeab65559f3b3af Binary files /dev/null and b/scripts/__pycache__/postprocessing_codeformer-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_codeformer.cpython-310.pyc b/scripts/__pycache__/postprocessing_codeformer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a599c68750bc21f19a2e8fa1ed109bc1ba8eccfb Binary files /dev/null and b/scripts/__pycache__/postprocessing_codeformer.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_gfpgan-Copy1.cpython-310.pyc b/scripts/__pycache__/postprocessing_gfpgan-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5b8bb8c6119851582967b97df373ec8d576262ab Binary files /dev/null and b/scripts/__pycache__/postprocessing_gfpgan-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_gfpgan.cpython-310.pyc b/scripts/__pycache__/postprocessing_gfpgan.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3440f61fa3ea3dc86f3cd15a795c6b9c9028831b Binary files /dev/null and b/scripts/__pycache__/postprocessing_gfpgan.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_upscale-Copy1.cpython-310.pyc b/scripts/__pycache__/postprocessing_upscale-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39bb07bbca8808c43aab9b396e024a9a5007ce70 Binary files /dev/null and b/scripts/__pycache__/postprocessing_upscale-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/postprocessing_upscale.cpython-310.pyc b/scripts/__pycache__/postprocessing_upscale.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5edfd1e07fc0da3977ecc59ce59e401c31b20e7a Binary files /dev/null and b/scripts/__pycache__/postprocessing_upscale.cpython-310.pyc differ diff --git a/scripts/__pycache__/process_png_metadata.cpython-310.pyc b/scripts/__pycache__/process_png_metadata.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b891e5e19e638cfd66162cb76e0b9da4762bf8ff Binary files /dev/null and b/scripts/__pycache__/process_png_metadata.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompt_matrix-Copy1.cpython-310.pyc b/scripts/__pycache__/prompt_matrix-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0de7343b2f2d8b89bb47bb2d27d032eef3ccd560 Binary files /dev/null and b/scripts/__pycache__/prompt_matrix-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompt_matrix.cpython-310.pyc b/scripts/__pycache__/prompt_matrix.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b1fc691106bb353a801ce4fee249eec5b8489626 Binary files /dev/null and b/scripts/__pycache__/prompt_matrix.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompter.cpython-310.pyc b/scripts/__pycache__/prompter.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..602b1af9d14c5278f8e1a68337541a584b62e8ac Binary files /dev/null and b/scripts/__pycache__/prompter.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompts_from_file-Copy1.cpython-310.pyc b/scripts/__pycache__/prompts_from_file-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56903aba012e887577d16ecaac852218eb5ae030 Binary files /dev/null and b/scripts/__pycache__/prompts_from_file-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompts_from_file.cpython-310.pyc b/scripts/__pycache__/prompts_from_file.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f52d771eadf5a7918746c2603d4cb118ace6011a Binary files /dev/null and b/scripts/__pycache__/prompts_from_file.cpython-310.pyc differ diff --git a/scripts/__pycache__/prompts_from_file_2.cpython-310.pyc b/scripts/__pycache__/prompts_from_file_2.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c573c0edbd3c1e98a8e1ee0b10753eef115008e7 Binary files /dev/null and b/scripts/__pycache__/prompts_from_file_2.cpython-310.pyc differ diff --git a/scripts/__pycache__/quick_upscale.cpython-310.pyc b/scripts/__pycache__/quick_upscale.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60c67c33a136d1e6dcb395636c8e7156277ac083 Binary files /dev/null and b/scripts/__pycache__/quick_upscale.cpython-310.pyc differ diff --git a/scripts/__pycache__/run_n_times.cpython-310.pyc b/scripts/__pycache__/run_n_times.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8328c0bb04f8c3685443a010c5bdf3cdfea46a9f Binary files /dev/null and b/scripts/__pycache__/run_n_times.cpython-310.pyc differ diff --git a/scripts/__pycache__/save-steps.cpython-310.pyc b/scripts/__pycache__/save-steps.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dff99a676ddeafef1020e2f23d6817bf2c693a07 Binary files /dev/null and b/scripts/__pycache__/save-steps.cpython-310.pyc differ diff --git a/scripts/__pycache__/sd_upscale-Copy1.cpython-310.pyc b/scripts/__pycache__/sd_upscale-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b8953e58b206a567ca650d80b4f7b2126bccf4ea Binary files /dev/null and b/scripts/__pycache__/sd_upscale-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/sd_upscale.cpython-310.pyc b/scripts/__pycache__/sd_upscale.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc6ec3688a1c11ceb0165be87919209167c3ca36 Binary files /dev/null and b/scripts/__pycache__/sd_upscale.cpython-310.pyc differ diff --git a/scripts/__pycache__/size_travel.cpython-310.pyc b/scripts/__pycache__/size_travel.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f9fde991af3e0942eba1e67feb0566b22237cfc7 Binary files /dev/null and b/scripts/__pycache__/size_travel.cpython-310.pyc differ diff --git a/scripts/__pycache__/txt2palette.cpython-310.pyc b/scripts/__pycache__/txt2palette.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b642ec18e77e8cf9658a2008e8b3ddca9bde781 Binary files /dev/null and b/scripts/__pycache__/txt2palette.cpython-310.pyc differ diff --git a/scripts/__pycache__/xy_grid.cpython-310.pyc b/scripts/__pycache__/xy_grid.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cfa53bb7b0affbaf153b631b9405708a7e4fceed Binary files /dev/null and b/scripts/__pycache__/xy_grid.cpython-310.pyc differ diff --git a/scripts/__pycache__/xyz_grid-Copy1.cpython-310.pyc b/scripts/__pycache__/xyz_grid-Copy1.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ed1e153e35fac83edbb4ce97d9a1457c0f636800 Binary files /dev/null and b/scripts/__pycache__/xyz_grid-Copy1.cpython-310.pyc differ diff --git a/scripts/__pycache__/xyz_grid.cpython-310.pyc b/scripts/__pycache__/xyz_grid.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..de05da7636e9ba6634e548c999606137f498bdbd Binary files /dev/null and b/scripts/__pycache__/xyz_grid.cpython-310.pyc differ diff --git a/scripts/__pycache__/xyz_grid_s.cpython-310.pyc b/scripts/__pycache__/xyz_grid_s.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..026f031b9992e56c16305c9760b5aa3ee84a9c1a Binary files /dev/null and b/scripts/__pycache__/xyz_grid_s.cpython-310.pyc differ diff --git a/scripts/__pycache__/xyzabc.cpython-310.pyc b/scripts/__pycache__/xyzabc.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1033e8b5bf2c17ba30027626cdb9b4cecbe2db26 Binary files /dev/null and b/scripts/__pycache__/xyzabc.cpython-310.pyc differ diff --git a/scripts/advanced_loopback.py b/scripts/advanced_loopback.py new file mode 100644 index 0000000000000000000000000000000000000000..0d93050401a8641b64a07473dd6a1351d80e4ba0 --- /dev/null +++ b/scripts/advanced_loopback.py @@ -0,0 +1,279 @@ +import numpy as np +from tqdm import trange +from PIL import Image, ImageEnhance + +import modules.scripts as scripts +import gradio as gr + +from modules import processing, shared, sd_samplers, images +from modules.processing import Processed +from modules.sd_samplers import samplers +from modules.shared import opts, cmd_opts, state + +from math import sin, pi + +class Script(scripts.Script): + def title(self): + return "Advanced loopback" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + loops = gr.Number(minimum=1, step=1, label='Loops', value=4) + use_first_image_colors = gr.Checkbox(label='Use first image colors (custom color correction) ', value=False) + denoising_strength_change_factor = gr.Slider(minimum=0.9, maximum=1.1, step=0.01, label='Denoising strength change factor (overridden if proportional used)', value=1) + zoom_level = gr.Slider(minimum=1, maximum=1.1, step=0.001, label='Zoom level', value=1) + with gr.Row(): + direction_x = gr.Slider(minimum=-0.1, maximum=0.1, step=0.01, label='Direction X', value=0) + direction_y = gr.Slider(minimum=-0.1, maximum=0.1, step=0.01, label='Direction Y', value=0) + with gr.Row(): + denoising_strength_first_image = gr.Number(minimum=0, step=1, label='Denoising strength start ', value=0) + denoising_strength_last_image = gr.Number(minimum=0, step=1, label='Denoising strength end ', value=4) + denoising_strength_min = gr.Slider(minimum=0.1, maximum=1, step=0.01, label='Denoising strength proportional change starting value ', value=0.1) + denoising_strength_max = gr.Slider(minimum=0.1, maximum=1, step=0.01, label='Denoising strength proportional change ending value (0.1 = disabled) ', value=0.1) + cfg_scale_min = gr.Slider(minimum=0.1, maximum=30, step=0.1, label='CFG scale proportional change starting value ', value=0.1) + cfg_scale_max = gr.Slider(minimum=0.1, maximum=30, step=0.1, label='CFG scale proportional change ending value (0.1 = disabled) ', value=0.1) + saturation_per_image = gr.Slider(minimum=0.99, maximum=1.01, step=0.001, label='Saturation enhancement per image ', value=1) + with gr.Row(): + use_sine_variation_dns = gr.Checkbox(label='Use sine denoising strength variation (CFG will be scaled with it if the slider is > 0.1)', value=True) + phase_diff_denoising = gr.Slider(minimum=0, maximum=1, step=0.05, label='Phase difference', value=0) + amplify_sine_variation_denoise = gr.Slider(minimum=1, maximum=10, step=1, label='Denoising strength exponentiation ', value=1) + with gr.Row(): + use_sine_variation_zoom = gr.Checkbox(label='Use sine zoom variation', value=False) + phase_diff_zoom = gr.Slider(minimum=0, maximum=1, step=0.05, label='Phase difference', value=0) + amplify_sine_variation_zoom = gr.Slider(minimum=1, maximum=10, step=1, label='Zoom exponentiation ', value=1) + with gr.Row(): + use_multi_prompts = gr.Checkbox(label='Use multiple prompts', value=False) + same_seed_per_prompt = gr.Checkbox(label='Same seed per prompt', value=False) + same_seed_always = gr.Checkbox(label='Same seed for everything', value=False) + same_init_image = gr.Checkbox(label='Original init image for everything', value=False) + multi_prompts = gr.Textbox(label="Multiple prompts : 1 line positive, 1 line negative, leave a blank line for no negative", lines=2, max_lines=2000) + return [ + loops, + denoising_strength_change_factor, + zoom_level, + direction_x, + direction_y, + denoising_strength_first_image, + denoising_strength_last_image, + denoising_strength_min, + denoising_strength_max, + cfg_scale_min, + cfg_scale_max, + saturation_per_image, + use_first_image_colors, + use_sine_variation_dns, + use_sine_variation_zoom, + phase_diff_zoom, + use_multi_prompts, + multi_prompts, + amplify_sine_variation_zoom, + same_seed_per_prompt, + phase_diff_denoising, + amplify_sine_variation_denoise, + same_seed_always, + same_init_image + ] + + def zoom_into(self, img, zoom, direction_x, direction_y): + neg = lambda x: 1 if x > 0 else -1 + if abs(direction_x) > zoom-1 : direction_x = (zoom-1)*neg(direction_x)*0.999999999999999 #*0.999999999999999 to avoid a float rounding error that makes it higher than desired + if abs(direction_y) > zoom-1 : direction_y = (zoom-1)*neg(direction_y)*0.999999999999999 + w, h = img.size + x = w/2+direction_x*w/4 + y = h/2-direction_y*h/4 + zoom2 = zoom * 2 + img = img.crop((x - w / zoom2, y - h / zoom2, + x + w / zoom2, y + h / zoom2)) + return img.resize((w, h), Image.LANCZOS) + + def run(self, p, + loops, + denoising_strength_change_factor, + zoom_level, + direction_x, + direction_y, + denoising_strength_first_image, + denoising_strength_last_image, + denoising_strength_min, + denoising_strength_max, + cfg_scale_min, + cfg_scale_max, + saturation_per_image, + use_first_image_colors, + use_sine_variation_dns, + use_sine_variation_zoom, + phase_diff_zoom, + use_multi_prompts, + multi_prompts, + amplify_sine_variation_zoom, + same_seed_per_prompt, + phase_diff_denoising, + amplify_sine_variation_denoise, + same_seed_always, + same_init_image + ): + + ppos = [] + pneg = [] + if use_multi_prompts : + prompts_list = multi_prompts.splitlines() + oddeven = lambda x: 1 if x%2==0 else 0 + for x in range(len(prompts_list)) : + if oddeven(x): + ppos.append(prompts_list[x]) + else: + pneg.append(prompts_list[x]) + if len(pneg) < len(ppos) : + pneg.append("") + + def remap_range(value, minIn, MaxIn, minOut, maxOut): + if value > MaxIn: value = MaxIn; + if value < minIn: value = minIn; + finalValue = ((value - minIn) / (MaxIn - minIn)) * (maxOut - minOut) + minOut; + return finalValue; + + def get_sin_steps(i,amplify,phase_diff=0): + i -= denoising_strength_first_image + range = (denoising_strength_last_image - denoising_strength_first_image) + x = i % (range) + y = remap_range(x,0,range,0,1) + y = y ** amplify + z = sin((y+phase_diff/2)*pi) + return z + + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Denoising strength change factor": denoising_strength_change_factor, + 'Denoising strength proportional change start image':denoising_strength_first_image, + 'Denoising strength proportional change end image':denoising_strength_last_image, + 'Denoising strength proportional change starting value':denoising_strength_min, + 'Denoising strength proportional change ending value':denoising_strength_max, + 'CFG min':cfg_scale_min, + 'CFG max':cfg_scale_max, + 'use first image colors': use_first_image_colors, + 'Saturation enhancement per image':saturation_per_image, + 'Zoom level':zoom_level, + } + + p.batch_size = 1 + p.n_iter = 1 + + output_images, info = None, None + initial_seed = None + initial_info = None + + grids = [] + all_images = [] + state.job_count = loops * batch_count + + original_image = p.init_images[0].copy() + if opts.img2img_color_correction: + p.color_corrections = [processing.setup_color_correction(p.init_images[0])] + + + for n in range(batch_count): + history = [] + multi_prompts_index = 0 + loops = round(loops) + for i in range(loops): + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if use_multi_prompts : + image_range = (denoising_strength_last_image - denoising_strength_first_image) + il = i % (image_range) + if i == 0: + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] + print("Prompt :",p.prompt) + print("Negative prompt :",p.negative_prompt) + if il == 0 and i > 0: + multi_prompts_index+=1 + try: + if same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] + except Exception as e: + multi_prompts_index = 0 + if same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] +# print("Prompt :",p.prompt) +# print("Negative prompt :",p.negative_prompt) + + if use_first_image_colors: + p.color_corrections = [processing.setup_color_correction(original_image)] + + state.job = f"Iteration {i + 1}/{loops}, batch {n + 1}/{batch_count}" + + if denoising_strength_max > 0.1 : + if use_sine_variation_dns : + ds = remap_range(get_sin_steps(i,amplify_sine_variation_denoise,phase_diff_denoising),0,1,denoising_strength_min,denoising_strength_max) + else: + ds = remap_range(i+1,denoising_strength_first_image,denoising_strength_last_image,denoising_strength_min,denoising_strength_max) + p.denoising_strength = round(ds,3) + print("Denoising strength : "+str(p.denoising_strength)) + + if cfg_scale_max > 0.1 : + if use_sine_variation_dns : + cfgs = remap_range(get_sin_steps(i,amplify_sine_variation_denoise,phase_diff_denoising),0,1,cfg_scale_min,cfg_scale_max) + else: + cfgs = remap_range(i+1,denoising_strength_first_image,denoising_strength_last_image,cfg_scale_min,cfg_scale_max) + p.cfg_scale = round(cfgs,2) + print("CFG scale : "+str(p.cfg_scale)) + + processed = processing.process_images(p) + if zoom_level != 1: + if use_sine_variation_zoom : + if loops >= denoising_strength_first_image : + z = remap_range(get_sin_steps(i,amplify_sine_variation_zoom,phase_diff_zoom),0,1,1,zoom_level) + processed.images[0] = self.zoom_into(processed.images[0], z, direction_x, direction_y) + print("Zoom level :",z) + else: + processed.images[0] = self.zoom_into(processed.images[0], zoom_level, direction_x, direction_y) + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + if not same_init_image : + init_img = processed.images[0] + else: + init_img = original_image + + if saturation_per_image != 1 : + init_img = ImageEnhance.Color(init_img).enhance(saturation_per_image) + + p.init_images = [init_img] + if not same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.denoising_strength = min(max(p.denoising_strength * denoising_strength_change_factor, 0.1), 1) + history.append(processed.images[0]) + if state.interrupted: + break + + grid = images.image_grid(history, rows=1) + if opts.grid_save: + images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + grids.append(grid) + all_images += history + + if opts.return_grid: + all_images = grids + all_images + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed diff --git a/scripts/advanced_loopback_blend.py b/scripts/advanced_loopback_blend.py new file mode 100644 index 0000000000000000000000000000000000000000..9d94b0b7ef696041f865e42fd6dcc516e35fc871 --- /dev/null +++ b/scripts/advanced_loopback_blend.py @@ -0,0 +1,285 @@ +import numpy as np +from tqdm import trange +from PIL import Image, ImageEnhance + +import modules.scripts as scripts +import gradio as gr + +from modules import processing, shared, sd_samplers, images +from modules.processing import Processed +from modules.sd_samplers import samplers +from modules.shared import opts, cmd_opts, state +from copy import deepcopy +from math import sin, pi + +class Script(scripts.Script): + def title(self): + return "Advanced loopback blend" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + loops = gr.Number(minimum=1, step=1, label='Loops', value=4) + use_first_image_colors = gr.Checkbox(label='Use first image colors (custom color correction) ', value=False) + denoising_strength_change_factor = gr.Slider(minimum=0.9, maximum=1.1, step=0.01, label='Denoising strength change factor (overridden if proportional used)', value=1) + with gr.Row(): + zoom_level = gr.Slider(minimum=0, maximum=50, step=1, label='Zoom level ', value=0) + zoom_blend = gr.Checkbox(label='Blend 50/50 with original when zoomed. Doesn\'t work with sine variation.', value=False) + # zoom_refresh = gr.Slider(minimum=0, maximum=50, step=1, label='Refresh base image for blend every n iterations', value=0) + # direction_x = gr.Slider(minimum=-0.1, maximum=0.1, step=0.01, label='Direction X', value=0) + # direction_y = gr.Slider(minimum=-0.1, maximum=0.1, step=0.01, label='Direction Y', value=0) + with gr.Row(): + denoising_strength_first_image = gr.Number(minimum=0, step=1, label='Denoising strength start ', value=0) + denoising_strength_last_image = gr.Number(minimum=0, step=1, label='Denoising strength end ', value=4) + denoising_strength_min = gr.Slider(minimum=0.1, maximum=1, step=0.01, label='Denoising strength proportional change starting value ', value=0.1) + denoising_strength_max = gr.Slider(minimum=0.1, maximum=1, step=0.01, label='Denoising strength proportional change ending value (0.1 = disabled) ', value=0.1) + cfg_scale_min = gr.Slider(minimum=0.1, maximum=30, step=0.1, label='CFG scale proportional change starting value ', value=0.1) + cfg_scale_max = gr.Slider(minimum=0.1, maximum=30, step=0.1, label='CFG scale proportional change ending value (0.1 = disabled) ', value=0.1) + saturation_per_image = gr.Slider(minimum=0.99, maximum=1.01, step=0.001, label='Saturation enhancement per image ', value=1) + with gr.Row(): + use_sine_variation_dns = gr.Checkbox(label='Use sine denoising strength variation (CFG will be scaled with it if the slider is > 0.1)', value=True) + phase_diff_denoising = gr.Slider(minimum=0, maximum=1, step=0.05, label='Phase difference', value=0) + amplify_sine_variation_denoise = gr.Slider(minimum=1, maximum=10, step=1, label='Denoising strength exponentiation ', value=1) + with gr.Row(): + use_sine_variation_zoom = gr.Checkbox(label='Use sine zoom variation', value=False) + phase_diff_zoom = gr.Slider(minimum=0, maximum=1, step=0.05, label='Phase difference', value=0) + amplify_sine_variation_zoom = gr.Slider(minimum=1, maximum=10, step=1, label='Zoom exponentiation ', value=1) + with gr.Row(): + use_multi_prompts = gr.Checkbox(label='Use multiple prompts', value=False) + same_seed_per_prompt = gr.Checkbox(label='Same seed per prompt', value=False) + same_seed_always = gr.Checkbox(label='Same seed for everything', value=False) + same_init_image = gr.Checkbox(label='Original init image for everything', value=False) + multi_prompts = gr.Textbox(label="Multiple prompts : 1 line positive, 1 line negative, leave a blank line for no negative", lines=2, max_lines=2000) + return [ + loops, + denoising_strength_change_factor, + zoom_level, + zoom_blend, + # zoom_refresh, + # direction_x, + # direction_y, + denoising_strength_first_image, + denoising_strength_last_image, + denoising_strength_min, + denoising_strength_max, + cfg_scale_min, + cfg_scale_max, + saturation_per_image, + use_first_image_colors, + use_sine_variation_dns, + use_sine_variation_zoom, + phase_diff_zoom, + use_multi_prompts, + multi_prompts, + amplify_sine_variation_zoom, + same_seed_per_prompt, + phase_diff_denoising, + amplify_sine_variation_denoise, + same_seed_always, + same_init_image + ] + + def zoom_into(self, img, zoom): + w, h = img.size + img = img.crop((zoom,zoom,w-zoom,h-zoom)) + return img.resize((w, h), Image.LANCZOS) + + def run(self, p, + loops, + denoising_strength_change_factor, + zoom_level, + zoom_blend, + # zoom_refresh, + # direction_x, + # direction_y, + denoising_strength_first_image, + denoising_strength_last_image, + denoising_strength_min, + denoising_strength_max, + cfg_scale_min, + cfg_scale_max, + saturation_per_image, + use_first_image_colors, + use_sine_variation_dns, + use_sine_variation_zoom, + phase_diff_zoom, + use_multi_prompts, + multi_prompts, + amplify_sine_variation_zoom, + same_seed_per_prompt, + phase_diff_denoising, + amplify_sine_variation_denoise, + same_seed_always, + same_init_image + ): + + ppos = [] + pneg = [] + if use_multi_prompts : + prompts_list = multi_prompts.splitlines() + oddeven = lambda x: 1 if x%2==0 else 0 + for x in range(len(prompts_list)) : + if oddeven(x): + ppos.append(prompts_list[x]) + else: + pneg.append(prompts_list[x]) + if len(pneg) < len(ppos) : + pneg.append("") + + def remap_range(value, minIn, MaxIn, minOut, maxOut): + if value > MaxIn: value = MaxIn; + if value < minIn: value = minIn; + finalValue = ((value - minIn) / (MaxIn - minIn)) * (maxOut - minOut) + minOut; + return finalValue; + + def get_sin_steps(i,amplify,phase_diff=0): + i -= denoising_strength_first_image + range = (denoising_strength_last_image - denoising_strength_first_image) + x = i % (range) + y = remap_range(x,0,range,0,1) + y = y ** amplify + z = sin((y+phase_diff/2)*pi) + return z + + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Denoising strength change factor": denoising_strength_change_factor, + 'Denoising strength proportional change start image':denoising_strength_first_image, + 'Denoising strength proportional change end image':denoising_strength_last_image, + 'Denoising strength proportional change starting value':denoising_strength_min, + 'Denoising strength proportional change ending value':denoising_strength_max, + 'CFG min':cfg_scale_min, + 'CFG max':cfg_scale_max, + 'use first image colors': use_first_image_colors, + 'Saturation enhancement per image':saturation_per_image, + 'Zoom level':zoom_level, + } + + p.batch_size = 1 + p.n_iter = 1 + + output_images, info = None, None + initial_seed = None + initial_info = None + + grids = [] + all_images = [] + state.job_count = loops * batch_count + + original_image = p.init_images[0].copy() + original_image_for_zoom = p.init_images[0].copy() + if opts.img2img_color_correction: + p.color_corrections = [processing.setup_color_correction(p.init_images[0])] + + + for n in range(batch_count): + history = [] + multi_prompts_index = 0 + loops = round(loops) + for i in range(loops): + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if use_multi_prompts : + image_range = (denoising_strength_last_image - denoising_strength_first_image) + il = i % (image_range) + if i == 0: + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] + print("Prompt :",p.prompt) + print("Negative prompt :",p.negative_prompt) + if il == 0 and i > 0: + multi_prompts_index+=1 + try: + if same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] + except Exception as e: + multi_prompts_index = 0 + if same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.prompt = ppos[multi_prompts_index] + p.negative_prompt = pneg[multi_prompts_index] +# print("Prompt :",p.prompt) +# print("Negative prompt :",p.negative_prompt) + + if use_first_image_colors: + p.color_corrections = [processing.setup_color_correction(original_image)] + + state.job = f"Iteration {i + 1}/{loops}, batch {n + 1}/{batch_count}" + + if denoising_strength_max > 0.1 : + if use_sine_variation_dns : + ds = remap_range(get_sin_steps(i,amplify_sine_variation_denoise,phase_diff_denoising),0,1,denoising_strength_min,denoising_strength_max) + else: + ds = remap_range(i+1,denoising_strength_first_image,denoising_strength_last_image,denoising_strength_min,denoising_strength_max) + p.denoising_strength = round(ds,3) + print("Denoising strength : "+str(p.denoising_strength)) + + if cfg_scale_max > 0.1 : + if use_sine_variation_dns : + cfgs = remap_range(get_sin_steps(i,amplify_sine_variation_denoise,phase_diff_denoising),0,1,cfg_scale_min,cfg_scale_max) + else: + cfgs = remap_range(i+1,denoising_strength_first_image,denoising_strength_last_image,cfg_scale_min,cfg_scale_max) + p.cfg_scale = round(cfgs,2) + print("CFG scale : "+str(p.cfg_scale)) + + processed = processing.process_images(p) + if zoom_level > 0: + if use_sine_variation_zoom : + if loops >= denoising_strength_first_image : + z = remap_range(get_sin_steps(i,amplify_sine_variation_zoom,phase_diff_zoom),0,1,1,zoom_level) + processed.images[0] = self.zoom_into(processed.images[0], z) + print("Zoom level :",z) + else: + processed.images[0] = self.zoom_into(processed.images[0], zoom_level) + if zoom_blend: + # if zoom_refresh > 0 and i%zoom_refresh == 0: + # original_image_for_zoom = processed.images[0].copy() + original_image_zoomed = self.zoom_into(original_image_for_zoom.copy(), zoom_level*(i+1)) + processed.images[0] = Image.blend(original_image_zoomed.copy().convert('RGB').resize(processed.images[0].size, Image.LANCZOS), processed.images[0].copy().convert('RGB'), alpha=0.5) + + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + if not same_init_image : + init_img = processed.images[0] + else: + init_img = original_image + + if saturation_per_image != 1 : + init_img = ImageEnhance.Color(init_img).enhance(saturation_per_image) + + p.init_images = [init_img] + if not same_seed_per_prompt: + if not same_seed_always: + p.subseed = p.subseed + 1 if p.subseed_strength > 0 else p.subseed + p.seed = p.seed + 1 if p.subseed_strength == 0 else p.seed + p.denoising_strength = min(max(p.denoising_strength * denoising_strength_change_factor, 0.1), 1) + history.append(processed.images[0]) + if state.interrupted: + break + + grid = images.image_grid(history, rows=1) + if opts.grid_save: + images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + grids.append(grid) + all_images += history + + if opts.return_grid: + all_images = grids + all_images + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed diff --git a/scripts/advanced_seed_blending.py b/scripts/advanced_seed_blending.py new file mode 100644 index 0000000000000000000000000000000000000000..08493a965bd2a70bb0c089ce2455da7f439198ec --- /dev/null +++ b/scripts/advanced_seed_blending.py @@ -0,0 +1,51 @@ +import modules.scripts as scripts +import modules.processing as processing +import gradio as gr + +from modules.processing import process_images, slerp +from modules import devices, shared +import torch + + +global_seeds = '' + + +def advanced_creator (shape, seeds, subseeds=None, subseed_strength=0.0, seed_resize_from_h=0, seed_resize_from_w=0, p=None): + global global_seeds + + parsed = [] + + for one in global_seeds.split(","): + parts = one.split(":") + parsed.append((int(parts[0]), float(parts[1]) if len(parts) > 1 else 1)) + + noises = list(map(lambda e: (devices.randn(e[0], shape), e[1]), parsed)) + while True: + cur = noises[0] + rest = noises[1:] + if len(rest) <= 0: + break + noises = list( + map(lambda r: (slerp(r[1] / (r[1] + cur[1]), cur[0], r[0]), r[1] * cur[1]), rest)) + + return torch.stack([noises[0][0]]).to(shared.device) + + +class Script(scripts.Script): + def title(self): + return "Advanced Seed Blending" + + def ui(self, is_img2img): + seeds = gr.Textbox(label='Seeds', value="") + + return [seeds] + + def run(self, p, seeds): + real_creator = processing.create_random_tensors + try: + processing.create_random_tensors = advanced_creator + global global_seeds + global_seeds = seeds + return process_images(p) + finally: + processing.create_random_tensors = real_creator diff --git a/scripts/alternate_sampler_noise_schedules.py b/scripts/alternate_sampler_noise_schedules.py new file mode 100644 index 0000000000000000000000000000000000000000..70798f82a62a7d9f62c11c58a101feb22b661599 --- /dev/null +++ b/scripts/alternate_sampler_noise_schedules.py @@ -0,0 +1,53 @@ +import inspect +from modules.processing import Processed, process_images +import gradio as gr +import modules.scripts as scripts +import k_diffusion.sampling +import torch + + +class Script(scripts.Script): + + def title(self): + return "Alternate Sampler Noise Schedules" + + def ui(self, is_img2img): + noise_scheduler = gr.Dropdown(label="Noise Scheduler", choices=['Default','Karras','Exponential', 'Variance Preserving'], value='Default', type="index") + sched_smin = gr.Slider(value=0.1, label="Sigma min", minimum=0.0, maximum=100.0, step=0.5) + sched_smax = gr.Slider(value=10.0, label="Sigma max", minimum=0.0, maximum=100.0, step=0.5) + sched_rho = gr.Slider(value=7.0, label="Sigma rho (Karras only)", minimum=7.0, maximum=100.0, step=0.5) + sched_beta_d = gr.Slider(value=19.9, label="Beta distribution (VP only)",minimum=0.0, maximum=40.0, step=0.5) + sched_beta_min = gr.Slider(value=0.1, label="Beta min (VP only)", minimum=0.0, maximum=40.0, step=0.1) + sched_eps_s = gr.Slider(value=0.001, label="Epsilon (VP only)", minimum=0.001, maximum=1.0, step=0.001) + + return [noise_scheduler, sched_smin, sched_smax, sched_rho, sched_beta_d, sched_beta_min, sched_eps_s] + + def run(self, p, noise_scheduler, sched_smin, sched_smax, sched_rho, sched_beta_d, sched_beta_min, sched_eps_s): + + noise_scheduler_func_name = ['-','get_sigmas_karras','get_sigmas_exponential','get_sigmas_vp'][noise_scheduler] + + base_params = { + "sigma_min":sched_smin, + "sigma_max":sched_smax, + "rho":sched_rho, + "beta_d":sched_beta_d, + "beta_min":sched_beta_min, + "eps_s":sched_eps_s, + "device":"cuda" if torch.cuda.is_available() else "cpu" + } + + if hasattr(k_diffusion.sampling,noise_scheduler_func_name): + + sigma_func = getattr(k_diffusion.sampling,noise_scheduler_func_name) + sigma_func_kwargs = {} + + for k,v in base_params.items(): + if k in inspect.signature(sigma_func).parameters: + sigma_func_kwargs[k] = v + + def substitute_noise_scheduler(n): + return sigma_func(n,**sigma_func_kwargs) + + p.sampler_noise_scheduler_override = substitute_noise_scheduler + + return process_images(p) \ No newline at end of file diff --git a/scripts/block_lora.py b/scripts/block_lora.py new file mode 100644 index 0000000000000000000000000000000000000000..9af69879319039cb4093b69531ea3bf23c3b7411 --- /dev/null +++ b/scripts/block_lora.py @@ -0,0 +1,210 @@ +import os +import re +import torch +from safetensors import safe_open +from safetensors.torch import save_file +import hashlib +from io import BytesIO +import safetensors.torch +from typing import Callable, Union, Optional + + +re_digits = re.compile(r"\d+") +re_x_proj = re.compile(r"(.*)_([qkv]_proj)$") +re_compiled = {} + +suffix_conversion = { + "attentions": {}, + "resnets": { + "conv1": "in_layers_2", + "conv2": "out_layers_3", + "time_emb_proj": "emb_layers_1", + "conv_shortcut": "skip_connection", + } +} + + +def convert_diffusers_name_to_compvis(key, is_sd2): + def match(match_list, regex_text): + regex = re_compiled.get(regex_text) + if regex is None: + regex = re.compile(regex_text) + re_compiled[regex_text] = regex + + r = re.match(regex, key) + if not r: + return False + + match_list.clear() + match_list.extend([int(x) if re.match(re_digits, x) else x for x in r.groups()]) + return True + + m = [] + + if match(m, r"lora_unet_conv_in(.*)"): + return f'diffusion_model_input_blocks_0_0{m[0]}' + + if match(m, r"lora_unet_conv_out(.*)"): + return f'diffusion_model_out_2{m[0]}' + + if match(m, r"lora_unet_time_embedding_linear_(\d+)(.*)"): + return f"diffusion_model_time_embed_{m[0] * 2 - 2}{m[1]}" + + if match(m, r"lora_unet_down_blocks_(\d+)_(attentions|resnets)_(\d+)_(.+)"): + suffix = suffix_conversion.get(m[1], {}).get(m[3], m[3]) + return f"diffusion_model_input_blocks_{1 + m[0] * 3 + m[2]}_{1 if m[1] == 'attentions' else 0}_{suffix}" + + if match(m, r"lora_unet_mid_block_(attentions|resnets)_(\d+)_(.+)"): + suffix = suffix_conversion.get(m[0], {}).get(m[2], m[2]) + return f"diffusion_model_middle_block_{1 if m[0] == 'attentions' else m[1] * 2}_{suffix}" + + if match(m, r"lora_unet_up_blocks_(\d+)_(attentions|resnets)_(\d+)_(.+)"): + suffix = suffix_conversion.get(m[1], {}).get(m[3], m[3]) + return f"diffusion_model_output_blocks_{m[0] * 3 + m[2]}_{1 if m[1] == 'attentions' else 0}_{suffix}" + + if match(m, r"lora_unet_down_blocks_(\d+)_downsamplers_0_conv"): + return f"diffusion_model_input_blocks_{3 + m[0] * 3}_0_op" + + if match(m, r"lora_unet_up_blocks_(\d+)_upsamplers_0_conv"): + return f"diffusion_model_output_blocks_{2 + m[0] * 3}_{2 if m[0]>0 else 1}_conv" + + if match(m, r"lora_te_text_model_encoder_layers_(\d+)_(.+)"): + if is_sd2: + if 'mlp_fc1' in m[1]: + return f"model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc1', 'mlp_c_fc')}" + elif 'mlp_fc2' in m[1]: + return f"model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc2', 'mlp_c_proj')}" + else: + return f"model_transformer_resblocks_{m[0]}_{m[1].replace('self_attn', 'attn')}" + + return f"transformer_text_model_encoder_layers_{m[0]}_{m[1]}" + + if match(m, r"lora_te2_text_model_encoder_layers_(\d+)_(.+)"): + if 'mlp_fc1' in m[1]: + return f"1_model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc1', 'mlp_c_fc')}" + elif 'mlp_fc2' in m[1]: + return f"1_model_transformer_resblocks_{m[0]}_{m[1].replace('mlp_fc2', 'mlp_c_proj')}" + else: + return f"1_model_transformer_resblocks_{m[0]}_{m[1].replace('self_attn', 'attn')}" + + return key + +def safetensors_hashes(tensors, metadata): + """Precalculate the model hashes needed by sd-webui-additional-networks to + save time on indexing the model later.""" + + # Because writing user metadata to the file can change the result of + # sd_models.model_hash(), only retain the training metadata for purposes of + # calculating the hash, as they are meant to be immutable + metadata = {k: v for k, v in metadata.items() if k.startswith("ss_")} + + bytes = safetensors.torch.save(tensors, metadata) + b = BytesIO(bytes) + + model_hash = addnet_hash_safetensors(b) + legacy_hash = addnet_hash_legacy(b) + return model_hash, legacy_hash + + +def addnet_hash_legacy(b): + """Old model hash used by sd-webui-additional-networks for .safetensors format files""" + m = hashlib.sha256() + + b.seek(0x100000) + m.update(b.read(0x10000)) + return m.hexdigest()[0:8] + + +def addnet_hash_safetensors(b): + """New model hash used by sd-webui-additional-networks for .safetensors format files""" + hash_sha256 = hashlib.sha256() + blksize = 1024 * 1024 + + b.seek(0) + header = b.read(8) + n = int.from_bytes(header, "little") + + offset = n + 8 + b.seek(offset) + for chunk in iter(lambda: b.read(blksize), b""): + hash_sha256.update(chunk) + + return hash_sha256.hexdigest() + + +def lbw_lora(input_, output, ratios): + print("Apply LBW") + + assert isinstance(input_, str) + assert isinstance(output, str) + assert isinstance(ratios, str) + assert os.path.exists(input_), f"{input_} is not exists" + assert os.path.exists(output) == False, f"{output} aleady exists" + + LOAD_PATH = input_ + SAVE_PATH = output + RATIOS = [float(x) for x in ratios.split(",")] + LAYERS = len(RATIOS) + assert LAYERS in [17, 26] + + BLOCKID17 = [ + "BASE", "IN01", "IN02", "IN04", "IN05", "IN07", "IN08", "M00", + "OUT03", "OUT04", "OUT05", "OUT06", "OUT07", "OUT08", "OUT09", "OUT10", "OUT11"] + BLOCKID26 = [ + "BASE", "IN00", "IN01", "IN02", "IN03", "IN04", "IN05", "IN06", "IN07", "IN08", "IN09", "IN10", "IN11", "M00", + "OUT00", "OUT01", "OUT02", "OUT03", "OUT04", "OUT05", "OUT06", "OUT07", "OUT08", "OUT09", "OUT10", "OUT11"] + + if LAYERS == 17: + RATIO_OF_ = dict(zip(BLOCKID17, RATIOS)) + if LAYERS == 26: + RATIO_OF_ = dict(zip(BLOCKID26, RATIOS)) + print(RATIO_OF_) + + PATTERNS = [ + r"^transformer_text_model_(encoder)_layers_(\d+)_.*", + r"^diffusion_model_(in)put_blocks_(\d+)_.*", + r"^diffusion_model_(middle)_block_(\d+)_.*", + r"^diffusion_model_(out)put_blocks_(\d+)_.*"] + + def replacement(match): + g1 = str(match.group(1)) # encoder, in, middle, out + g2 = int(match.group(2)) # number + assert g1 in ["encoder", "in", "middle", "out"] + assert isinstance(g2, int) + + if g1 == "encoder": + return "BASE" + if g1 == "middle": + return "M00" + return f"{str.upper(g1)}{g2:02}" + + def compvis_name_to_blockid(compvis_name): + strings = compvis_name + for pattern in PATTERNS: + strings = re.sub(pattern, replacement, strings) + if strings != compvis_name: + break + assert strings != compvis_name + blockid = strings + + if LAYERS == 17: + assert blockid in BLOCKID26, f"Incorrect layer {blockid}" + assert blockid in BLOCKID17, f"{blockid} is not included in 17 layers. May be 26 layers?" + if LAYERS == 26: + assert blockid in BLOCKID26, f"Incorrect layer {blockid}" + return blockid + + with safe_open(LOAD_PATH, framework="pt", device="cpu") as f: + tensors = {} + for key in f.keys(): + tensors[key] = f.get_tensor(key) # key = diffusers_name + compvis_name = convert_diffusers_name_to_compvis(key, is_sd2=False) + blockid = compvis_name_to_blockid(compvis_name) + if compvis_name.endswith("lora_up.weight"): + tensors[key] *= RATIO_OF_[blockid] + print(f"({blockid}) {compvis_name} " + f"updated with factor {RATIO_OF_[blockid]}") + + save_file(tensors, SAVE_PATH) + + print("Done") diff --git a/scripts/cache_cleaner(from sd-webui-gradio-cleaner).py b/scripts/cache_cleaner(from sd-webui-gradio-cleaner).py new file mode 100644 index 0000000000000000000000000000000000000000..ef75384ed66658b8185b48ba1bdf4d88c6ddf654 --- /dev/null +++ b/scripts/cache_cleaner(from sd-webui-gradio-cleaner).py @@ -0,0 +1,34 @@ +import shutil + +import gradio as gr +from fastapi import FastAPI +from fastapi import status +from fastapi.responses import JSONResponse +from modules.api.models import * +from modules.api import api + + +GRADIO_CACHE_DIR = '/tmp/gradio' + + +def sd_gradio_cleaner_api(_: gr.Blocks, app: FastAPI): + @app.post("/clean_cache") + async def clean_cache(): + try: + shutil.rmtree(GRADIO_CACHE_DIR) + except Exception as e: + return JSONResponse( + content=str(e), + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + ) + return JSONResponse( + content="Ok", + status_code=status.HTTP_200_OK, + ) + +try: + import modules.script_callbacks as script_callbacks + + script_callbacks.on_app_started(sd_gradio_cleaner_api) +except: + pass diff --git a/scripts/custom_code-Copy1.py b/scripts/custom_code-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..cc6f0d4908e89a865b8477de9b575515857c0923 --- /dev/null +++ b/scripts/custom_code-Copy1.py @@ -0,0 +1,90 @@ +import modules.scripts as scripts +import gradio as gr +import ast +import copy + +from modules.processing import Processed +from modules.shared import cmd_opts + + +def convertExpr2Expression(expr): + expr.lineno = 0 + expr.col_offset = 0 + result = ast.Expression(expr.value, lineno=0, col_offset = 0) + + return result + + +def exec_with_return(code, module): + """ + like exec() but can return values + https://stackoverflow.com/a/52361938/5862977 + """ + code_ast = ast.parse(code) + + init_ast = copy.deepcopy(code_ast) + init_ast.body = code_ast.body[:-1] + + last_ast = copy.deepcopy(code_ast) + last_ast.body = code_ast.body[-1:] + + exec(compile(init_ast, "", "exec"), module.__dict__) + if type(last_ast.body[0]) == ast.Expr: + return eval(compile(convertExpr2Expression(last_ast.body[0]), "", "eval"), module.__dict__) + else: + exec(compile(last_ast, "", "exec"), module.__dict__) + + +class Script(scripts.Script): + + def title(self): + return "Custom code" + + def show(self, is_img2img): + return cmd_opts.allow_code + + def ui(self, is_img2img): + example = """from modules.processing import process_images + +p.width = 768 +p.height = 768 +p.batch_size = 2 +p.steps = 10 + +return process_images(p) +""" + + + code = gr.Code(value=example, language="python", label="Python code", elem_id=self.elem_id("code")) + indent_level = gr.Number(label='Indent level', value=2, precision=0, elem_id=self.elem_id("indent_level")) + + return [code, indent_level] + + def run(self, p, code, indent_level): + assert cmd_opts.allow_code, '--allow-code option must be enabled' + + display_result_data = [[], -1, ""] + + def display(imgs, s=display_result_data[1], i=display_result_data[2]): + display_result_data[0] = imgs + display_result_data[1] = s + display_result_data[2] = i + + from types import ModuleType + module = ModuleType("testmodule") + module.__dict__.update(globals()) + module.p = p + module.display = display + + indent = " " * indent_level + indented = code.replace('\n', f"\n{indent}") + body = f"""def __webuitemp__(): +{indent}{indented} +__webuitemp__()""" + + result = exec_with_return(body, module) + + if isinstance(result, Processed): + return result + + return Processed(p, *display_result_data) diff --git a/scripts/custom_code.py b/scripts/custom_code.py new file mode 100644 index 0000000000000000000000000000000000000000..cc6f0d4908e89a865b8477de9b575515857c0923 --- /dev/null +++ b/scripts/custom_code.py @@ -0,0 +1,90 @@ +import modules.scripts as scripts +import gradio as gr +import ast +import copy + +from modules.processing import Processed +from modules.shared import cmd_opts + + +def convertExpr2Expression(expr): + expr.lineno = 0 + expr.col_offset = 0 + result = ast.Expression(expr.value, lineno=0, col_offset = 0) + + return result + + +def exec_with_return(code, module): + """ + like exec() but can return values + https://stackoverflow.com/a/52361938/5862977 + """ + code_ast = ast.parse(code) + + init_ast = copy.deepcopy(code_ast) + init_ast.body = code_ast.body[:-1] + + last_ast = copy.deepcopy(code_ast) + last_ast.body = code_ast.body[-1:] + + exec(compile(init_ast, "", "exec"), module.__dict__) + if type(last_ast.body[0]) == ast.Expr: + return eval(compile(convertExpr2Expression(last_ast.body[0]), "", "eval"), module.__dict__) + else: + exec(compile(last_ast, "", "exec"), module.__dict__) + + +class Script(scripts.Script): + + def title(self): + return "Custom code" + + def show(self, is_img2img): + return cmd_opts.allow_code + + def ui(self, is_img2img): + example = """from modules.processing import process_images + +p.width = 768 +p.height = 768 +p.batch_size = 2 +p.steps = 10 + +return process_images(p) +""" + + + code = gr.Code(value=example, language="python", label="Python code", elem_id=self.elem_id("code")) + indent_level = gr.Number(label='Indent level', value=2, precision=0, elem_id=self.elem_id("indent_level")) + + return [code, indent_level] + + def run(self, p, code, indent_level): + assert cmd_opts.allow_code, '--allow-code option must be enabled' + + display_result_data = [[], -1, ""] + + def display(imgs, s=display_result_data[1], i=display_result_data[2]): + display_result_data[0] = imgs + display_result_data[1] = s + display_result_data[2] = i + + from types import ModuleType + module = ModuleType("testmodule") + module.__dict__.update(globals()) + module.p = p + module.display = display + + indent = " " * indent_level + indented = code.replace('\n', f"\n{indent}") + body = f"""def __webuitemp__(): +{indent}{indented} +__webuitemp__()""" + + result = exec_with_return(body, module) + + if isinstance(result, Processed): + return result + + return Processed(p, *display_result_data) diff --git a/scripts/epiCFG_schedule_type.py b/scripts/epiCFG_schedule_type.py new file mode 100644 index 0000000000000000000000000000000000000000..81f7a14fed2e47271ca8d123556bedca728b7650 --- /dev/null +++ b/scripts/epiCFG_schedule_type.py @@ -0,0 +1,220 @@ +import modules.scripts as scripts +import gradio as gr +import os +import math +from modules import images +from modules.processing import process_images, Processed +from modules.shared import opts, cmd_opts, state +from math import floor + +class ProcessedImagesWrapper: + def __init__(self, images, schedule, script_title): + self.images = images + self.schedule = schedule + self.script_title = script_title # Store the script title + + def js(self): + flat_images = [] + for item in self.images: + if isinstance(item, list): + flat_images.extend([img for img in item if hasattr(img, 'js')]) + elif hasattr(item, 'js'): + flat_images.append(item) + return [image.js() for image in flat_images] + + @property + def info(self): + info_texts = [] + if isinstance(self.images, list): # Check if self.images is indeed a list + for index, image in enumerate(self.images): + if hasattr(image, 'info'): + # Append the schedule type to the existing parameters + original_params = image.info.get('parameters', 'Parameter Missing') + # Add the schedule type to the existing parameters + params_with_schedule = f"{original_params}, Script: {self.script_title}, Schedule: {self.schedule[index]}" + info_texts.append(params_with_schedule) + else: + return "self.images is not a list" + return "\n".join(info_texts) + + @property + def comments(self): + comments = [image.comments for image in self.images if hasattr(image, 'comments')] + return "\n".join(comments) if comments else "" + +class Script(scripts.Script): + def title(self): + return "epiCFG Schedule Type" + + def show(self, is_img2img): + return not(is_img2img) + + def ui(self, is_img2img): + schedule_options = [ + 'Constant', 'Linear', 'Clamp-Linear (c=4.0)', 'Clamp-Linear (c=2.0)', + 'Clamp-Linear (c=1.0)', 'Inverse-Linear', 'PCS (s=0.01)', 'PCS (s=0.1)', + 'PCS (s=1.0)', 'PCS (s=2.0)', 'PCS (s=4.0)', 'Clamp-Cosine (c=4.0)', + 'Clamp-Cosine (c=2.0)', 'Clamp-Cosine (c=1.0)', 'Cosine', 'Sine', + 'V-Shape', 'A-Shape', 'Interval' + ] + schedule_multiselect_dropdown = gr.components.Dropdown(label="Schedule", choices=schedule_options, default="Inverse-Linear", multiselect=True) + return [schedule_multiselect_dropdown] + + def run(self, p, schedules): + strength = 1.0 # Fixed strength value + processed_images = [] + all_processed_images = [] + script_title = self.title() # Get the title from the title method + + if p.sampler_name in ('Euler a', 'Euler', 'LMS', 'DPM++ 2M', 'DPM fast', 'LMS Karras', 'DPM++ 2M Karras','DPM++ 2M SDE','DPM++ 3M SDE','Restart'): + max_mul_count = p.steps * p.batch_size + steps_per_mul = p.batch_size + elif p.sampler_name in ('Heun', 'DPM2', 'DPM2 a', 'DPM++ 2S a', 'DPM2 Karras', 'DPM2 a Karras', 'DPM++ 2S a Karras', 'DPM++ SDE', 'DPM++ SDE Karras','UniPC'): + max_mul_count = ((p.steps * 2) - 1) * p.batch_size + steps_per_mul = 2 * p.batch_size + elif p.sampler_name == 'DDIM': + max_mul_count = fix_ddim_step_count(p.steps) + steps_per_mul = 1 + elif p.sampler_name == 'UniPC': + max_mul_count = fix_ddim_step_count(p.steps) + steps_per_mul = 1 + elif p.sampler_name == 'PLMS': + max_mul_count = fix_ddim_step_count(p.steps) + 1 + steps_per_mul = 1 + else: + print('!!!warning: unsupported sampler ', p.sampler_name) + return + target_value = p.cfg_scale * (1 - strength) + saved_obj = p.cfg_scale + + for schedule in schedules: + print('\nepiCFG: ', schedule, end='\n') + p.cfg_scale = Fake_float(p.cfg_scale, target_value, max_mul_count, steps_per_mul, p.steps, schedule) + proc = process_images(p) + processed_images = process_image_to_array(proc) + all_processed_images.extend(processed_images) + p.cfg_scale = saved_obj + return ProcessedImagesWrapper(all_processed_images, schedules, script_title) + +class Fake_float(float): + def __new__(self, orig_value, target_value, max_mul_count, steps_per_mul, max_steps, schedule): + return float.__new__(self, orig_value) + + def __init__(self, orig_value, target_value, max_mul_count, steps_per_mul, max_steps, schedule): + float.__init__(orig_value) + self.orig_value = orig_value + self.target_value = target_value + self.max_mul_count = max_mul_count + self.current_mul = 0 + self.steps_per_mul = steps_per_mul + self.current_step = 0 + self.max_step_count = (max_mul_count // steps_per_mul) + (max_mul_count % steps_per_mul > 0) + self.max_steps = max_steps + self.schedule = schedule + + def __mul__(self,other): + return self.fake_mul(other) + + def __rmul__(self,other): + return self.fake_mul(other) + + def fake_mul(self,other): + if (self.max_step_count==1): + fake_value= self.orig_value + else: + if self.schedule == 'Constant': + fake_value = constant_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'Linear': + fake_value = linear_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'Clamp-Linear (c=4.0)': + fake_value = clamp_linear_schedule(self.current_step, self.max_steps, self.orig_value, 4.0) + elif self.schedule == 'Clamp-Linear (c=2.0)': + fake_value = clamp_linear_schedule(self.current_step, self.max_steps, self.orig_value, 2.0) + elif self.schedule == 'Clamp-Linear (c=1.0)': + fake_value = clamp_linear_schedule(self.current_step, self.max_steps, self.orig_value, 1.0) + elif self.schedule == 'Inverse-Linear': + fake_value = invlinear_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'PCS (s=0.01)': + fake_value = powered_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 0.01) + elif self.schedule == 'PCS (s=0.1)': + fake_value = powered_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 0.1) + elif self.schedule == 'PCS (s=1.0)': + fake_value = powered_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 1.0) + elif self.schedule == 'PCS (s=2.0)': + fake_value = powered_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 2.0) + elif self.schedule == 'PCS (s=4.0)': + fake_value = powered_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 4.0) + elif self.schedule == 'Clamp-Cosine (c=4.0)': + fake_value = clamp_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 4.0) + elif self.schedule == 'Clamp-Cosine (c=2.0)': + fake_value = clamp_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 2.0) + elif self.schedule == 'Clamp-Cosine (c=1.0)': + fake_value = clamp_cosine_schedule(self.current_step, self.max_steps, self.orig_value, 1.0) + elif self.schedule == 'Cosine': + fake_value = cosine_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'Sine': + fake_value = sine_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'V-Shape': + fake_value = v_shape_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'A-Shape': + fake_value = a_shape_schedule(self.current_step, self.max_steps, self.orig_value) + elif self.schedule == 'Interval': + fake_value = interval_schedule(self.current_step, self.max_steps, self.orig_value, 0.25, 5.42) + else: + print(f"Invalid CFG schedule: {self.schedule}") + fake_value = self.orig_value + self.current_mul = (self.current_mul+1) % self.max_mul_count + self.current_step = (self.current_mul) // self.steps_per_mul + return fake_value * other + +def process_image_to_array(processed): + if hasattr(processed, 'images') and isinstance(processed.images, list): + return processed.images + else: + print("Processed object does not contain an iterable list of images.") + return [] + +def fix_ddim_step_count(steps): + valid_step = 999 / (1000 // steps) + if valid_step == floor(valid_step): steps=int(valid_step)+1 + if ((1000 % steps)!=0): steps +=1 + return steps + +def constant_schedule(step: int, max_steps: int, w0: float): + return w0 + +def linear_schedule(step: int, max_steps: int, w0: float): + return w0 * 2 * (1 - step / max_steps) + +def clamp_linear_schedule(step: int, max_steps: int, w0: float, c: float): + return max(c, linear_schedule(step, max_steps, w0)) + +def clamp_cosine_schedule(step: int, max_steps: int, w0: float, c: float): + return max(c, cosine_schedule(step, max_steps, w0)) + +def invlinear_schedule(step: int, max_steps: int, w0: float): + return w0 * 2 * (step / max_steps) + +def powered_cosine_schedule(step: int, max_steps: int, w0: float, s: float): + return w0 * ((1 - math.cos(math.pi * ((max_steps - step) / max_steps)**s))/2.0) + +def cosine_schedule(step: int, max_steps: int, w0: float): + return w0 * (1 + math.cos(math.pi * step / max_steps)) + +def sine_schedule(step: int, max_steps: int, w0: float): + return w0 * (math.sin((math.pi * step / max_steps) - (math.pi / 2)) + 1) + +def v_shape_schedule(step: int, max_steps: int, w0: float): + if step < max_steps / 2: + return invlinear_schedule(step, max_steps, w0) + return linear_schedule(step, max_steps, w0) + +def a_shape_schedule(step: int, max_steps: int, w0: float): + if step < max_steps / 2: + return linear_schedule(step, max_steps, w0) + return invlinear_schedule(step, max_steps, w0) + +def interval_schedule(step: int, max_steps: int, w0: float, low: float, high: float): + if low <= step <= high: + return w0 + return 1.0 diff --git a/scripts/external_masking.py b/scripts/external_masking.py new file mode 100644 index 0000000000000000000000000000000000000000..1ae80cb1764b445591e448c00d26df5af52e6bce --- /dev/null +++ b/scripts/external_masking.py @@ -0,0 +1,271 @@ +import math +import os +import sys +import traceback + + +import cv2 +from PIL import Image +import numpy as np + +lastx,lasty=None,None +zoomOrigin = 0,0 +zoomFactor = 1 + +midDragStart = None + +def display_mask_ui(image,mask,max_size,initPolys): + global lastx,lasty,zoomOrigin,zoomFactor + + lastx,lasty=None,None + zoomOrigin = 0,0 + zoomFactor = 1 + + polys = initPolys + + def on_mouse(event, x, y, buttons, param): + global lastx,lasty,zoomFactor,midDragStart,zoomOrigin + + lastx,lasty = (x+zoomOrigin[0])/zoomFactor,(y+zoomOrigin[1])/zoomFactor + + if event == cv2.EVENT_LBUTTONDOWN: + polys[-1].append((lastx,lasty)) + elif event == cv2.EVENT_RBUTTONDOWN: + polys.append([]) + elif event == cv2.EVENT_MBUTTONDOWN: + midDragStart = zoomOrigin[0]+x,zoomOrigin[1]+y + elif event == cv2.EVENT_MBUTTONUP: + if midDragStart is not None: + zoomOrigin = max(0,midDragStart[0]-x),max(0,midDragStart[1]-y) + midDragStart = None + elif event == cv2.EVENT_MOUSEMOVE: + if midDragStart is not None: + zoomOrigin = max(0,midDragStart[0]-x),max(0,midDragStart[1]-y) + elif event == cv2.EVENT_MOUSEWHEEL: + origZoom = zoomFactor + if buttons > 0: + zoomFactor *= 1.1 + else: + zoomFactor *= 0.9 + zoomFactor = max(1,zoomFactor) + + zoomOrigin = max(0,int(zoomOrigin[0]+ (max_size*0.25*(zoomFactor-origZoom)))) , max(0,int(zoomOrigin[1] + (max_size*0.25*(zoomFactor-origZoom)))) + + + + opencvImage = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + if mask is None: + opencvMask = cv2.cvtColor( np.array(opencvImage) , cv2.COLOR_BGR2GRAY) + else: + opencvMask = np.array(mask) + + + maxdim = max(opencvImage.shape[1],opencvImage.shape[0]) + + factor = max_size/maxdim + + + cv2.namedWindow('MaskingWindow', cv2.WINDOW_AUTOSIZE) + cv2.setWindowProperty('MaskingWindow', cv2.WND_PROP_TOPMOST, 1) + cv2.setMouseCallback('MaskingWindow', on_mouse) + + font = cv2.FONT_HERSHEY_SIMPLEX + + srcImage = opencvImage.copy() + combinedImage = opencvImage.copy() + + interp = cv2.INTER_CUBIC + if zoomFactor*factor < 0: + interp = cv2.INTER_AREA + + zoomedSrc = cv2.resize(srcImage,(None,None),fx=zoomFactor*factor,fy=zoomFactor*factor,interpolation=interp) + zoomedSrc = zoomedSrc[zoomOrigin[1]:zoomOrigin[1]+max_size,zoomOrigin[0]:zoomOrigin[0]+max_size,:] + + lastZoomFactor = zoomFactor + lastZoomOrigin = zoomOrigin + while 1: + + if lastZoomFactor != zoomFactor or lastZoomOrigin != zoomOrigin: + interp = cv2.INTER_CUBIC + if zoomFactor*factor < 0: + interp = cv2.INTER_AREA + zoomedSrc = cv2.resize(srcImage,(None,None),fx=zoomFactor*factor,fy=zoomFactor*factor,interpolation=interp) + zoomedSrc = zoomedSrc[zoomOrigin[1]:zoomOrigin[1]+max_size,zoomOrigin[0]:zoomOrigin[0]+max_size,:] + zoomedSrc = cv2.copyMakeBorder(zoomedSrc, 0, max_size-zoomedSrc.shape[0], 0, max_size-zoomedSrc.shape[1], cv2.BORDER_CONSTANT) + + lastZoomFactor = zoomFactor + lastZoomOrigin = zoomOrigin + + foreground = np.zeros_like(zoomedSrc) + + for i,polyline in enumerate(polys): + if len(polyline)>0: + + segs = polyline[::] + + active=False + if len(polys[-1])>0 and i==len(polys)-1 and lastx is not None: + segs = polyline+[(lastx,lasty)] + active=True + + segs = np.array(segs) - np.array([(zoomOrigin[0]/zoomFactor,zoomOrigin[1]/zoomFactor)]) + segs = (np.array([segs])*zoomFactor).astype(int) + + if active: + cv2.fillPoly(foreground, (np.array(segs)) , ( 190, 107, 253), 0) + else: + cv2.fillPoly(foreground, (np.array(segs)) , (255, 255, 255), 0) + + if active: + for x,y in segs[0]: + cv2.circle(foreground, (int(x),int(y)), 5, (25,25,25), 3) + cv2.circle(foreground, (int(x),int(y)), 5, (255,255,255), 2) + + + foreground[foreground<1] = zoomedSrc[foreground<1] + combinedImage = cv2.addWeighted(zoomedSrc, 0.5, foreground, 0.5, 0) + + helpText='Q=Save, C=Reset, LeftClick=Add new point to polygon, Rightclick=Close polygon, MouseWheel=Zoom, MidDrag=Pan' + combinedImage = cv2.putText(combinedImage, helpText, (0,11), font, 0.4, (0,0,0), 2, cv2.LINE_AA) + combinedImage = cv2.putText(combinedImage, helpText, (0,11), font, 0.4, (255,255,255), 1, cv2.LINE_AA) + + cv2.imshow('MaskingWindow',combinedImage) + + try: + key = cv2.waitKey(1) + if key == ord('q'): + if len(polys[0])>0: + newmask = np.zeros_like(cv2.cvtColor( opencvMask.astype('uint8') ,cv2.COLOR_GRAY2BGR) ) + for i,polyline in enumerate(polys): + if len(polyline)>0: + segs = [(int(a/factor),int(b/factor)) for a,b in polyline] + cv2.fillPoly(newmask, np.array([segs]), (255,255,255), 0) + cv2.destroyWindow('MaskingWindow') + return Image.fromarray( cv2.cvtColor( newmask, cv2.COLOR_BGR2GRAY) ),polys + break + if key == ord('c'): + polys = [[]] + + except Exception as e: + print(e) + break + + cv2.destroyWindow('MaskingWindow') + return mask,polys + +if __name__ == '__main__': + img = Image.open('K:\\test2.png') + oldmask = Image.new('L',img.size,(0,)) + newmask,newPolys = display_mask_ui(img,oldmask,1024,[[]]) + + opencvImg = cv2.cvtColor( np.array(img) , cv2.COLOR_RGB2BGR) + opencvMask = cv2.cvtColor( np.array(newmask) , cv2.COLOR_GRAY2BGR) + + combinedImage = cv2.addWeighted(opencvImg, 0.5, opencvMask, 0.5, 0) + combinedImage = Image.fromarray( cv2.cvtColor( combinedImage , cv2.COLOR_BGR2RGB)) + + display_mask_ui(combinedImage,oldmask,1024,[[]]) + + + exit() + +import modules.scripts as scripts +import gradio as gr + +from modules.processing import Processed, process_images +from modules.shared import opts, cmd_opts, state + +class Script(scripts.Script): + + def title(self): + return "External Image Masking" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + if not is_img2img: + return None + + initialSize = 1024 + + try: + import tkinter as tk + root = tk.Tk() + screen_width = int(root.winfo_screenwidth()) + screen_height = int(root.winfo_screenheight()) + print(screen_width,screen_height) + initialSize = min(screen_width,screen_height)-50 + print(initialSize) + except Exception as e: + print(e) + + max_size = gr.Slider(label="Masking preview size", minimum=512, maximum=initialSize*2, step=8, value=initialSize) + with gr.Row(): + ask_on_each_run = gr.Checkbox(label='Draw new mask on every run', value=False) + non_contigious_split = gr.Checkbox(label='Process non-contigious masks separately', value=False) + + return [max_size,ask_on_each_run,non_contigious_split] + + def run(self, p, max_size, ask_on_each_run, non_contigious_split): + + if not hasattr(self,'lastImg'): + self.lastImg = None + + if not hasattr(self,'lastMask'): + self.lastMask = None + + if not hasattr(self,'lastPolys'): + self.lastPolys = [[]] + + if ask_on_each_run or self.lastImg is None or self.lastImg != p.init_images[0]: + + if self.lastImg is None or self.lastImg != p.init_images[0]: + self.lastPolys = [[]] + + p.image_mask,self.lastPolys = display_mask_ui(p.init_images[0],p.image_mask,max_size,self.lastPolys) + self.lastImg = p.init_images[0] + if p.image_mask is not None: + self.lastMask = p.image_mask.copy() + elif hasattr(self,'lastMask') and self.lastMask is not None: + p.image_mask = self.lastMask.copy() + + if non_contigious_split: + maskImgArr = np.array(p.image_mask) + ret, markers = cv2.connectedComponents(maskImgArr) + markerCount = markers.max() + + if markerCount > 1: + tempimages = [] + tempMasks = [] + for maski in range(1,markerCount+1): + print('maski',maski) + maskSection = np.zeros_like(maskImgArr) + maskSection[markers==maski] = 255 + p.image_mask = Image.fromarray( maskSection.copy() ) + proc = process_images(p) + images = proc.images + tempimages.append(np.array(images[0])) + tempMasks.append(np.array(maskSection.copy())) + + finalImage = tempimages[0].copy() + + for outimg,outmask in zip(tempimages,tempMasks): + + resizeimg = cv2.resize(outimg, (finalImage.shape[0],finalImage.shape[1]) ) + resizedMask = cv2.resize(outmask, (finalImage.shape[0],finalImage.shape[1]) ) + + finalImage[resizedMask==255] = resizeimg[resizedMask==255] + images = [finalImage] + + + else: + proc = process_images(p) + images = proc.images + else: + proc = process_images(p) + images = proc.images + + proc.images = images + return proc diff --git a/scripts/img2imgalt-Copy1.py b/scripts/img2imgalt-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..1e833fa898fdc686231c50c2dfbb085d8346a8c2 --- /dev/null +++ b/scripts/img2imgalt-Copy1.py @@ -0,0 +1,218 @@ +from collections import namedtuple + +import numpy as np +from tqdm import trange + +import modules.scripts as scripts +import gradio as gr + +from modules import processing, shared, sd_samplers, sd_samplers_common + +import torch +import k_diffusion as K + +def find_noise_for_image(p, cond, uncond, cfg_scale, steps): + x = p.init_latent + + s_in = x.new_ones([x.shape[0]]) + if shared.sd_model.parameterization == "v": + dnw = K.external.CompVisVDenoiser(shared.sd_model) + skip = 1 + else: + dnw = K.external.CompVisDenoiser(shared.sd_model) + skip = 0 + sigmas = dnw.get_sigmas(steps).flip(0) + + shared.state.sampling_steps = steps + + for i in trange(1, len(sigmas)): + shared.state.sampling_step += 1 + + x_in = torch.cat([x] * 2) + sigma_in = torch.cat([sigmas[i] * s_in] * 2) + cond_in = torch.cat([uncond, cond]) + + image_conditioning = torch.cat([p.image_conditioning] * 2) + cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} + + c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] + t = dnw.sigma_to_t(sigma_in) + + eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) + denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) + + denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale + + d = (x - denoised) / sigmas[i] + dt = sigmas[i] - sigmas[i - 1] + + x = x + d * dt + + sd_samplers_common.store_latent(x) + + # This shouldn't be necessary, but solved some VRAM issues + del x_in, sigma_in, cond_in, c_out, c_in, t, + del eps, denoised_uncond, denoised_cond, denoised, d, dt + + shared.state.nextjob() + + return x / x.std() + + +Cached = namedtuple("Cached", ["noise", "cfg_scale", "steps", "latent", "original_prompt", "original_negative_prompt", "sigma_adjustment"]) + + +# Based on changes suggested by briansemrau in https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/736 +def find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg_scale, steps): + x = p.init_latent + + s_in = x.new_ones([x.shape[0]]) + if shared.sd_model.parameterization == "v": + dnw = K.external.CompVisVDenoiser(shared.sd_model) + skip = 1 + else: + dnw = K.external.CompVisDenoiser(shared.sd_model) + skip = 0 + sigmas = dnw.get_sigmas(steps).flip(0) + + shared.state.sampling_steps = steps + + for i in trange(1, len(sigmas)): + shared.state.sampling_step += 1 + + x_in = torch.cat([x] * 2) + sigma_in = torch.cat([sigmas[i - 1] * s_in] * 2) + cond_in = torch.cat([uncond, cond]) + + image_conditioning = torch.cat([p.image_conditioning] * 2) + cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} + + c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] + + if i == 1: + t = dnw.sigma_to_t(torch.cat([sigmas[i] * s_in] * 2)) + else: + t = dnw.sigma_to_t(sigma_in) + + eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) + denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) + + denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale + + if i == 1: + d = (x - denoised) / (2 * sigmas[i]) + else: + d = (x - denoised) / sigmas[i - 1] + + dt = sigmas[i] - sigmas[i - 1] + x = x + d * dt + + sd_samplers_common.store_latent(x) + + # This shouldn't be necessary, but solved some VRAM issues + del x_in, sigma_in, cond_in, c_out, c_in, t, + del eps, denoised_uncond, denoised_cond, denoised, d, dt + + shared.state.nextjob() + + return x / sigmas[-1] + + +class Script(scripts.Script): + def __init__(self): + self.cache = None + + def title(self): + return "img2img alternative test" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + info = gr.Markdown(''' + * `CFG Scale` should be 2 or lower. + ''') + + override_sampler = gr.Checkbox(label="Override `Sampling method` to Euler?(this method is built for it)", value=True, elem_id=self.elem_id("override_sampler")) + + override_prompt = gr.Checkbox(label="Override `prompt` to the same value as `original prompt`?(and `negative prompt`)", value=True, elem_id=self.elem_id("override_prompt")) + original_prompt = gr.Textbox(label="Original prompt", lines=1, elem_id=self.elem_id("original_prompt")) + original_negative_prompt = gr.Textbox(label="Original negative prompt", lines=1, elem_id=self.elem_id("original_negative_prompt")) + + override_steps = gr.Checkbox(label="Override `Sampling Steps` to the same value as `Decode steps`?", value=True, elem_id=self.elem_id("override_steps")) + st = gr.Slider(label="Decode steps", minimum=1, maximum=150, step=1, value=50, elem_id=self.elem_id("st")) + + override_strength = gr.Checkbox(label="Override `Denoising strength` to 1?", value=True, elem_id=self.elem_id("override_strength")) + + cfg = gr.Slider(label="Decode CFG scale", minimum=0.0, maximum=15.0, step=0.1, value=1.0, elem_id=self.elem_id("cfg")) + randomness = gr.Slider(label="Randomness", minimum=0.0, maximum=1.0, step=0.01, value=0.0, elem_id=self.elem_id("randomness")) + sigma_adjustment = gr.Checkbox(label="Sigma adjustment for finding noise for image", value=False, elem_id=self.elem_id("sigma_adjustment")) + + return [ + info, + override_sampler, + override_prompt, original_prompt, original_negative_prompt, + override_steps, st, + override_strength, + cfg, randomness, sigma_adjustment, + ] + + def run(self, p, _, override_sampler, override_prompt, original_prompt, original_negative_prompt, override_steps, st, override_strength, cfg, randomness, sigma_adjustment): + # Override + if override_sampler: + p.sampler_name = "Euler" + if override_prompt: + p.prompt = original_prompt + p.negative_prompt = original_negative_prompt + if override_steps: + p.steps = st + if override_strength: + p.denoising_strength = 1.0 + + def sample_extra(conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts): + lat = (p.init_latent.cpu().numpy() * 10).astype(int) + + same_params = self.cache is not None and self.cache.cfg_scale == cfg and self.cache.steps == st \ + and self.cache.original_prompt == original_prompt \ + and self.cache.original_negative_prompt == original_negative_prompt \ + and self.cache.sigma_adjustment == sigma_adjustment + same_everything = same_params and self.cache.latent.shape == lat.shape and np.abs(self.cache.latent-lat).sum() < 100 + + if same_everything: + rec_noise = self.cache.noise + else: + shared.state.job_count += 1 + cond = p.sd_model.get_learned_conditioning(p.batch_size * [original_prompt]) + uncond = p.sd_model.get_learned_conditioning(p.batch_size * [original_negative_prompt]) + if sigma_adjustment: + rec_noise = find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg, st) + else: + rec_noise = find_noise_for_image(p, cond, uncond, cfg, st) + self.cache = Cached(rec_noise, cfg, st, lat, original_prompt, original_negative_prompt, sigma_adjustment) + + rand_noise = processing.create_random_tensors(p.init_latent.shape[1:], seeds=seeds, subseeds=subseeds, subseed_strength=p.subseed_strength, seed_resize_from_h=p.seed_resize_from_h, seed_resize_from_w=p.seed_resize_from_w, p=p) + + combined_noise = ((1 - randomness) * rec_noise + randomness * rand_noise) / ((randomness**2 + (1-randomness)**2) ** 0.5) + + sampler = sd_samplers.create_sampler(p.sampler_name, p.sd_model) + + sigmas = sampler.model_wrap.get_sigmas(p.steps) + + noise_dt = combined_noise - (p.init_latent / sigmas[0]) + + p.seed = p.seed + 1 + + return sampler.sample_img2img(p, p.init_latent, noise_dt, conditioning, unconditional_conditioning, image_conditioning=p.image_conditioning) + + p.sample = sample_extra + + p.extra_generation_params["Decode prompt"] = original_prompt + p.extra_generation_params["Decode negative prompt"] = original_negative_prompt + p.extra_generation_params["Decode CFG scale"] = cfg + p.extra_generation_params["Decode steps"] = st + p.extra_generation_params["Randomness"] = randomness + p.extra_generation_params["Sigma Adjustment"] = sigma_adjustment + + processed = processing.process_images(p) + + return processed diff --git a/scripts/img2imgalt.py b/scripts/img2imgalt.py new file mode 100644 index 0000000000000000000000000000000000000000..1e833fa898fdc686231c50c2dfbb085d8346a8c2 --- /dev/null +++ b/scripts/img2imgalt.py @@ -0,0 +1,218 @@ +from collections import namedtuple + +import numpy as np +from tqdm import trange + +import modules.scripts as scripts +import gradio as gr + +from modules import processing, shared, sd_samplers, sd_samplers_common + +import torch +import k_diffusion as K + +def find_noise_for_image(p, cond, uncond, cfg_scale, steps): + x = p.init_latent + + s_in = x.new_ones([x.shape[0]]) + if shared.sd_model.parameterization == "v": + dnw = K.external.CompVisVDenoiser(shared.sd_model) + skip = 1 + else: + dnw = K.external.CompVisDenoiser(shared.sd_model) + skip = 0 + sigmas = dnw.get_sigmas(steps).flip(0) + + shared.state.sampling_steps = steps + + for i in trange(1, len(sigmas)): + shared.state.sampling_step += 1 + + x_in = torch.cat([x] * 2) + sigma_in = torch.cat([sigmas[i] * s_in] * 2) + cond_in = torch.cat([uncond, cond]) + + image_conditioning = torch.cat([p.image_conditioning] * 2) + cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} + + c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] + t = dnw.sigma_to_t(sigma_in) + + eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) + denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) + + denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale + + d = (x - denoised) / sigmas[i] + dt = sigmas[i] - sigmas[i - 1] + + x = x + d * dt + + sd_samplers_common.store_latent(x) + + # This shouldn't be necessary, but solved some VRAM issues + del x_in, sigma_in, cond_in, c_out, c_in, t, + del eps, denoised_uncond, denoised_cond, denoised, d, dt + + shared.state.nextjob() + + return x / x.std() + + +Cached = namedtuple("Cached", ["noise", "cfg_scale", "steps", "latent", "original_prompt", "original_negative_prompt", "sigma_adjustment"]) + + +# Based on changes suggested by briansemrau in https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/736 +def find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg_scale, steps): + x = p.init_latent + + s_in = x.new_ones([x.shape[0]]) + if shared.sd_model.parameterization == "v": + dnw = K.external.CompVisVDenoiser(shared.sd_model) + skip = 1 + else: + dnw = K.external.CompVisDenoiser(shared.sd_model) + skip = 0 + sigmas = dnw.get_sigmas(steps).flip(0) + + shared.state.sampling_steps = steps + + for i in trange(1, len(sigmas)): + shared.state.sampling_step += 1 + + x_in = torch.cat([x] * 2) + sigma_in = torch.cat([sigmas[i - 1] * s_in] * 2) + cond_in = torch.cat([uncond, cond]) + + image_conditioning = torch.cat([p.image_conditioning] * 2) + cond_in = {"c_concat": [image_conditioning], "c_crossattn": [cond_in]} + + c_out, c_in = [K.utils.append_dims(k, x_in.ndim) for k in dnw.get_scalings(sigma_in)[skip:]] + + if i == 1: + t = dnw.sigma_to_t(torch.cat([sigmas[i] * s_in] * 2)) + else: + t = dnw.sigma_to_t(sigma_in) + + eps = shared.sd_model.apply_model(x_in * c_in, t, cond=cond_in) + denoised_uncond, denoised_cond = (x_in + eps * c_out).chunk(2) + + denoised = denoised_uncond + (denoised_cond - denoised_uncond) * cfg_scale + + if i == 1: + d = (x - denoised) / (2 * sigmas[i]) + else: + d = (x - denoised) / sigmas[i - 1] + + dt = sigmas[i] - sigmas[i - 1] + x = x + d * dt + + sd_samplers_common.store_latent(x) + + # This shouldn't be necessary, but solved some VRAM issues + del x_in, sigma_in, cond_in, c_out, c_in, t, + del eps, denoised_uncond, denoised_cond, denoised, d, dt + + shared.state.nextjob() + + return x / sigmas[-1] + + +class Script(scripts.Script): + def __init__(self): + self.cache = None + + def title(self): + return "img2img alternative test" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + info = gr.Markdown(''' + * `CFG Scale` should be 2 or lower. + ''') + + override_sampler = gr.Checkbox(label="Override `Sampling method` to Euler?(this method is built for it)", value=True, elem_id=self.elem_id("override_sampler")) + + override_prompt = gr.Checkbox(label="Override `prompt` to the same value as `original prompt`?(and `negative prompt`)", value=True, elem_id=self.elem_id("override_prompt")) + original_prompt = gr.Textbox(label="Original prompt", lines=1, elem_id=self.elem_id("original_prompt")) + original_negative_prompt = gr.Textbox(label="Original negative prompt", lines=1, elem_id=self.elem_id("original_negative_prompt")) + + override_steps = gr.Checkbox(label="Override `Sampling Steps` to the same value as `Decode steps`?", value=True, elem_id=self.elem_id("override_steps")) + st = gr.Slider(label="Decode steps", minimum=1, maximum=150, step=1, value=50, elem_id=self.elem_id("st")) + + override_strength = gr.Checkbox(label="Override `Denoising strength` to 1?", value=True, elem_id=self.elem_id("override_strength")) + + cfg = gr.Slider(label="Decode CFG scale", minimum=0.0, maximum=15.0, step=0.1, value=1.0, elem_id=self.elem_id("cfg")) + randomness = gr.Slider(label="Randomness", minimum=0.0, maximum=1.0, step=0.01, value=0.0, elem_id=self.elem_id("randomness")) + sigma_adjustment = gr.Checkbox(label="Sigma adjustment for finding noise for image", value=False, elem_id=self.elem_id("sigma_adjustment")) + + return [ + info, + override_sampler, + override_prompt, original_prompt, original_negative_prompt, + override_steps, st, + override_strength, + cfg, randomness, sigma_adjustment, + ] + + def run(self, p, _, override_sampler, override_prompt, original_prompt, original_negative_prompt, override_steps, st, override_strength, cfg, randomness, sigma_adjustment): + # Override + if override_sampler: + p.sampler_name = "Euler" + if override_prompt: + p.prompt = original_prompt + p.negative_prompt = original_negative_prompt + if override_steps: + p.steps = st + if override_strength: + p.denoising_strength = 1.0 + + def sample_extra(conditioning, unconditional_conditioning, seeds, subseeds, subseed_strength, prompts): + lat = (p.init_latent.cpu().numpy() * 10).astype(int) + + same_params = self.cache is not None and self.cache.cfg_scale == cfg and self.cache.steps == st \ + and self.cache.original_prompt == original_prompt \ + and self.cache.original_negative_prompt == original_negative_prompt \ + and self.cache.sigma_adjustment == sigma_adjustment + same_everything = same_params and self.cache.latent.shape == lat.shape and np.abs(self.cache.latent-lat).sum() < 100 + + if same_everything: + rec_noise = self.cache.noise + else: + shared.state.job_count += 1 + cond = p.sd_model.get_learned_conditioning(p.batch_size * [original_prompt]) + uncond = p.sd_model.get_learned_conditioning(p.batch_size * [original_negative_prompt]) + if sigma_adjustment: + rec_noise = find_noise_for_image_sigma_adjustment(p, cond, uncond, cfg, st) + else: + rec_noise = find_noise_for_image(p, cond, uncond, cfg, st) + self.cache = Cached(rec_noise, cfg, st, lat, original_prompt, original_negative_prompt, sigma_adjustment) + + rand_noise = processing.create_random_tensors(p.init_latent.shape[1:], seeds=seeds, subseeds=subseeds, subseed_strength=p.subseed_strength, seed_resize_from_h=p.seed_resize_from_h, seed_resize_from_w=p.seed_resize_from_w, p=p) + + combined_noise = ((1 - randomness) * rec_noise + randomness * rand_noise) / ((randomness**2 + (1-randomness)**2) ** 0.5) + + sampler = sd_samplers.create_sampler(p.sampler_name, p.sd_model) + + sigmas = sampler.model_wrap.get_sigmas(p.steps) + + noise_dt = combined_noise - (p.init_latent / sigmas[0]) + + p.seed = p.seed + 1 + + return sampler.sample_img2img(p, p.init_latent, noise_dt, conditioning, unconditional_conditioning, image_conditioning=p.image_conditioning) + + p.sample = sample_extra + + p.extra_generation_params["Decode prompt"] = original_prompt + p.extra_generation_params["Decode negative prompt"] = original_negative_prompt + p.extra_generation_params["Decode CFG scale"] = cfg + p.extra_generation_params["Decode steps"] = st + p.extra_generation_params["Randomness"] = randomness + p.extra_generation_params["Sigma Adjustment"] = sigma_adjustment + + processed = processing.process_images(p) + + return processed diff --git a/scripts/loopback-Copy1.py b/scripts/loopback-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..800ee882a16cb3e98afc3358bfc7e6302534804c --- /dev/null +++ b/scripts/loopback-Copy1.py @@ -0,0 +1,140 @@ +import math + +import gradio as gr +import modules.scripts as scripts +from modules import deepbooru, images, processing, shared +from modules.processing import Processed +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "Loopback" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + loops = gr.Slider(minimum=1, maximum=32, step=1, label='Loops', value=4, elem_id=self.elem_id("loops")) + final_denoising_strength = gr.Slider(minimum=0, maximum=1, step=0.01, label='Final denoising strength', value=0.5, elem_id=self.elem_id("final_denoising_strength")) + denoising_curve = gr.Dropdown(label="Denoising strength curve", choices=["Aggressive", "Linear", "Lazy"], value="Linear") + append_interrogation = gr.Dropdown(label="Append interrogated prompt at each iteration", choices=["None", "CLIP", "DeepBooru"], value="None") + + return [loops, final_denoising_strength, denoising_curve, append_interrogation] + + def run(self, p, loops, final_denoising_strength, denoising_curve, append_interrogation): + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Final denoising strength": final_denoising_strength, + "Denoising curve": denoising_curve + } + + p.batch_size = 1 + p.n_iter = 1 + + info = None + initial_seed = None + initial_info = None + initial_denoising_strength = p.denoising_strength + + grids = [] + all_images = [] + original_init_image = p.init_images + original_prompt = p.prompt + original_inpainting_fill = p.inpainting_fill + state.job_count = loops * batch_count + + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + + def calculate_denoising_strength(loop): + strength = initial_denoising_strength + + if loops == 1: + return strength + + progress = loop / (loops - 1) + if denoising_curve == "Aggressive": + strength = math.sin((progress) * math.pi * 0.5) + elif denoising_curve == "Lazy": + strength = 1 - math.cos((progress) * math.pi * 0.5) + else: + strength = progress + + change = (final_denoising_strength - initial_denoising_strength) * strength + return initial_denoising_strength + change + + history = [] + + for n in range(batch_count): + # Reset to original init image at the start of each batch + p.init_images = original_init_image + + # Reset to original denoising strength + p.denoising_strength = initial_denoising_strength + + last_image = None + + for i in range(loops): + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if opts.img2img_color_correction: + p.color_corrections = initial_color_corrections + + if append_interrogation != "None": + p.prompt = f"{original_prompt}, " if original_prompt else "" + if append_interrogation == "CLIP": + p.prompt += shared.interrogator.interrogate(p.init_images[0]) + elif append_interrogation == "DeepBooru": + p.prompt += deepbooru.model.tag(p.init_images[0]) + + state.job = f"Iteration {i + 1}/{loops}, batch {n + 1}/{batch_count}" + + processed = processing.process_images(p) + + # Generation cancelled. + if state.interrupted or state.stopping_generation: + break + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + p.seed = processed.seed + 1 + p.denoising_strength = calculate_denoising_strength(i + 1) + + if state.skipped: + break + + last_image = processed.images[0] + p.init_images = [last_image] + p.inpainting_fill = 1 # Set "masked content" to "original" for next loop. + + if batch_count == 1: + history.append(last_image) + all_images.append(last_image) + + if batch_count > 1 and not state.skipped and not state.interrupted: + history.append(last_image) + all_images.append(last_image) + + p.inpainting_fill = original_inpainting_fill + + if state.interrupted or state.stopping_generation: + break + + if len(history) > 1: + grid = images.image_grid(history, rows=1) + if opts.grid_save: + images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + if opts.return_grid: + grids.append(grid) + + all_images = grids + all_images + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed diff --git a/scripts/loopback.py b/scripts/loopback.py new file mode 100644 index 0000000000000000000000000000000000000000..800ee882a16cb3e98afc3358bfc7e6302534804c --- /dev/null +++ b/scripts/loopback.py @@ -0,0 +1,140 @@ +import math + +import gradio as gr +import modules.scripts as scripts +from modules import deepbooru, images, processing, shared +from modules.processing import Processed +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "Loopback" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + loops = gr.Slider(minimum=1, maximum=32, step=1, label='Loops', value=4, elem_id=self.elem_id("loops")) + final_denoising_strength = gr.Slider(minimum=0, maximum=1, step=0.01, label='Final denoising strength', value=0.5, elem_id=self.elem_id("final_denoising_strength")) + denoising_curve = gr.Dropdown(label="Denoising strength curve", choices=["Aggressive", "Linear", "Lazy"], value="Linear") + append_interrogation = gr.Dropdown(label="Append interrogated prompt at each iteration", choices=["None", "CLIP", "DeepBooru"], value="None") + + return [loops, final_denoising_strength, denoising_curve, append_interrogation] + + def run(self, p, loops, final_denoising_strength, denoising_curve, append_interrogation): + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Final denoising strength": final_denoising_strength, + "Denoising curve": denoising_curve + } + + p.batch_size = 1 + p.n_iter = 1 + + info = None + initial_seed = None + initial_info = None + initial_denoising_strength = p.denoising_strength + + grids = [] + all_images = [] + original_init_image = p.init_images + original_prompt = p.prompt + original_inpainting_fill = p.inpainting_fill + state.job_count = loops * batch_count + + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + + def calculate_denoising_strength(loop): + strength = initial_denoising_strength + + if loops == 1: + return strength + + progress = loop / (loops - 1) + if denoising_curve == "Aggressive": + strength = math.sin((progress) * math.pi * 0.5) + elif denoising_curve == "Lazy": + strength = 1 - math.cos((progress) * math.pi * 0.5) + else: + strength = progress + + change = (final_denoising_strength - initial_denoising_strength) * strength + return initial_denoising_strength + change + + history = [] + + for n in range(batch_count): + # Reset to original init image at the start of each batch + p.init_images = original_init_image + + # Reset to original denoising strength + p.denoising_strength = initial_denoising_strength + + last_image = None + + for i in range(loops): + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if opts.img2img_color_correction: + p.color_corrections = initial_color_corrections + + if append_interrogation != "None": + p.prompt = f"{original_prompt}, " if original_prompt else "" + if append_interrogation == "CLIP": + p.prompt += shared.interrogator.interrogate(p.init_images[0]) + elif append_interrogation == "DeepBooru": + p.prompt += deepbooru.model.tag(p.init_images[0]) + + state.job = f"Iteration {i + 1}/{loops}, batch {n + 1}/{batch_count}" + + processed = processing.process_images(p) + + # Generation cancelled. + if state.interrupted or state.stopping_generation: + break + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + p.seed = processed.seed + 1 + p.denoising_strength = calculate_denoising_strength(i + 1) + + if state.skipped: + break + + last_image = processed.images[0] + p.init_images = [last_image] + p.inpainting_fill = 1 # Set "masked content" to "original" for next loop. + + if batch_count == 1: + history.append(last_image) + all_images.append(last_image) + + if batch_count > 1 and not state.skipped and not state.interrupted: + history.append(last_image) + all_images.append(last_image) + + p.inpainting_fill = original_inpainting_fill + + if state.interrupted or state.stopping_generation: + break + + if len(history) > 1: + grid = images.image_grid(history, rows=1) + if opts.grid_save: + images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + if opts.return_grid: + grids.append(grid) + + all_images = grids + all_images + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed diff --git a/scripts/loopback_for_chain.py b/scripts/loopback_for_chain.py new file mode 100644 index 0000000000000000000000000000000000000000..90f4533fd0b56d658de807d757e201c6483293c0 --- /dev/null +++ b/scripts/loopback_for_chain.py @@ -0,0 +1,144 @@ +import math + +import gradio as gr +import modules.scripts as scripts +from modules import deepbooru, images, processing, shared +from modules.processing import Processed +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "Loopback for chain" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + loops = gr.Slider(minimum=1, maximum=32, step=1, label='Loops', value=4, elem_id=self.elem_id("loops")) + final_denoising_strength = gr.Slider(minimum=0, maximum=1, step=0.01, label='Final denoising strength', value=0.5, elem_id=self.elem_id("final_denoising_strength")) + denoising_curve = gr.Dropdown(label="Denoising strength curve", choices=["Aggressive", "Linear", "Lazy"], value="Linear", elem_id=self.elem_id("denoising_curve")) + append_interrogation = gr.Dropdown(label="Append interrogated prompt at each iteration", choices=["None", "CLIP", "DeepBooru"], value="None", elem_id=self.elem_id("append_interrogation")) + + return [loops, final_denoising_strength, denoising_curve, append_interrogation] + + def run(self, p, loops, final_denoising_strength, denoising_curve, append_interrogation): + processing.fix_seed(p) + batch_count = p.n_iter + p.extra_generation_params = { + "Final denoising strength": final_denoising_strength, + "Denoising curve": denoising_curve + } + + p.batch_size = 1 + p.n_iter = 1 + + info = None + initial_seed = None + initial_info = None + initial_denoising_strength = p.denoising_strength + + grids = [] + all_images = [] + original_init_image = p.init_images + original_prompt = p.prompt + original_inpainting_fill = p.inpainting_fill + state.job_count = loops * batch_count + + initial_color_corrections = [processing.setup_color_correction(p.init_images[0])] + + def calculate_denoising_strength(loop): + strength = initial_denoising_strength + + if loops == 1: + return strength + + progress = loop / (loops - 1) + if denoising_curve == "Aggressive": + strength = math.sin((progress) * math.pi * 0.5) + elif denoising_curve == "Lazy": + strength = 1 - math.cos((progress) * math.pi * 0.5) + else: + strength = progress + + change = (final_denoising_strength - initial_denoising_strength) * strength + return initial_denoising_strength + change + + history = [] + + for n in range(batch_count): + # Reset to original init image at the start of each batch + p.init_images = original_init_image + + # Reset to original denoising strength + p.denoising_strength = initial_denoising_strength + + last_image = None + + for i in range(loops): + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + + if opts.img2img_color_correction: + p.color_corrections = initial_color_corrections + + if append_interrogation != "None": + p.prompt = f"{original_prompt}, " if original_prompt else "" + if append_interrogation == "CLIP": + p.prompt += shared.interrogator.interrogate(p.init_images[0]) + elif append_interrogation == "DeepBooru": + p.prompt += deepbooru.model.tag(p.init_images[0]) + + state.job = f"Iteration {i + 1}/{loops}, batch {n + 1}/{batch_count}" + + processed = processing.process_images(p) + + # Generation cancelled. + if state.interrupted: + break + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + p.seed = processed.seed + 1 + p.denoising_strength = calculate_denoising_strength(i + 1) + + if state.skipped: + break + + last_image = processed.images[0] + p.init_images = [last_image] + p.inpainting_fill = 1 # Set "masked content" to "original" for next loop. + + if batch_count == 1: + history.append(last_image) + all_images.append(last_image) + + if batch_count > 1 and not state.skipped and not state.interrupted: + history.append(last_image) + all_images.append(last_image) + + p.inpainting_fill = original_inpainting_fill + + if state.interrupted: + break + + # disable grid + # if len(history) > 1: + # grid = images.image_grid(history, rows=1) + # if opts.grid_save: + # images.save_image(grid, p.outpath_grids, "grid", initial_seed, p.prompt, opts.grid_format, info=info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + # if opts.return_grid: + # grids.append(grid) + + # all_images = grids + all_images + + if len(all_images) > 1: + all_images = [all_images.pop()] + + processed = Processed(p, all_images, initial_seed, initial_info) + + return processed diff --git a/scripts/outpainting_mk_2-Copy1.py b/scripts/outpainting_mk_2-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..5df9dff9c4897f5469917cb70635a688f7558432 --- /dev/null +++ b/scripts/outpainting_mk_2-Copy1.py @@ -0,0 +1,295 @@ +import math + +import numpy as np +import skimage + +import modules.scripts as scripts +import gradio as gr +from PIL import Image, ImageDraw + +from modules import images +from modules.processing import Processed, process_images +from modules.shared import opts, state + + +# this function is taken from https://github.com/parlance-zz/g-diffuser-bot +def get_matched_noise(_np_src_image, np_mask_rgb, noise_q=1, color_variation=0.05): + # helper fft routines that keep ortho normalization and auto-shift before and after fft + def _fft2(data): + if data.ndim > 2: # has channels + out_fft = np.zeros((data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128) + for c in range(data.shape[2]): + c_data = data[:, :, c] + out_fft[:, :, c] = np.fft.fft2(np.fft.fftshift(c_data), norm="ortho") + out_fft[:, :, c] = np.fft.ifftshift(out_fft[:, :, c]) + else: # one channel + out_fft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128) + out_fft[:, :] = np.fft.fft2(np.fft.fftshift(data), norm="ortho") + out_fft[:, :] = np.fft.ifftshift(out_fft[:, :]) + + return out_fft + + def _ifft2(data): + if data.ndim > 2: # has channels + out_ifft = np.zeros((data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128) + for c in range(data.shape[2]): + c_data = data[:, :, c] + out_ifft[:, :, c] = np.fft.ifft2(np.fft.fftshift(c_data), norm="ortho") + out_ifft[:, :, c] = np.fft.ifftshift(out_ifft[:, :, c]) + else: # one channel + out_ifft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128) + out_ifft[:, :] = np.fft.ifft2(np.fft.fftshift(data), norm="ortho") + out_ifft[:, :] = np.fft.ifftshift(out_ifft[:, :]) + + return out_ifft + + def _get_gaussian_window(width, height, std=3.14, mode=0): + window_scale_x = float(width / min(width, height)) + window_scale_y = float(height / min(width, height)) + + window = np.zeros((width, height)) + x = (np.arange(width) / width * 2. - 1.) * window_scale_x + for y in range(height): + fy = (y / height * 2. - 1.) * window_scale_y + if mode == 0: + window[:, y] = np.exp(-(x ** 2 + fy ** 2) * std) + else: + window[:, y] = (1 / ((x ** 2 + 1.) * (fy ** 2 + 1.))) ** (std / 3.14) # hey wait a minute that's not gaussian + + return window + + def _get_masked_window_rgb(np_mask_grey, hardness=1.): + np_mask_rgb = np.zeros((np_mask_grey.shape[0], np_mask_grey.shape[1], 3)) + if hardness != 1.: + hardened = np_mask_grey[:] ** hardness + else: + hardened = np_mask_grey[:] + for c in range(3): + np_mask_rgb[:, :, c] = hardened[:] + return np_mask_rgb + + width = _np_src_image.shape[0] + height = _np_src_image.shape[1] + num_channels = _np_src_image.shape[2] + + _np_src_image[:] * (1. - np_mask_rgb) + np_mask_grey = (np.sum(np_mask_rgb, axis=2) / 3.) + img_mask = np_mask_grey > 1e-6 + ref_mask = np_mask_grey < 1e-3 + + windowed_image = _np_src_image * (1. - _get_masked_window_rgb(np_mask_grey)) + windowed_image /= np.max(windowed_image) + windowed_image += np.average(_np_src_image) * np_mask_rgb # / (1.-np.average(np_mask_rgb)) # rather than leave the masked area black, we get better results from fft by filling the average unmasked color + + src_fft = _fft2(windowed_image) # get feature statistics from masked src img + src_dist = np.absolute(src_fft) + src_phase = src_fft / src_dist + + # create a generator with a static seed to make outpainting deterministic / only follow global seed + rng = np.random.default_rng(0) + + noise_window = _get_gaussian_window(width, height, mode=1) # start with simple gaussian noise + noise_rgb = rng.random((width, height, num_channels)) + noise_grey = (np.sum(noise_rgb, axis=2) / 3.) + noise_rgb *= color_variation # the colorfulness of the starting noise is blended to greyscale with a parameter + for c in range(num_channels): + noise_rgb[:, :, c] += (1. - color_variation) * noise_grey + + noise_fft = _fft2(noise_rgb) + for c in range(num_channels): + noise_fft[:, :, c] *= noise_window + noise_rgb = np.real(_ifft2(noise_fft)) + shaped_noise_fft = _fft2(noise_rgb) + shaped_noise_fft[:, :, :] = np.absolute(shaped_noise_fft[:, :, :]) ** 2 * (src_dist ** noise_q) * src_phase # perform the actual shaping + + brightness_variation = 0. # color_variation # todo: temporarily tying brightness variation to color variation for now + contrast_adjusted_np_src = _np_src_image[:] * (brightness_variation + 1.) - brightness_variation * 2. + + # scikit-image is used for histogram matching, very convenient! + shaped_noise = np.real(_ifft2(shaped_noise_fft)) + shaped_noise -= np.min(shaped_noise) + shaped_noise /= np.max(shaped_noise) + shaped_noise[img_mask, :] = skimage.exposure.match_histograms(shaped_noise[img_mask, :] ** 1., contrast_adjusted_np_src[ref_mask, :], channel_axis=1) + shaped_noise = _np_src_image[:] * (1. - np_mask_rgb) + shaped_noise * np_mask_rgb + + matched_noise = shaped_noise[:] + + return np.clip(matched_noise, 0., 1.) + + + +class Script(scripts.Script): + def title(self): + return "Outpainting mk2" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + if not is_img2img: + return None + + info = gr.HTML("

Recommended settings: Sampling Steps: 80-100, Sampler: Euler a, Denoising strength: 0.8

") + + pixels = gr.Slider(label="Pixels to expand", minimum=8, maximum=256, step=8, value=128, elem_id=self.elem_id("pixels")) + mask_blur = gr.Slider(label='Mask blur', minimum=0, maximum=64, step=1, value=8, elem_id=self.elem_id("mask_blur")) + direction = gr.CheckboxGroup(label="Outpainting direction", choices=['left', 'right', 'up', 'down'], value=['left', 'right', 'up', 'down'], elem_id=self.elem_id("direction")) + noise_q = gr.Slider(label="Fall-off exponent (lower=higher detail)", minimum=0.0, maximum=4.0, step=0.01, value=1.0, elem_id=self.elem_id("noise_q")) + color_variation = gr.Slider(label="Color variation", minimum=0.0, maximum=1.0, step=0.01, value=0.05, elem_id=self.elem_id("color_variation")) + + return [info, pixels, mask_blur, direction, noise_q, color_variation] + + def run(self, p, _, pixels, mask_blur, direction, noise_q, color_variation): + initial_seed_and_info = [None, None] + + process_width = p.width + process_height = p.height + + p.inpaint_full_res = False + p.inpainting_fill = 1 + p.do_not_save_samples = True + p.do_not_save_grid = True + + left = pixels if "left" in direction else 0 + right = pixels if "right" in direction else 0 + up = pixels if "up" in direction else 0 + down = pixels if "down" in direction else 0 + + if left > 0 or right > 0: + mask_blur_x = mask_blur + else: + mask_blur_x = 0 + + if up > 0 or down > 0: + mask_blur_y = mask_blur + else: + mask_blur_y = 0 + + p.mask_blur_x = mask_blur_x*4 + p.mask_blur_y = mask_blur_y*4 + + init_img = p.init_images[0] + target_w = math.ceil((init_img.width + left + right) / 64) * 64 + target_h = math.ceil((init_img.height + up + down) / 64) * 64 + + if left > 0: + left = left * (target_w - init_img.width) // (left + right) + + if right > 0: + right = target_w - init_img.width - left + + if up > 0: + up = up * (target_h - init_img.height) // (up + down) + + if down > 0: + down = target_h - init_img.height - up + + def expand(init, count, expand_pixels, is_left=False, is_right=False, is_top=False, is_bottom=False): + is_horiz = is_left or is_right + is_vert = is_top or is_bottom + pixels_horiz = expand_pixels if is_horiz else 0 + pixels_vert = expand_pixels if is_vert else 0 + + images_to_process = [] + output_images = [] + for n in range(count): + res_w = init[n].width + pixels_horiz + res_h = init[n].height + pixels_vert + process_res_w = math.ceil(res_w / 64) * 64 + process_res_h = math.ceil(res_h / 64) * 64 + + img = Image.new("RGB", (process_res_w, process_res_h)) + img.paste(init[n], (pixels_horiz if is_left else 0, pixels_vert if is_top else 0)) + mask = Image.new("RGB", (process_res_w, process_res_h), "white") + draw = ImageDraw.Draw(mask) + draw.rectangle(( + expand_pixels + mask_blur_x if is_left else 0, + expand_pixels + mask_blur_y if is_top else 0, + mask.width - expand_pixels - mask_blur_x if is_right else res_w, + mask.height - expand_pixels - mask_blur_y if is_bottom else res_h, + ), fill="black") + + np_image = (np.asarray(img) / 255.0).astype(np.float64) + np_mask = (np.asarray(mask) / 255.0).astype(np.float64) + noised = get_matched_noise(np_image, np_mask, noise_q, color_variation) + output_images.append(Image.fromarray(np.clip(noised * 255., 0., 255.).astype(np.uint8), mode="RGB")) + + target_width = min(process_width, init[n].width + pixels_horiz) if is_horiz else img.width + target_height = min(process_height, init[n].height + pixels_vert) if is_vert else img.height + p.width = target_width if is_horiz else img.width + p.height = target_height if is_vert else img.height + + crop_region = ( + 0 if is_left else output_images[n].width - target_width, + 0 if is_top else output_images[n].height - target_height, + target_width if is_left else output_images[n].width, + target_height if is_top else output_images[n].height, + ) + mask = mask.crop(crop_region) + p.image_mask = mask + + image_to_process = output_images[n].crop(crop_region) + images_to_process.append(image_to_process) + + p.init_images = images_to_process + + latent_mask = Image.new("RGB", (p.width, p.height), "white") + draw = ImageDraw.Draw(latent_mask) + draw.rectangle(( + expand_pixels + mask_blur_x * 2 if is_left else 0, + expand_pixels + mask_blur_y * 2 if is_top else 0, + mask.width - expand_pixels - mask_blur_x * 2 if is_right else res_w, + mask.height - expand_pixels - mask_blur_y * 2 if is_bottom else res_h, + ), fill="black") + p.latent_mask = latent_mask + + proc = process_images(p) + + if initial_seed_and_info[0] is None: + initial_seed_and_info[0] = proc.seed + initial_seed_and_info[1] = proc.info + + for n in range(count): + output_images[n].paste(proc.images[n], (0 if is_left else output_images[n].width - proc.images[n].width, 0 if is_top else output_images[n].height - proc.images[n].height)) + output_images[n] = output_images[n].crop((0, 0, res_w, res_h)) + + return output_images + + batch_count = p.n_iter + batch_size = p.batch_size + p.n_iter = 1 + state.job_count = batch_count * ((1 if left > 0 else 0) + (1 if right > 0 else 0) + (1 if up > 0 else 0) + (1 if down > 0 else 0)) + all_processed_images = [] + + for i in range(batch_count): + imgs = [init_img] * batch_size + state.job = f"Batch {i + 1} out of {batch_count}" + + if left > 0: + imgs = expand(imgs, batch_size, left, is_left=True) + if right > 0: + imgs = expand(imgs, batch_size, right, is_right=True) + if up > 0: + imgs = expand(imgs, batch_size, up, is_top=True) + if down > 0: + imgs = expand(imgs, batch_size, down, is_bottom=True) + + all_processed_images += imgs + + all_images = all_processed_images + + combined_grid_image = images.image_grid(all_processed_images) + unwanted_grid_because_of_img_count = len(all_processed_images) < 2 and opts.grid_only_if_multiple + if opts.return_grid and not unwanted_grid_because_of_img_count: + all_images = [combined_grid_image] + all_processed_images + + res = Processed(p, all_images, initial_seed_and_info[0], initial_seed_and_info[1]) + + if opts.samples_save: + for img in all_processed_images: + images.save_image(img, p.outpath_samples, "", res.seed, p.prompt, opts.samples_format, info=res.info, p=p) + + if opts.grid_save and not unwanted_grid_because_of_img_count: + images.save_image(combined_grid_image, p.outpath_grids, "grid", res.seed, p.prompt, opts.grid_format, info=res.info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + return res diff --git a/scripts/outpainting_mk_2.py b/scripts/outpainting_mk_2.py new file mode 100644 index 0000000000000000000000000000000000000000..5df9dff9c4897f5469917cb70635a688f7558432 --- /dev/null +++ b/scripts/outpainting_mk_2.py @@ -0,0 +1,295 @@ +import math + +import numpy as np +import skimage + +import modules.scripts as scripts +import gradio as gr +from PIL import Image, ImageDraw + +from modules import images +from modules.processing import Processed, process_images +from modules.shared import opts, state + + +# this function is taken from https://github.com/parlance-zz/g-diffuser-bot +def get_matched_noise(_np_src_image, np_mask_rgb, noise_q=1, color_variation=0.05): + # helper fft routines that keep ortho normalization and auto-shift before and after fft + def _fft2(data): + if data.ndim > 2: # has channels + out_fft = np.zeros((data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128) + for c in range(data.shape[2]): + c_data = data[:, :, c] + out_fft[:, :, c] = np.fft.fft2(np.fft.fftshift(c_data), norm="ortho") + out_fft[:, :, c] = np.fft.ifftshift(out_fft[:, :, c]) + else: # one channel + out_fft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128) + out_fft[:, :] = np.fft.fft2(np.fft.fftshift(data), norm="ortho") + out_fft[:, :] = np.fft.ifftshift(out_fft[:, :]) + + return out_fft + + def _ifft2(data): + if data.ndim > 2: # has channels + out_ifft = np.zeros((data.shape[0], data.shape[1], data.shape[2]), dtype=np.complex128) + for c in range(data.shape[2]): + c_data = data[:, :, c] + out_ifft[:, :, c] = np.fft.ifft2(np.fft.fftshift(c_data), norm="ortho") + out_ifft[:, :, c] = np.fft.ifftshift(out_ifft[:, :, c]) + else: # one channel + out_ifft = np.zeros((data.shape[0], data.shape[1]), dtype=np.complex128) + out_ifft[:, :] = np.fft.ifft2(np.fft.fftshift(data), norm="ortho") + out_ifft[:, :] = np.fft.ifftshift(out_ifft[:, :]) + + return out_ifft + + def _get_gaussian_window(width, height, std=3.14, mode=0): + window_scale_x = float(width / min(width, height)) + window_scale_y = float(height / min(width, height)) + + window = np.zeros((width, height)) + x = (np.arange(width) / width * 2. - 1.) * window_scale_x + for y in range(height): + fy = (y / height * 2. - 1.) * window_scale_y + if mode == 0: + window[:, y] = np.exp(-(x ** 2 + fy ** 2) * std) + else: + window[:, y] = (1 / ((x ** 2 + 1.) * (fy ** 2 + 1.))) ** (std / 3.14) # hey wait a minute that's not gaussian + + return window + + def _get_masked_window_rgb(np_mask_grey, hardness=1.): + np_mask_rgb = np.zeros((np_mask_grey.shape[0], np_mask_grey.shape[1], 3)) + if hardness != 1.: + hardened = np_mask_grey[:] ** hardness + else: + hardened = np_mask_grey[:] + for c in range(3): + np_mask_rgb[:, :, c] = hardened[:] + return np_mask_rgb + + width = _np_src_image.shape[0] + height = _np_src_image.shape[1] + num_channels = _np_src_image.shape[2] + + _np_src_image[:] * (1. - np_mask_rgb) + np_mask_grey = (np.sum(np_mask_rgb, axis=2) / 3.) + img_mask = np_mask_grey > 1e-6 + ref_mask = np_mask_grey < 1e-3 + + windowed_image = _np_src_image * (1. - _get_masked_window_rgb(np_mask_grey)) + windowed_image /= np.max(windowed_image) + windowed_image += np.average(_np_src_image) * np_mask_rgb # / (1.-np.average(np_mask_rgb)) # rather than leave the masked area black, we get better results from fft by filling the average unmasked color + + src_fft = _fft2(windowed_image) # get feature statistics from masked src img + src_dist = np.absolute(src_fft) + src_phase = src_fft / src_dist + + # create a generator with a static seed to make outpainting deterministic / only follow global seed + rng = np.random.default_rng(0) + + noise_window = _get_gaussian_window(width, height, mode=1) # start with simple gaussian noise + noise_rgb = rng.random((width, height, num_channels)) + noise_grey = (np.sum(noise_rgb, axis=2) / 3.) + noise_rgb *= color_variation # the colorfulness of the starting noise is blended to greyscale with a parameter + for c in range(num_channels): + noise_rgb[:, :, c] += (1. - color_variation) * noise_grey + + noise_fft = _fft2(noise_rgb) + for c in range(num_channels): + noise_fft[:, :, c] *= noise_window + noise_rgb = np.real(_ifft2(noise_fft)) + shaped_noise_fft = _fft2(noise_rgb) + shaped_noise_fft[:, :, :] = np.absolute(shaped_noise_fft[:, :, :]) ** 2 * (src_dist ** noise_q) * src_phase # perform the actual shaping + + brightness_variation = 0. # color_variation # todo: temporarily tying brightness variation to color variation for now + contrast_adjusted_np_src = _np_src_image[:] * (brightness_variation + 1.) - brightness_variation * 2. + + # scikit-image is used for histogram matching, very convenient! + shaped_noise = np.real(_ifft2(shaped_noise_fft)) + shaped_noise -= np.min(shaped_noise) + shaped_noise /= np.max(shaped_noise) + shaped_noise[img_mask, :] = skimage.exposure.match_histograms(shaped_noise[img_mask, :] ** 1., contrast_adjusted_np_src[ref_mask, :], channel_axis=1) + shaped_noise = _np_src_image[:] * (1. - np_mask_rgb) + shaped_noise * np_mask_rgb + + matched_noise = shaped_noise[:] + + return np.clip(matched_noise, 0., 1.) + + + +class Script(scripts.Script): + def title(self): + return "Outpainting mk2" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + if not is_img2img: + return None + + info = gr.HTML("

Recommended settings: Sampling Steps: 80-100, Sampler: Euler a, Denoising strength: 0.8

") + + pixels = gr.Slider(label="Pixels to expand", minimum=8, maximum=256, step=8, value=128, elem_id=self.elem_id("pixels")) + mask_blur = gr.Slider(label='Mask blur', minimum=0, maximum=64, step=1, value=8, elem_id=self.elem_id("mask_blur")) + direction = gr.CheckboxGroup(label="Outpainting direction", choices=['left', 'right', 'up', 'down'], value=['left', 'right', 'up', 'down'], elem_id=self.elem_id("direction")) + noise_q = gr.Slider(label="Fall-off exponent (lower=higher detail)", minimum=0.0, maximum=4.0, step=0.01, value=1.0, elem_id=self.elem_id("noise_q")) + color_variation = gr.Slider(label="Color variation", minimum=0.0, maximum=1.0, step=0.01, value=0.05, elem_id=self.elem_id("color_variation")) + + return [info, pixels, mask_blur, direction, noise_q, color_variation] + + def run(self, p, _, pixels, mask_blur, direction, noise_q, color_variation): + initial_seed_and_info = [None, None] + + process_width = p.width + process_height = p.height + + p.inpaint_full_res = False + p.inpainting_fill = 1 + p.do_not_save_samples = True + p.do_not_save_grid = True + + left = pixels if "left" in direction else 0 + right = pixels if "right" in direction else 0 + up = pixels if "up" in direction else 0 + down = pixels if "down" in direction else 0 + + if left > 0 or right > 0: + mask_blur_x = mask_blur + else: + mask_blur_x = 0 + + if up > 0 or down > 0: + mask_blur_y = mask_blur + else: + mask_blur_y = 0 + + p.mask_blur_x = mask_blur_x*4 + p.mask_blur_y = mask_blur_y*4 + + init_img = p.init_images[0] + target_w = math.ceil((init_img.width + left + right) / 64) * 64 + target_h = math.ceil((init_img.height + up + down) / 64) * 64 + + if left > 0: + left = left * (target_w - init_img.width) // (left + right) + + if right > 0: + right = target_w - init_img.width - left + + if up > 0: + up = up * (target_h - init_img.height) // (up + down) + + if down > 0: + down = target_h - init_img.height - up + + def expand(init, count, expand_pixels, is_left=False, is_right=False, is_top=False, is_bottom=False): + is_horiz = is_left or is_right + is_vert = is_top or is_bottom + pixels_horiz = expand_pixels if is_horiz else 0 + pixels_vert = expand_pixels if is_vert else 0 + + images_to_process = [] + output_images = [] + for n in range(count): + res_w = init[n].width + pixels_horiz + res_h = init[n].height + pixels_vert + process_res_w = math.ceil(res_w / 64) * 64 + process_res_h = math.ceil(res_h / 64) * 64 + + img = Image.new("RGB", (process_res_w, process_res_h)) + img.paste(init[n], (pixels_horiz if is_left else 0, pixels_vert if is_top else 0)) + mask = Image.new("RGB", (process_res_w, process_res_h), "white") + draw = ImageDraw.Draw(mask) + draw.rectangle(( + expand_pixels + mask_blur_x if is_left else 0, + expand_pixels + mask_blur_y if is_top else 0, + mask.width - expand_pixels - mask_blur_x if is_right else res_w, + mask.height - expand_pixels - mask_blur_y if is_bottom else res_h, + ), fill="black") + + np_image = (np.asarray(img) / 255.0).astype(np.float64) + np_mask = (np.asarray(mask) / 255.0).astype(np.float64) + noised = get_matched_noise(np_image, np_mask, noise_q, color_variation) + output_images.append(Image.fromarray(np.clip(noised * 255., 0., 255.).astype(np.uint8), mode="RGB")) + + target_width = min(process_width, init[n].width + pixels_horiz) if is_horiz else img.width + target_height = min(process_height, init[n].height + pixels_vert) if is_vert else img.height + p.width = target_width if is_horiz else img.width + p.height = target_height if is_vert else img.height + + crop_region = ( + 0 if is_left else output_images[n].width - target_width, + 0 if is_top else output_images[n].height - target_height, + target_width if is_left else output_images[n].width, + target_height if is_top else output_images[n].height, + ) + mask = mask.crop(crop_region) + p.image_mask = mask + + image_to_process = output_images[n].crop(crop_region) + images_to_process.append(image_to_process) + + p.init_images = images_to_process + + latent_mask = Image.new("RGB", (p.width, p.height), "white") + draw = ImageDraw.Draw(latent_mask) + draw.rectangle(( + expand_pixels + mask_blur_x * 2 if is_left else 0, + expand_pixels + mask_blur_y * 2 if is_top else 0, + mask.width - expand_pixels - mask_blur_x * 2 if is_right else res_w, + mask.height - expand_pixels - mask_blur_y * 2 if is_bottom else res_h, + ), fill="black") + p.latent_mask = latent_mask + + proc = process_images(p) + + if initial_seed_and_info[0] is None: + initial_seed_and_info[0] = proc.seed + initial_seed_and_info[1] = proc.info + + for n in range(count): + output_images[n].paste(proc.images[n], (0 if is_left else output_images[n].width - proc.images[n].width, 0 if is_top else output_images[n].height - proc.images[n].height)) + output_images[n] = output_images[n].crop((0, 0, res_w, res_h)) + + return output_images + + batch_count = p.n_iter + batch_size = p.batch_size + p.n_iter = 1 + state.job_count = batch_count * ((1 if left > 0 else 0) + (1 if right > 0 else 0) + (1 if up > 0 else 0) + (1 if down > 0 else 0)) + all_processed_images = [] + + for i in range(batch_count): + imgs = [init_img] * batch_size + state.job = f"Batch {i + 1} out of {batch_count}" + + if left > 0: + imgs = expand(imgs, batch_size, left, is_left=True) + if right > 0: + imgs = expand(imgs, batch_size, right, is_right=True) + if up > 0: + imgs = expand(imgs, batch_size, up, is_top=True) + if down > 0: + imgs = expand(imgs, batch_size, down, is_bottom=True) + + all_processed_images += imgs + + all_images = all_processed_images + + combined_grid_image = images.image_grid(all_processed_images) + unwanted_grid_because_of_img_count = len(all_processed_images) < 2 and opts.grid_only_if_multiple + if opts.return_grid and not unwanted_grid_because_of_img_count: + all_images = [combined_grid_image] + all_processed_images + + res = Processed(p, all_images, initial_seed_and_info[0], initial_seed_and_info[1]) + + if opts.samples_save: + for img in all_processed_images: + images.save_image(img, p.outpath_samples, "", res.seed, p.prompt, opts.samples_format, info=res.info, p=p) + + if opts.grid_save and not unwanted_grid_because_of_img_count: + images.save_image(combined_grid_image, p.outpath_grids, "grid", res.seed, p.prompt, opts.grid_format, info=res.info, short_filename=not opts.grid_extended_filename, grid=True, p=p) + + return res diff --git a/scripts/poor_mans_outpainting-Copy1.py b/scripts/poor_mans_outpainting-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..ea0632b68c1d8fc8ecc4109814fe1305d0101d3d --- /dev/null +++ b/scripts/poor_mans_outpainting-Copy1.py @@ -0,0 +1,146 @@ +import math + +import modules.scripts as scripts +import gradio as gr +from PIL import Image, ImageDraw + +from modules import images, devices +from modules.processing import Processed, process_images +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "Poor man's outpainting" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + if not is_img2img: + return None + + pixels = gr.Slider(label="Pixels to expand", minimum=8, maximum=256, step=8, value=128, elem_id=self.elem_id("pixels")) + mask_blur = gr.Slider(label='Mask blur', minimum=0, maximum=64, step=1, value=4, elem_id=self.elem_id("mask_blur")) + inpainting_fill = gr.Radio(label='Masked content', choices=['fill', 'original', 'latent noise', 'latent nothing'], value='fill', type="index", elem_id=self.elem_id("inpainting_fill")) + direction = gr.CheckboxGroup(label="Outpainting direction", choices=['left', 'right', 'up', 'down'], value=['left', 'right', 'up', 'down'], elem_id=self.elem_id("direction")) + + return [pixels, mask_blur, inpainting_fill, direction] + + def run(self, p, pixels, mask_blur, inpainting_fill, direction): + initial_seed = None + initial_info = None + + p.mask_blur = mask_blur * 2 + p.inpainting_fill = inpainting_fill + p.inpaint_full_res = False + + left = pixels if "left" in direction else 0 + right = pixels if "right" in direction else 0 + up = pixels if "up" in direction else 0 + down = pixels if "down" in direction else 0 + + init_img = p.init_images[0] + target_w = math.ceil((init_img.width + left + right) / 64) * 64 + target_h = math.ceil((init_img.height + up + down) / 64) * 64 + + if left > 0: + left = left * (target_w - init_img.width) // (left + right) + if right > 0: + right = target_w - init_img.width - left + + if up > 0: + up = up * (target_h - init_img.height) // (up + down) + + if down > 0: + down = target_h - init_img.height - up + + img = Image.new("RGB", (target_w, target_h)) + img.paste(init_img, (left, up)) + + mask = Image.new("L", (img.width, img.height), "white") + draw = ImageDraw.Draw(mask) + draw.rectangle(( + left + (mask_blur * 2 if left > 0 else 0), + up + (mask_blur * 2 if up > 0 else 0), + mask.width - right - (mask_blur * 2 if right > 0 else 0), + mask.height - down - (mask_blur * 2 if down > 0 else 0) + ), fill="black") + + latent_mask = Image.new("L", (img.width, img.height), "white") + latent_draw = ImageDraw.Draw(latent_mask) + latent_draw.rectangle(( + left + (mask_blur//2 if left > 0 else 0), + up + (mask_blur//2 if up > 0 else 0), + mask.width - right - (mask_blur//2 if right > 0 else 0), + mask.height - down - (mask_blur//2 if down > 0 else 0) + ), fill="black") + + devices.torch_gc() + + grid = images.split_grid(img, tile_w=p.width, tile_h=p.height, overlap=pixels) + grid_mask = images.split_grid(mask, tile_w=p.width, tile_h=p.height, overlap=pixels) + grid_latent_mask = images.split_grid(latent_mask, tile_w=p.width, tile_h=p.height, overlap=pixels) + + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + p.do_not_save_samples = True + + work = [] + work_mask = [] + work_latent_mask = [] + work_results = [] + + for (y, h, row), (_, _, row_mask), (_, _, row_latent_mask) in zip(grid.tiles, grid_mask.tiles, grid_latent_mask.tiles): + for tiledata, tiledata_mask, tiledata_latent_mask in zip(row, row_mask, row_latent_mask): + x, w = tiledata[0:2] + + if x >= left and x+w <= img.width - right and y >= up and y+h <= img.height - down: + continue + + work.append(tiledata[2]) + work_mask.append(tiledata_mask[2]) + work_latent_mask.append(tiledata_latent_mask[2]) + + batch_count = len(work) + print(f"Poor man's outpainting will process a total of {len(work)} images tiled as {len(grid.tiles[0][2])}x{len(grid.tiles)}.") + + state.job_count = batch_count + + for i in range(batch_count): + p.init_images = [work[i]] + p.image_mask = work_mask[i] + p.latent_mask = work_latent_mask[i] + + state.job = f"Batch {i + 1} out of {batch_count}" + processed = process_images(p) + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + p.seed = processed.seed + 1 + work_results += processed.images + + + image_index = 0 + for y, h, row in grid.tiles: + for tiledata in row: + x, w = tiledata[0:2] + + if x >= left and x+w <= img.width - right and y >= up and y+h <= img.height - down: + continue + + tiledata[2] = work_results[image_index] if image_index < len(work_results) else Image.new("RGB", (p.width, p.height)) + image_index += 1 + + combined_image = images.combine_grid(grid) + + if opts.samples_save: + images.save_image(combined_image, p.outpath_samples, "", initial_seed, p.prompt, opts.samples_format, info=initial_info, p=p) + + processed = Processed(p, [combined_image], initial_seed, initial_info) + + return processed + diff --git a/scripts/poor_mans_outpainting.py b/scripts/poor_mans_outpainting.py new file mode 100644 index 0000000000000000000000000000000000000000..ea0632b68c1d8fc8ecc4109814fe1305d0101d3d --- /dev/null +++ b/scripts/poor_mans_outpainting.py @@ -0,0 +1,146 @@ +import math + +import modules.scripts as scripts +import gradio as gr +from PIL import Image, ImageDraw + +from modules import images, devices +from modules.processing import Processed, process_images +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "Poor man's outpainting" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + if not is_img2img: + return None + + pixels = gr.Slider(label="Pixels to expand", minimum=8, maximum=256, step=8, value=128, elem_id=self.elem_id("pixels")) + mask_blur = gr.Slider(label='Mask blur', minimum=0, maximum=64, step=1, value=4, elem_id=self.elem_id("mask_blur")) + inpainting_fill = gr.Radio(label='Masked content', choices=['fill', 'original', 'latent noise', 'latent nothing'], value='fill', type="index", elem_id=self.elem_id("inpainting_fill")) + direction = gr.CheckboxGroup(label="Outpainting direction", choices=['left', 'right', 'up', 'down'], value=['left', 'right', 'up', 'down'], elem_id=self.elem_id("direction")) + + return [pixels, mask_blur, inpainting_fill, direction] + + def run(self, p, pixels, mask_blur, inpainting_fill, direction): + initial_seed = None + initial_info = None + + p.mask_blur = mask_blur * 2 + p.inpainting_fill = inpainting_fill + p.inpaint_full_res = False + + left = pixels if "left" in direction else 0 + right = pixels if "right" in direction else 0 + up = pixels if "up" in direction else 0 + down = pixels if "down" in direction else 0 + + init_img = p.init_images[0] + target_w = math.ceil((init_img.width + left + right) / 64) * 64 + target_h = math.ceil((init_img.height + up + down) / 64) * 64 + + if left > 0: + left = left * (target_w - init_img.width) // (left + right) + if right > 0: + right = target_w - init_img.width - left + + if up > 0: + up = up * (target_h - init_img.height) // (up + down) + + if down > 0: + down = target_h - init_img.height - up + + img = Image.new("RGB", (target_w, target_h)) + img.paste(init_img, (left, up)) + + mask = Image.new("L", (img.width, img.height), "white") + draw = ImageDraw.Draw(mask) + draw.rectangle(( + left + (mask_blur * 2 if left > 0 else 0), + up + (mask_blur * 2 if up > 0 else 0), + mask.width - right - (mask_blur * 2 if right > 0 else 0), + mask.height - down - (mask_blur * 2 if down > 0 else 0) + ), fill="black") + + latent_mask = Image.new("L", (img.width, img.height), "white") + latent_draw = ImageDraw.Draw(latent_mask) + latent_draw.rectangle(( + left + (mask_blur//2 if left > 0 else 0), + up + (mask_blur//2 if up > 0 else 0), + mask.width - right - (mask_blur//2 if right > 0 else 0), + mask.height - down - (mask_blur//2 if down > 0 else 0) + ), fill="black") + + devices.torch_gc() + + grid = images.split_grid(img, tile_w=p.width, tile_h=p.height, overlap=pixels) + grid_mask = images.split_grid(mask, tile_w=p.width, tile_h=p.height, overlap=pixels) + grid_latent_mask = images.split_grid(latent_mask, tile_w=p.width, tile_h=p.height, overlap=pixels) + + p.n_iter = 1 + p.batch_size = 1 + p.do_not_save_grid = True + p.do_not_save_samples = True + + work = [] + work_mask = [] + work_latent_mask = [] + work_results = [] + + for (y, h, row), (_, _, row_mask), (_, _, row_latent_mask) in zip(grid.tiles, grid_mask.tiles, grid_latent_mask.tiles): + for tiledata, tiledata_mask, tiledata_latent_mask in zip(row, row_mask, row_latent_mask): + x, w = tiledata[0:2] + + if x >= left and x+w <= img.width - right and y >= up and y+h <= img.height - down: + continue + + work.append(tiledata[2]) + work_mask.append(tiledata_mask[2]) + work_latent_mask.append(tiledata_latent_mask[2]) + + batch_count = len(work) + print(f"Poor man's outpainting will process a total of {len(work)} images tiled as {len(grid.tiles[0][2])}x{len(grid.tiles)}.") + + state.job_count = batch_count + + for i in range(batch_count): + p.init_images = [work[i]] + p.image_mask = work_mask[i] + p.latent_mask = work_latent_mask[i] + + state.job = f"Batch {i + 1} out of {batch_count}" + processed = process_images(p) + + if initial_seed is None: + initial_seed = processed.seed + initial_info = processed.info + + p.seed = processed.seed + 1 + work_results += processed.images + + + image_index = 0 + for y, h, row in grid.tiles: + for tiledata in row: + x, w = tiledata[0:2] + + if x >= left and x+w <= img.width - right and y >= up and y+h <= img.height - down: + continue + + tiledata[2] = work_results[image_index] if image_index < len(work_results) else Image.new("RGB", (p.width, p.height)) + image_index += 1 + + combined_image = images.combine_grid(grid) + + if opts.samples_save: + images.save_image(combined_image, p.outpath_samples, "", initial_seed, p.prompt, opts.samples_format, info=initial_info, p=p) + + processed = Processed(p, [combined_image], initial_seed, initial_info) + + return processed + diff --git a/scripts/postprocessing_codeformer-Copy1.py b/scripts/postprocessing_codeformer-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..53a0cc44cdeb3b252d632367d8745e649bfabfa4 --- /dev/null +++ b/scripts/postprocessing_codeformer-Copy1.py @@ -0,0 +1,36 @@ +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, codeformer_model, ui_components +import gradio as gr + + +class ScriptPostprocessingCodeFormer(scripts_postprocessing.ScriptPostprocessing): + name = "CodeFormer" + order = 3000 + + def ui(self): + with ui_components.InputAccordion(False, label="CodeFormer") as enable: + with gr.Row(): + codeformer_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_codeformer_visibility") + codeformer_weight = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Weight (0 = maximum effect, 1 = minimum effect)", value=0, elem_id="extras_codeformer_weight") + + return { + "enable": enable, + "codeformer_visibility": codeformer_visibility, + "codeformer_weight": codeformer_weight, + } + + def process(self, pp: scripts_postprocessing.PostprocessedImage, enable, codeformer_visibility, codeformer_weight): + if codeformer_visibility == 0 or not enable: + return + + restored_img = codeformer_model.codeformer.restore(np.array(pp.image.convert("RGB"), dtype=np.uint8), w=codeformer_weight) + res = Image.fromarray(restored_img) + + if codeformer_visibility < 1.0: + res = Image.blend(pp.image, res, codeformer_visibility) + + pp.image = res + pp.info["CodeFormer visibility"] = round(codeformer_visibility, 3) + pp.info["CodeFormer weight"] = round(codeformer_weight, 3) diff --git a/scripts/postprocessing_codeformer.py b/scripts/postprocessing_codeformer.py new file mode 100644 index 0000000000000000000000000000000000000000..53a0cc44cdeb3b252d632367d8745e649bfabfa4 --- /dev/null +++ b/scripts/postprocessing_codeformer.py @@ -0,0 +1,36 @@ +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, codeformer_model, ui_components +import gradio as gr + + +class ScriptPostprocessingCodeFormer(scripts_postprocessing.ScriptPostprocessing): + name = "CodeFormer" + order = 3000 + + def ui(self): + with ui_components.InputAccordion(False, label="CodeFormer") as enable: + with gr.Row(): + codeformer_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_codeformer_visibility") + codeformer_weight = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Weight (0 = maximum effect, 1 = minimum effect)", value=0, elem_id="extras_codeformer_weight") + + return { + "enable": enable, + "codeformer_visibility": codeformer_visibility, + "codeformer_weight": codeformer_weight, + } + + def process(self, pp: scripts_postprocessing.PostprocessedImage, enable, codeformer_visibility, codeformer_weight): + if codeformer_visibility == 0 or not enable: + return + + restored_img = codeformer_model.codeformer.restore(np.array(pp.image.convert("RGB"), dtype=np.uint8), w=codeformer_weight) + res = Image.fromarray(restored_img) + + if codeformer_visibility < 1.0: + res = Image.blend(pp.image, res, codeformer_visibility) + + pp.image = res + pp.info["CodeFormer visibility"] = round(codeformer_visibility, 3) + pp.info["CodeFormer weight"] = round(codeformer_weight, 3) diff --git a/scripts/postprocessing_gfpgan-Copy1.py b/scripts/postprocessing_gfpgan-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..57e3623995c2b4be2ed1d1ee40c27528df826a14 --- /dev/null +++ b/scripts/postprocessing_gfpgan-Copy1.py @@ -0,0 +1,32 @@ +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, gfpgan_model, ui_components +import gradio as gr + + +class ScriptPostprocessingGfpGan(scripts_postprocessing.ScriptPostprocessing): + name = "GFPGAN" + order = 2000 + + def ui(self): + with ui_components.InputAccordion(False, label="GFPGAN") as enable: + gfpgan_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_gfpgan_visibility") + + return { + "enable": enable, + "gfpgan_visibility": gfpgan_visibility, + } + + def process(self, pp: scripts_postprocessing.PostprocessedImage, enable, gfpgan_visibility): + if gfpgan_visibility == 0 or not enable: + return + + restored_img = gfpgan_model.gfpgan_fix_faces(np.array(pp.image.convert("RGB"), dtype=np.uint8)) + res = Image.fromarray(restored_img) + + if gfpgan_visibility < 1.0: + res = Image.blend(pp.image, res, gfpgan_visibility) + + pp.image = res + pp.info["GFPGAN visibility"] = round(gfpgan_visibility, 3) diff --git a/scripts/postprocessing_gfpgan.py b/scripts/postprocessing_gfpgan.py new file mode 100644 index 0000000000000000000000000000000000000000..57e3623995c2b4be2ed1d1ee40c27528df826a14 --- /dev/null +++ b/scripts/postprocessing_gfpgan.py @@ -0,0 +1,32 @@ +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, gfpgan_model, ui_components +import gradio as gr + + +class ScriptPostprocessingGfpGan(scripts_postprocessing.ScriptPostprocessing): + name = "GFPGAN" + order = 2000 + + def ui(self): + with ui_components.InputAccordion(False, label="GFPGAN") as enable: + gfpgan_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Visibility", value=1.0, elem_id="extras_gfpgan_visibility") + + return { + "enable": enable, + "gfpgan_visibility": gfpgan_visibility, + } + + def process(self, pp: scripts_postprocessing.PostprocessedImage, enable, gfpgan_visibility): + if gfpgan_visibility == 0 or not enable: + return + + restored_img = gfpgan_model.gfpgan_fix_faces(np.array(pp.image.convert("RGB"), dtype=np.uint8)) + res = Image.fromarray(restored_img) + + if gfpgan_visibility < 1.0: + res = Image.blend(pp.image, res, gfpgan_visibility) + + pp.image = res + pp.info["GFPGAN visibility"] = round(gfpgan_visibility, 3) diff --git a/scripts/postprocessing_upscale-Copy1.py b/scripts/postprocessing_upscale-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..2409fd2073e86cc78b1376d8a9d374f9750ce7e7 --- /dev/null +++ b/scripts/postprocessing_upscale-Copy1.py @@ -0,0 +1,195 @@ +import re + +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, shared +import gradio as gr + +from modules.ui_components import FormRow, ToolButton, InputAccordion +from modules.ui import switch_values_symbol + +upscale_cache = {} + + +def limit_size_by_one_dimention(w, h, limit): + if h > w and h > limit: + w = limit * w // h + h = limit + elif w > limit: + h = limit * h // w + w = limit + + return int(w), int(h) + + +class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): + name = "Upscale" + order = 1000 + + def ui(self): + selected_tab = gr.Number(value=0, visible=False) + + with InputAccordion(True, label="Upscale", elem_id="extras_upscale") as upscale_enabled: + with FormRow(): + extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + + with FormRow(): + extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + + with FormRow(): + with gr.Tabs(elem_id="extras_resize_mode"): + with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: + with gr.Row(): + with gr.Column(scale=4): + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + with gr.Column(scale=1, min_width=160): + max_side_length = gr.Number(label="Max side length", value=0, elem_id="extras_upscale_max_side_length", tooltip="If any of two sides of the image ends up larger than specified, will downscale it to fit. 0 = no limit.", min_width=160, step=8, minimum=0) + + with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: + with FormRow(): + with gr.Column(elem_id="upscaling_column_size", scale=4): + upscaling_resize_w = gr.Slider(minimum=64, maximum=8192, step=8, label="Width", value=512, elem_id="extras_upscaling_resize_w") + upscaling_resize_h = gr.Slider(minimum=64, maximum=8192, step=8, label="Height", value=512, elem_id="extras_upscaling_resize_h") + with gr.Column(elem_id="upscaling_dimensions_row", scale=1, elem_classes="dimensions-tools"): + upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn", tooltip="Switch width/height") + upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") + + def on_selected_upscale_method(upscale_method): + if not shared.opts.set_scale_by_when_changing_upscaler: + return gr.update() + + match = re.search(r'(\d)[xX]|[xX](\d)', upscale_method) + if not match: + return gr.update() + + return gr.update(value=int(match.group(1) or match.group(2))) + + upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False) + tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab]) + tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab]) + + extras_upscaler_1.change(on_selected_upscale_method, inputs=[extras_upscaler_1], outputs=[upscaling_resize], show_progress="hidden") + + return { + "upscale_enabled": upscale_enabled, + "upscale_mode": selected_tab, + "upscale_by": upscaling_resize, + "max_side_length": max_side_length, + "upscale_to_width": upscaling_resize_w, + "upscale_to_height": upscaling_resize_h, + "upscale_crop": upscaling_crop, + "upscaler_1_name": extras_upscaler_1, + "upscaler_2_name": extras_upscaler_2, + "upscaler_2_visibility": extras_upscaler_2_visibility, + } + + def upscale(self, image, info, upscaler, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop): + if upscale_mode == 1: + upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) + info["Postprocess upscale to"] = f"{upscale_to_width}x{upscale_to_height}" + else: + info["Postprocess upscale by"] = upscale_by + if max_side_length != 0 and max(*image.size)*upscale_by > max_side_length: + upscale_mode = 1 + upscale_crop = False + upscale_to_width, upscale_to_height = limit_size_by_one_dimention(image.width*upscale_by, image.height*upscale_by, max_side_length) + upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) + info["Max side length"] = max_side_length + + cache_key = (hash(np.array(image.getdata()).tobytes()), upscaler.name, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop) + cached_image = upscale_cache.pop(cache_key, None) + + if cached_image is not None: + image = cached_image + else: + image = upscaler.scaler.upscale(image, upscale_by, upscaler.data_path) + + upscale_cache[cache_key] = image + if len(upscale_cache) > shared.opts.upscaling_max_images_in_cache: + upscale_cache.pop(next(iter(upscale_cache), None), None) + + if upscale_mode == 1 and upscale_crop: + cropped = Image.new("RGB", (upscale_to_width, upscale_to_height)) + cropped.paste(image, box=(upscale_to_width // 2 - image.width // 2, upscale_to_height // 2 - image.height // 2)) + image = cropped + info["Postprocess crop to"] = f"{image.width}x{image.height}" + + return image + + def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + if upscale_mode == 1: + pp.shared.target_width = upscale_to_width + pp.shared.target_height = upscale_to_height + else: + pp.shared.target_width = int(pp.image.width * upscale_by) + pp.shared.target_height = int(pp.image.height * upscale_by) + + pp.shared.target_width, pp.shared.target_height = limit_size_by_one_dimention(pp.shared.target_width, pp.shared.target_height, max_side_length) + + def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + if not upscale_enabled: + return + + upscaler_1_name = upscaler_1_name + if upscaler_1_name == "None": + upscaler_1_name = None + + upscaler1 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_1_name]), None) + assert upscaler1 or (upscaler_1_name is None), f'could not find upscaler named {upscaler_1_name}' + + if not upscaler1: + return + + upscaler_2_name = upscaler_2_name + if upscaler_2_name == "None": + upscaler_2_name = None + + upscaler2 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_2_name and x.name != "None"]), None) + assert upscaler2 or (upscaler_2_name is None), f'could not find upscaler named {upscaler_2_name}' + + upscaled_image = self.upscale(pp.image, pp.info, upscaler1, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) + pp.info["Postprocess upscaler"] = upscaler1.name + + if upscaler2 and upscaler_2_visibility > 0: + second_upscale = self.upscale(pp.image, pp.info, upscaler2, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) + if upscaled_image.mode != second_upscale.mode: + second_upscale = second_upscale.convert(upscaled_image.mode) + upscaled_image = Image.blend(upscaled_image, second_upscale, upscaler_2_visibility) + + pp.info["Postprocess upscaler 2"] = upscaler2.name + + pp.image = upscaled_image + + def image_changed(self): + upscale_cache.clear() + + +class ScriptPostprocessingUpscaleSimple(ScriptPostprocessingUpscale): + name = "Simple Upscale" + order = 900 + + def ui(self): + with FormRow(): + upscaler_name = gr.Dropdown(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + upscale_by = gr.Slider(minimum=0.05, maximum=8.0, step=0.05, label="Upscale by", value=2) + + return { + "upscale_by": upscale_by, + "upscaler_name": upscaler_name, + } + + def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_by=2.0, upscaler_name=None): + pp.shared.target_width = int(pp.image.width * upscale_by) + pp.shared.target_height = int(pp.image.height * upscale_by) + + def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_by=2.0, upscaler_name=None): + if upscaler_name is None or upscaler_name == "None": + return + + upscaler1 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_name]), None) + assert upscaler1, f'could not find upscaler named {upscaler_name}' + + pp.image = self.upscale(pp.image, pp.info, upscaler1, 0, upscale_by, 0, 0, 0, False) + pp.info["Postprocess upscaler"] = upscaler1.name diff --git a/scripts/postprocessing_upscale.py b/scripts/postprocessing_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..2409fd2073e86cc78b1376d8a9d374f9750ce7e7 --- /dev/null +++ b/scripts/postprocessing_upscale.py @@ -0,0 +1,195 @@ +import re + +from PIL import Image +import numpy as np + +from modules import scripts_postprocessing, shared +import gradio as gr + +from modules.ui_components import FormRow, ToolButton, InputAccordion +from modules.ui import switch_values_symbol + +upscale_cache = {} + + +def limit_size_by_one_dimention(w, h, limit): + if h > w and h > limit: + w = limit * w // h + h = limit + elif w > limit: + h = limit * h // w + w = limit + + return int(w), int(h) + + +class ScriptPostprocessingUpscale(scripts_postprocessing.ScriptPostprocessing): + name = "Upscale" + order = 1000 + + def ui(self): + selected_tab = gr.Number(value=0, visible=False) + + with InputAccordion(True, label="Upscale", elem_id="extras_upscale") as upscale_enabled: + with FormRow(): + extras_upscaler_1 = gr.Dropdown(label='Upscaler 1', elem_id="extras_upscaler_1", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + + with FormRow(): + extras_upscaler_2 = gr.Dropdown(label='Upscaler 2', elem_id="extras_upscaler_2", choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + extras_upscaler_2_visibility = gr.Slider(minimum=0.0, maximum=1.0, step=0.001, label="Upscaler 2 visibility", value=0.0, elem_id="extras_upscaler_2_visibility") + + with FormRow(): + with gr.Tabs(elem_id="extras_resize_mode"): + with gr.TabItem('Scale by', elem_id="extras_scale_by_tab") as tab_scale_by: + with gr.Row(): + with gr.Column(scale=4): + upscaling_resize = gr.Slider(minimum=1.0, maximum=8.0, step=0.05, label="Resize", value=4, elem_id="extras_upscaling_resize") + with gr.Column(scale=1, min_width=160): + max_side_length = gr.Number(label="Max side length", value=0, elem_id="extras_upscale_max_side_length", tooltip="If any of two sides of the image ends up larger than specified, will downscale it to fit. 0 = no limit.", min_width=160, step=8, minimum=0) + + with gr.TabItem('Scale to', elem_id="extras_scale_to_tab") as tab_scale_to: + with FormRow(): + with gr.Column(elem_id="upscaling_column_size", scale=4): + upscaling_resize_w = gr.Slider(minimum=64, maximum=8192, step=8, label="Width", value=512, elem_id="extras_upscaling_resize_w") + upscaling_resize_h = gr.Slider(minimum=64, maximum=8192, step=8, label="Height", value=512, elem_id="extras_upscaling_resize_h") + with gr.Column(elem_id="upscaling_dimensions_row", scale=1, elem_classes="dimensions-tools"): + upscaling_res_switch_btn = ToolButton(value=switch_values_symbol, elem_id="upscaling_res_switch_btn", tooltip="Switch width/height") + upscaling_crop = gr.Checkbox(label='Crop to fit', value=True, elem_id="extras_upscaling_crop") + + def on_selected_upscale_method(upscale_method): + if not shared.opts.set_scale_by_when_changing_upscaler: + return gr.update() + + match = re.search(r'(\d)[xX]|[xX](\d)', upscale_method) + if not match: + return gr.update() + + return gr.update(value=int(match.group(1) or match.group(2))) + + upscaling_res_switch_btn.click(lambda w, h: (h, w), inputs=[upscaling_resize_w, upscaling_resize_h], outputs=[upscaling_resize_w, upscaling_resize_h], show_progress=False) + tab_scale_by.select(fn=lambda: 0, inputs=[], outputs=[selected_tab]) + tab_scale_to.select(fn=lambda: 1, inputs=[], outputs=[selected_tab]) + + extras_upscaler_1.change(on_selected_upscale_method, inputs=[extras_upscaler_1], outputs=[upscaling_resize], show_progress="hidden") + + return { + "upscale_enabled": upscale_enabled, + "upscale_mode": selected_tab, + "upscale_by": upscaling_resize, + "max_side_length": max_side_length, + "upscale_to_width": upscaling_resize_w, + "upscale_to_height": upscaling_resize_h, + "upscale_crop": upscaling_crop, + "upscaler_1_name": extras_upscaler_1, + "upscaler_2_name": extras_upscaler_2, + "upscaler_2_visibility": extras_upscaler_2_visibility, + } + + def upscale(self, image, info, upscaler, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop): + if upscale_mode == 1: + upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) + info["Postprocess upscale to"] = f"{upscale_to_width}x{upscale_to_height}" + else: + info["Postprocess upscale by"] = upscale_by + if max_side_length != 0 and max(*image.size)*upscale_by > max_side_length: + upscale_mode = 1 + upscale_crop = False + upscale_to_width, upscale_to_height = limit_size_by_one_dimention(image.width*upscale_by, image.height*upscale_by, max_side_length) + upscale_by = max(upscale_to_width/image.width, upscale_to_height/image.height) + info["Max side length"] = max_side_length + + cache_key = (hash(np.array(image.getdata()).tobytes()), upscaler.name, upscale_mode, upscale_by, upscale_to_width, upscale_to_height, upscale_crop) + cached_image = upscale_cache.pop(cache_key, None) + + if cached_image is not None: + image = cached_image + else: + image = upscaler.scaler.upscale(image, upscale_by, upscaler.data_path) + + upscale_cache[cache_key] = image + if len(upscale_cache) > shared.opts.upscaling_max_images_in_cache: + upscale_cache.pop(next(iter(upscale_cache), None), None) + + if upscale_mode == 1 and upscale_crop: + cropped = Image.new("RGB", (upscale_to_width, upscale_to_height)) + cropped.paste(image, box=(upscale_to_width // 2 - image.width // 2, upscale_to_height // 2 - image.height // 2)) + image = cropped + info["Postprocess crop to"] = f"{image.width}x{image.height}" + + return image + + def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + if upscale_mode == 1: + pp.shared.target_width = upscale_to_width + pp.shared.target_height = upscale_to_height + else: + pp.shared.target_width = int(pp.image.width * upscale_by) + pp.shared.target_height = int(pp.image.height * upscale_by) + + pp.shared.target_width, pp.shared.target_height = limit_size_by_one_dimention(pp.shared.target_width, pp.shared.target_height, max_side_length) + + def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_enabled=True, upscale_mode=1, upscale_by=2.0, max_side_length=0, upscale_to_width=None, upscale_to_height=None, upscale_crop=False, upscaler_1_name=None, upscaler_2_name=None, upscaler_2_visibility=0.0): + if not upscale_enabled: + return + + upscaler_1_name = upscaler_1_name + if upscaler_1_name == "None": + upscaler_1_name = None + + upscaler1 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_1_name]), None) + assert upscaler1 or (upscaler_1_name is None), f'could not find upscaler named {upscaler_1_name}' + + if not upscaler1: + return + + upscaler_2_name = upscaler_2_name + if upscaler_2_name == "None": + upscaler_2_name = None + + upscaler2 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_2_name and x.name != "None"]), None) + assert upscaler2 or (upscaler_2_name is None), f'could not find upscaler named {upscaler_2_name}' + + upscaled_image = self.upscale(pp.image, pp.info, upscaler1, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) + pp.info["Postprocess upscaler"] = upscaler1.name + + if upscaler2 and upscaler_2_visibility > 0: + second_upscale = self.upscale(pp.image, pp.info, upscaler2, upscale_mode, upscale_by, max_side_length, upscale_to_width, upscale_to_height, upscale_crop) + if upscaled_image.mode != second_upscale.mode: + second_upscale = second_upscale.convert(upscaled_image.mode) + upscaled_image = Image.blend(upscaled_image, second_upscale, upscaler_2_visibility) + + pp.info["Postprocess upscaler 2"] = upscaler2.name + + pp.image = upscaled_image + + def image_changed(self): + upscale_cache.clear() + + +class ScriptPostprocessingUpscaleSimple(ScriptPostprocessingUpscale): + name = "Simple Upscale" + order = 900 + + def ui(self): + with FormRow(): + upscaler_name = gr.Dropdown(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name) + upscale_by = gr.Slider(minimum=0.05, maximum=8.0, step=0.05, label="Upscale by", value=2) + + return { + "upscale_by": upscale_by, + "upscaler_name": upscaler_name, + } + + def process_firstpass(self, pp: scripts_postprocessing.PostprocessedImage, upscale_by=2.0, upscaler_name=None): + pp.shared.target_width = int(pp.image.width * upscale_by) + pp.shared.target_height = int(pp.image.height * upscale_by) + + def process(self, pp: scripts_postprocessing.PostprocessedImage, upscale_by=2.0, upscaler_name=None): + if upscaler_name is None or upscaler_name == "None": + return + + upscaler1 = next(iter([x for x in shared.sd_upscalers if x.name == upscaler_name]), None) + assert upscaler1, f'could not find upscaler named {upscaler_name}' + + pp.image = self.upscale(pp.image, pp.info, upscaler1, 0, upscale_by, 0, 0, 0, False) + pp.info["Postprocess upscaler"] = upscaler1.name diff --git a/scripts/process_png_metadata.py b/scripts/process_png_metadata.py new file mode 100644 index 0000000000000000000000000000000000000000..0919e18e6a53c1a340f051f38b1599f65a7c8358 --- /dev/null +++ b/scripts/process_png_metadata.py @@ -0,0 +1,255 @@ +import gradio as gr +import re +from PIL import Image +import pathlib + +import modules.scripts as scripts +from modules import processing +from modules import images +from modules.processing import process_images, Processed +from modules.shared import state +import modules.shared as shared +from modules.shared import opts +from modules.generation_parameters_copypaste import parse_generation_parameters +from modules.extras import run_pnginfo + +# github repository -> https://github.com/thundaga/SD-webui-txt2img-script + +def int_convert(text: str) -> int: + return int(text) + +def float_convert(text: str) -> float: + return float(text) + +def boolean_convert(text: str) -> bool: + return True if (text == "true") else False + +def hires_resize(p, parsed_text: dict): + # Fix the issue when the values doesn't exist + # Uses the default value (skip the reset part) + if not ('Hires upscale' in parsed_text or parsed_text['Hires resize-1'] != 0 or parsed_text['Hires resize-2'] != 0): + return p + + # Reset hr_settings to avoid wrong settings + p.hr_scale = None + p.hr_resize_x = int(0) + p.hr_resize_y = int(0) + if 'Hires upscale' in parsed_text: + p.hr_scale = float(parsed_text['Hires upscale']) + if 'Hires resize-1' in parsed_text: + p.hr_resize_x = int(parsed_text['Hires resize-1']) + if 'Hires resize-2' in parsed_text: + p.hr_resize_y = int(parsed_text['Hires resize-2']) + return p + +def override_settings(p, options: list, parsed_text: dict): + if "Checkpoint" in options and 'Model hash' in parsed_text: + p.override_settings['sd_model_checkpoint'] = parsed_text['Model hash'] + if "Clip Skip" in options and 'Clip skip' in parsed_text: + p.override_settings['CLIP_stop_at_last_layers'] = int(parsed_text['Clip skip']) + return p + +def width_height(p, parsed_text: dict): + if 'Size-1' in parsed_text: + p.width = int(parsed_text['Size-1']) + if 'Size-2' in parsed_text: + p.height = int(parsed_text['Size-2']) + return p + +def prompt_modifications(parsed_text: dict, front_tags: str, back_tags: str, remove_tags: str) -> str: + prompt = parsed_text['Prompt'] + + if remove_tags: + remove_tags = remove_tags.strip("\n") + tags = [x.strip() for x in remove_tags.split(',')] + while("" in tags): + tags.remove("") + text = prompt + + for tag in tags: + text = re.sub("\(\(" + tag + "\)\)|\(" + tag + ":.*?\)|<" + tag + ":.*?>|<" + tag + ">", "", text) + text = re.sub(r'\([^\(]*(%s)\S*\)' % tag, '', text) + text = re.sub(r'\[[^\[]*(%s)\S*\]' % tag, '', text) + text = re.sub(r'<[^<]*(%s)\S*>' % tag, '', text) + text = re.sub(r'\b' + tag + r'\b', '', text) + + # remove consecutive comma patterns with a coma and space + pattern = re.compile(r'(,\s){2,}') + text = re.sub(pattern, ', ', text) + + # remove final comma at start of prompt + text = text.replace(", ", "", 1) + prompt = text + + if front_tags: + if front_tags.endswith(' ') == False and front_tags.endswith(',') == False: + front_tags = front_tags + ',' + prompt = ''.join([front_tags, prompt]) + + if back_tags: + if back_tags.startswith(' ') == False and back_tags.startswith(',') == False: + back_tags = ',' + back_tags + prompt = ''.join([prompt, back_tags]) + return prompt + +# build valid txt and image files e.g (txt(utf-8),img(png)) into valid parsed dictionaries with metadata info +def build_file_list(file, tab_index: int, file_list: list[dict]) -> list[dict]: + + file = file.name if tab_index == 0 else file + file_ext = pathlib.Path(file).suffix + filename = pathlib.Path(file).stem + + if file_ext == ".txt": + text = open(file, "r", encoding="utf-8").read() + elif run_pnginfo(Image.open(file))[1] != None: + text = run_pnginfo(Image.open(file))[1] + + if text != None and text != "": + parsed_text = parse_generation_parameters(text) + parsed_text["filename"] = filename + file_list.append(parsed_text) + + return file_list + +# key->(option name) : Values->tuple(metadata name, object property, property specific functions) +prompt_options = { + "Checkpoint": ("Model hash", None, override_settings), + "Prompt": ("Prompt", "prompt", prompt_modifications), + "Negative Prompt": ("Negative prompt", "negative_prompt", None), + "Seed": ("Seed", "seed", float_convert), + "Variation Seed": ("Variation seed", "subseed", float_convert), + "Variation Seed Strength": ("Variation seed strength", "subseed_strength", float_convert), + "Sampler": ("Sampler", "sampler_name", None), + "Steps": ("Steps", "steps", int_convert), + "CFG scale": ("CFG scale", "cfg_scale", float_convert), + "Width and Height": (None, None, width_height), + "Upscaler": ("Hires upscaler", "hr_upscaler", None), + "Denoising Strength": ("Denoising strength", "denoising_strength", float_convert), + "Hires Scale or Width and Height": (None, None, hires_resize), + "Clip Skip": ("Clip skip", None, override_settings), + "Face restoration": ("Face restoration", "restore_faces", boolean_convert), +} + +class Script(scripts.Script): + + def title(self): + + return "Process PNG Metadata Info" + + def show(self, is_img2img): + + return not is_img2img + + # set up ui to drag and drop the processed images and hold their file info + def ui(self, is_img2img): + + tab_index = gr.State(value=0) + + with gr.Row().style(equal_height=False, variant='compact'): + with gr.Column(variant='compact'): + with gr.Tabs(elem_id="mode_extras"): + with gr.TabItem('Batch Process', elem_id="extras_batch_process_tab") as tab_batch: + upload_files = gr.File(label="Batch Process", file_count="multiple", interactive=True, type="file", elem_id=self.elem_id("files")) + + with gr.TabItem('Batch from Directory', elem_id="extras_batch_directory_tab") as tab_batch_dir: + input_dir = gr.Textbox(label="Input directory", **shared.hide_dirs, placeholder="Add input folder path", elem_id="files_batch_input_dir") + output_dir = gr.Textbox(label="Output directory", **shared.hide_dirs, placeholder="Add output folder path or Leave blank to use default path.", elem_id="files_batch_output_dir") + filename_format = gr.Dropdown(label="Output filename format", choices=["Exact same filename as Input file", "Same filename as Input file but with extrat digits", "Standard - Simple digits"], value="Standard - Simple digits", info="The \"Exact same filename\" option might crash or overwrite file(s) if there are multiple files with the same name in the input directory", interactive=True, elem_id="files_batch_filename_type") + + # CheckboxGroup with all parameters assignable from the input image (output is a list with the Name of the Checkbox checked ex: ["Checkpoint", "Prompt"]) + options = gr.Dropdown(list(prompt_options.keys()), label="Assign from input image", info="Select are assigned from the input, the rest from UI", multiselect = True) + + gr.HTML("

Optional tags to remove or add in front/end of a positive prompt on all images

") + front_tags = gr.Textbox(label="Tags to add at the front") + back_tags = gr.Textbox(label="Tags to add at the end") + remove_tags = gr.Textbox(label="Tags to remove") + + tab_batch.select(fn=lambda: 0, inputs=[], outputs=[tab_index]) + tab_batch_dir.select(fn=lambda: 1, inputs=[], outputs=[tab_index]) + + return [tab_index,upload_files,front_tags,back_tags,remove_tags,input_dir,output_dir,filename_format,options] + + # Files are open as images and the png info is set to the processed class for each iterated process + def run(self,p,tab_index,upload_files,front_tags,back_tags,remove_tags,input_dir,output_dir,filename_format,options): + + image_batch = [] + + # Operation based on current batch process tab + if tab_index == 0: + for file in upload_files: + image_batch = build_file_list(file, tab_index, image_batch) + elif tab_index == 1: + assert not shared.cmd_opts.hide_ui_dir_config, '--hide-ui-dir-config option must be disabled' + assert input_dir, 'input directory not selected' + + files_dir = shared.listfiles(input_dir) + for file in files_dir: + image_batch = build_file_list(file, tab_index, image_batch) + + if tab_index == 1 and output_dir != '': + p.do_not_save_samples = True + + image_count = len(image_batch) + state.job_count = image_count + + images_list = [] + all_prompts = [] + infotexts = [] + + for parsed_text in image_batch: + state.job = f"{state.job_no + 1} out of {state.job_count}" + + metadata, p_property, func = 0, 1, 2 + # go through dictionary and commit uniform actions on similar object properties + for option, tuple in prompt_options.items(): + match option: + case "Prompt": + if option in options and tuple[metadata] in parsed_text: + setattr(p, tuple[p_property], tuple[func](parsed_text,front_tags,back_tags,remove_tags)) + case "Width and Height": + if option in options: + p = tuple[func](p, parsed_text) + case "Hires Scale or Width and Height": + if option in options: + p = tuple[func](p, parsed_text) + case "Checkpoint" | "Clip Skip": + p = tuple[func](p, options, parsed_text) + case _: + if option in options and tuple[metadata] in parsed_text: + if tuple[func] == None: + setattr(p, tuple[p_property], parsed_text[tuple[metadata]]) + else: + setattr(p, tuple[p_property], tuple[func](parsed_text[tuple[metadata]])) + + proc = process_images(p) + + # Reset Hires prompts (else the prompts of the first image will be used as Hires prompt for all the others) + p.hr_prompt = "" + p.hr_negative_prompt = "" + + # Reset extra_generation_params as it stores the Hires resize and scale (Avoid having wrong info in the infotext) + p.extra_generation_params = {} + + # Modified directory to save generated images in cache + if tab_index == 1 and output_dir != '': + match filename_format: + case "Exact same filename as Input file": + basename = "" + forced_filename = parsed_text["filename"] + case "Same filename as Input file but with extrat digits": + basename = parsed_text["filename"] + forced_filename = None + case "Standard - Simple digits": + basename = "" + forced_filename = None + + for n, processed_image in enumerate(proc.images): + images.save_image(image=processed_image, path=output_dir, basename=basename, forced_filename=forced_filename, existing_info=processed_image.info) + + images_list += proc.images + all_prompts += proc.all_prompts + infotexts += proc.infotexts + + processing.fix_seed(p) + + return Processed(p, images_list, p.seed, "", all_prompts=all_prompts, infotexts=infotexts) diff --git a/scripts/prompt_matrix-Copy1.py b/scripts/prompt_matrix-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..88324fe6a74e952204abdeb6efb53d394a7d2af4 --- /dev/null +++ b/scripts/prompt_matrix-Copy1.py @@ -0,0 +1,108 @@ +import math + +import modules.scripts as scripts +import gradio as gr + +from modules import images +from modules.processing import process_images +from modules.shared import opts, state +import modules.sd_samplers + + +def draw_xy_grid(xs, ys, x_label, y_label, cell): + res = [] + + ver_texts = [[images.GridAnnotation(y_label(y))] for y in ys] + hor_texts = [[images.GridAnnotation(x_label(x))] for x in xs] + + first_processed = None + + state.job_count = len(xs) * len(ys) + + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + state.job = f"{ix + iy * len(xs) + 1} out of {len(xs) * len(ys)}" + + processed = cell(x, y) + if first_processed is None: + first_processed = processed + + res.append(processed.images[0]) + + grid = images.image_grid(res, rows=len(ys)) + grid = images.draw_grid_annotations(grid, res[0].width, res[0].height, hor_texts, ver_texts) + + first_processed.images = [grid] + + return first_processed + + +class Script(scripts.Script): + def title(self): + return "Prompt matrix" + + def ui(self, is_img2img): + gr.HTML('
') + with gr.Row(): + with gr.Column(): + put_at_start = gr.Checkbox(label='Put variable parts at start of prompt', value=False, elem_id=self.elem_id("put_at_start")) + different_seeds = gr.Checkbox(label='Use different seed for each picture', value=False, elem_id=self.elem_id("different_seeds")) + with gr.Column(): + prompt_type = gr.Radio(["positive", "negative"], label="Select prompt", elem_id=self.elem_id("prompt_type"), value="positive") + variations_delimiter = gr.Radio(["comma", "space"], label="Select joining char", elem_id=self.elem_id("variations_delimiter"), value="comma") + with gr.Column(): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + + return [put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size] + + def run(self, p, put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size): + modules.processing.fix_seed(p) + # Raise error if promp type is not positive or negative + if prompt_type not in ["positive", "negative"]: + raise ValueError(f"Unknown prompt type {prompt_type}") + # Raise error if variations delimiter is not comma or space + if variations_delimiter not in ["comma", "space"]: + raise ValueError(f"Unknown variations delimiter {variations_delimiter}") + + prompt = p.prompt if prompt_type == "positive" else p.negative_prompt + original_prompt = prompt[0] if type(prompt) == list else prompt + positive_prompt = p.prompt[0] if type(p.prompt) == list else p.prompt + + delimiter = ", " if variations_delimiter == "comma" else " " + + all_prompts = [] + prompt_matrix_parts = original_prompt.split("|") + combination_count = 2 ** (len(prompt_matrix_parts) - 1) + for combination_num in range(combination_count): + selected_prompts = [text.strip().strip(',') for n, text in enumerate(prompt_matrix_parts[1:]) if combination_num & (1 << n)] + + if put_at_start: + selected_prompts = selected_prompts + [prompt_matrix_parts[0]] + else: + selected_prompts = [prompt_matrix_parts[0]] + selected_prompts + + all_prompts.append(delimiter.join(selected_prompts)) + + p.n_iter = math.ceil(len(all_prompts) / p.batch_size) + p.do_not_save_grid = True + + print(f"Prompt matrix will create {len(all_prompts)} images using a total of {p.n_iter} batches.") + + if prompt_type == "positive": + p.prompt = all_prompts + else: + p.negative_prompt = all_prompts + p.seed = [p.seed + (i if different_seeds else 0) for i in range(len(all_prompts))] + p.prompt_for_display = positive_prompt + processed = process_images(p) + + grid = images.image_grid(processed.images, p.batch_size, rows=1 << ((len(prompt_matrix_parts) - 1) // 2)) + grid = images.draw_prompt_matrix(grid, processed.images[0].width, processed.images[0].height, prompt_matrix_parts, margin_size) + processed.images.insert(0, grid) + processed.index_of_first_image = 1 + processed.infotexts.insert(0, processed.infotexts[0]) + + if opts.grid_save: + images.save_image(processed.images[0], p.outpath_grids, "prompt_matrix", extension=opts.grid_format, prompt=original_prompt, seed=processed.seed, grid=True, p=p) + + return processed diff --git a/scripts/prompt_matrix.py b/scripts/prompt_matrix.py new file mode 100644 index 0000000000000000000000000000000000000000..88324fe6a74e952204abdeb6efb53d394a7d2af4 --- /dev/null +++ b/scripts/prompt_matrix.py @@ -0,0 +1,108 @@ +import math + +import modules.scripts as scripts +import gradio as gr + +from modules import images +from modules.processing import process_images +from modules.shared import opts, state +import modules.sd_samplers + + +def draw_xy_grid(xs, ys, x_label, y_label, cell): + res = [] + + ver_texts = [[images.GridAnnotation(y_label(y))] for y in ys] + hor_texts = [[images.GridAnnotation(x_label(x))] for x in xs] + + first_processed = None + + state.job_count = len(xs) * len(ys) + + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + state.job = f"{ix + iy * len(xs) + 1} out of {len(xs) * len(ys)}" + + processed = cell(x, y) + if first_processed is None: + first_processed = processed + + res.append(processed.images[0]) + + grid = images.image_grid(res, rows=len(ys)) + grid = images.draw_grid_annotations(grid, res[0].width, res[0].height, hor_texts, ver_texts) + + first_processed.images = [grid] + + return first_processed + + +class Script(scripts.Script): + def title(self): + return "Prompt matrix" + + def ui(self, is_img2img): + gr.HTML('
') + with gr.Row(): + with gr.Column(): + put_at_start = gr.Checkbox(label='Put variable parts at start of prompt', value=False, elem_id=self.elem_id("put_at_start")) + different_seeds = gr.Checkbox(label='Use different seed for each picture', value=False, elem_id=self.elem_id("different_seeds")) + with gr.Column(): + prompt_type = gr.Radio(["positive", "negative"], label="Select prompt", elem_id=self.elem_id("prompt_type"), value="positive") + variations_delimiter = gr.Radio(["comma", "space"], label="Select joining char", elem_id=self.elem_id("variations_delimiter"), value="comma") + with gr.Column(): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + + return [put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size] + + def run(self, p, put_at_start, different_seeds, prompt_type, variations_delimiter, margin_size): + modules.processing.fix_seed(p) + # Raise error if promp type is not positive or negative + if prompt_type not in ["positive", "negative"]: + raise ValueError(f"Unknown prompt type {prompt_type}") + # Raise error if variations delimiter is not comma or space + if variations_delimiter not in ["comma", "space"]: + raise ValueError(f"Unknown variations delimiter {variations_delimiter}") + + prompt = p.prompt if prompt_type == "positive" else p.negative_prompt + original_prompt = prompt[0] if type(prompt) == list else prompt + positive_prompt = p.prompt[0] if type(p.prompt) == list else p.prompt + + delimiter = ", " if variations_delimiter == "comma" else " " + + all_prompts = [] + prompt_matrix_parts = original_prompt.split("|") + combination_count = 2 ** (len(prompt_matrix_parts) - 1) + for combination_num in range(combination_count): + selected_prompts = [text.strip().strip(',') for n, text in enumerate(prompt_matrix_parts[1:]) if combination_num & (1 << n)] + + if put_at_start: + selected_prompts = selected_prompts + [prompt_matrix_parts[0]] + else: + selected_prompts = [prompt_matrix_parts[0]] + selected_prompts + + all_prompts.append(delimiter.join(selected_prompts)) + + p.n_iter = math.ceil(len(all_prompts) / p.batch_size) + p.do_not_save_grid = True + + print(f"Prompt matrix will create {len(all_prompts)} images using a total of {p.n_iter} batches.") + + if prompt_type == "positive": + p.prompt = all_prompts + else: + p.negative_prompt = all_prompts + p.seed = [p.seed + (i if different_seeds else 0) for i in range(len(all_prompts))] + p.prompt_for_display = positive_prompt + processed = process_images(p) + + grid = images.image_grid(processed.images, p.batch_size, rows=1 << ((len(prompt_matrix_parts) - 1) // 2)) + grid = images.draw_prompt_matrix(grid, processed.images[0].width, processed.images[0].height, prompt_matrix_parts, margin_size) + processed.images.insert(0, grid) + processed.index_of_first_image = 1 + processed.infotexts.insert(0, processed.infotexts[0]) + + if opts.grid_save: + images.save_image(processed.images[0], p.outpath_grids, "prompt_matrix", extension=opts.grid_format, prompt=original_prompt, seed=processed.seed, grid=True, p=p) + + return processed diff --git a/scripts/prompter.py b/scripts/prompter.py new file mode 100644 index 0000000000000000000000000000000000000000..8b35f693c48c354a11ae49283240a76628119060 --- /dev/null +++ b/scripts/prompter.py @@ -0,0 +1,616 @@ +import torch +import math +import re +import os +from .block_lora import lbw_lora +from typing import Union, Optional, List, Tuple +from abc import ABC + +class BaseTextualInversionManager(ABC): + def expand_textual_inversion_token_ids_if_necessary(self, token_ids: List[int]) -> List[int]: + raise NotImplementedError() + +class DiffusersTextualInversionManager(BaseTextualInversionManager): + """ + A textual inversion manager for use with diffusers. + """ + def __init__(self, pipe): + self.pipe = pipe + + def expand_textual_inversion_token_ids_if_necessary(self, token_ids: List[int]) -> List[int]: + if len(token_ids) == 0: + return token_ids + + prompt = self.pipe.tokenizer.decode(token_ids) + prompt = self.pipe.maybe_convert_prompt(prompt, self.pipe.tokenizer) + return self.pipe.tokenizer.encode(prompt, add_special_tokens=False) + + +re_attention = re.compile(r""" +\\\(| +\\\{| +\\\)| +\\\}| +\\\[| +\\]| +\\\\| +\\| +\(| +\{| +\[| +:([+-]?[.\d]+)\)| +\)| +\}| +]| +[^\\()\\{}\[\]:]+| +: +""", re.X) +re_AND = re.compile(r"\bAND\b") +re_weight = re.compile(r"^((?:\s|.)*?)(?:\s*:\s*([-+]?(?:\d+\.?|\d*\.\d+)))?\s*$") +re_break = re.compile(r"\s*\bBREAK\b\s*", re.S) + + +def parse_prompt_attention(text): + """ + Parses a string with attention tokens and returns a list of pairs: text and its assoicated weight. + Accepted tokens are: + (abc) - increases attention to abc by a multiplier of 1.1 + (abc:3.12) - increases attention to abc by a multiplier of 3.12 + [abc] - decreases attention to abc by a multiplier of 1.1 + \( - literal character '(' + \[ - literal character '[' + \) - literal character ')' + \] - literal character ']' + \\ - literal character '\' + anything else - just text + + >>> parse_prompt_attention('normal text') + [['normal text', 1.0]] + >>> parse_prompt_attention('an (important) word') + [['an ', 1.0], ['important', 1.1], [' word', 1.0]] + >>> parse_prompt_attention('(unbalanced') + [['unbalanced', 1.1]] + >>> parse_prompt_attention('\(literal\]') + [['(literal]', 1.0]] + >>> parse_prompt_attention('(unnecessary)(parens)') + [['unnecessaryparens', 1.1]] + >>> parse_prompt_attention('a (((house:1.3)) [on] a (hill:0.5), sun, (((sky))).') + [['a ', 1.0], + ['house', 1.5730000000000004], + [' ', 1.1], + ['on', 1.0], + [' a ', 1.1], + ['hill', 0.55], + [', sun, ', 1.1], + ['sky', 1.4641000000000006], + ['.', 1.1]] + """ + + res = [] + round_brackets = [] + square_brackets = [] + + round_bracket_multiplier = 1.1 + square_bracket_multiplier = 1 / 1.1 + + def multiply_range(start_position, multiplier): + for p in range(start_position, len(res)): + res[p][1] *= multiplier + + for m in re_attention.finditer(text): + text = m.group(0) + weight = m.group(1) + + if text.startswith('\\'): + res.append([text[1:], 1.0]) + elif text == '(': + round_brackets.append(len(res)) + elif text == '[': + square_brackets.append(len(res)) + elif weight is not None and round_brackets: + multiply_range(round_brackets.pop(), float(weight)) + elif text == ')' and round_brackets: + multiply_range(round_brackets.pop(), round_bracket_multiplier) + elif text == ']' and square_brackets: + multiply_range(square_brackets.pop(), square_bracket_multiplier) + else: + parts = re.split(re_break, text) + for i, part in enumerate(parts): + if i > 0: + res.append(["BREAK", -1]) + res.append([part, 1.0]) + + for pos in round_brackets: + multiply_range(pos, round_bracket_multiplier) + + for pos in square_brackets: + multiply_range(pos, square_bracket_multiplier) + + if len(res) == 0: + res = [["", 1.0]] + + # merge runs of identical weights + i = 0 + while i + 1 < len(res): + if res[i][1] == res[i + 1][1]: + res[i][0] += res[i + 1][0] + res.pop(i + 1) + else: + i += 1 + + return res + +class CLIPMultiTextCustomEmbedder(object): + def __init__(self, tokenizers, text_encoders, device, textual_inversion_manager: BaseTextualInversionManager, + clip_stop_at_last_layers=1, requires_pooled=False): + self.tokenizer = tokenizers[0] + self.tokenizer_2 = tokenizers[1] + self.text_encoder = text_encoders[0] + self.text_encoder_2 = text_encoders[1] + self.textual_inversion_manager = textual_inversion_manager + self.token_mults = {} + self.device = device + self.clip_stop_at_last_layers = clip_stop_at_last_layers + self.requires_pooled=requires_pooled + + def tokenize_line(self, line): + def get_target_prompt_token_count(token_count): + return math.ceil(max(token_count, 1) / 75) * 75 + id_start = self.tokenizer.bos_token_id + id_end = self.tokenizer.eos_token_id + parsed = parse_prompt_attention(line) + tokenized = self.tokenizer( + [text for text, _ in parsed], truncation=False, + add_special_tokens=False)["input_ids"] + + fixes = [] + remade_tokens = [] + multipliers = [] + + for tokens, (text, weight) in zip(tokenized, parsed): + i = 0 + while i < len(tokens): + token = tokens[i] + remade_tokens.append(token) + multipliers.append(weight) + i += 1 + + token_count = len(remade_tokens) + prompt_target_length = get_target_prompt_token_count(token_count) + tokens_to_add = prompt_target_length - len(remade_tokens) + remade_tokens = remade_tokens + [id_end] * tokens_to_add + multipliers = multipliers + [1.0] * tokens_to_add + return remade_tokens, fixes, multipliers, token_count + + def process_text(self, texts): + if isinstance(texts, str): + texts = [texts] + + remade_batch_tokens = [] + cache = {} + batch_multipliers = [] + for line in texts: + if line in cache: + remade_tokens, multipliers = cache[line] + else: + remade_tokens, _, multipliers, _ = self.tokenize_line(line) + cache[line] = (remade_tokens, multipliers) + remade_batch_tokens.append(remade_tokens) + batch_multipliers.append(multipliers) + + return batch_multipliers, remade_batch_tokens + + def __call__(self, text): + batch_multipliers, remade_batch_tokens = self.process_text(text) + + z = None + pooled = None + i = 0 + while max(map(len, remade_batch_tokens)) != 0: + rem_tokens = [x[75:] for x in remade_batch_tokens] + rem_multipliers = [x[75:] for x in batch_multipliers] + + tokens = [] + multipliers = [] + for j in range(len(remade_batch_tokens)): + if len(remade_batch_tokens[j]) > 0: + tokens.append(remade_batch_tokens[j][:75]) + multipliers.append(batch_multipliers[j][:75]) + else: + tokens.append([self.tokenizer.eos_token_id] * 75) + multipliers.append([1.0] * 75) + + z1, pooly = self.process_tokens(tokens, multipliers) + z = z1 if z is None else torch.cat((z, z1), axis=-2) + pooled = pooly if pooled is None else torch.cat((pooled, pooly), axis=-2) + + remade_batch_tokens = rem_tokens + batch_multipliers = rem_multipliers + i += 1 + + return [z, pooled] + + def process_tokens(self, remade_batch_tokens, batch_multipliers): + remade_batch_tokens = [[self.tokenizer.bos_token_id] + x[:75] + + [self.tokenizer.eos_token_id] for x in remade_batch_tokens] + batch_multipliers = [[1.0] + x[:75] + [1.0] for x in batch_multipliers] + + tokens = torch.asarray(remade_batch_tokens).to(self.device) + # print(tokens.shape) + # print(tokens) + encoder=[self.text_encoder,self.text_encoder_2] + plist=[] + batch_multipliers_of_same_length = [ + x + [1.0] * (75 - len(x)) for x in batch_multipliers] + batch_multipliers = torch.asarray( + batch_multipliers_of_same_length).to(self.device) + for text_encoder in encoder: + output = text_encoder(input_ids=tokens,output_hidden_states=True) + pooled=output[0] + if self.clip_stop_at_last_layers > 1: + z = output.hidden_states[-(2+self.clip_stop_at_last_layers)] + else: + z = output.hidden_states[-2] + z *= batch_multipliers.reshape(batch_multipliers.shape + (1,)).expand(z.shape) + plist.append(z) + + z = torch.concat(plist, dim=-1) + return z, pooled + + def get_text_tokens(self, text): + batch_multipliers, remade_batch_tokens = self.process_text(text) + return [[self.tokenizer.bos_token_id] + remade_batch_tokens[0]], \ + [[1.0] + batch_multipliers[0]] + + def get_token_ids(self, texts: List[str]) -> List[List[int]]: + token_ids_list = self.tokenizer( + texts, + truncation=True, + padding="max_length", + return_tensors=None, # just give me lists of ints + )['input_ids'] + + result = [] + for token_ids in token_ids_list: + # trim eos/bos + token_ids = token_ids[1:-1] + # pad for textual inversions with vector length >1 + if self.textual_inversion_manager is not None: + token_ids = self.textual_inversion_manager.expand_textual_inversion_token_ids_if_necessary(token_ids) + + token_ids = [self.tokenizer.bos_token_id] + token_ids + [self.tokenizer.eos_token_id] + + result.append(token_ids) + + return result + + def get_pooled_embeddings(self, texts: List[str], attention_mask: Optional[torch.Tensor]=None, device: Optional[str]=None) -> Optional[torch.Tensor]: + + device = device or self.device + + token_ids = self.get_token_ids(texts) + token_ids = torch.tensor(token_ids, dtype=torch.long).to(device) + + text_encoder_output = self.text_encoder(token_ids, attention_mask, return_dict=True) + pooled = [text_encoder_output.pooler_output] + pooled = torch.cat(pooled, dim=-1) + + return pooled + +class CLIPTextCustomEmbedder(object): + def __init__(self, tokenizer, text_encoder, device, textual_inversion_manager: BaseTextualInversionManager, + clip_stop_at_last_layers=1, requires_pooled=False): + self.tokenizer = tokenizer + self.text_encoder = text_encoder + self.textual_inversion_manager = textual_inversion_manager + self.token_mults = {} + self.device = device + self.clip_stop_at_last_layers = clip_stop_at_last_layers + self.requires_pooled=requires_pooled + + def tokenize_line(self, line): + def get_target_prompt_token_count(token_count): + return math.ceil(max(token_count, 1) / 75) * 75 + + id_end = self.tokenizer.eos_token_id + parsed = parse_prompt_attention(line) + tokenized = self.tokenizer( + [text for text, _ in parsed], truncation=False, + add_special_tokens=False)["input_ids"] + + fixes = [] + remade_tokens = [] + multipliers = [] + + for tokens, (text, weight) in zip(tokenized, parsed): + i = 0 + while i < len(tokens): + token = tokens[i] + remade_tokens.append(token) + multipliers.append(weight) + i += 1 + + token_count = len(remade_tokens) + prompt_target_length = get_target_prompt_token_count(token_count) + tokens_to_add = prompt_target_length - len(remade_tokens) + remade_tokens = remade_tokens + [id_end] * tokens_to_add + multipliers = multipliers + [1.0] * tokens_to_add + return remade_tokens, fixes, multipliers, token_count + + def process_text(self, texts): + if isinstance(texts, str): + texts = [texts] + + remade_batch_tokens = [] + cache = {} + batch_multipliers = [] + for line in texts: + if line in cache: + remade_tokens, fixes, multipliers = cache[line] + else: + remade_tokens, fixes, multipliers, _ = self.tokenize_line(line) + cache[line] = (remade_tokens, fixes, multipliers) + + remade_batch_tokens.append(remade_tokens) + batch_multipliers.append(multipliers) + + return batch_multipliers, remade_batch_tokens + + def __call__(self, text): + batch_multipliers, remade_batch_tokens = self.process_text(text) + + z = None + i = 0 + while max(map(len, remade_batch_tokens)) != 0: + rem_tokens = [x[75:] for x in remade_batch_tokens] + rem_multipliers = [x[75:] for x in batch_multipliers] + + tokens = [] + multipliers = [] + for j in range(len(remade_batch_tokens)): + if len(remade_batch_tokens[j]) > 0: + tokens.append(remade_batch_tokens[j][:75]) + multipliers.append(batch_multipliers[j][:75]) + else: + tokens.append([self.tokenizer.eos_token_id] * 75) + multipliers.append([1.0] * 75) + + z1 = self.process_tokens(tokens, multipliers) + z = z1 if z is None else torch.cat((z, z1), axis=-2) + + remade_batch_tokens = rem_tokens + batch_multipliers = rem_multipliers + i += 1 + + return z + + def process_tokens(self, remade_batch_tokens, batch_multipliers): + remade_batch_tokens = [[self.tokenizer.bos_token_id] + x[:75] + + [self.tokenizer.eos_token_id] for x in remade_batch_tokens] + batch_multipliers = [[1.0] + x[:75] + [1.0] for x in batch_multipliers] + + tokens = torch.asarray(remade_batch_tokens).to(self.device) + # print(tokens.shape) + # print(tokens) + outputs = self.text_encoder( + input_ids=tokens, output_hidden_states=True) + + if self.clip_stop_at_last_layers > 1: + z = self.text_encoder.text_model.final_layer_norm( + outputs.hidden_states[-self.clip_stop_at_last_layers]) + else: + z = outputs.last_hidden_state + + # restoring original mean is likely not correct, but it seems to work well + # to prevent artifacts that happen otherwise + batch_multipliers_of_same_length = [ + x + [1.0] * (75 - len(x)) for x in batch_multipliers] + batch_multipliers = torch.asarray( + batch_multipliers_of_same_length).to(self.device) + # print(batch_multipliers.shape) + # print(batch_multipliers) + + original_mean = z.mean() + z *= batch_multipliers.reshape(batch_multipliers.shape + + (1,)).expand(z.shape) + new_mean = z.mean() + z *= original_mean / new_mean + + return z + + def get_text_tokens(self, text): + batch_multipliers, remade_batch_tokens = self.process_text(text) + return [[self.tokenizer.bos_token_id] + remade_batch_tokens[0]], \ + [[1.0] + batch_multipliers[0]] + + def get_token_ids(self, texts: List[str]) -> List[List[int]]: + token_ids_list = self.tokenizer( + texts, + truncation=True, + padding="max_length", + return_tensors=None, # just give me lists of ints + )['input_ids'] + + result = [] + for token_ids in token_ids_list: + # trim eos/bos + token_ids = token_ids[1:-1] + # pad for textual inversions with vector length >1 + if self.textual_inversion_manager is not None: + token_ids = self.textual_inversion_manager.expand_textual_inversion_token_ids_if_necessary(token_ids) + + # add back eos/bos if requested + if include_start_and_end_markers: + token_ids = [self.tokenizer.bos_token_id] + token_ids + [self.tokenizer.eos_token_id] + + result.append(token_ids) + + return result + + def get_pooled_embeddings(self, texts: List[str], attention_mask: Optional[torch.Tensor]=None, device: Optional[str]=None) -> Optional[torch.Tensor]: + + device = device or self.device + + token_ids = self.get_token_ids(texts) + token_ids = torch.tensor(token_ids, dtype=torch.long).to(device) + + text_encoder_output = self.text_encoder(token_ids, attention_mask, return_dict=True) + pooled = text_encoder_output.pooler_output + + return pooled + +def text_embeddings_equal_len(text_embedder, prompt, negative_prompt) -> List[torch.Tensor]: + conds = text_embedder(prompt) + unconds = text_embedder(negative_prompt) + if text_embedder.requires_pooled: + conditionings = [conds[0], unconds[0]] + pooled = [text_embedder.get_pooled_embeddings([prompt], device=text_embedder.device), text_embedder.get_pooled_embeddings([negative_prompt], device=text_embedder.device),] + else: + conditionings = [conds,unconds] + pooled = None + + emptystring_conditioning = text_embedder("") + if type(emptystring_conditioning) is tuple or type(emptystring_conditioning) is list: + # discard pooled + emptystring_conditioning = emptystring_conditioning[0] + + # ensure all conditioning tensors are 3 dimensions + c0_shape = conditionings[0].shape + + if not all([c.shape[0] == c0_shape[0] and c.shape[2] == c0_shape[2] for c in conditionings]): + raise ValueError(f"All conditioning tensors must have the same batch size ({c0_shape[0]}) and number of embeddings per token ({c0_shape[1]}") + + if len(emptystring_conditioning.shape) == 2: + emptystring_conditioning = emptystring_conditioning.unsqueeze(0) + empty_z = torch.cat([emptystring_conditioning] * c0_shape[0]) + max_token_count = max([c.shape[1] for c in conditionings]) + # if necessary, pad shorter tensors out with an emptystring tensor + for i, c in enumerate(conditionings): + while c.shape[1] < max_token_count: + c = torch.cat([c, empty_z], dim=1) + conditionings[i] = c + return conditionings, pooled + +def text_embeddings(pipe, prompt, negative_prompt, clip_stop_at_last_layers=1, pool=False): + if pool: + text_embedder = CLIPMultiTextCustomEmbedder(tokenizers=[pipe.tokenizer,pipe.tokenizer_2], + text_encoders=[pipe.text_encoder,pipe.text_encoder_2], + device=pipe.text_encoder.device, + textual_inversion_manager=DiffusersTextualInversionManager(pipe), + clip_stop_at_last_layers=clip_stop_at_last_layers, + requires_pooled=pool) + else: + text_embedder = CLIPTextCustomEmbedder(tokenizer=pipe.tokenizer, + text_encoder=pipe.text_encoder, + device=pipe.text_encoder.device, + textual_inversion_manager=DiffusersTextualInversionManager(pipe), + clip_stop_at_last_layers=clip_stop_at_last_layers, + requires_pooled=pool) + conds, pooled = text_embeddings_equal_len(text_embedder, prompt, negative_prompt) + return conds, pooled + +def apply_embeddings(pipe, input_str,epath,pool): + for name, path in epath.items(): + new_string = input_str.replace(name,"") + + if new_string != input_str: + if pool: + from safetensors.torch import load_file + state = load_file(path) + pipe.load_textual_inversion(pretrained_model_name_or_path=state["clip_g"], token=name, text_encoder=pipe.text_encoder_2, tokenizer=pipe.tokenizer_2, local_files_only=True) + pipe.load_textual_inversion(pretrained_model_name_or_path=state["clip_l"], token=name, text_encoder=pipe.text_encoder, tokenizer=pipe.tokenizer, local_files_only=True) + else: + pipe.load_textual_inversion(pretrained_model_name_or_path=path, token=name, local_files_only=True) + + return input_str + +def bpro(prompt): + k = prompt.split(",") + thu = [] + for g in k: + f = g.count(" ") + thu.append([g, f+1]) + off = 0 + nl = [] + t = 0 + for x in thu: + if "BREAK" in x[0]: + tok = t+off + add = tok % 75 + nl += [" "]*add + off += add + continue + t += x[1] + nl.append(x[0]) + return ",".join(nl) + +def lora_prompt(prompt, pipe, lpath): + loras = [] + adap_list=[] + alphas=[] + add = [] + prompt=bpro(prompt) + def network_replacement(m): + alias = m.group(1) + num = m.group(2) + try: + data = lpath[alias] + mpath = data[1] + dpath = data[0] + alias = data[2] + except: + return "" + if "|" in num: + t = num.split("|") + alpha = float(t[0]) + apply = t[1] + npath = f"{mpath}{alias}_{apply}.safetensors" + try: + data = lpath[f"{alias}_{apply}"] + loras.append([data[0], alpha]) + return alias + except: + lpath[f"{alias}_{apply}"] = [npath, dpath] + lbw_lora(dpath, npath, apply) + dpath = npath + else: + alpha = float(num) + loras.append([dpath, alpha]) + return alias + re_lora = re.compile("") + prompt = re.sub(re_lora, network_replacement, prompt) + for k in loras: + p = os.path.abspath(os.path.join(k[0], "..")) + safe = os.path.basename(k[0]) + name = os.path.splitext(safe)[0].replace(".","_") + alphas.append(k[1]) + adap_list.append(name) + try: + pipe.load_lora_weights(p, weight_name=safe, adapter_name=name) + except: + pass + pipe.set_adapters(adap_list, adapter_weights=alphas) + return prompt, lpath + +def create_conditioning(pipe, positive: str, negative: str, epath, lora_list, clip_skip = 1, pool = False): + positive = apply_embeddings(pipe, positive, epath, pool) + negative = apply_embeddings(pipe, negative, epath, pool) + + positive, lora_list = lora_prompt(positive, pipe, lora_list) + + conds, pooled = text_embeddings(pipe, positive, negative, clip_skip, pool) + if pool: + embed_dict={ + 'prompt_embeds': conds[0], + 'pooled_prompt_embeds':pooled[0], + 'negative_prompt_embeds': conds[1], + 'negative_pooled_prompt_embeds': pooled[1], + } + + else: + embed_dict={ + 'prompt_embeds': conds[0], + 'negative_prompt_embeds': conds[1], + } + + return (embed_dict, lora_list) diff --git a/scripts/prompts_from_file-Copy1.py b/scripts/prompts_from_file-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..a4a2f24dd25d0bf982a48c6dc7258aa4b9a1153a --- /dev/null +++ b/scripts/prompts_from_file-Copy1.py @@ -0,0 +1,191 @@ +import copy +import random +import shlex + +import modules.scripts as scripts +import gradio as gr + +from modules import sd_samplers, errors, sd_models +from modules.processing import Processed, process_images +from modules.shared import state + + +def process_model_tag(tag): + info = sd_models.get_closet_checkpoint_match(tag) + assert info is not None, f'Unknown checkpoint: {tag}' + return info.name + + +def process_string_tag(tag): + return tag + + +def process_int_tag(tag): + return int(tag) + + +def process_float_tag(tag): + return float(tag) + + +def process_boolean_tag(tag): + return True if (tag == "true") else False + + +prompt_tags = { + "sd_model": process_model_tag, + "outpath_samples": process_string_tag, + "outpath_grids": process_string_tag, + "prompt_for_display": process_string_tag, + "prompt": process_string_tag, + "negative_prompt": process_string_tag, + "styles": process_string_tag, + "seed": process_int_tag, + "subseed_strength": process_float_tag, + "subseed": process_int_tag, + "seed_resize_from_h": process_int_tag, + "seed_resize_from_w": process_int_tag, + "sampler_index": process_int_tag, + "sampler_name": process_string_tag, + "batch_size": process_int_tag, + "n_iter": process_int_tag, + "steps": process_int_tag, + "cfg_scale": process_float_tag, + "width": process_int_tag, + "height": process_int_tag, + "restore_faces": process_boolean_tag, + "tiling": process_boolean_tag, + "do_not_save_samples": process_boolean_tag, + "do_not_save_grid": process_boolean_tag +} + + +def cmdargs(line): + args = shlex.split(line) + pos = 0 + res = {} + + while pos < len(args): + arg = args[pos] + + assert arg.startswith("--"), f'must start with "--": {arg}' + assert pos+1 < len(args), f'missing argument for command line option {arg}' + + tag = arg[2:] + + if tag == "prompt" or tag == "negative_prompt": + pos += 1 + prompt = args[pos] + pos += 1 + while pos < len(args) and not args[pos].startswith("--"): + prompt += " " + prompt += args[pos] + pos += 1 + res[tag] = prompt + continue + + + func = prompt_tags.get(tag, None) + assert func, f'unknown commandline option: {arg}' + + val = args[pos+1] + if tag == "sampler_name": + val = sd_samplers.samplers_map.get(val.lower(), None) + + res[tag] = func(val) + + pos += 2 + + return res + + +def load_prompt_file(file): + if file is None: + return None, gr.update(), gr.update(lines=7) + else: + lines = [x.strip() for x in file.decode('utf8', errors='ignore').split("\n")] + return None, "\n".join(lines), gr.update(lines=7) + + +class Script(scripts.Script): + def title(self): + return "Prompts from file or textbox" + + def ui(self, is_img2img): + checkbox_iterate = gr.Checkbox(label="Iterate seed every line", value=False, elem_id=self.elem_id("checkbox_iterate")) + checkbox_iterate_batch = gr.Checkbox(label="Use same random seed for all lines", value=False, elem_id=self.elem_id("checkbox_iterate_batch")) + prompt_position = gr.Radio(["start", "end"], label="Insert prompts at the", elem_id=self.elem_id("prompt_position"), value="start") + + prompt_txt = gr.Textbox(label="List of prompt inputs", lines=1, elem_id=self.elem_id("prompt_txt")) + file = gr.File(label="Upload prompt inputs", type='binary', elem_id=self.elem_id("file")) + + file.change(fn=load_prompt_file, inputs=[file], outputs=[file, prompt_txt, prompt_txt], show_progress=False) + + # We start at one line. When the text changes, we jump to seven lines, or two lines if no \n. + # We don't shrink back to 1, because that causes the control to ignore [enter], and it may + # be unclear to the user that shift-enter is needed. + prompt_txt.change(lambda tb: gr.update(lines=7) if ("\n" in tb) else gr.update(lines=2), inputs=[prompt_txt], outputs=[prompt_txt], show_progress=False) + return [checkbox_iterate, checkbox_iterate_batch, prompt_position, prompt_txt] + + def run(self, p, checkbox_iterate, checkbox_iterate_batch, prompt_position, prompt_txt: str): + lines = [x for x in (x.strip() for x in prompt_txt.splitlines()) if x] + + p.do_not_save_grid = True + + job_count = 0 + jobs = [] + + for line in lines: + if "--" in line: + try: + args = cmdargs(line) + except Exception: + errors.report(f"Error parsing line {line} as commandline", exc_info=True) + args = {"prompt": line} + else: + args = {"prompt": line} + + job_count += args.get("n_iter", p.n_iter) + + jobs.append(args) + + print(f"Will process {len(lines)} lines in {job_count} jobs.") + if (checkbox_iterate or checkbox_iterate_batch) and p.seed == -1: + p.seed = int(random.randrange(4294967294)) + + state.job_count = job_count + + images = [] + all_prompts = [] + infotexts = [] + for args in jobs: + state.job = f"{state.job_no + 1} out of {state.job_count}" + + copy_p = copy.copy(p) + for k, v in args.items(): + if k == "sd_model": + copy_p.override_settings['sd_model_checkpoint'] = v + else: + setattr(copy_p, k, v) + + if args.get("prompt") and p.prompt: + if prompt_position == "start": + copy_p.prompt = args.get("prompt") + " " + p.prompt + else: + copy_p.prompt = p.prompt + " " + args.get("prompt") + + if args.get("negative_prompt") and p.negative_prompt: + if prompt_position == "start": + copy_p.negative_prompt = args.get("negative_prompt") + " " + p.negative_prompt + else: + copy_p.negative_prompt = p.negative_prompt + " " + args.get("negative_prompt") + + proc = process_images(copy_p) + images += proc.images + + if checkbox_iterate: + p.seed = p.seed + (p.batch_size * p.n_iter) + all_prompts += proc.all_prompts + infotexts += proc.infotexts + + return Processed(p, images, p.seed, "", all_prompts=all_prompts, infotexts=infotexts) diff --git a/scripts/prompts_from_file.py b/scripts/prompts_from_file.py new file mode 100644 index 0000000000000000000000000000000000000000..a4a2f24dd25d0bf982a48c6dc7258aa4b9a1153a --- /dev/null +++ b/scripts/prompts_from_file.py @@ -0,0 +1,191 @@ +import copy +import random +import shlex + +import modules.scripts as scripts +import gradio as gr + +from modules import sd_samplers, errors, sd_models +from modules.processing import Processed, process_images +from modules.shared import state + + +def process_model_tag(tag): + info = sd_models.get_closet_checkpoint_match(tag) + assert info is not None, f'Unknown checkpoint: {tag}' + return info.name + + +def process_string_tag(tag): + return tag + + +def process_int_tag(tag): + return int(tag) + + +def process_float_tag(tag): + return float(tag) + + +def process_boolean_tag(tag): + return True if (tag == "true") else False + + +prompt_tags = { + "sd_model": process_model_tag, + "outpath_samples": process_string_tag, + "outpath_grids": process_string_tag, + "prompt_for_display": process_string_tag, + "prompt": process_string_tag, + "negative_prompt": process_string_tag, + "styles": process_string_tag, + "seed": process_int_tag, + "subseed_strength": process_float_tag, + "subseed": process_int_tag, + "seed_resize_from_h": process_int_tag, + "seed_resize_from_w": process_int_tag, + "sampler_index": process_int_tag, + "sampler_name": process_string_tag, + "batch_size": process_int_tag, + "n_iter": process_int_tag, + "steps": process_int_tag, + "cfg_scale": process_float_tag, + "width": process_int_tag, + "height": process_int_tag, + "restore_faces": process_boolean_tag, + "tiling": process_boolean_tag, + "do_not_save_samples": process_boolean_tag, + "do_not_save_grid": process_boolean_tag +} + + +def cmdargs(line): + args = shlex.split(line) + pos = 0 + res = {} + + while pos < len(args): + arg = args[pos] + + assert arg.startswith("--"), f'must start with "--": {arg}' + assert pos+1 < len(args), f'missing argument for command line option {arg}' + + tag = arg[2:] + + if tag == "prompt" or tag == "negative_prompt": + pos += 1 + prompt = args[pos] + pos += 1 + while pos < len(args) and not args[pos].startswith("--"): + prompt += " " + prompt += args[pos] + pos += 1 + res[tag] = prompt + continue + + + func = prompt_tags.get(tag, None) + assert func, f'unknown commandline option: {arg}' + + val = args[pos+1] + if tag == "sampler_name": + val = sd_samplers.samplers_map.get(val.lower(), None) + + res[tag] = func(val) + + pos += 2 + + return res + + +def load_prompt_file(file): + if file is None: + return None, gr.update(), gr.update(lines=7) + else: + lines = [x.strip() for x in file.decode('utf8', errors='ignore').split("\n")] + return None, "\n".join(lines), gr.update(lines=7) + + +class Script(scripts.Script): + def title(self): + return "Prompts from file or textbox" + + def ui(self, is_img2img): + checkbox_iterate = gr.Checkbox(label="Iterate seed every line", value=False, elem_id=self.elem_id("checkbox_iterate")) + checkbox_iterate_batch = gr.Checkbox(label="Use same random seed for all lines", value=False, elem_id=self.elem_id("checkbox_iterate_batch")) + prompt_position = gr.Radio(["start", "end"], label="Insert prompts at the", elem_id=self.elem_id("prompt_position"), value="start") + + prompt_txt = gr.Textbox(label="List of prompt inputs", lines=1, elem_id=self.elem_id("prompt_txt")) + file = gr.File(label="Upload prompt inputs", type='binary', elem_id=self.elem_id("file")) + + file.change(fn=load_prompt_file, inputs=[file], outputs=[file, prompt_txt, prompt_txt], show_progress=False) + + # We start at one line. When the text changes, we jump to seven lines, or two lines if no \n. + # We don't shrink back to 1, because that causes the control to ignore [enter], and it may + # be unclear to the user that shift-enter is needed. + prompt_txt.change(lambda tb: gr.update(lines=7) if ("\n" in tb) else gr.update(lines=2), inputs=[prompt_txt], outputs=[prompt_txt], show_progress=False) + return [checkbox_iterate, checkbox_iterate_batch, prompt_position, prompt_txt] + + def run(self, p, checkbox_iterate, checkbox_iterate_batch, prompt_position, prompt_txt: str): + lines = [x for x in (x.strip() for x in prompt_txt.splitlines()) if x] + + p.do_not_save_grid = True + + job_count = 0 + jobs = [] + + for line in lines: + if "--" in line: + try: + args = cmdargs(line) + except Exception: + errors.report(f"Error parsing line {line} as commandline", exc_info=True) + args = {"prompt": line} + else: + args = {"prompt": line} + + job_count += args.get("n_iter", p.n_iter) + + jobs.append(args) + + print(f"Will process {len(lines)} lines in {job_count} jobs.") + if (checkbox_iterate or checkbox_iterate_batch) and p.seed == -1: + p.seed = int(random.randrange(4294967294)) + + state.job_count = job_count + + images = [] + all_prompts = [] + infotexts = [] + for args in jobs: + state.job = f"{state.job_no + 1} out of {state.job_count}" + + copy_p = copy.copy(p) + for k, v in args.items(): + if k == "sd_model": + copy_p.override_settings['sd_model_checkpoint'] = v + else: + setattr(copy_p, k, v) + + if args.get("prompt") and p.prompt: + if prompt_position == "start": + copy_p.prompt = args.get("prompt") + " " + p.prompt + else: + copy_p.prompt = p.prompt + " " + args.get("prompt") + + if args.get("negative_prompt") and p.negative_prompt: + if prompt_position == "start": + copy_p.negative_prompt = args.get("negative_prompt") + " " + p.negative_prompt + else: + copy_p.negative_prompt = p.negative_prompt + " " + args.get("negative_prompt") + + proc = process_images(copy_p) + images += proc.images + + if checkbox_iterate: + p.seed = p.seed + (p.batch_size * p.n_iter) + all_prompts += proc.all_prompts + infotexts += proc.infotexts + + return Processed(p, images, p.seed, "", all_prompts=all_prompts, infotexts=infotexts) diff --git a/scripts/prompts_from_file_2.py b/scripts/prompts_from_file_2.py new file mode 100644 index 0000000000000000000000000000000000000000..6cb742299cbcbf461d1589f6f7c5a03ad86f13e0 --- /dev/null +++ b/scripts/prompts_from_file_2.py @@ -0,0 +1,255 @@ +import copy +import math +import os +import random +import sys +import traceback +import shlex + +import modules.scripts as scripts +import gradio as gr + +from modules import sd_samplers +from modules.processing import Processed, process_images +from PIL import Image +from modules.shared import opts, cmd_opts, state + + +def process_string_tag(tag): + return tag + + +def process_int_tag(tag): + return int(tag) + + +def process_float_tag(tag): + return float(tag) + + +def process_boolean_tag(tag): + return True if (tag == "true") else False + + +prompt_tags = { + "sd_model": None, + "outpath_samples": process_string_tag, + "outpath_grids": process_string_tag, + "prompt_for_display": process_string_tag, + "prompt": process_string_tag, + "negative_prompt": process_string_tag, + "styles": process_string_tag, + "seed": process_int_tag, + "subseed_strength": process_float_tag, + "subseed": process_int_tag, + "seed_resize_from_h": process_int_tag, + "seed_resize_from_w": process_int_tag, + "sampler_index": process_int_tag, + "sampler_name": process_string_tag, + "batch_size": process_int_tag, + "n_iter": process_int_tag, + "steps": process_int_tag, + "cfg_scale": process_float_tag, + "width": process_int_tag, + "height": process_int_tag, + "restore_faces": process_boolean_tag, + "tiling": process_boolean_tag, + "do_not_save_samples": process_boolean_tag, + "do_not_save_grid": process_boolean_tag +} + + +def cmdargs(line): + args = shlex.split(line) + pos = 0 + res = {} + + while pos < len(args): + arg = args[pos] + + assert arg.startswith("--"), f'must start with "--": {arg}' + assert pos+1 < len(args), f'missing argument for command line option {arg}' + + tag = arg[2:] + + if tag == "prompt" or tag == "negative_prompt": + pos += 1 + prompt = args[pos] + pos += 1 + while pos < len(args) and not args[pos].startswith("--"): + prompt += " " + prompt += args[pos] + pos += 1 + res[tag] = prompt + continue + + + func = prompt_tags.get(tag, None) + assert func, f'unknown commandline option: {arg}' + + val = args[pos+1] + if tag == "sampler_name": + val = sd_samplers.samplers_map.get(val.lower(), None) + + res[tag] = func(val) + + pos += 2 + + return res + + +def load_prompt_file(file): + if file is None: + lines = [] + else: + lines = [x.strip() for x in file.decode('utf8', errors='ignore').split("\n")] + + return None, "\n".join(lines), gr.update(lines=7) + +def on_stop_button_click(): + state.interrupt() + if state.job_count == 0: + print("🤖 Beep boop! I'm your idling graphic card. 🚀 It's like a vacation! 😂") + else: + print(f'{state.job_no} / {state.job_count} jobs interrupted... Aborting now...') + + +class Script(scripts.Script): + + def title(self): + return "Prompts from file or textbox (v2)" + + def ui(self, is_img2img): + + with gr.Row(): + with gr.Column(): + gr.HTML("

The script will sequently apply each line of prompt as a list from your input, and run jobs in a loop

") + prompt_txt = gr.Textbox(label="Prompt list (seperate with line break)", lines=3, elem_id=self.elem_id("prompt_txt"), placeholder='Enter your prompt(s) here \n(default seperator is line break)', + info='By default, enther something in the box will override text prompt (will keep all other parameters including negative prompt)') + + with gr.Row(): + with gr.Column(scale=1, min_width=100): + file = gr.File(label="Upload prompt file", type='binary', elem_id=self.elem_id("file")) + + with gr.Column(scale=2, min_width=200): + separator_txt = gr.Textbox(label="Custom line separator (Optional)", lines=1, elem_id=self.elem_id("separator_txt"), placeholder='Leave blank for default', + info='If you want to use prompt(s) that has multiple lines with line break(s), you might want to set a custom line seperator instead of line break by default') + repetitions_slider = gr.Slider(label="Job repeats", step=1, minimum=1, maximum=200, value=1, elem_id=self.elem_id("repetitions_slider"), + info='This will repeat your jobs n times. By default, each repeat will always start with a random seed.') + stop_button = gr.Button(label="Stop Loop", value ='Force Interrupt and Abort Jobs', elem_id=self.elem_id("force_stop_button")) + with gr.Column(scale=1, min_width=100): + + checkbox_iterate = gr.Checkbox(label="Iterate seed every line", value=False, elem_id=self.elem_id("checkbox_iterate")) + checkbox_iterate_batch = gr.Checkbox(label="When seed is -1, use same random seed for all lines", value=False, elem_id=self.elem_id("checkbox_iterate_batch")) + checkbox_same_repeat_seed = gr.Checkbox(label="Use same seed to start all repeat jobs (only when seed is NOT -1)", value=False, elem_id=self.elem_id("repeat_with_random_seed")) + checkbox_save_grid = gr.Checkbox(label="Save image grid", value=False, elem_id=self.elem_id("checkbox_save_grid")) + + with gr.Row(): + gr.Markdown('''Notes: + - You can also override other parameters (including negative prompt, checkpoint model, vae, batch setting... etc.) by using format below: + *--prompt "a cute cat" --negative_prompt "green" --width 1024 --seed 12345678 ...* + - Each batch job will execute before the prompt loop, meaning that when the batch size or count is set to a value greater than 1, the script will run a complete batch with the same prompt, then use next prompt to run another batch of jobs. + - If you have set both the repeater and batch greater than 1, the script will run the jobs in the following order: Firstly a complete batch of jobs with prompt 1, then a complete batch with prompt 2, and so on... then move to the next repeat. For example: + *repeat 1: [whole batch with prompt1] -> [whole batch with prompt2]... and then move on to repeat 2: [whole batch with prompt1] -> [whole batch with prompt2]...* + - Therefore, if you only want to generate repeat job in order: prompt1 prompt2 prompt3... just use the job repeat slider, do not set any batch job. + ''') + + # We start at one line. When the text changes, we jump to seven lines, or two lines if no \n. + # We don't shrink back to 1, because that causes the control to ignore [enter], and it may + # be unclear to the user that shift-enter is needed. + prompt_txt.change(lambda tb: gr.update(lines=7) if ("\n" in tb) else gr.update(lines=3), inputs=[prompt_txt], outputs=[prompt_txt]) + file.change(fn=load_prompt_file, inputs=[file], outputs=[file, prompt_txt, prompt_txt]) + stop_button.click(fn=on_stop_button_click) + + + return [checkbox_iterate, checkbox_iterate_batch, checkbox_save_grid, prompt_txt, separator_txt, repetitions_slider, checkbox_same_repeat_seed] + + def run(self, p, checkbox_iterate, checkbox_iterate_batch, checkbox_save_grid, prompt_txt: str, separator_txt: str, repetitions_slider, checkbox_same_repeat_seed): + #seperator determine + separator = separator_txt.strip() if separator_txt.strip() else "\n" + lines = [x.strip() for x in prompt_txt.split(separator) if x.strip()] + lines = [x for x in lines if len(x) > 0] + + if checkbox_save_grid: + p.do_not_save_grid = False + else: + p.do_not_save_grid = True + + job_count_each_repeat = 0 + job_count = 0 + jobs = [] + + repetitions = repetitions_slider + + for line in lines: + if "--" in line: + try: + args = cmdargs(line) + except Exception: + print(f"Error parsing line {line} as commandline:", file=sys.stderr) + print(traceback.format_exc(), file=sys.stderr) + args = {"prompt": line} + else: + args = {"prompt": line} + + job_count_each_repeat += args.get("n_iter", p.n_iter) + job_count = job_count_each_repeat * repetitions + + jobs.append(args) + + print(f"Starting... Will process {len(lines)} prompts in ", f"{job_count_each_repeat} jobs." if repetitions < 2 else f"{job_count_each_repeat} x {repetitions} = {job_count_each_repeat*repetitions} jobs.") + + state.job_count = job_count + + images = [] + all_prompts = [] + infotexts = [] + + initial_seed = p.seed + generated_random_seed = -1 + + if (checkbox_iterate or checkbox_iterate_batch) and p.seed == -1: + p.seed = generated_random_seed = int(random.randrange(4294967294)) + + for r in range(0, int(repetitions)): + if state.interrupted: + print(f"🖼️ We have generated a total of {len(images)} images, using the provided prompts. The process was stoped at {state.job_no} of {state.job_count} jobs.") + repetitions = 1 + break + + elif repetitions > 1: + print(f'Total [{len(images)}] images created in {state.job_no}/{state.job_count} jobs. Starting repetition [{r+1}/{repetitions}]...') + + #keep seed for "use same seed" + if r>0 and checkbox_same_repeat_seed and initial_seed == -1: + if generated_random_seed != -1: + p.seed = generated_random_seed + else: + p.seed = initial_seed + elif r>0 and not checkbox_same_repeat_seed and generated_random_seed != -1: + p.seed = int(random.randrange(4294967294)) + elif r>0 and not checkbox_same_repeat_seed: + p.seed = -1 + + for n, args in enumerate(jobs): + if state.interrupted: + print(f"[{r}/{repetitions}] repetitions" if state.job_no / state.job_count == r else f"{r - 1} / {repetitions} repetitions completed. Aborting jobs.") + break + else: + state.job = f"{state.job_no + 1} out of {state.job_count}" + + copy_p = copy.copy(p) + + for k, v in args.items(): + setattr(copy_p, k, v) + proc = process_images(copy_p) + + if checkbox_iterate and generated_random_seed != -1: + p.seed = p.seed + (p.batch_size * p.n_iter) + + images += proc.images + all_prompts += proc.all_prompts + infotexts += proc.infotexts + + + return Processed(p, images, p.seed, "", all_prompts=all_prompts, infotexts=infotexts) \ No newline at end of file diff --git a/scripts/quick_upscale.py b/scripts/quick_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..56ffba98e549a68814f5717bbb62018d4e41fadf --- /dev/null +++ b/scripts/quick_upscale.py @@ -0,0 +1,45 @@ + +import math +import os +import sys +import traceback +import random + +import modules.scripts as scripts +import modules.images as images +import gradio as gr + +from modules.processing import Processed, process_images +from PIL import Image +from modules.shared import opts, cmd_opts, state + +class Script(scripts.Script): + def title(self): + return "Lanczos quick upscale" + + def ui(self, is_img2img): + upscale_factor = gr.Slider(minimum=1, maximum=4, step=0.1, label='Upscale factor', value=2) + return [upscale_factor] + + def run(self, p, upscale_factor): + infotexts = [] + def simple_upscale(img, upscale_factor): + w, h = img.size + w = int(w * upscale_factor) + h = int(h * upscale_factor) + return img.resize((w, h), Image.Resampling.LANCZOS) + + state.job_count = p.n_iter + p.n_iter = 1 + p.do_not_save_samples = True + output_images = [] + for batch_no in range(state.job_count): +# print(f"\nJob : {batch_no}/{state.job_count}\nSeed : {p.seed}\nPrompt : {p.prompt}") + proc = process_images(p) + infotexts.append(proc.info) + proc.images[0] = simple_upscale(proc.images[0], upscale_factor) + images.save_image(proc.images[0], p.outpath_samples, "", proc.seed, proc.prompt, opts.samples_format, info= proc.info, p=p) + output_images += proc.images + p.seed = proc.seed + 1 + + return Processed(p, images, infotexts=infotexts,index_of_first_image=0) diff --git a/scripts/run_n_times.py b/scripts/run_n_times.py new file mode 100644 index 0000000000000000000000000000000000000000..5d0989e8f40e9b0d3c12b81667d34b94fc4e5b81 --- /dev/null +++ b/scripts/run_n_times.py @@ -0,0 +1,24 @@ +import math +import os +import sys +import traceback + +import modules.scripts as scripts +import gradio as gr + +from modules.processing import Processed, process_images + +class Script(scripts.Script): + def title(self): + return "Run n times" + + def ui(self, is_img2img): + n = gr.Textbox(label="n") + return [n] + + def run(self, p, n): + for x in range(int(n)): + p.seed = -1 + proc = process_images(p) + image = proc.images + return Processed(p, image, p.seed, proc.info) diff --git a/scripts/save-steps.py b/scripts/save-steps.py new file mode 100644 index 0000000000000000000000000000000000000000..d975cba172dec0c29660c0257448263a690b3594 --- /dev/null +++ b/scripts/save-steps.py @@ -0,0 +1,36 @@ +import os.path + +import modules.scripts as scripts +import gradio as gr + +from modules import shared, sd_samplers_common +from modules.processing import Processed, process_images + +class Script(scripts.Script): + def title(self): + return "Save steps of the sampling process to files" + + def ui(self, is_img2img): + path = gr.Textbox(label="Save images to path", placeholder="Enter folder path here. Defaults to webui's root folder") + return [path] + + def run(self, p, path): + if not os.path.exists(path): + os.makedirs(path) + index = [0] + + def store_latent(x): + image = shared.state.current_image = sd_samplers_common.sample_to_image(x) + image.save(os.path.join(path, f"sample-{index[0]:05}.png")) + index[0] += 1 + fun(x) + + fun = sd_samplers_common.store_latent + sd_samplers_common.store_latent = store_latent + + try: + proc = process_images(p) + finally: + sd_samplers_common.store_latent = fun + + return Processed(p, proc.images, p.seed, "") diff --git a/scripts/sd_upscale-Copy1.py b/scripts/sd_upscale-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..e614c23b987ca47daf822aa91183de0ab58ee960 --- /dev/null +++ b/scripts/sd_upscale-Copy1.py @@ -0,0 +1,101 @@ +import math + +import modules.scripts as scripts +import gradio as gr +from PIL import Image + +from modules import processing, shared, images, devices +from modules.processing import Processed +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "SD upscale" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + info = gr.HTML("

Will upscale the image by the selected scale factor; use width and height sliders to set tile size

") + overlap = gr.Slider(minimum=0, maximum=256, step=16, label='Tile overlap', value=64, elem_id=self.elem_id("overlap")) + scale_factor = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label='Scale Factor', value=2.0, elem_id=self.elem_id("scale_factor")) + upscaler_index = gr.Radio(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index", elem_id=self.elem_id("upscaler_index")) + + return [info, overlap, upscaler_index, scale_factor] + + def run(self, p, _, overlap, upscaler_index, scale_factor): + if isinstance(upscaler_index, str): + upscaler_index = [x.name.lower() for x in shared.sd_upscalers].index(upscaler_index.lower()) + processing.fix_seed(p) + upscaler = shared.sd_upscalers[upscaler_index] + + p.extra_generation_params["SD upscale overlap"] = overlap + p.extra_generation_params["SD upscale upscaler"] = upscaler.name + + initial_info = None + seed = p.seed + + init_img = p.init_images[0] + init_img = images.flatten(init_img, opts.img2img_background_color) + + if upscaler.name != "None": + img = upscaler.scaler.upscale(init_img, scale_factor, upscaler.data_path) + else: + img = init_img + + devices.torch_gc() + + grid = images.split_grid(img, tile_w=p.width, tile_h=p.height, overlap=overlap) + + batch_size = p.batch_size + upscale_count = p.n_iter + p.n_iter = 1 + p.do_not_save_grid = True + p.do_not_save_samples = True + + work = [] + + for _y, _h, row in grid.tiles: + for tiledata in row: + work.append(tiledata[2]) + + batch_count = math.ceil(len(work) / batch_size) + state.job_count = batch_count * upscale_count + + print(f"SD upscaling will process a total of {len(work)} images tiled as {len(grid.tiles[0][2])}x{len(grid.tiles)} per upscale in a total of {state.job_count} batches.") + + result_images = [] + for n in range(upscale_count): + start_seed = seed + n + p.seed = start_seed + + work_results = [] + for i in range(batch_count): + p.batch_size = batch_size + p.init_images = work[i * batch_size:(i + 1) * batch_size] + + state.job = f"Batch {i + 1 + n * batch_count} out of {state.job_count}" + processed = processing.process_images(p) + + if initial_info is None: + initial_info = processed.info + + p.seed = processed.seed + 1 + work_results += processed.images + + image_index = 0 + for _y, _h, row in grid.tiles: + for tiledata in row: + tiledata[2] = work_results[image_index] if image_index < len(work_results) else Image.new("RGB", (p.width, p.height)) + image_index += 1 + + combined_image = images.combine_grid(grid) + result_images.append(combined_image) + + if opts.samples_save: + images.save_image(combined_image, p.outpath_samples, "", start_seed, p.prompt, opts.samples_format, info=initial_info, p=p) + + processed = Processed(p, result_images, seed, initial_info) + + return processed diff --git a/scripts/sd_upscale.py b/scripts/sd_upscale.py new file mode 100644 index 0000000000000000000000000000000000000000..e614c23b987ca47daf822aa91183de0ab58ee960 --- /dev/null +++ b/scripts/sd_upscale.py @@ -0,0 +1,101 @@ +import math + +import modules.scripts as scripts +import gradio as gr +from PIL import Image + +from modules import processing, shared, images, devices +from modules.processing import Processed +from modules.shared import opts, state + + +class Script(scripts.Script): + def title(self): + return "SD upscale" + + def show(self, is_img2img): + return is_img2img + + def ui(self, is_img2img): + info = gr.HTML("

Will upscale the image by the selected scale factor; use width and height sliders to set tile size

") + overlap = gr.Slider(minimum=0, maximum=256, step=16, label='Tile overlap', value=64, elem_id=self.elem_id("overlap")) + scale_factor = gr.Slider(minimum=1.0, maximum=4.0, step=0.05, label='Scale Factor', value=2.0, elem_id=self.elem_id("scale_factor")) + upscaler_index = gr.Radio(label='Upscaler', choices=[x.name for x in shared.sd_upscalers], value=shared.sd_upscalers[0].name, type="index", elem_id=self.elem_id("upscaler_index")) + + return [info, overlap, upscaler_index, scale_factor] + + def run(self, p, _, overlap, upscaler_index, scale_factor): + if isinstance(upscaler_index, str): + upscaler_index = [x.name.lower() for x in shared.sd_upscalers].index(upscaler_index.lower()) + processing.fix_seed(p) + upscaler = shared.sd_upscalers[upscaler_index] + + p.extra_generation_params["SD upscale overlap"] = overlap + p.extra_generation_params["SD upscale upscaler"] = upscaler.name + + initial_info = None + seed = p.seed + + init_img = p.init_images[0] + init_img = images.flatten(init_img, opts.img2img_background_color) + + if upscaler.name != "None": + img = upscaler.scaler.upscale(init_img, scale_factor, upscaler.data_path) + else: + img = init_img + + devices.torch_gc() + + grid = images.split_grid(img, tile_w=p.width, tile_h=p.height, overlap=overlap) + + batch_size = p.batch_size + upscale_count = p.n_iter + p.n_iter = 1 + p.do_not_save_grid = True + p.do_not_save_samples = True + + work = [] + + for _y, _h, row in grid.tiles: + for tiledata in row: + work.append(tiledata[2]) + + batch_count = math.ceil(len(work) / batch_size) + state.job_count = batch_count * upscale_count + + print(f"SD upscaling will process a total of {len(work)} images tiled as {len(grid.tiles[0][2])}x{len(grid.tiles)} per upscale in a total of {state.job_count} batches.") + + result_images = [] + for n in range(upscale_count): + start_seed = seed + n + p.seed = start_seed + + work_results = [] + for i in range(batch_count): + p.batch_size = batch_size + p.init_images = work[i * batch_size:(i + 1) * batch_size] + + state.job = f"Batch {i + 1 + n * batch_count} out of {state.job_count}" + processed = processing.process_images(p) + + if initial_info is None: + initial_info = processed.info + + p.seed = processed.seed + 1 + work_results += processed.images + + image_index = 0 + for _y, _h, row in grid.tiles: + for tiledata in row: + tiledata[2] = work_results[image_index] if image_index < len(work_results) else Image.new("RGB", (p.width, p.height)) + image_index += 1 + + combined_image = images.combine_grid(grid) + result_images.append(combined_image) + + if opts.samples_save: + images.save_image(combined_image, p.outpath_samples, "", start_seed, p.prompt, opts.samples_format, info=initial_info, p=p) + + processed = Processed(p, result_images, seed, initial_info) + + return processed diff --git a/scripts/size_travel.py b/scripts/size_travel.py new file mode 100644 index 0000000000000000000000000000000000000000..ba17c750e7516b3c11bdff0fa2e5e4dbd624c341 --- /dev/null +++ b/scripts/size_travel.py @@ -0,0 +1,293 @@ +import os +import random +from traceback import print_exc +from typing import List, Tuple + +import gradio as gr +import numpy as np +try: from moviepy.editor import concatenate_videoclips, ImageClip +except ImportError: print(f"moviepy python module not installed. Will not be able to generate video.") + +import modules.scripts as scripts +from modules.processing import Processed, process_images, StableDiffusionProcessing, get_fixed_seed +from modules.shared import state +from modules.devices import torch_gc + +DEFAULT_MODE = 'simple' +DEFAULT_STEP = 64 +DEFAULT_SIZE = 512 +DEFAULT_VIDEO_SAVE = True +DEFAULT_VIDEO_FPS = 3 +DEFAULT_VIDEO_CONCAT = 'compose' +DEFAULT_DEBUG = True + +HINT_H_OPTS = '::, e.g.: 512:1024:64' +HINT_W_OPTS = '::, e.g.: 512:1024:64' +HINT_HW_OPTS = ':::::, e.g.: 512:768:768:512:32' + + +def _list_to_int(ls:List[str]): + return [int(x.strip()) for x in ls] + +def hwrange(start, end, step=DEFAULT_STEP): + def _offset(end:int, step:int): + if step > 0: return end + 1 + if step < 0: return end - 1 + + assert start > 0 and end > 0, 'range boundary should be positive' + assert step > 0, 'step size must be postive! (the ascending/descending order is auto inferred from `start` and `end`:)' + + if start > end: step = -step + return list(range(start, _offset(end, step), step)) + +def parse_simple_opts(s:str) -> List[int]: + r = [] + + sect = s.strip() # '::' + if ':' in sect: + segs = _list_to_int(sect.split(':')) + if len(segs) == 2: + start, end = segs[0], segs[1] + r.extend(hwrange(start, end)) + elif len(segs) == 3: + start, end, step = segs[0], segs[1], segs[2] + r.extend(hwrange(start, end, step)) + else: raise ValueError(f'unkonw format for sect {sect}') + else: + r.append(int(sect)) + + return r + +def zip_hw(heights:List[int], widths:List[int]) -> List[Tuple[int, int]]: + if not heights or not widths: return [ ] + + maxlen = max(len(heights), len(widths)) + while len(heights) < maxlen: heights.append(heights[-1]) + while len(widths) < maxlen: widths .append(widths[-1]) + + return [(h, w) for h, w in zip(heights, widths)] + +def parse_advance_opts(s:str) -> List[Tuple[int, int]]: + r = [] + + # replace -1 to current h/w + def _(x, hw): + if x == -1: + if r: return r[-1][hw] + else: return DEFAULT_SIZE + else: return x + def _h(x): return _(x, 0) + def _w(x): return _(x, 1) + + def parse_1_seg(segs): + hw, = segs + r.append((_h(hw), _w(hw))) + + def parse_2_seg(segs): + h, w = segs + r.append((_h(h), _w(w))) + + def parse_3_seg(segs): + hw_start, hw_end, step = segs + hw_start, hw_end = _h(hw_start), _w(hw_end) + r.extend([(hw, hw) for hw in hwrange(hw_start, hw_end, step)]) + + def parse_4_seg(segs): + h_start, h_end, w_start, w_end = segs + h_start, h_end = _h(h_start), _w(h_end) + w_start, w_end = _h(w_start), _w(w_end) + hs = hwrange(h_start, h_end) + ws = hwrange(w_start, w_end) + hws = zip_hw(hs, ws) + r.extend(hws) + + def parse_5_seg(segs): + h_start, h_end, w_start, w_end, step = segs + h_start, h_end = _h(h_start), _w(h_end) + w_start, w_end = _h(w_start), _w(w_end) + hs = hwrange(h_start, h_end, step) + ws = hwrange(w_start, w_end, step) + hws = zip_hw(hs, ws) + r.extend(hws) + + def parse_6_seg(segs): + h_start, h_end, h_step, w_start, w_end, w_step = segs + h_start, h_end = _h(h_start), _w(h_end) + w_start, w_end = _h(w_start), _w(w_end) + hs = hwrange(h_start, h_end, h_step) + ws = hwrange(w_start, w_end, w_step) + hws = zip_hw(hs, ws) + r.extend(hws) + + sects = s.strip().split(',') + for sect in sects: # ':::::' + segs = _list_to_int(sect.strip().split(':')) + locals().get(f'parse_{len(segs)}_seg')(segs) + + if r: # deduplicate + rr = [r[0]] + for hw in r[1:]: + if hw != rr[-1]: + rr.append(hw) + return rr + else: + return r + + +class Script(scripts.Script): + + def title(self): + return 'Size Travel' + + def describe(self): + return "Travel through a series of image sizes and generates a video." + + def show(self, is_img2img): + return True + + def ui(self, is_img2img): + with gr.Row(): + mode = gr.Radio(choices=['simple', 'advance'], value=lambda: DEFAULT_MODE) + + with gr.Row(visible=DEFAULT_MODE=='simple') as tab_simple: + height_opt = gr.Textbox(label='Height Variation', lines=1, placeholder=HINT_H_OPTS) + width_opt = gr.Textbox(label='Width Variation', lines=1, placeholder=HINT_W_OPTS) + + with gr.Row(visible=DEFAULT_MODE=='advance') as tab_advance: + advance_opt = gr.Textbox(label='Height/Width Variation', lines=3, placeholder=HINT_HW_OPTS) + + with gr.Row(): + video_fps = gr.Number(label='Video FPS', value=lambda: DEFAULT_VIDEO_FPS) + video_concat = gr.Radio(label='Video concat method', choices=['compose', 'chain'], value=lambda: DEFAULT_VIDEO_CONCAT) + + show_debug = gr.Checkbox(label='Show verbose debug info at console', value=lambda: DEFAULT_DEBUG) + + def switch_mode(mode): + return [ + { 'visible': mode == 'simple', '__type__': 'update' }, + { 'visible': mode == 'advance', '__type__': 'update' }, + ] + + mode.change(fn=switch_mode, inputs=[mode], outputs=[tab_simple, tab_advance]) + + return [mode, height_opt, width_opt, advance_opt, video_fps, video_concat, show_debug] + + def get_next_sequence_number(path): + from pathlib import Path + """ + Determines and returns the next sequence number to use when saving an image in the specified directory. + The sequence starts at 0. + """ + result = -1 + dir = Path(path) + for file in dir.iterdir(): + if not file.is_dir(): continue + try: + num = int(file.name) + if num > result: result = num + except ValueError: + pass + return result + 1 + + def run(self, p:StableDiffusionProcessing, mode, height_opt, width_opt, advance_opt, video_fps, video_concat, show_debug): + initial_info = None + images = [] + + if mode == 'simple': + if not height_opt or not width_opt: + return Processed(p, images, p.seed, 'run in simple mode but got empty "height_opt" or "width_opt"') + + hs = parse_simple_opts(height_opt) + ws = parse_simple_opts(width_opt) + hws = zip_hw(hs, ws) + elif mode == 'advance': + if not advance_opt: + return Processed(p, images, p.seed, 'run in advance mode, but get empty "advance_opt"') + + hws = parse_advance_opts(advance_opt) + else: + return Processed(p, images, p.seed, f'unknown size_travel mode {mode}') + + if show_debug: print('[size_travel] hws:', hws) + + # Custom seed travel saving + travel_path = os.path.join(p.outpath_samples, 'size_travel') + os.makedirs(travel_path, exist_ok=True) + travel_number = Script.get_next_sequence_number(travel_path) + travel_path = os.path.join(travel_path, f"{travel_number:05}") + p.outpath_samples = travel_path + + # Force Batch Count and Batch Size to 1. + p.n_iter = 1 + p.batch_size = 1 + + # Random unified const seed + p.seed = get_fixed_seed(p.seed) + self.subseed = p.subseed + if show_debug: + print('seed:', p.seed) + print('subseed:', p.subseed) + + # Start job + n_jobs = len(hws) + state.job_count = n_jobs + print(f"Generating {n_jobs} images.") + for h, w in hws: + if state.interrupted: break + torch_gc() + + p.height = h + p.width = w + p.subseed = self.subseed + + try: + proc = process_images(p) + if initial_info is None: initial_info = proc.info + images += proc.images + except: + print(f'>> error gen size ({h}, {w})') + if show_debug: print_exc() + + if video_fps > 0 and len(images) > 1: + try: + imgs = [np.asarray(t) for t in images] + frames = [ImageClip(img, duration=1/video_fps) for img in imgs] + clip = concatenate_videoclips(frames, method=video_concat) # images may have different size + clip.fps = video_fps + clip.write_videofile(os.path.join(travel_path, f"travel-{travel_number:05}.mp4"), verbose=False, audio=False) + except NameError: pass + except: print_exc() + + return Processed(p, images, p.seed, initial_info) + + +if __name__ == '__main__': + # simple mode + assert parse_simple_opts('512:768:32') == [512, 544, 576, 608, 640, 672, 704, 736, 768] + assert parse_simple_opts('768:512:32') == [768, 736, 704, 672, 640, 608, 576, 544, 512] + assert parse_simple_opts('512:768') == [512, 544, 576, 608, 640, 672, 704, 736, 768] + assert parse_simple_opts('512') == [512] + assert parse_simple_opts('512:768:114514') == [512] + + hs = parse_simple_opts('512:768:128') == [512, 640, 768] + ws = parse_simple_opts('512') == [512] + assert zip_hw(hs, ws) == [(512, 512), (640, 512), (768, 512)] + ws = parse_simple_opts('512:768:256') == [512, 768] + assert zip_hw(hs, ws) == [(512, 512), (640, 768), (768, 768)] + + # advance mode + hws = parse_advance_opts('512, 512:512:10, 512:512:512:512:10, 512:512:3:512:512:3') + assert hws == [(512, 512)] + + hws = parse_advance_opts('1:9:2:6:2') + assert hws == [(1, 2), (3, 4), (5, 6), (7, 6), (9, 6)] + + hws = parse_advance_opts('1:3:1:30:10:-10') + assert hws == [(1, 30), (2, 20), (3, 10)] + hws = parse_advance_opts('1:3:1:30:10:-20') + assert hws == [(1, 30), (2, 10), (3, 10)] + + hws = parse_advance_opts('512, 384:384, -1:768:128, 768:512:114514, -1:768:-1:512:128') + assert hws == [(512, 512), (384, 384), (512, 512), (640, 640), (768, 768), (768, 640), (768, 512)] + + print('All tests passed.') diff --git a/scripts/txt2palette.py b/scripts/txt2palette.py new file mode 100644 index 0000000000000000000000000000000000000000..2abf32f95117875abc3a470bdf55812aafe7ad34 --- /dev/null +++ b/scripts/txt2palette.py @@ -0,0 +1,271 @@ +import modules.scripts as scripts +import gradio as gr + +from modules import images +from modules.processing import process_images +from modules.shared import opts +import numpy as np + + +class Script(scripts.Script): + + def title(self): + return "txt2palette" + + def show(self, is_img2img): + return not is_img2img + + def ui(self, is_img2img): + palette_size = gr.Slider(minimum=1, maximum=64, step=1, value=0, + label="Palette size") + method = gr.Radio(choices=['Median cut', 'KMeans'], value='Median cut', label='Palette extraction method') + sort_by = gr.Radio(choices=["luminance", "hue", "saturation", "value", "lightness"], value="luminance", label="Sort colors by") + overwrite = gr.Checkbox(False, label="Overwrite existing files") + return [palette_size, method, sort_by, overwrite] + + def run(self, p, palette_size, method, sort_by, overwrite): + import colorsys + from PIL import Image + try: + from sklearn.cluster import KMeans + except ImportError: + if method == 'KMeans': + print('"sklearn" library is not installed, switching the extraction method to Median cut.') + method = "Median cut" + + class Color: + luminance_weights = np.array([0.2126, 0.7152, 0.0722]) + + def __init__(self, RGB, frequency): + self.rgb = tuple([c for c in RGB]) + self.freq = frequency + + def display(self, w=50, h=50): + """ + Displays the represented color in a w x h window. + :param w: width in pixels + :param h: height in pixels + """ + + img = Image.new("RGB", size=(w, h), color=self.rgb) + img.show() + + def __lt__(self, other): + return self.freq < other.freq + + def get_colors(self, colorspace="rgb"): + """ + Get the color in terms of a colorspace (string). + :param colorspace: rgb/hsv/hls + :return: corresponding color values + """ + colors = {"rgb": self.rgb, "hsv": self.hsv, "hls": self.hls} + return colors[colorspace] + + @property + def hsv(self): + return colorsys.rgb_to_hsv(*self.rgb) + + @property + def hls(self): + return colorsys.rgb_to_hls(*self.rgb) + + @property + def luminance(self): + return np.dot(self.luminance_weights, self.rgb) + + class ColorBox: + """ + Represents a box in the RGB color space, with associated attributes, used in the Median Cut algorithm. + """ + def __init__(self, colors): + """ + Initialize with a numpy array of RGB colors. + :param colors: np.ndarray (width * height, 3) + """ + + self.colors = colors + self._get_min_max() + + def _get_min_max(self): + min_channel = np.min(self.colors, axis=0) + max_channel = np.max(self.colors, axis=0) + + self.min_channel = min_channel + self.max_channel = max_channel + + def __lt__(self, other): + """ + Compare cubes by volume + :param other: + """ + return self.size < other.size + + @property + def size(self): + return self.volume + + def _get_dominant_channel(self): + dominant_channel = np.argmax(self.max_channel - self.min_channel) + return dominant_channel + + @property + def average(self): + """ + Returns the average color contained in ColorBox + :return: [R, G, B] + """ + + return np.mean(self.colors, axis=0) + + @property + def volume(self): + return np.prod( + self.max_channel - self.min_channel, + ) + + def split(self): + """ + Splits the ColorBox into two ColorBoxes at the median of the dominant color channel. + :return: [ColorBox1, ColorBox2] + """ + + # get the color channel with highest range + dominant_channel = self._get_dominant_channel() + + # sorting colors by the dominant channel + self.colors = self.colors[self.colors[:, dominant_channel].argsort()] + + median_index = len(self.colors) // 2 + + return [ + ColorBox(self.colors[:median_index]), + ColorBox(self.colors[median_index:]), + ] + + class Palette: + def __init__(self, colors): + """ + Initializes a color palette with a list of Color objects. + :param colors: a list of Color-objects + """ + + self.colors = colors + self.frequencies = [c.freq for c in colors] + self.number_of_colors = len(colors) + + def get_image(self, w=50, h=50): + img = Image.new("RGB", size=(w * self.number_of_colors, h)) + arr = np.asarray(img).copy() + for i in range(self.number_of_colors): + c = self.colors[i] + arr[:, i * h : (i + 1) * h, :] = c.rgb + img = Image.fromarray(arr, "RGB") + return img + + def k_means_extraction(arr, height, width, palette_size): + """ + Extracts a color palette using KMeans. + :param arr: pixel array (height, width, 3) + :param height: height + :param width: width + :param palette_size: number of colors + :return: a palette of colors sorted by frequency + """ + arr = np.reshape(arr, (width * height, -1)) + model = KMeans(n_clusters=palette_size) + labels = model.fit_predict(arr) + palette = np.array(model.cluster_centers_, dtype=int) + color_count = np.bincount(labels) + color_frequency = color_count / float(np.sum(color_count)) + colors = [] + for color, freq in zip(palette, color_frequency): + colors.append(Color(color, freq)) + return colors + + def median_cut_extraction(arr, height, width, palette_size): + """ + Extracts a color palette using the median cut algorithm. + :param arr: + :param height: + :param width: + :param palette_size: + :return: + """ + arr = arr.reshape((width * height, -1)) + c = [ColorBox(arr)] + full_box_size = c[0].size + # Each iteration, find the largest box, split it, remove original box from list of boxes, and add the two new boxes. + while len(c) < palette_size: + largest_c_idx = np.argmax(c) + # add the two new boxes to the list, while removing the split box. + c = c[:largest_c_idx] + c[largest_c_idx].split() + c[largest_c_idx + 1 :] + colors = [Color(map(int, box.average), box.size / full_box_size) for box in c] + return colors + + sort_methods = { + "luminance": lambda c: c.luminance, + "hue": lambda c: c.hsv[0], + "saturation": lambda c: c.hsv[1], + "value": lambda c: c.hsv[2], + "lightness": lambda c: c.hls[2], + } + + def extract_colors(image, palette_size=5, resize=True, mode="Median cut", sort_mode=None): + """ + Extracts a set of 'palette_size' colors from the given image. + :param image: PIL.Image object of path to Image file + :param palette_size: number of colors to extract + :param resize: whether to resize the image before processing, yielding faster results with lower quality + :param mode: the color quantization algorithm to use. Currently supports K-Means (KM) and Median Cut (MC) + :param sort_mode: sort colors by luminance, or by frequency + :return: a list of the extracted colors + """ + if isinstance(image, Image.Image): + img = image + else: + img = Image.open(image) + img = img.convert("RGB") + if resize: + img = img.resize((256, 256)) + width, height = img.size + arr = np.asarray(img) + + if mode == "KMeans": + colors = k_means_extraction(arr, height, width, palette_size) + elif mode == "Median cut": + colors = median_cut_extraction(arr, height, width, palette_size) + else: + raise NotImplementedError("Extraction mode not implemented!") + + if sort_mode in sort_methods: + colors.sort(key=sort_methods.get(sort_mode), reverse=False) + else: + raise NotImplementedError("Sorting mode not implemented!") + return Palette(colors) + + + if(not overwrite): + basename = f"_palette_{palette_size}x" + else: + p.do_not_save_samples = True + + proc = process_images(p) + + #do not make palettes out of grids + + if len(proc.images) > 1: + iter_offset = 1 + iter_num = len(proc.images) - 1 + else: + iter_offset = 0 + iter_num = 1 + + for i in range(iter_num): + pal = extract_colors(proc.images[i+iter_offset], palette_size=palette_size, sort_mode=sort_by, mode=method) + proc.images[i+iter_offset] = pal.get_image() + + images.save_image(proc.images[i+iter_offset], p.outpath_samples, basename, + proc.seed + i, proc.prompt, opts.samples_format, info= proc.info, p=p) + + return proc \ No newline at end of file diff --git a/scripts/xy_grid.py b/scripts/xy_grid.py new file mode 100644 index 0000000000000000000000000000000000000000..f513fa32e0108f246951c69da5528bdf3edcfde4 --- /dev/null +++ b/scripts/xy_grid.py @@ -0,0 +1,503 @@ +from collections import namedtuple +from copy import copy +from itertools import permutations, chain +import random +import csv +from io import StringIO +from PIL import Image +import numpy as np + +import modules.scripts as scripts +import gradio as gr + +from modules import images +from modules.hypernetworks import hypernetwork +from modules.processing import process_images, Processed, get_correct_sampler, StableDiffusionProcessingTxt2Img +from modules.shared import opts, cmd_opts, state +import modules.shared as shared +import modules.sd_samplers +import modules.sd_models +import re + +def process_axis(opt, vals): + if opt.label == 'Nothing': + return [0] + + valslist = [x.strip() for x in chain.from_iterable( + csv.reader(StringIO(vals)))] + + if opt.type == int: + valslist_ext = [] + + for val in valslist: + m = re_range.fullmatch(val) + mc = re_range_count.fullmatch(val) + if m is not None: + start = int(m.group(1)) + end = int(m.group(2))+1 + step = int(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += list(range(start, end, step)) + elif mc is not None: + start = int(mc.group(1)) + end = int(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += [int(x) for x in np.linspace( + start=start, stop=end, num=num).tolist()] + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == float: + valslist_ext = [] + + for val in valslist: + m = re_range_float.fullmatch(val) + mc = re_range_count_float.fullmatch(val) + if m is not None: + start = float(m.group(1)) + end = float(m.group(2)) + step = float(m.group(3)) if m.group( + 3) is not None else 1 + + valslist_ext += np.arange(start, + end + step, step).tolist() + elif mc is not None: + start = float(mc.group(1)) + end = float(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += np.linspace(start=start, + stop=end, num=num).tolist() + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == str_permutations: + valslist = list(permutations(valslist)) + + valslist = [opt.type(x) for x in valslist] + + return valslist + +def axis_opt_name_find(name): + for option in axis_options: + if option.label.lower()==name.lower(): + return option + print(f"could not find parameter option {name.lower()}") #should I have a lower() here? + return None + +def apply_field(field): + def fun(p, x, xs): + setattr(p, field, x) + + return fun + + +def apply_prompt(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + + +def SR_placeholder(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + if x == xs[0]: + x="" + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + + +def apply_multitool(p, x, xs): + #xs is the strings variable, so it needs to be parsed like this into a list of lists + fields = [] + data = [] + attributes=x.split(" | ") + for attr in attributes: + field, datum = attr.split(": ") + fields.append(field) + option = axis_opt_name_find(field) + if option.type == int: + data.append(int(datum)) + elif option.type == float: + data.append(float(datum)) + else: + data.append(datum) + for ind in range(len(data)): + datalist=[] + for x in xs: #parse xs to get local xs + attrs = x.split(" | ") + for attr in attrs: + field, datapiece = attr.split(": ") + if field == fields[ind] and datapiece not in datalist: + option = axis_opt_name_find(fields[ind]) + if option.type == int: + datalist.append(int(datapiece)) + elif option.type == float: + datalist.append(float(datapiece)) + else: + datalist.append(datapiece) + opt_field = axis_opt_name_find(fields[ind]) + opt_field.apply(p, data[ind], datalist) + #x will be like "Checkpoint name: 1.5 | Sampler: Euler a" + return + + +def parse_multitool(parse_input): + import itertools + data=[] + fields=[] + #parse the typed input (this is the main parser that gets all the values) + splitinput = parse_input.split(" | ") + for param in splitinput: + #param=param[1:-1] + field, datapiece = param.split(": ") + fields.append(field) + data.append(datapiece) + newdata = [] + #parse the subinputs + for ind in range(len(fields)): + field_selected = axis_opt_name_find(fields[ind]) + newdata.append(process_axis(field_selected, data[ind])) + data=newdata + + #find all combinations + results = [] # collect your products + for c in itertools.combinations(data, len(data)): + for res in itertools.product(*c): + results.append(res) + #convert combinations to labels on graph + strings = [] + for result in results: + string = [] + for index in range(len(result)): + string.append(f"{fields[index]}: {result[index]}") + strings.append(" | ".join(string)) + return strings + + +def apply_order(p, x, xs): + token_order = [] + + # Initally grab the tokens from the prompt, so they can be replaced in order of earliest seen + for token in x: + token_order.append((p.prompt.find(token), token)) + + token_order.sort(key=lambda t: t[0]) + + prompt_parts = [] + + # Split the prompt up, taking out the tokens + for _, token in token_order: + n = p.prompt.find(token) + prompt_parts.append(p.prompt[0:n]) + p.prompt = p.prompt[n + len(token):] + + # Rebuild the prompt with the tokens in the order we want + prompt_tmp = "" + for idx, part in enumerate(prompt_parts): + prompt_tmp += part + prompt_tmp += x[idx] + p.prompt = prompt_tmp + p.prompt + + +def build_samplers_dict(p): + samplers_dict = {} + for i, sampler in enumerate(get_correct_sampler(p)): + samplers_dict[sampler.name.lower()] = i + for alias in sampler.aliases: + samplers_dict[alias.lower()] = i + return samplers_dict + + +def apply_sampler(p, x, xs): + sampler_index = build_samplers_dict(p).get(x.lower(), None) + if sampler_index is None: + raise RuntimeError(f"Unknown sampler: {x}") + + p.sampler_index = sampler_index + + +def confirm_samplers(p, xs): + samplers_dict = build_samplers_dict(p) + for x in xs: + if x.lower() not in samplers_dict.keys(): + raise RuntimeError(f"Unknown sampler: {x}") + + +def apply_checkpoint(p, x, xs): + info = modules.sd_models.get_closet_checkpoint_match(x) + if info is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + modules.sd_models.reload_model_weights(shared.sd_model, info) + p.sd_model = shared.sd_model + + +def confirm_checkpoints(p, xs): + for x in xs: + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def apply_hypernetwork(p, x, xs): + if x.lower() in ["", "none"]: + name = None + else: + name = hypernetwork.find_closest_hypernetwork_name(x) + if not name: + raise RuntimeError(f"Unknown hypernetwork: {x}") + hypernetwork.load_hypernetwork(name) + + +def apply_hypernetwork_strength(p, x, xs): + hypernetwork.apply_strength(x) + + +def confirm_hypernetworks(p, xs): + for x in xs: + if x.lower() in ["", "none"]: + continue + if not hypernetwork.find_closest_hypernetwork_name(x): + raise RuntimeError(f"Unknown hypernetwork: {x}") + + +def apply_clip_skip(p, x, xs): + opts.data["CLIP_stop_at_last_layers"] = x + + +def format_value_add_label(p, opt, x): + if type(x) == float: + x = round(x, 8) + + return f"{opt.label}: {x}" + + +def format_value(p, opt, x): + if type(x) == float: + x = round(x, 8) + return x + + +def format_value_join_list(p, opt, x): + return ", ".join(x) + + +def do_nothing(p, x, xs): + pass + + +def format_nothing(p, opt, x): + return "" + + +def str_permutations(x): + """dummy function for specifying it in AxisOption's type when you want to get a list of permutations""" + return x + + +AxisOption = namedtuple("AxisOption", ["label", "type", "apply", "format_value", "confirm"]) +AxisOptionImg2Img = namedtuple("AxisOptionImg2Img", ["label", "type", "apply", "format_value", "confirm"]) + + +axis_options = [ + AxisOption("Nothing", str, do_nothing, format_nothing, None), + AxisOption("Seed", int, apply_field("seed"), format_value_add_label, None), + AxisOption("Var. seed", int, apply_field("subseed"), format_value_add_label, None), + AxisOption("Var. strength", float, apply_field("subseed_strength"), format_value_add_label, None), + AxisOption("Steps", int, apply_field("steps"), format_value_add_label, None), + AxisOption("CFG Scale", float, apply_field("cfg_scale"), format_value_add_label, None), + AxisOption("Prompt S/R", str, apply_prompt, format_value, None), + AxisOption("Prompt S/R Placeholder", str, SR_placeholder, format_value, None), + AxisOption("Prompt order", str_permutations, apply_order, format_value_join_list, None), + AxisOption("Sampler", str, apply_sampler, format_value, confirm_samplers), + AxisOption("Checkpoint name", str, apply_checkpoint, format_value, confirm_checkpoints), + AxisOption("Hypernetwork", str, apply_hypernetwork, format_value, confirm_hypernetworks), + AxisOption("Hypernet str.", float, apply_hypernetwork_strength, format_value_add_label, None), + AxisOption("Sigma Churn", float, apply_field("s_churn"), format_value_add_label, None), + AxisOption("Sigma min", float, apply_field("s_tmin"), format_value_add_label, None), + AxisOption("Sigma max", float, apply_field("s_tmax"), format_value_add_label, None), + AxisOption("Sigma noise", float, apply_field("s_noise"), format_value_add_label, None), + AxisOption("Eta", float, apply_field("eta"), format_value_add_label, None), + AxisOption("Clip skip", int, apply_clip_skip, format_value_add_label, None), + AxisOption("Denoising", float, apply_field("denoising_strength"), format_value_add_label, None), + AxisOption("Multitool", str, apply_multitool, format_value, None) +] + + +def draw_xy_grid(p, xs, ys, x_labels, y_labels, cell, draw_legend, include_lone_images): + ver_texts = [[images.GridAnnotation(y)] for y in y_labels] + hor_texts = [[images.GridAnnotation(x)] for x in x_labels] + + # Temporary list of all the images that are generated to be populated into the grid. + # Will be filled with empty images for any individual step that fails to process properly + image_cache = [] + + processed_result = None + cell_mode = "P" + cell_size = (1,1) + + state.job_count = len(xs) * len(ys) * p.n_iter + + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + state.job = f"{ix + iy * len(xs) + 1} out of {len(xs) * len(ys)}" + + processed:Processed = cell(x, y) + try: + # this dereference will throw an exception if the image was not processed + # (this happens in cases such as if the user stops the process from the UI) + processed_image = processed.images[0] + + if processed_result is None: + # Use our first valid processed result as a template container to hold our full results + processed_result = copy(processed) + cell_mode = processed_image.mode + cell_size = processed_image.size + processed_result.images = [Image.new(cell_mode, cell_size)] + + image_cache.append(processed_image) + if include_lone_images: + processed_result.images.append(processed_image) + processed_result.all_prompts.append(processed.prompt) + processed_result.all_seeds.append(processed.seed) + processed_result.infotexts.append(processed.infotexts[0]) + except: + image_cache.append(Image.new(cell_mode, cell_size)) + + if not processed_result: + print("Unexpected error: draw_xy_grid failed to return even a single processed image") + return Processed() + + grid = images.image_grid(image_cache, rows=len(ys)) + if draw_legend: + grid = images.draw_grid_annotations(grid, cell_size[0], cell_size[1], hor_texts, ver_texts) + + processed_result.images[0] = grid + + return processed_result + + +class SharedSettingsStackHelper(object): + def __enter__(self): + self.CLIP_stop_at_last_layers = opts.CLIP_stop_at_last_layers + self.hypernetwork = opts.sd_hypernetwork + self.model = shared.sd_model + + def __exit__(self, exc_type, exc_value, tb): + modules.sd_models.reload_model_weights(self.model) + + hypernetwork.load_hypernetwork(self.hypernetwork) + hypernetwork.apply_strength() + + opts.data["CLIP_stop_at_last_layers"] = self.CLIP_stop_at_last_layers + + +re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") +re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") + +re_range_count = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[(\d+)\s*\])?\s*") +re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[(\d+(?:.\d*)?)\s*\])?\s*") + +class Script(scripts.Script): + def title(self): + return "X/Y plot" + + def ui(self, is_img2img): + current_axis_options = [x for x in axis_options if type(x) == AxisOption or type(x) == AxisOptionImg2Img and is_img2img] + + with gr.Row(): + x_type = gr.Dropdown(label="X type", choices=[x.label for x in current_axis_options], value=current_axis_options[1].label, visible=False, type="index", elem_id="x_type") + x_values = gr.Textbox(label="X values", visible=False, lines=1) + + with gr.Row(): + y_type = gr.Dropdown(label="Y type", choices=[x.label for x in current_axis_options], value=current_axis_options[0].label, visible=False, type="index", elem_id="y_type") + y_values = gr.Textbox(label="Y values", visible=False, lines=1) + + draw_legend = gr.Checkbox(label='Draw legend', value=True) + include_lone_images = gr.Checkbox(label='Include Separate Images', value=False) + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False) + + return [x_type, x_values, y_type, y_values, draw_legend, include_lone_images, no_fixed_seeds] + + def run(self, p, x_type, x_values, y_type, y_values, draw_legend, include_lone_images, no_fixed_seeds): + if not no_fixed_seeds: + modules.processing.fix_seed(p) + + if not opts.return_grid: + p.batch_size = 1 + + xs = [] + x_opt = axis_options[x_type] + if x_opt.label == "Multitool": + xs = parse_multitool(x_values) + else: + xs = process_axis(x_opt, x_values) + if x_opt.confirm: + x_opt.confirm(p, xs) + ys = [] + y_opt = axis_options[y_type] + if y_opt.label == "Multitool": + ys = parse_multitool(y_values) + else: + ys = process_axis(y_opt, y_values) + if y_opt.confirm: + y_opt.confirm(p, ys) + def fix_axis_seeds(axis_opt, axis_list): + if axis_opt.label in ['Seed','Var. seed']: + return [int(random.randrange(4294967294)) if val is None or val == '' or val == -1 else val for val in axis_list] + else: + return axis_list + + #need to fix this too + if not no_fixed_seeds: + xs = fix_axis_seeds(x_opt, xs) + ys = fix_axis_seeds(y_opt, ys) + + #need to fix steps for multitool + if x_opt.label == 'Steps': + total_steps = sum(xs) * len(ys) + elif y_opt.label == 'Steps': + total_steps = sum(ys) * len(xs) + else: + total_steps = p.steps * len(xs) * len(ys) + + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr: + total_steps *= 2 + + print(f"X/Y plot will create {len(xs) * len(ys) * p.n_iter} images on a {len(xs)}x{len(ys)} grid. (Total steps to process: {total_steps * p.n_iter})") + shared.total_tqdm.updateTotal(total_steps * p.n_iter) + + def cell(x, y): + pc = copy(p) + x_opt.apply(pc, x, xs) + y_opt.apply(pc, y, ys) + + return process_images(pc) + + with SharedSettingsStackHelper(): + processed = draw_xy_grid( + p, + xs=xs, + ys=ys, + x_labels=[x_opt.format_value(p, x_opt, x) for x in xs], + y_labels=[y_opt.format_value(p, y_opt, y) for y in ys], + cell=cell, + draw_legend=draw_legend, + include_lone_images=include_lone_images + ) + infostring=f""" + {p.prompt} + Negative prompt: {p.negative_prompt} + Steps: {p.steps}, + Sampler: {p.sampler}, + CFG scale: {p.cfg_scale}, + Seed: {p.seed} + """ + if opts.grid_save: + images.save_image(processed.images[0], p.outpath_grids, "xy_grid", prompt=p.prompt, seed=processed.seed, grid=True, p=p, info = infostring) + + return processed diff --git a/scripts/xyz_grid-Copy1.py b/scripts/xyz_grid-Copy1.py new file mode 100644 index 0000000000000000000000000000000000000000..6a42a04d9a3768a49d79e2c584f504e20f1187d4 --- /dev/null +++ b/scripts/xyz_grid-Copy1.py @@ -0,0 +1,817 @@ +from collections import namedtuple +from copy import copy +from itertools import permutations, chain +import random +import csv +import os.path +from io import StringIO +from PIL import Image +import numpy as np + +import modules.scripts as scripts +import gradio as gr + +from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_schedulers, errors +from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img +from modules.shared import opts, state +import modules.shared as shared +import modules.sd_samplers +import modules.sd_models +import modules.sd_vae +import re + +from modules.ui_components import ToolButton + +fill_values_symbol = "\U0001f4d2" # 📒 + +AxisInfo = namedtuple('AxisInfo', ['axis', 'values']) + + +def apply_field(field): + def fun(p, x, xs): + setattr(p, field, x) + + return fun + + +def apply_prompt(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + + +def apply_order(p, x, xs): + token_order = [] + + # Initially grab the tokens from the prompt, so they can be replaced in order of earliest seen + for token in x: + token_order.append((p.prompt.find(token), token)) + + token_order.sort(key=lambda t: t[0]) + + prompt_parts = [] + + # Split the prompt up, taking out the tokens + for _, token in token_order: + n = p.prompt.find(token) + prompt_parts.append(p.prompt[0:n]) + p.prompt = p.prompt[n + len(token):] + + # Rebuild the prompt with the tokens in the order we want + prompt_tmp = "" + for idx, part in enumerate(prompt_parts): + prompt_tmp += part + prompt_tmp += x[idx] + p.prompt = prompt_tmp + p.prompt + + +def confirm_samplers(p, xs): + for x in xs: + if x.lower() not in sd_samplers.samplers_map: + raise RuntimeError(f"Unknown sampler: {x}") + + +def apply_checkpoint(p, x, xs): + info = modules.sd_models.get_closet_checkpoint_match(x) + if info is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + p.override_settings['sd_model_checkpoint'] = info.name + + +def confirm_checkpoints(p, xs): + for x in xs: + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def confirm_checkpoints_or_none(p, xs): + for x in xs: + if x in (None, "", "None", "none"): + continue + + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def confirm_range(min_val, max_val, axis_label): + """Generates a AxisOption.confirm() function that checks all values are within the specified range.""" + + def confirm_range_fun(p, xs): + for x in xs: + if not (max_val >= x >= min_val): + raise ValueError(f'{axis_label} value "{x}" out of range [{min_val}, {max_val}]') + + return confirm_range_fun + + +def apply_size(p, x: str, xs) -> None: + try: + width, _, height = x.partition('x') + width = int(width.strip()) + height = int(height.strip()) + p.width = width + p.height = height + except ValueError: + print(f"Invalid size in XYZ plot: {x}") + + +def find_vae(name: str): + if (name := name.strip().lower()) in ('auto', 'automatic'): + return 'Automatic' + elif name == 'none': + return 'None' + return next((k for k in modules.sd_vae.vae_dict if k.lower() == name), print(f'No VAE found for {name}; using Automatic') or 'Automatic') + + +def apply_vae(p, x, xs): + p.override_settings['sd_vae'] = find_vae(x) + + +def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): + p.styles.extend(x.split(',')) + + +def apply_uni_pc_order(p, x, xs): + p.override_settings['uni_pc_order'] = min(x, p.steps - 1) + + +def apply_face_restore(p, opt, x): + opt = opt.lower() + if opt == 'codeformer': + is_active = True + p.face_restoration_model = 'CodeFormer' + elif opt == 'gfpgan': + is_active = True + p.face_restoration_model = 'GFPGAN' + else: + is_active = opt in ('true', 'yes', 'y', '1') + + p.restore_faces = is_active + + +def apply_override(field, boolean: bool = False): + def fun(p, x, xs): + if boolean: + x = True if x.lower() == "true" else False + p.override_settings[field] = x + + return fun + + +def boolean_choice(reverse: bool = False): + def choice(): + return ["False", "True"] if reverse else ["True", "False"] + + return choice + + +def format_value_add_label(p, opt, x): + if type(x) == float: + x = round(x, 8) + + return f"{opt.label}: {x}" + + +def format_value(p, opt, x): + if type(x) == float: + x = round(x, 8) + return x + + +def format_value_join_list(p, opt, x): + return ", ".join(x) + + +def do_nothing(p, x, xs): + pass + + +def format_nothing(p, opt, x): + return "" + + +def format_remove_path(p, opt, x): + return os.path.basename(x) + + +def str_permutations(x): + """dummy function for specifying it in AxisOption's type when you want to get a list of permutations""" + return x + + +def list_to_csv_string(data_list): + with StringIO() as o: + csv.writer(o).writerow(data_list) + return o.getvalue().strip() + + +def csv_string_to_list_strip(data_str): + return list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str), skipinitialspace=True)))) + + +class AxisOption: + def __init__(self, label, type, apply, format_value=format_value_add_label, confirm=None, cost=0.0, choices=None, prepare=None): + self.label = label + self.type = type + self.apply = apply + self.format_value = format_value + self.confirm = confirm + self.cost = cost + self.prepare = prepare + self.choices = choices + + +class AxisOptionImg2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = True + + +class AxisOptionTxt2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = False + + +axis_options = [ + AxisOption("Nothing", str, do_nothing, format_value=format_nothing), + AxisOption("Seed", int, apply_field("seed")), + AxisOption("Var. seed", int, apply_field("subseed")), + AxisOption("Var. strength", float, apply_field("subseed_strength")), + AxisOption("Steps", int, apply_field("steps")), + AxisOptionTxt2Img("Hires steps", int, apply_field("hr_second_pass_steps")), + AxisOption("CFG Scale", float, apply_field("cfg_scale")), + AxisOptionImg2Img("Image CFG Scale", float, apply_field("image_cfg_scale")), + AxisOption("Prompt S/R", str, apply_prompt, format_value=format_value), + AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list), + AxisOptionTxt2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers if x.name not in opts.hide_samplers]), + AxisOptionTxt2Img("Hires sampler", str, apply_field("hr_sampler_name"), confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOptionImg2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_remove_path, confirm=confirm_checkpoints, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Negative Guidance minimum sigma", float, apply_field("s_min_uncond")), + AxisOption("Sigma Churn", float, apply_field("s_churn")), + AxisOption("Sigma min", float, apply_field("s_tmin")), + AxisOption("Sigma max", float, apply_field("s_tmax")), + AxisOption("Sigma noise", float, apply_field("s_noise")), + AxisOption("Schedule type", str, apply_field("scheduler"), choices=lambda: [x.label for x in sd_schedulers.schedulers]), + AxisOption("Schedule min sigma", float, apply_override("sigma_min")), + AxisOption("Schedule max sigma", float, apply_override("sigma_max")), + AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Beta schedule alpha", float, apply_override("beta_dist_alpha")), + AxisOption("Beta schedule beta", float, apply_override("beta_dist_beta")), + AxisOption("Eta", float, apply_field("eta")), + AxisOption("Clip skip", int, apply_override('CLIP_stop_at_last_layers')), + AxisOption("Denoising", float, apply_field("denoising_strength")), + AxisOption("Initial noise multiplier", float, apply_field("initial_noise_multiplier")), + AxisOption("Extra noise", float, apply_override("img2img_extra_noise")), + AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), + AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['Automatic', 'None'] + list(sd_vae.vae_dict)), + AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), + AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), + AxisOption("Face restore", str, apply_face_restore, format_value=format_value), + AxisOption("Token merging ratio", float, apply_override('token_merging_ratio')), + AxisOption("Token merging ratio high-res", float, apply_override('token_merging_ratio_hr')), + AxisOption("Always discard next-to-last sigma", str, apply_override('always_discard_next_to_last_sigma', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("SGM noise multiplier", str, apply_override('sgm_noise_multiplier', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("Refiner checkpoint", str, apply_field('refiner_checkpoint'), format_value=format_remove_path, confirm=confirm_checkpoints_or_none, cost=1.0, choices=lambda: ['None'] + sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Refiner switch at", float, apply_field('refiner_switch_at')), + AxisOption("RNG source", str, apply_override("randn_source"), choices=lambda: ["GPU", "CPU", "NV"]), + AxisOption("FP8 mode", str, apply_override("fp8_storage"), cost=0.9, choices=lambda: ["Disable", "Enable for SDXL", "Enable"]), + AxisOption("Size", str, apply_size), +] + + +def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size): + hor_texts = [[images.GridAnnotation(x)] for x in x_labels] + ver_texts = [[images.GridAnnotation(y)] for y in y_labels] + title_texts = [[images.GridAnnotation(z)] for z in z_labels] + + list_size = (len(xs) * len(ys) * len(zs)) + + processed_result = None + + state.job_count = list_size * p.n_iter + + def process_cell(x, y, z, ix, iy, iz): + nonlocal processed_result + + def index(ix, iy, iz): + return ix + iy * len(xs) + iz * len(xs) * len(ys) + + state.job = f"{index(ix, iy, iz) + 1} out of {list_size}" + + processed: Processed = cell(x, y, z, ix, iy, iz) + + if processed_result is None: + # Use our first processed result object as a template container to hold our full results + processed_result = copy(processed) + processed_result.images = [None] * list_size + processed_result.all_prompts = [None] * list_size + processed_result.all_seeds = [None] * list_size + processed_result.infotexts = [None] * list_size + processed_result.index_of_first_image = 1 + + idx = index(ix, iy, iz) + if processed.images: + # Non-empty list indicates some degree of success. + processed_result.images[idx] = processed.images[0] + processed_result.all_prompts[idx] = processed.prompt + processed_result.all_seeds[idx] = processed.seed + processed_result.infotexts[idx] = processed.infotexts[0] + else: + cell_mode = "P" + cell_size = (processed_result.width, processed_result.height) + if processed_result.images[0] is not None: + cell_mode = processed_result.images[0].mode + # This corrects size in case of batches: + cell_size = processed_result.images[0].size + processed_result.images[idx] = Image.new(cell_mode, cell_size) + + if first_axes_processed == 'x': + for ix, x in enumerate(xs): + if second_axes_processed == 'y': + for iy, y in enumerate(ys): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'y': + for iy, y in enumerate(ys): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'z': + for iz, z in enumerate(zs): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + else: + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + + if not processed_result: + # Should never happen, I've only seen it on one of four open tabs and it needed to refresh. + print("Unexpected error: Processing could not begin, you may need to refresh the tab or restart the service.") + return Processed(p, []) + elif not any(processed_result.images): + print("Unexpected error: draw_xyz_grid failed to return even a single processed image") + return Processed(p, []) + + z_count = len(zs) + + for i in range(z_count): + start_index = (i * len(xs) * len(ys)) + i + end_index = start_index + len(xs) * len(ys) + grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) + if draw_legend: + grid_max_w, grid_max_h = map(max, zip(*(img.size for img in processed_result.images[start_index:end_index]))) + grid = images.draw_grid_annotations(grid, grid_max_w, grid_max_h, hor_texts, ver_texts, margin_size) + processed_result.images.insert(i, grid) + processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) + processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) + processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) + + z_grid = images.image_grid(processed_result.images[:z_count], rows=1) + z_sub_grid_max_w, z_sub_grid_max_h = map(max, zip(*(img.size for img in processed_result.images[:z_count]))) + if draw_legend: + z_grid = images.draw_grid_annotations(z_grid, z_sub_grid_max_w, z_sub_grid_max_h, title_texts, [[images.GridAnnotation()]]) + processed_result.images.insert(0, z_grid) + # TODO: Deeper aspects of the program rely on grid info being misaligned between metadata arrays, which is not ideal. + # processed_result.all_prompts.insert(0, processed_result.all_prompts[0]) + # processed_result.all_seeds.insert(0, processed_result.all_seeds[0]) + processed_result.infotexts.insert(0, processed_result.infotexts[0]) + + return processed_result + + +class SharedSettingsStackHelper(object): + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, tb): + modules.sd_models.reload_model_weights() + modules.sd_vae.reload_vae_weights() + + +re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") +re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") + +re_range_count = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[(\d+)\s*])?\s*") +re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[(\d+(?:.\d*)?)\s*])?\s*") + + +class Script(scripts.Script): + def title(self): + return "X/Y/Z plot" + + def ui(self, is_img2img): + self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] + + with gr.Row(): + with gr.Column(scale=19): + with gr.Row(): + x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type")) + x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values")) + x_values_dropdown = gr.Dropdown(label="X values", visible=False, multiselect=True, interactive=True) + fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False) + + with gr.Row(): + y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type")) + y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values")) + y_values_dropdown = gr.Dropdown(label="Y values", visible=False, multiselect=True, interactive=True) + fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False) + + with gr.Row(): + z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type")) + z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values")) + z_values_dropdown = gr.Dropdown(label="Z values", visible=False, multiselect=True, interactive=True) + fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False) + + with gr.Row(variant="compact", elem_id="axis_options"): + with gr.Column(): + draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + with gr.Row(): + vary_seeds_x = gr.Checkbox(label='Vary seeds for X', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_x"), tooltip="Use different seeds for images along X axis.") + vary_seeds_y = gr.Checkbox(label='Vary seeds for Y', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_y"), tooltip="Use different seeds for images along Y axis.") + vary_seeds_z = gr.Checkbox(label='Vary seeds for Z', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_z"), tooltip="Use different seeds for images along Z axis.") + with gr.Column(): + include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) + include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + csv_mode = gr.Checkbox(label='Use text inputs instead of dropdowns', value=False, elem_id=self.elem_id("csv_mode")) + with gr.Column(): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + + with gr.Row(variant="compact", elem_id="swap_axes"): + swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button") + swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button") + swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button") + + def swap_axes(axis1_type, axis1_values, axis1_values_dropdown, axis2_type, axis2_values, axis2_values_dropdown): + return self.current_axis_options[axis2_type].label, axis2_values, axis2_values_dropdown, self.current_axis_options[axis1_type].label, axis1_values, axis1_values_dropdown + + xy_swap_args = [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown] + swap_xy_axes_button.click(swap_axes, inputs=xy_swap_args, outputs=xy_swap_args) + yz_swap_args = [y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown] + swap_yz_axes_button.click(swap_axes, inputs=yz_swap_args, outputs=yz_swap_args) + xz_swap_args = [x_type, x_values, x_values_dropdown, z_type, z_values, z_values_dropdown] + swap_xz_axes_button.click(swap_axes, inputs=xz_swap_args, outputs=xz_swap_args) + + def fill(axis_type, csv_mode): + axis = self.current_axis_options[axis_type] + if axis.choices: + if csv_mode: + return list_to_csv_string(axis.choices()), gr.update() + else: + return gr.update(), axis.choices() + else: + return gr.update(), gr.update() + + fill_x_button.click(fn=fill, inputs=[x_type, csv_mode], outputs=[x_values, x_values_dropdown]) + fill_y_button.click(fn=fill, inputs=[y_type, csv_mode], outputs=[y_values, y_values_dropdown]) + fill_z_button.click(fn=fill, inputs=[z_type, csv_mode], outputs=[z_values, z_values_dropdown]) + + def select_axis(axis_type, axis_values, axis_values_dropdown, csv_mode): + axis_type = axis_type or 0 # if axle type is None set to 0 + + choices = self.current_axis_options[axis_type].choices + has_choices = choices is not None + + if has_choices: + choices = choices() + if csv_mode: + if axis_values_dropdown: + axis_values = list_to_csv_string(list(filter(lambda x: x in choices, axis_values_dropdown))) + axis_values_dropdown = [] + else: + if axis_values: + axis_values_dropdown = list(filter(lambda x: x in choices, csv_string_to_list_strip(axis_values))) + axis_values = "" + + return (gr.Button.update(visible=has_choices), gr.Textbox.update(visible=not has_choices or csv_mode, value=axis_values), + gr.update(choices=choices if has_choices else None, visible=has_choices and not csv_mode, value=axis_values_dropdown)) + + x_type.change(fn=select_axis, inputs=[x_type, x_values, x_values_dropdown, csv_mode], outputs=[fill_x_button, x_values, x_values_dropdown]) + y_type.change(fn=select_axis, inputs=[y_type, y_values, y_values_dropdown, csv_mode], outputs=[fill_y_button, y_values, y_values_dropdown]) + z_type.change(fn=select_axis, inputs=[z_type, z_values, z_values_dropdown, csv_mode], outputs=[fill_z_button, z_values, z_values_dropdown]) + + def change_choice_mode(csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown): + _fill_x_button, _x_values, _x_values_dropdown = select_axis(x_type, x_values, x_values_dropdown, csv_mode) + _fill_y_button, _y_values, _y_values_dropdown = select_axis(y_type, y_values, y_values_dropdown, csv_mode) + _fill_z_button, _z_values, _z_values_dropdown = select_axis(z_type, z_values, z_values_dropdown, csv_mode) + return _fill_x_button, _x_values, _x_values_dropdown, _fill_y_button, _y_values, _y_values_dropdown, _fill_z_button, _z_values, _z_values_dropdown + + csv_mode.change(fn=change_choice_mode, inputs=[csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown], outputs=[fill_x_button, x_values, x_values_dropdown, fill_y_button, y_values, y_values_dropdown, fill_z_button, z_values, z_values_dropdown]) + + def get_dropdown_update_from_params(axis, params): + val_key = f"{axis} Values" + vals = params.get(val_key, "") + valslist = csv_string_to_list_strip(vals) + return gr.update(value=valslist) + + self.infotext_fields = ( + (x_type, "X Type"), + (x_values, "X Values"), + (x_values_dropdown, lambda params: get_dropdown_update_from_params("X", params)), + (y_type, "Y Type"), + (y_values, "Y Values"), + (y_values_dropdown, lambda params: get_dropdown_update_from_params("Y", params)), + (z_type, "Z Type"), + (z_values, "Z Values"), + (z_values_dropdown, lambda params: get_dropdown_update_from_params("Z", params)), + ) + + return [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode] + + def run(self, p, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode): + x_type, y_type, z_type = x_type or 0, y_type or 0, z_type or 0 # if axle type is None set to 0 + + if not no_fixed_seeds: + modules.processing.fix_seed(p) + + if not opts.return_grid: + p.batch_size = 1 + + def process_axis(opt, vals, vals_dropdown): + if opt.label == 'Nothing': + return [0] + + if opt.choices is not None and not csv_mode: + valslist = vals_dropdown + elif opt.prepare is not None: + valslist = opt.prepare(vals) + else: + valslist = csv_string_to_list_strip(vals) + + if opt.type == int: + valslist_ext = [] + + for val in valslist: + if val.strip() == '': + continue + m = re_range.fullmatch(val) + mc = re_range_count.fullmatch(val) + if m is not None: + start = int(m.group(1)) + end = int(m.group(2)) + 1 + step = int(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += list(range(start, end, step)) + elif mc is not None: + start = int(mc.group(1)) + end = int(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += [int(x) for x in np.linspace(start=start, stop=end, num=num).tolist()] + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == float: + valslist_ext = [] + + for val in valslist: + if val.strip() == '': + continue + m = re_range_float.fullmatch(val) + mc = re_range_count_float.fullmatch(val) + if m is not None: + start = float(m.group(1)) + end = float(m.group(2)) + step = float(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += np.arange(start, end + step, step).tolist() + elif mc is not None: + start = float(mc.group(1)) + end = float(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += np.linspace(start=start, stop=end, num=num).tolist() + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == str_permutations: + valslist = list(permutations(valslist)) + + valslist = [opt.type(x) for x in valslist] + + # Confirm options are valid before starting + if opt.confirm: + opt.confirm(p, valslist) + + return valslist + + x_opt = self.current_axis_options[x_type] + if x_opt.choices is not None and not csv_mode: + x_values = list_to_csv_string(x_values_dropdown) + xs = process_axis(x_opt, x_values, x_values_dropdown) + + y_opt = self.current_axis_options[y_type] + if y_opt.choices is not None and not csv_mode: + y_values = list_to_csv_string(y_values_dropdown) + ys = process_axis(y_opt, y_values, y_values_dropdown) + + z_opt = self.current_axis_options[z_type] + if z_opt.choices is not None and not csv_mode: + z_values = list_to_csv_string(z_values_dropdown) + zs = process_axis(z_opt, z_values, z_values_dropdown) + + # this could be moved to common code, but unlikely to be ever triggered anywhere else + Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes + grid_mp = round(len(xs) * len(ys) * len(zs) * p.width * p.height / 1000000) + assert grid_mp < opts.img_max_size_mp, f'Error: Resulting grid would be too large ({grid_mp} MPixels) (max configured size is {opts.img_max_size_mp} MPixels)' + + def fix_axis_seeds(axis_opt, axis_list): + if axis_opt.label in ['Seed', 'Var. seed']: + return [int(random.randrange(4294967294)) if val is None or val == '' or val == -1 else val for val in axis_list] + else: + return axis_list + + if not no_fixed_seeds: + xs = fix_axis_seeds(x_opt, xs) + ys = fix_axis_seeds(y_opt, ys) + zs = fix_axis_seeds(z_opt, zs) + + if x_opt.label == 'Steps': + total_steps = sum(xs) * len(ys) * len(zs) + elif y_opt.label == 'Steps': + total_steps = sum(ys) * len(xs) * len(zs) + elif z_opt.label == 'Steps': + total_steps = sum(zs) * len(xs) * len(ys) + else: + total_steps = p.steps * len(xs) * len(ys) * len(zs) + + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr: + if x_opt.label == "Hires steps": + total_steps += sum(xs) * len(ys) * len(zs) + elif y_opt.label == "Hires steps": + total_steps += sum(ys) * len(xs) * len(zs) + elif z_opt.label == "Hires steps": + total_steps += sum(zs) * len(xs) * len(ys) + elif p.hr_second_pass_steps: + total_steps += p.hr_second_pass_steps * len(xs) * len(ys) * len(zs) + else: + total_steps *= 2 + + total_steps *= p.n_iter + + image_cell_count = p.n_iter * p.batch_size + cell_console_text = f"; {image_cell_count} images per cell" if image_cell_count > 1 else "" + plural_s = 's' if len(zs) > 1 else '' + print(f"X/Y/Z plot will create {len(xs) * len(ys) * len(zs) * image_cell_count} images on {len(zs)} {len(xs)}x{len(ys)} grid{plural_s}{cell_console_text}. (Total steps to process: {total_steps})") + shared.total_tqdm.updateTotal(total_steps) + + state.xyz_plot_x = AxisInfo(x_opt, xs) + state.xyz_plot_y = AxisInfo(y_opt, ys) + state.xyz_plot_z = AxisInfo(z_opt, zs) + + # If one of the axes is very slow to change between (like SD model + # checkpoint), then make sure it is in the outer iteration of the nested + # `for` loop. + first_axes_processed = 'z' + second_axes_processed = 'y' + if x_opt.cost > y_opt.cost and x_opt.cost > z_opt.cost: + first_axes_processed = 'x' + if y_opt.cost > z_opt.cost: + second_axes_processed = 'y' + else: + second_axes_processed = 'z' + elif y_opt.cost > x_opt.cost and y_opt.cost > z_opt.cost: + first_axes_processed = 'y' + if x_opt.cost > z_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'z' + elif z_opt.cost > x_opt.cost and z_opt.cost > y_opt.cost: + first_axes_processed = 'z' + if x_opt.cost > y_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'y' + + grid_infotext = [None] * (1 + len(zs)) + + def cell(x, y, z, ix, iy, iz): + if shared.state.interrupted or state.stopping_generation: + return Processed(p, [], p.seed, "") + + pc = copy(p) + pc.styles = pc.styles[:] + x_opt.apply(pc, x, xs) + y_opt.apply(pc, y, ys) + z_opt.apply(pc, z, zs) + + xdim = len(xs) if vary_seeds_x else 1 + ydim = len(ys) if vary_seeds_y else 1 + + if vary_seeds_x: + pc.seed += ix + if vary_seeds_y: + pc.seed += iy * xdim + if vary_seeds_z: + pc.seed += iz * xdim * ydim + + try: + res = process_images(pc) + except Exception as e: + errors.display(e, "generating image for xyz plot") + + res = Processed(p, [], p.seed, "") + + # Sets subgrid infotexts + subgrid_index = 1 + iz + if grid_infotext[subgrid_index] is None and ix == 0 and iy == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + pc.extra_generation_params['Script'] = self.title() + + if x_opt.label != 'Nothing': + pc.extra_generation_params["X Type"] = x_opt.label + pc.extra_generation_params["X Values"] = x_values + if x_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed X Values"] = ", ".join([str(x) for x in xs]) + + if y_opt.label != 'Nothing': + pc.extra_generation_params["Y Type"] = y_opt.label + pc.extra_generation_params["Y Values"] = y_values + if y_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) + + grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + # Sets main grid infotext + if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + + if z_opt.label != 'Nothing': + pc.extra_generation_params["Z Type"] = z_opt.label + pc.extra_generation_params["Z Values"] = z_values + if z_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) + + grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + return res + + with SharedSettingsStackHelper(): + processed = draw_xyz_grid( + p, + xs=xs, + ys=ys, + zs=zs, + x_labels=[x_opt.format_value(p, x_opt, x) for x in xs], + y_labels=[y_opt.format_value(p, y_opt, y) for y in ys], + z_labels=[z_opt.format_value(p, z_opt, z) for z in zs], + cell=cell, + draw_legend=draw_legend, + include_lone_images=include_lone_images, + include_sub_grids=include_sub_grids, + first_axes_processed=first_axes_processed, + second_axes_processed=second_axes_processed, + margin_size=margin_size + ) + + if not processed.images: + # It broke, no further handling needed. + return processed + + z_count = len(zs) + + # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) + processed.infotexts[:1 + z_count] = grid_infotext[:1 + z_count] + + if not include_lone_images: + # Don't need sub-images anymore, drop from list: + processed.images = processed.images[:z_count + 1] + + if opts.grid_save: + # Auto-save main and sub-grids: + grid_count = z_count + 1 if z_count > 1 else 1 + for g in range(grid_count): + # TODO: See previous comment about intentional data misalignment. + adj_g = g - 1 if g > 0 else g + images.save_image(processed.images[g], p.outpath_grids, "xyz_grid", info=processed.infotexts[g], extension=opts.grid_format, prompt=processed.all_prompts[adj_g], seed=processed.all_seeds[adj_g], grid=True, p=processed) + if not include_sub_grids: # if not include_sub_grids then skip saving after the first grid + break + + if not include_sub_grids: + # Done with sub-grids, drop all related information: + for _ in range(z_count): + del processed.images[1] + del processed.all_prompts[1] + del processed.all_seeds[1] + del processed.infotexts[1] + + return processed diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py new file mode 100644 index 0000000000000000000000000000000000000000..6a42a04d9a3768a49d79e2c584f504e20f1187d4 --- /dev/null +++ b/scripts/xyz_grid.py @@ -0,0 +1,817 @@ +from collections import namedtuple +from copy import copy +from itertools import permutations, chain +import random +import csv +import os.path +from io import StringIO +from PIL import Image +import numpy as np + +import modules.scripts as scripts +import gradio as gr + +from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_schedulers, errors +from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img +from modules.shared import opts, state +import modules.shared as shared +import modules.sd_samplers +import modules.sd_models +import modules.sd_vae +import re + +from modules.ui_components import ToolButton + +fill_values_symbol = "\U0001f4d2" # 📒 + +AxisInfo = namedtuple('AxisInfo', ['axis', 'values']) + + +def apply_field(field): + def fun(p, x, xs): + setattr(p, field, x) + + return fun + + +def apply_prompt(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + + +def apply_order(p, x, xs): + token_order = [] + + # Initially grab the tokens from the prompt, so they can be replaced in order of earliest seen + for token in x: + token_order.append((p.prompt.find(token), token)) + + token_order.sort(key=lambda t: t[0]) + + prompt_parts = [] + + # Split the prompt up, taking out the tokens + for _, token in token_order: + n = p.prompt.find(token) + prompt_parts.append(p.prompt[0:n]) + p.prompt = p.prompt[n + len(token):] + + # Rebuild the prompt with the tokens in the order we want + prompt_tmp = "" + for idx, part in enumerate(prompt_parts): + prompt_tmp += part + prompt_tmp += x[idx] + p.prompt = prompt_tmp + p.prompt + + +def confirm_samplers(p, xs): + for x in xs: + if x.lower() not in sd_samplers.samplers_map: + raise RuntimeError(f"Unknown sampler: {x}") + + +def apply_checkpoint(p, x, xs): + info = modules.sd_models.get_closet_checkpoint_match(x) + if info is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + p.override_settings['sd_model_checkpoint'] = info.name + + +def confirm_checkpoints(p, xs): + for x in xs: + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def confirm_checkpoints_or_none(p, xs): + for x in xs: + if x in (None, "", "None", "none"): + continue + + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def confirm_range(min_val, max_val, axis_label): + """Generates a AxisOption.confirm() function that checks all values are within the specified range.""" + + def confirm_range_fun(p, xs): + for x in xs: + if not (max_val >= x >= min_val): + raise ValueError(f'{axis_label} value "{x}" out of range [{min_val}, {max_val}]') + + return confirm_range_fun + + +def apply_size(p, x: str, xs) -> None: + try: + width, _, height = x.partition('x') + width = int(width.strip()) + height = int(height.strip()) + p.width = width + p.height = height + except ValueError: + print(f"Invalid size in XYZ plot: {x}") + + +def find_vae(name: str): + if (name := name.strip().lower()) in ('auto', 'automatic'): + return 'Automatic' + elif name == 'none': + return 'None' + return next((k for k in modules.sd_vae.vae_dict if k.lower() == name), print(f'No VAE found for {name}; using Automatic') or 'Automatic') + + +def apply_vae(p, x, xs): + p.override_settings['sd_vae'] = find_vae(x) + + +def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): + p.styles.extend(x.split(',')) + + +def apply_uni_pc_order(p, x, xs): + p.override_settings['uni_pc_order'] = min(x, p.steps - 1) + + +def apply_face_restore(p, opt, x): + opt = opt.lower() + if opt == 'codeformer': + is_active = True + p.face_restoration_model = 'CodeFormer' + elif opt == 'gfpgan': + is_active = True + p.face_restoration_model = 'GFPGAN' + else: + is_active = opt in ('true', 'yes', 'y', '1') + + p.restore_faces = is_active + + +def apply_override(field, boolean: bool = False): + def fun(p, x, xs): + if boolean: + x = True if x.lower() == "true" else False + p.override_settings[field] = x + + return fun + + +def boolean_choice(reverse: bool = False): + def choice(): + return ["False", "True"] if reverse else ["True", "False"] + + return choice + + +def format_value_add_label(p, opt, x): + if type(x) == float: + x = round(x, 8) + + return f"{opt.label}: {x}" + + +def format_value(p, opt, x): + if type(x) == float: + x = round(x, 8) + return x + + +def format_value_join_list(p, opt, x): + return ", ".join(x) + + +def do_nothing(p, x, xs): + pass + + +def format_nothing(p, opt, x): + return "" + + +def format_remove_path(p, opt, x): + return os.path.basename(x) + + +def str_permutations(x): + """dummy function for specifying it in AxisOption's type when you want to get a list of permutations""" + return x + + +def list_to_csv_string(data_list): + with StringIO() as o: + csv.writer(o).writerow(data_list) + return o.getvalue().strip() + + +def csv_string_to_list_strip(data_str): + return list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str), skipinitialspace=True)))) + + +class AxisOption: + def __init__(self, label, type, apply, format_value=format_value_add_label, confirm=None, cost=0.0, choices=None, prepare=None): + self.label = label + self.type = type + self.apply = apply + self.format_value = format_value + self.confirm = confirm + self.cost = cost + self.prepare = prepare + self.choices = choices + + +class AxisOptionImg2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = True + + +class AxisOptionTxt2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = False + + +axis_options = [ + AxisOption("Nothing", str, do_nothing, format_value=format_nothing), + AxisOption("Seed", int, apply_field("seed")), + AxisOption("Var. seed", int, apply_field("subseed")), + AxisOption("Var. strength", float, apply_field("subseed_strength")), + AxisOption("Steps", int, apply_field("steps")), + AxisOptionTxt2Img("Hires steps", int, apply_field("hr_second_pass_steps")), + AxisOption("CFG Scale", float, apply_field("cfg_scale")), + AxisOptionImg2Img("Image CFG Scale", float, apply_field("image_cfg_scale")), + AxisOption("Prompt S/R", str, apply_prompt, format_value=format_value), + AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list), + AxisOptionTxt2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers if x.name not in opts.hide_samplers]), + AxisOptionTxt2Img("Hires sampler", str, apply_field("hr_sampler_name"), confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOptionImg2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_remove_path, confirm=confirm_checkpoints, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Negative Guidance minimum sigma", float, apply_field("s_min_uncond")), + AxisOption("Sigma Churn", float, apply_field("s_churn")), + AxisOption("Sigma min", float, apply_field("s_tmin")), + AxisOption("Sigma max", float, apply_field("s_tmax")), + AxisOption("Sigma noise", float, apply_field("s_noise")), + AxisOption("Schedule type", str, apply_field("scheduler"), choices=lambda: [x.label for x in sd_schedulers.schedulers]), + AxisOption("Schedule min sigma", float, apply_override("sigma_min")), + AxisOption("Schedule max sigma", float, apply_override("sigma_max")), + AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Beta schedule alpha", float, apply_override("beta_dist_alpha")), + AxisOption("Beta schedule beta", float, apply_override("beta_dist_beta")), + AxisOption("Eta", float, apply_field("eta")), + AxisOption("Clip skip", int, apply_override('CLIP_stop_at_last_layers')), + AxisOption("Denoising", float, apply_field("denoising_strength")), + AxisOption("Initial noise multiplier", float, apply_field("initial_noise_multiplier")), + AxisOption("Extra noise", float, apply_override("img2img_extra_noise")), + AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), + AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['Automatic', 'None'] + list(sd_vae.vae_dict)), + AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), + AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), + AxisOption("Face restore", str, apply_face_restore, format_value=format_value), + AxisOption("Token merging ratio", float, apply_override('token_merging_ratio')), + AxisOption("Token merging ratio high-res", float, apply_override('token_merging_ratio_hr')), + AxisOption("Always discard next-to-last sigma", str, apply_override('always_discard_next_to_last_sigma', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("SGM noise multiplier", str, apply_override('sgm_noise_multiplier', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("Refiner checkpoint", str, apply_field('refiner_checkpoint'), format_value=format_remove_path, confirm=confirm_checkpoints_or_none, cost=1.0, choices=lambda: ['None'] + sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Refiner switch at", float, apply_field('refiner_switch_at')), + AxisOption("RNG source", str, apply_override("randn_source"), choices=lambda: ["GPU", "CPU", "NV"]), + AxisOption("FP8 mode", str, apply_override("fp8_storage"), cost=0.9, choices=lambda: ["Disable", "Enable for SDXL", "Enable"]), + AxisOption("Size", str, apply_size), +] + + +def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size): + hor_texts = [[images.GridAnnotation(x)] for x in x_labels] + ver_texts = [[images.GridAnnotation(y)] for y in y_labels] + title_texts = [[images.GridAnnotation(z)] for z in z_labels] + + list_size = (len(xs) * len(ys) * len(zs)) + + processed_result = None + + state.job_count = list_size * p.n_iter + + def process_cell(x, y, z, ix, iy, iz): + nonlocal processed_result + + def index(ix, iy, iz): + return ix + iy * len(xs) + iz * len(xs) * len(ys) + + state.job = f"{index(ix, iy, iz) + 1} out of {list_size}" + + processed: Processed = cell(x, y, z, ix, iy, iz) + + if processed_result is None: + # Use our first processed result object as a template container to hold our full results + processed_result = copy(processed) + processed_result.images = [None] * list_size + processed_result.all_prompts = [None] * list_size + processed_result.all_seeds = [None] * list_size + processed_result.infotexts = [None] * list_size + processed_result.index_of_first_image = 1 + + idx = index(ix, iy, iz) + if processed.images: + # Non-empty list indicates some degree of success. + processed_result.images[idx] = processed.images[0] + processed_result.all_prompts[idx] = processed.prompt + processed_result.all_seeds[idx] = processed.seed + processed_result.infotexts[idx] = processed.infotexts[0] + else: + cell_mode = "P" + cell_size = (processed_result.width, processed_result.height) + if processed_result.images[0] is not None: + cell_mode = processed_result.images[0].mode + # This corrects size in case of batches: + cell_size = processed_result.images[0].size + processed_result.images[idx] = Image.new(cell_mode, cell_size) + + if first_axes_processed == 'x': + for ix, x in enumerate(xs): + if second_axes_processed == 'y': + for iy, y in enumerate(ys): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'y': + for iy, y in enumerate(ys): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'z': + for iz, z in enumerate(zs): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + else: + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + + if not processed_result: + # Should never happen, I've only seen it on one of four open tabs and it needed to refresh. + print("Unexpected error: Processing could not begin, you may need to refresh the tab or restart the service.") + return Processed(p, []) + elif not any(processed_result.images): + print("Unexpected error: draw_xyz_grid failed to return even a single processed image") + return Processed(p, []) + + z_count = len(zs) + + for i in range(z_count): + start_index = (i * len(xs) * len(ys)) + i + end_index = start_index + len(xs) * len(ys) + grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) + if draw_legend: + grid_max_w, grid_max_h = map(max, zip(*(img.size for img in processed_result.images[start_index:end_index]))) + grid = images.draw_grid_annotations(grid, grid_max_w, grid_max_h, hor_texts, ver_texts, margin_size) + processed_result.images.insert(i, grid) + processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) + processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) + processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) + + z_grid = images.image_grid(processed_result.images[:z_count], rows=1) + z_sub_grid_max_w, z_sub_grid_max_h = map(max, zip(*(img.size for img in processed_result.images[:z_count]))) + if draw_legend: + z_grid = images.draw_grid_annotations(z_grid, z_sub_grid_max_w, z_sub_grid_max_h, title_texts, [[images.GridAnnotation()]]) + processed_result.images.insert(0, z_grid) + # TODO: Deeper aspects of the program rely on grid info being misaligned between metadata arrays, which is not ideal. + # processed_result.all_prompts.insert(0, processed_result.all_prompts[0]) + # processed_result.all_seeds.insert(0, processed_result.all_seeds[0]) + processed_result.infotexts.insert(0, processed_result.infotexts[0]) + + return processed_result + + +class SharedSettingsStackHelper(object): + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, tb): + modules.sd_models.reload_model_weights() + modules.sd_vae.reload_vae_weights() + + +re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") +re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") + +re_range_count = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[(\d+)\s*])?\s*") +re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[(\d+(?:.\d*)?)\s*])?\s*") + + +class Script(scripts.Script): + def title(self): + return "X/Y/Z plot" + + def ui(self, is_img2img): + self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] + + with gr.Row(): + with gr.Column(scale=19): + with gr.Row(): + x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type")) + x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values")) + x_values_dropdown = gr.Dropdown(label="X values", visible=False, multiselect=True, interactive=True) + fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False) + + with gr.Row(): + y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type")) + y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values")) + y_values_dropdown = gr.Dropdown(label="Y values", visible=False, multiselect=True, interactive=True) + fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False) + + with gr.Row(): + z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type")) + z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values")) + z_values_dropdown = gr.Dropdown(label="Z values", visible=False, multiselect=True, interactive=True) + fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False) + + with gr.Row(variant="compact", elem_id="axis_options"): + with gr.Column(): + draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + with gr.Row(): + vary_seeds_x = gr.Checkbox(label='Vary seeds for X', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_x"), tooltip="Use different seeds for images along X axis.") + vary_seeds_y = gr.Checkbox(label='Vary seeds for Y', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_y"), tooltip="Use different seeds for images along Y axis.") + vary_seeds_z = gr.Checkbox(label='Vary seeds for Z', value=False, min_width=80, elem_id=self.elem_id("vary_seeds_z"), tooltip="Use different seeds for images along Z axis.") + with gr.Column(): + include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) + include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + csv_mode = gr.Checkbox(label='Use text inputs instead of dropdowns', value=False, elem_id=self.elem_id("csv_mode")) + with gr.Column(): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + + with gr.Row(variant="compact", elem_id="swap_axes"): + swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button") + swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button") + swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button") + + def swap_axes(axis1_type, axis1_values, axis1_values_dropdown, axis2_type, axis2_values, axis2_values_dropdown): + return self.current_axis_options[axis2_type].label, axis2_values, axis2_values_dropdown, self.current_axis_options[axis1_type].label, axis1_values, axis1_values_dropdown + + xy_swap_args = [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown] + swap_xy_axes_button.click(swap_axes, inputs=xy_swap_args, outputs=xy_swap_args) + yz_swap_args = [y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown] + swap_yz_axes_button.click(swap_axes, inputs=yz_swap_args, outputs=yz_swap_args) + xz_swap_args = [x_type, x_values, x_values_dropdown, z_type, z_values, z_values_dropdown] + swap_xz_axes_button.click(swap_axes, inputs=xz_swap_args, outputs=xz_swap_args) + + def fill(axis_type, csv_mode): + axis = self.current_axis_options[axis_type] + if axis.choices: + if csv_mode: + return list_to_csv_string(axis.choices()), gr.update() + else: + return gr.update(), axis.choices() + else: + return gr.update(), gr.update() + + fill_x_button.click(fn=fill, inputs=[x_type, csv_mode], outputs=[x_values, x_values_dropdown]) + fill_y_button.click(fn=fill, inputs=[y_type, csv_mode], outputs=[y_values, y_values_dropdown]) + fill_z_button.click(fn=fill, inputs=[z_type, csv_mode], outputs=[z_values, z_values_dropdown]) + + def select_axis(axis_type, axis_values, axis_values_dropdown, csv_mode): + axis_type = axis_type or 0 # if axle type is None set to 0 + + choices = self.current_axis_options[axis_type].choices + has_choices = choices is not None + + if has_choices: + choices = choices() + if csv_mode: + if axis_values_dropdown: + axis_values = list_to_csv_string(list(filter(lambda x: x in choices, axis_values_dropdown))) + axis_values_dropdown = [] + else: + if axis_values: + axis_values_dropdown = list(filter(lambda x: x in choices, csv_string_to_list_strip(axis_values))) + axis_values = "" + + return (gr.Button.update(visible=has_choices), gr.Textbox.update(visible=not has_choices or csv_mode, value=axis_values), + gr.update(choices=choices if has_choices else None, visible=has_choices and not csv_mode, value=axis_values_dropdown)) + + x_type.change(fn=select_axis, inputs=[x_type, x_values, x_values_dropdown, csv_mode], outputs=[fill_x_button, x_values, x_values_dropdown]) + y_type.change(fn=select_axis, inputs=[y_type, y_values, y_values_dropdown, csv_mode], outputs=[fill_y_button, y_values, y_values_dropdown]) + z_type.change(fn=select_axis, inputs=[z_type, z_values, z_values_dropdown, csv_mode], outputs=[fill_z_button, z_values, z_values_dropdown]) + + def change_choice_mode(csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown): + _fill_x_button, _x_values, _x_values_dropdown = select_axis(x_type, x_values, x_values_dropdown, csv_mode) + _fill_y_button, _y_values, _y_values_dropdown = select_axis(y_type, y_values, y_values_dropdown, csv_mode) + _fill_z_button, _z_values, _z_values_dropdown = select_axis(z_type, z_values, z_values_dropdown, csv_mode) + return _fill_x_button, _x_values, _x_values_dropdown, _fill_y_button, _y_values, _y_values_dropdown, _fill_z_button, _z_values, _z_values_dropdown + + csv_mode.change(fn=change_choice_mode, inputs=[csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown], outputs=[fill_x_button, x_values, x_values_dropdown, fill_y_button, y_values, y_values_dropdown, fill_z_button, z_values, z_values_dropdown]) + + def get_dropdown_update_from_params(axis, params): + val_key = f"{axis} Values" + vals = params.get(val_key, "") + valslist = csv_string_to_list_strip(vals) + return gr.update(value=valslist) + + self.infotext_fields = ( + (x_type, "X Type"), + (x_values, "X Values"), + (x_values_dropdown, lambda params: get_dropdown_update_from_params("X", params)), + (y_type, "Y Type"), + (y_values, "Y Values"), + (y_values_dropdown, lambda params: get_dropdown_update_from_params("Y", params)), + (z_type, "Z Type"), + (z_values, "Z Values"), + (z_values_dropdown, lambda params: get_dropdown_update_from_params("Z", params)), + ) + + return [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode] + + def run(self, p, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, vary_seeds_x, vary_seeds_y, vary_seeds_z, margin_size, csv_mode): + x_type, y_type, z_type = x_type or 0, y_type or 0, z_type or 0 # if axle type is None set to 0 + + if not no_fixed_seeds: + modules.processing.fix_seed(p) + + if not opts.return_grid: + p.batch_size = 1 + + def process_axis(opt, vals, vals_dropdown): + if opt.label == 'Nothing': + return [0] + + if opt.choices is not None and not csv_mode: + valslist = vals_dropdown + elif opt.prepare is not None: + valslist = opt.prepare(vals) + else: + valslist = csv_string_to_list_strip(vals) + + if opt.type == int: + valslist_ext = [] + + for val in valslist: + if val.strip() == '': + continue + m = re_range.fullmatch(val) + mc = re_range_count.fullmatch(val) + if m is not None: + start = int(m.group(1)) + end = int(m.group(2)) + 1 + step = int(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += list(range(start, end, step)) + elif mc is not None: + start = int(mc.group(1)) + end = int(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += [int(x) for x in np.linspace(start=start, stop=end, num=num).tolist()] + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == float: + valslist_ext = [] + + for val in valslist: + if val.strip() == '': + continue + m = re_range_float.fullmatch(val) + mc = re_range_count_float.fullmatch(val) + if m is not None: + start = float(m.group(1)) + end = float(m.group(2)) + step = float(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += np.arange(start, end + step, step).tolist() + elif mc is not None: + start = float(mc.group(1)) + end = float(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += np.linspace(start=start, stop=end, num=num).tolist() + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == str_permutations: + valslist = list(permutations(valslist)) + + valslist = [opt.type(x) for x in valslist] + + # Confirm options are valid before starting + if opt.confirm: + opt.confirm(p, valslist) + + return valslist + + x_opt = self.current_axis_options[x_type] + if x_opt.choices is not None and not csv_mode: + x_values = list_to_csv_string(x_values_dropdown) + xs = process_axis(x_opt, x_values, x_values_dropdown) + + y_opt = self.current_axis_options[y_type] + if y_opt.choices is not None and not csv_mode: + y_values = list_to_csv_string(y_values_dropdown) + ys = process_axis(y_opt, y_values, y_values_dropdown) + + z_opt = self.current_axis_options[z_type] + if z_opt.choices is not None and not csv_mode: + z_values = list_to_csv_string(z_values_dropdown) + zs = process_axis(z_opt, z_values, z_values_dropdown) + + # this could be moved to common code, but unlikely to be ever triggered anywhere else + Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes + grid_mp = round(len(xs) * len(ys) * len(zs) * p.width * p.height / 1000000) + assert grid_mp < opts.img_max_size_mp, f'Error: Resulting grid would be too large ({grid_mp} MPixels) (max configured size is {opts.img_max_size_mp} MPixels)' + + def fix_axis_seeds(axis_opt, axis_list): + if axis_opt.label in ['Seed', 'Var. seed']: + return [int(random.randrange(4294967294)) if val is None or val == '' or val == -1 else val for val in axis_list] + else: + return axis_list + + if not no_fixed_seeds: + xs = fix_axis_seeds(x_opt, xs) + ys = fix_axis_seeds(y_opt, ys) + zs = fix_axis_seeds(z_opt, zs) + + if x_opt.label == 'Steps': + total_steps = sum(xs) * len(ys) * len(zs) + elif y_opt.label == 'Steps': + total_steps = sum(ys) * len(xs) * len(zs) + elif z_opt.label == 'Steps': + total_steps = sum(zs) * len(xs) * len(ys) + else: + total_steps = p.steps * len(xs) * len(ys) * len(zs) + + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr: + if x_opt.label == "Hires steps": + total_steps += sum(xs) * len(ys) * len(zs) + elif y_opt.label == "Hires steps": + total_steps += sum(ys) * len(xs) * len(zs) + elif z_opt.label == "Hires steps": + total_steps += sum(zs) * len(xs) * len(ys) + elif p.hr_second_pass_steps: + total_steps += p.hr_second_pass_steps * len(xs) * len(ys) * len(zs) + else: + total_steps *= 2 + + total_steps *= p.n_iter + + image_cell_count = p.n_iter * p.batch_size + cell_console_text = f"; {image_cell_count} images per cell" if image_cell_count > 1 else "" + plural_s = 's' if len(zs) > 1 else '' + print(f"X/Y/Z plot will create {len(xs) * len(ys) * len(zs) * image_cell_count} images on {len(zs)} {len(xs)}x{len(ys)} grid{plural_s}{cell_console_text}. (Total steps to process: {total_steps})") + shared.total_tqdm.updateTotal(total_steps) + + state.xyz_plot_x = AxisInfo(x_opt, xs) + state.xyz_plot_y = AxisInfo(y_opt, ys) + state.xyz_plot_z = AxisInfo(z_opt, zs) + + # If one of the axes is very slow to change between (like SD model + # checkpoint), then make sure it is in the outer iteration of the nested + # `for` loop. + first_axes_processed = 'z' + second_axes_processed = 'y' + if x_opt.cost > y_opt.cost and x_opt.cost > z_opt.cost: + first_axes_processed = 'x' + if y_opt.cost > z_opt.cost: + second_axes_processed = 'y' + else: + second_axes_processed = 'z' + elif y_opt.cost > x_opt.cost and y_opt.cost > z_opt.cost: + first_axes_processed = 'y' + if x_opt.cost > z_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'z' + elif z_opt.cost > x_opt.cost and z_opt.cost > y_opt.cost: + first_axes_processed = 'z' + if x_opt.cost > y_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'y' + + grid_infotext = [None] * (1 + len(zs)) + + def cell(x, y, z, ix, iy, iz): + if shared.state.interrupted or state.stopping_generation: + return Processed(p, [], p.seed, "") + + pc = copy(p) + pc.styles = pc.styles[:] + x_opt.apply(pc, x, xs) + y_opt.apply(pc, y, ys) + z_opt.apply(pc, z, zs) + + xdim = len(xs) if vary_seeds_x else 1 + ydim = len(ys) if vary_seeds_y else 1 + + if vary_seeds_x: + pc.seed += ix + if vary_seeds_y: + pc.seed += iy * xdim + if vary_seeds_z: + pc.seed += iz * xdim * ydim + + try: + res = process_images(pc) + except Exception as e: + errors.display(e, "generating image for xyz plot") + + res = Processed(p, [], p.seed, "") + + # Sets subgrid infotexts + subgrid_index = 1 + iz + if grid_infotext[subgrid_index] is None and ix == 0 and iy == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + pc.extra_generation_params['Script'] = self.title() + + if x_opt.label != 'Nothing': + pc.extra_generation_params["X Type"] = x_opt.label + pc.extra_generation_params["X Values"] = x_values + if x_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed X Values"] = ", ".join([str(x) for x in xs]) + + if y_opt.label != 'Nothing': + pc.extra_generation_params["Y Type"] = y_opt.label + pc.extra_generation_params["Y Values"] = y_values + if y_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) + + grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + # Sets main grid infotext + if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + + if z_opt.label != 'Nothing': + pc.extra_generation_params["Z Type"] = z_opt.label + pc.extra_generation_params["Z Values"] = z_values + if z_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) + + grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + return res + + with SharedSettingsStackHelper(): + processed = draw_xyz_grid( + p, + xs=xs, + ys=ys, + zs=zs, + x_labels=[x_opt.format_value(p, x_opt, x) for x in xs], + y_labels=[y_opt.format_value(p, y_opt, y) for y in ys], + z_labels=[z_opt.format_value(p, z_opt, z) for z in zs], + cell=cell, + draw_legend=draw_legend, + include_lone_images=include_lone_images, + include_sub_grids=include_sub_grids, + first_axes_processed=first_axes_processed, + second_axes_processed=second_axes_processed, + margin_size=margin_size + ) + + if not processed.images: + # It broke, no further handling needed. + return processed + + z_count = len(zs) + + # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) + processed.infotexts[:1 + z_count] = grid_infotext[:1 + z_count] + + if not include_lone_images: + # Don't need sub-images anymore, drop from list: + processed.images = processed.images[:z_count + 1] + + if opts.grid_save: + # Auto-save main and sub-grids: + grid_count = z_count + 1 if z_count > 1 else 1 + for g in range(grid_count): + # TODO: See previous comment about intentional data misalignment. + adj_g = g - 1 if g > 0 else g + images.save_image(processed.images[g], p.outpath_grids, "xyz_grid", info=processed.infotexts[g], extension=opts.grid_format, prompt=processed.all_prompts[adj_g], seed=processed.all_seeds[adj_g], grid=True, p=processed) + if not include_sub_grids: # if not include_sub_grids then skip saving after the first grid + break + + if not include_sub_grids: + # Done with sub-grids, drop all related information: + for _ in range(z_count): + del processed.images[1] + del processed.all_prompts[1] + del processed.all_seeds[1] + del processed.infotexts[1] + + return processed diff --git a/scripts/xyz_grid_s.py b/scripts/xyz_grid_s.py new file mode 100644 index 0000000000000000000000000000000000000000..f062027b0e8f64ce5488d058f9251327bc122d2e --- /dev/null +++ b/scripts/xyz_grid_s.py @@ -0,0 +1,852 @@ +from collections import namedtuple +from copy import copy +from itertools import permutations, chain +import random +import csv +import os.path +from io import StringIO +from PIL import Image +import numpy as np +import subprocess +import json + +import modules.scripts as scripts +import gradio as gr + +from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_samplers_kdiffusion, errors +from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img +from modules.shared import opts, state +import modules.shared as shared +import modules.sd_samplers +import modules.sd_models +import modules.sd_vae +import re + +from modules.ui_components import ToolButton + +fill_values_symbol = "\U0001f4d2" # 📒 + +AxisInfo = namedtuple('AxisInfo', ['axis', 'values']) + + +def apply_field(field): + def fun(p, x, xs): + setattr(p, field, x) + + return fun + + +def apply_prompt(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + + +def apply_order(p, x, xs): + token_order = [] + + # Initally grab the tokens from the prompt, so they can be replaced in order of earliest seen + for token in x: + token_order.append((p.prompt.find(token), token)) + + token_order.sort(key=lambda t: t[0]) + + prompt_parts = [] + + # Split the prompt up, taking out the tokens + for _, token in token_order: + n = p.prompt.find(token) + prompt_parts.append(p.prompt[0:n]) + p.prompt = p.prompt[n + len(token):] + + # Rebuild the prompt with the tokens in the order we want + prompt_tmp = "" + for idx, part in enumerate(prompt_parts): + prompt_tmp += part + prompt_tmp += x[idx] + p.prompt = prompt_tmp + p.prompt + + +def confirm_samplers(p, xs): + for x in xs: + if x.lower() not in sd_samplers.samplers_map: + raise RuntimeError(f"Unknown sampler: {x}") + + +def apply_checkpoint(p, x, xs): + info = modules.sd_models.get_closet_checkpoint_match(x) + if info is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + p.override_settings['sd_model_checkpoint'] = info.name + + +def confirm_checkpoints(p, xs): + for x in xs: + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def confirm_checkpoints_or_none(p, xs): + for x in xs: + if x in (None, "", "None", "none"): + continue + + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + + +def apply_clip_skip(p, x, xs): + opts.data["CLIP_stop_at_last_layers"] = x + + +def apply_upscale_latent_space(p, x, xs): + if x.lower().strip() != '0': + opts.data["use_scale_latent_for_hires_fix"] = True + else: + opts.data["use_scale_latent_for_hires_fix"] = False + + +def find_vae(name: str): + if name.lower() in ['auto', 'automatic']: + return modules.sd_vae.unspecified + if name.lower() == 'none': + return None + else: + choices = [x for x in sorted(modules.sd_vae.vae_dict, key=lambda x: len(x)) if name.lower().strip() in x.lower()] + if len(choices) == 0: + print(f"No VAE found for {name}; using automatic") + return modules.sd_vae.unspecified + else: + return modules.sd_vae.vae_dict[choices[0]] + + +def apply_vae(p, x, xs): + modules.sd_vae.reload_vae_weights(shared.sd_model, vae_file=find_vae(x)) + + +def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): + p.styles.extend(x.split(',')) + + +def apply_uni_pc_order(p, x, xs): + opts.data["uni_pc_order"] = min(x, p.steps - 1) + + +def apply_face_restore(p, opt, x): + opt = opt.lower() + if opt == 'codeformer': + is_active = True + p.face_restoration_model = 'CodeFormer' + elif opt == 'gfpgan': + is_active = True + p.face_restoration_model = 'GFPGAN' + else: + is_active = opt in ('true', 'yes', 'y', '1') + + p.restore_faces = is_active + + +def apply_override(field, boolean: bool = False): + def fun(p, x, xs): + if boolean: + x = True if x.lower() == "true" else False + p.override_settings[field] = x + return fun + + +def boolean_choice(reverse: bool = False): + def choice(): + return ["False", "True"] if reverse else ["True", "False"] + return choice + + +def format_value_add_label(p, opt, x): + if type(x) == float: + x = round(x, 8) + + return f"{opt.label}: {x}" + + +def format_value(p, opt, x): + if type(x) == float: + x = round(x, 8) + return x + + +def format_value_join_list(p, opt, x): + return ", ".join(x) + + +def do_nothing(p, x, xs): + pass + + +def format_nothing(p, opt, x): + return "" + + +def format_remove_path(p, opt, x): + return os.path.basename(x) + + +def str_permutations(x): + """dummy function for specifying it in AxisOption's type when you want to get a list of permutations""" + return x + + +def list_to_csv_string(data_list): + with StringIO() as o: + csv.writer(o).writerow(data_list) + return o.getvalue().strip() + + +def csv_string_to_list_strip(data_str): + return list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str))))) + + +class AxisOption: + def __init__(self, label, type, apply, format_value=format_value_add_label, confirm=None, cost=0.0, choices=None): + self.label = label + self.type = type + self.apply = apply + self.format_value = format_value + self.confirm = confirm + self.cost = cost + self.choices = choices + + +class AxisOptionImg2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = True + + +class AxisOptionTxt2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = False + + +axis_options = [ + AxisOption("Nothing", str, do_nothing, format_value=format_nothing), + AxisOption("Seed", int, apply_field("seed")), + AxisOption("Var. seed", int, apply_field("subseed")), + AxisOption("Var. strength", float, apply_field("subseed_strength")), + AxisOption("Steps", int, apply_field("steps")), + AxisOptionTxt2Img("Hires steps", int, apply_field("hr_second_pass_steps")), + AxisOption("CFG Scale", float, apply_field("cfg_scale")), + AxisOptionImg2Img("Image CFG Scale", float, apply_field("image_cfg_scale")), + AxisOption("Prompt S/R", str, apply_prompt, format_value=format_value), + AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list), + AxisOptionTxt2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers if x.name not in opts.hide_samplers]), + AxisOptionTxt2Img("Hires sampler", str, apply_field("hr_sampler_name"), confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOptionImg2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_remove_path, confirm=confirm_checkpoints, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Negative Guidance minimum sigma", float, apply_field("s_min_uncond")), + AxisOption("Sigma Churn", float, apply_field("s_churn")), + AxisOption("Sigma min", float, apply_field("s_tmin")), + AxisOption("Sigma max", float, apply_field("s_tmax")), + AxisOption("Sigma noise", float, apply_field("s_noise")), + AxisOption("Schedule type", str, apply_override("k_sched_type"), choices=lambda: list(sd_samplers_kdiffusion.k_diffusion_scheduler)), + AxisOption("Schedule min sigma", float, apply_override("sigma_min")), + AxisOption("Schedule max sigma", float, apply_override("sigma_max")), + AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Eta", float, apply_field("eta")), + AxisOption("Clip skip", int, apply_clip_skip), + AxisOption("Denoising", float, apply_field("denoising_strength")), + AxisOption("Initial noise multiplier", float, apply_field("initial_noise_multiplier")), + AxisOption("Extra noise", float, apply_override("img2img_extra_noise")), + AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), + AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['None'] + list(sd_vae.vae_dict)), + AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), + AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), + AxisOption("Face restore", str, apply_face_restore, format_value=format_value), + AxisOption("Token merging ratio", float, apply_override('token_merging_ratio')), + AxisOption("Token merging ratio high-res", float, apply_override('token_merging_ratio_hr')), + AxisOption("Always discard next-to-last sigma", str, apply_override('always_discard_next_to_last_sigma', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("SGM noise multiplier", str, apply_override('sgm_noise_multiplier', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("Refiner checkpoint", str, apply_field('refiner_checkpoint'), format_value=format_remove_path, confirm=confirm_checkpoints_or_none, cost=1.0, choices=lambda: ['None'] + sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Refiner switch at", float, apply_field('refiner_switch_at')), + AxisOption("RNG source", str, apply_override("randn_source"), choices=lambda: ["GPU", "CPU", "NV"]), +] + + +def draw_xyz_grid(p, xs, ys, zs, x_labels, y_labels, z_labels, cell, draw_legend, include_lone_images, include_sub_grids, first_axes_processed, second_axes_processed, margin_size): + hor_texts = [[images.GridAnnotation(x)] for x in x_labels] + ver_texts = [[images.GridAnnotation(y)] for y in y_labels] + title_texts = [[images.GridAnnotation(z)] for z in z_labels] + + list_size = (len(xs) * len(ys) * len(zs)) + + processed_result = None + + state.job_count = list_size * p.n_iter + + def process_cell(x, y, z, ix, iy, iz): + nonlocal processed_result + + def index(ix, iy, iz): + return ix + iy * len(xs) + iz * len(xs) * len(ys) + + state.job = f"{index(ix, iy, iz) + 1} out of {list_size}" + + processed: Processed = cell(x, y, z, ix, iy, iz) + + if processed_result is None: + # Use our first processed result object as a template container to hold our full results + processed_result = copy(processed) + processed_result.images = [None] * list_size + processed_result.all_prompts = [None] * list_size + processed_result.all_seeds = [None] * list_size + processed_result.infotexts = [None] * list_size + processed_result.index_of_first_image = 1 + + idx = index(ix, iy, iz) + if processed.images: + # Non-empty list indicates some degree of success. + processed_result.images[idx] = processed.images[0] + processed_result.all_prompts[idx] = processed.prompt + processed_result.all_seeds[idx] = processed.seed + processed_result.infotexts[idx] = processed.infotexts[0] + else: + cell_mode = "P" + cell_size = (processed_result.width, processed_result.height) + if processed_result.images[0] is not None: + cell_mode = processed_result.images[0].mode + # This corrects size in case of batches: + cell_size = processed_result.images[0].size + processed_result.images[idx] = Image.new(cell_mode, cell_size) + + if first_axes_processed == 'x': + for ix, x in enumerate(xs): + if second_axes_processed == 'y': + for iy, y in enumerate(ys): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'y': + for iy, y in enumerate(ys): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iz, z in enumerate(zs): + process_cell(x, y, z, ix, iy, iz) + else: + for iz, z in enumerate(zs): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + elif first_axes_processed == 'z': + for iz, z in enumerate(zs): + if second_axes_processed == 'x': + for ix, x in enumerate(xs): + for iy, y in enumerate(ys): + process_cell(x, y, z, ix, iy, iz) + else: + for iy, y in enumerate(ys): + for ix, x in enumerate(xs): + process_cell(x, y, z, ix, iy, iz) + + if not processed_result: + # Should never happen, I've only seen it on one of four open tabs and it needed to refresh. + print("Unexpected error: Processing could not begin, you may need to refresh the tab or restart the service.") + return Processed(p, []) + elif not any(processed_result.images): + print("Unexpected error: draw_xyz_grid failed to return even a single processed image") + return Processed(p, []) + + z_count = len(zs) + + for i in range(z_count): + start_index = (i * len(xs) * len(ys)) + i + end_index = start_index + len(xs) * len(ys) + grid = images.image_grid(processed_result.images[start_index:end_index], rows=len(ys)) + if draw_legend: + grid = images.draw_grid_annotations(grid, processed_result.images[start_index].size[0], processed_result.images[start_index].size[1], hor_texts, ver_texts, margin_size, divide = p.batch_size) + processed_result.images.insert(i, grid) + processed_result.all_prompts.insert(i, processed_result.all_prompts[start_index]) + processed_result.all_seeds.insert(i, processed_result.all_seeds[start_index]) + processed_result.infotexts.insert(i, processed_result.infotexts[start_index]) + + sub_grid_size = processed_result.images[0].size + z_grid = images.image_grid(processed_result.images[:z_count], rows=1) + if draw_legend: + z_grid = images.draw_grid_annotations(z_grid, sub_grid_size[0], sub_grid_size[1], title_texts, [[images.GridAnnotation()]]) + processed_result.images.insert(0, z_grid) + # TODO: Deeper aspects of the program rely on grid info being misaligned between metadata arrays, which is not ideal. + # processed_result.all_prompts.insert(0, processed_result.all_prompts[0]) + # processed_result.all_seeds.insert(0, processed_result.all_seeds[0]) + processed_result.infotexts.insert(0, processed_result.infotexts[0]) + + return processed_result + + +class SharedSettingsStackHelper(object): + def __enter__(self): + self.CLIP_stop_at_last_layers = opts.CLIP_stop_at_last_layers + self.vae = opts.sd_vae + self.uni_pc_order = opts.uni_pc_order + + def __exit__(self, exc_type, exc_value, tb): + opts.data["sd_vae"] = self.vae + opts.data["uni_pc_order"] = self.uni_pc_order + modules.sd_models.reload_model_weights() + modules.sd_vae.reload_vae_weights() + + opts.data["CLIP_stop_at_last_layers"] = self.CLIP_stop_at_last_layers + + +re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") +re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") + +re_range_count = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[(\d+)\s*])?\s*") +re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[(\d+(?:.\d*)?)\s*])?\s*") + + +class Script(scripts.Script): + def title(self): + return "X/Y/Z plot S" + + def ui(self, is_img2img): + self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] + xyzpath = xyzpath = os.path.join(os.getcwd(),"xyzpresets.json") + + def getxyzpresetlist(): + try: + with open(xyzpath, 'r') as file: + data = json.load(file) + except FileNotFoundError: + data = {} + return list(data.keys()) + + with gr.Row(): + with gr.Column(scale=19): + with gr.Row(): + x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type")) + x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values")) + x_values_dropdown = gr.Dropdown(label="X values", visible=False, multiselect=True, interactive=True) + fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False) + + with gr.Row(): + y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type")) + y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values")) + y_values_dropdown = gr.Dropdown(label="Y values", visible=False, multiselect=True, interactive=True) + fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False) + + with gr.Row(): + z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type")) + z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values")) + z_values_dropdown = gr.Dropdown(label="Z values", visible=False, multiselect=True, interactive=True) + fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False) + + with gr.Row(variant="compact", elem_id="axis_options"): + with gr.Column(): + draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend")) + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + with gr.Column(): + include_lone_images = gr.Checkbox(label='Include Sub Images', value=False, elem_id=self.elem_id("include_lone_images")) + include_sub_grids = gr.Checkbox(label='Include Sub Grids', value=False, elem_id=self.elem_id("include_sub_grids")) + with gr.Column(): + margin_size = gr.Slider(label="Grid margins (px)", minimum=0, maximum=500, value=0, step=2, elem_id=self.elem_id("margin_size")) + with gr.Column(): + csv_mode = gr.Checkbox(label='Use text inputs instead of dropdowns', value=False, elem_id=self.elem_id("csv_mode")) + + with gr.Row(variant="compact", elem_id="swap_axes"): + swap_xy_axes_button = gr.Button(value="Swap X/Y axes", elem_id="xy_grid_swap_axes_button") + swap_yz_axes_button = gr.Button(value="Swap Y/Z axes", elem_id="yz_grid_swap_axes_button") + swap_xz_axes_button = gr.Button(value="Swap X/Z axes", elem_id="xz_grid_swap_axes_button") + + with gr.Row(): + xyzpresetname = gr.Textbox(label="Preset Name") + xyzmode = gr.Radio(label="Set Mode", choices=["Save", "Overwrite", "Delete"],value = "Save") + xyzpresets = gr.Dropdown(label="Presets",choices=getxyzpresetlist()) + + with gr.Row(): + savexyzpreset_b = gr.Button(value="Set XYZ Plot as Preset",variant='primary') + openxyzpreset = gr.Button(value="Open XYZ Preset file",variant='primary') + loadxyzpreset_b = gr.Button(value="Load XYZ Plot",variant='primary') + + openxyzpreset.click(fn=lambda:subprocess.Popen(['start', xyzpath], shell=True)) + + def swap_axes(axis1_type, axis1_values, axis1_values_dropdown, axis2_type, axis2_values, axis2_values_dropdown): + return self.current_axis_options[axis2_type].label, axis2_values, axis2_values_dropdown, self.current_axis_options[axis1_type].label, axis1_values, axis1_values_dropdown + + xy_swap_args = [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown] + swap_xy_axes_button.click(swap_axes, inputs=xy_swap_args, outputs=xy_swap_args) + yz_swap_args = [y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown] + swap_yz_axes_button.click(swap_axes, inputs=yz_swap_args, outputs=yz_swap_args) + xz_swap_args = [x_type, x_values, x_values_dropdown, z_type, z_values, z_values_dropdown] + swap_xz_axes_button.click(swap_axes, inputs=xz_swap_args, outputs=xz_swap_args) + + def fill(axis_type, csv_mode): + axis = self.current_axis_options[axis_type] + if axis.choices: + if csv_mode: + return list_to_csv_string(axis.choices()), gr.update() + else: + return gr.update(), axis.choices() + else: + return gr.update(), gr.update() + + fill_x_button.click(fn=fill, inputs=[x_type, csv_mode], outputs=[x_values, x_values_dropdown]) + fill_y_button.click(fn=fill, inputs=[y_type, csv_mode], outputs=[y_values, y_values_dropdown]) + fill_z_button.click(fn=fill, inputs=[z_type, csv_mode], outputs=[z_values, z_values_dropdown]) + + def select_axis(axis_type, axis_values, axis_values_dropdown, csv_mode): + choices = self.current_axis_options[axis_type].choices + has_choices = choices is not None + + if has_choices: + choices = choices() + if csv_mode: + if axis_values_dropdown: + axis_values = list_to_csv_string(list(filter(lambda x: x in choices, axis_values_dropdown))) + axis_values_dropdown = [] + else: + if axis_values: + axis_values_dropdown = list(filter(lambda x: x in choices, csv_string_to_list_strip(axis_values))) + axis_values = "" + + return (gr.Button.update(visible=has_choices), gr.Textbox.update(visible=not has_choices or csv_mode, value=axis_values), + gr.update(choices=choices if has_choices else None, visible=has_choices and not csv_mode, value=axis_values_dropdown)) + + x_type.change(fn=select_axis, inputs=[x_type, x_values, x_values_dropdown, csv_mode], outputs=[fill_x_button, x_values, x_values_dropdown]) + y_type.change(fn=select_axis, inputs=[y_type, y_values, y_values_dropdown, csv_mode], outputs=[fill_y_button, y_values, y_values_dropdown]) + z_type.change(fn=select_axis, inputs=[z_type, z_values, z_values_dropdown, csv_mode], outputs=[fill_z_button, z_values, z_values_dropdown]) + + def change_choice_mode(csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown): + _fill_x_button, _x_values, _x_values_dropdown = select_axis(x_type, x_values, x_values_dropdown, csv_mode) + _fill_y_button, _y_values, _y_values_dropdown = select_axis(y_type, y_values, y_values_dropdown, csv_mode) + _fill_z_button, _z_values, _z_values_dropdown = select_axis(z_type, z_values, z_values_dropdown, csv_mode) + return _fill_x_button, _x_values, _x_values_dropdown, _fill_y_button, _y_values, _y_values_dropdown, _fill_z_button, _z_values, _z_values_dropdown + + csv_mode.change(fn=change_choice_mode, inputs=[csv_mode, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown], outputs=[fill_x_button, x_values, x_values_dropdown, fill_y_button, y_values, y_values_dropdown, fill_z_button, z_values, z_values_dropdown]) + + def get_dropdown_update_from_params(axis, params): + val_key = f"{axis} Values" + vals = params.get(val_key, "") + valslist = csv_string_to_list_strip(vals) + return gr.update(value=valslist) + + self.infotext_fields = ( + (x_type, "X Type"), + (x_values, "X Values"), + (x_values_dropdown, lambda params: get_dropdown_update_from_params("X", params)), + (y_type, "Y Type"), + (y_values, "Y Values"), + (y_values_dropdown, lambda params: get_dropdown_update_from_params("Y", params)), + (z_type, "Z Type"), + (z_values, "Z Values"), + (z_values_dropdown, lambda params: get_dropdown_update_from_params("Z", params)), + ) + + def savexyzpreset_f(x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, name, mode): + new_data = {"xtype": self.current_axis_options[x_type].label, "xvalues": x_values,"xvalues2": x_values_dropdown, + "ytype": self.current_axis_options[y_type].label, "yvalues": y_values,"yvalues2": y_values_dropdown, + "ztype": self.current_axis_options[z_type].label, "zvalues": z_values,"zvalues2": z_values_dropdown + } + try: + with open(xyzpath, 'r') as file: + data = json.load(file) + except FileNotFoundError: + data = {} + + if "Delete" == mode: + if name in data:del data[name] + elif "Save" == mode: + if name in data: + print(f"Preset name {name} already exists.") + else: + data[name] = new_data + else: + data[name] = new_data + + with open(xyzpath, 'w') as file: + json.dump(data, file, indent=4) + + return gr.update(choices = list(data.keys())) + + def loadxyzpreset_f(name): + try: + with open(xyzpath, 'r') as file: + data = json.load(file) + except FileNotFoundError: + return None + + preset_data = data.get(name) + if not preset_data: + return [gr.update(value = x) for x in ["Seed","",[],"Nothing","",[],"Nothing","",[]]] + + sets = ["xtype","xvalues","xvalues2","ytype","yvalues","yvalues2","ztype","zvalues","zvalues2"] + + return [gr.update(value = preset_data.get(x)) for x in sets] + + savexyzpreset_b.click(fn=savexyzpreset_f,inputs=[x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown,xyzpresetname,xyzmode],outputs=[xyzpresets]) + loadxyzpreset_b.click(fn=loadxyzpreset_f,inputs=[xyzpresets],outputs=[x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown]) + + return [x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size, csv_mode] + + def run(self, p, x_type, x_values, x_values_dropdown, y_type, y_values, y_values_dropdown, z_type, z_values, z_values_dropdown, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size, csv_mode): + if not no_fixed_seeds: + modules.processing.fix_seed(p) + + if not opts.return_grid: + p.batch_size = 1 + + def process_axis(opt, vals, vals_dropdown): + if opt.label == 'Nothing': + return [0] + + if opt.choices is not None and not csv_mode: + valslist = vals_dropdown + else: + valslist = csv_string_to_list_strip(vals) + + if opt.type == int: + valslist_ext = [] + + for val in valslist: + m = re_range.fullmatch(val) + mc = re_range_count.fullmatch(val) + if m is not None: + start = int(m.group(1)) + end = int(m.group(2))+1 + step = int(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += list(range(start, end, step)) + elif mc is not None: + start = int(mc.group(1)) + end = int(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += [int(x) for x in np.linspace(start=start, stop=end, num=num).tolist()] + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == float: + valslist_ext = [] + + for val in valslist: + m = re_range_float.fullmatch(val) + mc = re_range_count_float.fullmatch(val) + if m is not None: + start = float(m.group(1)) + end = float(m.group(2)) + step = float(m.group(3)) if m.group(3) is not None else 1 + + valslist_ext += np.arange(start, end + step, step).tolist() + elif mc is not None: + start = float(mc.group(1)) + end = float(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + + valslist_ext += np.linspace(start=start, stop=end, num=num).tolist() + else: + valslist_ext.append(val) + + valslist = valslist_ext + elif opt.type == str_permutations: + valslist = list(permutations(valslist)) + + valslist = [opt.type(x) for x in valslist] + + # Confirm options are valid before starting + if opt.confirm: + opt.confirm(p, valslist) + + return valslist + + x_opt = self.current_axis_options[x_type] + if x_opt.choices is not None and not csv_mode: + x_values = list_to_csv_string(x_values_dropdown) + xs = process_axis(x_opt, x_values, x_values_dropdown) + + y_opt = self.current_axis_options[y_type] + if y_opt.choices is not None and not csv_mode: + y_values = list_to_csv_string(y_values_dropdown) + ys = process_axis(y_opt, y_values, y_values_dropdown) + + z_opt = self.current_axis_options[z_type] + if z_opt.choices is not None and not csv_mode: + z_values = list_to_csv_string(z_values_dropdown) + zs = process_axis(z_opt, z_values, z_values_dropdown) + + # this could be moved to common code, but unlikely to be ever triggered anywhere else + Image.MAX_IMAGE_PIXELS = None # disable check in Pillow and rely on check below to allow large custom image sizes + grid_mp = round(len(xs) * len(ys) * len(zs) * p.width * p.height / 1000000) + assert grid_mp < opts.img_max_size_mp, f'Error: Resulting grid would be too large ({grid_mp} MPixels) (max configured size is {opts.img_max_size_mp} MPixels)' + + def fix_axis_seeds(axis_opt, axis_list): + if axis_opt.label in ['Seed', 'Var. seed']: + return [int(random.randrange(4294967294)) if val is None or val == '' or val == -1 else val for val in axis_list] + else: + return axis_list + + if not no_fixed_seeds: + xs = fix_axis_seeds(x_opt, xs) + ys = fix_axis_seeds(y_opt, ys) + zs = fix_axis_seeds(z_opt, zs) + + if x_opt.label == 'Steps': + total_steps = sum(xs) * len(ys) * len(zs) + elif y_opt.label == 'Steps': + total_steps = sum(ys) * len(xs) * len(zs) + elif z_opt.label == 'Steps': + total_steps = sum(zs) * len(xs) * len(ys) + else: + total_steps = p.steps * len(xs) * len(ys) * len(zs) + + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr: + if x_opt.label == "Hires steps": + total_steps += sum(xs) * len(ys) * len(zs) + elif y_opt.label == "Hires steps": + total_steps += sum(ys) * len(xs) * len(zs) + elif z_opt.label == "Hires steps": + total_steps += sum(zs) * len(xs) * len(ys) + elif p.hr_second_pass_steps: + total_steps += p.hr_second_pass_steps * len(xs) * len(ys) * len(zs) + else: + total_steps *= 2 + + total_steps *= p.n_iter + + image_cell_count = p.n_iter * p.batch_size + cell_console_text = f"; {image_cell_count} images per cell" if image_cell_count > 1 else "" + plural_s = 's' if len(zs) > 1 else '' + print(f"X/Y/Z plot will create {len(xs) * len(ys) * len(zs) * image_cell_count} images on {len(zs)} {len(xs)}x{len(ys)} grid{plural_s}{cell_console_text}. (Total steps to process: {total_steps})") + shared.total_tqdm.updateTotal(total_steps) + + state.xyz_plot_x = AxisInfo(x_opt, xs) + state.xyz_plot_y = AxisInfo(y_opt, ys) + state.xyz_plot_z = AxisInfo(z_opt, zs) + + # If one of the axes is very slow to change between (like SD model + # checkpoint), then make sure it is in the outer iteration of the nested + # `for` loop. + first_axes_processed = 'z' + second_axes_processed = 'y' + if x_opt.cost > y_opt.cost and x_opt.cost > z_opt.cost: + first_axes_processed = 'x' + if y_opt.cost > z_opt.cost: + second_axes_processed = 'y' + else: + second_axes_processed = 'z' + elif y_opt.cost > x_opt.cost and y_opt.cost > z_opt.cost: + first_axes_processed = 'y' + if x_opt.cost > z_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'z' + elif z_opt.cost > x_opt.cost and z_opt.cost > y_opt.cost: + first_axes_processed = 'z' + if x_opt.cost > y_opt.cost: + second_axes_processed = 'x' + else: + second_axes_processed = 'y' + + grid_infotext = [None] * (1 + len(zs)) + + def cell(x, y, z, ix, iy, iz): + if shared.state.interrupted: + return Processed(p, [], p.seed, "") + + pc = copy(p) + pc.styles = pc.styles[:] + x_opt.apply(pc, x, xs) + y_opt.apply(pc, y, ys) + z_opt.apply(pc, z, zs) + + try: + res = process_images(pc) + except Exception as e: + errors.display(e, "generating image for xyz plot") + + res = Processed(p, [], p.seed, "") + + # Sets subgrid infotexts + subgrid_index = 1 + iz + if grid_infotext[subgrid_index] is None and ix == 0 and iy == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + pc.extra_generation_params['Script'] = self.title() + + if x_opt.label != 'Nothing': + pc.extra_generation_params["X Type"] = x_opt.label + pc.extra_generation_params["X Values"] = x_values + if x_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed X Values"] = ", ".join([str(x) for x in xs]) + + if y_opt.label != 'Nothing': + pc.extra_generation_params["Y Type"] = y_opt.label + pc.extra_generation_params["Y Values"] = y_values + if y_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Y Values"] = ", ".join([str(y) for y in ys]) + + grid_infotext[subgrid_index] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + # Sets main grid infotext + if grid_infotext[0] is None and ix == 0 and iy == 0 and iz == 0: + pc.extra_generation_params = copy(pc.extra_generation_params) + + if z_opt.label != 'Nothing': + pc.extra_generation_params["Z Type"] = z_opt.label + pc.extra_generation_params["Z Values"] = z_values + if z_opt.label in ["Seed", "Var. seed"] and not no_fixed_seeds: + pc.extra_generation_params["Fixed Z Values"] = ", ".join([str(z) for z in zs]) + + grid_infotext[0] = processing.create_infotext(pc, pc.all_prompts, pc.all_seeds, pc.all_subseeds) + + return res + + with SharedSettingsStackHelper(): + processed = draw_xyz_grid( + p, + xs=xs, + ys=ys, + zs=zs, + x_labels=[x_opt.format_value(p, x_opt, x) for x in xs], + y_labels=[y_opt.format_value(p, y_opt, y) for y in ys], + z_labels=[z_opt.format_value(p, z_opt, z) for z in zs], + cell=cell, + draw_legend=draw_legend, + include_lone_images=include_lone_images, + include_sub_grids=include_sub_grids, + first_axes_processed=first_axes_processed, + second_axes_processed=second_axes_processed, + margin_size=margin_size + ) + + if not processed.images: + # It broke, no further handling needed. + return processed + + z_count = len(zs) + + # Set the grid infotexts to the real ones with extra_generation_params (1 main grid + z_count sub-grids) + processed.infotexts[:1+z_count] = grid_infotext[:1+z_count] + + if not include_lone_images: + # Don't need sub-images anymore, drop from list: + processed.images = processed.images[:z_count+1] + + if opts.grid_save: + # Auto-save main and sub-grids: + grid_count = z_count + 1 if z_count > 1 else 1 + for g in range(grid_count): + # TODO: See previous comment about intentional data misalignment. + adj_g = g-1 if g > 0 else g + images.save_image(processed.images[g], p.outpath_grids, "xyz_grid", info=processed.infotexts[g], extension=opts.grid_format, prompt=processed.all_prompts[adj_g], seed=processed.all_seeds[adj_g], grid=True, p=processed) + + if not include_sub_grids: + # Done with sub-grids, drop all related information: + for _ in range(z_count): + del processed.images[1] + del processed.all_prompts[1] + del processed.all_seeds[1] + del processed.infotexts[1] + + return processed diff --git a/scripts/xyzabc.py b/scripts/xyzabc.py new file mode 100644 index 0000000000000000000000000000000000000000..7dd88ee776b5e233e824ca35b4110573369756cc --- /dev/null +++ b/scripts/xyzabc.py @@ -0,0 +1,569 @@ +from collections import namedtuple +from copy import copy +from itertools import permutations, chain, product +from functools import reduce +import random +import argparse +import shlex +import csv +import os.path +from io import StringIO +import numpy as np + +import modules.scripts as scripts +import gradio as gr + +from modules import images, sd_samplers, processing, sd_models, sd_vae, sd_samplers_kdiffusion, errors +from modules.processing import process_images, Processed, StableDiffusionProcessingTxt2Img +from modules.shared import opts, state +import modules.shared as shared +import modules.sd_samplers +import modules.sd_models +import modules.sd_vae +import re + +from modules.ui_components import ToolButton + +fill_values_symbol = "\U0001f4d2" # 📒 + +AxisInfo = namedtuple('AxisInfo', ['axis', 'values']) + +parser = argparse.ArgumentParser(description = 'Parse prompt replacement strings') +parser.add_argument('--prompt', required=True) +parser.add_argument('--negative_prompt', default=None) +parser.add_argument('--cfg_scale', type = float, default=None) +parser.add_argument('--seed', type = int, default=None) +parser.add_argument('--steps', type = int, default=None) + +def prepare_sr_tuple_prompt(xs): + valslist = csv_string_to_list_strip(xs) + return [(valslist[0], x) for x in valslist[1:]] + +def prepare_prompt_replacement(xs): + # the namespace objects travel a bit awkwardly, + # so we turn them into dictionaries + results = [] + for item in xs.split('\n'): + pa, unknown = parser.parse_known_args(shlex.split(item)) + results.append(vars(pa)) + return results + +def apply_field(field): + def fun(p, x, xs): + setattr(p, field, x) + return fun + +def apply_prompt(p, x, xs): + if xs[0] not in p.prompt and xs[0] not in p.negative_prompt: + raise RuntimeError(f"Prompt S/R did not find {xs[0]} in prompt or negative prompt.") + p.prompt = p.prompt.replace(xs[0], x) + p.negative_prompt = p.negative_prompt.replace(xs[0], x) + +def apply_global_reweight(p, x, xs, and_negative = True): + p.prompt = f'({p.prompt}:{x})' + if and_negative: + p.negative_prompt = f'({p.negative_prompt}:{x})' + +def apply_global_reweight_positive(p, x, xs): + apply_global_reweight(p, x, xs, and_negative = False) + +def apply_tuple_prompt(p, x, xs): + k, v = x + if k not in p.prompt and k not in p.negative_prompt: + raise RuntimeError(f'Prompt S/R (tuple) did not find {k} in prompt or negative prompt.') + p.prompt = p.prompt.replace(k, v) + p.negative_prompt = p.negative_prompt.replace(k, v) + +def apply_prompt_replacement(p, x, xs): + for k, v in x.items(): + if v: + setattr(p, k, v) + +def apply_order(p, x, xs): + token_order = [] + # Initally grab the tokens from the prompt, so they can be replaced in order of earliest seen + for token in x: + token_order.append((p.prompt.find(token), token)) + token_order.sort(key=lambda t: t[0]) + prompt_parts = [] + # Split the prompt up, taking out the tokens + for _, token in token_order: + n = p.prompt.find(token) + prompt_parts.append(p.prompt[0:n]) + p.prompt = p.prompt[n + len(token):] + # Rebuild the prompt with the tokens in the order we want + prompt_tmp = "" + for idx, part in enumerate(prompt_parts): + prompt_tmp += part + prompt_tmp += x[idx] + p.prompt = prompt_tmp + p.prompt + +def confirm_samplers(p, xs): + for x in xs: + if x.lower() not in sd_samplers.samplers_map: + raise RuntimeError(f"Unknown sampler: {x}") + +def apply_checkpoint(p, x, xs): + info = modules.sd_models.get_closet_checkpoint_match(x) + if info is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + p.override_settings['sd_model_checkpoint'] = info.name + +def confirm_checkpoints(p, xs): + for x in xs: + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + +def confirm_checkpoints_or_none(p, xs): + for x in xs: + if x in (None, "", "None", "none"): + continue + if modules.sd_models.get_closet_checkpoint_match(x) is None: + raise RuntimeError(f"Unknown checkpoint: {x}") + +def apply_clip_skip(p, x, xs): + opts.data["CLIP_stop_at_last_layers"] = x + +def apply_upscale_latent_space(p, x, xs): + if x.lower().strip() != '0': + opts.data["use_scale_latent_for_hires_fix"] = True + else: + opts.data["use_scale_latent_for_hires_fix"] = False + +def find_vae(name: str): + if name.lower() in ['auto', 'automatic']: + return modules.sd_vae.unspecified + if name.lower() == 'none': + return None + else: + choices = [x for x in sorted(modules.sd_vae.vae_dict, key=lambda x: len(x)) if name.lower().strip() in x.lower()] + if len(choices) == 0: + print(f"No VAE found for {name}; using automatic") + return modules.sd_vae.unspecified + else: + return modules.sd_vae.vae_dict[choices[0]] + +def apply_vae(p, x, xs): + modules.sd_vae.reload_vae_weights(shared.sd_model, vae_file=find_vae(x)) + +def apply_styles(p: StableDiffusionProcessingTxt2Img, x: str, _): + p.styles.extend(x.split(',')) + +def apply_uni_pc_order(p, x, xs): + opts.data["uni_pc_order"] = min(x, p.steps - 1) + +def apply_face_restore(p, opt, x): + opt = opt.lower() + if opt == 'codeformer': + is_active = True + p.face_restoration_model = 'CodeFormer' + elif opt == 'gfpgan': + is_active = True + p.face_restoration_model = 'GFPGAN' + else: + is_active = opt in ('true', 'yes', 'y', '1') + p.restore_faces = is_active + +def apply_override(field, boolean: bool = False): + def fun(p, x, xs): + if boolean: + x = True if x.lower() == "true" else False + p.override_settings[field] = x + return fun + +def boolean_choice(reverse: bool = False): + def choice(): + return ["False", "True"] if reverse else ["True", "False"] + return choice + +def format_value_add_label(p, opt, x): + if type(x) == float: + x = round(x, 8) + return f"{opt.label}: {x}" + +def format_value(p, opt, x): + if type(x) == float: + x = round(x, 8) + return x + +def format_value_join_list(p, opt, x): + return ", ".join(x) + +def do_nothing(p, x, xs): + pass + +def format_nothing(p, opt, x): + return "" + +def format_remove_path(p, opt, x): + return os.path.basename(x) + +def str_permutations(x): + """dummy function for specifying it in AxisOption's type when you want to get a list of permutations""" + return x + +def list_to_csv_string(data_list): + with StringIO() as o: + csv.writer(o).writerow(data_list) + return o.getvalue().strip() + +def csv_string_to_list_strip(data_str): + return [x for x in list(map(str.strip, chain.from_iterable(csv.reader(StringIO(data_str))))) if x] + +class AxisOption: + def __init__(self, label, type, apply, format_value=format_value_add_label, confirm=None, cost=0.0, choices=None, prepare=None): + self.label = label + self.type = type + self.apply = apply + self.format_value = format_value + self.confirm = confirm + self.cost = cost + self.prepare = prepare + self.choices = choices + + +class AxisOptionImg2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = True + + +class AxisOptionTxt2Img(AxisOption): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_img2img = False + + +axis_options = [ + AxisOption("Nothing", str, do_nothing, format_value=format_nothing), + AxisOption("Seed", int, apply_field("seed")), + AxisOption("Var. seed", int, apply_field("subseed")), + AxisOption("Var. strength", float, apply_field("subseed_strength")), + AxisOption("Steps", int, apply_field("steps")), + AxisOptionTxt2Img("Hires steps", int, apply_field("hr_second_pass_steps")), + AxisOption("CFG Scale", float, apply_field("cfg_scale")), + AxisOptionImg2Img("Image CFG Scale", float, apply_field("image_cfg_scale")), + AxisOption("Prompt S/R", str, apply_prompt, format_value=format_value), + AxisOption("Prompt S/R (skip first)", tuple, apply_tuple_prompt, prepare=prepare_sr_tuple_prompt, format_value=format_value), + AxisOption("Prompt Replacement", dict, apply_prompt_replacement, prepare=prepare_prompt_replacement, format_value=format_value), + AxisOption("Prompt order", str_permutations, apply_order, format_value=format_value_join_list), + AxisOption("Global Prompt Reweight", float, apply_global_reweight), + AxisOption("Global Prompt Reweight (Positive Only)", float, apply_global_reweight_positive), + AxisOptionTxt2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers if x.name not in opts.hide_samplers]), + AxisOptionTxt2Img("Hires sampler", str, apply_field("hr_sampler_name"), confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOptionImg2Img("Sampler", str, apply_field("sampler_name"), format_value=format_value, confirm=confirm_samplers, choices=lambda: [x.name for x in sd_samplers.samplers_for_img2img if x.name not in opts.hide_samplers]), + AxisOption("Checkpoint name", str, apply_checkpoint, format_value=format_remove_path, confirm=confirm_checkpoints, cost=1.0, choices=lambda: sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Negative Guidance minimum sigma", float, apply_field("s_min_uncond")), + AxisOption("Sigma Churn", float, apply_field("s_churn")), + AxisOption("Sigma min", float, apply_field("s_tmin")), + AxisOption("Sigma max", float, apply_field("s_tmax")), + AxisOption("Sigma noise", float, apply_field("s_noise")), + AxisOption("Schedule type", str, apply_override("k_sched_type"), choices=lambda: list(sd_samplers_kdiffusion.k_diffusion_scheduler)), + AxisOption("Schedule min sigma", float, apply_override("sigma_min")), + AxisOption("Schedule max sigma", float, apply_override("sigma_max")), + AxisOption("Schedule rho", float, apply_override("rho")), + AxisOption("Eta", float, apply_field("eta")), + AxisOption("Clip skip", int, apply_clip_skip), + AxisOption("Denoising", float, apply_field("denoising_strength")), + AxisOption("Initial noise multiplier", float, apply_field("initial_noise_multiplier")), + AxisOption("Extra noise", float, apply_override("img2img_extra_noise")), + AxisOptionTxt2Img("Hires upscaler", str, apply_field("hr_upscaler"), choices=lambda: [*shared.latent_upscale_modes, *[x.name for x in shared.sd_upscalers]]), + AxisOptionImg2Img("Cond. Image Mask Weight", float, apply_field("inpainting_mask_weight")), + AxisOption("VAE", str, apply_vae, cost=0.7, choices=lambda: ['None'] + list(sd_vae.vae_dict)), + AxisOption("Styles", str, apply_styles, choices=lambda: list(shared.prompt_styles.styles)), + AxisOption("UniPC Order", int, apply_uni_pc_order, cost=0.5), + AxisOption("Face restore", str, apply_face_restore, format_value=format_value), + AxisOption("Token merging ratio", float, apply_override('token_merging_ratio')), + AxisOption("Token merging ratio high-res", float, apply_override('token_merging_ratio_hr')), + AxisOption("Always discard next-to-last sigma", str, apply_override('always_discard_next_to_last_sigma', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("SGM noise multiplier", str, apply_override('sgm_noise_multiplier', boolean=True), choices=boolean_choice(reverse=True)), + AxisOption("Refiner checkpoint", str, apply_field('refiner_checkpoint'), format_value=format_remove_path, confirm=confirm_checkpoints_or_none, cost=1.0, choices=lambda: ['None'] + sorted(sd_models.checkpoints_list, key=str.casefold)), + AxisOption("Refiner switch at", float, apply_field('refiner_switch_at')), + AxisOption("RNG source", str, apply_override("randn_source"), choices=lambda: ["GPU", "CPU", "NV"]), +] + +class SharedSettingsStackHelper(object): + def __enter__(self): + self.CLIP_stop_at_last_layers = opts.CLIP_stop_at_last_layers + self.vae = opts.sd_vae + self.uni_pc_order = opts.uni_pc_order + + def __exit__(self, exc_type, exc_value, tb): + opts.data["sd_vae"] = self.vae + opts.data["uni_pc_order"] = self.uni_pc_order + modules.sd_models.reload_model_weights() + modules.sd_vae.reload_vae_weights() + opts.data["CLIP_stop_at_last_layers"] = self.CLIP_stop_at_last_layers + +re_range = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\(([+-]\d+)\s*\))?\s*") +re_range_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\(([+-]\d+(?:.\d*)?)\s*\))?\s*") + +re_range_count = re.compile(r"\s*([+-]?\s*\d+)\s*-\s*([+-]?\s*\d+)(?:\s*\[(\d+)\s*])?\s*") +re_range_count_float = re.compile(r"\s*([+-]?\s*\d+(?:.\d*)?)\s*-\s*([+-]?\s*\d+(?:.\d*)?)(?:\s*\[(\d+(?:.\d*)?)\s*])?\s*") + +class Script(scripts.Script): + def title(self): + return "X/Y/Z/A plot" + + def ui(self, is_img2img): + self.current_axis_options = [x for x in axis_options if type(x) == AxisOption or x.is_img2img == is_img2img] + + with gr.Row(): + with gr.Column(scale=19): + with gr.Row(): + x_type = gr.Dropdown(label="X type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[1].label, type="index", elem_id=self.elem_id("x_type")) + x_values = gr.Textbox(label="X values", lines=1, elem_id=self.elem_id("x_values")) + fill_x_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_x_tool_button", visible=False) + + with gr.Row(): + y_type = gr.Dropdown(label="Y type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("y_type")) + y_values = gr.Textbox(label="Y values", lines=1, elem_id=self.elem_id("y_values")) + fill_y_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_y_tool_button", visible=False) + + with gr.Row(): + z_type = gr.Dropdown(label="Z type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("z_type")) + z_values = gr.Textbox(label="Z values", lines=1, elem_id=self.elem_id("z_values")) + fill_z_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_z_tool_button", visible=False) + + with gr.Row(): + a_type = gr.Dropdown(label="A type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("a_type")) + a_values = gr.Textbox(label="A values", lines=1, elem_id=self.elem_id("a_values")) + fill_a_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_a_tool_button", visible=False) + + with gr.Row(): + b_type = gr.Dropdown(label="B type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("b_type")) + b_values = gr.Textbox(label="B values", lines=1, elem_id=self.elem_id("b_values")) + fill_b_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_b_tool_button", visible=False) + + with gr.Row(): + c_type = gr.Dropdown(label="C type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("c_type")) + c_values = gr.Textbox(label="C values", lines=1, elem_id=self.elem_id("c_values")) + fill_c_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_c_tool_button", visible=False) + + with gr.Row(): + d_type = gr.Dropdown(label="D type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("d_type")) + d_values = gr.Textbox(label="D values", lines=1, elem_id=self.elem_id("d_values")) + fill_d_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_d_tool_button", visible=False) + + with gr.Row(): + e_type = gr.Dropdown(label="E type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("e_type")) + e_values = gr.Textbox(label="E values", lines=1, elem_id=self.elem_id("e_values")) + fill_e_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_e_tool_button", visible=False) + + with gr.Row(): + f_type = gr.Dropdown(label="F type", choices=[x.label for x in self.current_axis_options], value=self.current_axis_options[0].label, type="index", elem_id=self.elem_id("f_type")) + f_values = gr.Textbox(label="F values", lines=1, elem_id=self.elem_id("f_values")) + fill_f_button = ToolButton(value=fill_values_symbol, elem_id="xyz_grid_fill_f_tool_button", visible=False) + + with gr.Row(variant="compact", elem_id="axis_options"): + no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds")) + + def fill(axis_type): + axis = self.current_axis_options[axis_type] + if axis.choices: + return list_to_csv_string(axis.choices()) + return gr.update() + + fill_x_button.click(fn=fill, inputs=[x_type], outputs=[x_values]) + fill_y_button.click(fn=fill, inputs=[y_type], outputs=[y_values]) + fill_z_button.click(fn=fill, inputs=[z_type], outputs=[z_values]) + fill_a_button.click(fn=fill, inputs=[a_type], outputs=[a_values]) + fill_b_button.click(fn=fill, inputs=[b_type], outputs=[b_values]) + fill_c_button.click(fn=fill, inputs=[c_type], outputs=[c_values]) + fill_d_button.click(fn=fill, inputs=[d_type], outputs=[d_values]) + fill_e_button.click(fn=fill, inputs=[e_type], outputs=[e_values]) + fill_f_button.click(fn=fill, inputs=[f_type], outputs=[f_values]) + + def select_axis(axis_type, axis_values): + choices = self.current_axis_options[axis_type].choices + has_choices = choices is not None + return (gr.Button.update(visible=has_choices), gr.Textbox.update(visible=True, value=axis_values)) + + x_type.change(fn=select_axis, inputs=[x_type, x_values], outputs=[fill_x_button, x_values]) + y_type.change(fn=select_axis, inputs=[y_type, y_values], outputs=[fill_y_button, y_values]) + z_type.change(fn=select_axis, inputs=[z_type, z_values], outputs=[fill_z_button, z_values]) + a_type.change(fn=select_axis, inputs=[a_type, a_values], outputs=[fill_a_button, a_values]) + b_type.change(fn=select_axis, inputs=[b_type, b_values], outputs=[fill_b_button, b_values]) + c_type.change(fn=select_axis, inputs=[c_type, c_values], outputs=[fill_c_button, c_values]) + d_type.change(fn=select_axis, inputs=[d_type, d_values], outputs=[fill_d_button, d_values]) + e_type.change(fn=select_axis, inputs=[e_type, e_values], outputs=[fill_e_button, e_values]) + f_type.change(fn=select_axis, inputs=[f_type, f_values], outputs=[fill_f_button, f_values]) + + self.infotext_fields = ( + (x_type, "X Type"), (x_values, "X Values"), + (y_type, "Y Type"), (y_values, "Y Values"), + (z_type, "Z Type"), (z_values, "Z Values"), + (a_type, "A Type"), (a_values, "A Values"), + (b_type, "B Type"), (b_values, "B Values"), + (c_type, "C Type"), (c_values, "C Values"), + (d_type, "D Type"), (d_values, "D Values"), + (e_type, "E Type"), (e_values, "E Values"), + (f_type, "F Type"), (f_values, "F Values") + ) + + # it's a crime they don't let me pack these into some kind of list + return [ + x_type, x_values, + y_type, y_values, + z_type, z_values, + a_type, a_values, + b_type, b_values, + c_type, c_values, + d_type, d_values, + e_type, e_values, + f_type, f_values, + no_fixed_seeds] + + def run(self, p, x_t, x_v, y_t, y_v, z_t, z_v, a_t, a_v, b_t, b_v, c_t, c_v, d_t, d_v, e_t, e_v, f_t, f_v, no_fixed_seeds): + axis_setup = [(x_t, x_v), (y_t, y_v), (z_t, z_v), (a_t, a_v), (b_t, b_v), (c_t, c_v), (d_t, d_v), (e_t, e_v), (f_t, f_v)] + if not no_fixed_seeds: + modules.processing.fix_seed(p) + p.batch_size = 1 + + def process_axis(opt, vals): + if opt.label == 'Nothing': + return [0] + if opt.prepare is not None: + valslist = opt.prepare(vals) + else: + valslist = csv_string_to_list_strip(vals) + if opt.type == int: + valslist_ext = [] + for val in valslist: + m = re_range.fullmatch(val) + mc = re_range_count.fullmatch(val) + if m is not None: + start = int(m.group(1)) + end = int(m.group(2))+1 + step = int(m.group(3)) if m.group(3) is not None else 1 + valslist_ext += list(range(start, end, step)) + elif mc is not None: + start = int(mc.group(1)) + end = int(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + valslist_ext += [int(x) for x in np.linspace(start=start, stop=end, num=num).tolist()] + else: + valslist_ext.append(val) + valslist = valslist_ext + elif opt.type == float: + valslist_ext = [] + for val in valslist: + m = re_range_float.fullmatch(val) + mc = re_range_count_float.fullmatch(val) + if m is not None: + start = float(m.group(1)) + end = float(m.group(2)) + step = float(m.group(3)) if m.group(3) is not None else 1 + valslist_ext += np.arange(start, end + step, step).tolist() + elif mc is not None: + start = float(mc.group(1)) + end = float(mc.group(2)) + num = int(mc.group(3)) if mc.group(3) is not None else 1 + valslist_ext += np.linspace(start=start, stop=end, num=num).tolist() + else: + valslist_ext.append(val) + valslist = valslist_ext + elif opt.type == str_permutations: + valslist = list(permutations(valslist)) + valslist = [opt.type(x) for x in valslist] + # Confirm options are valid before starting + if opt.confirm: + opt.confirm(p, valslist) + return valslist + + # axis_setup is a list of tuples of axis type numbers and user inputs + # we will retrieve the true axis types using the numbers + + axis_setup = [(self.current_axis_options[atn], atn, av) for atn, av in axis_setup] + + # remove any non-activated items + axis_setup = [(at, atn, av) for at, atn, av in axis_setup if at.label != 'Nothing'] + + # sort the list by expected cost, largest to smallest + + axis_setup.sort(key = lambda x: x[0].cost, reverse = True) + axis_labels = [x[0].label for x in axis_setup] + + # process the arguments away from their user input forms into 'real' forms + + axis_setup = [(at, atn, process_axis(at, av)) for at, atn, av in axis_setup] + + # process any 'Seed' or 'Var. seed' items if allowed + def fix_axis_seeds(axis_opt, axis_list): + if axis_opt.label in ['Seed', 'Var. seed']: + return [int(random.randrange(4294967294)) if val is None or val == '' or val == -1 else val for val in axis_list] + else: + return axis_list + if not no_fixed_seeds: + axis_setup = [(at, atn, fix_axis_seeds(at, av)) for at, atn, av in axis_setup] + + def expectedStepsForAxis(at, atn, av): + if at.label == 'Steps': + return sum(av) + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr and at.label == 'Hires steps': + return sum(av) + return len(av) + + expected_images = reduce(lambda x,y: x*y, [len(av) for at, atn, av in axis_setup]) + + expected_steps = reduce(lambda x,y: x*y, [expectedStepsForAxis(at, atn, av) for at, atn, av in axis_setup]) + if 'Steps' not in axis_labels: + expected_steps *= p.steps + if isinstance(p, StableDiffusionProcessingTxt2Img) and p.enable_hr and 'Hires steps' not in axis_labels: + if p.hr_second_pass_steps: + expected_steps *= p.hr_second_pass_steps + else: + expected_steps *= 2 + + expected_steps *= p.n_iter + + print(f"X/Y/Z/A plot will create {expected_images} images. (Total steps to process: {expected_steps})") + shared.total_tqdm.updateTotal(expected_steps) + + state.plot_state = [AxisInfo(at, av) for at, atn, av in axis_setup] + + def draw_single(axis_setup, axis_values): + if shared.state.interrupted: + return Processed(p, [], p.seed, "") + pc = copy(p) + pc.styles = pc.styles[:] + print() + print('setting x/y/z/a axis values:') + for i in range(len(axis_values)): + axis_type = axis_setup[i][0] + axis_value = axis_values[i] + axis_all_values = axis_setup[i][2] + print(f' ** {axis_type.label}: {axis_value}') + axis_type.apply(pc, axis_value, axis_all_values) + try: + res = process_images(pc) + except Exception as e: + errors.display(e, "generating image for xyza plot") + res = Processed(p, [], p.seed, "") + return res + + with SharedSettingsStackHelper(): + results_container = None + for value_combination in product(*[av for at, atn, av in axis_setup]): + processed: Processed = draw_single(axis_setup, value_combination) + if results_container is None: + results_container = copy(processed) + results_container.images = [] + results_container.all_prompts = [] + results_container.all_seeds = [] + results_container.infotexts = [] + results_container.index_of_first_image = 1 + if processed.images: + results_container.images.append(processed.images[0]) + results_container.all_prompts.append(processed.prompt) + results_container.all_seeds.append(processed.seed) + results_container.infotexts.append(processed.infotexts[0]) + if not results_container: + # Should never happen, I've only seen it on one of four open tabs and it needed to refresh. + print("Unexpected error: Processing could not begin, you may need to refresh the tab or restart the service.") + results_container = Processed(p, []) + elif not any(results_container.images): + print("Unexpected error: draw_xyza_grid failed to return even a single processed image") + results_container = Processed(p, []) + + return results_container