Loading ballontranslator/ui/canvas.py +33 −37 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ from .misc import ndarray2pixmap, ProjImgTrans from .textitem import TextBlkItem, TextBlock from .texteditshapecontrol import TextBlkShapeControl from .stylewidgets import FadeLabel from .image_edit import StrokeItem, StrokeItem, ImageEditMode, DrawingLayer from .image_edit import ImageEditMode, DrawingLayer, StrokeImgItem CANVAS_SCALE_MAX = 3.0 CANVAS_SCALE_MIN = 0.1 Loading Loading @@ -74,8 +74,8 @@ class Canvas(QGraphicsScene): scalefactor_changed = Signal() end_create_textblock = Signal(QRectF) end_create_rect = Signal(QRectF, int) finish_painting = Signal(StrokeItem) finish_erasing = Signal(StrokeItem) finish_painting = Signal(StrokeImgItem) finish_erasing = Signal(StrokeImgItem) delete_textblks = Signal() format_textblks = Signal() layout_textblks = Signal() Loading Loading @@ -155,7 +155,8 @@ class Canvas(QGraphicsScene): self.scalefactor_changed.connect(self.onScaleFactorChanged) self.selectionChanged.connect(self.on_selection_changed) self.stroke_path_item: StrokeItem = None self.stroke_img_item: StrokeImgItem = None self.stroke_path_item = None self.editor_index = 0 # 0: drawing 1: text editor self.mid_btn_pressed = False Loading Loading @@ -259,10 +260,15 @@ class Canvas(QGraphicsScene): self.alt_pressed = False return super().keyReleaseEvent(event) def addStrokeItem(self, item: StrokeItem, pen: QPen): self.addItem(item) item.setPen(pen) item.setParentItem(self.drawingLayer) def addStrokeImageItem(self, pos: QPointF, pen: QPen, erasing: bool = False): if self.stroke_img_item is not None: self.stroke_img_item.startNewPoint(pos) else: self.stroke_img_item = StrokeImgItem(pen, pos, self.imgLayer.pixmap().size()) self.stroke_img_item.setParentItem(self.baseLayer) def updateStrokeImgItem(self, pos: QPointF): self.stroke_img_item.lineTo(pos) def startCreateTextblock(self, pos: QPointF, hide_control: bool = False): pos = pos / self.scale_factor Loading Loading @@ -297,8 +303,9 @@ class Canvas(QGraphicsScene): elif self.creating_textblock: self.txtblkShapeControl.setRect(QRectF(self.create_block_origin, event.scenePos() / self.scale_factor).normalized()) elif self.stroke_path_item is not None: self.stroke_path_item.addNewPoint(self.imgLayer.mapFromScene(event.scenePos())) elif self.stroke_img_item is not None: if self.stroke_img_item.is_painting: self.stroke_img_item.lineTo(self.imgLayer.mapFromScene(event.scenePos())) elif self.scale_tool_mode: self.scale_tool.emit(event.scenePos()) elif self.rubber_band.isVisible() and self.rubber_band_origin is not None: Loading Loading @@ -327,16 +334,14 @@ class Canvas(QGraphicsScene): if self.alt_pressed: self.scale_tool_mode = True self.begin_scale_tool.emit(event.scenePos()) elif self.painting and self.stroke_path_item is None: self.stroke_path_item = StrokeItem(self.imgLayer.mapFromScene(event.scenePos())) self.addStrokeItem(self.stroke_path_item, self.painting_pen) elif self.painting: self.addStrokeImageItem(self.imgLayer.mapFromScene(event.scenePos()), self.painting_pen) elif btn == Qt.MouseButton.RightButton: # user is drawing using eraser if self.painting and self.stroke_path_item is None: if self.painting: erasing = self.image_edit_mode == ImageEditMode.PenTool self.stroke_path_item = StrokeItem(self.imgLayer.mapFromScene(event.scenePos()), erasing) self.addStrokeItem(self.stroke_path_item, self.erasing_pen) self.addStrokeImageItem(self.imgLayer.mapFromScene(event.scenePos()), self.painting_pen, erasing) else: # rubber band selection self.rubber_band_origin = event.scenePos() self.rubber_band.setGeometry(QRectF(self.rubber_band_origin, self.rubber_band_origin).normalized()) Loading @@ -357,23 +362,22 @@ class Canvas(QGraphicsScene): btn = 0 if btn == Qt.MouseButton.LeftButton else 1 return self.endCreateTextblock(btn=btn) elif btn == Qt.MouseButton.RightButton: if self.stroke_path_item is not None: self.finish_erasing.emit(self.stroke_path_item) if self.stroke_img_item is not None: self.finish_erasing.emit(self.stroke_img_item) if self.rubber_band.isVisible(): self.rubber_band.hide() self.rubber_band_origin = None elif btn == Qt.MouseButton.LeftButton: if self.stroke_path_item is not None: self.finish_painting.emit(self.stroke_path_item) if self.stroke_img_item is not None: self.finish_painting.emit(self.stroke_img_item) elif self.scale_tool_mode: self.scale_tool_mode = False self.end_scale_tool.emit() self.stroke_path_item = None return super().mouseReleaseEvent(event) def updateCanvas(self): self.editing_textblkitem = None self.stroke_path_item = None self.stroke_img_item = None self.txtblkShapeControl.setBlkItem(None) self.mid_btn_pressed = False Loading @@ -395,9 +399,7 @@ class Canvas(QGraphicsScene): def setDrawingLayer(self, img: Union[QPixmap, np.ndarray] = None): ditems = self.get_drawings(visible=False) for item in ditems: self.removeItem(item) self.drawingLayer.clearAllDrawings() if not self.imgtrans_proj.img_valid: return Loading Loading @@ -454,9 +456,8 @@ class Canvas(QGraphicsScene): self.creating_textblock = False self.create_block_origin = None self.editing_textblkitem = None if self.stroke_path_item is not None: self.removeItem(self.stroke_path_item) self.stroke_path_item = None if self.stroke_img_item is not None: self.removeItem(self.stroke_img_item) def on_undostack_changed(self): if self.undoStack.count() != 0: Loading @@ -470,12 +471,7 @@ class Canvas(QGraphicsScene): self.proj_savestate_changed.emit(un_saved) def removeItem(self, item: QGraphicsItem) -> None: if item == self.stroke_path_item: self.stroke_path_item = None return super().removeItem(item) def get_drawings(self, visible=False) -> List[QGraphicsItem]: ditems = self.drawingLayer.childItems() if visible: ditems = [item for item in ditems if item.isVisible()] return ditems No newline at end of file super().removeItem(item) if isinstance(item, StrokeImgItem): item.setParentItem(None) self.stroke_img_item = None No newline at end of file ballontranslator/ui/drawingpanel.py +48 −57 Original line number Diff line number Diff line from qtpy.QtCore import Signal, Qt, QPointF, QSize, QLineF, QDateTime, QRectF from qtpy.QtCore import Signal, Qt, QPointF, QSize, QLineF, QDateTime, QRectF, QPoint from qtpy.QtWidgets import QPushButton, QComboBox, QSizePolicy, QBoxLayout, QCheckBox, QHBoxLayout, QGraphicsView, QStackedWidget, QVBoxLayout, QLabel, QGraphicsPixmapItem, QGraphicsEllipseItem from qtpy.QtGui import QPen, QColor, QCursor, QPainter, QPixmap, QBrush, QFontMetrics, QImage Loading @@ -16,7 +16,7 @@ from utils.textblock_mask import canny_flood, connected_canny_flood from utils.logger import logger from .dl_manager import DLManager from .image_edit import ImageEditMode, StrokeItem, PixmapItem, DrawingLayer from .image_edit import ImageEditMode, PixmapItem, DrawingLayer, StrokeImgItem from .configpanel import InpaintConfigPanel from .stylewidgets import Widget, SeparatorWidget, ColorPicker, PaintQSlider from .canvas import Canvas Loading Loading @@ -221,7 +221,7 @@ class DrawingPanel(Widget): super().__init__(*args, **kwargs) self.dl_manager: DLManager = None self.canvas = canvas self.inpaint_stroke: StrokeItem = None self.inpaint_stroke: StrokeImgItem = None self.rect_inpaint_dict: dict = None border_pen = QPen(INPAINT_BRUSH_COLOR, 3, Qt.PenStyle.DashLine) Loading Loading @@ -478,29 +478,54 @@ class DrawingPanel(Widget): self.currentTool.setChecked(True) return super().showEvent(event) def on_finish_painting(self, stroke_item: StrokeItem): if stroke_item.isEmpty(): self.canvas.removeItem(stroke_item) return def on_finish_painting(self, stroke_item: StrokeImgItem): stroke_item.finishPainting() if not self.canvas.imgtrans_proj.img_valid: self.canvas.removeItem(stroke_item) return if self.currentTool == self.penTool: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, stroke_item)) rect, _, qimg = stroke_item.clip() if rect is not None: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, rect, qimg)) self.canvas.removeItem(stroke_item) elif self.currentTool == self.inpaintTool: self.mergeInpaintStroke(stroke_item) self.inpaint_stroke = stroke_item if self.canvas.gv.ctrl_pressed: return else: self.runInpaint() def mergeInpaintStroke(self, inpaint_stroke: StrokeItem): if self.inpaint_stroke is None: self.inpaint_stroke = inpaint_stroke else: if not inpaint_stroke.isEmpty(): self.inpaint_stroke.addStroke(inpaint_stroke.stroke) self.canvas.removeItem(inpaint_stroke) def on_finish_erasing(self, stroke_item: StrokeImgItem): stroke_item.finishPainting() # inpainted-erasing logic is essentially the same as inpainting if self.currentTool == self.inpaintTool: rect, mask, _ = stroke_item.clip(mask_only=True) if mask is None: self.canvas.removeItem(stroke_item) return mask = 255 - mask mask_h, mask_w = mask.shape[:2] mask_x, mask_y = rect[0], rect[1] inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] origin = self.canvas.imgtrans_proj.img_array origin = origin[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpainted = self.canvas.imgtrans_proj.inpainted_array inpainted = inpainted[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpaint_mask = self.canvas.imgtrans_proj.mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] # no inpainted need to be erased if inpaint_mask.sum() == 0: return mask = cv2.bitwise_and(mask, inpaint_mask) inpaint_mask = np.zeros_like(inpainted) inpaint_mask[mask > 0] = 1 erased_img = inpaint_mask * inpainted + (1 - inpaint_mask) * origin self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, erased_img, mask, inpaint_rect)) elif self.currentTool == self.penTool: rect, _, qimg = stroke_item.clip() if rect is not None: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, rect, qimg, True)) self.canvas.removeItem(stroke_item) def runInpaint(self, inpaint_dict=None): Loading @@ -511,15 +536,14 @@ class DrawingPanel(Widget): logger.warning("inpainting goes wrong") self.clearInpaintItems() return mask = self.inpaint_stroke.getSubimg(convert_mask=True) pos = self.inpaint_stroke.subBlockPos() rect, mask, _ = self.inpaint_stroke.clip(mask_only=True) if mask is None: self.clearInpaintItems() return # we need to enlarge the mask window a bit to get better results mask_h, mask_w = mask.shape[:2] mask_x, mask_y = pos.x(), pos.y() mask_x, mask_y = rect[0], rect[1] img = self.canvas.imgtrans_proj.inpainted_array inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] rect_enlarged = enlarge_window(inpaint_rect, img.shape[1], img.shape[0]) Loading @@ -545,36 +569,6 @@ class DrawingPanel(Widget): self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, inpainted, mask, inpaint_rect)) self.clearInpaintItems() def on_finish_erasing(self, stroke_item: StrokeItem): # inpainted-erasing logic is essentially the same as inpainting if self.currentTool == self.inpaintTool: mask = stroke_item.getSubimg(convert_mask=True) self.canvas.removeItem(stroke_item) if mask is None: return mask = 255 - mask pos = stroke_item.subBlockPos() mask_h, mask_w = mask.shape[:2] mask_x, mask_y = pos.x(), pos.y() inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] origin = self.canvas.imgtrans_proj.img_array origin = origin[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpainted = self.canvas.imgtrans_proj.inpainted_array inpainted = inpainted[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpaint_mask = self.canvas.imgtrans_proj.mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] # no inpainted need to be erased if inpaint_mask.sum() == 0: return mask = cv2.bitwise_and(mask, inpaint_mask) inpaint_mask = np.zeros_like(inpainted) inpaint_mask[mask > 0] = 1 erased_img = inpaint_mask * inpainted + (1 - inpaint_mask) * origin self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, erased_img, mask, inpaint_rect)) elif self.currentTool == self.penTool: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, self.canvas.stroke_path_item, True)) def on_inpaint_failed(self): if self.currentTool == self.inpaintTool and self.inpaint_stroke is not None: self.clearInpaintItems() Loading Loading @@ -737,16 +731,13 @@ class DrawingPanel(Widget): self.canvas.image_edit_mode = ImageEditMode.InpaintTool class StrokeItemUndoCommand(QUndoCommand): def __init__(self, target_layer: DrawingLayer, stroke_item: StrokeItem, erasing=False): def __init__(self, target_layer: DrawingLayer, rect: Tuple[int], qimg: QImage, erasing=False): super().__init__() self.qimg = stroke_item.convertToQImg().convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) pos = stroke_item.subBlockPos() self.x = pos.x() self.y = pos.y() self.qimg = qimg self.x = rect[0] self.y = rect[1] self.target_layer = target_layer self.key = str(QDateTime.currentMSecsSinceEpoch()) target_layer.scene().removeItem(stroke_item) if erasing: self.compose_mode = QPainter.CompositionMode.CompositionMode_DestinationOut else: Loading ballontranslator/ui/image_edit.py +60 −134 Original line number Diff line number Diff line from typing import Tuple, List, Union import numpy as np import cv2 from qtpy.QtCore import QRectF, Qt, QPointF, QSize, QPoint, QDateTime from qtpy.QtWidgets import QStyleOptionGraphicsItem, QGraphicsPixmapItem, QWidget, QGraphicsPathItem, QGraphicsItem from qtpy.QtGui import QPen, QColor, QPainterPath, QCursor, QPainter, QPixmap, QImage, QBrush Loading @@ -24,155 +25,67 @@ class ImageEditMode: class StrokeImgItem(QGraphicsItem): def __init__(self, pen: QPen, point: QPointF, size: QSize, format: QImage.Format = QImage.Format.Format_ARGB32, ): super().__init__() self._img = QImage(size, format) self._img.fill(Qt.GlobalColor.transparent) self.pen = pen self.painter = QPainter() self._d = d = self.pen.widthF() self._r = d / 2 self.clipped_rect = None self.painter = QPainter(self._img) self.painter.setPen(pen) self.painter.setRenderHint(QPainter.RenderHint.Antialiasing) self.painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) self.setBoundingRegionGranularity(0) self.cur_point = point self.drawLine(point, point) self._br = QRectF(0, 0, size.width(), size.height()) self.is_painting = True self.lineTo(point) def finishPainting(self): self.painter.end() self.is_painting = False def clip(self, mask_only=False, format=QImage.Format.Format_ARGB32_Premultiplied) -> Tuple[List, np.ndarray, QImage]: img_array = pixmap2ndarray(self._img, True) ar = cv2.boundingRect(cv2.findNonZero(img_array[..., -1])) img_array = img_array[ar[1]: ar[1] + ar[3], ar[0]: ar[0] + ar[2]] if not (ar[2] > 0 and ar[3] > 0): return None, None, None if mask_only: img_array = img_array[..., -1] img_array[img_array > 0] = 255 return ar, img_array, self._img.copy(*ar).convertToFormat(format) def startNewPoint(self, pos: QPointF): self.is_painting = True self.painter.begin(self._img) self.painter.setPen(self.pen) self.painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) self.cur_point = pos self.lineTo(pos) def boundingRect(self) -> QRectF: return self._br def drawLine(self, new_pnt: QPointF): self.painter.begin(self._img) def lineTo(self, new_pnt: QPointF): delta = self.cur_point - new_pnt delta_w, delta_h = abs(delta.x()), abs(delta.y()) if delta_w + delta_h > 2: min_x = min(self.cur_point.x(), new_pnt.x()) - self._r min_y = min(self.cur_point.y(), new_pnt.y()) - self._r delta_w += self._d delta_h += self._d rect = QRectF(min_x, min_y, delta_w, delta_h) self.painter.drawLine(self.cur_point, new_pnt) self.painter.end() self.cur_point = new_pnt self.update(rect) def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget) -> None: painter.drawImage(0, 0, self._img) class StrokeItem(QGraphicsPathItem): def __init__(self, origin_point: QPointF, erasing: bool = False): super().__init__() # self.stroke = QPainterPath(QPointF(0, 0)) self.stroke = QPainterPath(QPointF(origin_point)) self.erasing = erasing self.last_point = origin_point self.setPath(self.stroke) self.setBoundingRegionGranularity(0) self.clip_offset = QPointF(0, 0) def addNewPoint(self, new_point: QPointF): if new_point != self.last_point: self.stroke.lineTo(new_point) self.setPath(self.stroke) self.last_point = new_point def addStroke(self, stroke: QPainterPath): self.stroke.addPath(stroke) self.setPath(self.stroke) def isEmpty(self): return self.stroke.isEmpty() def convertToPixmapItem(self, convert_mask=False, remove_stroke=True, target_layer: QGraphicsPixmapItem = None) -> QGraphicsPixmapItem: if target_layer is None: target_layer = self.parentItem() pixmap = self.getSubimg(convert_mask, return_pixmap=True) if pixmap is None: self.scene().removeItem(self) return None, None, None # pixmap = ndarray2pixmap(img_array) pixmap_item = QGraphicsPixmapItem(pixmap) pixmap_item.setParentItem(target_layer) pos = self.subBlockPos() pixmap_item.setPos(pos.x(), pos.y()) if self.scene() is not None: if remove_stroke: self.scene().removeItem(self) else: self.setZValue(3) return pixmap_item def convertToQImg(self, convert_mask=False) -> QImage: img_array = self.getSubimg(convert_mask) if img_array is None: return None qimg = ndarray2pixmap(img_array, return_qimg=True) return qimg def originOffset(self) -> QPointF: thickness = self.pen().widthF() / 2 return QPointF(thickness, thickness) - self.stroke.boundingRect().topLeft() - self.clip_offset def subBlockPos(self) -> QPoint: pos = self.pos() - self.originOffset() pos.setX(int(round(max(0, pos.x())))) pos.setY(int(round(max(0, pos.y())))) return pos.toPoint() def getSubimg(self, convert_mask=False, return_pixmap=False) -> np.ndarray: if self.isEmpty(): return None origin_offset = self.originOffset() parent_layer = self.parentItem() while parent_layer.parentItem() is not None: if isinstance(parent_layer, QGraphicsPixmapItem): layer_size = parent_layer.pixmap().size() parent_layer = parent_layer.parentItem() scale_factor = parent_layer.scale() # layer_size = parent_layer.pixmap().size() max_width, max_height = layer_size.width(), layer_size.height() scene_br = self.sceneBoundingRect() stroke_size = scene_br.size() stroke_size.setHeight(stroke_size.height() / scale_factor) stroke_size.setWidth(stroke_size.width() / scale_factor) stroke_size = stroke_size.toSize() lt = self.pos() - origin_offset xywh = [lt.x(), lt.y(), scene_br.width() / scale_factor, scene_br.height() / scale_factor] xyxy = np.array(xywh) xyxy[[2, 3]] += xyxy[[0, 1]] xyxy = xyxy.astype(np.int) xyxy_clip = xyxy.copy() xyxy_clip[[0, 2]] = np.clip(xyxy[[0, 2]], 0, max_width - 1) xyxy_clip[[1, 3]] = np.clip(xyxy[[1, 3]], 0, max_height - 1) # clipped stroke is empty if xyxy_clip[0] >= xyxy_clip[2] or xyxy_clip[1] >= xyxy_clip[3]: return None pixmap = QPixmap(stroke_size) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.translate(self.originOffset()) painter.setPen(self.pen()) painter.setBrush(self.brush()) painter.drawPath(self.stroke) painter.end() stroke_clip = xyxy_clip - xyxy stroke_clip[2] += stroke_size.width() stroke_clip[3] += stroke_size.height() self.clip_offset = QPointF(stroke_clip[0], stroke_clip[1]) if return_pixmap: return pixmap imgarray = pixmap2ndarray(pixmap, keep_alpha=True) imgarray = imgarray[stroke_clip[1]: stroke_clip[3], stroke_clip[0]: stroke_clip[2]] # print(imgarray.shape, stroke_clip) if convert_mask: mask = imgarray[..., -1] mask[mask > 0] = 255 imgarray[..., :] = mask[..., np.newaxis] return mask return imgarray class PenCursor(QCursor): def __init__(self, *args, **kwargs): super().__init__() Loading Loading @@ -212,6 +125,7 @@ class DrawingLayer(QGraphicsPixmapItem): super().__init__() self.qimg_dict = {} self.drawing_items_info = {} self.drawed_pixmap = None def addQImage(self, x: int, y: int, qimg: QImage, compose_mode, key: str): self.qimg_dict[key] = qimg Loading @@ -235,4 +149,16 @@ class DrawingLayer(QGraphicsPixmapItem): p.drawImage(info['pos'][0], info['pos'][1], item) p.end() painter.drawPixmap(self.offset(), pixmap) self.drawed_pixmap = pixmap def get_drawed_pixmap(self, format=QImage.Format.Format_ARGB32) -> QPixmap: pixmap = self.pixmap() if self.drawed_pixmap is None else self.drawed_pixmap return pixmap def drawed(self) -> bool: return len(self.qimg_dict) > 0 def clearAllDrawings(self): self.qimg_dict.clear() self.drawing_items_info.clear() ballontranslator/ui/io_thread.py +2 −2 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import numpy as np from utils.io_utils import imread, imwrite from qtpy.QtCore import Qt, Signal, QPoint, QSize, QThread from qtpy.QtGui import QImage from qtpy.QtGui import QImage, QPixmap class ImgSaveThread(QThread): Loading @@ -23,7 +23,7 @@ class ImgSaveThread(QThread): if len(self.im_save_list) == 0: break save_path, img = self.im_save_list.pop(0) if isinstance(img, QImage): if isinstance(img, QImage) or isinstance(img, QPixmap): img.save(save_path) elif isinstance(img, np.ndarray): imwrite(save_path, img) Loading ballontranslator/ui/mainwindow.py +18 −12 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
ballontranslator/ui/canvas.py +33 −37 Original line number Diff line number Diff line Loading @@ -15,7 +15,7 @@ from .misc import ndarray2pixmap, ProjImgTrans from .textitem import TextBlkItem, TextBlock from .texteditshapecontrol import TextBlkShapeControl from .stylewidgets import FadeLabel from .image_edit import StrokeItem, StrokeItem, ImageEditMode, DrawingLayer from .image_edit import ImageEditMode, DrawingLayer, StrokeImgItem CANVAS_SCALE_MAX = 3.0 CANVAS_SCALE_MIN = 0.1 Loading Loading @@ -74,8 +74,8 @@ class Canvas(QGraphicsScene): scalefactor_changed = Signal() end_create_textblock = Signal(QRectF) end_create_rect = Signal(QRectF, int) finish_painting = Signal(StrokeItem) finish_erasing = Signal(StrokeItem) finish_painting = Signal(StrokeImgItem) finish_erasing = Signal(StrokeImgItem) delete_textblks = Signal() format_textblks = Signal() layout_textblks = Signal() Loading Loading @@ -155,7 +155,8 @@ class Canvas(QGraphicsScene): self.scalefactor_changed.connect(self.onScaleFactorChanged) self.selectionChanged.connect(self.on_selection_changed) self.stroke_path_item: StrokeItem = None self.stroke_img_item: StrokeImgItem = None self.stroke_path_item = None self.editor_index = 0 # 0: drawing 1: text editor self.mid_btn_pressed = False Loading Loading @@ -259,10 +260,15 @@ class Canvas(QGraphicsScene): self.alt_pressed = False return super().keyReleaseEvent(event) def addStrokeItem(self, item: StrokeItem, pen: QPen): self.addItem(item) item.setPen(pen) item.setParentItem(self.drawingLayer) def addStrokeImageItem(self, pos: QPointF, pen: QPen, erasing: bool = False): if self.stroke_img_item is not None: self.stroke_img_item.startNewPoint(pos) else: self.stroke_img_item = StrokeImgItem(pen, pos, self.imgLayer.pixmap().size()) self.stroke_img_item.setParentItem(self.baseLayer) def updateStrokeImgItem(self, pos: QPointF): self.stroke_img_item.lineTo(pos) def startCreateTextblock(self, pos: QPointF, hide_control: bool = False): pos = pos / self.scale_factor Loading Loading @@ -297,8 +303,9 @@ class Canvas(QGraphicsScene): elif self.creating_textblock: self.txtblkShapeControl.setRect(QRectF(self.create_block_origin, event.scenePos() / self.scale_factor).normalized()) elif self.stroke_path_item is not None: self.stroke_path_item.addNewPoint(self.imgLayer.mapFromScene(event.scenePos())) elif self.stroke_img_item is not None: if self.stroke_img_item.is_painting: self.stroke_img_item.lineTo(self.imgLayer.mapFromScene(event.scenePos())) elif self.scale_tool_mode: self.scale_tool.emit(event.scenePos()) elif self.rubber_band.isVisible() and self.rubber_band_origin is not None: Loading Loading @@ -327,16 +334,14 @@ class Canvas(QGraphicsScene): if self.alt_pressed: self.scale_tool_mode = True self.begin_scale_tool.emit(event.scenePos()) elif self.painting and self.stroke_path_item is None: self.stroke_path_item = StrokeItem(self.imgLayer.mapFromScene(event.scenePos())) self.addStrokeItem(self.stroke_path_item, self.painting_pen) elif self.painting: self.addStrokeImageItem(self.imgLayer.mapFromScene(event.scenePos()), self.painting_pen) elif btn == Qt.MouseButton.RightButton: # user is drawing using eraser if self.painting and self.stroke_path_item is None: if self.painting: erasing = self.image_edit_mode == ImageEditMode.PenTool self.stroke_path_item = StrokeItem(self.imgLayer.mapFromScene(event.scenePos()), erasing) self.addStrokeItem(self.stroke_path_item, self.erasing_pen) self.addStrokeImageItem(self.imgLayer.mapFromScene(event.scenePos()), self.painting_pen, erasing) else: # rubber band selection self.rubber_band_origin = event.scenePos() self.rubber_band.setGeometry(QRectF(self.rubber_band_origin, self.rubber_band_origin).normalized()) Loading @@ -357,23 +362,22 @@ class Canvas(QGraphicsScene): btn = 0 if btn == Qt.MouseButton.LeftButton else 1 return self.endCreateTextblock(btn=btn) elif btn == Qt.MouseButton.RightButton: if self.stroke_path_item is not None: self.finish_erasing.emit(self.stroke_path_item) if self.stroke_img_item is not None: self.finish_erasing.emit(self.stroke_img_item) if self.rubber_band.isVisible(): self.rubber_band.hide() self.rubber_band_origin = None elif btn == Qt.MouseButton.LeftButton: if self.stroke_path_item is not None: self.finish_painting.emit(self.stroke_path_item) if self.stroke_img_item is not None: self.finish_painting.emit(self.stroke_img_item) elif self.scale_tool_mode: self.scale_tool_mode = False self.end_scale_tool.emit() self.stroke_path_item = None return super().mouseReleaseEvent(event) def updateCanvas(self): self.editing_textblkitem = None self.stroke_path_item = None self.stroke_img_item = None self.txtblkShapeControl.setBlkItem(None) self.mid_btn_pressed = False Loading @@ -395,9 +399,7 @@ class Canvas(QGraphicsScene): def setDrawingLayer(self, img: Union[QPixmap, np.ndarray] = None): ditems = self.get_drawings(visible=False) for item in ditems: self.removeItem(item) self.drawingLayer.clearAllDrawings() if not self.imgtrans_proj.img_valid: return Loading Loading @@ -454,9 +456,8 @@ class Canvas(QGraphicsScene): self.creating_textblock = False self.create_block_origin = None self.editing_textblkitem = None if self.stroke_path_item is not None: self.removeItem(self.stroke_path_item) self.stroke_path_item = None if self.stroke_img_item is not None: self.removeItem(self.stroke_img_item) def on_undostack_changed(self): if self.undoStack.count() != 0: Loading @@ -470,12 +471,7 @@ class Canvas(QGraphicsScene): self.proj_savestate_changed.emit(un_saved) def removeItem(self, item: QGraphicsItem) -> None: if item == self.stroke_path_item: self.stroke_path_item = None return super().removeItem(item) def get_drawings(self, visible=False) -> List[QGraphicsItem]: ditems = self.drawingLayer.childItems() if visible: ditems = [item for item in ditems if item.isVisible()] return ditems No newline at end of file super().removeItem(item) if isinstance(item, StrokeImgItem): item.setParentItem(None) self.stroke_img_item = None No newline at end of file
ballontranslator/ui/drawingpanel.py +48 −57 Original line number Diff line number Diff line from qtpy.QtCore import Signal, Qt, QPointF, QSize, QLineF, QDateTime, QRectF from qtpy.QtCore import Signal, Qt, QPointF, QSize, QLineF, QDateTime, QRectF, QPoint from qtpy.QtWidgets import QPushButton, QComboBox, QSizePolicy, QBoxLayout, QCheckBox, QHBoxLayout, QGraphicsView, QStackedWidget, QVBoxLayout, QLabel, QGraphicsPixmapItem, QGraphicsEllipseItem from qtpy.QtGui import QPen, QColor, QCursor, QPainter, QPixmap, QBrush, QFontMetrics, QImage Loading @@ -16,7 +16,7 @@ from utils.textblock_mask import canny_flood, connected_canny_flood from utils.logger import logger from .dl_manager import DLManager from .image_edit import ImageEditMode, StrokeItem, PixmapItem, DrawingLayer from .image_edit import ImageEditMode, PixmapItem, DrawingLayer, StrokeImgItem from .configpanel import InpaintConfigPanel from .stylewidgets import Widget, SeparatorWidget, ColorPicker, PaintQSlider from .canvas import Canvas Loading Loading @@ -221,7 +221,7 @@ class DrawingPanel(Widget): super().__init__(*args, **kwargs) self.dl_manager: DLManager = None self.canvas = canvas self.inpaint_stroke: StrokeItem = None self.inpaint_stroke: StrokeImgItem = None self.rect_inpaint_dict: dict = None border_pen = QPen(INPAINT_BRUSH_COLOR, 3, Qt.PenStyle.DashLine) Loading Loading @@ -478,29 +478,54 @@ class DrawingPanel(Widget): self.currentTool.setChecked(True) return super().showEvent(event) def on_finish_painting(self, stroke_item: StrokeItem): if stroke_item.isEmpty(): self.canvas.removeItem(stroke_item) return def on_finish_painting(self, stroke_item: StrokeImgItem): stroke_item.finishPainting() if not self.canvas.imgtrans_proj.img_valid: self.canvas.removeItem(stroke_item) return if self.currentTool == self.penTool: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, stroke_item)) rect, _, qimg = stroke_item.clip() if rect is not None: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, rect, qimg)) self.canvas.removeItem(stroke_item) elif self.currentTool == self.inpaintTool: self.mergeInpaintStroke(stroke_item) self.inpaint_stroke = stroke_item if self.canvas.gv.ctrl_pressed: return else: self.runInpaint() def mergeInpaintStroke(self, inpaint_stroke: StrokeItem): if self.inpaint_stroke is None: self.inpaint_stroke = inpaint_stroke else: if not inpaint_stroke.isEmpty(): self.inpaint_stroke.addStroke(inpaint_stroke.stroke) self.canvas.removeItem(inpaint_stroke) def on_finish_erasing(self, stroke_item: StrokeImgItem): stroke_item.finishPainting() # inpainted-erasing logic is essentially the same as inpainting if self.currentTool == self.inpaintTool: rect, mask, _ = stroke_item.clip(mask_only=True) if mask is None: self.canvas.removeItem(stroke_item) return mask = 255 - mask mask_h, mask_w = mask.shape[:2] mask_x, mask_y = rect[0], rect[1] inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] origin = self.canvas.imgtrans_proj.img_array origin = origin[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpainted = self.canvas.imgtrans_proj.inpainted_array inpainted = inpainted[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpaint_mask = self.canvas.imgtrans_proj.mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] # no inpainted need to be erased if inpaint_mask.sum() == 0: return mask = cv2.bitwise_and(mask, inpaint_mask) inpaint_mask = np.zeros_like(inpainted) inpaint_mask[mask > 0] = 1 erased_img = inpaint_mask * inpainted + (1 - inpaint_mask) * origin self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, erased_img, mask, inpaint_rect)) elif self.currentTool == self.penTool: rect, _, qimg = stroke_item.clip() if rect is not None: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, rect, qimg, True)) self.canvas.removeItem(stroke_item) def runInpaint(self, inpaint_dict=None): Loading @@ -511,15 +536,14 @@ class DrawingPanel(Widget): logger.warning("inpainting goes wrong") self.clearInpaintItems() return mask = self.inpaint_stroke.getSubimg(convert_mask=True) pos = self.inpaint_stroke.subBlockPos() rect, mask, _ = self.inpaint_stroke.clip(mask_only=True) if mask is None: self.clearInpaintItems() return # we need to enlarge the mask window a bit to get better results mask_h, mask_w = mask.shape[:2] mask_x, mask_y = pos.x(), pos.y() mask_x, mask_y = rect[0], rect[1] img = self.canvas.imgtrans_proj.inpainted_array inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] rect_enlarged = enlarge_window(inpaint_rect, img.shape[1], img.shape[0]) Loading @@ -545,36 +569,6 @@ class DrawingPanel(Widget): self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, inpainted, mask, inpaint_rect)) self.clearInpaintItems() def on_finish_erasing(self, stroke_item: StrokeItem): # inpainted-erasing logic is essentially the same as inpainting if self.currentTool == self.inpaintTool: mask = stroke_item.getSubimg(convert_mask=True) self.canvas.removeItem(stroke_item) if mask is None: return mask = 255 - mask pos = stroke_item.subBlockPos() mask_h, mask_w = mask.shape[:2] mask_x, mask_y = pos.x(), pos.y() inpaint_rect = [mask_x, mask_y, mask_w + mask_x, mask_h + mask_y] origin = self.canvas.imgtrans_proj.img_array origin = origin[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpainted = self.canvas.imgtrans_proj.inpainted_array inpainted = inpainted[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] inpaint_mask = self.canvas.imgtrans_proj.mask_array[inpaint_rect[1]: inpaint_rect[3], inpaint_rect[0]: inpaint_rect[2]] # no inpainted need to be erased if inpaint_mask.sum() == 0: return mask = cv2.bitwise_and(mask, inpaint_mask) inpaint_mask = np.zeros_like(inpainted) inpaint_mask[mask > 0] = 1 erased_img = inpaint_mask * inpainted + (1 - inpaint_mask) * origin self.canvas.undoStack.push(InpaintUndoCommand(self.canvas, erased_img, mask, inpaint_rect)) elif self.currentTool == self.penTool: self.canvas.undoStack.push(StrokeItemUndoCommand(self.canvas.drawingLayer, self.canvas.stroke_path_item, True)) def on_inpaint_failed(self): if self.currentTool == self.inpaintTool and self.inpaint_stroke is not None: self.clearInpaintItems() Loading Loading @@ -737,16 +731,13 @@ class DrawingPanel(Widget): self.canvas.image_edit_mode = ImageEditMode.InpaintTool class StrokeItemUndoCommand(QUndoCommand): def __init__(self, target_layer: DrawingLayer, stroke_item: StrokeItem, erasing=False): def __init__(self, target_layer: DrawingLayer, rect: Tuple[int], qimg: QImage, erasing=False): super().__init__() self.qimg = stroke_item.convertToQImg().convertToFormat(QImage.Format.Format_ARGB32_Premultiplied) pos = stroke_item.subBlockPos() self.x = pos.x() self.y = pos.y() self.qimg = qimg self.x = rect[0] self.y = rect[1] self.target_layer = target_layer self.key = str(QDateTime.currentMSecsSinceEpoch()) target_layer.scene().removeItem(stroke_item) if erasing: self.compose_mode = QPainter.CompositionMode.CompositionMode_DestinationOut else: Loading
ballontranslator/ui/image_edit.py +60 −134 Original line number Diff line number Diff line from typing import Tuple, List, Union import numpy as np import cv2 from qtpy.QtCore import QRectF, Qt, QPointF, QSize, QPoint, QDateTime from qtpy.QtWidgets import QStyleOptionGraphicsItem, QGraphicsPixmapItem, QWidget, QGraphicsPathItem, QGraphicsItem from qtpy.QtGui import QPen, QColor, QPainterPath, QCursor, QPainter, QPixmap, QImage, QBrush Loading @@ -24,155 +25,67 @@ class ImageEditMode: class StrokeImgItem(QGraphicsItem): def __init__(self, pen: QPen, point: QPointF, size: QSize, format: QImage.Format = QImage.Format.Format_ARGB32, ): super().__init__() self._img = QImage(size, format) self._img.fill(Qt.GlobalColor.transparent) self.pen = pen self.painter = QPainter() self._d = d = self.pen.widthF() self._r = d / 2 self.clipped_rect = None self.painter = QPainter(self._img) self.painter.setPen(pen) self.painter.setRenderHint(QPainter.RenderHint.Antialiasing) self.painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) self.setBoundingRegionGranularity(0) self.cur_point = point self.drawLine(point, point) self._br = QRectF(0, 0, size.width(), size.height()) self.is_painting = True self.lineTo(point) def finishPainting(self): self.painter.end() self.is_painting = False def clip(self, mask_only=False, format=QImage.Format.Format_ARGB32_Premultiplied) -> Tuple[List, np.ndarray, QImage]: img_array = pixmap2ndarray(self._img, True) ar = cv2.boundingRect(cv2.findNonZero(img_array[..., -1])) img_array = img_array[ar[1]: ar[1] + ar[3], ar[0]: ar[0] + ar[2]] if not (ar[2] > 0 and ar[3] > 0): return None, None, None if mask_only: img_array = img_array[..., -1] img_array[img_array > 0] = 255 return ar, img_array, self._img.copy(*ar).convertToFormat(format) def startNewPoint(self, pos: QPointF): self.is_painting = True self.painter.begin(self._img) self.painter.setPen(self.pen) self.painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) self.cur_point = pos self.lineTo(pos) def boundingRect(self) -> QRectF: return self._br def drawLine(self, new_pnt: QPointF): self.painter.begin(self._img) def lineTo(self, new_pnt: QPointF): delta = self.cur_point - new_pnt delta_w, delta_h = abs(delta.x()), abs(delta.y()) if delta_w + delta_h > 2: min_x = min(self.cur_point.x(), new_pnt.x()) - self._r min_y = min(self.cur_point.y(), new_pnt.y()) - self._r delta_w += self._d delta_h += self._d rect = QRectF(min_x, min_y, delta_w, delta_h) self.painter.drawLine(self.cur_point, new_pnt) self.painter.end() self.cur_point = new_pnt self.update(rect) def paint(self, painter: QPainter, option: QStyleOptionGraphicsItem, widget: QWidget) -> None: painter.drawImage(0, 0, self._img) class StrokeItem(QGraphicsPathItem): def __init__(self, origin_point: QPointF, erasing: bool = False): super().__init__() # self.stroke = QPainterPath(QPointF(0, 0)) self.stroke = QPainterPath(QPointF(origin_point)) self.erasing = erasing self.last_point = origin_point self.setPath(self.stroke) self.setBoundingRegionGranularity(0) self.clip_offset = QPointF(0, 0) def addNewPoint(self, new_point: QPointF): if new_point != self.last_point: self.stroke.lineTo(new_point) self.setPath(self.stroke) self.last_point = new_point def addStroke(self, stroke: QPainterPath): self.stroke.addPath(stroke) self.setPath(self.stroke) def isEmpty(self): return self.stroke.isEmpty() def convertToPixmapItem(self, convert_mask=False, remove_stroke=True, target_layer: QGraphicsPixmapItem = None) -> QGraphicsPixmapItem: if target_layer is None: target_layer = self.parentItem() pixmap = self.getSubimg(convert_mask, return_pixmap=True) if pixmap is None: self.scene().removeItem(self) return None, None, None # pixmap = ndarray2pixmap(img_array) pixmap_item = QGraphicsPixmapItem(pixmap) pixmap_item.setParentItem(target_layer) pos = self.subBlockPos() pixmap_item.setPos(pos.x(), pos.y()) if self.scene() is not None: if remove_stroke: self.scene().removeItem(self) else: self.setZValue(3) return pixmap_item def convertToQImg(self, convert_mask=False) -> QImage: img_array = self.getSubimg(convert_mask) if img_array is None: return None qimg = ndarray2pixmap(img_array, return_qimg=True) return qimg def originOffset(self) -> QPointF: thickness = self.pen().widthF() / 2 return QPointF(thickness, thickness) - self.stroke.boundingRect().topLeft() - self.clip_offset def subBlockPos(self) -> QPoint: pos = self.pos() - self.originOffset() pos.setX(int(round(max(0, pos.x())))) pos.setY(int(round(max(0, pos.y())))) return pos.toPoint() def getSubimg(self, convert_mask=False, return_pixmap=False) -> np.ndarray: if self.isEmpty(): return None origin_offset = self.originOffset() parent_layer = self.parentItem() while parent_layer.parentItem() is not None: if isinstance(parent_layer, QGraphicsPixmapItem): layer_size = parent_layer.pixmap().size() parent_layer = parent_layer.parentItem() scale_factor = parent_layer.scale() # layer_size = parent_layer.pixmap().size() max_width, max_height = layer_size.width(), layer_size.height() scene_br = self.sceneBoundingRect() stroke_size = scene_br.size() stroke_size.setHeight(stroke_size.height() / scale_factor) stroke_size.setWidth(stroke_size.width() / scale_factor) stroke_size = stroke_size.toSize() lt = self.pos() - origin_offset xywh = [lt.x(), lt.y(), scene_br.width() / scale_factor, scene_br.height() / scale_factor] xyxy = np.array(xywh) xyxy[[2, 3]] += xyxy[[0, 1]] xyxy = xyxy.astype(np.int) xyxy_clip = xyxy.copy() xyxy_clip[[0, 2]] = np.clip(xyxy[[0, 2]], 0, max_width - 1) xyxy_clip[[1, 3]] = np.clip(xyxy[[1, 3]], 0, max_height - 1) # clipped stroke is empty if xyxy_clip[0] >= xyxy_clip[2] or xyxy_clip[1] >= xyxy_clip[3]: return None pixmap = QPixmap(stroke_size) pixmap.fill(Qt.GlobalColor.transparent) painter = QPainter(pixmap) painter.setRenderHint(QPainter.RenderHint.Antialiasing) painter.translate(self.originOffset()) painter.setPen(self.pen()) painter.setBrush(self.brush()) painter.drawPath(self.stroke) painter.end() stroke_clip = xyxy_clip - xyxy stroke_clip[2] += stroke_size.width() stroke_clip[3] += stroke_size.height() self.clip_offset = QPointF(stroke_clip[0], stroke_clip[1]) if return_pixmap: return pixmap imgarray = pixmap2ndarray(pixmap, keep_alpha=True) imgarray = imgarray[stroke_clip[1]: stroke_clip[3], stroke_clip[0]: stroke_clip[2]] # print(imgarray.shape, stroke_clip) if convert_mask: mask = imgarray[..., -1] mask[mask > 0] = 255 imgarray[..., :] = mask[..., np.newaxis] return mask return imgarray class PenCursor(QCursor): def __init__(self, *args, **kwargs): super().__init__() Loading Loading @@ -212,6 +125,7 @@ class DrawingLayer(QGraphicsPixmapItem): super().__init__() self.qimg_dict = {} self.drawing_items_info = {} self.drawed_pixmap = None def addQImage(self, x: int, y: int, qimg: QImage, compose_mode, key: str): self.qimg_dict[key] = qimg Loading @@ -235,4 +149,16 @@ class DrawingLayer(QGraphicsPixmapItem): p.drawImage(info['pos'][0], info['pos'][1], item) p.end() painter.drawPixmap(self.offset(), pixmap) self.drawed_pixmap = pixmap def get_drawed_pixmap(self, format=QImage.Format.Format_ARGB32) -> QPixmap: pixmap = self.pixmap() if self.drawed_pixmap is None else self.drawed_pixmap return pixmap def drawed(self) -> bool: return len(self.qimg_dict) > 0 def clearAllDrawings(self): self.qimg_dict.clear() self.drawing_items_info.clear()
ballontranslator/ui/io_thread.py +2 −2 Original line number Diff line number Diff line Loading @@ -2,7 +2,7 @@ import numpy as np from utils.io_utils import imread, imwrite from qtpy.QtCore import Qt, Signal, QPoint, QSize, QThread from qtpy.QtGui import QImage from qtpy.QtGui import QImage, QPixmap class ImgSaveThread(QThread): Loading @@ -23,7 +23,7 @@ class ImgSaveThread(QThread): if len(self.im_save_list) == 0: break save_path, img = self.im_save_list.pop(0) if isinstance(img, QImage): if isinstance(img, QImage) or isinstance(img, QPixmap): img.save(save_path) elif isinstance(img, np.ndarray): imwrite(save_path, img) Loading
ballontranslator/ui/mainwindow.py +18 −12 File changed.Preview size limit exceeded, changes collapsed. Show changes