Files
NegStation/negstation/widgets/framing_widget.py
2025-08-04 18:12:08 +02:00

178 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import dearpygui.dearpygui as dpg
import numpy as np
import time
import scipy.ndimage as snd
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)
self.needs_publishing = False
# Rotation line endpoints (canvas coords)
self.rot_start = None # (x, y)
self.rot_end = None # (x, y)
self.angle = 0.0 # computed deskew angle
# Crop rect endpoints (canvas coords)
self.crop_start = None # (x, y)
self.crop_end = None # (x, y)
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)
self.manager.bus.subscribe("img_released", self.on_release)
def create_pipeline_stage_content(self):
super().create_pipeline_stage_content()
def on_full_res_pipeline_data(self, img):
if img is None:
return
img = self.rotate(img)
self.publish_stage(self.crop(img))
def on_pipeline_data(self, img):
if img is None:
return
self.original_img = img.copy()
self.needs_update = True
# Apply transformation and publish
self.img = self.rotate(img)
self.publish_stage(self.crop(self.img))
def update_texture(self, img):
super().update_texture(img)
# Draw rotation guide
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, 0, 0, 255),
thickness=2,
parent=self.drawlist,
)
# Draw crop rect
if self.crop_start and self.crop_end:
p0 = self._pos_to_canvas(self.crop_start)
p1 = self._pos_to_canvas(self.crop_end)
dpg.draw_rectangle(
pmin=p0,
pmax=p1,
color=(255, 0, 255, 255),
thickness=2,
parent=self.drawlist,
)
if self.needs_publishing:
img = self.crop(self.img)
self.publish_stage(img)
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 on_click(self, data):
if data.get("obj") is not self:
return
x, y = data.get("pos")
if data["button"] == "left":
self.rot_start = (x, y)
self.rot_end = None
elif data["button"] == "right":
self.crop_start = (x,y)
self.crop_end = None
self.needs_update = True
def on_drag(self, data):
if data.get("obj") is not self:
return
x, y = data.get("pos")
if data["button"] == "left":
self.rot_end = (x, y)
elif data["button"] == "right":
self.crop_end = (x,y)
self.needs_update = True
def on_scroll(self, data):
if data.get("obj") is not self:
return
def on_release(self, data):
if data.get("obj") is not self:
return
if data["button"] == "left":
# End of rotation line dragging
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))
# Do the rotation
self.img = self.rotate(self.original_img)
# Delete lines
self.rot_start = self.rot_end = None
self.needs_publishing = True
elif data["button"] == "right":
self.needs_publishing = True
self.needs_update = True
def rotate(self, img):
img = snd.rotate(img, self.angle, reshape=False)
return img
def crop(self, img):
"""
Crop `img` according to a rectangle drawn in the DISPLAY,
by converting displaycoords to imagecoords via percentages.
"""
# only crop if user has dragged out a box
if not (self.crop_start and self.crop_end):
return img
# 1) get actual image dims
h, w = img.shape[:2]
# 2) get display info (set in update_texture)
disp_h, disp_w, _ = self.img.shape
# 3) unpack the two drag points (in screen coords)
sx, sy = self.crop_start
ex, ey = self.crop_end
# 4) normalize into [0..1]
nx0 = np.clip(sx / disp_w, 0.0, 1.0)
ny0 = np.clip(sy / disp_h, 0.0, 1.0)
nx1 = np.clip(ex / disp_w, 0.0, 1.0)
ny1 = np.clip(ey / disp_h, 0.0, 1.0)
# 5) map to imagepixel coords
x0 = int(nx0 * w)
y0 = int(ny0 * h)
x1 = int(nx1 * w)
y1 = int(ny1 * h)
# 6) sort & clamp again just in case
x0, x1 = sorted((max(0, x0), min(w, x1)))
y0, y1 = sorted((max(0, y0), min(h, y1)))
# 7) avoid zeroarea
if x0 == x1 or y0 == y1:
return img
return img[y0:y1, x0:x1]