Fixed image viewer

This commit is contained in:
2025-07-30 23:17:27 +02:00
parent d0cbc63859
commit 9306f2d6ea
4 changed files with 100 additions and 217 deletions

BIN
img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

View File

@ -13,7 +13,8 @@ from .layout_manager import LayoutManager
from .widgets.base_widget import BaseWidget
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s %(levelname)s %(message)s")
logging.basicConfig(level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger(__name__)
@ -59,14 +60,16 @@ class EditorManager:
and issubclass(cls, ModuleBaseWidget)
and cls is not ModuleBaseWidget
):
logging.info(f" -> Found and registered widget: {name}")
logging.info(
f" -> Found and registered widget: {name}")
self._register_widget(name, cls)
except Exception as e:
logging.error(f"Failed to import widget '{py_file.name}': {e}")
def _register_widget(self, name: str, widget_class: object):
if name in self.widget_classes:
logging.warning(f"Widget '{name}' is already registered. Overwriting.")
logging.warning(
f"Widget '{name}' is already registered. Overwriting.")
self.widget_classes[name] = widget_class
def _add_widget(self, widget_type: str):
@ -81,7 +84,7 @@ class EditorManager:
)
self.layout_manager.load_layout()
dpg.create_viewport(title="NegStation", width=960, height=400)
dpg.create_viewport(title="NegStation", width=800, height=600)
dpg.configure_app(docking=True, docking_space=True)
with dpg.viewport_menu_bar():

View File

