Start of framing widget
This commit is contained in:
@ -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.INFO,
|
||||
format="%(asctime)s %(levelname)s %(message)s")
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -61,14 +62,16 @@ class EditorManager:
|
||||
and cls is not ModuleBaseWidget
|
||||
and cls.register
|
||||
):
|
||||
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, config: dict = {}):
|
||||
@ -115,7 +118,8 @@ class EditorManager:
|
||||
"process_full_res", None
|
||||
),
|
||||
)
|
||||
dpg.add_menu_item(label="Quit", callback=lambda: dpg.stop_dearpygui())
|
||||
dpg.add_menu_item(
|
||||
label="Quit", callback=lambda: dpg.stop_dearpygui())
|
||||
|
||||
with dpg.menu(label="View"):
|
||||
for widget_name in sorted(self.widget_classes.keys()):
|
||||
|
118
negstation/widgets/framing_widget.py
Normal file
118
negstation/widgets/framing_widget.py
Normal file
@ -0,0 +1,118 @@
|
||||
import dearpygui.dearpygui as dpg
|
||||
import numpy as np
|
||||
import time
|
||||
from scipy.ndimage import rotate
|
||||
|
||||
from .stage_viewer_widget import PipelineStageViewer
|
||||
|
||||
|
||||
class FramingWidget(PipelineStageViewer):
|
||||
name = "Framing"
|
||||
register = True
|
||||
has_pipeline_in = True
|
||||
has_pipeline_out = True
|
||||
|
||||
def __init__(self, manager, logger):
|
||||
super().__init__(manager, logger)
|
||||
|
||||
# Rotation line endpoints (canvas coords)
|
||||
self.rot_start = None # (x, y)
|
||||
self.rot_end = None # (x, y)
|
||||
self.angle = 0.0 # computed deskew angle
|
||||
|
||||
# Throttle publishing to a few Hz
|
||||
self._last_pub_time = 0.0
|
||||
self._publish_interval = 0.5 # seconds
|
||||
|
||||
self.manager.bus.subscribe("img_clicked", self.on_click)
|
||||
self.manager.bus.subscribe("img_dragged", self.on_drag)
|
||||
self.manager.bus.subscribe("img_scrolled", self.on_scroll)
|
||||
|
||||
def create_pipeline_stage_content(self):
|
||||
super().create_pipeline_stage_content()
|
||||
|
||||
def on_pipeline_data(self, img):
|
||||
if img is None:
|
||||
return
|
||||
self.img = img.copy()
|
||||
self._publish_rotated_and_cropped()
|
||||
self.needs_update = True
|
||||
|
||||
def update_texture(self, img):
|
||||
super().update_texture(img)
|
||||
# Draw rotation guide if active
|
||||
if self.rot_start and self.rot_end:
|
||||
p0 = self._pos_to_canvas(self.rot_start)
|
||||
p1 = self._pos_to_canvas(self.rot_end)
|
||||
dpg.draw_line(p1=p0, p2=p1, color=(
|
||||
255, 255, 0, 255), thickness=2, parent=self.drawlist)
|
||||
|
||||
def on_click(self, data):
|
||||
if data.get("obj") is not self:
|
||||
return
|
||||
x, y = data.get("pos")
|
||||
button = data.get("button")
|
||||
if button == "right":
|
||||
self.rot_start = (x, y)
|
||||
self.rot_end = (x, y)
|
||||
self.needs_update = True
|
||||
|
||||
def on_drag(self, data):
|
||||
if data.get("obj") is not self:
|
||||
return
|
||||
x, y = data.get("pos")
|
||||
button = data.get("button")
|
||||
if button == "right":
|
||||
self.rot_end = (x, y)
|
||||
# Update angle on rotation drag
|
||||
if self.rot_start and self.rot_end:
|
||||
dx = self.rot_end[0] - self.rot_start[0]
|
||||
dy = self.rot_end[1] - self.rot_start[1]
|
||||
self.angle = np.degrees(np.arctan2(dy, dx))
|
||||
# Throttle publishes
|
||||
now = time.time()
|
||||
if now - self._last_pub_time >= self._publish_interval:
|
||||
self._publish_rotated_and_cropped()
|
||||
self._last_pub_time = now
|
||||
self.needs_update = True
|
||||
|
||||
def on_scroll(self, data):
|
||||
print(data)
|
||||
|
||||
def _publish_rotated_and_cropped(self):
|
||||
w, h, _ = self.img.shape
|
||||
out = self.rotate_and_crop(self.img, self.angle, (0, 0, w, h))
|
||||
self.publish_stage(out)
|
||||
|
||||
def _pos_to_canvas(self, img_pos):
|
||||
x, y = img_pos
|
||||
ix, iy = self.image_position
|
||||
iw, ih = self.img.shape[1], self.img.shape[0]
|
||||
sw, sh = self.scaled_size
|
||||
return (ix + x / iw * sw, iy + y / ih * sh)
|
||||
|
||||
def rotate_and_crop(
|
||||
self,
|
||||
img: np.ndarray,
|
||||
angle: float,
|
||||
rect: tuple[int, int, int, int],
|
||||
cval: float = 0.0
|
||||
) -> np.ndarray:
|
||||
h, w = img.shape[:2]
|
||||
x, y, cw, ch = rect
|
||||
rotated = np.empty_like(img)
|
||||
for c in range(img.shape[2]):
|
||||
rotated[..., c] = rotate(
|
||||
img[..., c],
|
||||
angle,
|
||||
reshape=False,
|
||||
order=1, # bilinear interpolation
|
||||
mode='constant',
|
||||
cval=cval,
|
||||
prefilter=False
|
||||
)
|
||||
x0 = max(0, min(int(x), w - 1))
|
||||
y0 = max(0, min(int(y), h - 1))
|
||||
x1 = max(0, min(int(x + cw), w))
|
||||
y1 = max(0, min(int(y + ch), h))
|
||||
return rotated[y0:y1, x0:x1]
|
@ -81,10 +81,10 @@ Collapsed=0
|
||||
DockId=0x00000012,1
|
||||
|
||||
[Window][###107]
|
||||
Pos=939,19
|
||||
Size=261,781
|
||||
Pos=958,19
|
||||
Size=242,460
|
||||
Collapsed=0
|
||||
DockId=0x00000011,0
|
||||
DockId=0x00000059,0
|
||||
|
||||
[Window][###35]
|
||||
Pos=272,19
|
||||
@ -112,7 +112,7 @@ DockId=0x00000013,1
|
||||
|
||||
[Window][###87]
|
||||
Pos=272,646
|
||||
Size=684,154
|
||||
Size=710,154
|
||||
Collapsed=0
|
||||
DockId=0x00000046,0
|
||||
|
||||
@ -168,7 +168,7 @@ DockId=0x0000004E,0
|
||||
Pos=939,555
|
||||
Size=261,245
|
||||
Collapsed=0
|
||||
DockId=0x00000016,0
|
||||
DockId=0x0000002C,0
|
||||
|
||||
[Window][###97]
|
||||
Pos=972,19
|
||||
@ -279,9 +279,9 @@ DockId=0x0000004A,0
|
||||
|
||||
[Window][###36]
|
||||
Pos=272,19
|
||||
Size=684,625
|
||||
Size=710,625
|
||||
Collapsed=0
|
||||
DockId=0x00000014,1
|
||||
DockId=0x00000014,0
|
||||
|
||||
[Window][###48]
|
||||
Pos=0,714
|
||||
@ -320,10 +320,10 @@ Collapsed=0
|
||||
DockId=0x00000057,0
|
||||
|
||||
[Window][###131]
|
||||
Pos=958,653
|
||||
Size=242,147
|
||||
Pos=958,664
|
||||
Size=242,136
|
||||
Collapsed=0
|
||||
DockId=0x00000056,0
|
||||
DockId=0x00000016,0
|
||||
|
||||
[Window][###128]
|
||||
Pos=60,60
|
||||
@ -370,7 +370,7 @@ DockId=0x00000014,1
|
||||
Pos=272,19
|
||||
Size=710,625
|
||||
Collapsed=0
|
||||
DockId=0x00000014,0
|
||||
DockId=0x00000014,1
|
||||
|
||||
[Window][###169]
|
||||
Pos=272,19
|
||||
@ -384,6 +384,23 @@ Size=242,368
|
||||
Collapsed=0
|
||||
DockId=0x00000058,0
|
||||
|
||||
[Window][###157]
|
||||
Pos=958,481
|
||||
Size=242,181
|
||||
Collapsed=0
|
||||
DockId=0x0000005A,0
|
||||
|
||||
[Window][###178]
|
||||
Pos=60,60
|
||||
Size=148,100
|
||||
Collapsed=0
|
||||
|
||||
[Window][###184]
|
||||
Pos=272,19
|
||||
Size=710,625
|
||||
Collapsed=0
|
||||
DockId=0x00000014,1
|
||||
|
||||
[Docking][Data]
|
||||
DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,19 Size=1200,781 Split=X
|
||||
DockNode ID=0x00000053 Parent=0x7C6B3D9B SizeRef=956,781 Split=X
|
||||
@ -460,9 +477,7 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,
|
||||
DockNode ID=0x0000002A Parent=0x00000028 SizeRef=216,170 Split=Y Selected=0xF8004A44
|
||||
DockNode ID=0x0000004D Parent=0x0000002A SizeRef=216,575 Selected=0x7D28643F
|
||||
DockNode ID=0x0000004E Parent=0x0000002A SizeRef=216,204 Selected=0xF8004A44
|
||||
DockNode ID=0x0000002C Parent=0x0000002D SizeRef=261,781 Split=Y Selected=0xC8700185
|
||||
DockNode ID=0x00000011 Parent=0x0000002C SizeRef=142,534 Selected=0x3A881EEF
|
||||
DockNode ID=0x00000016 Parent=0x0000002C SizeRef=142,245 Selected=0xC8700185
|
||||
DockNode ID=0x0000002C Parent=0x0000002D SizeRef=261,781 Selected=0xC8700185
|
||||
DockNode ID=0x0000002E Parent=0x00000037 SizeRef=228,781 Split=Y Selected=0x4C2F06CB
|
||||
DockNode ID=0x0000002F Parent=0x0000002E SizeRef=216,560 Selected=0x4C2F06CB
|
||||
DockNode ID=0x00000030 Parent=0x0000002E SizeRef=216,219 Selected=0x04546B8A
|
||||
@ -473,5 +488,9 @@ DockSpace ID=0x7C6B3D9B Window=0xA87D555D Pos=0,
|
||||
DockNode ID=0x00000055 Parent=0x00000054 SizeRef=158,632 Split=Y Selected=0xA2A5002C
|
||||
DockNode ID=0x00000057 Parent=0x00000055 SizeRef=158,262 Selected=0x85B8A08E
|
||||
DockNode ID=0x00000058 Parent=0x00000055 SizeRef=158,368 Selected=0xA2A5002C
|
||||
DockNode ID=0x00000056 Parent=0x00000054 SizeRef=158,147 Selected=0x335C99E1
|
||||
DockNode ID=0x00000056 Parent=0x00000054 SizeRef=158,147 Split=Y Selected=0x335C99E1
|
||||
DockNode ID=0x00000011 Parent=0x00000056 SizeRef=242,643 Split=Y Selected=0x3A881EEF
|
||||
DockNode ID=0x00000059 Parent=0x00000011 SizeRef=242,460 Selected=0x3A881EEF
|
||||
DockNode ID=0x0000005A Parent=0x00000011 SizeRef=242,181 Selected=0x6A458F5C
|
||||
DockNode ID=0x00000016 Parent=0x00000056 SizeRef=242,136 Selected=0x335C99E1
|
||||
|
||||
|
@ -5,7 +5,7 @@
|
||||
"2": "opened_raw",
|
||||
"3": "monochrome",
|
||||
"4": "oriented_image",
|
||||
"5": "cropped_image"
|
||||
"6": "framed_image"
|
||||
},
|
||||
"widgets": [
|
||||
{
|
||||
@ -21,7 +21,7 @@
|
||||
"widget_type": "PipelineStageViewer",
|
||||
"config": {
|
||||
"pipeline_config": {
|
||||
"stage_in": 5,
|
||||
"stage_in": 6,
|
||||
"stage_out": null
|
||||
}
|
||||
}
|
||||
@ -74,7 +74,7 @@
|
||||
"widget_type": "HistogramWidget",
|
||||
"config": {
|
||||
"pipeline_config": {
|
||||
"stage_in": 5,
|
||||
"stage_in": 6,
|
||||
"stage_out": null
|
||||
}
|
||||
}
|
||||
@ -103,11 +103,11 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"widget_type": "CropWidget",
|
||||
"widget_type": "FramingWidget",
|
||||
"config": {
|
||||
"pipeline_config": {
|
||||
"stage_in": 4,
|
||||
"stage_out": 5
|
||||
"stage_out": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user