def paintEvent(self, event): option = QStyleOption() option.initFrom(self) contents_rect = self.style().subElementRect(QStyle.SE_FrameContents, option, self) or self.contentsRect() # the SE_FrameContents rect is Null unless the stylesheet defines decorations if self.graphStyle == self.BarStyle: graph_width = self.__dict__['graph_width'] = int(ceil(float(contents_rect.width()) / self.horizontalPixelsPerUnit)) else: graph_width = self.__dict__['graph_width'] = int(ceil(float(contents_rect.width() - 1) / self.horizontalPixelsPerUnit) + 1) max_value = self.__dict__['max_value'] = max(chain([0], *(islice(reversed(graph.data), graph_width) for graph in self.graphs if graph.enabled))) if self.graphHeight == self.AutomaticHeight or self.graphHeight < 0: graph_height = self.__dict__['graph_height'] = max(self.scaler.get_height(max_value), self.minHeight) else: graph_height = self.__dict__['graph_height'] = max(self.graphHeight, self.minHeight) if self.graphStyle == self.BarStyle: height_scaling = float(contents_rect.height()) / graph_height else: height_scaling = float(contents_rect.height() - self.lineThickness) / graph_height painter = QStylePainter(self) painter.drawPrimitive(QStyle.PE_Widget, option) painter.setClipRect(contents_rect) painter.save() painter.translate(contents_rect.x() + contents_rect.width() - 1, contents_rect.y() + contents_rect.height() - 1) painter.scale(-1, -1) painter.setRenderHint(QStylePainter.Antialiasing, self.graphStyle != self.BarStyle) for graph in (graph for graph in self.graphs if graph.enabled and graph.data): if self.boundary is not None and 0 < self.boundary < graph_height: boundary_width = min(5.0/height_scaling, self.boundary-0, graph_height-self.boundary) pen_color = QLinearGradient(0, (self.boundary - boundary_width) * height_scaling, 0, (self.boundary + boundary_width) * height_scaling) pen_color.setColorAt(0, graph.color) pen_color.setColorAt(1, graph.over_boundary_color) brush_color = QLinearGradient(0, (self.boundary - boundary_width) * height_scaling, 0, (self.boundary + boundary_width) * height_scaling) brush_color.setColorAt(0, self.color_with_alpha(graph.color, self.fillTransparency)) brush_color.setColorAt(1, self.color_with_alpha(graph.over_boundary_color, self.fillTransparency)) else: pen_color = graph.color brush_color = self.color_with_alpha(graph.color, self.fillTransparency) dataset = islice(reversed(graph.data), graph_width) if self.graphStyle == self.BarStyle: lines = [QLineF(x*self.horizontalPixelsPerUnit, 0, x*self.horizontalPixelsPerUnit, y*height_scaling) for x, y in enumerate(dataset)] painter.setPen(QPen(pen_color, self.lineThickness)) painter.drawLines(lines) else: painter.translate(0, +self.lineThickness/2 - 1) if self.smoothEnvelope and self.smoothFactor > 0: min_value = 0 max_value = graph_height * height_scaling cx_offset = self.horizontalPixelsPerUnit / 3.0 smoothness = self.smoothFactor last_values = deque(3*[next(dataset) * height_scaling], maxlen=3) # last 3 values: 0 last, 1 previous, 2 previous previous envelope = QPainterPath() envelope.moveTo(0, last_values[0]) for x, y in enumerate(dataset, 1): x = x * self.horizontalPixelsPerUnit y = y * height_scaling * (1 - smoothness) + last_values[0] * smoothness last_values.appendleft(y) c1x = x - cx_offset * 2 c2x = x - cx_offset c1y = limit((1 + smoothness) * last_values[1] - smoothness * last_values[2], min_value, max_value) # same gradient as previous previous value to previous value c2y = limit((1 - smoothness) * last_values[0] + smoothness * last_values[1], min_value, max_value) # same gradient as previous value to last value envelope.cubicTo(c1x, c1y, c2x, c2y, x, y) else: envelope = QPainterPath() envelope.addPolygon(QPolygonF([QPointF(x*self.horizontalPixelsPerUnit, y*height_scaling) for x, y in enumerate(dataset)])) if self.fillEnvelope or graph.fill_envelope: first_element = envelope.elementAt(0) last_element = envelope.elementAt(envelope.elementCount() - 1) fill_path = QPainterPath() fill_path.moveTo(last_element.x, last_element.y) fill_path.lineTo(last_element.x + 1, last_element.y) fill_path.lineTo(last_element.x + 1, -self.lineThickness) fill_path.lineTo(-self.lineThickness, -self.lineThickness) fill_path.lineTo(-self.lineThickness, first_element.y) fill_path.connectPath(envelope) painter.fillPath(fill_path, brush_color) painter.strokePath(envelope, QPen(pen_color, self.lineThickness, join=Qt.RoundJoin)) painter.translate(0, -self.lineThickness/2 + 1) if self.boundary is not None and self.boundaryColor: painter.setRenderHint(QStylePainter.Antialiasing, False) painter.setPen(QPen(self.boundaryColor, 1.0)) painter.drawLine(0, self.boundary*height_scaling, contents_rect.width(), self.boundary*height_scaling) painter.restore() # queue the 'updated' signal to be emitted after returning to the main loop QMetaObject.invokeMethod(self, 'updated', Qt.QueuedConnection)
def hash_path(path: QPainterPath) -> int: h = 35891237 ^ path.fillRule() for i in range(path.elementCount()): el = path.elementAt(i) h ^= hash((el.type, el.x, el.y)) return h
class ViewFisheye(QWidget): # sample selection SelectionType = Enum('SelectType', 'Exact Closest Rect') SelectionMode = Enum('SelectMode', 'Select Add Remove') SelectionRectMin = 10 # pixels, width and height, scales as photo scales SampleRadius = 10 # pixels, scales as photo scales SelectedPixelBox = 64 # pixels, width and height def __init__(self, parent): super().__init__() # members self.parent = parent self.myPhoto = QImage() self.myPhotoPixels = np.zeros(shape=(1, 1, 4)) self.myPhotoPath = "" self.myPhotoTime = datetime(1,1,1) self.myPhotoSrcRect = QRect() self.myPhotoDestRect = QRect() self.myPhotoRadius = 0 self.myPhotoRotation = 0 self.rawAvailable = False self.coordsMouse = (0, 0) self.viewCenter = (0, 0) self.dragSelectRect = QRect(0, 0, 0, 0) self.sunPosition = (0, 0) # (azimuth (theta), altitude (phi)(90-zenith)) self.sunPositionVisible = (0,0) # point (x,y) of sun location rendered on screen (scaled) self.sunPathPoints = [] # [(azimuth (theta), altitude (phi)(90-zenith), datetime)] self.compassTicks = [] # [[x1, y1, x2, y2, x1lbl, y1lbl, angle]] self.lensIdealRadii = [] # list of radii for ideal lens latitudes to draw self.lensRealRadii = [] # list of radii for real/warped lens latitudes to draw self.samplePoints = [] # (x,y) coords of all samples on the photo rendered on screen (scaled) self.sampleAreaVisible = [] # area of 4 points for each sample rendered on screen (scaled) self.samplePointsInFile = [] # points (x,y) of all samples in the photo on file self.samplesSelected = [] # indices of selected samples self.skyCover = common.SkyCover.UNK # members - preloaded graphics self.painter = QPainter() self.mask = QImage() self.pathSun = QPainterPath() self.penText = QPen(Qt.white, 1, Qt.SolidLine) self.penLens = QPen(Qt.magenta, 1, Qt.SolidLine) self.penSun = QPen(QColor(255, 165, 0), 2, Qt.SolidLine) self.penSelected = [] # list of pens, one for each sampling pattern location self.penSelectRect = QPen(Qt.white, 1, Qt.DashLine) self.penShadowText = QPen(Qt.black, 1, Qt.SolidLine) self.penShadowSun = QPen(Qt.black, 2, Qt.SolidLine) self.penShadowSelected = QPen(Qt.black, 3, Qt.SolidLine) self.brushGrid = QBrush(Qt.white, Qt.SolidPattern) self.fontFixed = QFont('Courier New', 8) self.fontScaled = QFont('Courier New', 8) self.fontMetrics = QFontMetrics(self.fontScaled) self.iconWarning = self.style().standardIcon(QStyle.SP_MessageBoxWarning).pixmap(ViewFisheye.SelectedPixelBox / 2) def dataLoaded(self): # Note - this function only runs once the data directory has been loaded self.setMouseTracking(True) color = QColor(255, 255, 255) self.samplesSelected.clear() self.samplePoints.clear() self.sampleAreaVisible.clear() self.samplePointsInFile.clear() self.penSelected.clear() for t, p in common.SamplingPattern: self.samplePoints.append((0, 0)) # these will need to be recomputed as photo scales self.samplePointsInFile.append((0, 0)) # these only need to be computed once per photo self.sampleAreaVisible.append([]) color.setHsv(t, int(utility.normalize(p, 0, 90) * 127 + 128), 255) self.penSelected.append(QPen(color, 3, Qt.SolidLine)) def setPhoto(self, path, exif=None): # if photo is valid if path is not None and os.path.exists(path): self.myPhotoPath = path self.myPhoto = QImage(path) self.myPhotoSrcRect = QRect(0, 0, self.myPhoto.width(), self.myPhoto.height()) self.myPhotoDestRect = QRect(0, 0, self.width(), self.height()) self.rawAvailable = utility_data.isHDRRawAvailable(path) if exif is not None: self.myPhotoTime = datetime.strptime(str(exif["EXIF DateTimeOriginal"]), '%Y:%m:%d %H:%M:%S') else: self.myPhotoTime = utility_data.imageEXIFDateTime(path) # cache each sample's coordinate in the photo # note: technically doesn't need to be recalculated if all photos have same resolution! self.samplePointsInFile = utility_data.computePointsInImage(path, common.SamplingPattern) # keep a copy the image's pixels in memory (used later for exporting, etc.) ptr = self.myPhoto.bits() ptr.setsize(self.myPhoto.byteCount()) pixbgr = np.asarray(ptr).reshape(self.myPhoto.height(), self.myPhoto.width(), 4) # HACKAROONIE: byte order is not the same as image format, so swapped it around :/ # TODO: should handle this better self.myPhotoPixels = np.copy(pixbgr) red = np.copy(self.myPhotoPixels[:, :, 0]) self.myPhotoPixels[:, :, 0] = self.myPhotoPixels[:, :, 2] self.myPhotoPixels[:, :, 2] = red # rgba = self.myPhoto.pixelColor(center[0], center[1]) # print((rgba.red(), rgba.green(), rgba.blue())) # rgba = pixrgb[center[1], center[0]] # print(rgba) # photo is null or missing else: self.myPhoto = QImage() self.myPhotoPixels = np.zeros(shape=(1,1,4)) self.myPhotoPath = "" self.myPhotoTime = datetime(1, 1, 1) self.myPhotoSrcRect = QRect() self.myPhotoDestRect = QRect() self.rawAvailable = False # precompute as much as we can before any drawing self.computeBounds() def setSunPath(self, sunpath): self.sunPathPoints = sunpath def setSunPosition(self, pos): self.sunPosition = pos def setSkycover(self, sc): self.skyCover = sc def getSamplePatternRGB(self, index): if index < 0 or index >= len(common.SamplingPattern): return (0,0,0) color = self.penSelected[index].color() return (color.red(), color.green(), color.blue()) def resetRotation(self, angles=0): self.myPhotoRotation = angles def selectSamples(self, message="none"): # nothing to do if no photo loaded if self.myPhoto.isNull(): return # handle selection message if message == "none": self.samplesSelected.clear() elif message == "all": self.samplesSelected[:] = [i for i in range(0, len(common.SamplingPattern))] elif message == "inverse": allidx = set([i for i in range(0, len(common.SamplingPattern))]) selidx = set(self.samplesSelected) self.samplesSelected[:] = list(allidx - selidx) # remove samples in circumsolar avoidance region if necessary sunAvoid = common.AppSettings["AvoidSunAngle"] if sunAvoid > 0: sunAvoidRads = math.radians(common.AppSettings["AvoidSunAngle"]) sunPosRads = (math.radians(self.sunPosition[0]), math.radians(self.sunPosition[1])) self.samplesSelected[:] = [idx for idx in self.samplesSelected if utility_angles.CentralAngle(sunPosRads, common.SamplingPatternRads[idx], inRadians=True) > sunAvoidRads] # update self.repaint() self.parent.graphSamples(self.samplesSelected) def mouseMoveEvent(self, event): # nothing to do if no photo loaded if self.myPhoto.isNull(): return # detect primary mouse button drag for sample selection if event.buttons() == Qt.LeftButton: # update drag selection bounds self.dragSelectRect.setWidth(event.x() - self.dragSelectRect.x()) self.dragSelectRect.setHeight(event.y() - self.dragSelectRect.y()) # detect middle mouse button drag for image rotation elif (event.buttons() == Qt.MidButton): old = (self.coordsMouse[0] - self.viewCenter[0], self.coordsMouse[1] - self.viewCenter[1]) new = (event.x() - self.viewCenter[0], event.y() - self.viewCenter[1]) # clockwise drag decreases rotation if old[1]*new[0] < old[0]*new[1]: self.myPhotoRotation -= 1 # counter-clockwise drag increases rotation else: self.myPhotoRotation += 1 # rotation if self.myPhotoRotation >= 0: self.myPhotoRotation %= 360 else: self.myPhotoRotation %= -360 # lastly, cache mouse coordinates and update self.coordsMouse = (event.x(), event.y()) self.repaint() def mousePressEvent(self, event): # nothing to do if no photo loaded if self.myPhoto.isNull(): return # we only care about a left click for point and drag selection # right click is for context menu - handled elsewhere # middle click is for rotation - handled elsewhere if event.buttons() != Qt.LeftButton: return # start logging drag selection (whether user drags or not) self.dragSelectRect.setX(event.x()) self.dragSelectRect.setY(event.y()) self.dragSelectRect.setWidth(0) self.dragSelectRect.setHeight(0) def mouseReleaseEvent(self, event): # nothing to do if no photo loaded if self.myPhoto.isNull(): return # detect primary mouse button release for stopping sample selection if event.button() == Qt.LeftButton: # read modifier keys for user desired selection mode mode = ViewFisheye.SelectionMode.Select if event.modifiers() == Qt.ControlModifier: mode = ViewFisheye.SelectionMode.Add elif event.modifiers() == Qt.ShiftModifier: mode = ViewFisheye.SelectionMode.Remove # unflip coordinates of rect so that width and height are always positive r = self.dragSelectRect r = utility.rectForwardFacing([r.x(), r.y(), r.right(), r.bottom()]) self.dragSelectRect.setCoords(r[0], r[1], r[2], r[3]) # select samples prevSelected = list(self.samplesSelected) if self.dragSelectRect.width() < ViewFisheye.SelectionRectMin and self.dragSelectRect.height() < ViewFisheye.SelectionRectMin: self.computeSelectedSamples(ViewFisheye.SelectionType.Closest, mode) else: self.computeSelectedSamples(ViewFisheye.SelectionType.Rect, mode) # reset drag selection self.dragSelectRect.setX(event.x()) self.dragSelectRect.setY(event.y()) self.dragSelectRect.setWidth(0) self.dragSelectRect.setHeight(0) # update self.repaint() if self.samplesSelected != prevSelected: self.parent.graphSamples(self.samplesSelected) def wheelEvent(self, event): # nothing to do if no photo loaded if self.myPhoto.isNull(): return self.parent.timeChangeWheelEvent(event) def leaveEvent(self, event): self.coordsMouse = (-1, -1) self.repaint() def resizeEvent(self, event): self.computeBounds() def contextMenuEvent(self, event): # nothing to do if no photo loaded if self.myPhoto.isNull(): return self.parent.triggerContextMenu(self, event) def computeSelectedSamples(self, type, mode): px = 0 py = 0 x1 = 0 y1 = 0 x2 = 0 y2 = 0 # in select mode, clear current selection if mode == ViewFisheye.SelectionMode.Select: self.samplesSelected = [] # these are the samples we will be adding or removing sampleAdjustments = [] # which single sample did user select by point if type == ViewFisheye.SelectionType.Exact: px = self.coordsMouse[0] py = self.coordsMouse[1] for i in range(0, len(self.samplePoints)): x, y = self.samplePoints[i] x1 = x - ViewFisheye.SampleRadius y1 = y - ViewFisheye.SampleRadius x2 = x + ViewFisheye.SampleRadius y2 = y + ViewFisheye.SampleRadius if px >= x1 and px <= x2 and py >= y1 and py <= y2: sampleAdjustments.append(i) break # which single sample is the closest to the mouse coordinate elif type == ViewFisheye.SelectionType.Closest: px = self.coordsMouse[0] py = self.coordsMouse[1] dist = math.sqrt((py-self.viewCenter[1])*(py-self.viewCenter[1]) + (px-self.viewCenter[0])*(px-self.viewCenter[0])) if dist <= self.myPhotoRadius: close = math.inf closest = -1 for i in range(0, len(self.samplePoints)): x, y = self.samplePoints[i] dist = math.sqrt((y-py)*(y-py) + (x-px)*(x-px)) if dist < close: close = dist closest = i if closest >= 0: sampleAdjustments.append(closest) # which samples are in the drag selection rect elif type == ViewFisheye.SelectionType.Rect: x1 = self.dragSelectRect.x() y1 = self.dragSelectRect.y() x2 = self.dragSelectRect.x() + self.dragSelectRect.width() y2 = self.dragSelectRect.y() + self.dragSelectRect.height() for i in range(0, len(self.samplePoints)): x, y = self.samplePoints[i] if x >= x1 and x <= x2 and y >= y1 and y <= y2: sampleAdjustments.append(i) # remove samples in circumsolar avoidance region sunAvoid = common.AppSettings["AvoidSunAngle"] if sunAvoid > 0: sunAvoidRads = math.radians(common.AppSettings["AvoidSunAngle"]) sunPosRads = (math.radians(self.sunPosition[0]), math.radians(self.sunPosition[1])) sampleAdjustments[:] = [idx for idx in sampleAdjustments if utility_angles.CentralAngle(sunPosRads, common.SamplingPatternRads[idx], inRadians=True) > sunAvoidRads] # no changes to be made if len(sampleAdjustments) <= 0: return # finally modify sample selection and return difference if mode == ViewFisheye.SelectionMode.Select or mode == ViewFisheye.SelectionMode.Add: for i in range(0, len(sampleAdjustments)): if sampleAdjustments[i] not in self.samplesSelected: # don't readd existing indices self.samplesSelected.append(sampleAdjustments[i]) elif mode == ViewFisheye.SelectionMode.Remove: for i in range(0, len(sampleAdjustments)): try: self.samplesSelected.remove(sampleAdjustments[i]) except: pass # ignore trying to remove indices that aren't currently selected # sort selection for easier searching later self.samplesSelected.sort() def computeBounds(self): if self.myPhoto.isNull(): self.myPhotoDestRect = QRect(0, 0, self.width(), self.height()) self.viewCenter = (self.width() / 2, self.height() / 2) self.myPhotoRadius = 0 self.myPhotoDiameter = 0 for i in range(0, len(common.SamplingPattern)): self.samplePoints[i] = (0, 0) self.sampleAreaVisible[i] = [] return # scale photo destination rect to fit photo on screen # scale by the scaling factor that requires the most scaling ( - 2 to fit in border ) wRatio = self.width() / self.myPhoto.width() hRatio = self.height() / self.myPhoto.height() if wRatio <= hRatio: self.myPhotoDestRect.setWidth(self.myPhotoSrcRect.width() * wRatio - 2) self.myPhotoDestRect.setHeight(self.myPhotoSrcRect.height() * wRatio - 2) else: self.myPhotoDestRect.setWidth(self.myPhotoSrcRect.width() * hRatio - 2) self.myPhotoDestRect.setHeight(self.myPhotoSrcRect.height() * hRatio - 2) # center the photo dest rect self.myPhotoDestRect.moveTo(self.width() / 2 - self.myPhotoDestRect.width() / 2, self.height() / 2 - self.myPhotoDestRect.height() / 2) # NOTE - THESE ARE THE MOST IMPORTANT COMPUTATIONS FROM WHICH EVERYTHING ELSE IS PLOTTED self.viewCenter = (self.width() / 2, self.height() / 2) self.myPhotoRadius = self.myPhotoDestRect.height() / 2 self.myPhotoDiameter = self.myPhotoRadius * 2 self.myPhotoTopLeft = ((self.viewCenter[0] - self.myPhotoRadius), (self.viewCenter[1] - self.myPhotoRadius)) # compute new scaled font size self.fontScaled = QFont('Courier New', self.myPhotoRadius * (1/(101-common.AppSettings["HUDTextScale"]))) self.fontMetrics = QFontMetrics(self.fontScaled) # compute sampling pattern collision bounds ViewFisheye.SampleRadius = self.myPhotoRadius / 50 hFOV = common.DataConfig["RadianceFOV"] / 2 for i in range(0, len(common.SamplingPattern)): # compute sample bounds u, v = utility_angles.SkyCoord2FisheyeUV(common.SamplingPattern[i][0], common.SamplingPattern[i][1]) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) y = self.myPhotoTopLeft[1] + (v * self.myPhotoDiameter) self.samplePoints[i] = (x, y) # compute sampling pattern actual sampling areas (projected differential angle area) p1 = utility_angles.SkyCoord2FisheyeUV(common.SamplingPattern[i][0] - hFOV, common.SamplingPattern[i][1] - hFOV) p2 = utility_angles.SkyCoord2FisheyeUV(common.SamplingPattern[i][0] - hFOV, common.SamplingPattern[i][1] + hFOV) p3 = utility_angles.SkyCoord2FisheyeUV(common.SamplingPattern[i][0] + hFOV, common.SamplingPattern[i][1] + hFOV) p4 = utility_angles.SkyCoord2FisheyeUV(common.SamplingPattern[i][0] + hFOV, common.SamplingPattern[i][1] - hFOV) p1 = QPoint(self.myPhotoTopLeft[0] + (p1[0] * self.myPhotoDiameter), self.myPhotoTopLeft[1] + (p1[1] * self.myPhotoDiameter)) p2 = QPoint(self.myPhotoTopLeft[0] + (p2[0] * self.myPhotoDiameter), self.myPhotoTopLeft[1] + (p2[1] * self.myPhotoDiameter)) p3 = QPoint(self.myPhotoTopLeft[0] + (p3[0] * self.myPhotoDiameter), self.myPhotoTopLeft[1] + (p3[1] * self.myPhotoDiameter)) p4 = QPoint(self.myPhotoTopLeft[0] + (p4[0] * self.myPhotoDiameter), self.myPhotoTopLeft[1] + (p4[1] * self.myPhotoDiameter)) self.sampleAreaVisible[i] = [p1, p2, p3, p4] # compute compass lines self.compassTicks.clear() tickLength = self.myPhotoRadius / 90 for angle in range(0, 360, 10): theta = 360 - ((angle + 270) % 360) # angles eastward from North, North facing down rads = theta * math.pi / 180.0 cx1 = (math.cos(rads) * (self.myPhotoRadius - tickLength)) + self.viewCenter[0] cy1 = (math.sin(rads) * (self.myPhotoRadius - tickLength)) + self.viewCenter[1] cx2 = (math.cos(rads) * self.myPhotoRadius) + self.viewCenter[0] cy2 = (math.sin(rads) * self.myPhotoRadius) + self.viewCenter[1] lx1 = (math.cos(rads) * (self.myPhotoRadius - tickLength*4)) + self.viewCenter[0] - self.fontMetrics.width(str(angle))/2 ly1 = (math.sin(rads) * (self.myPhotoRadius - tickLength*4)) + self.viewCenter[1] - self.fontMetrics.height()/2 self.compassTicks.append([cx1, cy1, cx2, cy2, lx1, ly1, angle]) # x1, y1, x2, y2, x1lbl, y1lbl, angle # compute new grid for debugging coordinates griddivs = 5 gridwidth = int(round(self.myPhotoDiameter / griddivs)) self.gridpoints = [] self.gridUVs = [] self.gridskycoords = [] for r in range(1, griddivs): for c in range(1, griddivs): point = (self.myPhotoTopLeft[0] + (c * gridwidth), self.myPhotoTopLeft[1] + (r * gridwidth)) self.gridpoints.append(point) u = (point[0] - self.myPhotoTopLeft[0]) / self.myPhotoDiameter v = (point[1] - self.myPhotoTopLeft[1]) / self.myPhotoDiameter self.gridUVs.append((u, v)) t, p = utility_angles.FisheyeUV2SkyCoord(u, v) self.gridskycoords.append((t, p)) # compute lens (ideal and actual) radii for drawn latitude ellipses along zenith self.lensIdealRadii.clear() self.lensRealRadii.clear() for alt in common.SamplingPatternAlts: # ideal lens u, v = utility_angles.SkyCoord2FisheyeUV(90, alt, lenswarp=False) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) r = x - self.viewCenter[0] self.lensIdealRadii.append((r, alt)) # (radius, altitude) # warped lens u, v = utility_angles.SkyCoord2FisheyeUV(90, alt) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) r = x - self.viewCenter[0] self.lensRealRadii.append((r, alt)) # (radius, altitude) # compute sun path screen points self.pathSun = QPainterPath() if len(self.sunPathPoints) > 0: azi, alt, dt = self.sunPathPoints[0] u, v = utility_angles.SkyCoord2FisheyeUV(azi, alt) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) y = self.myPhotoTopLeft[1] + (v * self.myPhotoDiameter) self.pathSun.moveTo(x, y) for i in range(1, len(self.sunPathPoints)): azi, alt, dt = self.sunPathPoints[i] u, v = utility_angles.SkyCoord2FisheyeUV(azi, alt) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) y = self.myPhotoTopLeft[1] + (v * self.myPhotoDiameter) self.pathSun.lineTo(x, y) # compute sun position screen point u, v = utility_angles.SkyCoord2FisheyeUV(self.sunPosition[0], self.sunPosition[1]) x = self.myPhotoTopLeft[0] + (u * self.myPhotoDiameter) y = self.myPhotoTopLeft[1] + (v * self.myPhotoDiameter) self.sunPositionVisible = (x, y) # compute new mask self.mask = QPixmap(self.width(), self.height()).toImage() def paintEvent(self, event): super().paintEvent(event) painter = QPainter() painter.begin(self) # background brushBG = QBrush(Qt.black, Qt.SolidPattern) if not common.AppSettings["ShowMask"]: brushBG.setColor(Qt.darkGray) brushBG.setStyle(Qt.Dense1Pattern) painter.setBackground(Qt.gray) else: brushBG.setColor(Qt.black) brushBG.setStyle(Qt.SolidPattern) painter.setBackground(Qt.black) painter.setBackgroundMode(Qt.OpaqueMode) painter.setBrush(brushBG) painter.setPen(Qt.NoPen) painter.drawRect(0, 0, self.width(), self.height()) # draw photo if not self.myPhoto.isNull(): # rotate and draw photo as specified by user transform = QTransform() transform.translate(self.myPhotoDestRect.center().x(), self.myPhotoDestRect.center().y()) transform.rotate(-self.myPhotoRotation) transform.translate(-self.myPhotoDestRect.center().x(), -self.myPhotoDestRect.center().y()) painter.setTransform(transform) painter.drawImage(self.myPhotoDestRect, self.myPhoto, self.myPhotoSrcRect) # draw it painter.resetTransform() # useful local vars centerPoint = QPoint(self.viewCenter[0], self.viewCenter[1]) destRect = QRect(0, 0, self.myPhotoDestRect.width(), self.myPhotoDestRect.height()) fontWidth = self.fontMetrics.width("X") # mask if common.AppSettings["ShowMask"]: maskPainter = QPainter() maskPainter.begin(self.mask) maskPainter.setBrush(QBrush(Qt.magenta, Qt.SolidPattern)) maskPainter.drawEllipse(self.viewCenter[0] - self.myPhotoRadius, self.viewCenter[1] - self.myPhotoRadius, self.myPhotoDiameter, self.myPhotoDiameter) maskPainter.end() painter.setCompositionMode(QPainter.CompositionMode_DestinationIn) painter.drawImage(0, 0, self.mask) painter.setCompositionMode(QPainter.CompositionMode_SourceOver) # HUD if common.AppSettings["ShowHUD"]: painter.setBackgroundMode(Qt.TransparentMode) #painter.setBackground(Qt.black) painter.setBrush(Qt.NoBrush) painter.setFont(self.fontScaled) # draw UV grid if common.AppSettings["ShowUVGrid"]: painter.setPen(self.penText) # box tl = self.myPhotoTopLeft tr = (self.viewCenter[0] + self.myPhotoRadius, self.viewCenter[1] - self.myPhotoRadius) bl = (self.viewCenter[0] - self.myPhotoRadius, self.viewCenter[1] + self.myPhotoRadius) br = (self.viewCenter[0] + self.myPhotoRadius, self.viewCenter[1] + self.myPhotoRadius) painter.drawLine(tl[0], tl[1], tr[0], tr[1]) painter.drawLine(bl[0], bl[1], br[0], br[1]) painter.drawLine(tl[0], tl[1], bl[0], bl[1]) painter.drawLine(tr[0], tr[1], br[0], br[1]) # crosshairs painter.drawLine(tl[0], self.viewCenter[1], tr[0], self.viewCenter[1]) painter.drawLine(self.viewCenter[0], tr[1], self.viewCenter[0], br[1]) # labels destRect.setCoords(tl[0] + 4, tl[1] + 4, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "0") destRect.setCoords(tr[0] - (fontWidth+4), tr[1] + 4, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "1") destRect.setCoords(bl[0] + 3, bl[1] - (self.fontMetrics.height()+3), self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "1") destRect.setCoords(br[0] - (fontWidth+3), br[1] - (self.fontMetrics.height()+3), self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "1") # grid coordinates gpntrad = self.myPhotoRadius * 0.005 painter.setPen(self.penText) painter.setBrush(self.brushGrid) painter.setFont(self.fontScaled) for i in range(0, len(self.gridpoints)): point = self.gridpoints[i] u, v = self.gridUVs[i] t, p = self.gridskycoords[i] painter.drawEllipse(QPoint(point[0], point[1]), gpntrad, gpntrad) destRect.setCoords(point[0]+fontWidth/2, point[1]-self.fontMetrics.height(), self.width(), self.height()) textuv = "{0:.1f}u, {1:.1f}v".format(round(u,1), round(v,1)) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, textuv) destRect.setCoords(point[0]+fontWidth/2, point[1], self.width(), self.height()) textuv = "{0:d}°, {1:d}°".format(int(round(t)), int(round(p))) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, textuv) painter.setBrush(Qt.NoBrush) # draw lens warp if common.AppSettings["ShowLensWarp"]: # ideal lens longitudes along azimuth painter.setPen(self.penText) for i in range(0, int(len(self.compassTicks)/2), 3): p1 = QPoint(self.compassTicks[i][2], self.compassTicks[i][3]) p2 = QPoint(self.compassTicks[i+18][2], self.compassTicks[i+18][3]) # tick opposite 180 degrees painter.drawLine(p1, p2) # ideal lens latitudes along zenith for r, alt in self.lensIdealRadii: painter.drawEllipse(centerPoint, r, r) # actual/warped lens latitudes along zenith painter.setPen(self.penLens) for r, alt in self.lensRealRadii: painter.drawEllipse(centerPoint, r, r) destRect.setCoords(self.viewCenter[0] + r + 3, self.viewCenter[1] - (self.fontMetrics.height() + 3), self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "{0:d}°".format(int(alt))) # draw compass if common.AppSettings["ShowCompass"]: # compass ticks text shadows if common.AppSettings["ShowShadows"]: painter.setPen(self.penShadowText) for tick in self.compassTicks: destRect.setCoords(tick[4] + 1, tick[5] + 1, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, str(tick[6])+"°") # compass ticks text painter.setPen(self.penText) for tick in self.compassTicks: painter.drawLine(tick[0], tick[1], tick[2], tick[3]) destRect.setCoords(tick[4], tick[5], self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, str(tick[6])+"°") # photo radius #painter.drawEllipse(self.viewCenter[0] - self.myPhotoRadius, self.viewCenter[1] - self.myPhotoRadius, self.myPhotoDiameter, self.myPhotoDiameter) painter.drawEllipse(centerPoint, self.myPhotoRadius, self.myPhotoRadius) # cardinal directions destRect.setCoords(self.viewCenter[0] - self.myPhotoRadius - (fontWidth+4), self.viewCenter[1] - self.fontMetrics.height()/2, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "W") destRect.setCoords(self.viewCenter[0] + self.myPhotoRadius + 4, self.viewCenter[1] - self.fontMetrics.height()/2, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "E") destRect.setCoords(self.viewCenter[0] - fontWidth/2, self.viewCenter[1] - self.myPhotoRadius - (self.fontMetrics.height()+3), self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "S") destRect.setCoords(self.viewCenter[0] - fontWidth/2, self.viewCenter[1] + self.myPhotoRadius + 3, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "N") # draw sampling pattern if common.AppSettings["ShowSamples"]: painter.setPen(self.penText) for i, points in enumerate(self.sampleAreaVisible): painter.drawLine(QLine(points[0], points[1])) painter.drawLine(QLine(points[1], points[2])) painter.drawLine(QLine(points[2], points[3])) painter.drawLine(QLine(points[3], points[0])) for i in range(0, len(self.samplePoints)): p = self.samplePoints[i] painter.drawEllipse(QPoint(p[0],p[1]), ViewFisheye.SampleRadius, ViewFisheye.SampleRadius) painter.drawText(p[0] + ViewFisheye.SampleRadius, p[1], str(i)) # draw sun path if common.AppSettings["ShowSunPath"]: sunradius = self.myPhotoRadius * 0.1 # shadows painter.setPen(self.penShadowSun) if common.AppSettings["ShowShadows"]: painter.drawEllipse(QPoint(self.sunPositionVisible[0]+1, self.sunPositionVisible[1]+1), sunradius, sunradius) self.pathSun.translate(1.0, 1.0) painter.drawPath(self.pathSun) self.pathSun.translate(-1.0, -1.0) for i in range(0, self.pathSun.elementCount()): e = self.pathSun.elementAt(i) destRect.setCoords(e.x, e.y + self.fontMetrics.height()/2 + 1, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, str(self.sunPathPoints[i][2].hour)) # sun, path, hours painter.setPen(self.penSun) painter.drawEllipse(QPoint(self.sunPositionVisible[0], self.sunPositionVisible[1]), sunradius, sunradius) painter.drawPath(self.pathSun) for i in range(0, self.pathSun.elementCount()): e = self.pathSun.elementAt(i) destRect.setCoords(e.x, e.y + self.fontMetrics.height() / 2, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, str(self.sunPathPoints[i][2].hour)) # draw selected samples (ALWAYS) r = QRect() # shadows if common.AppSettings["ShowShadows"]: painter.setPen(self.penShadowSelected) for i in self.samplesSelected: x, y = self.samplePoints[i] painter.drawEllipse(QPoint(x+1, y+1), ViewFisheye.SampleRadius, ViewFisheye.SampleRadius) # samples for i in self.samplesSelected: painter.setPen(self.penSelected[i]) x, y = self.samplePoints[i] painter.drawEllipse(QPoint(x, y), ViewFisheye.SampleRadius, ViewFisheye.SampleRadius) # draw user's selection bounds if (abs(self.dragSelectRect.right()-self.dragSelectRect.left()) >= ViewFisheye.SelectionRectMin and abs(self.dragSelectRect.bottom()-self.dragSelectRect.top()) >= ViewFisheye.SelectionRectMin): painter.setPen(self.penSelectRect) painter.drawRect(self.dragSelectRect) # draw timestamp painter.setPen(self.penText) painter.setFont(self.fontFixed) destRect.setCoords(10, 10, self.width() / 2, 50) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, str(self.myPhotoTime)) # draw sky cover assessment destRect.setCoords(10, 25, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, self.skyCover.name + "/" + common.SkyCoverDesc[self.skyCover]) # draw photo rotation if self.myPhotoRotation != 0: destRect.setCoords(10, self.height()-25, self.width(), self.height()) painter.drawText(destRect, Qt.AlignTop | Qt.AlignLeft, "Rotation: " + str(self.myPhotoRotation) + "°") # where is the mouse relative to the center? # this is used as an optimization to only display information when mouse is in fisheye portion dx = self.coordsMouse[0] - self.viewCenter[0] dy = self.coordsMouse[1] - self.viewCenter[1] distance = math.sqrt((dx * dx) + (dy * dy)) # distance from mouse to view center # coordinates we are interested in #self.coordsMouse # x,y of this widget coordsxy = (-1, -1) # x,y over photo as scaled/rendered on this widget coordsXY = (-1, -1) # x,y over actual original photo on disk coordsUV = (-1, -1) # u,v coords of fisheye portion of photo w/ 0,0 top left and 1,1 bottom right coordsTP = (-1, -1) # theta,phi polar coordinates # text textxy = "-1, -1 xy" textXY = "-1, -1 xy" textUV = "-1, -1 uv" textTP = "-1, -1 θφ" textPX = "0 0 0 px" # compute all relevant information only when mouse is within fisheye portion of photo if distance < self.myPhotoRadius: coordsxy = (self.coordsMouse[0] - self.myPhotoDestRect.x(), self.coordsMouse[1] - self.myPhotoDestRect.y()) coordsXY = (int(coordsxy[0] / self.myPhotoDestRect.width() * self.myPhoto.width()), int(coordsxy[1] / self.myPhotoDestRect.height() * self.myPhoto.height())) coordsUV = ((self.coordsMouse[0] - self.myPhotoTopLeft[0]) / self.myPhotoDiameter, (self.coordsMouse[1] - self.myPhotoTopLeft[1]) / self.myPhotoDiameter) coordsTP = utility_angles.FisheyeUV2SkyCoord(coordsUV[0], coordsUV[1]) # text textxy = str(coordsxy[0]) + ", " + str(coordsxy[1]) + " xy" textXY = str(coordsXY[0]) + ", " + str(coordsXY[1]) + " xy" textUV = "{:.2f}".format(coordsUV[0]) + ", " + "{:.2f}".format(coordsUV[1]) + " uv" textTP = "{:.2f}".format(coordsTP[0]) + ", " + "{:.2f}".format(coordsTP[1]) + " θφ" # pixels colors pixreg = common.AppSettings["PixelRegion"] colorsRegion = np.zeros((pixreg, pixreg, 4)) colorFinal = colorsRegion[0,0] # RGBA of pixel under mouse of photo on disk # colorFinal = self.myPhoto.pixelColor(coordsXY[0], coordsXY[1]) if distance < self.myPhotoRadius: halfdim = int(pixreg / 2) rstart = coordsXY[1]-halfdim rstop = coordsXY[1]+halfdim+1 cstart = coordsXY[0]-halfdim cstop = coordsXY[0]+halfdim+1 if (rstart >= 0 and rstop<=self.myPhotoPixels.shape[0] and cstart >= 0 and cstop<=self.myPhotoPixels.shape[1]): colorsRegion = self.myPhotoPixels[rstart:rstop, cstart:cstop] colorFinal = colorsRegion[halfdim, halfdim] if pixreg > 1: # with pixel weighting colorFinal = utility_data.collectPixels([coordsXY], [pixreg], pixels=self.myPhotoPixels, weighting=common.PixelWeighting(common.AppSettings["PixelWeighting"]))[0] textPX = str(colorFinal[0]) + " " + str(colorFinal[1]) + " " + str(colorFinal[2]) + " px" # draw HUD text strings # x,y coords destRect.setCoords(0, 0, self.width() - 10, self.height()- 124) painter.drawText(destRect, Qt.AlignBottom | Qt.AlignRight, textxy) # X,Y coords destRect.setCoords(0, 0, self.width() - 10, self.height() - 114) painter.drawText(destRect, Qt.AlignBottom | Qt.AlignRight, textXY) # u,v coords destRect.setCoords(0, 0, self.width() - 10, self.height() - 104) painter.drawText(destRect, Qt.AlignBottom | Qt.AlignRight, textUV) # t,p coords destRect.setCoords(0, 0, self.width() - 10, self.height() - 94) painter.drawText(destRect, Qt.AlignBottom | Qt.AlignRight, textTP) # pixel color destRect.setCoords(0, 0, self.width() - 10, self.height() - 84) painter.drawText(destRect, Qt.AlignBottom | Qt.AlignRight, textPX) # compute pixel visualization coordinates circleX = self.width() - 10 - ViewFisheye.SelectedPixelBox - 10 - ViewFisheye.SelectedPixelBox - 10 - ViewFisheye.SelectedPixelBox circleY = self.height() - 10 - ViewFisheye.SelectedPixelBox pixelsX = self.width() - 10 - ViewFisheye.SelectedPixelBox - 10 - ViewFisheye.SelectedPixelBox pixelsY = self.height() - 10 - ViewFisheye.SelectedPixelBox pixelsWeightedX = self.width() - ViewFisheye.SelectedPixelBox - 10 pixelsWeightedY = self.height() - 10 - ViewFisheye.SelectedPixelBox # draw pixel visualization - fills pixreg = common.AppSettings["PixelRegion"] if distance < self.myPhotoRadius: painter.setPen(Qt.NoPen) # pixel region pixdim = ViewFisheye.SelectedPixelBox / pixreg for row in range(0, pixreg): for col in range(0, pixreg): color = colorsRegion[row, col] color = QColor(color[0], color[1], color[2]) painter.setBrush(QBrush(color, Qt.SolidPattern)) painter.drawRect(pixelsX + (col * pixdim), pixelsY + (row * pixdim), math.ceil(pixdim), math.ceil(pixdim)) # final pixel color color = QColor(colorFinal[0], colorFinal[1], colorFinal[2]) painter.setBrush(QBrush(color, Qt.SolidPattern)) cx = circleX + (coordsUV[0] * ViewFisheye.SelectedPixelBox) cy = circleY + (coordsUV[1] * ViewFisheye.SelectedPixelBox) painter.drawEllipse(cx - 5, cy - 5, 10, 10) painter.drawRect(pixelsWeightedX, pixelsWeightedY, ViewFisheye.SelectedPixelBox, ViewFisheye.SelectedPixelBox) # draw pixel visualization - outlines painter.setPen(self.penText) painter.setBrush(Qt.NoBrush) painter.drawEllipse(circleX, circleY, ViewFisheye.SelectedPixelBox, ViewFisheye.SelectedPixelBox) painter.drawRect(pixelsX, pixelsY, ViewFisheye.SelectedPixelBox, ViewFisheye.SelectedPixelBox) painter.drawRect(pixelsWeightedX, pixelsWeightedY, ViewFisheye.SelectedPixelBox, ViewFisheye.SelectedPixelBox) # raw data missing indicator # if (not self.rawAvailable): # painter.drawPixmap(pixelX + ViewFisheye.SelectedPixelBox / 2, # pixelY + ViewFisheye.SelectedPixelBox / 2, # self.iconWarning) # end draw painter.end()
def paintEvent(self, event): option = QStyleOption() option.initFrom(self) contents_rect = self.style().subElementRect( QStyle.SE_FrameContents, option, self ) or self.contentsRect( ) # the SE_FrameContents rect is Null unless the stylesheet defines decorations if self.graphStyle == self.BarStyle: graph_width = self.__dict__['graph_width'] = int( ceil( float(contents_rect.width()) / self.horizontalPixelsPerUnit)) else: graph_width = self.__dict__['graph_width'] = int( ceil( float(contents_rect.width() - 1) / self.horizontalPixelsPerUnit) + 1) max_value = self.__dict__['max_value'] = max( chain([0], *(islice(reversed(graph.data), graph_width) for graph in self.graphs if graph.enabled))) if self.graphHeight == self.AutomaticHeight or self.graphHeight < 0: graph_height = self.__dict__['graph_height'] = max( self.scaler.get_height(max_value), self.minHeight) else: graph_height = self.__dict__['graph_height'] = max( self.graphHeight, self.minHeight) if self.graphStyle == self.BarStyle: height_scaling = float(contents_rect.height()) / graph_height else: height_scaling = float(contents_rect.height() - self.lineThickness) / graph_height painter = QStylePainter(self) painter.drawPrimitive(QStyle.PE_Widget, option) painter.setClipRect(contents_rect) painter.save() painter.translate(contents_rect.x() + contents_rect.width() - 1, contents_rect.y() + contents_rect.height() - 1) painter.scale(-1, -1) painter.setRenderHint(QStylePainter.Antialiasing, self.graphStyle != self.BarStyle) for graph in (graph for graph in self.graphs if graph.enabled and graph.data): if self.boundary is not None and 0 < self.boundary < graph_height: boundary_width = min(5.0 / height_scaling, self.boundary - 0, graph_height - self.boundary) pen_color = QLinearGradient( 0, (self.boundary - boundary_width) * height_scaling, 0, (self.boundary + boundary_width) * height_scaling) pen_color.setColorAt(0, graph.color) pen_color.setColorAt(1, graph.over_boundary_color) brush_color = QLinearGradient( 0, (self.boundary - boundary_width) * height_scaling, 0, (self.boundary + boundary_width) * height_scaling) brush_color.setColorAt( 0, self.color_with_alpha(graph.color, self.fillTransparency)) brush_color.setColorAt( 1, self.color_with_alpha(graph.over_boundary_color, self.fillTransparency)) else: pen_color = graph.color brush_color = self.color_with_alpha(graph.color, self.fillTransparency) dataset = islice(reversed(graph.data), graph_width) if self.graphStyle == self.BarStyle: lines = [ QLineF(x * self.horizontalPixelsPerUnit, 0, x * self.horizontalPixelsPerUnit, y * height_scaling) for x, y in enumerate(dataset) ] painter.setPen(QPen(pen_color, self.lineThickness)) painter.drawLines(lines) else: painter.translate(0, +self.lineThickness / 2 - 1) if self.smoothEnvelope and self.smoothFactor > 0: min_value = 0 max_value = graph_height * height_scaling cx_offset = self.horizontalPixelsPerUnit / 3.0 smoothness = self.smoothFactor last_values = deque( 3 * [next(dataset) * height_scaling], maxlen=3 ) # last 3 values: 0 last, 1 previous, 2 previous previous envelope = QPainterPath() envelope.moveTo(0, last_values[0]) for x, y in enumerate(dataset, 1): x = x * self.horizontalPixelsPerUnit y = y * height_scaling * ( 1 - smoothness) + last_values[0] * smoothness last_values.appendleft(y) c1x = x - cx_offset * 2 c2x = x - cx_offset c1y = limit( (1 + smoothness) * last_values[1] - smoothness * last_values[2], min_value, max_value ) # same gradient as previous previous value to previous value c2y = limit( (1 - smoothness) * last_values[0] + smoothness * last_values[1], min_value, max_value ) # same gradient as previous value to last value envelope.cubicTo(c1x, c1y, c2x, c2y, x, y) else: envelope = QPainterPath() envelope.addPolygon( QPolygonF([ QPointF(x * self.horizontalPixelsPerUnit, y * height_scaling) for x, y in enumerate(dataset) ])) if self.fillEnvelope or graph.fill_envelope: first_element = envelope.elementAt(0) last_element = envelope.elementAt(envelope.elementCount() - 1) fill_path = QPainterPath() fill_path.moveTo(last_element.x, last_element.y) fill_path.lineTo(last_element.x + 1, last_element.y) fill_path.lineTo(last_element.x + 1, -self.lineThickness) fill_path.lineTo(-self.lineThickness, -self.lineThickness) fill_path.lineTo(-self.lineThickness, first_element.y) fill_path.connectPath(envelope) painter.fillPath(fill_path, brush_color) painter.strokePath( envelope, QPen(pen_color, self.lineThickness, join=Qt.RoundJoin)) painter.translate(0, -self.lineThickness / 2 + 1) if self.boundary is not None and self.boundaryColor: painter.setRenderHint(QStylePainter.Antialiasing, False) painter.setPen(QPen(self.boundaryColor, 1.0)) painter.drawLine(0, self.boundary * height_scaling, contents_rect.width(), self.boundary * height_scaling) painter.restore() # queue the 'updated' signal to be emitted after returning to the main loop QMetaObject.invokeMethod(self, 'updated', Qt.QueuedConnection)
class Canvas(QWidget): def __init__(self, parent=None): super(Canvas, self).__init__(parent) self.is_enable_knee_control = False # 画像専用のレイヤであるかを制御する # 1度Trueになったら2度とFalseにならないことを意図する self.is_picture_canvas = False self.picture_file_name = "" self.image = QImage() # マウストラック有効化 self.setMouseTracking(True) # マウス移動で出る予測線とクリックして出る本線を描画するときに区別する self.is_line_prediction = False # イベント同士の競合を防ぐ self.event_Locker = False self.rounded_polygon = RoundedPolygon(10000) self.existing_paths = [] # 確定したパスを保存 self.recorded_points = [] # 確定した点を保存(実験の記録用) self.clicked_points = [] # 今描いている線の制御点を記録 self.cursor_position = QPointF() self.cursor_position_mousePressed = QPointF() self.knee_position = QPointF() self.knee_position_mousePressed = QPointF() self.current_drawing_mode = OperationMode.DRAWING_POINTS self.current_knee_operation_mode = OperationMode.NONE self.__line_color = [] self.current_line_color = QColor() self.nearest_path = QPainterPath() self.nearest_distance = 50.0 self.nearest_index = 0 self.is_dragging = False self.pen_width = 2 self.show() def set_experiment_controller(self, excontroller): self.experiment_controller = excontroller def mousePressEvent(self, event: QMouseEvent): if self.current_drawing_mode == OperationMode.DRAWING_POINTS: # 制御点の追加 if event.button() == Qt.LeftButton: self.clicked_points.append(event.pos()) # print(self.clickedPoints) # 直前の制御点の消去 if event.button() == Qt.RightButton: if len(self.clicked_points) > 0: self.clicked_points.pop() self.update() elif self.current_drawing_mode == OperationMode.MOVING_POINTS: if event.button() == Qt.LeftButton: self.is_dragging = True if self.is_enable_knee_control: self.recode_knee_and_cursor_position() self.cursor_position = event.pos() self.update() def mouseMoveEvent(self, event: QMouseEvent): self.experiment_controller.current_mouse_position = event.pos() self.experiment_controller.record_frame( self.current_drawing_mode, self.current_knee_operation_mode) if self.current_drawing_mode == OperationMode.DRAWING_POINTS: self.clicked_points.append(event.pos()) self.is_line_prediction = True self.update() elif self.current_drawing_mode == OperationMode.MOVING_POINTS: print(self.nearest_distance) self.cursor_position = event.pos() if self.is_dragging: self.move_point() self.update() def mouseReleaseEvent(self, event: QMouseEvent): self.is_dragging = False def paintEvent(self, event: QPaintEvent): # if not self.event_Locker: painter = QPainter(self) if self.is_picture_canvas: painter.drawImage(QRect(0, 0, 600, 600), self.image) else: # すでに確定されているパスの描画 if len(self.existing_paths) > 0: for i in range(len(self.existing_paths)): print("linecolor {}: {}".format( i, self.__line_color[i].hue())) painter.setPen(QPen(self.__line_color[i], self.pen_width)) painter.drawPath(self.existing_paths[i]) if self.current_drawing_mode == OperationMode.DRAWING_POINTS: # 現在描いているパスの描画 if len(self.clicked_points) > 3: painter.setPen( QPen(self.current_line_color, self.pen_width)) # print(self.clickedPoints) # クリックした点まで線を伸ばすため、終点を一時的にリストに入れている self.clicked_points.append( self.clicked_points[len(self.clicked_points) - 1]) painter_path = self.rounded_polygon.get_path( self.clicked_points) # 設置した点の描画 painter.setPen(Qt.black) for i in range(len(self.clicked_points)): painter.drawEllipse(self.clicked_points[i], 2, 2) painter.setPen( QPen(self.current_line_color, self.pen_width)) # 現在のマウス位置での予告線 if self.is_line_prediction: self.clicked_points.pop() self.is_line_prediction = False painter.drawPath(painter_path) self.clicked_points.pop() # 線が描けない時 else: # 現在のマウス位置での予告線 if self.is_line_prediction: painter.setPen(Qt.red) for i in range(len(self.clicked_points)): painter.drawEllipse(self.clicked_points[i], 2, 2) if not len(self.clicked_points) == 0: self.clicked_points.pop() self.is_line_prediction = False # 予告線でもない場合は単に点を書く else: for i in range(len(self.clicked_points)): painter.drawEllipse(self.clicked_points[i], 2, 2) # 制御点を移動するとき elif self.current_drawing_mode == OperationMode.MOVING_POINTS: # すでに確定されているパスの制御点の描画 self.nearest_distance = 50.0 painter.setPen(Qt.black) for path in self.existing_paths: for i in range(path.elementCount()): control_point = QPointF( path.elementAt(i).x, path.elementAt(i).y) painter.drawEllipse(control_point, 3, 3) # 現在のカーソル位置から最も近い点と、その点が属するpathを記録、更新 # if not self.is_dragging & self.is_enable_knee_control: distance = math.sqrt( (control_point.x() - self.cursor_position.x())**2 + (control_point.y() - self.cursor_position.y())**2) if distance < self.nearest_distance: self.nearest_distance = distance self.nearest_path = path self.nearest_index = i # 一定の距離未満かつ最も近い点を赤く描画 if self.nearest_distance < 20: painter.setPen(QPen(Qt.red, self.pen_width)) nearest_control_point = QPointF( self.nearest_path.elementAt(self.nearest_index).x, self.nearest_path.elementAt(self.nearest_index).y) painter.drawEllipse(nearest_control_point, 3, 3) def move_point(self): if self.is_enable_knee_control: if self.nearest_distance < 20 or self.is_dragging: self.nearest_path.setElementPositionAt( self.nearest_index, self.cursor_position.x(), self.cursor_position.y()) amount_of_change = QPointF( self.cursor_position.x() + (self.knee_position.x() - self.knee_position_mousePressed.x()), self.cursor_position.y() - (self.knee_position.y() - self.knee_position_mousePressed.y())) self.nearest_path.setElementPositionAt(self.nearest_index, amount_of_change.x(), amount_of_change.y()) else: if self.nearest_distance < 20: self.nearest_path.setElementPositionAt( self.nearest_index, self.cursor_position.x(), self.cursor_position.y()) def set_knee_position(self, x, y): self.knee_position.setX(x) self.knee_position.setY(y) if self.is_dragging: if self.current_drawing_mode == OperationMode.MOVING_POINTS: self.move_point() self.update() def set_line_color(self, color): self.current_line_color = color def recode_knee_and_cursor_position(self): self.knee_position_mousePressed.setX(self.knee_position.x()) self.knee_position_mousePressed.setY(self.knee_position.y()) self.cursor_position_mousePressed = self.cursor_position def fix_path(self): # パスを確定 if self.is_line_prediction and not len(self.clicked_points) == 0: self.clicked_points.pop() # クリックした点まで線を伸ばすため、終点をリストに入れている if len(self.clicked_points) > 0: self.clicked_points.append( self.clicked_points[len(self.clicked_points) - 1]) painter_path = self.rounded_polygon.get_path(self.clicked_points) # 線と色を記録 self.existing_paths.append(painter_path) self.__line_color.append(self.current_line_color) for i in range(len(self.__line_color)): print("{}, {}".format(i, self.__line_color[i].value())) self.clicked_points.pop() self.recorded_points.append(self.clicked_points) # 点をリセット self.clicked_points = [] self.update() def delete_last_path(self): if len(self.existing_paths) > 0: self.existing_paths.pop() self.__line_color.pop() self.update() def switch_visible(self, is_visible: bool): palette = self.palette() if is_visible: palette.setColor(QPalette.Background, QColor(255, 255, 255, 120)) else: palette.setColor(QPalette.Background, QColor(255, 255, 255, 255)) self.setPalette(palette) def operation_mode_changed(self, to_drawing: OperationMode, to_knee: OperationMode): self.current_drawing_mode = to_drawing self.current_knee_operation_mode = to_knee self.fix_path() def set_picture_file_name(self, picture_file_name: str): self.is_picture_canvas = True self.picture_file_name = picture_file_name self.update() def set_enable_knee_control(self, is_enable_knee_control): self.is_enable_knee_control = is_enable_knee_control def load_picture(self, image: QImage): self.image = image self.is_picture_canvas = True self.update()