def __init__(self, sizeTuple, num_of_nodes=10, bbox=BBOX, save_name=SAVENAME, debug_draw=False, n=10, max_steps=MAX_STEPS): assert(isinstance(sizeTuple, tuple)) assert(isinstance(bbox, np.ndarray)) assert(bbox.shape == (4,)) self.current_step = 0 self.sX = sizeTuple[0] self.sY = sizeTuple[1] self.nodeSize = num_of_nodes self.max_steps = max_steps #Min Heap of site/circle events self.events = [] #backup of the original sites self.sites = [] #backup of all circle events self.circles = [] #storage of breakpoint tuples -> halfedge self.halfEdges = {} #The bbox of the diagram self.bbox = bbox VEvent.offset = self.bbox[3] - self.bbox[1] #The Beach Line Data Structure self.beachline = None #The sweep line position self.sweep_position = None #The output voronoi diagram as a DCEL self.dcel = DCEL(bbox=bbox) #File name to pickle data to: self.save_file_name = save_name self.debug_draw = debug_draw self.debug = Voronoi_Debug(n, image_dir, self)
def reset(self): """ Reset the internal data structures """ self.dcel = DCEL(bbox=self.bbox) self.events = [] self.circles = [] self.halfEdges = {} self.sweep_position = None self.beachline = rbtree.RBTree(cmpFunc=arc_comparison, eqFunc=arc_equality) self.current_step = 0
def load_file_and_average(): logging.info("Opening DCEL pickle") the_dcel = DCEL.loadfile(DCEL_PICKLE) #Select a number of faces to fill: if len(the_dcel.faces) > 0: NUM_OF_FACES = min(VORONOI_SIZE, 10) aface = choice(list(the_dcel.faces), NUM_OF_FACES) for x in aface: x.data = {FaceE.FILL: True} #Draw if DRAW_FINAL: logging.info("Drawing final diagram") utils.drawing.clear_canvas(ctx, bbox=bbox) utils.dcel.drawing.drawDCEL(ctx, the_dcel) utils.drawing.write_to_png(surface, "{}__FINAL".format(savePath))
class Voronoi: """ Creates a random selection of points, and step by step constructs a voronoi diagram """ def __init__(self, sizeTuple, num_of_nodes=10, bbox=BBOX, save_name=SAVENAME, debug_draw=False, n=10, max_steps=MAX_STEPS): assert(isinstance(sizeTuple, tuple)) assert(isinstance(bbox, np.ndarray)) assert(bbox.shape == (4,)) self.current_step = 0 self.sX = sizeTuple[0] self.sY = sizeTuple[1] self.nodeSize = num_of_nodes self.max_steps = max_steps #Min Heap of site/circle events self.events = [] #backup of the original sites self.sites = [] #backup of all circle events self.circles = [] #storage of breakpoint tuples -> halfedge self.halfEdges = {} #The bbox of the diagram self.bbox = bbox VEvent.offset = self.bbox[3] - self.bbox[1] #The Beach Line Data Structure self.beachline = None #The sweep line position self.sweep_position = None #The output voronoi diagram as a DCEL self.dcel = DCEL(bbox=bbox) #File name to pickle data to: self.save_file_name = save_name self.debug_draw = debug_draw self.debug = Voronoi_Debug(n, image_dir, self) #-------------------- # PUBLIC METHODS #-------------------- def reset(self): """ Reset the internal data structures """ self.dcel = DCEL(bbox=self.bbox) self.events = [] self.circles = [] self.halfEdges = {} self.sweep_position = None self.beachline = rbtree.RBTree(cmpFunc=arc_comparison, eqFunc=arc_equality) self.current_step = 0 def initGraph(self,data=None,rerun=False): """ Create a graph of initial random sites """ logging.debug("Initialising graph") self.reset() values = data if values is None and not rerun: values = self.load_graph() assert(values is None or isinstance(values, np.ndarray)) #create a (n,2) array of coordinates for the sites, if no data has been loaded if values is None or len(values) != self.nodeSize: logging.debug("Generating values") for n in range(self.nodeSize): rndAmnt = random.random((1,2)) #scale the new site scaler = self.bbox.reshape((2,2)).transpose() newSite = scaler[:,0] + (rndAmnt * (scaler[:,1] - scaler[:,0])) if values is None: values = newSite else: values = np.row_stack((values,newSite)) #setup the initial site events: usedCoords = [] for site in values: #Avoid duplications: try: if (site[0],site[1]) in usedCoords: logging.warn("Skipping Duplicate: {}".format(site)) continue except: IPython.embed(simple_prompt=True) #Create an empty face for the site futureFace = self.dcel.newFace(site) event = SiteEvent(site,face=futureFace) heapq.heappush(self.events,event) self.sites.append(event) usedCoords.append((site[0],site[1])) #Save the nodes if not rerun: self.save_graph(values) return values def relax(self, amnt=0.5, faces=None): """ Having calculated the voronoi diagram, use the centroids of the faces instead of the sites, and rerun the calculation. Can be passed in a subset of faces """ assert(not bool(self.events)) if faces is None: faces = self.dcel.faces #Figure out any faces that are excluded faceIndices = set([x.index for x in faces]) otherFaceSites = np.array([x.site for x in self.dcel.faces if x.index not in faceIndices]) #Get a line of origin - centroid lines = np.array([np.concatenate((x.site, x.getAvgCentroid())) for x in faces]) #Move along that line toward the centroid newSites = utils.math.sampleAlongLine(lines, amnt).reshape((len(lines),2)) #Combine with excluded faces if len(otherFaceSites) > 0 and len(newSites) > 0: totalSites = np.row_stack((newSites, otherFaceSites)) elif len(newSites) > 0: totalSites = newSites else: totalSites = otherFaceSites assert(len(self.dcel.faces) == len(totalSites)) #Setup the datastructures with the new sites self.reset() self.initGraph(data=newSites,rerun=True) self.calculate_to_completion() def calculate_to_completion(self): """ Calculate the entire voronoi for all points """ finished = False #Max Steps for a guaranteed exit while not finished and self.current_step < self.max_steps: logging.debug("----------------------------------------") logging.debug("Calculating step: {}".format(self.current_step)) finished = self._calculate() if self.debug_draw: self.debug.draw_intermediate_states(self.current_step, dcel=True, text=True) self.current_step += 1 def finalise_DCEL(self, constrain_to_bbox=True, radius=100): """ Cleanup the DCEL of the voronoi diagram, completing faces and constraining to a bbox """ if bool(self.events): logging.warning("Finalising with events still to process") logging.debug("-------------------- Finalising DCEL") logging.debug(self.dcel) self._update_arcs(self.sweep_position.y() - 1000) #Not a pure DCEL operation as it requires curve intersection: self._complete_edges() self.dcel.purge() tempbbox = self.bbox + np.array([100,100,-100,-100]) #np.array([100,100,-100,-100]) if constrain_to_bbox: #modify or mark edges outside bbox self.dcel.constrain_to_bbox(tempbbox, force=True) else: centre = bbox_centre(self.bbox) self.dcel.constrain_to_circle(centre, radius) self.dcel.purge() logging.debug("---------- Constrained to bbox") #ensure CCW ordering for f in self.dcel.faces: f.fixup(tempbbox) #cleanup faces logging.debug("---------- Fixed up faces") self.dcel.purge() logging.debug("---------- Purged 3") logging.debug(self.dcel) self.dcel.verify_all() return self.dcel def save_graph(self,values): with open(self.save_file_name,'wb') as f: pickle.dump(values,f) def load_graph(self): if isfile(self.save_file_name): with open(self.save_file_name,'rb') as f: return pickle.load(f) #-------------------- # PRIVATE METHODS #-------------------- def _calculate(self): """ Calculate the next step of the voronoi diagram, Return True on completion, False otherwise """ if not bool(self.events): #finished calculating, early exit return True ##Get the next event event = heapq.heappop(self.events) #update the sweep position self.sweep_position = event logging.debug("Sweep position: {}".format(self.sweep_position.loc)) #update the arcs: self._update_arcs(self.sweep_position.y()) #handle the event: if isinstance(event,SiteEvent): self._handleSiteEvent(event) elif isinstance(event,CircleEvent): if event.active: self._handleCircleEvent(event) else: logging.debug("-------------------- Skipping deactivated circle event") logging.debug(event) else: raise Exception("Unrecognised Event") return False #---------- # MAIN VORONOI CALLS #---------- def _handleSiteEvent(self,event): """ provided with a site event, add it to the beachline in the appropriate place then update/remove any circle events that trios of arcs generate """ assert(isinstance(event, SiteEvent)) logging.debug("Handling Site Event: {}".format(event)) #The new parabola made from the site new_arc = Parabola(*event.loc,self.sweep_position.y()) #get the x position of the event xPos = new_arc.fx #if beachline is empty: add and return if not bool(self.beachline): newNode = self.beachline.insert(new_arc)[0] newNode.data['face'] = event.face return #Otherwise, slot the arc between existing nodes closest_node, direction = self._get_closest_arc_node(xPos) assert(closest_node is not None) #remove the obsolete circle event self._delete_circle_events(closest_node) new_node, duplicate_node = self._split_beachline(direction, closest_node, new_arc, event.face) #Create an edge between the two nodes, without origin points yet logging.debug("Adding edge on side: {}".format(direction)) node_face = closest_node.data['face'] if direction is Directions.LEFT: theFace = event.face twinFace = node_face nodePair = (new_node, closest_node) else: theFace = node_face twinFace = event.face nodePair = (closest_node, new_node) newEdge = self.dcel.newEdge(None, None, face=theFace, twinFace=twinFace) self._storeEdge(newEdge, *nodePair) self._cleanup_edges(direction, newEdge, new_node, closest_node, duplicate_node) #create circle events: self._calculate_circle_events(new_node) def _handleCircleEvent(self,event): """ provided a circle event, add a new vertex to the dcel, then update the beachline to connect the two sides of the arc that has disappeared """ assert(isinstance(event, CircleEvent)) logging.debug("Handling Circle Event: {}".format(event)) #remove disappearing arc from tree #and update breakpoints, remove false alarm circle events node = event.source pre = node.getPredecessor() suc = node.getSuccessor() assert('face' in pre.data) assert('face' in suc.data) self._delete_circle_events(node, pre, suc, event) #add the centre of the circle causing the event as a vertex record logging.debug("Creating Vertex") newVertex = self.dcel.newVertex(event.vertex) #attach the vertex as a defined point in the half edges for the three faces, #these faces are pre<->node and node<->succ e1 = self._getEdge(pre,node) e2 = self._getEdge(node,suc) #create two half-edge records for the new breakpoint of the beachline logging.debug("Creating a new half-edge {}-{}".format(pre,suc)) newEdge = self.dcel.newEdge(newVertex, None,face=pre.data['face'],twinFace=suc.data['face']) if e1: #predecessor face logging.debug("Adding vertex to {}-{}".format(pre,node)) assert(e1.face == pre.data['face']) assert(e1.twin.face == node.data['face']) e1.addVertex(newVertex) e1.addPrev(newEdge, force=True) else: logging.debug("No r-edge found for {}-{}".format(pre,node)) IPython.embed(simple_prompt=True) if e2: #successor face logging.debug("Adding vertex to {}-{}".format(node,suc)) assert(e2.twin.face == suc.data['face']) assert(e2.face == node.data['face']) e2.addVertex(newVertex) e2.twin.addNext(newEdge.twin, force=True) else: logging.debug("No r-edge found for {}-{}".format(node,suc)) IPython.embed(simple_prompt=True) #store the new edge, but only for the open breakpoint #the breakpoint being the predecessor and successor, now partners following #removal of the middle node above in this function self._storeEdge(newEdge,pre,suc) #delete the node, no longer needed as the arc has reduced to 0 logging.debug("Pre-Deletion: {}".format(self.beachline.get_chain())) self.beachline.delete(node) logging.debug("Post-Deletion: {}".format(self.beachline.get_chain())) #recheck for new circle events if pre: self._calculate_circle_events(pre,left=False, right=True) if suc: self._calculate_circle_events(suc,left=True, right=False) #---------- # UTILITIES #---------- def _cleanup_edges(self, direction, edge, new_node, node, duplicate_node): """ if there was an edge of closest_arc -> closest_arc.successor: update it because closest_arc is not adjacent to successor any more, duplicate_node is """ if direction is Directions.LEFT: logging.debug("Cleaning up left") dup_node_sibling = duplicate_node.getPredecessor() if dup_node_sibling is not None: e1 = self._getEdge(dup_node_sibling, node) if e1 is not None: self._removeEdge(dup_node_sibling, node) self._storeEdge(e1,dup_node_sibling, duplicate_node) else: logging.debug("Cleaning up right") dup_node_sibling = duplicate_node.getSuccessor() if dup_node_sibling is not None: e1 = self._getEdge(node, dup_node_sibling) if e1 is not None: self._removeEdge(node, dup_node_sibling) self._storeEdge(e1, duplicate_node, dup_node_sibling) if direction is Directions.LEFT: self._storeEdge(edge, new_node, node) self._storeEdge(edge.twin, duplicate_node, new_node) else: self._storeEdge(edge, node, new_node) self._storeEdge(edge.twin, new_node, duplicate_node) def _get_closest_arc_node(self, xPos): #search for the breakpoint interval of the beachline closest_arc_node, direction = self.beachline.search(xPos, closest=True) if closest_arc_node is not None: logging.debug("Closest Arc Triple: {} *{}* {}".format(closest_arc_node.getPredecessor(), closest_arc_node, closest_arc_node.getSuccessor())) logging.debug("Direction: {}".format(direction)) return (closest_arc_node, direction) def _split_beachline(self, direction, node, arc, event_face): #If site is directly below the arc, or on the right of the arc, add it as a successor if direction is Directions.RIGHT: new_node = self.beachline.insert_successor(node, arc) duplicate_node = self.beachline.insert_successor(new_node, node.value) else: #otherwise add it as a predecessor new_node = self.beachline.insert_predecessor(node, arc) duplicate_node = self.beachline.insert_predecessor(new_node, node.value) assert(isinstance(new_node, rbtree.Node)) #add in the faces as a data point for the new node and duplicate new_node.data['face'] = event_face duplicate_node.data['face'] = node.data['face'] #Debug the new triple: [ A, B, A] tripleString = "-".join([repr(x) for x in [node,new_node,duplicate_node]]) logging.debug("Split {} into {}".format(repr(node),tripleString)) return new_node, duplicate_node #-------------------- Fortune Methods def _calculate_circle_events(self,node,left=True,right=True): """ Given an arc node, get the arcs either side, and determine if/when it will disappear """ logging.debug("Calculating circle events for: {}".format(node)) #Generate a circle event for left side, and right side left_triple = self.beachline.get_predecessor_triple(node) right_triple = self.beachline.get_successor_triple(node) #Calculate chords and determine circle event point: #add circle event to events and the relevant leaf if left_triple: logging.debug("Calc Left Triple: {}".format("-".join([str(x) for x in left_triple]))) if left and left_triple and left_triple[0].value != left_triple[2].value: left_points = np.array([x.value.get_focus() for x in left_triple]) left_circle = utils.math.get_circle_3p(*left_points) #possibly use ccw for this, with glpoc from below if left_circle is not None and isClockwise(*left_points): left_circle_loc = utils.math.get_lowest_point_on_circle(*left_circle) #check the l_t_p/s arent in this circle #note: swapped this to add on the right ftm self._add_circle_event(left_circle_loc,left_triple[1],left_circle[0],left=True) else: logging.debug("Left points failed: {}".format(left_points)) if right_triple: logging.debug("Calc Right Triple: {}".format("-".join([str(x) for x in right_triple]))) if right and right_triple and right_triple[0].value != right_triple[2].value: right_points = np.array([x.value.get_focus() for x in right_triple]) right_circle = utils.math.get_circle_3p(*right_points) if right_circle is not None and isClockwise(*right_points): right_circle_loc = utils.math.get_lowest_point_on_circle(*right_circle) #note: swapped this to add on the left ftm self._add_circle_event(right_circle_loc,right_triple[1],right_circle[0], left=False) else: logging.debug("Right points failed: {}".format(right_points)) def _update_arcs(self,d): """ Trigger the update of all stored arcs with a new frontier line position """ self.beachline.update_values(lambda v,q: v.update_d(q) ,d) #-------------------- DCEL Completion def _complete_edges(self): """ get any infinite edges, and complete their intersections """ logging.debug("\n---------- Infinite Edges Completion") i = 0 #get only the halfedges that are originless, rather than full edges that are infinite i_pairs = [x for x in self.halfEdges.items() if x[1].isInfinite()] logging.debug("Origin-less half edges num: {}".format(len(i_pairs))) #---- #i_pairs = [((breakpoint nodes),edge)] for (bw,c) in i_pairs: i += 1 #a and b are nodes a = bw.bp1 b = bw.bp2 logging.debug("{} Infinite Edge resolution: {}-{}, infinite? {}".format(i,a,b,c.isInfinite())) if c.origin is None and c.twin.origin is None: logging.debug("Found an undefined edge, cleaning up") c.markForCleanup() continue if not c.isInfinite(): continue #raise Exception("An Edge is not infinite") #intersect the breakpoints to find the vertex point intersection = a.value.intersect(b.value) if intersection is None or len(intersection) < 1: raise Exception("No intersections detected when completing an infinite edge") elif len(intersection) == 2: verts = [x for x in c.getVertices() if x is not None] assert(len(verts) == 1) lines = [] lines += bound_line_in_bbox(np.array([verts[0].toArray(), intersection[0]]), self.bbox) lines += bound_line_in_bbox(np.array([verts[0].toArray(), intersection[1]]), self.bbox) distances = np.array([get_distance_raw(*x) for x in lines]) minLine = np.argmin(distances) newVertex = self.dcel.newVertex(lines[minLine][1]) c.addVertex(newVertex) if c.isInfinite(): logging.debug("Edge is still infinite, marking for cleanup") c.markForCleanup() #-------------------- Beachline Edge Interaction def _storeEdge(self,edge,bp1,bp2): """ Store an incomplete edge by the 2 pairs of nodes that define the breakpoints """ assert(isinstance(edge, HalfEdge)) assert(isinstance(bp1, rbtree.Node)) assert(isinstance(bp2, rbtree.Node)) if self._hasEdge(bp1,bp2) and self._getEdge(bp1,bp2) != edge: raise Exception("Overrighting edge breakpoint: {}, {}".format(bp1, bp2)) logging.debug("Storing Edge: ({},{}): {}".format(bp1, bp2, edge)) self.halfEdges[BreakWrapper(bp1,bp2)] = edge def _hasEdge(self,bp1,bp2): assert(bp1 is None or isinstance(bp1, rbtree.Node)) assert(bp2 is None or isinstance(bp2, rbtree.Node)) return BreakWrapper(bp1,bp2) in self.halfEdges def _getEdge(self,bp1,bp2): assert(bp1 is None or isinstance(bp1, rbtree.Node) ) assert(bp2 is None or isinstance(bp2, rbtree.Node)) if self._hasEdge(bp1,bp2): return self.halfEdges[BreakWrapper(bp1,bp2)] else: return None def _removeEdge(self,bp1,bp2): assert(isinstance(bp1, rbtree.Node)) assert(isinstance(bp2, rbtree.Node)) if not self._hasEdge(bp1,bp2): raise Exception("trying to remove a non-existing edge") logging.debug("Removing Edge: ({},{}) : {}".format(bp1,bp2, self.halfEdges[BreakWrapper(bp1,bp2)])) del self.halfEdges[BreakWrapper(bp1,bp2)] #-------------------- Circle Event Interaction def _add_circle_event(self,loc,sourceNode,voronoiVertex,left=True): if loc[1] > self.sweep_position.y():# or np.allclose(loc[1],self.sweep_position.y()): logging.debug("Breaking out of add circle event: Wrong side of Beachline") return event = CircleEvent(loc,sourceNode,voronoiVertex,i=self.current_step, left=left) logging.debug("Adding: {}".format(event)) heapq.heappush(self.events,event) self.circles.append(event) def _delete_circle_events(self,node, pre=None, post=None, event=None): """ Deactivate a circle event rather than deleting it. This means instead of removal and re-heapifying, you just skip the event when you come to process it """ logging.debug("Deactivating Circle Event: {}".format(node)) if node is not None: if CIRCLE_EVENTS.LEFT in node.data: node.data[CIRCLE_EVENTS.LEFT].deactivate() if CIRCLE_EVENTS.RIGHT in node.data: node.data[CIRCLE_EVENTS.RIGHT].deactivate() if pre != None and CIRCLE_EVENTS.RIGHT in pre.data: pre.data[CIRCLE_EVENTS.RIGHT].deactivate() if post != None and CIRCLE_EVENTS.LEFT in post.data: post.data[CIRCLE_EVENTS.LEFT].deactivate()
if isOrigin: #origin is closer, replace the twin he.addVertex(orig1) he.addVertex(newVert) else: #twin is closer, replace the origin he.addVertex(newVert) he.addVertex(orig2) modified_edges.append(he) #todo: fixup faces dcel.complete_faces() if __name__ == "__main__": the_dcel = DCEL.loadfile(DCEL_PICKLE) the_dcel.verify_faces_and_edges() #Pre MOD utils.drawing.clear_canvas(ctx, colour=[0, 0, 0, 1]) utils.dcel.dcel_drawing.drawDCEL(ctx, the_dcel, edges=True, faces=False) ctx.set_source_rgba(0, 1, 0, 1) ctx.set_line_width(0.002) utils.drawing.drawCircle(ctx, 0.5, 0.5, RADIUS, fill=False) utils.drawing.write_to_png(surface, save_string_unmod) #Post MOD utils.drawing.clear_canvas(ctx, colour=[0, 0, 0, 1]) utils.dcel.dcel_drawing.drawDCEL(ctx, the_dcel, edges=False, faces=True, verts=True)