#!/usr/bin/env python # -*- coding: utf-8 -*- # detection.py # # Author: Yann KOETH # Created: Mon Jul 14 13:51:02 2014 (+0200) # Last-Updated: Thu Jul 24 13:50:06 2014 (+0200) # By: Yann KOETH # Update #: 2186 # import sys import os import cv2 import time import numpy as np from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtWidgets import (QApplication, QWidget, QFileDialog, QPushButton, QHBoxLayout, QVBoxLayout, QScrollArea, QColorDialog, QLabel, QLineEdit, QListWidget, QComboBox, QSplitter, QGroupBox, QTextEdit, QAbstractItemView, QFrame, QSizePolicy, QSlider) from PyQt5.QtGui import QImage, QPixmap, QColor, QIcon, QPalette from window_ui import WindowUI import detector from detector import Detector, ClassifierParameters import common from tree import Tree class MediaThread(QtCore.QThread): def __init__(self, mw): super(MediaThread, self).__init__(mw) self.mw = mw self.stopped = False self.capture = None self.nextFrame = False self.mutex = QtCore.QMutex() self.mode = None def stop(self): """Stop media thread. """ with QtCore.QMutexLocker(self.mutex): self.stopped = True def clean(self): """Clean media thread. """ self.wait() del self.capture self.capture = None def setNextFrameMode(self, enable): self.nextFrame = enable def main(self, mode, path): """Main loop. """ if not self.capture or not self.capture.isOpened() or self.mode != mode: self.capture = cv2.VideoCapture(path) self.mode = mode if not self.capture.isOpened(): print "Couldn't read media " + path while self.capture.isOpened(): if self.stopped: break ret, frame = self.capture.read() if frame is None: #self.capture = cv2.VideoCapture(path) #ret, frame = self.capture.read() if frame is None: break if mode == self.mw.SOURCE_CAMERA: cv2.flip(frame, 1, frame) self.mw.displayImage(frame) QApplication.processEvents() if self.nextFrame: self.setNextFrameMode(False) break def run(self): """Method Override. """ with QtCore.QMutexLocker(self.mutex): self.stopped = False mode = self.mw.getSourceMode() path = self.mw.sourcePath.text() if mode == self.mw.SOURCE_FILE else 0 if mode == self.mw.SOURCE_FILE and not path: print 'File path is empty' else: self.main(mode, path) self.mw.togglePlayButton(play=False) class Window(QWidget, WindowUI): SOURCE_FILE = 'File' SOURCE_CAMERA = 'Camera' DISPLAY_INPUT = 'Input' DISPLAY_PREPROCESSED = 'Pre-processed' SHAPE_RECT = 'Rectangle' SHAPE_ELLIPSE = 'Ellipse' FILL_NONE = 'None' FILL_OUTLINE = 'Outline' FILL_COLOR = 'Color' FILL_BLUR = 'Blur' FILL_IMAGE = 'Image' FILL_MASK = 'Mask' BG_INPUT = 'Input' BG_COLOR = 'Color' BG_TRANSPARENT = 'Transparent' BG_IMAGE = 'Image' __sourceModes = [SOURCE_FILE, SOURCE_CAMERA] __displayModes = [DISPLAY_INPUT, DISPLAY_PREPROCESSED] __shapeModes = [SHAPE_RECT, SHAPE_ELLIPSE] __fillModes = [FILL_NONE, FILL_OUTLINE, FILL_BLUR, FILL_IMAGE, FILL_MASK, FILL_COLOR] __bgModes = [BG_INPUT, BG_COLOR, BG_TRANSPARENT, BG_IMAGE] IMAGE_FILTERS = '*.jpg *.png *.jpeg *.bmp' VIDEO_FILTERS = '*.avi *.mp4 *.mov' MASK_PATH = 'other/mask.png' debugSignal = QtCore.pyqtSignal(object, int) def __init__(self, parent=None): super(Window, self).__init__(parent) self.detector = Detector() self.mediaThread = MediaThread(self) sys.stdout = common.EmittingStream(textWritten=self.normalOutputWritten) self.debugSignal.connect(self.debugTable) self.currentFrame = None self.bgColor = QColor(255, 255, 255) self.bgPath = '' self.classifiersParameters = {} self.setupUI() self.populateUI() self.connectUI() self.initUI() def __del__(self): sys.stdout = sys.__stdout__ def keyPressEvent(self, e): if e.key() == QtCore.Qt.Key_Escape: self.close() def populateUI(self): self.availableObjectsList.addItems(Detector.getDefaultAvailableObjects()) for sourceMode in self.__sourceModes: self.sourceCBox.addItem(sourceMode) for displayMode in self.__displayModes: self.displayCBox.addItem(displayMode) for shapeMode in self.__shapeModes: self.shapeCBox.addItem(shapeMode) for fillMode in self.__fillModes: self.fillCBox.addItem(fillMode) for bgMode in self.__bgModes: self.bgCBox.addItem(bgMode) model = QtGui.QStandardItemModel(self) func = lambda node, parent: self.populateTree(node, parent) Detector.getDefaultObjectsTree().map(model, func) self.objectsTree.setModel(model) def connectUI(self): self.hsplitter.splitterMoved.connect(self.splitterMoved) self.vsplitter.splitterMoved.connect(self.splitterMoved) self.showDetails.clicked[bool].connect(self.toggleDebugInfo) self.addButton.clicked.connect(self.addObject) self.removeButton.clicked.connect(self.removeObject) self.sourceCBox.currentIndexChanged.connect(self.togglePath) self.sourcePathButton.clicked.connect(self.loadMedia) self.playButton.clicked.connect(self.play) self.refreshButton.clicked.connect(self.refresh) self.nextFrameButton.clicked.connect(self.nextFrame) self.objectsTree.customSelectionChanged.connect(self.showClassifierParameters) self.colorPicker.clicked.connect(self.colorDialog) self.classifierName.textChanged.connect(self.updateClassifierParameters) self.scaleFactor.valueChanged.connect(self.updateScaleFactor) self.minNeighbors.valueChanged.connect(self.updateMinNeighbors) self.minWidth.valueChanged.connect(self.updateMinWidth) self.minHeight.valueChanged.connect(self.updateMinHeight) self.shapeCBox.currentIndexChanged.connect(self.updateShape) self.fillCBox.currentIndexChanged.connect(self.updateFill) self.autoNeighbors.clicked.connect(self.calcNeighbors) self.stabilize.stateChanged.connect(self.updateStabilize) self.tracking.stateChanged.connect(self.updateTracking) self.showName.stateChanged.connect(self.updateShowName) self.fillPath.clicked.connect(self.loadFillPath) self.bgColorPicker.clicked.connect(self.bgColorDialog) self.bgPathButton.clicked.connect(self.bgPathDialog) self.bgCBox.currentIndexChanged.connect(self.toggleBgParams) def initUI(self): self.showClassifierParameters(None, None) self.equalizeHist.setChecked(True) self.toggleFillPath() self.toggleBgParams(self.bgCBox.currentIndex()) common.setPickerColor(self.bgColor, self.bgColorPicker) self.togglePlayButton(False) ######################################################## # Utils def getIcon(self, color): """Returns a colored icon. """ pixmap = QPixmap(14, 14) pixmap.fill(color) return QIcon(pixmap) def populateTree(self, node, parent): """Create a QTreeView node under 'parent'. """ item = QtGui.QStandardItem(node) h, s, v = Detector.getDefaultHSVColor(node) color = QColor() color.setHsvF(h, s, v) item.setIcon(self.getIcon(color)) # Unique hash for QStandardItem key = hash(item) while key in self.classifiersParameters: key += 1 item.setData(key) cp = ClassifierParameters(item.data(), node, node, color, self.SHAPE_RECT, self.FILL_OUTLINE) self.classifiersParameters[key] = cp parent.appendRow(item) return item def getMask(self, pixmap, x, y, w, h, shape, overlay, bg=QtCore.Qt.transparent, fg=QtCore.Qt.black, progressive=False): """Create a shape mask with the same size of pixmap. """ mask = QPixmap(pixmap.width(), pixmap.height()) mask.fill(bg) path = QtGui.QPainterPath() if progressive: progressive = QPixmap(pixmap.width(), pixmap.height()) progressive.fill(QtCore.Qt.transparent) progressiveMask = QPixmap(self.MASK_PATH) progressiveMask = progressiveMask.scaled(w, h, QtCore.Qt.IgnoreAspectRatio) progressivePainter = QtGui.QPainter(progressive) progressivePainter.drawPixmap(x, y, progressiveMask) del progressivePainter fg = QtGui.QBrush(progressive) painter = QtGui.QPainter(mask) if shape == self.SHAPE_ELLIPSE: path.addEllipse(x, y, w, h) painter.fillPath(path, fg) elif shape == self.SHAPE_RECT: painter.fillRect(x, y, w, h, fg) painter.setCompositionMode(QtGui.QPainter.CompositionMode_SourceAtop) if overlay: painter.drawPixmap(0, 0, overlay) return mask def drawTracking(self, painter, tracking, scale): """Draw lines between each position in tracking list. """ if not tracking: return hashTable = {} # Group tracks by item hash for track in tracking: for rect, hash in track: x, y, w, h = common.scaleRect(rect, scale) x, y = x + w / 2, y + h / 2 if hash in hashTable: hashTable[hash].append((x, y)) else: hashTable[hash] = [(x, y)] # Draw for hash, points in hashTable.iteritems(): prev = None for point in points: if prev: x1, y1 = point x2, y2 = prev painter.drawLine(x1, y1, x2, y2) prev = point def drawBackground(self, pixmap): """Draw background in pixmap. """ w, h = pixmap.width(), pixmap.height() mode = self.__bgModes[self.bgCBox.currentIndex()] source = QPixmap(pixmap) painter = QtGui.QPainter(pixmap) if mode == self.BG_COLOR: painter.fillRect(0, 0, w, h, self.bgColor) if mode == self.BG_TRANSPARENT or mode == self.BG_IMAGE or mode == self.BG_INPUT: painter.drawPixmap(0, 0, common.checkerboard(pixmap.size())) if mode == self.BG_IMAGE and self.bgPath: bgPixmap = QPixmap(self.bgPath) if not bgPixmap.isNull(): bgPixmap = bgPixmap.scaled(w, h, QtCore.Qt.IgnoreAspectRatio) painter.drawPixmap(0, 0, bgPixmap) if mode == self.BG_INPUT: painter.drawPixmap(0, 0, source) def fillMask(self, pixmap, painter, roi, param, source): """Draw mask in roi. """ x, y, w, h = roi masked = self.getMask(pixmap, x, y, w, h, param.shape, source, progressive=True) painter.drawPixmap(0, 0, masked) def fillBlur(self, pixmap, painter, roi, param, source): """Draw blur in roi. """ x, y, w, h = roi blurred = self.getMask(pixmap, x, y, w, h, param.shape, common.blurPixmap(pixmap, 20)) painter.drawPixmap(0, 0, blurred) def fillImage(self, pixmap, painter, roi, param, source): """Draw image in roi. """ x, y, w, h = roi fillPixmap = QPixmap(param.fillPath) if not fillPixmap.isNull(): fillPixmap = fillPixmap.scaled(w, h, QtCore.Qt.IgnoreAspectRatio) mask = self.getMask(pixmap, x, y, w, h, param.shape, None, QtCore.Qt.white, QtCore.Qt.black) painter.setClipRegion(QtGui.QRegion(QtGui.QBitmap(mask))) painter.drawPixmap(x, y, fillPixmap) painter.setClipping(False) def fillColor(self, pixmap, painter, roi, param, source): """Draw color in roi. """ x, y, w, h = roi if param.shape == self.SHAPE_ELLIPSE: path = QtGui.QPainterPath() path.addEllipse(x, y, w, h) painter.fillPath(path, param.color) elif param.shape == self.SHAPE_RECT: painter.fillRect(x, y, w, h, param.color) def fillOutline(self, pixmap, painter, roi, param, source): """Draw outline in roi. """ x, y, w, h = roi drawFunc = painter.drawRect if param.shape == self.SHAPE_ELLIPSE: drawFunc = painter.drawEllipse drawFunc(x, y, w, h) def drawRects(self, source, rectsTree, scale): """Draw rectangles in 'rectsTree' on 'source'. """ pixmap = QPixmap(source) self.drawBackground(pixmap) def drawRect(node, parentHash): painter = QtGui.QPainter(pixmap) roi, param, tracking = node.data x, y, w, h = common.scaleRect(roi, scale) painter.setPen(param.color) funcTable = { self.FILL_MASK: self.fillMask, self.FILL_BLUR: self.fillBlur, self.FILL_IMAGE: self.fillImage, self.FILL_COLOR: self.fillColor, self.FILL_OUTLINE: self.fillOutline } for fill, func in funcTable.iteritems(): if param.fill == fill: func(pixmap, painter, (x, y, w, h), param, source) if param.tracking: self.drawTracking(painter, tracking, scale) if param.showName: painter.drawText(x, y, param.name) return param.hash h, w = pixmap.height(), pixmap.width() rectsTree.map(None, drawRect) painter = QtGui.QPainter(source) painter.drawPixmap(0, 0, pixmap) def debugTable(self, args, append=False): """Display debug info into table. """ rows = len(args) if not append: self.debugCursor = self.debugText.textCursor() format = QtGui.QTextTableFormat() constraints = [QtGui.QTextLength(QtGui.QTextLength.FixedLength, size) for arg, size in args] format.setColumnWidthConstraints(constraints) format.setBorder(0) format.setCellPadding(2) format.setCellSpacing(0) self.debugCursor.insertTable(1, rows, format) scroll = self.debugText.verticalScrollBar() for arg, size in args: if arg: self.debugCursor.insertText(arg) self.debugCursor.movePosition(QtGui.QTextCursor.NextCell) scroll.setSliderPosition(scroll.maximum()) def debugEmitter(self, args, append=False): """Debug signal to allow threaded debug infos. """ self.debugSignal.emit(args, append) def detect(self, img, autoNeighbors=False, autoNeighborsParam=None): """Detect objects in img. """ self.currentFrame = img item = None indexes = self.objectsTree.selectedIndexes() if autoNeighbors and indexes: item = self.objectsTree.model().itemFromIndex(indexes[0]) # Detect on full size image tree, extracted = common.getObjectsTree(self.objectsTree, self.classifiersParameters, indexes, item) equalizeHist = self.equalizeHist.isChecked() rectsTree = self.detector.detect(img, tree, equalizeHist, self.debugEmitter, extracted, autoNeighborsParam) return rectsTree def displayImage(self, img): """Display numpy 'img' in 'mediaLabel'. """ rectsTree = self.detect(img) displayMode = self.__displayModes[self.displayCBox.currentIndex()] if displayMode == self.DISPLAY_PREPROCESSED: img = self.detector.preprocessed pixmap = common.np2Qt(img) w = float(pixmap.width()) # Scale image pixmap = common.fitImageToScreen(pixmap) scaleFactor = pixmap.width() / w # Draw scaled rectangles self.drawRects(pixmap, rectsTree, scaleFactor) self.mediaLabel.setPixmap(pixmap) self.mediaLabel.setFixedSize(pixmap.size()) def displayMedia(self, path): """Load and display media. """ if self.mediaThread.isRunning(): self.mediaThread.stop() self.mediaThread.wait() sourceMode = self.__sourceModes[self.sourceCBox.currentIndex()] self.mediaThread.start() self.togglePlayButton(True) def getSourceMode(self): """Return the current source mode. """ return self.__sourceModes[self.sourceCBox.currentIndex()] def getCurrentClassifierParameters(self): """Return current classifier parameters. """ index = self.objectsTree.currentIndex() item = index.model().itemFromIndex(index) param = self.classifiersParameters[item.data()] return item, param def toggleFillPath(self): """Display fill path dialog button if needed. """ index = self.fillCBox.currentIndex() fillMode = self.__fillModes[index] if fillMode == self.FILL_IMAGE: self.fillPath.show() else: self.fillPath.hide() def togglePlayButton(self, play=True): """Switch between play and pause icons. """ if play is True: self.playButton.setIcon(QIcon('assets/pause.png')) else: self.playButton.setIcon(QIcon('assets/play.png')) ######################################################## # Signals handlers def normalOutputWritten(self, text): """Append text to debug infos. """ cursor = self.debugText.textCursor() cursor.movePosition(QtGui.QTextCursor.End) cursor.insertText(text) self.debugText.setTextCursor(cursor) QApplication.processEvents() def play(self): """Play current media. """ if self.mediaThread.isRunning(): self.mediaThread.stop() self.mediaThread.wait() else: self.displayMedia(self.sourcePath.text()) def refresh(self): """Refresh current media. """ if self.mediaThread.isRunning(): self.mediaThread.stop() self.mediaThread.clean() self.displayMedia(self.sourcePath.text()) def nextFrame(self): """Go to next frame of current media. """ self.mediaThread.setNextFrameMode(True) self.displayMedia(self.sourcePath.text()) def loadFillPath(self): """Display file dialog to choose fill image. """ filters = self.tr('Image (' + self.IMAGE_FILTERS + ')') path, filters = QFileDialog.getOpenFileName(self, self.tr('Open file'), '.', filters) if path: item, param = self.getCurrentClassifierParameters() param.fillPath = path def loadMedia(self): """Load image or video. """ filters = "" filters = self.tr('Image (' + self.IMAGE_FILTERS + self.VIDEO_FILTERS + ')') path, filters = QFileDialog.getOpenFileName(self, self.tr('Open file'), '.', filters) if path: self.sourcePath.setText(path) self.mediaThread.stop() self.mediaThread.clean() self.displayMedia(path) def togglePath(self, index): """Hide path for camera mode. """ if self.__sourceModes[index] == self.SOURCE_CAMERA: self.sourcePath.hide() self.sourcePathButton.hide() self.displayMedia(self.sourcePath.text()) else: self.sourcePath.show() self.sourcePathButton.show() def colorDialog(self): """Open color dialog to pick a color. """ color = QColorDialog.getColor() if color.isValid(): common.setPickerColor(color, self.colorPicker) self.updateClassifierParameters(color=color) def bgColorDialog(self): """Open color dialog to pick a color. """ color = QColorDialog.getColor() if color.isValid(): self.bgColor = color common.setPickerColor(color, self.bgColorPicker) def bgPathDialog(self): """Open file dialog to select background image. """ filters = self.tr('Image (' + self.IMAGE_FILTERS + ')') path, filters = QFileDialog.getOpenFileName(self, self.tr('Open file'), '.', filters) if path: self.bgPath = path def toggleBgParams(self, index): """Hide / Show color picker and button path for background. """ mode = self.__bgModes[index] if mode == self.BG_COLOR: self.bgColorPicker.show() else: self.bgColorPicker.hide() if mode == self.BG_IMAGE: self.bgPathButton.show() else: self.bgPathButton.hide() def calcNeighbors(self): """Automatically calculate minimum neighbors. """ running = False if self.mediaThread.isRunning(): running = True self.mediaThread.stop() self.mediaThread.wait() self.detect(self.currentFrame, autoNeighbors=True, autoNeighborsParam=self.autoNeighborsParam.value()) self.showClassifierParameters(None, None) if running: self.displayMedia(self.sourcePath.text()) else: self.displayImage(self.currentFrame) def updateClassifierParameters(self, name=None, color=None): """Update classifier parameters. """ item, param = self.getCurrentClassifierParameters() if name: item.setText(name) param.name = name if color: item.setIcon(self.getIcon(color)) param.color = color def updateStabilize(self, checked): """Update stabilize classifier parameter. """ running = False if self.mediaThread.isRunning(): running = True self.mediaThread.stop() self.mediaThread.wait() item, param = self.getCurrentClassifierParameters() param.stabilize = checked self.detect(self.currentFrame) if running: self.displayMedia(self.sourcePath.text()) else: self.displayImage(self.currentFrame) def updateTracking(self, checked): """Update tracking classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.tracking = checked def updateScaleFactor(self, value): """Update scale factor classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.scaleFactor = value def updateShape(self, index): """Update shape classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.shape = self.__shapeModes[index] def updateFill(self, index): """Update fill classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.fill = self.__fillModes[index] self.toggleFillPath() def updateShowName(self, checked): """Update show name classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.showName = checked def updateMinNeighbors(self, value): """Update min neighbors classifier parameter. """ item, param = self.getCurrentClassifierParameters() param.minNeighbors = value def updateMinWidth(self, value): """Update minimum width classifier parameter. """ item, param = self.getCurrentClassifierParameters() w, h = param.minSize param.minSize = (value, h) def updateMinHeight(self, value): """Update minimum height classifier parameter. """ item, param = self.getCurrentClassifierParameters() w, h = param.minSize param.minSize = (w, value) def showClassifierParameters(self, selected, deselected): """Show the selected classifier parameters. """ selectedCount = len(self.objectsTree.selectedIndexes()) if selectedCount == 1: self.displayBox.setEnabled(True) self.parametersBox.setEnabled(True) index = self.objectsTree.currentIndex() item = index.model().itemFromIndex(index) param = self.classifiersParameters[item.data()] self.classifierName.setText(param.name) common.setPickerColor(param.color, self.colorPicker) self.classifierType.setText('(' + param.classifier + ')') self.scaleFactor.setValue(param.scaleFactor) self.minNeighbors.setValue(param.minNeighbors) w, h = param.minSize self.minWidth.setValue(w) self.minHeight.setValue(h) self.shapeCBox.setCurrentIndex(self.__shapeModes.index(param.shape)) self.fillCBox.setCurrentIndex(self.__fillModes.index(param.fill)) self.stabilize.setChecked(param.stabilize) self.showName.setChecked(param.showName) self.tracking.setChecked(param.tracking) else: self.displayBox.setEnabled(False) self.parametersBox.setEnabled(False) def addObject(self): """Add object to detect. """ selected = self.availableObjectsList.selectedItems() for item in selected: row = (self.availableObjectsList.row(item) + 1) % self.availableObjectsList.count() self.availableObjectsList.setCurrentRow(row) node = item.text() self.populateTree(node, self.objectsTree.model()) def removeObject(self): """Remove object to detect. """ selected = self.objectsTree.selectedIndexes() while selected and selected[0].model(): index = selected[0] item = index.model().itemFromIndex(index) del self.classifiersParameters[item.data()] if item.parent(): index.model().removeRow(item.row(), item.parent().index()) else: index.model().removeRow(item.row()) selected = self.objectsTree.selectedIndexes() def splitterMoved(self, pos, index): """Avoid segfault when QListWidget has focus and is going to be collapsed. """ focusedWidget = QApplication.focusWidget() if focusedWidget: focusedWidget.clearFocus() def toggleDebugInfo(self, pressed): """Toggle debug infos widget. """ if pressed: self.debugText.show() self.showDetails.setText(self.tr('Details <<<')) else: self.debugText.hide() self.showDetails.setText(self.tr('Details >>>')) def main(): app = QApplication(sys.argv) app.setStyle(QtWidgets.QStyleFactory.create("Fusion")) p = app.palette() p.setColor(QPalette.Base, QColor(40, 40, 40)) p.setColor(QPalette.Window, QColor(55, 55, 55)) p.setColor(QPalette.Button, QColor(49, 49, 49)) p.setColor(QPalette.Highlight, QColor(135, 135, 135)) p.setColor(QPalette.ButtonText, QColor(155, 155, 155)) p.setColor(QPalette.WindowText, QColor(155, 155, 155)) p.setColor(QPalette.Text, QColor(155, 155, 155)) p.setColor(QPalette.Disabled, QPalette.Base, QColor(49, 49, 49)) p.setColor(QPalette.Disabled, QPalette.Text, QColor(90, 90, 90)) p.setColor(QPalette.Disabled, QPalette.Button, QColor(42, 42, 42)) p.setColor(QPalette.Disabled, QPalette.ButtonText, QColor(90, 90, 90)) p.setColor(QPalette.Disabled, QPalette.Window, QColor(49, 49, 49)) p.setColor(QPalette.Disabled, QPalette.WindowText, QColor(90, 90, 90)) app.setPalette(p) QApplication.addLibraryPath(QApplication.applicationDirPath() + "/../PlugIns") main = Window() main.setWindowTitle('Detection') main.setWindowIcon(QtGui.QIcon('assets/icon.png')) main.show() try: sys.exit(app.exec_()) except KeyboardInterrupt: pass if __name__ == '__main__': main()