Source code for arthropod_describer.tools.brush
import importlib.resources
from typing import List, Dict, Any, Tuple, Optional
import numpy as np
from PySide2.QtCore import QPoint, Slot, QRect
from PySide2.QtGui import QImage, QPainter, QColor, QBrush, QPen, QBitmap, QRegion, QPaintDevice, Qt, QPixmap, QIcon
from skimage import draw
import skimage.morphology as M
import qimage2ndarray
from arthropod_describer.common.photo import LabelImg
from arthropod_describer.common.state import State
from arthropod_describer.common.tool import Tool, EditContext
from arthropod_describer.common.user_params import UserParam, ParamType
from arthropod_describer.common.label_change import LabelChange, label_difference_to_label_changes, CommandEntry
TOOL_CLASS_NAME = 'Brush'
[docs]class Tool_Brush(Tool):
def __init__(self, state: State):
Tool.__init__(self, state)
self._tool_name = "Brush"
self._user_params = {'Radius': UserParam('Radius', ParamType.INT, 9, min_val=1, max_val=75, step=2)}
self._user_params['Radius'].default_instance.value_changed.connect(lambda e: self.cursor_changed.emit(self.tool_id, self.cursor_image))
self._current_img = None
self.edit_mask = QImage()
self.edit_painter = QPainter(self.edit_mask)
radius = self._user_params['Radius'].value
self.state = state
self._primary_label: int = 0
self._secondary_label: int = 0
self.commands: List[CommandEntry] = []
self.label_changes: List[LabelChange] = []
self._brush_mask: np.ndarray = M.disk(radius, np.uint16)
self._brush_icon = QImage()
self._brush_center = np.array([radius, radius])
self._brush_coords = np.argwhere(self._brush_mask > 0) - self._brush_center
self.modified_coords: List[np.ndarray] = []
self._active = False
with importlib.resources.path("tools.icons", "brush.png") as path:
self._tool_icon = QIcon(str(path))
self._update_icon()
@property
def tool_name(self) -> str:
return self._tool_name
@property
def cursor_image(self) -> QImage:
radius = self._user_params['Radius'].value
self._brush_mask = M.disk(radius, np.uint16)
self._brush_center = np.array([radius, radius])
self._brush_coords = np.argwhere(self._brush_mask > 0) - self._brush_center
self._update_icon()
return self._brush_icon
@property
def user_params(self) -> Dict[str, UserParam]:
return self._user_params
[docs] def set_user_param(self, param_name: str, value: Any) -> Any:
if param_name not in self._user_params.keys():
return None
param: UserParam = self._user_params[param_name]
if param.param_type == ParamType.INT:
if value % 2 == 0:
value += 1
self._user_params[param_name].value = value
elif param.param_type == ParamType.BOOL:
pass
self._user_params[param_name].value = value
if param_name == 'Radius':
self.cursor_changed.emit(self.tool_id, self.cursor_image)
return value
[docs] def get_auto_scroll_distance(self):
return Tool.get_auto_scroll_distance(self) + self._user_params['Radius'].value * self.state.current_view_transform.m11()
[docs] def left_press(self, painter: QPainter, pos: QPoint, ctx: EditContext) -> Tuple[Optional[CommandEntry], QRect]:
self._active = True
self._current_img = ctx.image
self._primary_label = ctx.label
self.label_changes = []
if self.edit_mask.size().toTuple() == ctx.label_img.label_image.shape[::-1]:
self.edit_mask.fill(QColor(0, 0, 0))
else:
self.edit_mask = QImage(ctx.label_img.label_image.shape[1],
ctx.label_img.label_image.shape[0], QImage.Format_Grayscale8)
self.edit_mask.fill(QColor(0, 0, 0))
return self.mouse_move(painter, pos, pos, ctx)
[docs] def mouse_move(self, painter: QPainter, new_pos: QPoint, old_pos: QPoint, ctx: EditContext) -> Tuple[Optional[CommandEntry], QRect]:
if not self.active:
return None, QRect()
if ctx.label != self._primary_label:
self.label_changes.extend(self.get_label_changes(ctx.label_img))
self.edit_mask.fill(QColor(0, 0, 0))
self._primary_label = ctx.label
#brush_color = QColor.fromRgb(*self.cmap[ctx.label])
brush_color = QColor.fromRgb(*self.state.colormap[ctx.label])
if ctx.label == 0:
#brush_color = QColor.fromRgb(20, 20, 200, 255)
brush_color = QColor(Qt.transparent)
rr, cc = draw.line(old_pos.y(), old_pos.x(),
new_pos.y(), new_pos.x())
self.paint_on(ctx.label_viz, rr, cc, QPen(brush_color), QBrush(brush_color), ctx.clip_region)
brush_color = QColor(255, 255, 255, 255)
self.paint_on(self.edit_mask, rr, cc, QPen(brush_color), QBrush(brush_color), ctx.clip_region)
rad = self.user_params['Radius'].value
top, bottom = np.min(rr) - rad, np.max(rr) + rad
left, right = np.min(cc) - rad, np.max(cc) + rad
return None, QRect(left, top, right - left + 1, bottom - top + 1)
[docs] def left_release(self, painter: QPainter, pos: QPoint, ctx: EditContext) -> Tuple[Optional[CommandEntry], QRect]:
self._active = False
#edit_nd = np.squeeze(qimage2ndarray.byte_view(self.edit_mask))
#lab_diff = np.where(edit_nd > 0, ctx.label, -1) #np.where(qimage2ndarray(self.edit_mask) > 0, ctx.label, -1)
#lab_changes = label_difference_to_label_changes(lab_diff, ctx.label_img)
#if len(lab_changes) == 0:
# return None, QRect()
return self.get_command(ctx.label_img), QRect() #CommandEntry(lab_changes, update_canvas=False), QRect()
[docs] def mouse_wheel(self, delta: int, painter: QPainter, pos: QPoint, context: EditContext) -> Tuple[Optional[CommandEntry], QRect]:
my_delta = 2 if delta > 0 else -2
curr_rad = self.user_params['Radius'].value
new_rad = max(1, curr_rad + my_delta)
# print(f'delta is {delta}')
self.user_params['Radius'].value = new_rad
self.cursor_changed.emit(self.tool_id, self.cursor_image)
return None, QRect()
[docs] def get_command(self, lab_img: LabelImg) -> Optional[CommandEntry]:
edit_nd = np.squeeze(qimage2ndarray.byte_view(self.edit_mask))
lab_diff = np.where(edit_nd > 0, self._primary_label, -1) #np.where(qimage2ndarray(self.edit_mask) > 0, ctx.label, -1)
yy, xx = np.nonzero(lab_diff >= 0)
top, left, bottom, right = np.min(yy), np.min(xx), np.max(yy), np.max(xx)
# lab_diff_ = lab_diff[top:bottom+1, left:right+1]
lab_changes = label_difference_to_label_changes(lab_diff, lab_img, (top, bottom+1, left, right+1))
all_label_changes = self.label_changes
self.label_changes = []
all_label_changes.extend(lab_changes)
if len(all_label_changes) == 0:
return None
return CommandEntry(all_label_changes, update_canvas=False, source=self.tool_name)
[docs] def get_label_changes(self, lab_img: LabelImg) -> List[LabelChange]:
edit_nd = np.squeeze(qimage2ndarray.byte_view(self.edit_mask))
lab_diff = np.where(edit_nd > 0, self._primary_label, -1) #np.where(qimage2ndarray(self.edit_mask) > 0, ctx.label, -1)
return label_difference_to_label_changes(lab_diff, lab_img)
#def right_press(self, painter: QPainter, pos: QPoint, ctx: EditContext) -> List[LabelChange]:
# return np.array([]), -1
#def right_release(self, painter: QPainter, pos: QPoint, ctx: EditContext) -> List[LabelChange]:
# return np.array([]), -1
#def middle_click(self, painter: QPainter, pos: QPoint, ctx: EditContext) -> List[LabelChange]:
# pass
@property
def active(self) -> bool:
return self._active
@property
def viz_active(self) -> bool:
return self._active
[docs] @Slot(int)
def update_primary_label(self, label: int):
if not self.active:
self._primary_label = label
self._update_icon()
def _update_icon(self):
if self.state.colormap is not None and len(self.state.colormap.keys()) > 0:
sz = self._brush_mask.shape
self._brush_mask = self.state.primary_label * self._brush_mask #self._primary_label * self._brush_mask
self._brush_icon = QImage(sz[1], sz[0], QImage.Format_ARGB32)
self._brush_icon.fill(QColor(0, 0, 0, 0))
#brush_color = QColor.fromRgb(*self.cmap[self.state.primary_label])
brush_color = QColor.fromRgb(*self.state.colormap[self.state.primary_label])
painter = QPainter(self._brush_icon)
painter.setPen(brush_color)
radius = self.user_params['Radius'].value
painter.setBrush(QBrush(brush_color))
painter.drawEllipse(QPoint(radius, radius), radius, radius)
# Add a crosshair "hole" for pipetting
if radius > 1:
original_composition_mode = painter.compositionMode()
painter.setCompositionMode(painter.CompositionMode_Clear)
painter.setPen(QColor(255, 255, 255, 0))
painter.drawLine(radius - 2, radius, radius + 2, radius)
painter.drawLine(radius, radius - 2, radius, radius + 2)
painter.setCompositionMode(original_composition_mode)
[docs] @Slot(dict)
def color_map_changed(self, cmap: Dict[int, Tuple[int, int, int]]):
if cmap is None:
return
self._update_icon()
#@property
#def tool_id(self) -> int:
# return self._tool_id
#def set_tool_id(self, tool_id: int):
# self._tool_id = tool_id
[docs] def paint_on(self, canvas: QPaintDevice, rr, cc, pen: QPen, brush: QBrush, clip_mask: Optional[QRegion]):
painter = QPainter(canvas)
if clip_mask is not None:
painter.setClipRegion(clip_mask, Qt.ReplaceClip)
painter.setCompositionMode(QPainter.CompositionMode_Source)
painter.setPen(pen)
painter.setBrush(brush)
for x, y in zip(cc, rr):
painter.drawEllipse(QPoint(x, y), self.user_params['Radius'].value,
self.user_params['Radius'].value)
painter.end()