@ -4,223 +4,104 @@ import numpy as np
from .base_widget import BaseWidget
# class StageViewerWidget(BaseWidget):
# name: str = "Stage Viewer"
# def __init__(self, manager, logger):
# super().__init__(manager, logger)
# # ensure texture registry
# if not hasattr(manager, "texture_registry"):
# manager.texture_registry = dpg.add_texture_registry(tag=dpg.generate_uuid())
# self.registry = manager.texture_registry
# self.stages = []
# self.current = None
# self.texture_tag = dpg.generate_uuid()
# self.image_item = None
# manager.bus.subscribe("pipeline_stages", self.on_stage_list, main_thread=True)
# manager.bus.subscribe("pipeline_stage", self.on_stage_data, main_thread=True)
# def create(self):
# with dpg.window(
# label="Stage Viewer",
# tag=self.window_tag,
# width=400,
# height=400,
# on_close=self._on_window_close,
# ):
# self.combo = dpg.add_combo(label="Stage", items=[], callback=self.on_select)
# # placeholder 1×1 texture
# dpg.add_dynamic_texture(
# width=1,
# height=1,
# default_value=[0.0, 0.0, 0.0, 0.0],
# tag=self.texture_tag,
# parent=self.registry,
# )
# self.image_item = dpg.add_image(self.texture_tag)
# def on_stage_list(self, stages):
# self.stages = stages
# dpg.configure_item(self.combo, items=stages)
# if not self.current and stages:
# self.current = stages[0]
# dpg.set_value(self.combo, self.current)
# def on_select(self, sender, stage_name):
# self.current = stage_name
# img = self.manager.pipeline.get_stage(stage_name)
# if img is not None:
# self.update_texture(img)
# def on_stage_data(self, data):
# name, img = data
# if name == self.current:
# self.update_texture(img)
# def update_texture(self, img: np.ndarray):
# h, w, _ = img.shape
# flat = img.flatten().tolist()
# # recreate texture at correct size
# if dpg.does_item_exist(self.texture_tag):
# dpg.delete_item(self.texture_tag)
# dpg.add_dynamic_texture(
# width=w,
# height=h,
# default_value=flat,
# tag=self.texture_tag,
# parent=self.registry,
# )
# # determine available window size
# win_w, win_h = dpg.get_item_rect_size(self.window_tag)
# # reserve space for combo box (approx 30px)
# available_h = max(win_h - 30, 1)
# # compute scale to fit
# scale = min(win_w / w, available_h / h)
# disp_w = int(w * scale)
# disp_h = int(h * scale)
# # update image widget
# dpg.configure_item(
# self.image_item, texture_tag=self.texture_tag, width=disp_w, height=disp_h
# )
class StageViewerWidget(BaseWidget):
"""
A robust, zoomable stage viewer using a Dear PyGui Plot to display
dynamic textures without ever deleting them—avoiding segfaults.
"""
name = "Stage Viewer"
name: str = "Image Stage Viewer"
def __init__(self, manager, logger):
super().__init__(manager, logger)
self.manager.bus.subscribe("pipeline_stages", self._on_stage_list, main_thread=True)
self.manager.bus.subscribe("pipeline_stage", self._on_stage_data, main_thread=True)
# Ensure shared texture registry
if not hasattr(manager, "texture_registry"):
manager.texture_registry = dpg.add_texture_registry(
tag=dpg.generate_uuid())
self.registry = manager.texture_registry
# onetime flags and tags
self._initialized = False
self.stages = []
self.current = None
self.texture_tag = dpg.generate_uuid()
self.image_draw_tag = dpg.generate_uuid()
self.plot_tag = dpg.generate_uuid()
self.xaxis_tag = dpg.generate_uuid()
self.yaxis_tag = dpg.generate_uuid()
self.last_size = (1, 1)
self.current_stage = None
self.needs_fit = False
self.img = None
# Subscribe only to stage list updates
self.manager.bus.subscribe(
"pipeline_stages", self.on_stage_list, main_thread=True)
self.manager.bus.subscribe(
"pipeline_stage", self.on_stage, main_thread=True)
def create(self):
if dpg.does_item_exist(self.window_tag):
return
with dpg.window(label="Stage Viewer", tag=self.window_tag, width=400, height=400):
# Dropdown for selecting a stage
self.combo = dpg.add_combo(
label="Stage", items=[], callback=self.on_select)
# Placeholder 1×1 texture in registry
dpg.add_dynamic_texture(
1, 1, [0, 0, 0, 0], tag=self.texture_tag, parent=self.registry)
# Image widget that will display the texture
self.image_item = dpg.add_image(self.texture_tag)
# ensure a texture registry exists
if not hasattr(self.manager, "texture_registry"):
self.manager.texture_registry = dpg.add_texture_registry(tag=dpg.generate_uuid())
with dpg.window(label="Stage Viewer",
tag=self.window_tag,
on_close=self._on_window_close,
width=600, height=600):
# stage selector
self.combo = dpg.add_combo(label="Stage", items=[], callback=self._on_select)
# plot container, equal_aspects ensures no distortion
with dpg.plot(label="Image Plot", tag=self.plot_tag, height=-1, width=-1, equal_aspects=True):
self.xaxis_tag = dpg.add_plot_axis(dpg.mvXAxis,
no_tick_labels=True, no_gridlines=True)
self.yaxis_tag = dpg.add_plot_axis(dpg.mvYAxis,
no_tick_labels=True, no_gridlines=True)
# resize handler to refit on window/plot size changes
with dpg.item_handler_registry() as handler:
dpg.add_item_resize_handler(callback=lambda s,a,u: self._fit_image(), user_data=None)
dpg.add_item_resize_handler(
callback=self.on_resize)
dpg.bind_item_handler_registry(self.window_tag, handler)
def _on_stage_list(self, stages):
def on_resize(self, app_data):
if self.img is not None:
self.update_texture(self.img)
def on_stage_list(self, stages):
# Update dropdown items
self.stages = stages
dpg.configure_item(self.combo, items=stages)
if not self.current_stage and stages:
self.current_stage = stages[0]
dpg.set_value(self.combo, self.current_stage)
def _on_select(self, sender, stage_name):
self.current_stage = stage_name
img = self.manager.pipeline.get_stage(stage_name)
def on_stage(self, stage):
name, img = stage
if name == self.current:
if img is not None:
self._update_image(img)
self.img = img
self.update_texture(img)
def _on_stage_data(self, data):
name, img = data
if name == self.current_stage:
self._update_image(img)
def on_select(self, sender, selected_stage):
# User-picked stage: fetch and render
self.current = selected_stage
img = self.manager.pipeline.get_stage(selected_stage)
if img is not None:
self.img = img
self.update_texture(img)
def _update_image(self, img: np.ndarray):
def update_texture(self, img: np.ndarray):
# img is a NumPy array with shape (h, w, 4)
h, w, _ = img.shape
self.last_size = (w, h)
flat = img.flatten().tolist()
# First time: create texture & draw-image inside the plot
if not self._initialized:
dpg.add_dynamic_texture(w, h, img, tag=self.texture_tag,
parent=self.manager.texture_registry)
dpg.draw_image(self.texture_tag,
pmin=(0, h), pmax=(w, 0),
tag=self.image_draw_tag,
parent=self.plot_tag)
self._initialized = True
else:
# Subsequent updates: just set_value and adjust draw coords
dpg.set_value(self.texture_tag, img)
dpg.configure_item(self.image_draw_tag,
pmin=(0, self.last_size[1]),
pmax=(self.last_size[0], 0))
# 1) Recreate the dynamic texture at the correct size
if dpg.does_item_exist(self.texture_tag):
dpg.delete_item(self.texture_tag)
dpg.add_dynamic_texture(
width=w, height=h,
default_value=flat,
tag=self.texture_tag,
parent=self.registry
)
# show & focus window
dpg.configure_item(self.window_tag, show=True)
dpg.focus_item(self.window_tag)
# 2) Compute available space: full window width, from just below combo to bottom
win_w, win_h = dpg.get_item_rect_size(self.window_tag)
combo_w, combo_h = dpg.get_item_rect_size(self.combo)
combo_x, combo_y = dpg.get_item_pos(self.combo)
avail_w = win_w - 15
avail_h = win_h - combo_h - combo_y - 15
# flag to refit axes
self.needs_fit = True
# 3) Compute scale to fit the available rectangle
scale = min(avail_w / w, avail_h / h, 1.0)
disp_w = int(w * scale)
disp_h = int(h * scale)
def _fit_image(self):
"""Adjust plot axes so the image fills the available space."""
if not self._initialized or not self.needs_fit:
return
# 4) Center horizontally, start exactly below the combo
x_off = (avail_w - disp_w) / 2 + 7
y_off = combo_h + combo_y + 7 # flush immediately below the dropdown
# get plot area size
plot_w = dpg.get_item_width(self.window_tag)
plot_h = dpg.get_item_height(self.window_tag) - 30 # reserve combo height
if plot_w <= 0 or plot_h <= 0:
return
img_w, img_h = self.last_size
if img_w <= 0 or img_h <= 0:
return
plot_aspect = plot_w / plot_h
img_aspect = img_w / img_h
if img_aspect > plot_aspect:
x_min, x_max = 0, img_w
needed_h = img_w / plot_aspect
center_y = img_h / 2
y_min = center_y - needed_h / 2
y_max = center_y + needed_h / 2
else:
y_min, y_max = 0, img_h
needed_w = img_h * plot_aspect
center_x = img_w / 2
x_min = center_x - needed_w / 2
x_max = center_x + needed_w / 2
dpg.set_axis_limits(self.xaxis_tag, x_min, x_max)
dpg.set_axis_limits(self.yaxis_tag, y_min, y_max)
self.needs_fit = False
def update(self):
# If we flagged a refit, do it now
if self.needs_fit:
self._fit_image()
# 5) Apply to the image widget
dpg.configure_item(
self.image_item,
texture_tag=self.texture_tag,
pos=(x_off, y_off),
width=disp_w,
height=disp_h
)

