class SpectrogramViewer(QtWidgets.QWidget, Ui_SpectrogramViewer): seek = QtCore.Signal(float) spectrogram_drawn = QtCore.Signal() def __init__(self, parent=None, audio=None, settings=None): super().__init__(parent) self.setupUi(self) self.bg_image = None self.spectrogram_scene = QGraphicsScene(self) self._audio = None self._spectrogram = None self._image_generator = None self.sound_marker = None self.marker_position = 0 self.yscale = 1 self.settings = settings self.audio = audio self.setup_graphics_view() self.define_shortcuts() self.install_filters() def setup_graphics_view(self): self.spectrogram_view.setScene(self.spectrogram_scene) def define_shortcuts(self): QtGui.QShortcut( QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Plus), self, self.zoom_in) QtGui.QShortcut( QtGui.QKeySequence(QtCore.Qt.CTRL + QtCore.Qt.Key_Minus), self, self.zoom_out, ) def install_filters(self): self.mouse_filter = SpectrogramMouseFilter(self) self.spectrogram_scene.installEventFilter(self.mouse_filter) @property def audio(self): return self._audio @audio.setter def audio(self, audio): if audio is not None: self._audio = audio self.spectrogram = Spectrogram(audio, self.spectrogram_options) self.marker_position = 0 @property def spectrogram(self): return self._spectrogram @spectrogram.setter def spectrogram(self, spectrogram): self._spectrogram = spectrogram self.display_spectrogram() @property def image_options(self): return self.settings.image_options if self.settings else {} @property def spectrogram_options(self): return self.settings.spectrogram_options if self.settings else {} @property def image_generator(self): if self._image_generator is None: self._image_generator = ImageGenerator(self.image_options) return self._image_generator def display_spectrogram(self): # TODO: save image somewhere im = self.image_generator.spec2img(self.spectrogram) # TODO: change events when checkbox is checked # if self.checkbox_draw_events.isChecked(): # im = self.draw_events(im, max_duration) img = ImageQt.ImageQt(im) pixmap = QtGui.QPixmap.fromImage(img) # Change Qt array to a Qt graphic self.bg_image = QGraphicsPixmapItem(pixmap) self.spectrogram_scene.clear() self.spectrogram_scene.addItem(self.bg_image) # Ensure spectrogram graphic is displayed as background self.bg_image.setZValue(-100) self.bg_image.setPos(0, 0) self.sound_marker = None if self.marker_position: self.update_sound_marker(None) self.spectrogram_drawn.emit() def display_text(self, text): text_item = QGraphicsTextItem() text_item.setPos(150, 100) text_item.setPlainText(text) self.spectrogram_scene.clear() self.spectrogram_scene.addItem(text_item) def update_sound_marker(self, position_sec): # 100 # multiply by step-size in SpecGen() if position_sec is not None: self.marker_position = self.image_generator.sec2pixels( position_sec) line = QtCore.QLineF( self.marker_position, 0, self.marker_position, self.image_generator["height"], ) if not self.sound_marker: penCol = QtGui.QColor() penCol.setRgb(255, 0, 0) self.sound_marker = self.spectrogram_scene.addLine( line, QtGui.QPen(penCol)) else: self.sound_marker.setLine(line) self.spectrogram_scene.update() if self.spectrogram_options["follow_sound"]: self.center_view() def center_view(self): self.spectrogram_view.centerOn(self.marker_position, self.get_center().y()) def zoom(self, scale, scene_pos=None): self.yscale *= scale self.spectrogram_view.scale(scale, scale) if scene_pos: self.spectrogram_view.centerOn(scene_pos) def zoom_in(self): self.zoom(1.5) def zoom_out(self): self.zoom(0.75) def seek_sound(self, pos): self.seek.emit(self.image_generator.pixels2sec(pos)) def update_spectrogram(self, option, redraw): if redraw: self.spectrogram = Spectrogram(self.audio, self.spectrogram_options) self.freq2pixels(6000) def update_image(self, option, redraw): if redraw: self.display_spectrogram() def get_center(self): return self.spectrogram_view.mapToScene( self.spectrogram_view.viewport().rect().center()) def clear_rects(self): items = self.spectrogram_scene.items() for item in items: if isinstance(item, AnnotatedRectItem): self.spectrogram_scene.removeItem(item) def freq2pixels(self, freq): res = 0 if self.spectrogram["scale"] == "Linear": height = self.image_generator["height"] max_freq = self.audio.sr / 2 freq_step = height / max_freq res = height - (freq * freq_step) else: print("Only linear scale is supported so far") return res def draw_annotation(self, opts): x1 = self.image_generator.sec2pixels(opts.get("start", 0)) x2 = self.image_generator.sec2pixels(opts.get("end", 0)) y1 = self.freq2pixels(opts.get("max_freq", 0)) y2 = self.freq2pixels(opts.get("min_freq", 0)) text = opts.get("text", "") buffer = opts.get("vertical_buffer", 1) top_offset = opts.get("top_offset", 0) bottom_offset = opts.get("bottom_offset", 0) if y2 - y1 <= 0: y1 = 0 y2 = self.spectrogram_scene.height() - 2 if buffer: v_offset = buffer * y2 / 100 y1 += v_offset + top_offset y2 -= v_offset - bottom_offset if text: font = QtGui.QFont(opts.get("text_font", ""), opts.get("text_fontsize", 12)) font_height = QtGui.QFontMetrics(font).height() y1 += font_height coords = (x1, y1, x2 - x1, y2 - y1) opts["coords"] = coords rect = AnnotatedRectItem(opts) self.spectrogram_scene.addItem(rect) def draw_rect(self, start, end, y=0, height=-1, color="#ffffff", fill="", buffer=1): x = self.image_generator.sec2pixels(start) width = self.image_generator.sec2pixels(end - start) if height == -1: height = self.spectrogram_scene.height() - 2 rect = QtWidgets.QGraphicsRectItem() rect.setPen(QtGui.QPen(color)) if fill is not None: if not fill: fill = color rect.setBrush(QtGui.QBrush(fill, QtCore.Qt.SolidPattern)) if buffer: y += buffer * height / 100 height -= buffer * height / 100 rect.setRect(x, y, width, height) self.spectrogram_scene.addItem(rect)
def delete_selected(scene: QGraphicsScene, gim: GraphicItemModel): gim.delete_all_from_references(scene.selectedItems()) for item in scene.selectedItems(): scene.removeItem(item)
class QRScanner(QWidget): QR_SIZE = 0.75 # Size of rectangle for QR capture readyForCapture = Signal(bool) decodedQR = Signal(str) def __init__(self, parent=None): QWidget.__init__(self, parent) self.processing = False self.rectangle = None self.setMinimumHeight(405) self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.scene = QGraphicsScene(self) self.scene.setBackgroundBrush(QBrush(Qt.black)) self.view = QGraphicsView(self.scene) self.viewfinder = QGraphicsVideoItem() self.scene.addItem(self.viewfinder) self.layout.addWidget(self.view) self.setLayout(self.layout) self.camera = None self.captureSession = None self.imageCapture = None self.captureTimer = None def startScan(self): if len(QMediaDevices.videoInputs()) == 0: logging.warning(self.tr("There are no cameras available")) return self.processing = True # disable any capture while camera is starting self.camera = QCamera(QMediaDevices.defaultVideoInput()) self.captureSession = QMediaCaptureSession() self.imageCapture = QImageCapture(self.camera) self.captureSession.setCamera(self.camera) self.captureSession.setVideoOutput(self.viewfinder) self.captureSession.setImageCapture(self.imageCapture) self.camera.errorOccurred.connect(self.onCameraError) self.readyForCapture.connect(self.onReadyForCapture) self.imageCapture.errorOccurred.connect(self.onCaptureError) self.imageCapture.readyForCaptureChanged.connect( self.onReadyForCapture) self.imageCapture.imageCaptured.connect(self.onImageCaptured) self.viewfinder.nativeSizeChanged.connect(self.onVideoSizeChanged) self.camera.start() self.processing = False self.readyForCapture.emit(self.imageCapture.isReadyForCapture()) def stopScan(self): if self.camera is None: return self.processing = True # disable capture self.camera.stop() self.camera = None self.captureSession = None self.imageCapture = None self.captureTimer = None def onVideoSizeChanged(self, _size): self.resizeEvent(None) # Take QImage or QRect (object with 'width' and 'height' properties and calculate position and size # of the square with side of self.QR_SIZE from minimum of height or width def calculate_center_square(self, img_rect) -> QRectF: a = self.QR_SIZE * min(img_rect.height(), img_rect.width()) # Size of square side x = (img_rect.width() - a) / 2 # Position of the square inside rectangle y = (img_rect.height() - a) / 2 if type(img_rect ) != QImage: # if we have a bounding rectangle, not an image x += img_rect.left( ) # then we need to shift our square inside this rectangle y += img_rect.top() return QRectF(x, y, a, a) def resizeEvent(self, event): bounds = self.scene.itemsBoundingRect() if bounds.width() <= 0 or bounds.height() <= 0: return # do nothing if size is zero self.view.fitInView(bounds, Qt.KeepAspectRatio) if self.rectangle is not None: self.scene.removeItem(self.rectangle) pen = QPen(Qt.green) pen.setWidth(0) pen.setStyle(Qt.DashLine) self.rectangle = self.scene.addRect( self.calculate_center_square(bounds), pen) self.view.centerOn(0, 0) self.view.raise_() def onCaptureError(self, _id, error, error_str): self.processing = False self.onCameraError(error, error_str) def onCameraError(self, error, error_str): logging.error( self.tr("Camera error: " + str(error) + " / " + error_str)) def onReadyForCapture(self, ready: bool): if ready and not self.processing: self.imageCapture.capture() self.processing = True def onImageCaptured(self, _id: int, img: QImage): self.decodeQR(img) self.processing = False if self.imageCapture is not None: self.readyForCapture.emit(self.imageCapture.isReadyForCapture()) def decodeQR(self, qr_image: QImage): cropped = qr_image.copy( self.calculate_center_square(qr_image).toRect()) # TODO: the same code is present in slips.py -> move to one place buffer = QBuffer() buffer.open(QBuffer.ReadWrite) cropped.save(buffer, "BMP") try: pillow_image = Image.open(io.BytesIO(buffer.data())) except UnidentifiedImageError: print("Image format isn't supported") return barcodes = pyzbar.decode(pillow_image, symbols=[pyzbar.ZBarSymbol.QRCODE]) if barcodes: self.decodedQR.emit(barcodes[0].data.decode('utf-8'))