Repository: AUTOMATIC1111/stable-diffusion-webui

Issue:
[Feature Request]: xy plot, "dividing" lines between batches
### Is there an existing issue for this?

- [X] I have searched the existing issues and checked the recent builds/commits

### What would your feature do ?

Add a few px of space betwen different columns and rows. the bathes themselves that are from the same X and Y option, can stay together.

Currently it can be tedious in big grids to figure out which picture goes to which option when like 8+ images are created per option. especially for those on the edge.

### Proposed workflow

add checkbox "add margins" or similar, and just add a few PX of black between batches

### Additional information

-


Discussion:

Resolving patch:
diff --git a/modules/images.py b/modules/images.py
--- a/modules/images.py
+++ b/modules/images.py
@@ -199,7 +199,7 @@ def draw_texts(drawing, draw_x, draw_y, lines, initial_fnt, initial_fontsize):
 
     pad_top = 0 if sum(hor_text_heights) == 0 else max(hor_text_heights) + line_spacing * 2
 
-    result = Image.new("RGB", (im.width + pad_left + margin * (rows-1), im.height + pad_top + margin * (cols-1)), "white")
+    result = Image.new("RGB", (im.width + pad_left + margin * (cols-1), im.height + pad_top + margin * (rows-1)), "white")
 
     for row in range(rows):
         for col in range(cols):
@@ -223,7 +223,7 @@ def draw_texts(drawing, draw_x, draw_y, lines, initial_fnt, initial_fontsize):
     return result
 
 
-def draw_prompt_matrix(im, width, height, all_prompts):
+def draw_prompt_matrix(im, width, height, all_prompts, margin=0):
     prompts = all_prompts[1:]
     boundary = math.ceil(len(prompts) / 2)
 
@@ -233,7 +233,7 @@ def draw_prompt_matrix(im, width, height, all_prompts):
     hor_texts = [[GridAnnotation(x, is_active=pos & (1 << i) != 0) for i, x in enumerate(prompts_horiz)] for pos in range(1 << len(prompts_horiz))]
     ver_texts = [[GridAnnotation(x, is_active=pos & (1 << i) != 0) for i, x in enumerate(prompts_vert)] for pos in range(1 << len(prompts_vert))]
 
-    return draw_grid_annotations(im, width, height, hor_texts, ver_texts)
+    return draw_grid_annotations(im, width, height, hor_texts, ver_texts, margin)
 
 
 def resize_image(resize_mode, im, width, height, upscaler_name=None):
diff --git a/scripts/prompt_matrix.py b/scripts/prompt_matrix.py
--- a/scripts/prompt_matrix.py
+++ b/scripts/prompt_matrix.py
@@ -48,23 +48,17 @@ def ui(self, is_img2img):
         gr.HTML('<br />')
         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"))
+                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():
-                # Radio buttons for selecting the prompt between positive and negative
-                prompt_type = gr.Radio(["positive", "negative"], label="Select prompt",
-                                       elem_id=self.elem_id("prompt_type"), value="positive")
-        with gr.Row():
-            with gr.Column():
-                different_seeds = gr.Checkbox(
-                    label='Use different seed for each picture', value=False, elem_id=self.elem_id("different_seeds"))
+                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():
-                # Radio buttons for selecting the delimiter to use in the resulting prompt
-                variations_delimiter = gr.Radio(["comma", "space"], label="Select delimiter", elem_id=self.elem_id(
-                    "variations_delimiter"), value="comma")
-        return [put_at_start, different_seeds, prompt_type, variations_delimiter]
+                margin_size = gr.Slider(label="Grid margins (px)", min=0, max=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):
+    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"]:
@@ -106,7 +100,7 @@ def run(self, p, put_at_start, different_seeds, prompt_type, variations_delimite
         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, p.width, p.height, prompt_matrix_parts)
+        grid = images.draw_prompt_matrix(grid, p.width, p.height, prompt_matrix_parts, margin_size)
         processed.images.insert(0, grid)
         processed.index_of_first_image = 1
         processed.infotexts.insert(0, processed.infotexts[0])
diff --git a/scripts/xyz_grid.py b/scripts/xyz_grid.py
--- a/scripts/xyz_grid.py
+++ b/scripts/xyz_grid.py
@@ -205,7 +205,7 @@ def __init__(self, *args, **kwargs):
 ]
 
 
-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):
+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]
@@ -292,7 +292,7 @@ def index(ix, iy, iz):
         end_index = start_index + len(xs) * len(ys)
         grid = images.image_grid(image_cache[start_index:end_index], rows=len(ys))
         if draw_legend:
-            grid = images.draw_grid_annotations(grid, cell_size[0], cell_size[1], hor_texts, ver_texts)
+            grid = images.draw_grid_annotations(grid, cell_size[0], cell_size[1], hor_texts, ver_texts, margin_size)
         sub_grids[i] = grid
         if include_sub_grids and len(zs) > 1:
             processed_result.images.insert(i+1, grid)
@@ -351,10 +351,16 @@ def ui(self, is_img2img):
                     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"):
-            draw_legend = gr.Checkbox(label='Draw legend', value=True, elem_id=self.elem_id("draw_legend"))
-            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"))
-            no_fixed_seeds = gr.Checkbox(label='Keep -1 for seeds', value=False, elem_id=self.elem_id("no_fixed_seeds"))
+            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)", min=0, max=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")
@@ -393,9 +399,9 @@ def select_axis(x_type):
             (z_values, "Z Values"),
         )
 
-        return [x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds]
+        return [x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size]
 
-    def run(self, p, x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds):
+    def run(self, p, x_type, x_values, y_type, y_values, z_type, z_values, draw_legend, include_lone_images, include_sub_grids, no_fixed_seeds, margin_size):
         if not no_fixed_seeds:
             modules.processing.fix_seed(p)
 
@@ -590,7 +596,8 @@ def cell(x, y, z):
                 include_lone_images=include_lone_images,
                 include_sub_grids=include_sub_grids,
                 first_axes_processed=first_axes_processed,
-                second_axes_processed=second_axes_processed
+                second_axes_processed=second_axes_processed,
+                margin_size=margin_size
             )
 
         if opts.grid_save and len(sub_grids) > 1:


Test patch: