def addCanvasDashedWedge(self, p1, p2, p3, dash=(2, 2), color=(0, 0, 0), color2=None, **kwargs): rgb = [int(c * 255) for c in color] pen = QtGui.QPen(QtGui.QColor(*rgb), 1, PenStile_DashLine) self.painter.setPen(pen) dash = (4, 4) pts1 = self._getLinePoints(p1, p2, dash) pts2 = self._getLinePoints(p1, p3, dash) if len(pts2) < len(pts1): pts2, pts1 = pts1, pts2 for i in range(len(pts1)): qp1 = QtCore.QPointF(pts1[i][0], pts1[i][1]) qp2 = QtCore.QPointF(pts2[i][0], pts2[i][1]) self.painter.drawLine(qp1, qp2)
def addCanvasLine(self, p1, p2, color=(0, 0, 0), color2=None, **kwargs): if 'dash' in kwargs: line_type = PenStile_DashLine else: line_type = PenStile_SolidLine qp1 = QtCore.QPointF(*p1) qp2 = QtCore.QPointF(*p2) qpm = QtCore.QPointF((p1[0] + p2[0]) / 2, (p1[1] + p2[1]) / 2) if color2 and color2 != color: rgb = [int(c * 255) for c in color] pen = QtGui.QPen(QtGui.QColor(*rgb), 1, line_type) self.painter.setPen(pen) self.painter.drawLine(qp1, qpm) rgb2 = [int(c * 255) for c in color2] pen.setColor(QtGui.QColor(*rgb2)) self.painter.setPen(pen) self.painter.drawLine(qpm, qp2) else: rgb = [int(c * 255) for c in color] pen = QtGui.QPen(QtGui.QColor(*rgb), 1, line_type) self.painter.setPen(pen) self.painter.drawLine(qp1, qp2)
def load_points(self, file_name): file = open(file_name, 'r') self.directory = os.path.split(file_name)[0] self.directory_set.emit(self.directory) data = json.load(file) file.close() survey_id = data['metadata']['survey_id'] # Backward compat if 'custom_fields' in data: self.custom_fields = data['custom_fields'] else: self.custom_fields = {'fields': [], 'data': {}} if 'ui' in data: self.ui = data['ui'] else: self.ui = { 'grid': { 'size': 200, 'color': [255, 255, 255] }, 'point': { 'radius': 25, 'color': [255, 255, 0] } } # End Backward compat self.colors = data['colors'] self.classes = data['classes'] self.coordinates = data['metadata']['coordinates'] self.points = {} if 'points' in data: self.points = data['points'] for image in self.points: for class_name in self.points[image]: for p in range(len(self.points[image][class_name])): point = self.points[image][class_name][p] self.points[image][class_name][p] = QtCore.QPointF( point['x'], point['y']) for class_name in data['colors']: self.colors[class_name] = QtGui.QColor(self.colors[class_name][0], self.colors[class_name][1], self.colors[class_name][2]) self.points_loaded.emit(survey_id) self.fields_updated.emit(self.custom_fields['fields']) path = os.path.split(file_name)[0] if self.points.keys(): path = os.path.join(path, list(self.points.keys())[0]) self.load_image(path)
def addCanvasText(self, text, pos, font, color=(0, 0, 0), **kwargs): orientation = kwargs.get('orientation', 'E') qfont = QtGui.QFont("Helvetica", int(font.size * 1.5)) qtext = QtGui.QTextDocument() qtext.setDefaultFont(qfont) colored = [int(c * 255) for c in color] colored.append(text) html_format = "<span style='color:rgb({},{},{})'>{}</span>" formatted = html_format.format(*colored) qtext.setHtml(formatted) if orientation == 'N': qpos = QtCore.QPointF(pos[0] - qtext.idealWidth() / 2, pos[1] - font.size) elif orientation == 'W': qpos = QtCore.QPointF(pos[0] - qtext.idealWidth() + font.size, pos[1] - font.size) else: qpos = QtCore.QPointF(pos[0] - font.size, pos[1] - font.size) self.painter.save() self.painter.translate(qpos) qtext.drawContents(self.painter) self.painter.restore() return font.size * 1.8, font.size * 1.8, 0
def addCanvasPolygon(self, ps, color=(0, 0, 0), fill=True, stroke=False, **kwargs): polygon = QtGui.QPolygonF() for ver in ps: polygon.append(QtCore.QPointF(*ver)) color = [int(c * 255) for c in color] pen = QtGui.QPen(QtGui.QColor(*color), 1, PenStile_DashLine) self.painter.setPen(pen) self.painter.setBrush(QtGui.QColor(0, 0, 0)) self.painter.drawPolygon(polygon)
def polyClose(self): #make into hot key not button if self.measuring_area: if self.line_count > 2: #cant make polygon w/ two lines self.measuring_area = False A = self.A.calcArea() self.areaValues = np.append(self.areaValues, A) #add area values #draw permanent polygon points = [ QtCore.QPointF(x,y) for x,y in zip( self.A.x, self.A.y ) ] self.scene.polyItem2 = QGraphicsPolygonItem(QtGui.QPolygonF(points)) self.scene.polyItem2.setBrush( QtGui.QBrush(QtGui.QColor(255,255,255,127)) ) if self.scene.polyItem: self.scene.removeItem(self.scene.polyItem) #remove mouseover polygon self.scene.polyItem = False #remove mouseover polygon self.scene.removeItem(self.scene.testline) self.scene.testline = False self.scene.addItem(self.scene.polyItem2) #shade in polygon self.parent().statusbar.showMessage('Polygon area measurement completed') self.parent().areaButton.setChecked(False) self.parent().bezier.setEnabled(True) #make bezier fit available again else: print("cannot draw polygon with fewer than three vertices")
def __init__(self, parent=None): super(imwin, self).__init__(parent) self.scene = QGraphicsScene() self.view = QGraphicsView(self.scene) self.pixmap = None self._lastpos = None self._thispos = None self.delta = QtCore.QPointF(0, 0) self.nm = None self.measuring_length = False self.measuring_widths = False self.measuring_area = False self.measuring_angle = False self._zoom = 1 self.newPos = None self.oldPos = None self.factor = 1.0 self.numwidths = None self.widthNames = [] #initialize as empty list self.d = {} #dictionary for line items #self.k = 0 #initialize counter so lines turn yellow self.L = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.W = posData(np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) self.scene.realline = None self.scene.testline = None self.setMouseTracking(True) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) #self.setRenderHints(QtGui.QPainter.Antialiasing # | QtGui.QPainter.SmoothPixmapTransform) #self.setRenderHint(QtGui.QPainter.Antialiasing, True) #self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, True) self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse) self.setInteractive(False)
def mousePressEvent(self, event): #http://pyqt.sourceforge.net/Docs/PyQt4/qgraphicsscenemouseevent.html #https://stackoverflow.com/questions/21197658/how-to-get-pixel-on-qgraphicspixmapitem-on-a-qgraphicsview-from-a-mouse-click data = self.mapToScene(event.pos()) #draw piecewise lines for non-width measurements rules = [self.measuring_length, self.measuring_angle, self.measuring_area] if self.scene.testline and self._thispos and any(rules): start = self._thispos end = QtCore.QPointF(data) if self._lastpos and self.measuring_angle: a = self._lastpos - self._thispos b = data - self._thispos a = np.array([a.x(), a.y()]) b = np.array([b.x(), b.y()]) self.measuring_angle = False t = np.arccos(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) t *= 180 / np.pi #convert to degrees self.T.update(t) self.angleValues = np.append(self.angleValues,t) self.parent().statusbar.showMessage('Angle measurement complete') self.parent().angleButton.setChecked(False) self.parent().bezier.setEnabled(True) self.scene.realline = QGraphicsLineItem(QtCore.QLineF(start, end)) self.scene.addItem(self.scene.realline) #Collect piecewise line start/end points self._lastpos = self._thispos # save old position value self._thispos = QtCore.QPointF(data) # update current position if self.measuring_length: self.L.update(data.x(), data.y()) # update total length self.line_count += 1 elif self.measuring_area: self.line_count += 1 intersect = False if self.line_count > 2: #cant make polygon w/ two lines intersect, xi, yi, k = self.A.checkIntersect(data.x(),data.y()) self.parent().areaButton.setEnabled(True) if intersect: self.measuring_area = False self.A.update(xi,yi) #update with intersect point self.A.x, self.A.y = self.A.x[k:], self.A.y[k:] #only use points after intersection A = self.A.calcArea() self.areaValues = np.append(self.areaValues, A) #add area values #draw permanent polygon points = [ QtCore.QPointF(x,y) for x,y in zip( self.A.x, self.A.y ) ] self.scene.polyItem2 = QGraphicsPolygonItem(QtGui.QPolygonF(points)) self.scene.polyItem2.setBrush( QtGui.QBrush(QtGui.QColor(255,255,255,127)) ) self.scene.removeItem(self.scene.polyItem) #remove mouseover polygon self.scene.polyItem = False #remove mouseover polygon self.scene.addItem(self.scene.polyItem2) #shade in polygon self.parent().statusbar.showMessage('Polygon area measurement completed') self.parent().areaButton.setChecked(False) self.parent().bezier.setEnabled(True) #make bezier fit available again QApplication.setOverrideCursor(QtCore.Qt.ArrowCursor) #change cursor else: self.A.update(data.x(),data.y()) #update with click point #https://stackoverflow.com/questions/30898846/qgraphicsview-items-not-being-placed-where-they-should-be if self.measuring_widths: #measure widths, snap to spines k = int(self.k / 2) + 1 #same origin for spine on either side x0, y0 = self.xp[k], self.yp[k] x1, y1 = data.x(), data.y() #perpindicular slopes vx = self.slopes[:,k][1] vy = -self.slopes[:,k][0] A = np.matrix([[vx, -vy], [vy, vx]]) b = np.array([x1 - x0, y1 - y0]) t = np.linalg.solve(A,b) xi = x0 + t[0]*vx yi = y0 + t[0]*vy self.W.update(xi,yi) p = QtCore.QPointF(xi, yi) s = 10 #dot size self.scene.ellipseItem = QGraphicsEllipseItem(0, 0, s, s) self.scene.ellipseItem.setPos(p.x() - s / 2, p.y() - s / 2) qb = QtGui.QBrush() qb.setColor(QtGui.QColor('red')) self.scene.ellipseItem.setBrush(qb) self.scene.ellipseItem.setFlag( QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations, False) #size stays small, but doesnt translate if false self.scene.addItem(self.scene.ellipseItem) self.k += 1 if self.k < self.nspines: self.d[str(self.k)].setPen(QtGui.QPen( QtGui.QColor('yellow'))) #Highlight next spine if self.k == self.nspines: self.parent().statusbar.showMessage('Width measurements complete') self.measuring_widths = False self.parent().widthsButton.setEnabled(False) self.parent().widthsButton.setChecked(False) self.parent().bezier.setEnabled(True) width = np.sqrt( (self.W.x[1::2] - self.W.x[0::2])**2 + (self.W.y[1::2] - self.W.y[0::2])**2) #calculate widths self.widths[-1] = width
def measure_widths(self): def qpt2pt(x, y): Q = self.mapFromScene( self.mapToScene( int(x), int(y)) ) return Q.x(), Q.y() self.measuring_widths = True self.parent().widthsButton.setChecked(True) self.k = 0 self.W = posData( np.empty(shape=(0, 0)), np.empty(shape=(0, 0))) #preallocate custom widths #number of possible measurements per segment (length + #widths) #self.measurements = np.empty((0, self.iw.nm + 1), int) * np.nan self.numwidths = int(self.parent().subWin.numwidths.text()) #self.measurements[-1] = np.append( self.l[-1], np.zeros(self.numwidths-1)*np.nan ) #preallocate measurements self.widths[-1] = np.empty(self.numwidths-1, dtype='float') #preallocate measurements self.widthNames[-1] = [ '{0:2.2f}% Width'.format(100 * f / self.numwidths) for f in np.arange(1, self.numwidths) ] self.nspines = 2 * (self.numwidths - 1) self.parent().statusbar.showMessage( 'Click point along spines to make width measurements perpindicular to the length segment' ) #get pts for width drawing bins = np.linspace(0, self.l[-1], self.numwidths + 1) inds = np.digitize(self.l, bins) __, self.inddec = np.unique(inds, return_index = True) pts = np.array(list(map(qpt2pt, self.xs, self.ys))) x, y = pts[:, 0], pts[:, 1] self.xp, self.yp = x[self.inddec], y[self.inddec] self.slopes = self.m[:,self.inddec] #Identify width spine points self.xsw = x[inds] self.ysw = y[inds] #Draw Widths for k,(x,y) in enumerate(zip(self.xp[1:-1], self.yp[1:-1])): x1, y1 = x,y L = self.pixmap_fit.width() H = self.pixmap_fit.height() v = self.slopes[:,k+1] vx = v[1] vy = -v[0] t0 = np.hypot(L,H) t2 = 0 #intersect: rectangle for offset in ([0,0],[L,H]): for ev in ([1,0],[0,1]): A = np.matrix([ [vx, ev[0]] , [vy, ev[1]] ]) b = np.array([offset[0] - x1, offset[1] - y1]) T = np.linalg.solve(A,b)[0] t0 = min(T, t0, key=abs) #find nearest intersection to bounds #Find 2nd furthest intersection within bounds bounds = np.array( [(L - x1)/vx, (H - y1)/vy, -x1/vx, -y1/vy] ) t2 = max(-t0, np.sign(-t0)* np.partition(bounds,-2)[-2], key=abs) x0 = x1 + t0*vx y0 = y1 + t0*vy x2 = x1 + t2*vx y2 = y1 + t2*vy for l, (x, y) in enumerate(zip([x0, x2], [y0, y2])): start = QtCore.QPointF(x1, y1) end = QtCore.QPointF(x, y) self.scene.interpLine = QGraphicsLineItem( QtCore.QLineF(start, end)) self.d["{}".format(2 * k + l)] = self.scene.interpLine self.scene.addItem(self.scene.interpLine) if k == 0 and l == 0: self.scene.interpLine.setPen( QtGui.QPen(QtGui.QColor('yellow')))
def mouseDoubleClickEvent(self, event): def qpt2pt(x, y): Q = self.mapFromScene(self.mapToScene( int(x), int(y))) return Q.x(), Q.y() #only delete lines if bezier fit if self.measuring_length and self.parent().bezier.isChecked() and (len(np.vstack((self.L.x, self.L.y)).T) > 2): self.parent().statusbar.showMessage('Length measurement complete.') #Remove most recent items drawn (exact lines) nl = self.line_count for k, i in enumerate(self.scene.items()): if k < nl: self.scene.removeItem(i) #set item to false? if self._lastpos and self.measuring_length: # catmull roms spline instead? # or rational bezier curve - tuneable approximating/interpolating. ref. wikipedia # https://codeplea.com/introduction-to-splines if (self.parent().bezier.isChecked()) and (len(np.vstack((self.L.x, self.L.y)).T) > 2): nt = 2000 #max(1000, self.numwidths * 50) #num of interpolating points def bernstein(i, n, t): return comb(n,i) * t**(n-i) * (1-t)**i def bezier_rational(points, nt): """Rational Bezier Curve fit""" # https://gist.github.com/Alquimista/1274149 # https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html n = len(points) xp = np.array([p[0] for p in points]) yp = np.array([p[1] for p in points]) t = np.linspace(0.0, 1.0, nt) #Bezier curve B = np.array([ bernstein(i,n-1,t) for i in range(0,n) ]) xb = np.dot(xp, B)[::-1] yb = np.dot(yp, B)[::-1] #Analytic gradient for bezier curve Qx = n*np.diff(xp) Qy = n*np.diff(yp) Bq = np.array([ bernstein(i,n-2,t) for i in range(0,n-1) ]) dxb = np.dot(Qx, Bq)[::-1] dyb = np.dot(Qy, Bq)[::-1] m = np.vstack((dxb,dyb)) m *= (1/np.linalg.norm(m, axis=0)) return xb, yb, m points = np.vstack((self.L.x, self.L.y)).T self.xs, self.ys, self.m = bezier_rational(points, nt) pts = np.array(list(map(qpt2pt, self.xs, self.ys))) x, y = pts[:, 0], pts[:, 1] self.l = np.cumsum(np.hypot(np.gradient(x), np.gradient(y))) #integrate for length #draw cubic line to interpolated points for i in range(1, nt - 1): P0 = QtCore.QPointF( self.xs[i-1], self.ys[i-1] )#.toPoint() P1 = QtCore.QPointF( self.xs[i ], self.ys[i ] )#.toPoint() P2 = QtCore.QPointF( self.xs[i+1], self.ys[i+1] )#.toPoint() start = self.mapFromScene(self.mapToScene(P0.toPoint())) mid = self.mapFromScene(self.mapToScene(P1.toPoint())) end = self.mapFromScene(self.mapToScene(P2.toPoint())) path = QtGui.QPainterPath(P0) path.cubicTo(P0, P1, P2) self.scene.addPath(path) if (not self.parent().bezier.isChecked()) or (len(np.vstack((self.L.x, self.L.y)).T) <= 2): """Simple linear points if piecewise mode (or only two points used?)""" pts = np.array(list(map(qpt2pt, self.L.x, self.L.y))) x, y = pts[:, 0], pts[:, 1] slope = (y[-1] - y[0]) / (x[-1] - x[0]) theta = np.arctan(slope) distance = np.hypot( x[-1] - x[0], y[-1] - y[0] ) r = np.linspace(0, distance, 1000) self.xs, self.ys = x[0] + r*np.cos(theta), y[0] + r*np.sin(theta) self.m = np.vstack(( slope*(r*0 + 1), -slope*(r*0 + 1) )) #self.m = np.vstack(( (y[-1] - y[0])*(r*0 + 1), (x[-1] - x[0])*(r*0 + 1) )) self.m = np.vstack(( (x[-1] - x[0])*(r*0 + 1), (y[-1] - y[0])*(r*0 + 1) )) self.l = np.cumsum(np.hypot(np.diff(self.xs), np.diff(self.ys))) #integrate for length self.lengths[-1] = self.l[-1] self.lengths.extend([np.nan]) self.widths.append([]) self.widthNames.append([]) QApplication.setOverrideCursor(QtCore.Qt.CursorShape.ArrowCursor) #change cursor if self.parent().bezier.isChecked() or (len(np.vstack((self.L.x, self.L.y)).T) <= 2): #measure widths possible if bezier or if single piecewise segment self.parent().widthsButton.setEnabled(True) self.parent().lengthButton.setChecked(False) self.parent().angleButton.setChecked(False) self.measuring_length = False self.measuring_angle = False self._thispos = False
def mouseMoveEvent(self, event): data = self.mapToScene(event.position().toPoint()) rules = [self.measuring_length, self.measuring_angle, self.measuring_area] modifiers = QApplication.keyboardModifiers() if modifiers == QtCore.Qt.KeyboardModifier.ShiftModifier and self.oldPos: QApplication.setOverrideCursor(QtCore.Qt.CursorShape.OpenHandCursor) self.newPos = data delta = self.newPos - self.oldPos self.translate(delta.x(), delta.y()) elif (any(rules) or self.measuring_widths): QApplication.setOverrideCursor(QtCore.Qt.CursorShape.CrossCursor) #change cursor else: QApplication.setOverrideCursor(QtCore.Qt.CursorShape.ArrowCursor) #change cursor #dragging line if self._thispos and any(rules): if self.measuring_length: self.parent().statusbar.showMessage( 'Click to place next point... double click to finish') if self.measuring_area: self.parent().statusbar.showMessage( 'Click to place next point... close polygon to finish') if self.measuring_angle: self.parent().statusbar.showMessage( 'Click point to define vector') end = QtCore.QPointF(data)#self.mapToScene(event.pos())) start = self._thispos if self.measuring_angle and self._lastpos: start = self._thispos if self.scene.testline: #remove old line self.scene.removeItem(self.scene.testline) self.scene.testline = False if self.measuring_area and self.line_count > 2: intersect, xi, yi, k = self.A.checkIntersect(data.x(),data.y()) if self.scene.area_ellipseItem: #remove existing intersect self.scene.removeItem(self.scene.area_ellipseItem) self.scene.area_ellipseItem = False if self.scene.polyItem: self.scene.removeItem(self.scene.polyItem) self.scene.polyItem = False if intersect: #indicate intersect point p = QtCore.QPointF(xi, yi) self.scene.area_ellipseItem = QGraphicsEllipseItem(0, 0, 10, 10) self.scene.area_ellipseItem.setPos(p.x() - 10 / 2, p.y() - 10 / 2) self.scene.area_ellipseItem.setBrush( QtGui.QBrush(QtCore.QtColor('blue'))) #, style=QtCore.Qt.BrushStyle.SolidPattern)) self.scene.area_ellipseItem.setFlag( QGraphicsItem.GraphicsItemFlag.ItemIgnoresTransformations, False) #size stays small, but doesnt translate if set to false self.scene.addItem(self.scene.area_ellipseItem) #shade polygon region points = [ QtCore.QPointF(x,y) for x,y in zip( self.A.x[k:], self.A.y[k:] ) ] points.append(QtCore.QPointF(xi,yi)) self.scene.polyItem = QGraphicsPolygonItem(QtGui.QPolygonF(points)) self.scene.polyItem.setBrush( QtGui.QBrush(QtGui.QColor(255,255,255,127)) ) self.scene.addItem(self.scene.polyItem) self.scene.testline = QGraphicsLineItem(QtCore.QLineF(start, end)) self.scene.addItem(self.scene.testline)