Added image clicking, dragging and orientation

This commit is contained in:
2025-08-03 20:17:33 +02:00
parent 1a5abca8e1
commit 34cc28897d
7 changed files with 647 additions and 123 deletions

View File

@ -0,0 +1,121 @@
import dearpygui.dearpygui as dpg
import numpy as np
from .pipeline_stage_widget import PipelineStageWidget
class OrientationStage(PipelineStageWidget):
name = "Orient Image"
register = True
has_pipeline_in = True
has_pipeline_out = True
def __init__(self, manager, logger):
super().__init__(manager, logger, default_stage_out="oriented_image")
self.rotation = 0
self.mirror_h = False
self.mirror_v = False
self.rotation_combo_tag = dpg.generate_uuid()
self.mirror_h_tag = dpg.generate_uuid()
self.mirror_v_tag = dpg.generate_uuid()
self.last_image = None
def create_pipeline_stage_content(self):
dpg.add_combo(
label="Rotation",
items=["", "90°", "180°", "270°"],
default_value="",
callback=self._on_rotation_change,
tag=self.rotation_combo_tag
)
dpg.add_checkbox(
label="Mirror Horizontal",
default_value=False,
callback=self._on_mirror_h_change,
tag=self.mirror_h_tag
)
dpg.add_checkbox(
label="Mirror Vertical",
default_value=False,
callback=self._on_mirror_v_change,
tag=self.mirror_v_tag
)
def _on_rotation_change(self, sender, value, user_data):
degree_map = {
"": 0,
"90°": 90,
"180°": 180,
"270°": 270
}
self.rotation = degree_map.get(value, 0)
self.on_pipeline_data(self.last_img)
def _on_mirror_h_change(self, sender, value, user_data):
self.mirror_h = value
self.on_pipeline_data(self.last_img)
def _on_mirror_v_change(self, sender, value, user_data):
self.mirror_v = value
self.on_pipeline_data(self.last_img)
def on_pipeline_data(self, img):
if img is None:
return
self.last_img = img
img_out = img.copy()
# Apply rotation
if self.rotation == 90:
img_out = np.rot90(img_out, k=3)
elif self.rotation == 180:
img_out = np.rot90(img_out, k=2)
elif self.rotation == 270:
img_out = np.rot90(img_out, k=1)
# Apply mirroring
if self.mirror_h:
img_out = np.fliplr(img_out)
if self.mirror_v:
img_out = np.flipud(img_out)
self.publish_stage(img_out)
def get_config(self):
config = super().get_config()
config["orientation"] = {
"rotation": self.rotation,
"mirror_h": str(self.mirror_h),
"mirror_v": str(self.mirror_v),
}
return config
def set_config(self, config):
super().set_config(config)
orient_cfg = config.get("orientation", {})
self.rotation = int(orient_cfg.get("rotation", 0))
self.mirror_h = orient_cfg.get("mirror_h", "False") == "True"
self.mirror_v = orient_cfg.get("mirror_v", "False") == "True"
self._update_ui()
def _update_ui(self):
# Update rotation combo
reverse_map = {
0: "",
90: "90°",
180: "180°",
270: "270°"
}
dpg.set_value(self.rotation_combo_tag, reverse_map.get(self.rotation, ""))
# Update checkboxes
dpg.set_value(self.mirror_h_tag, self.mirror_h)
dpg.set_value(self.mirror_v_tag, self.mirror_v)

View File

@ -64,7 +64,8 @@ class PipelineStageWidget(BaseWidget):
tag=self.stage_out_input,
)
dpg.add_separator()
self.create_pipeline_stage_content()
with dpg.group():
self.create_pipeline_stage_content()
def create_pipeline_stage_content(self):
"""Must be implemented by the widget, creates the content of the window"""
@ -176,5 +177,4 @@ class PipelineStageWidget(BaseWidget):
group_x, group_y = dpg.get_item_pos(self.pipeline_config_group_tag)
self.window_height = win_h - group_h - group_y - 12
self.window_width = win_w - 7
self.window_offset_y = group_h + group_y + 3
self.on_resize(win_w, win_h)

View File

