├── .gitignore ├── LICENSE ├── README.md ├── info.jpg ├── javascript ├── comp_loader.js ├── img_comp.js └── mouse_handler.js ├── scripts └── img_comp.py ├── style.css └── tab.gif /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Haoming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SD Webui Image Comparison 2 | This is an Extension for the [Automatic1111 Webui](https://github.com/AUTOMATIC1111/stable-diffusion-webui), which adds a new tab for image comparison. 3 | 4 | > Compatible with [Forge](https://github.com/lllyasviel/stable-diffusion-webui-forge) 5 | 6 |

7 |
8 | Draggable Image Slider 9 |

10 | 11 | ## Features 12 | 13 | Wanna check how your **img2img** went? Simply compare the results in the brand new **Comparison** tab! 14 | 15 | ### How to Use 16 | 17 | > This Extension comes with a few ways to load images; and a few utilities to compare details 18 | 19 | - Load Images 20 | - Manually upload two images to the **Comparison** tab, then click on the `Compare Upload` button 21 | - Click on the `Compare img2img` / `Compare Inpaint` button to automatically pull from the `img2img` input and output images 22 | - Click on the `Compare Extras` button to automatically pull from the `Extras` input and output images 23 | 24 | - Settings 25 | 26 | > These options are in the `Image Comparison` section under the User Interface category in the **Settings** tab 27 | 28 | - Add new buttons to the `img2img` and `Extras` tabs, that send the input and output images to the `Comparison` tab when clicked 29 | - Add a new button to the `txt2img` tab, that sends the current result image along with the cached previous result image to the `Comparison` tab when clicked 30 | - Allow you to `Ctrl` + `Alt` + `Left Click` / `Right Click` on any element, to send its image to either side of the `Comparison` tab 31 | - Doesn't seem to work on images within the result Gallery, so it's mainly for images from other Extensions / Tabs 32 | 33 | - Controls 34 | - Default 35 | - Use `left click` to drag the comparison bar 36 | - Press the `backspace` key or click the `🔄` button to reset the scale and position 37 | - Press the `+` key or click the `➕` button to zoom in 38 | - Press the `-` key or click the `➖` button to zoom out 39 | - Use `arrow keys` to pan around 40 | - Holding Shift 41 | - Use `left click` to pan around 42 | - Use `scrollwheel` to zoom in & out 43 | - Press the `0` key to reset the scale and position 44 | - Buttons 45 | - Use the `Horizontal Slider` checkbox to toggle between *side-by-side* and *top-to-bottom* comparisons 46 | - Use the `Opacity` slider to alter the transparency of the 2nd image 47 | - Click on the `Swap` button to quickly exchange the two images 48 | 49 | ### Infotext 50 | 51 | At the bottom of the **Comparison** tab, there is also an `Infotext` panel. When you **manually upload** images generated via the Webui, the panel will display the generation parameters for both images. Parameters that are different between the two images will be highlighted in red. 52 | 53 |

54 | 55 |

56 | -------------------------------------------------------------------------------- /info.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haoming02/sd-webui-image-comparison/4354edab87ca6634fe1f0b94d3e0dba65f6221f0/info.jpg -------------------------------------------------------------------------------- /javascript/comp_loader.js: -------------------------------------------------------------------------------- 1 | class ImgCompLoader { 2 | 3 | static swapImage() { 4 | let temp = ImageComparator.img_A.src; 5 | ImageComparator.img_A.src = ImageComparator.img_B.src; 6 | ImageComparator.img_B.src = temp; 7 | } 8 | 9 | static loadImage(tab) { 10 | let source_a = null; 11 | let source_b = null; 12 | 13 | switch (tab) { 14 | case 'i2i': 15 | source_a = document.getElementById('img2img_image').querySelector('img'); 16 | source_b = document.getElementById('img2img_gallery').querySelector('img'); 17 | break; 18 | case 'inpaint': 19 | source_a = document.getElementById('img2img_inpaint_tab').querySelector('img'); 20 | source_b = document.getElementById('img2img_gallery').querySelector('img'); 21 | break; 22 | case 'extras': 23 | source_a = document.getElementById('extras_image').querySelector('img'); 24 | source_b = document.getElementById('extras_gallery').querySelector('img'); 25 | break; 26 | case 'upload': 27 | source_a = document.getElementById('img_comp_input_A').querySelector('img'); 28 | source_b = document.getElementById('img_comp_input_B').querySelector('img'); 29 | break; 30 | default: 31 | alert('Invalid Tab...?'); 32 | break; 33 | } 34 | 35 | if (source_a == null || source_b == null) 36 | return; 37 | 38 | ImageComparator.img_A.src = source_a.src; 39 | ImageComparator.img_B.src = source_b.src; 40 | ImageComparator.reset(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /javascript/img_comp.js: -------------------------------------------------------------------------------- 1 | class ImageComparator { 2 | 3 | static #IMG_COMP_WIDTH; 4 | static img_A; 5 | static img_B; 6 | static #alpha_slider; 7 | static #bar; 8 | static #direction_checkbox; 9 | static #cached_image = undefined; 10 | 11 | static #translateX = 0.0; 12 | static #translateY = 0.0; 13 | static #scale = 1.0; 14 | 15 | static get #isHorizontal() { return this.#direction_checkbox.checked; } 16 | 17 | static switch_to_comparison() { 18 | const tabs = document.querySelector('#tabs').querySelector('.tab-nav').querySelectorAll('button'); 19 | // Surely, everyone uses txt2img, img2img, and Extras... 20 | for (let i = 3; i < tabs.length; i++) { 21 | if (tabs[i].textContent.trim() === "Comparison") { 22 | tabs[i].click(); 23 | break; 24 | } 25 | } 26 | } 27 | 28 | static reset() { 29 | if (this.#isHorizontal) { 30 | this.img_B.parentNode.style.left = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px)`; 31 | this.img_B.style.left = `${this.#IMG_COMP_WIDTH}px`; 32 | this.#bar.style.left = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px)`; 33 | 34 | this.img_B.parentNode.style.top = '0px'; 35 | this.img_B.style.top = '0px'; 36 | this.#bar.style.top = '0px'; 37 | 38 | this.#bar.style.height = `${this.#IMG_COMP_WIDTH}px`; 39 | this.#bar.style.width = '2px'; 40 | 41 | this.img_A.classList.add('hor'); 42 | this.img_A.classList.remove('ver'); 43 | } else { 44 | this.img_B.parentNode.style.left = `calc(50% - ${this.#IMG_COMP_WIDTH / 2}px)`; 45 | this.img_B.style.left = '0px'; 46 | this.#bar.style.left = `calc(50% - ${this.#IMG_COMP_WIDTH / 2}px)`; 47 | 48 | this.img_B.parentNode.style.top = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px)`; 49 | this.img_B.style.top = `${this.#IMG_COMP_WIDTH}px`; 50 | this.#bar.style.top = `${this.#IMG_COMP_WIDTH}px`; 51 | 52 | this.#bar.style.width = `${this.#IMG_COMP_WIDTH}px`; 53 | this.#bar.style.height = '2px'; 54 | 55 | this.img_A.classList.remove('hor'); 56 | this.img_A.classList.add('ver'); 57 | } 58 | 59 | this.img_B.style.opacity = 1.0; 60 | this.#alpha_slider.querySelector('input').value = 1.0; 61 | updateInput(this.#alpha_slider.querySelector('input')); 62 | } 63 | 64 | static #addButtons() { 65 | // 0: Off ; 1: Text ; 2: Icon 66 | const config = Array.from(document.getElementById('setting_comp_send_btn').querySelectorAll('label')); 67 | const option = config.findIndex(label => label.classList.contains('selected')); 68 | if (option === 0) 69 | return; 70 | 71 | for (const mode of ['img2img', 'extras']) { 72 | const buttons = document.getElementById(`image_buttons_${mode}`).querySelectorAll("button"); 73 | const btn = buttons[buttons.length - 1].cloneNode(); 74 | 75 | btn.id = `${mode}_send_to_comp`; 76 | btn.title = "Send images to comparison tab."; 77 | btn.textContent = (option === 1) ? "Send to Comparison" : "🆚"; 78 | 79 | if (mode === "extras") { 80 | btn.addEventListener('click', () => { 81 | ImgCompLoader.loadImage("extras"); 82 | this.switch_to_comparison(); 83 | }); 84 | } 85 | else { 86 | const tabs = document.getElementById('img2img_settings').querySelector('.tabs').querySelector('.tab-nav'); 87 | 88 | btn.addEventListener('click', () => { 89 | for (const tab of tabs.querySelectorAll('button')) { 90 | if (tab.classList.contains('selected')) { 91 | const t = tab.textContent.trim(); 92 | 93 | if (t === "img2img") { 94 | ImgCompLoader.loadImage("i2i"); 95 | this.switch_to_comparison(); 96 | } 97 | else if (t === "Inpaint") { 98 | ImgCompLoader.loadImage("inpaint"); 99 | this.switch_to_comparison(); 100 | } 101 | else { 102 | alert("Only img2img and Inpaint are supported in Comparison!"); 103 | return; 104 | } 105 | } 106 | } 107 | }); 108 | } 109 | 110 | buttons[0].parentElement.appendChild(btn); 111 | } 112 | } 113 | 114 | static #addTxt2ImgButton() { 115 | // 0: Off ; 1: Text ; 2: Icon 116 | const config = Array.from(document.getElementById('setting_comp_send_btn_t2i').querySelectorAll('label')); 117 | const option = config.findIndex(label => label.classList.contains('selected')); 118 | if (option === 0) 119 | return; 120 | 121 | for (const btn of ["txt2img_generate", "txt2img_upscale"]) { 122 | const generate = document.getElementById(btn); 123 | generate?.addEventListener("click", () => { 124 | this.#cached_image = document.getElementById('txt2img_gallery').querySelector('img')?.src; 125 | }); 126 | } 127 | 128 | const buttons = document.getElementById("image_buttons_txt2img").querySelectorAll("button"); 129 | const btn = buttons[buttons.length - 1].cloneNode(); 130 | 131 | btn.id = "txt2img_send_to_comp"; 132 | btn.title = "Send images to comparison tab."; 133 | btn.textContent = (option === 1) ? "Send to Comparison" : "🆚"; 134 | 135 | btn.addEventListener('click', () => { 136 | if (this.#cached_image == null) { 137 | alert("No cached result exists!"); 138 | return; 139 | } 140 | 141 | ImageComparator.img_A.src = this.#cached_image; 142 | ImageComparator.img_B.src = document.getElementById('txt2img_gallery').querySelector('img').src; 143 | ImageComparator.reset(); 144 | 145 | this.switch_to_comparison(); 146 | }); 147 | 148 | buttons[0].parentElement.appendChild(btn); 149 | } 150 | 151 | static #clamp01(val) { 152 | return Math.min(Math.max(val, 0.0), 1.0); 153 | } 154 | 155 | static init() { 156 | const block_A = document.getElementById('img_comp_A'); 157 | this.img_A = block_A.querySelector('img'); 158 | const block_B = document.getElementById('img_comp_B'); 159 | this.img_B = block_B.querySelector('img'); 160 | 161 | const tab = document.getElementById('tab_sd-webui-image-comparison'); 162 | this.#IMG_COMP_WIDTH = parseFloat(getComputedStyle(tab).getPropertyValue('--img-comp-width').split('px')[0]); 163 | 164 | const row = document.getElementById('img_comp_row'); 165 | row.setAttribute("tabindex", 0); 166 | row.style.display = 'block'; 167 | 168 | block_A.insertBefore(this.img_A, block_A.firstChild); 169 | while (block_A.children.length > 1) 170 | block_A.lastChild.remove(); 171 | 172 | block_B.insertBefore(this.img_B, block_B.firstChild); 173 | while (block_B.children.length > 1) 174 | block_B.lastChild.remove(); 175 | 176 | block_A.classList.add('comp-block'); 177 | block_B.classList.add('comp-block'); 178 | 179 | this.img_A.ondragstart = (e) => { e.preventDefault(); return false; }; 180 | this.img_B.ondragstart = (e) => { e.preventDefault(); return false; }; 181 | 182 | block_B.style.pointerEvents = 'none'; 183 | block_B.style.left = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px)`; 184 | this.img_B.style.left = `${this.#IMG_COMP_WIDTH}px`; 185 | 186 | this.#alpha_slider = document.getElementById('img_comp_alpha'); 187 | for (const ev of ['mousemove', 'touchmove']) { 188 | this.#alpha_slider.addEventListener(ev, () => { 189 | this.img_B.style.opacity = this.#alpha_slider.querySelector('input').value; 190 | }, { passive: true }); 191 | }; 192 | 193 | this.#direction_checkbox = document.getElementById('img_comp_horizontal').querySelector('input[type=checkbox]'); 194 | 195 | this.#bar = document.createElement('div'); 196 | this.#bar.classList.add('bar'); 197 | row.appendChild(this.#bar); 198 | 199 | let startX, startY; 200 | let freezeX, freezeY; 201 | row.addEventListener("mousedown", (e) => { 202 | if (!e.shiftKey) 203 | return; 204 | 205 | startX = e.clientX; 206 | startY = e.clientY; 207 | freezeX = this.#translateX; 208 | freezeY = this.#translateY; 209 | }); 210 | 211 | for (const ev of ['mousemove', 'touchmove']) { 212 | row.addEventListener(ev, (e) => { 213 | e.preventDefault(); 214 | 215 | if (ev.startsWith('touch')) 216 | e = e.changedTouches[0]; 217 | else if (e.buttons != 1) 218 | return; 219 | 220 | if (e.shiftKey) { 221 | const deltaX = e.clientX - startX; 222 | const deltaY = e.clientY - startY; 223 | 224 | this.#translateX = freezeX + deltaX / this.#scale; 225 | this.#translateY = freezeY + deltaY / this.#scale; 226 | 227 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 228 | } else { 229 | const rect = e.target.getBoundingClientRect(); 230 | const notImage = e.target.tagName !== 'IMG'; 231 | let ratio; 232 | 233 | if (this.#isHorizontal) { 234 | if (notImage) 235 | ratio = (e.clientX > (rect.left + rect.right) / 2) ? 1.0 : 0.0; 236 | else 237 | ratio = ((e.clientX - rect.left) / (rect.right - rect.left)); 238 | 239 | ratio = this.#clamp01(ratio); 240 | const SLIDE_VALUE = this.#IMG_COMP_WIDTH * (1.0 - ratio); 241 | 242 | this.#bar.style.left = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px - ${SLIDE_VALUE}px)`; 243 | block_B.style.left = `calc(50% + ${this.#IMG_COMP_WIDTH / 2}px - ${SLIDE_VALUE}px)`; 244 | this.img_B.style.left = `calc(${-this.#IMG_COMP_WIDTH}px + ${SLIDE_VALUE}px)`; 245 | } else { 246 | if (notImage) 247 | ratio = (e.clientY > (rect.bottom + rect.top) / 2) ? 1.0 : 0.0; 248 | else 249 | ratio = ((e.clientY - rect.top) / (rect.bottom - rect.top)); 250 | 251 | ratio = this.#clamp01(ratio); 252 | const SLIDE_VALUE = this.#IMG_COMP_WIDTH * (1.0 - ratio); 253 | 254 | this.#bar.style.top = `calc(${this.#IMG_COMP_WIDTH}px - ${SLIDE_VALUE}px)`; 255 | block_B.style.top = `calc(${this.#IMG_COMP_WIDTH}px - ${SLIDE_VALUE}px)`; 256 | this.img_B.style.top = `calc(${-this.#IMG_COMP_WIDTH}px + ${SLIDE_VALUE}px)`; 257 | } 258 | } 259 | }, { passive: false }); 260 | } 261 | 262 | row.addEventListener("wheel", (e) => { 263 | if (!e.shiftKey) 264 | return; 265 | 266 | let flag = false; 267 | if (e.deltaY < 0) { 268 | this.#scale = Math.min(this.#scale + 0.5, 10.0); 269 | flag = true; 270 | } 271 | if (e.deltaY > 0) { 272 | this.#scale = Math.max(this.#scale - 0.5, 0.5) 273 | flag = true; 274 | } 275 | 276 | if (flag) { 277 | e.preventDefault(); 278 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 279 | return false; 280 | } 281 | }, { passive: false }); 282 | 283 | row.addEventListener("keyup", (e) => { 284 | if (e.key == "Shift") 285 | this.img_A.classList.remove('drag'); 286 | }); 287 | 288 | row.addEventListener("keydown", (e) => { 289 | if (e.key == "Shift") 290 | this.img_A.classList.add('drag'); 291 | 292 | let flag = false; 293 | if (e.key == "=" || e.key == "+") { 294 | this.#scale = Math.min(this.#scale + 0.5, 10.0); 295 | flag = true; 296 | } 297 | if (e.key == "-") { 298 | this.#scale = Math.max(this.#scale - 0.5, 0.5) 299 | flag = true; 300 | } 301 | if (e.key == "ArrowUp") { 302 | this.#translateY += (100 / this.#scale); 303 | flag = true; 304 | } 305 | if (e.key == "ArrowDown") { 306 | this.#translateY -= (100 / this.#scale); 307 | flag = true; 308 | } 309 | if (e.key == "ArrowRight") { 310 | this.#translateX -= (100 / this.#scale); 311 | flag = true; 312 | } 313 | if (e.key == "ArrowLeft") { 314 | this.#translateX += (100 / this.#scale); 315 | flag = true; 316 | } 317 | 318 | if (e.key == "Backspace" || e.key == ")" || e.key == "Insert") { 319 | this.#translateX = 0.0; 320 | this.#translateY = 0.0; 321 | this.#scale = 1.0; 322 | flag = true; 323 | } 324 | 325 | if (flag) { 326 | e.preventDefault(); 327 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 328 | return false; 329 | } 330 | }); 331 | 332 | document.getElementById("icomp_in").addEventListener("click", () => { 333 | this.#scale = Math.min(this.#scale + 1.0, 10.0); 334 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 335 | }); 336 | 337 | document.getElementById("icomp_reset").addEventListener("click", () => { 338 | this.#translateX = 0.0; 339 | this.#translateY = 0.0; 340 | this.#scale = 1.0; 341 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 342 | }); 343 | 344 | document.getElementById("icomp_out").addEventListener("click", () => { 345 | this.#scale = Math.max(this.#scale - 1.0, 0.5); 346 | row.style.transform = `scale(${this.#scale}) translate(${this.#translateX}px, ${this.#translateY}px)`; 347 | }); 348 | 349 | ImageComparator.reset(); 350 | this.#addButtons(); 351 | this.#addTxt2ImgButton(); 352 | 353 | const container = document.createElement("div"); 354 | container.id = "img_comp_row_container"; 355 | row.parentNode.insertBefore(container, row); 356 | container.appendChild(row); 357 | } 358 | } 359 | 360 | onUiLoaded(() => { ImageComparator.init(); }); 361 | -------------------------------------------------------------------------------- /javascript/mouse_handler.js: -------------------------------------------------------------------------------- 1 | class MouseComparator { 2 | 3 | /** @param {string} src @param {boolean} is_a @param {boolean} auto */ 4 | static #send(src, side, auto) { 5 | (side ? ImageComparator.img_B : ImageComparator.img_A).src = src; 6 | ImageComparator.reset(); 7 | 8 | if (auto) ImageComparator.switch_to_comparison(); 9 | } 10 | 11 | static init() { 12 | const enable = document.getElementById("setting_comp_send_mouse_click").querySelector("input[type=checkbox]").checked; 13 | if (!enable) 14 | return; 15 | 16 | const auto = document.getElementById("setting_comp_send_mouse_auto").querySelector("input[type=checkbox]").checked; 17 | for (const [event, side] of [["click", false], ["contextmenu", true]]) { 18 | document.addEventListener(event, (e) => { 19 | if (!(e.ctrlKey && e.altKey)) 20 | return; 21 | 22 | const img = (e.target.nodeName.toLowerCase() === 'img') ? e.target : e.target.querySelector("img"); 23 | if (img != null) { 24 | this.#send(img.src, side, auto); 25 | e.preventDefault(); 26 | return false; 27 | } 28 | }); 29 | } 30 | } 31 | 32 | } 33 | 34 | onUiLoaded(() => { MouseComparator.init(); }); 35 | -------------------------------------------------------------------------------- /scripts/img_comp.py: -------------------------------------------------------------------------------- 1 | from modules.script_callbacks import on_ui_tabs, on_ui_settings 2 | from modules.images import read_info_from_image 3 | from modules.ui_components import ToolButton 4 | from modules.shared import OptionInfo, opts 5 | 6 | from PIL import Image 7 | import gradio as gr 8 | import re 9 | 10 | COMMA = re.compile(',(?=(?:[^"]*["][^"]*["])*[^"]*$)') 11 | is_gradio_4: bool = str(gr.__version__).startswith("4") 12 | 13 | 14 | def _gjs(func: str) -> dict: 15 | return {("js" if is_gradio_4 else "_js"): func} 16 | 17 | 18 | def parsePrompts(info: str) -> tuple[list[str], list[str], dict[str, str]]: 19 | info = info.replace("<", "<").replace(">", ">") 20 | 21 | positive: list[str] = [] 22 | negative: list[str] = [] 23 | params: dict[str, str] = {} 24 | 25 | if "Negative prompt:" in info: 26 | p_chunk, chunk = info.split("Negative prompt:") 27 | if "Steps" in chunk: 28 | n_chunk, args = chunk.split("Steps") 29 | else: 30 | n_chunk = chunk 31 | args = "" 32 | 33 | else: 34 | n_chunk = "" 35 | if "Steps" in info: 36 | p_chunk, args = info.split("Steps") 37 | else: 38 | p_chunk = info 39 | args = "" 40 | 41 | if p_chunk: 42 | positive = [tag.strip() for tag in p_chunk.split(",") if tag.strip()] 43 | 44 | if n_chunk: 45 | negative = [tag.strip() for tag in n_chunk.split(",") if tag.strip()] 46 | 47 | if args: 48 | chunks = re.split(COMMA, (f"Steps{args}")) 49 | for c in chunks: 50 | k, v = c.split(":", 1) 51 | params[k.strip()] = v.strip() 52 | 53 | return positive, negative, params 54 | 55 | 56 | def _to_diff(s: str) -> str: 57 | return f'{s}' if s else "" 58 | 59 | 60 | def img2info(imgA: Image.Image, imgB: Image.Image) -> list[str, str]: 61 | if (imgA is None) or (imgB is None): 62 | return [gr.update(value=""), gr.update(value="")] 63 | 64 | infoA, _ = read_info_from_image(imgA) 65 | infoB, _ = read_info_from_image(imgB) 66 | 67 | if (infoA is None) or (infoB is None): 68 | return [gr.update(value=""), gr.update(value="")] 69 | 70 | contentA: list[str] = [] 71 | contentB: list[str] = [] 72 | 73 | posA, negA, argsA = parsePrompts(infoA) 74 | posB, negB, argsB = parsePrompts(infoB) 75 | 76 | # === Positive Prompt === # 77 | 78 | if posA and posB: 79 | common_pos: list[str] = list(set(posA) & set(posB)) 80 | 81 | lineA: list[str] = [] 82 | lineB: list[str] = [] 83 | for tag in posA: 84 | lineA.append(tag if tag in common_pos else _to_diff(tag)) 85 | for tag in posB: 86 | lineB.append(tag if tag in common_pos else _to_diff(tag)) 87 | 88 | contentA.append(", ".join(lineA)) 89 | contentB.append(", ".join(lineB)) 90 | 91 | else: 92 | contentA.append(_to_diff(", ".join(posA))) 93 | contentB.append(_to_diff(", ".join(posB))) 94 | 95 | contentA[0] = f"Positive: {contentA[0]}" 96 | contentB[0] = f"Positive: {contentB[0]}" 97 | 98 | # === Negative Prompt === # 99 | 100 | if negA and negB: 101 | common_neg: list[str] = list(set(negA) & set(negB)) 102 | 103 | lineA: list[str] = [] 104 | lineB: list[str] = [] 105 | for tag in negA: 106 | lineA.append(tag if tag in common_neg else _to_diff(tag)) 107 | for tag in negB: 108 | lineB.append(tag if tag in common_neg else _to_diff(tag)) 109 | 110 | contentA.append(", ".join(lineA)) 111 | contentB.append(", ".join(lineB)) 112 | 113 | else: 114 | contentA.append(_to_diff(", ".join(negA))) 115 | contentB.append(_to_diff(", ".join(negB))) 116 | 117 | contentA[1] = f"Negative Prompt: {contentA[1]}" 118 | contentB[1] = f"Negative Prompt: {contentB[1]}" 119 | 120 | # === Parameters === # 121 | 122 | paramsA: list[str] = [] 123 | paramsB: list[str] = [] 124 | 125 | for key, val in argsA.items(): 126 | if key in argsB: 127 | if val == argsB[key]: 128 | paramsA.append(f"{key}: {val}") 129 | paramsB.append(f"{key}: {val}") 130 | else: 131 | paramsA.append(_to_diff(f"{key}: {val}")) 132 | paramsB.append(_to_diff(f"{key}: {argsB[key]}")) 133 | del argsB[key] 134 | else: 135 | paramsA.append(_to_diff(f"{key}: {val}")) 136 | 137 | for key, val in argsB.items(): 138 | paramsB.append(_to_diff(f"{key}: {val}")) 139 | 140 | contentA.append(f'Params: {", ".join(paramsA)}') 141 | contentB.append(f'Params: {", ".join(paramsB)}') 142 | 143 | return [ 144 | gr.update( 145 | value=f""" 146 |
Infotext
147 |

{'
'.join(contentA)}

148 | """ 149 | ), 150 | gr.update( 151 | value=f""" 152 |
Infotext
153 |

{'
'.join(contentB)}

154 | """ 155 | ), 156 | ] 157 | 158 | 159 | def img_ui(): 160 | dummy = Image.new("RGB", (16, 16), "dimgrey") 161 | 162 | with gr.Blocks() as IMG_COMP: 163 | with gr.Row(elem_id="img_comp_row"): 164 | gr.Image( 165 | value=dummy, 166 | image_mode="RGB", 167 | type="pil", 168 | show_download_button=False, 169 | interactive=False, 170 | container=False, 171 | height=768, 172 | elem_id="img_comp_A", 173 | ) 174 | 175 | gr.Image( 176 | value=dummy, 177 | image_mode="RGB", 178 | type="pil", 179 | show_download_button=False, 180 | interactive=False, 181 | container=False, 182 | height=768, 183 | elem_id="img_comp_B", 184 | ) 185 | 186 | with gr.Column(elem_classes="img_comp_buttons"): 187 | ToolButton( 188 | value="\U00002795", 189 | elem_id="icomp_in", 190 | tooltip="Zoom In", 191 | ) 192 | ToolButton( 193 | value="\U0001f504", 194 | elem_id="icomp_reset", 195 | tooltip="Reset to Default Scale", 196 | ) 197 | ToolButton( 198 | value="\U00002796", 199 | elem_id="icomp_out", 200 | tooltip="Zoom Out", 201 | ) 202 | 203 | with gr.Row(elem_id="img_comp_utils"): 204 | swap_btn = gr.Button("Swap", elem_id="img_comp_swap", scale=1) 205 | gr.Slider( 206 | label="Opacity", 207 | elem_id="img_comp_alpha", 208 | minimum=0.0, 209 | maximum=1.0, 210 | step=0.1, 211 | value=1.0, 212 | interactive=True, 213 | scale=3, 214 | ) 215 | dir_cb = gr.Checkbox( 216 | label="Horizontal Slider", 217 | elem_id="img_comp_horizontal", 218 | interactive=True, 219 | value=True, 220 | scale=1, 221 | ) 222 | 223 | with gr.Row(elem_id="img_comp_tools"): 224 | upload_a = gr.Image( 225 | image_mode="RGB", 226 | label="Image A", 227 | type="pil", 228 | sources="upload", 229 | show_download_button=False, 230 | interactive=True, 231 | height=256, 232 | elem_id="img_comp_input_A", 233 | ) 234 | 235 | with gr.Column(): 236 | comp_btn = gr.Button("Compare Upload", elem_id="img_comp_btn") 237 | i2i_btn = gr.Button("Compare img2img", elem_id="img_comp_i2i") 238 | inp_btn = gr.Button("Compare Inpaint", elem_id="img_comp_inpaint") 239 | ex_btn = gr.Button("Compare Extras", elem_id="img_comp_extras") 240 | 241 | upload_b = gr.Image( 242 | image_mode="RGB", 243 | label="Image B", 244 | type="pil", 245 | sources="upload", 246 | show_download_button=False, 247 | interactive=True, 248 | height=256, 249 | elem_id="img_comp_input_B", 250 | ) 251 | 252 | def js(mode: str) -> str: 253 | return f'() => {{ ImgCompLoader.loadImage("{mode}"); }}' 254 | 255 | comp_btn.click(fn=None, **_gjs(js("upload"))) 256 | i2i_btn.click(fn=None, **_gjs(js("i2i"))) 257 | inp_btn.click(fn=None, **_gjs(js("inpaint"))) 258 | ex_btn.click(fn=None, **_gjs(js("extras"))) 259 | 260 | with gr.Row(elem_id="img_comp_info"): 261 | infotextA = gr.HTML() 262 | infotextB = gr.HTML() 263 | 264 | upload_a.change(img2info, [upload_a, upload_b], [infotextA, infotextB]) 265 | upload_b.change(img2info, [upload_a, upload_b], [infotextA, infotextB]) 266 | 267 | swap_btn.click(fn=None, **_gjs("() => { ImgCompLoader.swapImage(); }")) 268 | dir_cb.change(fn=None, **_gjs("() => { ImageComparator.reset(); }")) 269 | 270 | return [(IMG_COMP, "Comparison", "sd-webui-image-comparison")] 271 | 272 | 273 | def on_settings(): 274 | args = {"section": ("icomp", "Image Comparison"), "category_id": "ui"} 275 | btn = ("Off", "Text", "Icon") 276 | 277 | opts.add_option( 278 | "comp_send_btn", 279 | OptionInfo( 280 | "Off", 281 | 'Add a "Send to Comparison" button under img2img and Extras generation result', 282 | gr.Radio, 283 | lambda: {"choices": btn}, 284 | **args, 285 | ).needs_reload_ui(), 286 | ) 287 | 288 | opts.add_option( 289 | "comp_send_btn_t2i", 290 | OptionInfo( 291 | "Off", 292 | 'Add a "Send to Comparison" button under txt2img generation result to compare against previous generation', 293 | gr.Radio, 294 | lambda: {"choices": btn}, 295 | **args, 296 | ).needs_reload_ui(), 297 | ) 298 | 299 | opts.add_option( 300 | "comp_send_mouse_click", 301 | OptionInfo( 302 | False, 303 | 'Use "Ctrl" + "Alt" + "Left Click" / "Right Click" to quickly send any image to comparison', 304 | **args, 305 | ).needs_reload_ui(), 306 | ) 307 | 308 | opts.add_option( 309 | "comp_send_mouse_auto", 310 | OptionInfo( 311 | False, 312 | "Switch to the Comparison tab afterwards", 313 | **args, 314 | ) 315 | .info("for the above option") 316 | .needs_reload_ui(), 317 | ) 318 | 319 | 320 | on_ui_tabs(img_ui) 321 | on_ui_settings(on_settings) 322 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | #tab_sd-webui-image-comparison { 2 | overflow: hidden; 3 | --img-comp-width: 768px; 4 | } 5 | 6 | #img_comp_row:focus { 7 | outline: none; 8 | } 9 | 10 | #img_comp_row { 11 | height: var(--img-comp-width); 12 | user-select: none; 13 | } 14 | 15 | #img_comp_row img { 16 | position: relative; 17 | width: 100%; 18 | height: 100%; 19 | object-fit: contain; 20 | } 21 | 22 | #img_comp_row img.drag:hover { 23 | cursor: move !important; 24 | } 25 | 26 | #img_comp_row img.hor:hover { 27 | cursor: ew-resize; 28 | } 29 | 30 | #img_comp_row img.ver:hover { 31 | cursor: ns-resize; 32 | } 33 | 34 | #img_comp_row .comp-block { 35 | width: var(--img-comp-width); 36 | height: var(--img-comp-width); 37 | position: absolute; 38 | z-index: 100; 39 | left: calc(50% - (var(--img-comp-width) / 2)); 40 | border-radius: 0px; 41 | } 42 | 43 | #img_comp_row .bar { 44 | display: block; 45 | position: absolute; 46 | background-color: white; 47 | z-index: 500; 48 | min-width: 0px; 49 | min-height: 0px; 50 | opacity: 0.9; 51 | pointer-events: none; 52 | } 53 | 54 | #img_comp_btn { 55 | margin-top: auto; 56 | } 57 | 58 | #img_comp_extras { 59 | margin-bottom: auto; 60 | } 61 | 62 | #img_comp_horizontal { 63 | margin: 1em; 64 | } 65 | 66 | #img_comp_utils { 67 | user-select: none; 68 | } 69 | 70 | #img_comp_tools { 71 | background-color: var(--panel-background-fill); 72 | padding: 1em; 73 | border-radius: 1em; 74 | user-select: none; 75 | } 76 | 77 | #img_comp_info p { 78 | background: var(--panel-background-fill); 79 | padding: 1em; 80 | border-radius: 1em; 81 | } 82 | 83 | #img_comp_info p .comp-diff { 84 | color: red; 85 | } 86 | 87 | #img_comp_row_container { 88 | height: var(--img-comp-width); 89 | width: 100%; 90 | overflow: hidden; 91 | } 92 | 93 | #icomp_in { 94 | cursor: zoom-in; 95 | } 96 | 97 | #icomp_out { 98 | cursor: zoom-out; 99 | } 100 | 101 | #tab_sd-webui-image-comparison .img_comp_buttons { 102 | min-width: 0px !important; 103 | display: block; 104 | position: absolute; 105 | left: 1em; 106 | top: 25%; 107 | width: 2.4em; 108 | } 109 | -------------------------------------------------------------------------------- /tab.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Haoming02/sd-webui-image-comparison/4354edab87ca6634fe1f0b94d3e0dba65f6221f0/tab.gif --------------------------------------------------------------------------------