Added image clicking, dragging and orientation
This commit is contained in:
121
negstation/widgets/orientation_widget.py
Normal file
121
negstation/widgets/orientation_widget.py
Normal 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=["0°", "90°", "180°", "270°"],
|
||||
default_value="0°",
|
||||
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°": 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: "0°",
|
||||
90: "90°",
|
||||
180: "180°",
|
||||
270: "270°"
|
||||
}
|
||||
dpg.set_value(self.rotation_combo_tag, reverse_map.get(self.rotation, "0°"))
|
||||
|
||||
# Update checkboxes
|
||||
dpg.set_value(self.mirror_h_tag, self.mirror_h)
|
||||
dpg.set_value(self.mirror_v_tag, self.mirror_v)
|
@ -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)
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user