From da8f931d775490b88e73b7b30ee2bc278b673b94 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Fri, 12 Jun 2026 23:12:14 +0530 Subject: [PATCH 1/8] Add Anima regional control support --- ai_diffusion/backend/resources.py | 5 ++++- ai_diffusion/model/control.py | 31 +++++++++++++++++++++++++++++-- ai_diffusion/presets/control.json | 21 ++++++++++++++++++++- ai_diffusion/ui/control.py | 3 ++- 4 files changed, 55 insertions(+), 5 deletions(-) diff --git a/ai_diffusion/backend/resources.py b/ai_diffusion/backend/resources.py index 2830ee27d..f1e8bff5f 100644 --- a/ai_diffusion/backend/resources.py +++ b/ai_diffusion/backend/resources.py @@ -181,7 +181,7 @@ def has_controlnet_inpaint(self): @property def supports_regions(self): - return self in [Arch.sd15, Arch.sdxl, Arch.illu, Arch.illu_v] + return self in [Arch.sd15, Arch.sdxl, Arch.illu, Arch.illu_v, Arch.anima] @property def supports_lcm(self): @@ -321,6 +321,7 @@ class ControlMode(Enum): blur = 10 stencil = 11 hands = 12 + regional = 17 @property def is_lines(self): @@ -338,6 +339,7 @@ def has_preprocessor(self): ControlMode.blur, ControlMode.stencil, ControlMode.universal, + ControlMode.regional, ] @property @@ -738,6 +740,7 @@ def is_required(kind: ResourceKind, arch: Arch, identifier: ControlMode | Upscal resource_id(ResourceKind.controlnet, Arch.illu, ControlMode.universal): ["union-sdxl", "xinsirunion"], resource_id(ResourceKind.controlnet, Arch.illu_v, ControlMode.universal): ["union-sdxl", "xinsirunion"], resource_id(ResourceKind.controlnet, Arch.anima, ControlMode.universal): ["anima*lllite*any"], + resource_id(ResourceKind.controlnet, Arch.anima, ControlMode.regional): ["anima-lllite-region-cn", "anima*lllite*region"], resource_id(ResourceKind.controlnet, Arch.flux, ControlMode.universal): ["flux.1-dev-controlnet-union-pro-2.0", "flux.1-dev-controlnet-union-pro", "flux.1-dev-controlnet-union", "flux1devcontrolnetunion"], resource_id(ResourceKind.controlnet, Arch.qwen, ControlMode.universal): ["qwen-image-instantx-controlnet-union"], resource_id(ResourceKind.controlnet, Arch.sd15, ControlMode.scribble): ["control_v11p_sd15_scribble", "control_lora_rank128_v11p_sd15_scribble"], diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 961be2cd9..1032e47a2 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -123,7 +123,11 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): if self.mode.is_ip_adapter and not layer.bounds.is_zero: bounds = None # ignore mask bounds, use layer bounds - image = layer.get_pixels(bounds, time) + image = ( + self._regional_control_image(bounds, time) + if self.mode is ControlMode.regional + else layer.get_pixels(bounds, time) + ) if self.mode.is_lines or self.mode is ControlMode.stencil: image.make_opaque(background=Qt.GlobalColor.white) @@ -139,6 +143,25 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): strength = self.strength / self.strength_multiplier return ControlInput(self.mode, image, strength, (self.start, self.end)) + def _regional_control_image(self, bounds: Bounds | None, time: int | None): + from .region import RegionLink + + bounds = bounds or Bounds.from_extent(self._model.document.extent) + image = Image.create(bounds.extent, fill=Qt.GlobalColor.white) + root = self._model.regions + + for layer in root.layers.all: + if root.find_linked(layer, RegionLink.direct) is None: + continue + if layer.compute_bounds().area == 0: + continue + if Bounds.intersection(bounds, layer.bounds).area == 0: + continue + + image.draw_image(layer.get_pixels(bounds, time)) + + return image + def generate(self): self._generate_job = self._model.generate_control_layer(self) self.has_active_job = True @@ -160,7 +183,10 @@ def _update_is_supported(self): self.error_text = _("The server is missing the ClipVision model") + f" {search}" is_supported = False - if self.mode.is_ip_adapter and models.arch.supports_edit: + if self.mode is ControlMode.regional and models.arch is not Arch.anima: + self.error_text = _("Not supported for") + f" {models.arch.value}" + is_supported = False + elif self.mode.is_ip_adapter and models.arch.supports_edit: is_supported = True # Reference images are merged into the conditioning context elif self.mode.is_ip_adapter and models.ip_adapter.find(self.mode) is None: search_path = resources.search_path(ResourceKind.ip_adapter, models.arch, self.mode) @@ -405,6 +431,7 @@ def _validate_presets(filepath: Path, data: dict[str, Any]) -> bool: ControlMode.blur: _("Unblur"), ControlMode.stencil: _("Stencil"), ControlMode.hands: _("Hands"), + ControlMode.regional: _("Regional"), } diff --git a/ai_diffusion/presets/control.json b/ai_diffusion/presets/control.json index d4561ff1e..db11700d0 100644 --- a/ai_diffusion/presets/control.json +++ b/ai_diffusion/presets/control.json @@ -199,5 +199,24 @@ "end": 1.0 } ] + }, + "regional": { + "all": [ + { + "strength": 0.75, + "start": 0.0, + "end": 0.45 + }, + { + "strength": 1.0, + "start": 0.0, + "end": 0.45 + }, + { + "strength": 1.5, + "start": 0.0, + "end": 0.45 + } + ] } -} \ No newline at end of file +} diff --git a/ai_diffusion/ui/control.py b/ai_diffusion/ui/control.py index 7981714e5..9648f97d2 100644 --- a/ai_diffusion/ui/control.py +++ b/ai_diffusion/ui/control.py @@ -41,7 +41,8 @@ def __init__( self.mode_select = QComboBox(self) self.mode_select.setStyleSheet(theme.flat_combo_stylesheet) for mode in (m for m in ControlMode if not m.is_internal): - icon = theme.icon(f"control-{mode.name}") + icon_name = "region-prompt" if mode is ControlMode.regional else f"control-{mode.name}" + icon = theme.icon(icon_name) self.mode_select.addItem(icon, mode.text, mode) self.layer_select = QComboBox(self) From a702110dd5421d816f329a2411c470c59a8e9fd7 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Sat, 13 Jun 2026 16:10:58 +0530 Subject: [PATCH 2/8] Fixed regional controlnet only usage --- ai_diffusion/model/control.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 1032e47a2..964d76c70 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -123,11 +123,13 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): if self.mode.is_ip_adapter and not layer.bounds.is_zero: bounds = None # ignore mask bounds, use layer bounds - image = ( - self._regional_control_image(bounds, time) - if self.mode is ControlMode.regional - else layer.get_pixels(bounds, time) - ) + image = None + if self.mode is ControlMode.regional: + image = self._regional_control_image(bounds, time) + if image is None: + image = self._regional_control_image_from_layer(layer, bounds, time) + if image is None: + image = layer.get_pixels(bounds, time) if self.mode.is_lines or self.mode is ControlMode.stencil: image.make_opaque(background=Qt.GlobalColor.white) @@ -143,11 +145,20 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): strength = self.strength / self.strength_multiplier return ControlInput(self.mode, image, strength, (self.start, self.end)) + def _regional_control_image_from_layer( + self, layer: Layer, bounds: Bounds | None, time: int | None + ): + bounds = bounds or Bounds.from_extent(self._model.document.extent) + image = Image.create(bounds.extent, fill=Qt.GlobalColor.white) + image.draw_image(layer.get_pixels(bounds, time)) + return image + def _regional_control_image(self, bounds: Bounds | None, time: int | None): from .region import RegionLink bounds = bounds or Bounds.from_extent(self._model.document.extent) image = Image.create(bounds.extent, fill=Qt.GlobalColor.white) + has_region_layer = False root = self._model.regions for layer in root.layers.all: @@ -159,8 +170,9 @@ def _regional_control_image(self, bounds: Bounds | None, time: int | None): continue image.draw_image(layer.get_pixels(bounds, time)) + has_region_layer = True - return image + return image if has_region_layer else None def generate(self): self._generate_job = self._model.generate_control_layer(self) From 8e98cd4f849b275305cceff254d671e563cfcf27 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Sat, 27 Jun 2026 13:54:49 +0530 Subject: [PATCH 3/8] Removed regional control mode and integrated it as segmentation control mode support for Anima --- ai_diffusion/backend/resources.py | 4 +--- ai_diffusion/model/control.py | 24 +++++++++++++----------- ai_diffusion/presets/control.json | 4 ++-- ai_diffusion/ui/control.py | 3 +-- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/ai_diffusion/backend/resources.py b/ai_diffusion/backend/resources.py index f1e8bff5f..ea77fc60a 100644 --- a/ai_diffusion/backend/resources.py +++ b/ai_diffusion/backend/resources.py @@ -321,7 +321,6 @@ class ControlMode(Enum): blur = 10 stencil = 11 hands = 12 - regional = 17 @property def is_lines(self): @@ -339,7 +338,6 @@ def has_preprocessor(self): ControlMode.blur, ControlMode.stencil, ControlMode.universal, - ControlMode.regional, ] @property @@ -740,7 +738,7 @@ def is_required(kind: ResourceKind, arch: Arch, identifier: ControlMode | Upscal resource_id(ResourceKind.controlnet, Arch.illu, ControlMode.universal): ["union-sdxl", "xinsirunion"], resource_id(ResourceKind.controlnet, Arch.illu_v, ControlMode.universal): ["union-sdxl", "xinsirunion"], resource_id(ResourceKind.controlnet, Arch.anima, ControlMode.universal): ["anima*lllite*any"], - resource_id(ResourceKind.controlnet, Arch.anima, ControlMode.regional): ["anima-lllite-region-cn", "anima*lllite*region"], + resource_id(ResourceKind.controlnet, Arch.anima, ControlMode.segmentation): ["anima-lllite-region-cn", "anima*lllite*region"], resource_id(ResourceKind.controlnet, Arch.flux, ControlMode.universal): ["flux.1-dev-controlnet-union-pro-2.0", "flux.1-dev-controlnet-union-pro", "flux.1-dev-controlnet-union", "flux1devcontrolnetunion"], resource_id(ResourceKind.controlnet, Arch.qwen, ControlMode.universal): ["qwen-image-instantx-controlnet-union"], resource_id(ResourceKind.controlnet, Arch.sd15, ControlMode.scribble): ["control_v11p_sd15_scribble", "control_lora_rank128_v11p_sd15_scribble"], diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 964d76c70..8b136b7fa 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -124,10 +124,10 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): bounds = None # ignore mask bounds, use layer bounds image = None - if self.mode is ControlMode.regional: - image = self._regional_control_image(bounds, time) + if self._is_anima_segmentation: + image = self._segmentation_control_image(bounds, time) if image is None: - image = self._regional_control_image_from_layer(layer, bounds, time) + image = self._segmentation_control_image_from_layer(layer, bounds, time) if image is None: image = layer.get_pixels(bounds, time) @@ -145,7 +145,11 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): strength = self.strength / self.strength_multiplier return ControlInput(self.mode, image, strength, (self.start, self.end)) - def _regional_control_image_from_layer( + @property + def _is_anima_segmentation(self): + return self.mode is ControlMode.segmentation and self._model.arch is Arch.anima + + def _segmentation_control_image_from_layer( self, layer: Layer, bounds: Bounds | None, time: int | None ): bounds = bounds or Bounds.from_extent(self._model.document.extent) @@ -153,7 +157,7 @@ def _regional_control_image_from_layer( image.draw_image(layer.get_pixels(bounds, time)) return image - def _regional_control_image(self, bounds: Bounds | None, time: int | None): + def _segmentation_control_image(self, bounds: Bounds | None, time: int | None): from .region import RegionLink bounds = bounds or Bounds.from_extent(self._model.document.extent) @@ -175,6 +179,8 @@ def _regional_control_image(self, bounds: Bounds | None, time: int | None): return image if has_region_layer else None def generate(self): + if not self.can_generate: + return self._generate_job = self._model.generate_control_layer(self) self.has_active_job = True @@ -195,10 +201,7 @@ def _update_is_supported(self): self.error_text = _("The server is missing the ClipVision model") + f" {search}" is_supported = False - if self.mode is ControlMode.regional and models.arch is not Arch.anima: - self.error_text = _("Not supported for") + f" {models.arch.value}" - is_supported = False - elif self.mode.is_ip_adapter and models.arch.supports_edit: + if self.mode.is_ip_adapter and models.arch.supports_edit: is_supported = True # Reference images are merged into the conditioning context elif self.mode.is_ip_adapter and models.ip_adapter.find(self.mode) is None: search_path = resources.search_path(ResourceKind.ip_adapter, models.arch, self.mode) @@ -238,7 +241,7 @@ def _update_is_supported(self): is_supported = False self.is_supported = is_supported - self.can_generate = is_supported and self.mode.has_preprocessor + self.can_generate = is_supported and self.mode.has_preprocessor and not self._is_anima_segmentation def _update_is_pose_vector(self): self.is_pose_vector = self.mode is ControlMode.pose and self.layer.type is LayerType.vector @@ -443,7 +446,6 @@ def _validate_presets(filepath: Path, data: dict[str, Any]) -> bool: ControlMode.blur: _("Unblur"), ControlMode.stencil: _("Stencil"), ControlMode.hands: _("Hands"), - ControlMode.regional: _("Regional"), } diff --git a/ai_diffusion/presets/control.json b/ai_diffusion/presets/control.json index db11700d0..2a43fe5cc 100644 --- a/ai_diffusion/presets/control.json +++ b/ai_diffusion/presets/control.json @@ -200,8 +200,8 @@ } ] }, - "regional": { - "all": [ + "segmentation": { + "anima": [ { "strength": 0.75, "start": 0.0, diff --git a/ai_diffusion/ui/control.py b/ai_diffusion/ui/control.py index 9648f97d2..7981714e5 100644 --- a/ai_diffusion/ui/control.py +++ b/ai_diffusion/ui/control.py @@ -41,8 +41,7 @@ def __init__( self.mode_select = QComboBox(self) self.mode_select.setStyleSheet(theme.flat_combo_stylesheet) for mode in (m for m in ControlMode if not m.is_internal): - icon_name = "region-prompt" if mode is ControlMode.regional else f"control-{mode.name}" - icon = theme.icon(icon_name) + icon = theme.icon(f"control-{mode.name}") self.mode_select.addItem(icon, mode.text, mode) self.layer_select = QComboBox(self) From 050d1789bf95780b2e2275bb79b82b64c0bbb43b Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Sun, 28 Jun 2026 12:29:07 +0530 Subject: [PATCH 4/8] Add explicit region segmentation control layer generation --- ai_diffusion/model/control.py | 94 ++++++++++++++++++++----------- ai_diffusion/presets/control.json | 17 ++++++ ai_diffusion/ui/control.py | 30 ++++++++++ 3 files changed, 109 insertions(+), 32 deletions(-) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 8b136b7fa..40b569658 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -5,12 +5,13 @@ from typing import Any, NamedTuple from PyQt5.QtCore import QObject, Qt, QUuid, pyqtSignal +from PyQt5.QtGui import QColor from .. import util from ..backend import resources from ..backend.api import ControlInput from ..backend.resources import Arch, ControlMode, ResourceKind, resource_id -from ..image import Bounds, Extent, Image +from ..image import BlendMode, Bounds, Extent, Image from ..layer import Layer, LayerType from ..localization import translate as _ from ..util import PluginError @@ -23,6 +24,20 @@ class ControlLayer(QObject, ObservableProperties): max_preset_value = 4 strength_multiplier = 50 clip_vision_extent = Extent(224, 224) + segmentation_colors = [ + (120, 120, 120), + (180, 120, 120), + (120, 180, 120), + (120, 120, 180), + (180, 180, 120), + (180, 120, 180), + (120, 180, 180), + (220, 140, 100), + (140, 220, 100), + (100, 140, 220), + (220, 100, 140), + (100, 220, 140), + ] mode = Property(ControlMode.reference, persist=True, setter="set_mode") layer_id = Property(QUuid(), persist=True) @@ -123,13 +138,7 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): if self.mode.is_ip_adapter and not layer.bounds.is_zero: bounds = None # ignore mask bounds, use layer bounds - image = None - if self._is_anima_segmentation: - image = self._segmentation_control_image(bounds, time) - if image is None: - image = self._segmentation_control_image_from_layer(layer, bounds, time) - if image is None: - image = layer.get_pixels(bounds, time) + image = layer.get_pixels(bounds, time) if self.mode.is_lines or self.mode is ControlMode.stencil: image.make_opaque(background=Qt.GlobalColor.white) @@ -145,39 +154,60 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): strength = self.strength / self.strength_multiplier return ControlInput(self.mode, image, strength, (self.start, self.end)) - @property - def _is_anima_segmentation(self): - return self.mode is ControlMode.segmentation and self._model.arch is Arch.anima + def generate_segmentation(self): + if self.mode is not ControlMode.segmentation: + return - def _segmentation_control_image_from_layer( - self, layer: Layer, bounds: Bounds | None, time: int | None - ): - bounds = bounds or Bounds.from_extent(self._model.document.extent) - image = Image.create(bounds.extent, fill=Qt.GlobalColor.white) - image.draw_image(layer.get_pixels(bounds, time)) - return image + ok, msg = self._model.document.check_color_mode() + if not ok and msg: + self._model.report_error(msg) + return + + try: + bounds = Bounds.from_extent(self._model.document.extent) + image = self._segmentation_image_from_regions(bounds) + if image is None: + self._model.report_error(_("Text prompt regions have not been set up.")) + return - def _segmentation_control_image(self, bounds: Bounds | None, time: int | None): + layer = self._model.layers.create(f"[Control] {self.mode.text}", image, bounds) + self.layer_id = layer.id + except Exception as e: + self._model.report_error(util.log_error(e)) + else: + self._model.clear_error() + + def _segmentation_image_from_regions(self, bounds: Bounds): from .region import RegionLink - bounds = bounds or Bounds.from_extent(self._model.document.extent) image = Image.create(bounds.extent, fill=Qt.GlobalColor.white) has_region_layer = False - root = self._model.regions - - for layer in root.layers.all: - if root.find_linked(layer, RegionLink.direct) is None: - continue - if layer.compute_bounds().area == 0: - continue - if Bounds.intersection(bounds, layer.bounds).area == 0: - continue - - image.draw_image(layer.get_pixels(bounds, time)) + root = self._model.active_regions + + layers = [ + layer + for layer in root.layers.all + if root.find_linked(layer, RegionLink.direct) is not None + and layer.compute_bounds().area > 0 + and Bounds.intersection(bounds, layer.bounds).area > 0 + ] + + for index, layer in enumerate(layers): + color = self.segmentation_colors[index % len(self.segmentation_colors)] + region_image = self._segmentation_region_image(layer, bounds, color) + image.draw_image(region_image, blend=BlendMode.alpha) has_region_layer = True return image if has_region_layer else None + def _segmentation_region_image( + self, layer: Layer, bounds: Bounds, color: tuple[int, int, int] + ): + mask = layer.get_mask(bounds) + image = Image.create(bounds.extent, fill=QColor(*color, 255)) + image._qimage.setAlphaChannel(mask._qimage) + return image + def generate(self): if not self.can_generate: return @@ -241,7 +271,7 @@ def _update_is_supported(self): is_supported = False self.is_supported = is_supported - self.can_generate = is_supported and self.mode.has_preprocessor and not self._is_anima_segmentation + self.can_generate = is_supported and self.mode.has_preprocessor def _update_is_pose_vector(self): self.is_pose_vector = self.mode is ControlMode.pose and self.layer.type is LayerType.vector diff --git a/ai_diffusion/presets/control.json b/ai_diffusion/presets/control.json index 2a43fe5cc..99d5a5a81 100644 --- a/ai_diffusion/presets/control.json +++ b/ai_diffusion/presets/control.json @@ -201,6 +201,23 @@ ] }, "segmentation": { + "all": [ + { + "strength": 0.7, + "start": 0.0, + "end": 0.5 + }, + { + "strength": 1.0, + "start": 0.0, + "end": 0.8 + }, + { + "strength": 1.0, + "start": 0.0, + "end": 1.0 + } + ], "anima": [ { "strength": 0.75, diff --git a/ai_diffusion/ui/control.py b/ai_diffusion/ui/control.py index 7981714e5..07fccc181 100644 --- a/ai_diffusion/ui/control.py +++ b/ai_diffusion/ui/control.py @@ -73,6 +73,11 @@ def __init__( ) self.generate_tool_button.clicked.connect(control.generate) + self.generate_regions_tool_button = _create_generate_regions_button( + self, Qt.ToolButtonStyle.ToolButtonIconOnly + ) + self.generate_regions_tool_button.clicked.connect(control.generate_segmentation) + self.add_pose_tool_button = _create_add_pose_button( self, Qt.ToolButtonStyle.ToolButtonIconOnly ) @@ -91,6 +96,7 @@ def __init__( bar_layout.addWidget(self.mode_select) bar_layout.addWidget(self.layer_select, 3) bar_layout.addWidget(self.generate_tool_button) + bar_layout.addWidget(self.generate_regions_tool_button) bar_layout.addWidget(self.add_pose_tool_button) bar_layout.addWidget(self.preset_slider, 1) bar_layout.addWidget(self.error_text, 3) @@ -129,6 +135,11 @@ def __init__( ) self.generate_button.clicked.connect(control.generate) + self.generate_regions_button = _create_generate_regions_button( + self.extended_widget, Qt.ToolButtonStyle.ToolButtonTextBesideIcon + ) + self.generate_regions_button.clicked.connect(control.generate_segmentation) + self.add_pose_button = _create_add_pose_button( self.extended_widget, Qt.ToolButtonStyle.ToolButtonTextBesideIcon ) @@ -141,6 +152,7 @@ def __init__( actions_layout = QHBoxLayout() actions_layout.addWidget(self.custom_checkbox, stretch=1) actions_layout.addWidget(self.generate_button) + actions_layout.addWidget(self.generate_regions_button) actions_layout.addWidget(self.add_pose_button) extended_layout.addLayout(actions_layout) @@ -228,6 +240,7 @@ def _add_pose_character(self): def _update_visibility(self): is_small = self.width() < 420 is_pose = self._control.mode is ControlMode.pose + is_segmentation = self._control.mode is ControlMode.segmentation is_edit = root.active_model.arch.supports_edit def controls(): @@ -236,6 +249,12 @@ def controls(): self.expand_button.setVisible(self._control.is_supported and not is_edit) self.generate_button.setVisible(self._control.can_generate and is_small) self.generate_tool_button.setVisible(self._control.can_generate and not is_small) + self.generate_regions_button.setVisible( + self._control.is_supported and is_segmentation and is_small + ) + self.generate_regions_tool_button.setVisible( + self._control.is_supported and is_segmentation and not is_small + ) self.add_pose_button.setVisible(is_pose and is_small) self.add_pose_tool_button.setVisible(is_pose and not is_small) self.range_label.setVisible(self._control.has_range) @@ -272,6 +291,8 @@ def _update_strength(self): def _update_job_active(self): self.generate_button.setEnabled(not self._control.has_active_job) self.generate_tool_button.setEnabled(not self._control.has_active_job) + self.generate_regions_button.setEnabled(not self._control.has_active_job) + self.generate_regions_tool_button.setEnabled(not self._control.has_active_job) self.layer_select.setEnabled(not self._control.has_active_job) def _update_custom_values(self): @@ -309,6 +330,15 @@ def _create_generate_button(parent, style: Qt.ToolButtonStyle): return button +def _create_generate_regions_button(parent, style: Qt.ToolButtonStyle): + button = QToolButton(parent) + button.setToolButtonStyle(style) + button.setText(_("From Regions")) + button.setIcon(theme.icon("region-prompt")) + button.setToolTip(_("Generate segmentation control layer from current regions")) + return button + + def _create_add_pose_button(parent, style: Qt.ToolButtonStyle): button = QToolButton(parent) button.setToolButtonStyle(style) From 9fa5755755abbd80003c13a56a72e29ace3015d1 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Sun, 28 Jun 2026 12:42:23 +0530 Subject: [PATCH 5/8] Fill transparent segmentation controls with white --- ai_diffusion/model/control.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 40b569658..944d4a57d 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -140,6 +140,9 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): image = layer.get_pixels(bounds, time) + if self.mode is ControlMode.segmentation: + image.make_opaque(background=Qt.GlobalColor.white) + if self.mode.is_lines or self.mode is ControlMode.stencil: image.make_opaque(background=Qt.GlobalColor.white) From dc7dbc46d9dc80464137c867e448f9ae0e14a3d3 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Sun, 28 Jun 2026 23:03:46 +0530 Subject: [PATCH 6/8] Use computed region bounds for segmentation control --- ai_diffusion/model/control.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 944d4a57d..38a88f82d 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -158,8 +158,7 @@ def to_api(self, bounds: Bounds | None = None, time: int | None = None): return ControlInput(self.mode, image, strength, (self.start, self.end)) def generate_segmentation(self): - if self.mode is not ControlMode.segmentation: - return + assert self.mode is ControlMode.segmentation ok, msg = self._model.document.check_color_mode() if not ok and msg: @@ -191,8 +190,7 @@ def _segmentation_image_from_regions(self, bounds: Bounds): layer for layer in root.layers.all if root.find_linked(layer, RegionLink.direct) is not None - and layer.compute_bounds().area > 0 - and Bounds.intersection(bounds, layer.bounds).area > 0 + and Bounds.intersection(bounds, layer.compute_bounds()).area > 0 ] for index, layer in enumerate(layers): From 9b5e085f8c07fe82d61afbd024d6e82855bb25a2 Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Tue, 30 Jun 2026 18:20:26 +0530 Subject: [PATCH 7/8] cleanup --- ai_diffusion/model/control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index 38a88f82d..add388f16 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -210,8 +210,6 @@ def _segmentation_region_image( return image def generate(self): - if not self.can_generate: - return self._generate_job = self._model.generate_control_layer(self) self.has_active_job = True From f51f547a84253018bd9000743d19a6cadc3486ab Mon Sep 17 00:00:00 2001 From: Sen-sou Date: Tue, 30 Jun 2026 18:23:08 +0530 Subject: [PATCH 8/8] lint --- ai_diffusion/model/control.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ai_diffusion/model/control.py b/ai_diffusion/model/control.py index add388f16..c3a67f938 100644 --- a/ai_diffusion/model/control.py +++ b/ai_diffusion/model/control.py @@ -2,7 +2,7 @@ import json from pathlib import Path -from typing import Any, NamedTuple +from typing import Any, ClassVar, NamedTuple from PyQt5.QtCore import QObject, Qt, QUuid, pyqtSignal from PyQt5.QtGui import QColor @@ -24,7 +24,7 @@ class ControlLayer(QObject, ObservableProperties): max_preset_value = 4 strength_multiplier = 50 clip_vision_extent = Extent(224, 224) - segmentation_colors = [ + segmentation_colors: ClassVar[list[tuple[int, int, int]]] = [ (120, 120, 120), (180, 120, 120), (120, 180, 120), @@ -201,9 +201,7 @@ def _segmentation_image_from_regions(self, bounds: Bounds): return image if has_region_layer else None - def _segmentation_region_image( - self, layer: Layer, bounds: Bounds, color: tuple[int, int, int] - ): + def _segmentation_region_image(self, layer: Layer, bounds: Bounds, color: tuple[int, int, int]): mask = layer.get_mask(bounds) image = Image.create(bounds.extent, fill=QColor(*color, 255)) image._qimage.setAlphaChannel(mask._qimage)