previously on
- Recently, I was watching reinforcement learning and wanted to quickly make a visualization of MDP. The main body is a drawing board, similar to visio, and then I can run RL algorithm in real time to see the change of value
- But the problem is, my visualization tool will use a PyQt5, or the level of half a bottle of water... So I thought not to make wheels. At first, I felt that this thing was similar to automata (DFA) or Turing machine. Just find a visual Open-Source Library to change it. It really didn't work. The library of mind mapping may also be similar. Unexpectedly, I can't find such a library, and I turn to find open source code or something. It's also very few
- Finally, I've worked hard to implement it with QPainter. So far, it's been six or seven hours. The more I do, the more trouble I feel. The code also tends to be chaotic. I don't want to do it anymore
- I didn't expect that just by searching and drawing arrows, I found that PyQt5 actually has a graphics view framework, which is specially used for things like me... Crack, no wonder I can't find the library... So I decided to give up my own work and learn that thing when I have time.
- It takes two days. The lesson is that sharpening the knife doesn't miss the firewood chopper... It's time to really go to graduate school at the beginning of school. The foundation of RL/DL / traditional ML has not been laid yet. It's annoying
- Now this semi-finished product is a pity to abandon. Just send a blog
Demo demo
-
In the MDP schematic diagram originally planned to be drawn, the large circle is the state node, the small circle is the action node, and the middle is connected with a directed edge
-
At present, only the functions of drawing circle (status / action node), dragging, scaling, box selection and so on are done. The demonstration is as follows
This part I have done has a complete wheel in the graphic view frame
code
- The general idea is,
- The drawing part is embedded in the main window as a widget object, which is called "canvas"
- The core elements in MDP are nodes and wires, so they are abstracted as "node" and "wire" objects, encapsulated with relevant control methods, and instantiated, managed and controlled by "canvas" objects
- Reload various mouse events on the canvas widget and draw using the graphics drawing method provided by QPainter
- Other parts of the main window realize peripheral functions, such as file management, RL algorithm test, etc
- At present, there are three classes: node, canvas and main window (Editor). The following three paragraphs are copied to three py file, put it in the same folder and you can run it
1. Node class
- The body is a circle with four control points around it for scaling
from PyQt5 import QtGui, QtCore, QtWidgets import math COLORS = {'state':'#DDDDDD','action':'#888888','select':'#FAC8C8'} X = [-0.707,0.707,0.707,-0.707] Y = [-0.707,-0.707,0.707,0.707] def pointDistance(point1,point2): deltaX = point1.x()-point2.x() deltaY = point1.y()-point2.y() return math.sqrt(deltaY*deltaY + deltaX*deltaX) class Node: def __init__(self,canvas,type,fixPoint,text='S'): self.center = QtCore.QPoint() # Central coordinates self.lastCenterPonit = None # Last central coordinate self.ctrlPoints = [QtCore.QPoint(),QtCore.QPoint(),QtCore.QPoint(),QtCore.QPoint()] # Control points: top left / top right / bottom right / bottom left self.ctrlPointsRadius = 4 # Control point radius self.fixPoint = fixPoint # Fixed point of diameter segment when init and resize self.movePoint = None # When init and resize, the diameter segment moves the point self.canvas = canvas # Canvas object reference self.type = type # 'action' or 'state' self.text = text # Prompt text self.color = COLORS['select'] self.radius = 0 self.zoom = 1.0 self.selected = False # Is selected self.drawn = False # End of drawing self.resizing = False # Zooming def setResizing(self,resizing,moveIndex=-1): self.resizing = resizing if not resizing: self.fixPoint = None elif moveIndex != -1: fixIndex = moveIndex-2 if moveIndex >=2 else moveIndex+2 self.fixPoint = QtCore.QPoint(self.ctrlPoints[fixIndex]) print('set fix') else: assert False def setSelected(self,selected): self.selected = selected if selected: self.color = COLORS['select'] else: self.color = COLORS[self.type] self.lastCenterPonit = QtCore.QPoint(self.center) def setDrawn(self,drawn): self.drawn = drawn if drawn: self.resetCtrlPoints() def setRadius(self,radius): if radius < 0: radius = 0 self.radius = radius def getRadius(self): return self.radius def setColor(self,color): self.color = color def getType(self): return self.type def resize(self,endPoint): self.center.setX(0.5*(self.fixPoint.x() + endPoint.x())) self.center.setY(0.5*(self.fixPoint.y() + endPoint.y())) self.radius = 0.5*pointDistance(endPoint,self.fixPoint)*self.zoom def move(self,startPoint,endPoint): self.center.setX(self.lastCenterPonit.x() + endPoint.x() - startPoint.x()) self.center.setY(self.lastCenterPonit.y() + endPoint.y() - startPoint.y()) self.resetCtrlPoints() # Reset control point def resetCtrlPoints(self): for i in range(4): point = self.ctrlPoints[i] point.setX(self.center.x()+X[i]*self.radius) point.setY(self.center.y()+Y[i]*self.radius) # Does the cursor point inside the circle def cursorInside(self,cursorPos): if not self.selected: return pointDistance(cursorPos,self.center) < self.radius else: return pointDistance(cursorPos,self.center) < self.radius or self.cursorInCtrlPoint(cursorPos) != -1 # Which control point does the cursor point to def cursorInCtrlPoint(self,cursorPos): for i in range(4): point = self.ctrlPoints[i] if pointDistance(cursorPos,point) < self.ctrlPointsRadius: return i return -1 # Draw control points def printCtrlPoint(self): self.canvas.setPainterColor('#FFFFFF') if not self.drawn: for i in range(4): point = QtCore.QPoint() point.setX(self.center.x()+X[i]*self.radius) point.setY(self.center.y()+Y[i]*self.radius) self.canvas.painter.drawEllipse(point,self.ctrlPointsRadius,self.ctrlPointsRadius) else: for point in self.ctrlPoints: self.canvas.painter.drawEllipse(point,self.ctrlPointsRadius,self.ctrlPointsRadius) # Draw node def print(self): self.canvas.setPainterColor(self.color) self.canvas.painter.drawEllipse(self.center,self.radius,self.radius) self.canvas.painter.drawText(QtCore.QRect(self.center.x()-self.radius,self.center.y()-self.radius,2*self.radius,2*self.radius),QtCore.Qt.AlignCenter ,self.text) if self.selected: self.printCtrlPoint()
2. Canvas class
- Inherited from QWidget, the core is to overload mouse practice and QPainter drawing
from PyQt5 import QtGui, QtCore, QtWidgets from sklearn.metrics import pairwise import numpy as np import math from Node import Node class Canvas(QtWidgets.QWidget): newNodeSignal = QtCore.pyqtSignal() def __init__(self): super().__init__() self.brush = QtGui.QBrush(QtGui.QColor('#222222'),QtCore.Qt.SolidPattern) self.painter = QtGui.QPainter(self) self.nodes = [] # state node & action node self.startPoint = QtCore.QPoint() # Drag the cursor to the start point self.endPoint = QtCore.QPoint() # Drag the cursor to the end point self.mode = 'select' # select/state/action/resize/boxing self.selectedNodes = [] # Currently selected point self.drawingNode = None # Initializing drawn points self.initUI() def initUI(self): pass def setMode(self,mode): if self.mode == 'select' and mode != 'resize': self.clearSelected() self.mode = mode def setPainterColor(self,color): self.brush.setColor(QtGui.QColor(color)) self.painter.setBrush(self.brush) def mouseMoveEvent(self, ev): pos = ev.pos() # Mouse position if ev.buttons() & QtCore.Qt.LeftButton: # Resize when creating new nodes if self.drawingNode != None and self.mode in ['state','action']: self.drawingNode.resize(ev.pos()) # Frame selection elif self.mode == 'boxing': self.endPoint = ev.pos() # Move the selected node elif self.selectedNodes != [] and self.mode == 'select': self.endPoint = ev.pos() for node in self.selectedNodes: node.move(self.startPoint,self.endPoint) # Resize nodes elif len(self.selectedNodes) == 1 and self.mode == 'resize': node = self.selectedNodes[0] node.resize(ev.pos()) node.resetCtrlPoints() else: pass self.update() def mousePressEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: self.startPoint = self.endPoint = ev.pos() # Create a new node if self.mode in ['state','action']: self.drawingNode = Node(self,self.mode,ev.pos(),self.mode) self.nodes.append(self.drawingNode) # Select the node to resize elif len(self.selectedNodes) == 1 and self.selectedNodes[0].cursorInCtrlPoint(ev.pos()) != -1: self.setMode('resize') self.selectedNodes[0].setResizing(True,self.selectedNodes[0].cursorInCtrlPoint(ev.pos())) # Select node elif self.nodes != [] and self.mode == 'select': clickedSpace = True # Node in point, single point selection for node in reversed(self.nodes): # Select nodes according to occlusion relationship if node.cursorInside(ev.pos()): if len(self.selectedNodes) <= 1: self.clearSelected() # Clear last check mark node.setSelected(True) self.selectedNodes = [node] self.nodes.remove(node) # The selected node is placed at the top level self.nodes.append(node) clickedSpace = False break # Click blank in the box to select if clickedSpace: self.setMode('boxing') # Blank canvas, box selection mode elif self.nodes == []: self.setMode('boxing') else: pass self.update() def mouseReleaseEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: # The new node has been created, go to the selected mode if self.mode in ['state','action']: self.newNodeSignal.emit() self.selectedNodes = [self.drawingNode] self.drawingNode.setDrawn(True) self.drawingNode.setSelected(True) self.drawingNode = None # Node sizing complete elif self.mode == 'resize': self.setMode('select') self.selectedNodes[0].setResizing(False) # Box selection completed elif self.mode == 'boxing': self.boxNodes() self.startPoint.setX(0),self.startPoint.setY(0) self.endPoint.setX(0),self.endPoint.setY(0) self.setMode('select') else: pass self.update() def wheelEvent(self, ev): angle = ev.angleDelta() / 8 # Returns the QPoint object, which is the value of the roller rotation, and the unit is 1 / 8 degree angleY = angle.y() # Vertical rolling distance print(angleY) #self.update() def paintEvent(self,event): self.painter.begin(self) self.paintEvent = event self.updateCanvas(self.painter) self.painter.end() def updateCanvas(self,painter): # node if self.nodes != []: for node in self.nodes: node.print() # Selection box self.painter.setBrush(QtCore.Qt.NoBrush) self.painter.setPen(QtCore.Qt.darkGreen) if self.mode == 'boxing': self.painter.drawRect(self.startPoint.x(), self.startPoint.y(), self.endPoint.x() - self.startPoint.x(), self.endPoint.y() - self.startPoint.y()) def clearSelected(self): if self.selectedNodes != []: for node in self.selectedNodes: node.setSelected(False) self.selectedNodes = [] self.update() def deleteNode(self): if self.selectedNodes != []: for node in self.selectedNodes: self.nodes.remove(node) self.selectedNodes = [] self.setMode('select') self.update() def boxNodes(self): self.clearSelected() for node in self.nodes: if min(self.startPoint.x(),self.endPoint.x()) <= node.center.x() <= max(self.startPoint.x(),self.endPoint.x()) and \ min(self.startPoint.y(),self.endPoint.y()) <= node.center.y() <= max(self.startPoint.y(),self.endPoint.y()): node.setSelected(True) self.selectedNodes.append(node) self.update()
3. Editor class
- This is the main window
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5.QtWidgets import QMainWindow, QApplication, QAction, QGraphicsView ,QScrollArea from PyQt5.QtGui import QPainter, QPainterPath, QIcon from PyQt5.QtCore import Qt import sys from Canvas import Canvas class Editor(QMainWindow): def __init__(self): super().__init__() self.canves = Canvas() self.setupUi() self.setupToolBar() def setupUi(self): self.setObjectName("EditorWindow") self.resize(760, 544) self.centralwidget = QtWidgets.QWidget(self) self.gridLayout = QtWidgets.QGridLayout(self.centralwidget) self.gridLayout.setObjectName("gridLayout") self.scroller = QScrollArea(self.centralwidget) self.scrollerGridLayout = QtWidgets.QGridLayout(self.scroller) self.scrollerGridLayout.setObjectName("scrollerGridLayout") self.scrollerGridLayout.addWidget(self.canves,1,1,1,1) self.scroller.setLayout(self.scrollerGridLayout) self.gridLayout.addWidget(self.scroller,1,1,1,1) self.setCentralWidget(self.centralwidget) # Once a new node is drawn, it turns to the selected state self.canves.newNodeSignal.connect(lambda:self.setMode('select')) def setupToolBar(self): self.toolbar = self.addToolBar('toolbar') self.toolbar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) action_new_file = QAction(QIcon('./images/filenew.png'), 'New File', self) #action_new_file.triggered.connect(self.file_new) self.toolbar.addAction(action_new_file) action_save_file = QAction(QIcon('./images/filesave.png'), 'Save File', self) #action_save_file.triggered.connect(self.file_save) self.toolbar.addAction(action_save_file) self.action_state_ponit = QAction(QIcon('./images/state.png'), 'State node', self) self.action_state_ponit.triggered.connect(lambda:self.setMode('state')) self.toolbar.addAction(self.action_state_ponit) self.action_act_ponit = QAction(QIcon('./images/action.png'), 'Action node', self) self.action_act_ponit.triggered.connect(lambda:self.setMode('action')) self.toolbar.addAction(self.action_act_ponit) self.action_selcet_ponit = QAction(QIcon('./images/select2.png'), 'select', self) self.action_selcet_ponit.triggered.connect(lambda:self.setMode('select')) self.toolbar.addAction(self.action_selcet_ponit) action_delete_point = QAction(QIcon('./images/delete.png'), 'Delete node', self) action_delete_point.triggered.connect(self.canves.deleteNode) self.toolbar.addAction(action_delete_point) def setMode(self,mode): # The icon changes to indicate the current mode (both resize and boxing belong to select mode) self.action_state_ponit.setIcon(QIcon('./images/state.png')) self.action_act_ponit.setIcon(QIcon('./images/action.png')) self.action_selcet_ponit.setIcon(QIcon('./images/select.png')) if self.canves.mode == mode or mode == 'select': self.action_selcet_ponit.setIcon(QIcon('./images/select2.png')) mode == 'select' elif mode == 'state': self.action_state_ponit.setIcon(QIcon('./images/state2.png')) elif mode == 'action': self.action_act_ponit.setIcon(QIcon('./images/action2.png')) else: pass # Setting mode self.canves.setMode(mode) if __name__ == '__main__': app = QApplication(sys.argv) editor = Editor() editor.show() sys.exit(app.exec_())