class Window(QLabel): def __init__(self, parent=None): QLabel.__init__(self, parent) self.rubberBand = ResizableRubberBand(self) self.origin = QPoint() def mousePressEvent(self, event): if event.button() == Qt.LeftButton: self.origin = QPoint(event.pos()) self.rubberBand.setGeometry(QRect(self.origin, QSize())) self.rubberBand.show() def mouseMoveEvent(self, event): if not self.origin.isNull(): self.rubberBand.setGeometry( QRect(self.origin, event.pos()).normalized()) def keyPressEvent(self, event): if event.key() == Qt.Key_Enter: print(str(self.rubberBand.pos())) print(str(self.rubberBand.size())) self.rubberBand.hide()
class AreaEditWidget(TransparentWidget): # add a signal that emits the area selected areaSelected = Signal(QRect) areaRemoved = Signal(QPoint) def __init__(self, parent=None): super().__init__(opacity=0.25) # select area self.rubberband = QRubberBand(QRubberBand.Rectangle, self) # coords of mouse click self.origin = QPoint() def mousePressEvent(self, event): # left click starts the rubber band if event.button() == Qt.LeftButton: self.origin = QPoint(event.pos()) self.rubberband.setGeometry(QRect(self.origin, QSize())) self.rubberband.show() # right click on a selected area to remove it if event.button() == Qt.RightButton: self.areaRemoved.emit(event.pos()) def mouseMoveEvent(self, event): if not self.origin.isNull(): self.rubberband.setGeometry( QRect(self.origin, event.pos()).normalized()) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.rubberband.hide() area_selected = self.rubberband.geometry() self.areaSelected.emit(area_selected)
class MandelbrotWidget(QWidget): def __init__(self, parent=None): super(MandelbrotWidget, self).__init__(parent) self.thread = RenderThread() self.pixmap = QPixmap() self.pixmapOffset = QPoint() self.lastDragPos = QPoint() self.centerX = DefaultCenterX self.centerY = DefaultCenterY self.pixmapScale = DefaultScale self.curScale = DefaultScale self.thread.renderedImage.connect(self.updatePixmap) self.setWindowTitle("Mandelbrot") self.setCursor(Qt.CrossCursor) self.resize(550, 400) def paintEvent(self, event): painter = QPainter(self) painter.fillRect(self.rect(), Qt.black) if self.pixmap.isNull(): painter.setPen(Qt.white) painter.drawText(self.rect(), Qt.AlignCenter, "Rendering initial image, please wait...") return if self.curScale == self.pixmapScale: painter.drawPixmap(self.pixmapOffset, self.pixmap) else: scaleFactor = self.pixmapScale / self.curScale newWidth = int(self.pixmap.width() * scaleFactor) newHeight = int(self.pixmap.height() * scaleFactor) newX = self.pixmapOffset.x() + (self.pixmap.width() - newWidth) / 2 newY = self.pixmapOffset.y() + (self.pixmap.height() - newHeight) / 2 painter.save() painter.translate(newX, newY) painter.scale(scaleFactor, scaleFactor) exposed, _ = painter.matrix().inverted() exposed = exposed.mapRect(self.rect()).adjusted(-1, -1, 1, 1) painter.drawPixmap(exposed, self.pixmap, exposed) painter.restore() text = "Use mouse wheel or the '+' and '-' keys to zoom. Press and " \ "hold left mouse button to scroll." metrics = painter.fontMetrics() textWidth = metrics.width(text) painter.setPen(Qt.NoPen) painter.setBrush(QColor(0, 0, 0, 127)) painter.drawRect((self.width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5) painter.setPen(Qt.white) painter.drawText((self.width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text) def resizeEvent(self, event): self.thread.render(self.centerX, self.centerY, self.curScale, self.size()) def keyPressEvent(self, event): if event.key() == Qt.Key_Plus: self.zoom(ZoomInFactor) elif event.key() == Qt.Key_Minus: self.zoom(ZoomOutFactor) elif event.key() == Qt.Key_Left: self.scroll(-ScrollStep, 0) elif event.key() == Qt.Key_Right: self.scroll(+ScrollStep, 0) elif event.key() == Qt.Key_Down: self.scroll(0, -ScrollStep) elif event.key() == Qt.Key_Up: self.scroll(0, +ScrollStep) else: super(MandelbrotWidget, self).keyPressEvent(event) def wheelEvent(self, event): numDegrees = event.angleDelta().y() / 8 numSteps = numDegrees / 15.0 self.zoom(pow(ZoomInFactor, numSteps)) def mousePressEvent(self, event): if event.buttons() == Qt.LeftButton: self.lastDragPos = QPoint(event.pos()) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: self.pixmapOffset += event.pos() - self.lastDragPos self.lastDragPos = QPoint(event.pos()) self.update() def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.pixmapOffset += event.pos() - self.lastDragPos self.lastDragPos = QPoint() deltaX = (self.width() - self.pixmap.width()) / 2 - self.pixmapOffset.x() deltaY = (self.height() - self.pixmap.height()) / 2 - self.pixmapOffset.y() self.scroll(deltaX, deltaY) def updatePixmap(self, image, scaleFactor): if not self.lastDragPos.isNull(): return self.pixmap = QPixmap.fromImage(image) self.pixmapOffset = QPoint() self.lastDragPosition = QPoint() self.pixmapScale = scaleFactor self.update() def zoom(self, zoomFactor): self.curScale *= zoomFactor self.update() self.thread.render(self.centerX, self.centerY, self.curScale, self.size()) def scroll(self, deltaX, deltaY): self.centerX += deltaX * self.curScale self.centerY += deltaY * self.curScale self.update() self.thread.render(self.centerX, self.centerY, self.curScale, self.size())
class CameraPreview(QOpenGLWidget): """ widget to display camera feed and overlay droplet approximations from opencv """ def __init__(self, parent=None): super(CameraPreview, self).__init__(parent) self.roi_origin = QPoint(0, 0) self._pixmap: QPixmap = QPixmap(480, 360) self._double_buffer: QImage = None self._raw_image: np.ndarray = None self._image_size = (1, 1) self._image_size_invalid = True self._roi_rubber_band = ResizableRubberBand(self) self._needle_mask = DynamicNeedleMask(self) self._needle_mask.update_mask_signal.connect(self.update_mask) self._baseline = Baseline(self) self._droplet = Droplet() self._mask = None logging.debug("initialized camera preview") def prepare(self): """ preset the baseline to 250 which is roughly base of the test image droplet """ self._baseline.y_level = self.mapFromImage(y=250) def paintEvent(self, event: QPaintEvent): """ custom paint event to draw camera stream and droplet approximation if available uses double buffering to avoid flicker """ # completely override super.paintEvent() to use double buffering painter = QPainter(self) buf = self.doubleBufferPaint(self._double_buffer) # painting the buffer pixmap to screen painter.drawImage(0, 0, buf) painter.end() def doubleBufferPaint(self, buffer=None): self.blockSignals(True) #self.drawFrame(painter) if buffer is None: buffer = QImage(self.width(), self.height(), QImage.Format_RGB888) buffer.fill(Qt.black) # calculate offset and scale of droplet image pixmap scale_x, scale_y, offset_x, offset_y = self.get_from_image_transform() db_painter = QPainter(buffer) db_painter.setRenderHints(QPainter.Antialiasing | QPainter.NonCosmeticDefaultPen) db_painter.setBackground(QBrush(Qt.black)) db_painter.setPen(QPen(Qt.black, 0)) db_painter.drawPixmap(offset_x, offset_y, self._pixmap) pen = QPen(Qt.magenta, 1) pen_fine = QPen(Qt.blue, 1) pen.setCosmetic(True) db_painter.setPen(pen) # draw droplet outline and tangent only if evaluate_droplet was successful if self._droplet.is_valid: try: # transforming true image coordinates to scaled pixmap coordinates db_painter.translate(offset_x, offset_y) db_painter.scale(scale_x, scale_y) # drawing tangents and baseline db_painter.drawLine(*self._droplet.line_l) db_painter.drawLine(*self._droplet.line_r) db_painter.drawLine(*self._droplet.int_l, *self._droplet.int_r) # move origin to ellipse origin db_painter.translate(*self._droplet.center) # draw diagnostics # db_painter.setPen(pen_fine) # # lines parallel to coordinate axes # db_painter.drawLine(0,0,20*scale_x,0) # db_painter.drawLine(0,0,0,20*scale_y) # # angle arc # db_painter.drawArc(-5*scale_x, -5*scale_y, 10*scale_x, 10*scale_y, 0, -self._droplet.tilt_deg*16) # rotate coordinates to ellipse tilt db_painter.rotate(self._droplet.tilt_deg) # draw ellipse # db_painter.setPen(pen) db_painter.drawEllipse(-self._droplet.maj / 2, -self._droplet.min / 2, self._droplet.maj, self._droplet.min) # # major and minor axis for diagnostics # db_painter.drawLine(0, 0, self._droplet.maj/2, 0) # db_painter.drawLine(0, 0, 0, self._droplet.min/2) except Exception as ex: logging.error(ex) db_painter.end() self.blockSignals(False) return buffer def mousePressEvent(self, event): """ mouse pressed handler creates ROI rubberband rectangle """ if event.button() == Qt.LeftButton: # create new rubberband rectangle self.roi_origin = QPoint(event.pos()) self._roi_rubber_band.hide() self._roi_rubber_band.setGeometry(QRect(self.roi_origin, QSize())) self._roi_rubber_band.show() def mouseMoveEvent(self, event): """ mouse moved handler resizes the ROI rubberband rectangle if left mouse button is pressed """ if event.buttons() == Qt.NoButton: pass elif event.buttons() == Qt.LeftButton: # resize rubberband while mouse is moving if not self.roi_origin.isNull(): self._roi_rubber_band.setGeometry( QRect(self.roi_origin, event.pos()).normalized()) elif event.buttons() == Qt.RightButton: pass def keyPressEvent(self, event): """ keyboard pressed handler - Esc: aborts ROI rubberband and hides it - Enter: applys ROI from rubberband to camera """ if event.key() == Qt.Key_Enter: # apply the ROI set by the rubberband self.parent().apply_roi() elif event.key() == Qt.Key_Escape: # hide rubberband self._abort_roi() self.update() def hide_mask(self): """hides and disables the needle mask """ self._needle_mask.hide() self._mask = None def show_mask(self): """shows the needle mask """ self._needle_mask.show() self.update_mask() def update_mask(self): """update mask from widget """ mask_rect = self._needle_mask.get_mask_geometry() self._mask = self.mapToImage(*mask_rect[:]) @Slot(np.ndarray, bool) def update_image(self, cv_img: np.ndarray, eval: bool = True): """ Updates the image_label with a new opencv image :param cv_img: camera image array :param eval: if True: do image processing on given image .. seealso:: :py:meth:`camera_control.CameraControl.update_image` """ self._raw_image = cv_img try: # evaluate droplet only if camera is running or if a oneshot eval is requested if eval: try: self._droplet.is_valid = False evaluate_droplet(cv_img, self.get_baseline_y(), self._mask) except (ContourError, cv2.error, TypeError): pass except Exception as ex: logging.exception("Exception thrown in %s", "fcn:evaluate_droplet", exc_info=ex) else: self._droplet.is_valid = False qt_img = self._convert_cv_qt(cv_img) self._pixmap = qt_img if self._image_size_invalid: self._image_size = np.shape(cv_img) self.set_new_baseline_constraints() self._image_size_invalid = False self.update() # del cv_img except Exception as ex: logging.exception("Exception thrown in %s", "class:camera_preview fcn:update_image", exc_info=ex) def grab_image(self, raw=False): if raw: return self._convert_cv_qt(self._raw_image, False) else: return self.doubleBufferPaint(self._double_buffer) def _convert_cv_qt(self, cv_img: np.ndarray, scaled=True): """ Convert from an opencv image to QPixmap :param cv_img: opencv image as numpy array :param scaled: if true or omitted returns an image scaled to widget dimensions :returns: opencv image as full size QImage or QPixmap scaled to widget dimensions """ #rgb_image = cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB) #h, w, ch = rgb_image.shape h, w, ch = cv_img.shape bytes_per_line = ch * w qimg = QtGui.QImage(cv_img, w, h, bytes_per_line, QtGui.QImage.Format_Grayscale8) if scaled: qimg_scaled = qimg.scaled(self.size(), Qt.KeepAspectRatio) return QPixmap.fromImage(qimg_scaled) else: return qimg def map_droplet_drawing_vals(self, droplet: Droplet): """ convert the droplet values from image coords into pixmap coords and values better for drawing :param droplet: droplet object containing the data :returns: **tuple** (tangent_l, tangent_r, int_l, int_r, center, maj, min) - **tangent_l**: start and end coordinates left tangent as (x1,y1,x2,y2) - **tangent_r**: start and end coordinates right tangent as (x1,y1,x2,y2) - **int_l**: left intersection of ellipse and baseline as (x,y) - **int_l**: right intersection of ellipse and baseline as (x,y) - **center**: center of the ellipse as (x,y) - **maj**: major axis length of the ellipse - **min**: minor axis length of the ellipse """ tangent_l = tuple( self.mapFromImage(droplet.line_l[0:1]) + self.mapFromImage(droplet.line_l[2:3])) #tuple(map(lambda x: self.mapFromImage(*x), droplet.line_l)) tangent_r = tuple( self.mapFromImage(droplet.line_r[0:1]) + self.mapFromImage(droplet.line_r[2:3])) #tuple(map(lambda x: self.mapFromImage(*x), droplet.line_r)) center = self.mapFromImage(*droplet.center) maj, min = self.mapFromImage(droplet.maj, droplet.min) int_l = self.mapFromImage(*droplet.int_l) int_r = self.mapFromImage(*droplet.int_r) return tangent_l, tangent_r, int_l, int_r, center, maj, min def mapToImage(self, x=None, y=None, w=None, h=None): """ Convert QLabel coordinates to image pixel coordinates :param x: x coordinate to be transformed :param y: y coordinate to be transformed :returns: x or y or Tuple (x,y) of the transformed coordinates, depending on what parameters where given """ scale_x, scale_y, offset_x, offset_y = self.get_from_image_transform() res: List[int] = [] if x is not None: # subtract half the width delta, then scale tr_x = int(round((x - offset_x) / scale_x)) res.append(tr_x) if y is not None: tr_y = int(round((y - offset_y) / scale_y)) res.append(tr_y) if w is not None: tr_w = int(round(w / scale_x)) res.append(tr_w) if h is not None: tr_h = int(round(h / scale_y)) res.append(tr_h) return tuple(res) if len(res) > 1 else res[0] def mapFromImage(self, x=None, y=None, w=None, h=None): """ Convert Image pixel coordinates to QLabel coordinates :param x: x coordinate to be transformed :param y: y coordinate to be transformed :returns: x or y or Tuple (x,y) of the transformed coordinates, depending on what parameters where given """ scale_x, scale_y, offset_x, offset_y = self.get_from_image_transform() res: List[int] = [] if x is not None: tr_x = int(round((x * scale_x) + offset_x)) res.append(tr_x) if y is not None: tr_y = int(round((y * scale_y) + offset_y)) res.append(tr_y) if w is not None: tr_w = int(round(w * scale_x)) res.append(tr_w) if h is not None: tr_h = int(round(h * scale_y)) res.append(tr_h) return tuple(res) if len(res) > 1 else res[0] def get_from_image_transform(self): """ Gets the scale and offset for a Image to QLabel coordinate transform :returns: 4-Tuple: Scale factors for x and y as tuple, Offset as tuple (x,y) """ pw, ph = self._pixmap.size().toTuple() # scaled image size ih, iw = self._image_size[0], self._image_size[ 1] # original size of image cw, ch = self.size().toTuple() # display container size scale_x = float(pw / iw) offset_x = abs(pw - cw) / 2 scale_y = float(ph / ih) offset_y = abs(ph - ch) / 2 return scale_x, scale_y, offset_x, offset_y def show_baseline(self): """ Show the baseline selector """ self._baseline.show() def hide_baseline(self): """ Hide the baseline selector """ self._baseline.hide() def hide_rubberband(self): """ Hide the rubberband """ self._roi_rubber_band.hide() def get_baseline_y(self) -> int: """ return the y value the baseline is on in image coordinates :returns: y value of baseline in image coordinates """ y_base = self._baseline.y_level y = self.mapToImage(y=y_base) return y def set_new_baseline_constraints(self): """ set the min and max y value for the baseline """ pix_size = self._pixmap.size() offset_y = int(round(abs(pix_size.height() - self.height()) / 2)) self._baseline.max_level = pix_size.height() + offset_y self._baseline.min_level = offset_y def get_roi(self): """ return the ROI selected by the rubberband """ x, y = self._roi_rubber_band.mapToParent(QPoint(0, 0)).toTuple() w, h = self._roi_rubber_band.size().toTuple() self.hide_rubberband() x, y, w, h = self.mapToImage(x, y, w, h) #w,h = self.mapToImage(x=w, y=h) return x, y, w, h def _abort_roi(self): """ abort ROI set by hiding the rubberband selector """ self._roi_rubber_band.hide() logging.info("aborted ROI select") def invalidate_imagesize(self): """ invalidate image size, causes image size to be reevaluated on next camera image """ self._image_size_invalid = True
class MandelbrotWidget(QWidget): def __init__(self, parent=None): super(MandelbrotWidget, self).__init__(parent) self.thread = RenderThread() self.pixmap = QPixmap() self.pixmapOffset = QPoint() self.lastDragPos = QPoint() self.centerX = DefaultCenterX self.centerY = DefaultCenterY self.pixmapScale = DefaultScale self.curScale = DefaultScale self.thread.renderedImage.connect(self.updatePixmap) self.setWindowTitle("Mandelbrot") self.setCursor(Qt.CrossCursor) self.resize(550, 400) def paintEvent(self, event): painter = QPainter(self) painter.fillRect(self.rect(), Qt.black) if self.pixmap.isNull(): painter.setPen(Qt.white) painter.drawText(self.rect(), Qt.AlignCenter, "Rendering initial image, please wait...") return if self.curScale == self.pixmapScale: painter.drawPixmap(self.pixmapOffset, self.pixmap) else: scaleFactor = self.pixmapScale / self.curScale newWidth = int(self.pixmap.width() * scaleFactor) newHeight = int(self.pixmap.height() * scaleFactor) newX = self.pixmapOffset.x() + (self.pixmap.width() - newWidth) / 2 newY = self.pixmapOffset.y() + (self.pixmap.height() - newHeight) / 2 painter.save() painter.translate(newX, newY) painter.scale(scaleFactor, scaleFactor) exposed, _ = painter.matrix().inverted() exposed = exposed.mapRect(self.rect()).adjusted(-1, -1, 1, 1) painter.drawPixmap(exposed, self.pixmap, exposed) painter.restore() text = "Use mouse wheel or the '+' and '-' keys to zoom. Press and " \ "hold left mouse button to scroll." metrics = painter.fontMetrics() textWidth = metrics.width(text) painter.setPen(Qt.NoPen) painter.setBrush(QColor(0, 0, 0, 127)) painter.drawRect((self.width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5) painter.setPen(Qt.white) painter.drawText((self.width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text) def resizeEvent(self, event): self.thread.render(self.centerX, self.centerY, self.curScale, self.size()) def keyPressEvent(self, event): if event.key() == Qt.Key_Plus: self.zoom(ZoomInFactor) elif event.key() == Qt.Key_Minus: self.zoom(ZoomOutFactor) elif event.key() == Qt.Key_Left: self.scroll(-ScrollStep, 0) elif event.key() == Qt.Key_Right: self.scroll(+ScrollStep, 0) elif event.key() == Qt.Key_Down: self.scroll(0, -ScrollStep) elif event.key() == Qt.Key_Up: self.scroll(0, +ScrollStep) else: super(MandelbrotWidget, self).keyPressEvent(event) def wheelEvent(self, event): numDegrees = event.angleDelta().y() / 8 numSteps = numDegrees / 15.0 self.zoom(pow(ZoomInFactor, numSteps)) def mousePressEvent(self, event): if event.buttons() == Qt.LeftButton: self.lastDragPos = QPoint(event.pos()) def mouseMoveEvent(self, event): if event.buttons() & Qt.LeftButton: self.pixmapOffset += event.pos() - self.lastDragPos self.lastDragPos = QPoint(event.pos()) self.update() def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.pixmapOffset += event.pos() - self.lastDragPos self.lastDragPos = QPoint() deltaX = (self.width() - self.pixmap.width()) / 2 - self.pixmapOffset.x() deltaY = (self.height() - self.pixmap.height()) / 2 - self.pixmapOffset.y() self.scroll(deltaX, deltaY) def updatePixmap(self, image, scaleFactor): if not self.lastDragPos.isNull(): return self.pixmap = QPixmap.fromImage(image) self.pixmapOffset = QPoint() self.lastDragPosition = QPoint() self.pixmapScale = scaleFactor self.update() def zoom(self, zoomFactor): self.curScale *= zoomFactor self.update() self.thread.render(self.centerX, self.centerY, self.curScale, self.size()) def scroll(self, deltaX, deltaY): self.centerX += deltaX * self.curScale self.centerY += deltaY * self.curScale self.update() self.thread.render(self.centerX, self.centerY, self.curScale, self.size())