├── .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
--------------------------------------------------------------------------------