View File

@ -1,6 +1,6 @@
[Window][WindowOverViewport_11111111]
Pos=0,19
Size=960,381
Size=810,581
Collapsed=0
[Window][Debug##Default]
@ -25,15 +25,15 @@ Collapsed=0
[Window][###22]
Pos=0,19
Size=207,381
Size=278,581
Collapsed=0
DockId=0x00000005,0
[Window][###27]
Pos=498,19
Size=462,381
Pos=280,19
Size=530,581
Collapsed=0
DockId=0x00000004,0
DockId=0x00000001,0
[Window][###31]
Pos=516,19
@ -45,19 +45,18 @@ DockId=0x00000002,0
Pos=185,19
Size=821,577
Collapsed=0
DockId=0x00000003,0
DockId=0x00000001,0
[Window][###38]
Pos=249,45
Size=192,274
Pos=209,19
Size=751,574
Collapsed=0
DockId=0x00000001,0
[Docking][Data]
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=960,381 Split=X
DockNode ID=0x00000005 Parent=0x7C6B3D9B SizeRef=207,381 Selected=0xEE087978
DockNode ID=0x00000006 Parent=0x7C6B3D9B SizeRef=751,381 Split=X
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=514,381 Split=X
DockNode ID=0x00000003 Parent=0x00000001 SizeRef=287,381 CentralNode=1
DockNode ID=0x00000004 Parent=0x00000001 SizeRef=462,381 Selected=0x26E8F608
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=810,581 Split=X
DockNode ID=0x00000005 Parent=0x7C6B3D9B SizeRef=278,381 Selected=0xEE087978
DockNode ID=0x00000006 Parent=0x7C6B3D9B SizeRef=680,381 Split=X
DockNode ID=0x00000001 Parent=0x00000006 SizeRef=514,381 CentralNode=1 Selected=0x26E8F608
DockNode ID=0x00000002 Parent=0x00000006 SizeRef=444,381 Selected=0x62F4D00D