Add image adjustment sliders, mask alpha, Load Prev Mask, and button state colours
- Three vertical sliders (Brightness, Contrast, Gamma) to the left of the canvas for display-only image adjustment; all use power/linear formulae applied on-the-fly without touching saved data - Alpha slider controls mask overlay transparency - Brush size slider moved to its own row - Each slider has a reset (↺) button restoring its default value - Hide Mask button turns red when active; Eraser button turns orange - Load Prev Mask button copies the saved mask from the previous clip in the list onto the current clip; the action is pushed onto the undo stack so it can be reverted with Undo - Right survey panel narrowed (stretch factor 2 → 1) - README Controls section updated to document all new features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,12 @@ from PySide6.QtWidgets import QPushButton, QSlider
|
||||
class MaskCanvas:
|
||||
"""Matplotlib canvas with brush-based mask drawing, undo/redo, and erase."""
|
||||
|
||||
_BRUSH_DEFAULT = 5
|
||||
_ALPHA_DEFAULT = 40
|
||||
_BRIGHTNESS_DEFAULT = 0
|
||||
_CONTRAST_DEFAULT = 0
|
||||
_GAMMA_DEFAULT = 100
|
||||
|
||||
def __init__(self, frames, dh: int, dw: int):
|
||||
self.dh = dh
|
||||
self.dw = dw
|
||||
@@ -19,6 +25,7 @@ class MaskCanvas:
|
||||
self.erase_mode = False
|
||||
self.drawing = False
|
||||
self.mask_visible = True
|
||||
self._current_frame = frames[0]
|
||||
|
||||
self._build_figure(frames)
|
||||
self._build_controls()
|
||||
@@ -40,9 +47,36 @@ class MaskCanvas:
|
||||
def _build_controls(self):
|
||||
self.btn_erase = QPushButton("Eraser")
|
||||
self.btn_mask = QPushButton("Hide Mask")
|
||||
|
||||
self.brush_slider = QSlider(Qt.Horizontal)
|
||||
self.brush_slider.setRange(2, 50)
|
||||
self.brush_slider.setValue(5)
|
||||
self.brush_slider.setValue(self._BRUSH_DEFAULT)
|
||||
self.brush_reset = QPushButton("↺")
|
||||
self.brush_reset.setFixedWidth(28)
|
||||
|
||||
self.alpha_slider = QSlider(Qt.Horizontal)
|
||||
self.alpha_slider.setRange(0, 100)
|
||||
self.alpha_slider.setValue(self._ALPHA_DEFAULT)
|
||||
self.alpha_reset = QPushButton("↺")
|
||||
self.alpha_reset.setFixedWidth(28)
|
||||
|
||||
self.brightness_slider = QSlider(Qt.Vertical)
|
||||
self.brightness_slider.setRange(-100, 100)
|
||||
self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
|
||||
self.brightness_reset = QPushButton("↺")
|
||||
self.brightness_reset.setFixedWidth(28)
|
||||
|
||||
self.contrast_slider = QSlider(Qt.Vertical)
|
||||
self.contrast_slider.setRange(-100, 100)
|
||||
self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
|
||||
self.contrast_reset = QPushButton("↺")
|
||||
self.contrast_reset.setFixedWidth(28)
|
||||
|
||||
self.gamma_slider = QSlider(Qt.Vertical)
|
||||
self.gamma_slider.setRange(10, 300)
|
||||
self.gamma_slider.setValue(self._GAMMA_DEFAULT)
|
||||
self.gamma_reset = QPushButton("↺")
|
||||
self.gamma_reset.setFixedWidth(28)
|
||||
|
||||
def _connect_events(self):
|
||||
self.canvas.mpl_connect("button_press_event", self._on_press)
|
||||
@@ -51,6 +85,25 @@ class MaskCanvas:
|
||||
self.canvas.mpl_connect("axes_leave_event", self._on_axes_leave)
|
||||
self.btn_erase.clicked.connect(self.toggle_erase)
|
||||
self.btn_mask.clicked.connect(self.toggle_mask)
|
||||
self.alpha_slider.valueChanged.connect(self.redraw)
|
||||
self.brightness_slider.valueChanged.connect(self._refresh_frame)
|
||||
self.contrast_slider.valueChanged.connect(self._refresh_frame)
|
||||
self.gamma_slider.valueChanged.connect(self._refresh_frame)
|
||||
self.brush_reset.clicked.connect(
|
||||
lambda: self.brush_slider.setValue(self._BRUSH_DEFAULT)
|
||||
)
|
||||
self.alpha_reset.clicked.connect(
|
||||
lambda: self.alpha_slider.setValue(self._ALPHA_DEFAULT)
|
||||
)
|
||||
self.brightness_reset.clicked.connect(
|
||||
lambda: self.brightness_slider.setValue(self._BRIGHTNESS_DEFAULT)
|
||||
)
|
||||
self.contrast_reset.clicked.connect(
|
||||
lambda: self.contrast_slider.setValue(self._CONTRAST_DEFAULT)
|
||||
)
|
||||
self.gamma_reset.clicked.connect(
|
||||
lambda: self.gamma_slider.setValue(self._GAMMA_DEFAULT)
|
||||
)
|
||||
|
||||
# ── clip transition ────────────────────────────────────────────
|
||||
def load_clip(self, frames, dh: int, dw: int, mask=None, title: str = ""):
|
||||
@@ -59,15 +112,33 @@ class MaskCanvas:
|
||||
self.mask = mask if mask is not None else np.zeros((dh, dw), dtype=np.uint8)
|
||||
self.history = []
|
||||
self.redo_stack = []
|
||||
self.img_artist.set_data(frames[0])
|
||||
self._current_frame = frames[0]
|
||||
self.img_artist.set_data(self._apply_image_adjustments(frames[0]))
|
||||
self.set_title(title)
|
||||
self.redraw()
|
||||
|
||||
# ── frame / title ──────────────────────────────────────────────
|
||||
def set_frame(self, frame):
|
||||
self.img_artist.set_data(frame)
|
||||
self._current_frame = frame
|
||||
self.img_artist.set_data(self._apply_image_adjustments(frame))
|
||||
self.canvas.draw_idle()
|
||||
|
||||
# ── image adjustments ──────────────────────────────────────────
|
||||
def _apply_image_adjustments(self, frame):
|
||||
img = frame.astype(np.float32)
|
||||
img += self.brightness_slider.value()
|
||||
c = self.contrast_slider.value() / 100.0
|
||||
img = (1.0 + c) * (img - 128.0) + 128.0
|
||||
np.clip(img, 0, 255, out=img)
|
||||
g = self.gamma_slider.value() / 100.0
|
||||
img = (img / 255.0) ** (1.0 / g) * 255.0
|
||||
return np.clip(img, 0, 255).astype(np.uint8)
|
||||
|
||||
def _refresh_frame(self):
|
||||
if self._current_frame is not None:
|
||||
self.img_artist.set_data(self._apply_image_adjustments(self._current_frame))
|
||||
self.canvas.draw_idle()
|
||||
|
||||
def set_title(self, text: str):
|
||||
self.title_text.set_text(text)
|
||||
|
||||
@@ -80,11 +151,19 @@ class MaskCanvas:
|
||||
self.redo_stack = []
|
||||
self.redraw()
|
||||
|
||||
def set_mask(self, mask):
|
||||
"""Replace the mask and push the previous state onto the undo stack."""
|
||||
self.history.append(self.mask.copy())
|
||||
self.redo_stack.clear()
|
||||
self.mask = mask
|
||||
self.redraw()
|
||||
|
||||
def redraw(self):
|
||||
if self.mask_visible:
|
||||
alpha = self.alpha_slider.value() / 100.0
|
||||
rgba = np.zeros((self.dh, self.dw, 4))
|
||||
rgba[..., 1] = self.mask * 0.7
|
||||
rgba[..., 3] = self.mask * 0.4
|
||||
rgba[..., 3] = self.mask * alpha
|
||||
else:
|
||||
rgba = np.zeros((self.dh, self.dw, 4))
|
||||
self.mask_artist.set_data(rgba)
|
||||
@@ -116,11 +195,21 @@ class MaskCanvas:
|
||||
|
||||
def toggle_erase(self):
|
||||
self.erase_mode = not self.erase_mode
|
||||
self.btn_erase.setText("Eraser ON" if self.erase_mode else "Eraser")
|
||||
if self.erase_mode:
|
||||
self.btn_erase.setText("Eraser ON")
|
||||
self.btn_erase.setStyleSheet("background-color: orange; color: black;")
|
||||
else:
|
||||
self.btn_erase.setText("Eraser")
|
||||
self.btn_erase.setStyleSheet("")
|
||||
|
||||
def toggle_mask(self):
|
||||
self.mask_visible = not self.mask_visible
|
||||
self.btn_mask.setText("Show Mask" if not self.mask_visible else "Hide Mask")
|
||||
if self.mask_visible:
|
||||
self.btn_mask.setText("Hide Mask")
|
||||
self.btn_mask.setStyleSheet("")
|
||||
else:
|
||||
self.btn_mask.setText("Show Mask")
|
||||
self.btn_mask.setStyleSheet("background-color: red; color: white;")
|
||||
self.redraw()
|
||||
|
||||
def stamp(self, x, y):
|
||||
|
||||
Reference in New Issue
Block a user