Skip to content

Commit c5671cf

Browse files
committed
Adds image-to-image support for Z-Image
1 parent 22e6fef commit c5671cf

File tree

4 files changed

+106
-3
lines changed

4 files changed

+106
-3
lines changed
Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
from PySide6.QtCore import Slot
1+
from PySide6.QtCore import Slot, QTimer
22
from airunner.components.art.gui.widgets.canvas.templates.image_manipulation_tools_container_ui import (
33
Ui_image_manipulation_tools_container,
44
)
55
from airunner.components.application.gui.widgets.base_widget import BaseWidget
66
from airunner.utils.settings.get_qsettings import get_qsettings
7+
from airunner.enums import StableDiffusionVersion
8+
9+
10+
# Versions that don't support ControlNet or Inpaint
11+
_NO_CONTROLNET_INPAINT_VERSIONS = (
12+
StableDiffusionVersion.Z_IMAGE_TURBO.value,
13+
StableDiffusionVersion.Z_IMAGE_BASE.value,
14+
)
715

816

917
class ImageManipulationToolsContainer(BaseWidget):
@@ -12,23 +20,82 @@ class ImageManipulationToolsContainer(BaseWidget):
1220
def __init__(self, *args, **kwargs):
1321
super().__init__(*args, **kwargs)
1422
self.qsettings = get_qsettings()
23+
self._controlnet_tab = None
24+
self._inpaint_tab = None
25+
self._last_version = None
1526
self.ui.image_manipulation_tools_tab_container.currentChanged.connect(
1627
self.on_tab_changed
1728
)
29+
# Set up a timer to periodically check version changes
30+
self._version_check_timer = QTimer(self)
31+
self._version_check_timer.setInterval(500) # Check every 500ms
32+
self._version_check_timer.timeout.connect(self._check_version_and_update_tabs)
1833

1934
@Slot(int)
2035
def on_tab_changed(self, index: int):
2136
self.qsettings.setValue(
2237
"tabs/image_manipulation_tools_container/active_index", index
2338
)
2439

40+
def _check_version_and_update_tabs(self):
41+
"""Check if version changed and update tab visibility accordingly."""
42+
current_version = self.generator_settings.version
43+
if current_version != self._last_version:
44+
self._last_version = current_version
45+
self._update_tab_visibility()
46+
47+
def _update_tab_visibility(self):
48+
"""Hide/show ControlNet and Inpaint tabs based on model version.
49+
50+
Z-Image models don't support ControlNet or Inpaint pipelines,
51+
so those tabs should be hidden when Z-Image is selected.
52+
"""
53+
tab_widget = self.ui.image_manipulation_tools_tab_container
54+
current_version = self.generator_settings.version
55+
should_hide = current_version in _NO_CONTROLNET_INPAINT_VERSIONS
56+
57+
# Store references to removed tabs so they can be restored
58+
if should_hide:
59+
# Remove ControlNet tab (index 1) and Inpaint tab (index 2)
60+
# Note: Remove in reverse order to preserve indices
61+
if tab_widget.count() > 2:
62+
self._inpaint_tab = tab_widget.widget(2)
63+
tab_widget.removeTab(2)
64+
if tab_widget.count() > 1:
65+
self._controlnet_tab = tab_widget.widget(1)
66+
tab_widget.removeTab(1)
67+
else:
68+
# Restore tabs if they were previously removed
69+
if self._controlnet_tab is not None and tab_widget.count() == 1:
70+
tab_widget.insertTab(1, self._controlnet_tab, "Controlnet")
71+
self._controlnet_tab = None
72+
if self._inpaint_tab is not None and tab_widget.count() == 2:
73+
tab_widget.insertTab(2, self._inpaint_tab, "Inpaint")
74+
self._inpaint_tab = None
75+
2576
def showEvent(self, event):
2677
super().showEvent(event)
78+
# Update tab visibility based on current model version
79+
self._last_version = self.generator_settings.version
80+
self._update_tab_visibility()
81+
82+
# Start the version check timer
83+
self._version_check_timer.start()
84+
85+
# Restore active tab index
2786
active_index = int(
2887
self.qsettings.value(
2988
"tabs/image_manipulation_tools_container/active_index", 0
3089
)
3190
)
91+
# Ensure index is valid for current tab count
92+
max_index = self.ui.image_manipulation_tools_tab_container.count() - 1
93+
active_index = min(active_index, max_index)
3294
self.ui.image_manipulation_tools_tab_container.setCurrentIndex(
3395
active_index
3496
)
97+
98+
def hideEvent(self, event):
99+
super().hideEvent(event)
100+
# Stop the timer when widget is hidden
101+
self._version_check_timer.stop()

src/airunner/components/art/gui/widgets/canvas/input_image.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,9 @@ def load_image_from_object(self, image: Image):
377377

378378
# Adjust scene rect to the item bounds if present
379379
if self._scene.item and hasattr(self._scene.item, "boundingRect"):
380-
rect = self._scene.item.boundingRect()
380+
# Use sceneBoundingRect() to get the rect in scene coordinates
381+
# (includes the item's position)
382+
rect = self._scene.item.sceneBoundingRect()
381383
self._scene.setSceneRect(rect)
382384

383385
# Force scene and view updates

src/airunner/components/art/gui/widgets/canvas/input_image_scene.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from typing import Dict
22
from PIL.Image import Image
33
from PIL import ImageQt
4+
from PySide6.QtCore import QPoint
45

56
from airunner.components.art.data.controlnet_settings import ControlnetSettings
67
from airunner.components.art.data.drawingpad_settings import DrawingPadSettings
@@ -29,6 +30,37 @@ def __init__(
2930
def settings_key(self):
3031
return self._settings_key
3132

33+
def _get_default_image_position(self) -> QPoint:
34+
"""Override to always place images at (0, 0) in input image scenes.
35+
36+
Unlike the main canvas which uses drawing_pad_settings for position,
37+
input image scenes are small preview panels that should always display
38+
images starting at the origin.
39+
40+
Returns:
41+
QPoint at (0, 0).
42+
"""
43+
return QPoint(0, 0)
44+
45+
def _update_item_position(
46+
self, root_point: QPoint, canvas_offset
47+
) -> None:
48+
"""Override to always position items at (0, 0) in input image scenes.
49+
50+
Input image scenes are small preview panels that should always display
51+
images at the origin, ignoring any stored position or canvas offset.
52+
53+
Args:
54+
root_point: Ignored - always uses (0, 0).
55+
canvas_offset: Ignored - always uses (0, 0).
56+
"""
57+
try:
58+
if self.item is not None:
59+
self.item.setPos(0, 0)
60+
except (RuntimeError, AttributeError):
61+
# Item was deleted or is no longer valid
62+
pass
63+
3264
@property
3365
def current_active_image(self) -> Image:
3466
if self._is_mask:

src/airunner/components/art/managers/stablediffusion/mixins/sd_generation_preparation_mixin.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,9 @@ def _prepare_data(self, active_rect=None) -> Dict:
113113
data.update({"width": width, "height": height})
114114

115115
if self.is_img2img:
116-
image = self.img2img_image
116+
# Use image from image_request if available (passed from GUI),
117+
# otherwise fall back to img2img_image property
118+
image = self.image_request.image if self.image_request.image is not None else self.img2img_image
117119
if (
118120
data["num_inference_steps"]
119121
< AIRUNNER_MIN_NUM_INFERENCE_STEPS_IMG2IMG

0 commit comments

Comments
 (0)