@ -1,6 +1,5 @@
import dearpygui.dearpygui as dpg
import numpy as np
from PIL import Image
from .pipeline_stage_widget import PipelineStageWidget
@ -13,25 +12,96 @@ class PipelineStageViewer(PipelineStageWidget):
def __init__(self, manager, logger):
super().__init__(manager, logger, default_stage_in="pipeline_out")
self.texture_tag = dpg.generate_uuid()
self.drawlist = None
self.img = None
self.needs_update = False
self.registry = manager.texture_registry
self.needs_update = False
self.canvas_handler = None
self.scaled_size = (0, 0)
self.image_position = (0, 0)
self.manager.bus.subscribe("mouse_dragged", self._on_mouse_drag, False)
def create_pipeline_stage_content(self):
# Create an empty dynamic texture
dpg.add_dynamic_texture(
1, 1, [0, 0, 0, 0], tag=self.texture_tag, parent=self.registry
)
self.image_item = dpg.add_image(self.texture_tag)
# Add drawlist
with dpg.drawlist(width=-1, height=-1) as self.drawlist:
pass
# Register click handler
with dpg.item_handler_registry() as self.canvas_handler:
dpg.add_item_clicked_handler(callback=self.on_canvas_click)
dpg.bind_item_handler_registry(self.drawlist, self.canvas_handler)
def on_canvas_click(self, sender, app_data, user_data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_clicked",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"button": (
"right"
if app_data[0] == 0
else ("left" if app_data[0] == 1 else ("middle"))
),
},
)
def _on_mouse_drag(self, data):
mouse_x, mouse_y = dpg.get_mouse_pos(local=False)
canvas_x, canvas_y = dpg.get_item_rect_min(self.drawlist)
local_x = mouse_x - canvas_x
local_y = mouse_y - canvas_y
img_x, img_y = self.image_position
img_w, img_h = self.scaled_size
if (
local_x >= img_x
and local_x < img_x + img_w
and local_y >= img_y
and local_y < img_y + img_h
):
# calculate the image coordinate
x = int((local_x - img_x) * self.img.shape[1] / img_w)
y = int((local_y - img_y) * self.img.shape[0] / img_h)
self.manager.bus.publish_deferred(
"img_dragged",
{
"stage_id": self.pipeline_stage_in_id,
"pos": (x, y),
"button": data['button'],
"delta": data['delta']
},
)
def on_resize(self, width, height):
self.needs_update = True
def on_pipeline_data(self, img):
# Resize if needed
if img is None:
return
h, w, _ = img.shape
self.img = img
self.needs_update = True
@ -39,15 +109,13 @@ class PipelineStageViewer(PipelineStageWidget):
pass
def update_texture(self, img: np.ndarray):
"""Only call from update function"""
# TODO show a smaller version of the image to speed things up
if img is None:
dpg.configure_item(self.image_item, show=False)
return
h, w, _ = img.shape
flat = img.flatten().tolist()
# Replace texture
if dpg.does_item_exist(self.texture_tag):
dpg.delete_item(self.texture_tag)
dpg.add_dynamic_texture(
@ -59,25 +127,32 @@ class PipelineStageViewer(PipelineStageWidget):
)
win_w, win_h = self.window_width, self.window_height
avail_w = win_w
avail_h = win_h
scale = min(avail_w / w, avail_h / h) # , 1.0)
scale = min(win_w / w, win_h / h)
disp_w = int(w * scale)
disp_h = int(h * scale)
x_off = (avail_w - disp_w) / 2
y_off = self.window_offset_y
x_off = (win_w - disp_w) / 2
y_off = (win_h - disp_h) / 2
dpg.configure_item(
self.image_item,
texture_tag=self.texture_tag,
pos=(x_off, y_off),
width=disp_w,
height=disp_h,
show=True
self.scaled_size = (disp_w, disp_h)
self.image_position = (x_off, y_off)
# Clear old drawings
dpg.delete_item(self.drawlist, children_only=True)
# Draw image
dpg.draw_image(
self.texture_tag,
pmin=(x_off, y_off),
pmax=(x_off + disp_w, y_off + disp_h),
uv_min=(0, 0),
uv_max=(1, 1),
parent=self.drawlist,
)
# Resize drawlist
dpg.configure_item(self.drawlist, width=win_w, height=win_h)
def update(self):
if self.needs_update:
self.needs_update = False