def draw(self, p, rect): if not self._map(): return #self._setupPainter(p) zoomFactor = self.edgeOverlay._zoom #p.drawPolygon(self._qpointarray, True, s, e-s) r = p.clipRegion().boundingRect() bbox = BoundingBox((r.left() / zoomFactor - 0.5, r.top() / zoomFactor - 0.5), (r.right() / zoomFactor + 0.5, r.bottom() / zoomFactor + 0.5)) map = self._map() if self.colors: try: for edge in map.edgeIter(): edgeColor = self.colors[edge.label()] if edgeColor and bbox.intersects(edge.boundingBox()): p.setPen(QtGui.QPen(edgeColor, self.width)) p.drawPolyline(self._getZoomedEdge(edge)) except IndexError, e: print e #"IndexError: %d > %d (maxEdgeLabel: %d)!" % (
def __init__(self, scale=30, roi=None, offset=(0.5, 0.5)): """fe = FigExporter(90.0, BoundingBox((10, 10), (50, 50))) Initializes a FigExporter with the given scale and roi. A scale of 450 makes one pixel (source unit) 450 fig units large, which equals 1cm. Default is scale = 30. A roi is given as BoundingBox object (default None == no clipping) and represents a range in the original image (before scaling). An optional parameter offset (default: (0.5, 0.5)) is used to move all edges / points within the roi; this is intended to make e.g. (14.1, 10.0) be a little to the right of the *center* of the pixel (14, 10). The transformation parameters are stored as properties scale, roi, and offset, and these settings can be queried or even changed at any time (between adding objects).""" self.f = fig.File() self.scale = scale self.roi = roi if type(roi) == Rect2D: self.roi = BoundingBox(roi.upperLeft(), roi.lowerRight()) elif type(roi) == tuple: self.roi = BoundingBox(*roi) self.offset = numpy.array(offset)
def __init__(self, scale = 30, roi = None, offset = (0.5, 0.5)): """fe = FigExporter(90.0, BoundingBox((10, 10), (50, 50))) Initializes a FigExporter with the given scale and roi. A scale of 450 makes one pixel (source unit) 450 fig units large, which equals 1cm. Default is scale = 30. A roi is given as BoundingBox object (default None == no clipping) and represents a range in the original image (before scaling). An optional parameter offset (default: (0.5, 0.5)) is used to move all edges / points within the roi; this is intended to make e.g. (14.1, 10.0) be a little to the right of the *center* of the pixel (14, 10). The transformation parameters are stored as properties scale, roi, and offset, and these settings can be queried or even changed at any time (between adding objects).""" self.f = fig.File() self.scale = scale self.roi = roi if type(roi) == Rect2D: self.roi = BoundingBox(roi.upperLeft(), roi.lowerRight()) elif type(roi) == tuple: self.roi = BoundingBox(*roi) self.offset = numpy.array(offset)
def addPointCircles(self, points, radius, returnIndices=False, container=True, **attr): """fe.addPointCircles(points, radius, returnIndices = False,...) Marks each point in points with a circle of the given radius (in pixels) if it is within the clipping rect. The list of fig.Circle objects is returned. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). By default, circles will be filled, but have lineWidth=0. To draw a transparent circle, call: fi.addPointCircles([(5.2,5.3)], 2, penColor=fig.Color.Cyan,fillStyle=fig.FillStyle.None,lineWidth=1) If returnIndices is set to True (default: False), a list of (i, c) pairs is returned instead, where c is the fig.Circle object, and i is the index of the corresponding position in points.""" if container == True: container = self.f attr = dict(attr) if "fillStyle" not in attr: attr["fillStyle"] = fig.FillStyle.Solid if "lineWidth" not in attr and attr["fillStyle"] != fig.FillStyle.None: attr["lineWidth"] = 0 compound = fig.Compound(container) if returnIndices: result = [] else: result = compound o = self.offset + attr.get('offset', (0, 0)) o2 = self.roi and o - self.roi.begin() or o for i, point in enumerate(points): if self.roi: radiusOff = (radius, radius) circleBounds = BoundingBox(o + point - radiusOff, o + point + radiusOff) if not self.roi.contains(circleBounds): continue # FIXME: add arcs if intersects p = intPos((numpy.array((point[0], point[1])) + o2) * self.scale) dc = fig.Circle(p, radius * self.scale) for a in attr: if a != "offset": setattr(dc, a, attr[a]) if returnIndices: result.append((i, dc)) compound.append(dc) return result
def addClippedPoly(self, polygon, **attr): """fe.addClippedPoly(polygon, ...) Adds and returns exactly fig.Polygon objects for each part of the given polygon which is in the clipping range. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid # no ROI to clip to? if not self.roi: return [self.addEdge(polygon, **attr)] if type(polygon) != Polygon: if not isinstance(polygon, list): polygon = Polygon(list(polygon)) else: polygon = Polygon(polygon) clipRect = BoundingBox(self.roi) o = self.offset + attr.get('offset', (0,0)) clipRect.moveBy(-o) # handle all-or-none cases: if not clipRect.intersects(polygon.boundingBox()): return [] if clipRect.contains(polygon.boundingBox()): return [self.addEdge(polygon, **attr)] # general case: perform clipping, add parts one-by-one: result = [] # fig.Compound(container) - I dont't dare grouping here.. closeAtBorder = ( attr.get("fillStyle", fig.FillStyle.None) != fig.FillStyle.None) for part in clipPoly(polygon, clipRect, closeAtBorder): if part.length(): # don't add zero-length polygons result.append(self.addEdge(part, **attr)) return result
def draw(self, p, rect=None): if not self._map(): return #self._setupPainter(p) r = p.clipRegion().boundingRect() bbox = BoundingBox( (r.left() / self._zoom - 0.5, r.top() / self._zoom - 0.5), (r.right() / self._zoom + 0.5, r.bottom() / self._zoom + 0.5)) map = self._map() if self.colors: try: for edge in map.edgeIter(): edgeColor = self.colors[edge.label()] if edgeColor and bbox.intersects(edge.boundingBox()): p.setPen(QtGui.QPen(edgeColor, self.width)) p.drawPolyline(self._getZoomedEdge(edge)) except IndexError, e: print e #"IndexError: %d > %d (maxEdgeLabel: %d)!" % (
def addClippedPoly(self, polygon, **attr): """fe.addClippedPoly(polygon, ...) Adds and returns exactly fig.Polygon objects for each part of the given polygon which is in the clipping range. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid # no ROI to clip to? if not self.roi: return [self.addEdge(polygon, **attr)] if type(polygon) != Polygon: if not isinstance(polygon, list): polygon = Polygon(list(polygon)) else: polygon = Polygon(polygon) clipRect = BoundingBox(self.roi) o = self.offset + attr.get('offset', (0, 0)) clipRect.moveBy(-o) # handle all-or-none cases: if not clipRect.intersects(polygon.boundingBox()): return [] if clipRect.contains(polygon.boundingBox()): return [self.addEdge(polygon, **attr)] # general case: perform clipping, add parts one-by-one: result = [] # fig.Compound(container) - I dont't dare grouping here.. closeAtBorder = (attr.get("fillStyle", fig.FillStyle.None) != fig.FillStyle.None) for part in clipPoly(polygon, clipRect, closeAtBorder): if part.length(): # don't add zero-length polygons result.append(self.addEdge(part, **attr)) return result
class FigExporter: """FigExporter objects represent an image range at a given scale, and allow adding objects: You can add polygons or points, which will be clipped, scaled, positioned, and converted to appropriate fig objects and added to an internal fig.File (which is accessible via figExporter.f). All addFoo() variants allow to set properties of the resulting fig objects via keyword arguments, e.g. addROIRect(myroi, depth = 40, penColor = fig.Color.Yellow), cf. documentation of fig.Object . For a discussion of the scaling/clipping, see documentation of __init__().""" def __init__(self, scale=30, roi=None, offset=(0.5, 0.5)): """fe = FigExporter(90.0, BoundingBox((10, 10), (50, 50))) Initializes a FigExporter with the given scale and roi. A scale of 450 makes one pixel (source unit) 450 fig units large, which equals 1cm. Default is scale = 30. A roi is given as BoundingBox object (default None == no clipping) and represents a range in the original image (before scaling). An optional parameter offset (default: (0.5, 0.5)) is used to move all edges / points within the roi; this is intended to make e.g. (14.1, 10.0) be a little to the right of the *center* of the pixel (14, 10). The transformation parameters are stored as properties scale, roi, and offset, and these settings can be queried or even changed at any time (between adding objects).""" self.f = fig.File() self.scale = scale self.roi = roi if type(roi) == Rect2D: self.roi = BoundingBox(roi.upperLeft(), roi.lowerRight()) elif type(roi) == tuple: self.roi = BoundingBox(*roi) self.offset = numpy.array(offset) def position2Fig(self, pos): """fe.position2Fig(Vector2 pos) -> Vector2 Maps a pixel position to fig coordinates in the same way in which e.g. polygon points are mapped by addEdge().""" result = (self.offset + pos) if self.roi: result -= self.roi.begin() result *= self.scale return result def figPt(self, width): """Convert a distance into XFig points (1/80 inches), which can be used e.g. for line widths.""" return width * self.scale * 80 / self.f.ppi def addROIRect(self, roi=None, container=True, **attr): """fe.addROIRect(roi, depth = 85, ...) Adds a rectangle around the given roi (ignoring fe.offset). If roi == None (default), the roi of the FigExporter itself is used. The fig.PolyBox object is returned.""" assert roi or self.roi, "addROIRect(): no ROI given!?" if container == True: container = self.f if isinstance(roi, Rect2D): roi = BoundingBox(roi) roi.moveBy((-0.5, -0.5)) if roi is None: roi = self.roi assert not isinstance(roi, Rect2D) roi = BoundingBox(roi) roi.moveBy(-self.offset) # BAAH! What a bad design (self.roi) elif self.roi and not self.roi.contains(roi): sys.stderr.write("WARNING: addROIRect: ROI out of bounds!\n") poly = Polygon([ roi.begin(), roi.begin() + (0, roi.size()[1]), roi.end(), roi.begin() + (roi.size()[0], 0), roi.begin() ]) result = self.addClippedPoly(poly, container=container, **attr) if result: assert len(result) == 1 return result[0] return roi = BoundingBox(roi) roi.moveBy(self.offset) if self.roi: roi.moveBy(-self.roi.begin()) if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid result = fig.PolyBox(roi.begin()[0] * self.scale, roi.begin()[1] * self.scale, roi.end()[0] * self.scale, roi.end()[1] * self.scale) container.append(result) for a in attr: setattr(result, a, attr[a]) return result def addBackgroundWithFrame(self, bgImageFilename, container=True, **params): """fe.addBackgroundWithFrame(bgImageFilename, depth = 85, ...) Adds a picture object to the fig.File, framed by an additional rectangle. See addROIRect() and addImage(). If no roi is given (via a keyword parameter), the image file is opened (using readImage) and its size is used to initialize a BoundingBox positioned at the origin. Returns the pair (bgImage, bgRect) of both added fig objects.""" if not params.has_key("roi") and not self.roi: size = readImage(bgImageFilename).size() params["roi"] = Rect2D(size) if container == True: container = self.f if not params.has_key("depth"): params["depth"] = 1 bgRect = self.addROIRect(**params) bgImage = fig.PictureBBox(0, 0, 1, 1, bgImageFilename) bgImage.points = list(bgRect.points) bgImage.depth = 999 container.append(bgImage) return bgImage, bgRect def addImage(self, filename, container=True, **params): """fe.addImage(filename, depth = 85, ...) Add a picture object to the fig.File. See addROIRect() and addBackgroundWithFrame(). If no roi is given (via a keyword parameter), the image file is opened (using readImage) and its size is used to initialize a BoundingBox positioned at the origin. Returns the fig.PictureBBox object.""" bgImage, bgRect = self.addBackgroundWithFrame(filename, container, **params) if container == True: container = self.f container.remove(bgRect) return bgImage def addPixelRaster(self, rect=None, labels=None, fill=None, container=True, labelType=int, **attr): """The default label size looks good with 1cm^2 pixels (scale = 450).""" assert rect or labels or fill if rect is None: rect = Rect2D((labels or fill).size()) elif not hasattr(rect, "upperLeft"): w, h = rect rect = Rect2D((w, h)) if container == True: container = self.f result = fig.Compound(container) for x, y in meshIter(rect): boxX1 = (x - rect.left()) * self.scale boxY1 = (y - rect.top()) * self.scale boxX2 = boxX1 + self.scale boxY2 = boxY1 + self.scale pixelRect = fig.PolyBox(boxX1, boxY1, boxX2, boxY2) for a in attr: setattr(pixelRect, a, attr[a]) if labels: textX = (boxX1 + boxX2) / 2 textY = (boxY1 + boxY2) / 2 label = fig.Text(fig.Vector(textX, textY), str(labelType(labels[x, y])), alignment=fig.Alignment.Centered) label.pos += (0, label.height / 2) label.depth = pixelRect.depth - 10 label.font = fig.Font.Helvetica result.append(label) if fill: pixelRect.fillStyle = fig.FillStyle.Solid pixelRect.fillColor = self.f.getColor(int(fill[x, y])) result.append(pixelRect) return result def addText(self, position, text, container=True, **attr): if container == True: container = self.f obj = fig.Text(self.position2Fig(position), text) for a in attr: setattr(obj, a, attr[a]) container.append(obj) return obj def addEdge(self, points, simplifyEpsilon=0.5, container=True, figClass=fig.Polygon, **attr): """fe.addEdge(points, simplifyEpsilon, ...) Adds and returns exactly one fig.Polygon object representing the given points. You will probably want to use addClippedPoly() instead. If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if container == True: container = self.f o = self.offset + attr.get('offset', (0, 0)) if self.roi: o -= self.roi.begin() pp = Polygon([(o + point) * self.scale for point in points]) if simplifyEpsilon: pp = simplifyPolygon(pp, simplifyEpsilon) fp = figClass([intPos(v) for v in pp], closed=pp[0] == pp[-1]) for a in attr: if a != "offset": setattr(fp, a, attr[a]) container.append(fp) return fp def addClippedPoly(self, polygon, **attr): """fe.addClippedPoly(polygon, ...) Adds and returns exactly fig.Polygon objects for each part of the given polygon which is in the clipping range. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid # no ROI to clip to? if not self.roi: return [self.addEdge(polygon, **attr)] if type(polygon) != Polygon: if not isinstance(polygon, list): polygon = Polygon(list(polygon)) else: polygon = Polygon(polygon) clipRect = BoundingBox(self.roi) o = self.offset + attr.get('offset', (0, 0)) clipRect.moveBy(-o) # handle all-or-none cases: if not clipRect.intersects(polygon.boundingBox()): return [] if clipRect.contains(polygon.boundingBox()): return [self.addEdge(polygon, **attr)] # general case: perform clipping, add parts one-by-one: result = [] # fig.Compound(container) - I dont't dare grouping here.. closeAtBorder = (attr.get("fillStyle", fig.FillStyle.None) != fig.FillStyle.None) for part in clipPoly(polygon, clipRect, closeAtBorder): if part.length(): # don't add zero-length polygons result.append(self.addEdge(part, **attr)) return result def addPointCircles(self, points, radius, returnIndices=False, container=True, **attr): """fe.addPointCircles(points, radius, returnIndices = False,...) Marks each point in points with a circle of the given radius (in pixels) if it is within the clipping rect. The list of fig.Circle objects is returned. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). By default, circles will be filled, but have lineWidth=0. To draw a transparent circle, call: fi.addPointCircles([(5.2,5.3)], 2, penColor=fig.Color.Cyan,fillStyle=fig.FillStyle.None,lineWidth=1) If returnIndices is set to True (default: False), a list of (i, c) pairs is returned instead, where c is the fig.Circle object, and i is the index of the corresponding position in points.""" if container == True: container = self.f attr = dict(attr) if "fillStyle" not in attr: attr["fillStyle"] = fig.FillStyle.Solid if "lineWidth" not in attr and attr["fillStyle"] != fig.FillStyle.None: attr["lineWidth"] = 0 compound = fig.Compound(container) if returnIndices: result = [] else: result = compound o = self.offset + attr.get('offset', (0, 0)) o2 = self.roi and o - self.roi.begin() or o for i, point in enumerate(points): if self.roi: radiusOff = (radius, radius) circleBounds = BoundingBox(o + point - radiusOff, o + point + radiusOff) if not self.roi.contains(circleBounds): continue # FIXME: add arcs if intersects p = intPos((numpy.array((point[0], point[1])) + o2) * self.scale) dc = fig.Circle(p, radius * self.scale) for a in attr: if a != "offset": setattr(dc, a, attr[a]) if returnIndices: result.append((i, dc)) compound.append(dc) return result def addEdgels(self, edgels, length=0.65, container=True, **attr): if container == True: container = self.f result = fig.Compound(container) for edgel in edgels: pos = numpy.array((edgel.x, edgel.y)) c = math.cos(edgel.orientation) * length / 2 s = -math.sin(edgel.orientation) * length / 2 self.addClippedPoly([pos + (c, s), pos - (c, s)], container=result, **attr) return result def _setOverlayColor(self, overlay, colorAttr, attr): """Set color (and possibly line width) from overlay attributes.""" if colorAttr not in attr: color = overlay.color if type(color) == QtGui.QColor: color = qtColor2figColor(color, self.f) attr[colorAttr] = color #print "fetched %s %s from %s" % (colorAttr, color, overlay) if colorAttr == "penColor" and \ hasattr(overlay, "fillColor") and "fillColor" not in attr: color = overlay.fillColor if type(color) == QtGui.QColor: color = qtColor2figColor(color, self.f) if color is not None: attr["fillColor"] = color if "lineWidth" not in attr: if colorAttr == "penColor" and overlay.color is None: attr["lineWidth"] = 0 elif hasattr(overlay, "width"): attr["lineWidth"] = overlay.width + 1 def addPointOverlay(self, pointOverlay, container=True, **attr): """See addPointCircles(), this function simply takes the points and radius from a PointOverlay object for your convenience.""" points = pointOverlay.originalPoints radius = float(pointOverlay.origRadius) if not pointOverlay.relativeRadius: radius /= pointOverlay.zoom attr = dict(attr) self._setOverlayColor(pointOverlay, "fillColor", attr) attr["lineWidth"] = attr.get("lineWidth", 0) return self.addPointCircles(points, radius, container=container, **attr) def addEdgeOverlay(self, edgeOverlay, container=True, **attr): """Adds and returns fig.Polygon for all edges (or -parts, see addClippedPoly) of the given overlay, using the overlays' color.""" if container == True: container = self.f edges = edgeOverlay.originalEdges attr = dict(attr) self._setOverlayColor(edgeOverlay, "penColor", attr) attr["offset"] = (edgeOverlay.offset[0] - 0.5, edgeOverlay.offset[1] - 0.5) result = fig.Compound(container) for edge in edges: parts = self.addClippedPoly(edge, container=result, **attr) return result def addCircleOverlay(self, circleOverlay, container=True, **attr): """Adds and returns fig.Circle for all circles of the given overlay, using the overlays' color and width.""" if container == True: container = self.f circles = circleOverlay.originalCircles attr = dict(attr) self._setOverlayColor(circleOverlay, "penColor", attr) o = self.offset + attr.get('offset', (0, 0)) if self.roi: o = o - self.roi.begin() # don't modify in-place! result = fig.Compound(container) for center, radius in circles: if self.roi and not self.roi.contains(center + o): continue p = intPos(((center[0], center[1]) + o) * self.scale) dc = fig.Circle(p, radius * self.scale) for a in attr: setattr(dc, a, attr[a]) result.append(dc) return result def addMapNodes(self, map, radius, returnNodes=False, container=True, **attr): """fe.addMapNodes(map, radius, ...) See addPointCircles(), this function simply takes the positions of all nodes in the given map and marks them with a circle of the given radius (in pixels). If the optional parameter returnNodes is set to True, a list of (node, circleObject) pairs is returned, similar to the returnIndices parameter of addPointCircles().""" points = [node.position() for node in map.nodeIter()] result = self.addPointCircles(points, radius, returnIndices=returnNodes, container=container, **attr) if returnNodes: nodes = list(map.nodeIter()) result = [(nodes[i], circle) for i, circle in result] return result def addMapEdges(self, map, skipBorder=False, returnEdges=False, container=True, **attr): """Adds and returns fig.Polygons for all map edges (or -parts, see addClippedPoly). If no penColor is given, only edges with a valid 'color' attribute are exported (can be either a fig or a Qt color). For example, to draw only a subregion, and shift the upper left of the region to the origin, call fi.addMapEdges(map, penColor=fig.Color.Green, \ offset=(-13,-13), roi=BoundingBox((13,13), (24,24))) """ if container == True: container = self.f compound = fig.Compound(container) if returnEdges: result = [] else: result = compound for edge in map.edgeIter(): if skipBorder and edge.flag(flag_constants.BORDER_PROTECTION): continue parts = self.addClippedPoly(edge, container=compound, **attr) if returnEdges: result.extend([(edge, part) for part in parts]) return result def addMapFaces(self, geomap, faceMeans=None, similarity=None, returnFaces=False, container=True, **attr): """fe.addMapFaces(geomap, faceMeans, ...) Adds and returns fig.Polygons for all map faces (or -parts, see addClippedPoly). Clipping closed polygons should work nowadays, too.""" import maputils def getGray(face): faceColor = faceMeans[face.label()] return self.f.gray(int(faceColor)) def getRGB(face): faceColor = faceMeans[face.label()] return self.f.getColor(map(int, tuple(faceColor)), similarity) if not faceMeans: if not hasattr(geomap, "faceMeans"): raise ValueError( "addMapFaces: need faceMeans for proper coloring") faceMeans = geomap.faceMeans if hasattr(faceMeans, "bands"): getFaceColor = getGray if faceMeans.bands() == 3: getFaceColor = getRGB else: getFaceColor = lambda face: faceMeans[face.label()] if container == True: container = self.f attr = dict(attr) attr["lineWidth"] = attr.get("lineWidth", 0) attr["fillStyle"] = attr.get("fillStyle", fig.FillStyle.Solid) compound = fig.Compound(container) if returnFaces: result = [] else: result = compound todo = [geomap.face(0)] # +1 because the first iteration will not add any objects: currentDepth = attr.get("depth", 100) + 1 while todo: thisLayer = todo todo = [] for face in thisLayer: if face.area() > 0: color = getFaceColor(face) if color is not None: thisattr = dict(attr) thisattr["fillColor"] = getFaceColor(face) thisattr["depth"] = currentDepth parts = self.addClippedPoly(contourPoly( face.contour()), container=compound, **thisattr) if returnFaces: result.extend([(face, part) for part in parts]) for anchor in face.holeContours(): todo.extend(maputils.holeComponent(anchor)) currentDepth -= 1 return result def save(self, filename, fig2dev=None): """Save the resulting XFig file to 'filename' (cf. fig.File.save).""" if fig2dev is not None and filename.endswith("." + fig2dev): filename = filename[:-len(fig2dev) - 1] return self.f.save(filename, fig2dev) def saveEPS(self, basename): """Save the resulting XFig file to [basename].{fig,eps} (cf. fig.File.save).""" return self.save(basename, fig2dev="eps") def savePDF(self, basename): """Save the resulting XFig file to [basename].{fig,pdf} (cf. fig.File.save).""" return self.save(basename, fig2dev="pdf")
def addROIRect(self, roi=None, container=True, **attr): """fe.addROIRect(roi, depth = 85, ...) Adds a rectangle around the given roi (ignoring fe.offset). If roi == None (default), the roi of the FigExporter itself is used. The fig.PolyBox object is returned.""" assert roi or self.roi, "addROIRect(): no ROI given!?" if container == True: container = self.f if isinstance(roi, Rect2D): roi = BoundingBox(roi) roi.moveBy((-0.5, -0.5)) if roi is None: roi = self.roi assert not isinstance(roi, Rect2D) roi = BoundingBox(roi) roi.moveBy(-self.offset) # BAAH! What a bad design (self.roi) elif self.roi and not self.roi.contains(roi): sys.stderr.write("WARNING: addROIRect: ROI out of bounds!\n") poly = Polygon([ roi.begin(), roi.begin() + (0, roi.size()[1]), roi.end(), roi.begin() + (roi.size()[0], 0), roi.begin() ]) result = self.addClippedPoly(poly, container=container, **attr) if result: assert len(result) == 1 return result[0] return roi = BoundingBox(roi) roi.moveBy(self.offset) if self.roi: roi.moveBy(-self.roi.begin()) if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid result = fig.PolyBox(roi.begin()[0] * self.scale, roi.begin()[1] * self.scale, roi.end()[0] * self.scale, roi.end()[1] * self.scale) container.append(result) for a in attr: setattr(result, a, attr[a]) return result
if scale == None: scale = 20 * 450 / roi.width() # default: 20cm width print "auto-adjusted scale to %s." % (scale, ) fe = FigExporter(scale, roi) if bgFilename != False: fe.addBackgroundWithFrame(bgFilename, depth=100, lineWidth=0) else: fe.addROIRect(depth=100, lineWidth=0) _exportOverlays(fe, w.viewer.overlays, overlayHandler) fe.save(figFilename) return fe # -------------------------------------------------------------------- # USAGE # -------------------------------------------------------------------- if False: # default scale, no ROI: fe = FigExporter() # use given scale and ROI: fe = FigExporter(scale=15, roi=BoundingBox(Rect2D(dm.imageSize()))) # add background image: fe.addBackgroundWithFrame("background.png") fe.addMapEdges(someMap) # give optional properties like lineWidth = 4 someRadius = 0.1 # pixels fe.addMapNodes(someMap, someRadius) fe.saveEPS("example_basename")
def addMapOverlay(fe, overlay, skipBorder=False, **attr): qtColor2figColor = figexport.qtColor2figColor # FIXME: str(type(overlay)).contains(...) instead? if isinstance(overlay, ROISelector): color = qtColor2figColor(overlay.color, fe.f) return fe.addROIRect(overlay.roi, penColor=color, **attr) elif isinstance(overlay, (MapNodes, MapEdges, MapFaces)): oldScale, oldOffset, oldROI = fe.scale, fe.offset, fe.roi if isinstance(overlay, MapFaces): zoom = overlay.edgeOverlay._zoom else: zoom = overlay._zoom extraZoom = float(zoom) / overlay.viewer.zoomFactor() fe.scale *= extraZoom fe.roi = BoundingBox(fe.roi.begin() / extraZoom, fe.roi.end() / extraZoom) map = overlay._map() if isinstance(overlay, MapNodes): radius = overlay.origRadius if not overlay.relativeRadius: radius /= float(overlay._zoom) color = qtColor2figColor(overlay.color, fe.f) result = fe.addMapNodes(map, radius, fillColor=color, lineWidth=0, **attr) elif isinstance(overlay, MapEdges): attr = dict(attr) if overlay.width: attr["lineWidth"] = overlay.width if overlay.colors: result = fig.Compound(fe.f) for edge in map.edgeIter(): edgeColor = overlay.colors[edge.label()] if edgeColor: fe.addClippedPoly(edge, penColor=qtColor2figColor( edgeColor, fe.f), container=result, **attr) elif overlay.color: result = fe.addMapEdges(map, skipBorder=skipBorder, penColor=qtColor2figColor( overlay.color, fe.f), **attr) else: result = fig.Compound(fe.f) if overlay.protectedColor: attr["penColor"] = \ qtColor2figColor(overlay.protectedColor, fe.f) attr["lineWidth"] = overlay.protectedWidth or overlay.width it = skipBorder and maputils.nonBorderEdges(map) \ or map.edgeIter() for edge in it: if edge.flag(flag_constants.ALL_PROTECTION): fe.addClippedPoly(edge, container=result, **attr) else: # isinstance(overlay, MapFaces) attr = dict(attr) if overlay.color: if overlay.width: attr["lineWidth"] = overlay.width attr["penColor"] = qtColor2figColor(overlay.color, fe.f) if overlay.fillColor: attr["fillColor"] = qtColor2figColor(overlay.fillColor, fe.f) attr["fillStyle"] = fig.FillStyle.Solid result = fig.Compound(fe.f) for face in map.faceIter(): if face.flag(overlay.flags): if face.holeCount: assert not overlay.fillColor or not overlay.color, "FIXME: cannot currently export filled+stroked polygons with holes" if not overlay.color: wholePoly = list(contourPoly(face.contour())) back = wholePoly[0] assert wholePoly[-1] == back for dart in face.holeContours(): wholePoly.extend(contourPoly(dart)) wholePoly.append(back) fe.addClippedPoly(wholePoly, container=result, **attr) else: for dart in face.contours(): fe.addClippedPoly(contourPoly(dart), container=result, **attr) fe.scale, fe.offset, fe.roi = oldScale, oldOffset, oldROI return result else: return figexport.addStandardOverlay(fe, overlay, **attr)
class FigExporter: """FigExporter objects represent an image range at a given scale, and allow adding objects: You can add polygons or points, which will be clipped, scaled, positioned, and converted to appropriate fig objects and added to an internal fig.File (which is accessible via figExporter.f). All addFoo() variants allow to set properties of the resulting fig objects via keyword arguments, e.g. addROIRect(myroi, depth = 40, penColor = fig.Color.Yellow), cf. documentation of fig.Object . For a discussion of the scaling/clipping, see documentation of __init__().""" def __init__(self, scale = 30, roi = None, offset = (0.5, 0.5)): """fe = FigExporter(90.0, BoundingBox((10, 10), (50, 50))) Initializes a FigExporter with the given scale and roi. A scale of 450 makes one pixel (source unit) 450 fig units large, which equals 1cm. Default is scale = 30. A roi is given as BoundingBox object (default None == no clipping) and represents a range in the original image (before scaling). An optional parameter offset (default: (0.5, 0.5)) is used to move all edges / points within the roi; this is intended to make e.g. (14.1, 10.0) be a little to the right of the *center* of the pixel (14, 10). The transformation parameters are stored as properties scale, roi, and offset, and these settings can be queried or even changed at any time (between adding objects).""" self.f = fig.File() self.scale = scale self.roi = roi if type(roi) == Rect2D: self.roi = BoundingBox(roi.upperLeft(), roi.lowerRight()) elif type(roi) == tuple: self.roi = BoundingBox(*roi) self.offset = numpy.array(offset) def position2Fig(self, pos): """fe.position2Fig(Vector2 pos) -> Vector2 Maps a pixel position to fig coordinates in the same way in which e.g. polygon points are mapped by addEdge().""" result = (self.offset + pos) if self.roi: result -= self.roi.begin() result *= self.scale return result def figPt(self, width): """Convert a distance into XFig points (1/80 inches), which can be used e.g. for line widths.""" return width*self.scale*80/self.f.ppi def addROIRect(self, roi = None, container = True, **attr): """fe.addROIRect(roi, depth = 85, ...) Adds a rectangle around the given roi (ignoring fe.offset). If roi == None (default), the roi of the FigExporter itself is used. The fig.PolyBox object is returned.""" assert roi or self.roi, "addROIRect(): no ROI given!?" if container == True: container = self.f if isinstance(roi, Rect2D): roi = BoundingBox(roi) roi.moveBy((-0.5, -0.5)) if roi is None: roi = self.roi assert not isinstance(roi, Rect2D) roi = BoundingBox(roi) roi.moveBy(-self.offset) # BAAH! What a bad design (self.roi) elif self.roi and not self.roi.contains(roi): sys.stderr.write("WARNING: addROIRect: ROI out of bounds!\n") poly = Polygon([roi.begin(), roi.begin() + (0, roi.size()[1]), roi.end(), roi.begin() + (roi.size()[0], 0), roi.begin()]) result = self.addClippedPoly(poly, container = container, **attr) if result: assert len(result) == 1 return result[0] return roi = BoundingBox(roi) roi.moveBy(self.offset) if self.roi: roi.moveBy(-self.roi.begin()) if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid result = fig.PolyBox(roi.begin()[0] * self.scale, roi.begin()[1] * self.scale, roi.end()[0] * self.scale, roi.end()[1] * self.scale) container.append(result) for a in attr: setattr(result, a, attr[a]) return result def addBackgroundWithFrame(self, bgImageFilename, container = True, **params): """fe.addBackgroundWithFrame(bgImageFilename, depth = 85, ...) Adds a picture object to the fig.File, framed by an additional rectangle. See addROIRect() and addImage(). If no roi is given (via a keyword parameter), the image file is opened (using readImage) and its size is used to initialize a BoundingBox positioned at the origin. Returns the pair (bgImage, bgRect) of both added fig objects.""" if not params.has_key("roi") and not self.roi: size = readImage(bgImageFilename).size() params["roi"] = Rect2D(size) if container == True: container = self.f if not params.has_key("depth"): params["depth"] = 1 bgRect = self.addROIRect(**params) bgImage = fig.PictureBBox(0, 0, 1, 1, bgImageFilename) bgImage.points = list(bgRect.points) bgImage.depth = 999 container.append(bgImage) return bgImage, bgRect def addImage(self, filename, container = True, **params): """fe.addImage(filename, depth = 85, ...) Add a picture object to the fig.File. See addROIRect() and addBackgroundWithFrame(). If no roi is given (via a keyword parameter), the image file is opened (using readImage) and its size is used to initialize a BoundingBox positioned at the origin. Returns the fig.PictureBBox object.""" bgImage, bgRect = self.addBackgroundWithFrame( filename, container, **params) if container == True: container = self.f container.remove(bgRect) return bgImage def addPixelRaster(self, rect = None, labels = None, fill = None, container = True, labelType = int, **attr): """The default label size looks good with 1cm^2 pixels (scale = 450).""" assert rect or labels or fill if rect is None: rect = Rect2D((labels or fill).size()) elif not hasattr(rect, "upperLeft"): w, h = rect rect = Rect2D((w, h)) if container == True: container = self.f result = fig.Compound(container) for x, y in meshIter(rect): boxX1 = (x - rect.left()) * self.scale boxY1 = (y - rect.top()) * self.scale boxX2 = boxX1 + self.scale boxY2 = boxY1 + self.scale pixelRect = fig.PolyBox(boxX1, boxY1, boxX2, boxY2) for a in attr: setattr(pixelRect, a, attr[a]) if labels: textX = (boxX1 + boxX2) / 2 textY = (boxY1 + boxY2) / 2 label = fig.Text(fig.Vector(textX, textY), str(labelType(labels[x, y])), alignment = fig.Alignment.Centered) label.pos += (0, label.height / 2) label.depth = pixelRect.depth - 10 label.font = fig.Font.Helvetica result.append(label) if fill: pixelRect.fillStyle = fig.FillStyle.Solid pixelRect.fillColor = self.f.getColor(int(fill[x, y])) result.append(pixelRect) return result def addText(self, position, text, container = True, **attr): if container == True: container = self.f obj = fig.Text(self.position2Fig(position), text) for a in attr: setattr(obj, a, attr[a]) container.append(obj) return obj def addEdge(self, points, simplifyEpsilon = 0.5, container = True, figClass = fig.Polygon, **attr): """fe.addEdge(points, simplifyEpsilon, ...) Adds and returns exactly one fig.Polygon object representing the given points. You will probably want to use addClippedPoly() instead. If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if container == True: container = self.f o = self.offset + attr.get('offset', (0,0)) if self.roi: o -= self.roi.begin() pp = Polygon([(o + point) * self.scale for point in points]) if simplifyEpsilon: pp = simplifyPolygon(pp, simplifyEpsilon) fp = figClass([intPos(v) for v in pp], closed = pp[0] == pp[-1]) for a in attr: if a != "offset": setattr(fp, a, attr[a]) container.append(fp) return fp def addClippedPoly(self, polygon, **attr): """fe.addClippedPoly(polygon, ...) Adds and returns exactly fig.Polygon objects for each part of the given polygon which is in the clipping range. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). If simplifyEpsilon (default: 0.5) is not None, simplifyPolygon is called on the *scaled* polygon (i.e. the default is to simplify the polygon to 0.5 fig units, which are integer anyways).""" if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid # no ROI to clip to? if not self.roi: return [self.addEdge(polygon, **attr)] if type(polygon) != Polygon: if not isinstance(polygon, list): polygon = Polygon(list(polygon)) else: polygon = Polygon(polygon) clipRect = BoundingBox(self.roi) o = self.offset + attr.get('offset', (0,0)) clipRect.moveBy(-o) # handle all-or-none cases: if not clipRect.intersects(polygon.boundingBox()): return [] if clipRect.contains(polygon.boundingBox()): return [self.addEdge(polygon, **attr)] # general case: perform clipping, add parts one-by-one: result = [] # fig.Compound(container) - I dont't dare grouping here.. closeAtBorder = ( attr.get("fillStyle", fig.FillStyle.None) != fig.FillStyle.None) for part in clipPoly(polygon, clipRect, closeAtBorder): if part.length(): # don't add zero-length polygons result.append(self.addEdge(part, **attr)) return result def addPointCircles(self, points, radius, returnIndices = False, container = True, **attr): """fe.addPointCircles(points, radius, returnIndices = False,...) Marks each point in points with a circle of the given radius (in pixels) if it is within the clipping rect. The list of fig.Circle objects is returned. Again, note the possibility of setting properties (depth, penColor, lineStyle, lineWidth, ...) on all resulting objects via keyword arguments (cf. documentation of the FigExporter class). By default, circles will be filled, but have lineWidth=0. To draw a transparent circle, call: fi.addPointCircles([(5.2,5.3)], 2, penColor=fig.Color.Cyan,fillStyle=fig.FillStyle.None,lineWidth=1) If returnIndices is set to True (default: False), a list of (i, c) pairs is returned instead, where c is the fig.Circle object, and i is the index of the corresponding position in points.""" if container == True: container = self.f attr = dict(attr) if "fillStyle" not in attr: attr["fillStyle"] = fig.FillStyle.Solid if "lineWidth" not in attr and attr["fillStyle"] != fig.FillStyle.None: attr["lineWidth"] = 0 compound = fig.Compound(container) if returnIndices: result = [] else: result = compound o = self.offset + attr.get('offset', (0,0)) o2 = self.roi and o - self.roi.begin() or o for i, point in enumerate(points): if self.roi: radiusOff = (radius, radius) circleBounds = BoundingBox( o + point - radiusOff, o + point + radiusOff) if not self.roi.contains(circleBounds): continue # FIXME: add arcs if intersects p = intPos((numpy.array((point[0], point[1])) + o2) * self.scale) dc = fig.Circle(p, radius*self.scale) for a in attr: if a != "offset": setattr(dc, a, attr[a]) if returnIndices: result.append((i, dc)) compound.append(dc) return result def addEdgels(self, edgels, length = 0.65, container = True, **attr): if container == True: container = self.f result = fig.Compound(container) for edgel in edgels: pos = numpy.array((edgel.x, edgel.y)) c = math.cos(edgel.orientation)*length/2 s = -math.sin(edgel.orientation)*length/2 self.addClippedPoly([pos+(c,s), pos-(c,s)], container = result, **attr) return result def _setOverlayColor(self, overlay, colorAttr, attr): """Set color (and possibly line width) from overlay attributes.""" if colorAttr not in attr: color = overlay.color if type(color) == QtGui.QColor: color = qtColor2figColor(color, self.f) attr[colorAttr] = color #print "fetched %s %s from %s" % (colorAttr, color, overlay) if colorAttr == "penColor" and \ hasattr(overlay, "fillColor") and "fillColor" not in attr: color = overlay.fillColor if type(color) == QtGui.QColor: color = qtColor2figColor(color, self.f) if color is not None: attr["fillColor"] = color if "lineWidth" not in attr: if colorAttr == "penColor" and overlay.color is None: attr["lineWidth"] = 0 elif hasattr(overlay, "width"): attr["lineWidth"] = overlay.width + 1 def addPointOverlay(self, pointOverlay, container = True, **attr): """See addPointCircles(), this function simply takes the points and radius from a PointOverlay object for your convenience.""" points = pointOverlay.originalPoints radius = float(pointOverlay.origRadius) if not pointOverlay.relativeRadius: radius /= pointOverlay.zoom attr = dict(attr) self._setOverlayColor(pointOverlay, "fillColor", attr) attr["lineWidth"] = attr.get("lineWidth", 0) return self.addPointCircles(points, radius, container = container, **attr) def addEdgeOverlay(self, edgeOverlay, container = True, **attr): """Adds and returns fig.Polygon for all edges (or -parts, see addClippedPoly) of the given overlay, using the overlays' color.""" if container == True: container = self.f edges = edgeOverlay.originalEdges attr = dict(attr) self._setOverlayColor(edgeOverlay, "penColor", attr) attr["offset"] = (edgeOverlay.offset[0] - 0.5, edgeOverlay.offset[1] - 0.5) result = fig.Compound(container) for edge in edges: parts = self.addClippedPoly(edge, container = result, **attr) return result def addCircleOverlay(self, circleOverlay, container = True, **attr): """Adds and returns fig.Circle for all circles of the given overlay, using the overlays' color and width.""" if container == True: container = self.f circles = circleOverlay.originalCircles attr = dict(attr) self._setOverlayColor(circleOverlay, "penColor", attr) o = self.offset + attr.get('offset', (0,0)) if self.roi: o = o - self.roi.begin() # don't modify in-place! result = fig.Compound(container) for center, radius in circles: if self.roi and not self.roi.contains(center+o): continue p = intPos(((center[0], center[1]) + o) * self.scale) dc = fig.Circle(p, radius * self.scale) for a in attr: setattr(dc, a, attr[a]) result.append(dc) return result def addMapNodes(self, map, radius, returnNodes = False, container = True, **attr): """fe.addMapNodes(map, radius, ...) See addPointCircles(), this function simply takes the positions of all nodes in the given map and marks them with a circle of the given radius (in pixels). If the optional parameter returnNodes is set to True, a list of (node, circleObject) pairs is returned, similar to the returnIndices parameter of addPointCircles().""" points = [node.position() for node in map.nodeIter()] result = self.addPointCircles( points, radius, returnIndices = returnNodes, container = container, **attr) if returnNodes: nodes = list(map.nodeIter()) result = [(nodes[i], circle) for i, circle in result] return result def addMapEdges(self, map, skipBorder = False, returnEdges = False, container = True, **attr): """Adds and returns fig.Polygons for all map edges (or -parts, see addClippedPoly). If no penColor is given, only edges with a valid 'color' attribute are exported (can be either a fig or a Qt color). For example, to draw only a subregion, and shift the upper left of the region to the origin, call fi.addMapEdges(map, penColor=fig.Color.Green, \ offset=(-13,-13), roi=BoundingBox((13,13), (24,24))) """ if container == True: container = self.f compound = fig.Compound(container) if returnEdges: result = [] else: result = compound for edge in map.edgeIter(): if skipBorder and edge.flag(flag_constants.BORDER_PROTECTION): continue parts = self.addClippedPoly(edge, container = compound, **attr) if returnEdges: result.extend([(edge, part) for part in parts]) return result def addMapFaces(self, geomap, faceMeans = None, similarity = None, returnFaces = False, container = True, **attr): """fe.addMapFaces(geomap, faceMeans, ...) Adds and returns fig.Polygons for all map faces (or -parts, see addClippedPoly). Clipping closed polygons should work nowadays, too.""" import maputils def getGray(face): faceColor = faceMeans[face.label()] return self.f.gray(int(faceColor)) def getRGB(face): faceColor = faceMeans[face.label()] return self.f.getColor(map(int, tuple(faceColor)), similarity) if not faceMeans: if not hasattr(geomap, "faceMeans"): raise ValueError("addMapFaces: need faceMeans for proper coloring") faceMeans = geomap.faceMeans if hasattr(faceMeans, "bands"): getFaceColor = getGray if faceMeans.bands() == 3: getFaceColor = getRGB else: getFaceColor = lambda face: faceMeans[face.label()] if container == True: container = self.f attr = dict(attr) attr["lineWidth"] = attr.get("lineWidth", 0) attr["fillStyle"] = attr.get("fillStyle", fig.FillStyle.Solid) compound = fig.Compound(container) if returnFaces: result = [] else: result = compound todo = [geomap.face(0)] # +1 because the first iteration will not add any objects: currentDepth = attr.get("depth", 100) + 1 while todo: thisLayer = todo todo = [] for face in thisLayer: if face.area() > 0: color = getFaceColor(face) if color is not None: thisattr = dict(attr) thisattr["fillColor"] = getFaceColor(face) thisattr["depth"] = currentDepth parts = self.addClippedPoly( contourPoly(face.contour()), container = compound, **thisattr) if returnFaces: result.extend([(face, part) for part in parts]) for anchor in face.holeContours(): todo.extend(maputils.holeComponent(anchor)) currentDepth -= 1 return result def save(self, filename, fig2dev = None): """Save the resulting XFig file to 'filename' (cf. fig.File.save).""" if fig2dev is not None and filename.endswith("." + fig2dev): filename = filename[:-len(fig2dev)-1] return self.f.save(filename, fig2dev) def saveEPS(self, basename): """Save the resulting XFig file to [basename].{fig,eps} (cf. fig.File.save).""" return self.save(basename, fig2dev = "eps") def savePDF(self, basename): """Save the resulting XFig file to [basename].{fig,pdf} (cf. fig.File.save).""" return self.save(basename, fig2dev = "pdf")
def addROIRect(self, roi = None, container = True, **attr): """fe.addROIRect(roi, depth = 85, ...) Adds a rectangle around the given roi (ignoring fe.offset). If roi == None (default), the roi of the FigExporter itself is used. The fig.PolyBox object is returned.""" assert roi or self.roi, "addROIRect(): no ROI given!?" if container == True: container = self.f if isinstance(roi, Rect2D): roi = BoundingBox(roi) roi.moveBy((-0.5, -0.5)) if roi is None: roi = self.roi assert not isinstance(roi, Rect2D) roi = BoundingBox(roi) roi.moveBy(-self.offset) # BAAH! What a bad design (self.roi) elif self.roi and not self.roi.contains(roi): sys.stderr.write("WARNING: addROIRect: ROI out of bounds!\n") poly = Polygon([roi.begin(), roi.begin() + (0, roi.size()[1]), roi.end(), roi.begin() + (roi.size()[0], 0), roi.begin()]) result = self.addClippedPoly(poly, container = container, **attr) if result: assert len(result) == 1 return result[0] return roi = BoundingBox(roi) roi.moveBy(self.offset) if self.roi: roi.moveBy(-self.roi.begin()) if "fillColor" in attr and not "fillStyle" in attr: attr["fillStyle"] = fig.FillStyle.Solid result = fig.PolyBox(roi.begin()[0] * self.scale, roi.begin()[1] * self.scale, roi.end()[0] * self.scale, roi.end()[1] * self.scale) container.append(result) for a in attr: setattr(result, a, attr[a]) return result