def combine_seperate(lg_layout): """Given a line graph layout (List of (homography, line graph) pairs) this merges them all together into a single LineGraph. This version doesn't do anything clever.""" args = [] for hg, lg in lg_layout: args.append(hg) args.append(lg) ret = LineGraph() ret.from_many(*args) return ret
def gen_bias(lg, hg): """Helper function - creates and returns a bias object for use when creating Glyphs. Basically weights each line with the amount of ink on it, so when a writter uses every other line it strongly biases towards letters being assigned to the lines they wrote on.""" bias = defaultdict(float) # Transform the line graph to line space... ls_lg = LineGraph() ls_lg.from_many(lg) ihg = la.inv(hg) ls_lg.transform(ihg, True) # Add weight from all of the line segments... for ei in xrange(ls_lg.edge_count): edge = ls_lg.get_edge(ei) vf = ls_lg.get_vertex(edge[0]) vt = ls_lg.get_vertex(edge[1]) dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass = (vf[5] + vt[5]) * numpy.sqrt(dx * dx + dy * dy) line = int(numpy.floor(0.5 * (vt[1] + vf[1]))) bias[line] += mass # Normalise and return... maximum = max(bias.values()) for key in bias.keys(): bias[key] /= maximum return bias
def gen_bias(lg, hg): """Helper function - creates and returns a bias object for use when creating Glyphs. Basically weights each line with the amount of ink on it, so when a writter uses every other line it strongly biases towards letters being assigned to the lines they wrote on.""" bias = defaultdict(float) # Transform the line graph to line space... ls_lg = LineGraph() ls_lg.from_many(lg) ihg = la.inv(hg) ls_lg.transform(ihg, True) # Add weight from all of the line segments... for ei in xrange(ls_lg.edge_count): edge = ls_lg.get_edge(ei) vf = ls_lg.get_vertex(edge[0]) vt = ls_lg.get_vertex(edge[1]) dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass = (vf[5] + vt[5]) * numpy.sqrt(dx*dx + dy*dy) line = int(numpy.floor(0.5 * (vt[1] + vf[1]))) bias[line] += mass # Normalise and return... maximum = max(bias.values()) for key in bias.keys(): bias[key] /= maximum return bias
def add(self, fn): """Given a filename for a linegraph file this loads it, extracts all chunks and stores them in the db.""" if fn in self.fnl: return 0 self.fnl.append(fn) self.kdtree = None # Load the LineGraph from the given filename... data = ply2.read(fn) lg = LineGraph() lg.from_dict(data) texture = os.path.normpath(os.path.join(os.path.dirname(fn), data['meta']['image'])) # Calculate the radius scaler and distance for this line graph, by calculating the median radius... rads = map(lambda i: lg.get_vertex(i)[5], xrange(lg.vertex_count)) rads.sort() median_radius = rads[len(rads)//2] radius_mult = 1.0 / median_radius dist = self.dist * median_radius # Chop it up into chains, extract chunks and store them in the database... ret = 0 for raw_chain in lg.chains(): for chain in filter(lambda c: len(c)>1, [raw_chain, raw_chain[::-1]]): head = 0 tail = 0 length = 0.0 while True: # Move tail so its long enough, or has reached the end... while length<dist and tail+1<len(chain): tail += 1 v1 = lg.get_vertex(chain[tail-1]) v2 = lg.get_vertex(chain[tail]) length += numpy.sqrt((v1[0]-v2[0])**2 + (v1[1]-v2[1])**2) # Create the chunk... chunk = LineGraph() chunk.from_vertices(lg, chain[head:tail+1]) # Tag it... chunk.add_tag(0, 0.1, 'file:%s'%fn) chunk.add_tag(0, 0.2, 'texture:%s'%texture) # Store it... self.chunks.append((chunk, median_radius)) ret += 1 # If tail is at the end exit the loop... if tail+1 >= len(chain): break # Move head along for the next chunk... to_move = dist * self.factor while to_move>0.0 and head+2<len(chain): head += 1 v1 = lg.get_vertex(chain[head-1]) v2 = lg.get_vertex(chain[head]) offset = numpy.sqrt((v1[0]-v2[0])**2 + (v1[1]-v2[1])**2) length -= offset to_move -= offset return ret
def convert(self, lg, choices = 1, adv_match = False, textures = TextureCache(), memory = 0): """Given a line graph this chops it into chunks, matches each chunk to the database of chunks and returns a new line graph with these chunks instead of the original. Output will involve heavy overlap requiring clever blending. choices is the number of options it select from the db - it grabs this many closest to the requirements and then randomly selects from them. If adv_match is True then instead of random selection from the choices it does a more advanced match, and select the best match in terms of colour distance from already-rendered chunks. This option is reasonably expensive. memory is how many recently use chunks to remember, to avoid repetition.""" if memory > (choices - 1): memory = choices - 1 # If we have no data just return the input... if self.empty(): return lg # Check if the indexing structure is valid - if not create it... if self.kdtree==None: data = numpy.array(map(lambda p: self.feature_vect(p[0], p[1]), self.chunks), dtype=numpy.float) self.kdtree = scipy.spatial.cKDTree(data, 4) # Calculate the radius scaler and distance for this line graph, by calculating the median radius... rads = map(lambda i: lg.get_vertex(i)[5], xrange(lg.vertex_count)) rads.sort() median_radius = rads[len(rads)//2] radius_mult = 1.0 / median_radius dist = self.dist * median_radius # Create the list into which we dump all the chunks that will make up the return... chunks = [] temp = LineGraph() # List of recently used chunks, to avoid obvious patterns... recent = [] # If advanced match we need a Composite of the image thus far, to compare against... if adv_match: canvas = Composite() min_x, max_x, min_y, max_y = lg.get_bounds() canvas.set_size(int(max_x+8), int(max_y+8)) # Iterate the line graph, choping it into chunks and matching a chunk to each chop... for chain in lg.chains(): head = 0 tail = 0 length = 0.0 while True: # Move tail so its long enough, or has reached the end... while length<dist and tail+1<len(chain): tail += 1 v1 = lg.get_vertex(chain[tail-1]) v2 = lg.get_vertex(chain[tail]) length += numpy.sqrt((v1[0]-v2[0])**2 + (v1[1]-v2[1])**2) # Extract a feature vector for this chunk... temp.from_vertices(lg, chain[head:tail+1]) fv = self.feature_vect(temp, median_radius) # Select a chunk from the database... if choices==1: selected = self.kdtree.query(fv)[1] orig_chunk = self.chunks[selected] else: options = list(self.kdtree.query(fv, choices)[1]) options = filter(lambda v: v not in recent, options) if not adv_match: selected = random.choice(options) orig_chunk = self.chunks[selected] else: cost = 1e64 * numpy.ones(len(options)) for i, option in enumerate(options): fn = filter(lambda t: t[0].startswith('texture:'), self.chunks[option][0].get_tags()) if len(fn)!=0: fn = fn[0][0][len('texture:'):] tex = textures[fn] chunk = LineGraph() chunk.from_many(self.chunks[option][0]) chunk.morph_to(lg, chain[head:tail+1]) part = canvas.draw_line_graph(chunk) cost[i] = canvas.cost_texture_nearest(tex, part) selected = options[numpy.argmin(cost)] orig_chunk = self.chunks[selected] # Update recent list... recent.append(selected) if len(recent)>memory: recent.pop(0) # Distort it to match the source line graph... chunk = LineGraph() chunk.from_many(orig_chunk[0]) chunk.morph_to(lg, chain[head:tail+1]) # Record it for output... chunks.append(chunk) # If advanced matching is on write it out to canvas, so future choices will take it into account... if adv_match: fn = filter(lambda t: t[0].startswith('texture:'), chunk.get_tags()) if len(fn)!=0: fn = fn[0][0][len('texture:'):] tex = textures[fn] part = canvas.draw_line_graph(chunk) canvas.paint_texture_nearest(tex, part) # If tail is at the end exit the loop... if tail+1 >= len(chain): break # Move head along for the next chunk... to_move = dist * self.factor while to_move>0.0 and head+2<len(chain): head += 1 v1 = lg.get_vertex(chain[head-1]) v2 = lg.get_vertex(chain[head]) offset = numpy.sqrt((v1[0]-v2[0])**2 + (v1[1]-v2[1])**2) length -= offset to_move -= offset # Return the final line graph... ret = LineGraph() ret.from_many(*chunks) return ret
def add(self, fn): """Given a filename for a linegraph file this loads it, extracts all chunks and stores them in the db.""" if fn in self.fnl: return 0 self.fnl.append(fn) self.kdtree = None # Load the LineGraph from the given filename... data = ply2.read(fn) lg = LineGraph() lg.from_dict(data) texture = os.path.normpath( os.path.join(os.path.dirname(fn), data['meta']['image'])) # Calculate the radius scaler and distance for this line graph, by calculating the median radius... rads = map(lambda i: lg.get_vertex(i)[5], xrange(lg.vertex_count)) rads.sort() median_radius = rads[len(rads) // 2] radius_mult = 1.0 / median_radius dist = self.dist * median_radius # Chop it up into chains, extract chunks and store them in the database... ret = 0 for raw_chain in lg.chains(): for chain in filter(lambda c: len(c) > 1, [raw_chain, raw_chain[::-1]]): head = 0 tail = 0 length = 0.0 while True: # Move tail so its long enough, or has reached the end... while length < dist and tail + 1 < len(chain): tail += 1 v1 = lg.get_vertex(chain[tail - 1]) v2 = lg.get_vertex(chain[tail]) length += numpy.sqrt((v1[0] - v2[0])**2 + (v1[1] - v2[1])**2) # Create the chunk... chunk = LineGraph() chunk.from_vertices(lg, chain[head:tail + 1]) # Tag it... chunk.add_tag(0, 0.1, 'file:%s' % fn) chunk.add_tag(0, 0.2, 'texture:%s' % texture) # Store it... self.chunks.append((chunk, median_radius)) ret += 1 # If tail is at the end exit the loop... if tail + 1 >= len(chain): break # Move head along for the next chunk... to_move = dist * self.factor while to_move > 0.0 and head + 2 < len(chain): head += 1 v1 = lg.get_vertex(chain[head - 1]) v2 = lg.get_vertex(chain[head]) offset = numpy.sqrt((v1[0] - v2[0])**2 + (v1[1] - v2[1])**2) length -= offset to_move -= offset return ret
def convert(self, lg, choices=1, adv_match=False, textures=TextureCache(), memory=0): """Given a line graph this chops it into chunks, matches each chunk to the database of chunks and returns a new line graph with these chunks instead of the original. Output will involve heavy overlap requiring clever blending. choices is the number of options it select from the db - it grabs this many closest to the requirements and then randomly selects from them. If adv_match is True then instead of random selection from the choices it does a more advanced match, and select the best match in terms of colour distance from already-rendered chunks. This option is reasonably expensive. memory is how many recently use chunks to remember, to avoid repetition.""" if memory > (choices - 1): memory = choices - 1 # If we have no data just return the input... if self.empty(): return lg # Check if the indexing structure is valid - if not create it... if self.kdtree == None: data = numpy.array(map(lambda p: self.feature_vect(p[0], p[1]), self.chunks), dtype=numpy.float) self.kdtree = scipy.spatial.cKDTree(data, 4) # Calculate the radius scaler and distance for this line graph, by calculating the median radius... rads = map(lambda i: lg.get_vertex(i)[5], xrange(lg.vertex_count)) rads.sort() median_radius = rads[len(rads) // 2] radius_mult = 1.0 / median_radius dist = self.dist * median_radius # Create the list into which we dump all the chunks that will make up the return... chunks = [] temp = LineGraph() # List of recently used chunks, to avoid obvious patterns... recent = [] # If advanced match we need a Composite of the image thus far, to compare against... if adv_match: canvas = Composite() min_x, max_x, min_y, max_y = lg.get_bounds() canvas.set_size(int(max_x + 8), int(max_y + 8)) # Iterate the line graph, choping it into chunks and matching a chunk to each chop... for chain in lg.chains(): head = 0 tail = 0 length = 0.0 while True: # Move tail so its long enough, or has reached the end... while length < dist and tail + 1 < len(chain): tail += 1 v1 = lg.get_vertex(chain[tail - 1]) v2 = lg.get_vertex(chain[tail]) length += numpy.sqrt((v1[0] - v2[0])**2 + (v1[1] - v2[1])**2) # Extract a feature vector for this chunk... temp.from_vertices(lg, chain[head:tail + 1]) fv = self.feature_vect(temp, median_radius) # Select a chunk from the database... if choices == 1: selected = self.kdtree.query(fv)[1] orig_chunk = self.chunks[selected] else: options = list(self.kdtree.query(fv, choices)[1]) options = filter(lambda v: v not in recent, options) if not adv_match: selected = random.choice(options) orig_chunk = self.chunks[selected] else: cost = 1e64 * numpy.ones(len(options)) for i, option in enumerate(options): fn = filter(lambda t: t[0].startswith('texture:'), self.chunks[option][0].get_tags()) if len(fn) != 0: fn = fn[0][0][len('texture:'):] tex = textures[fn] chunk = LineGraph() chunk.from_many(self.chunks[option][0]) chunk.morph_to(lg, chain[head:tail + 1]) part = canvas.draw_line_graph(chunk) cost[i] = canvas.cost_texture_nearest( tex, part) selected = options[numpy.argmin(cost)] orig_chunk = self.chunks[selected] # Update recent list... recent.append(selected) if len(recent) > memory: recent.pop(0) # Distort it to match the source line graph... chunk = LineGraph() chunk.from_many(orig_chunk[0]) chunk.morph_to(lg, chain[head:tail + 1]) # Record it for output... chunks.append(chunk) # If advanced matching is on write it out to canvas, so future choices will take it into account... if adv_match: fn = filter(lambda t: t[0].startswith('texture:'), chunk.get_tags()) if len(fn) != 0: fn = fn[0][0][len('texture:'):] tex = textures[fn] part = canvas.draw_line_graph(chunk) canvas.paint_texture_nearest(tex, part) # If tail is at the end exit the loop... if tail + 1 >= len(chain): break # Move head along for the next chunk... to_move = dist * self.factor while to_move > 0.0 and head + 2 < len(chain): head += 1 v1 = lg.get_vertex(chain[head - 1]) v2 = lg.get_vertex(chain[head]) offset = numpy.sqrt((v1[0] - v2[0])**2 + (v1[1] - v2[1])**2) length -= offset to_move -= offset # Return the final line graph... ret = LineGraph() ret.from_many(*chunks) return ret
def __init__(self, lg, seg, hg, extra=0.4, bias=None): """Given a segmented LineGraph and segment number this extracts it, transforms it into the standard coordinate system and stores the homography used to get there. (hg transforms from line space, where there is a line for each y=integer, to the space of the original pixels.) Also records its position on its assigned line and line number so it can be ordered suitably. Does not store connectivity information - that is done later. extra is used for infering the line position, and is extra falloff to have either side of a line voting for it - a smoothing term basically. bias is an optional dictionary indexed by line number that gives a weight to assign to being assigned to that line - used to utilise the fact that data collection asks the writter to use every-other line, which helps avoid misassigned dropped j's for instance.""" if lg == None: return # Extract the line graph... self.lg = LineGraph() self.adjacent = self.lg.from_segment(lg, seg) self.seg = seg # Tranform it to line space... ihg = la.inv(hg) self.lg.transform(ihg, True) # Check if which line its on is tagged - exists as an override for annoying glyphs... line = None for tag in self.lg.get_tags(): if tag[0] == 'line': # We have a tag of line - its position specifies the line the glyph is on... point = self.lg.get_point(tag[1], tag[2]) line = int(numpy.floor(point[1])) break # Record which line it is on and its position along the line... # (Works by assuming that the line is the one below the space where most of the mass of the glyph is. Takes it range to be that within the space, so crazy tails are cut off.) min_x, max_x, min_y, max_y = self.lg.get_bounds() self.source = (0.5 * (min_x + max_x), 0.5 * (min_y + max_y)) if line == None: best_mass = 0.0 self.left_x = min_x self.right_x = max_x line = 0 start = int(numpy.trunc(min_y)) for pl in xrange(start, int(numpy.ceil(max_y))): mass = 0.0 low_y = float(pl) - extra high_y = float(pl + 1) + extra left_x = None right_x = None for es in self.lg.within(min_x, max_x, low_y, high_y): for ei in xrange(*es.indices(self.lg.edge_count)): edge = self.lg.get_edge(ei) vf = self.lg.get_vertex(edge[0]) vt = self.lg.get_vertex(edge[1]) if vf[1] > low_y and vf[1] < high_y and vt[ 1] > low_y and vt[1] < high_y: dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass += (vf[5] + vt[5]) * numpy.sqrt(dx * dx + dy * dy) if left_x == None: left_x = min(vf[0], vt[0]) else: left_x = min(vf[0], vt[0], left_x) if right_x == None: right_x = max(vf[0], vt[0]) else: right_x = max(vf[0], vt[0], right_x) mass *= 1.0 / (1.0 + pl - start ) # Bias to choosing higher, for tails. if bias != None: mass *= bias[pl] if mass > best_mass: best_mass = mass self.left_x = left_x self.right_x = right_x line = pl # Transform it so it is positioned to be sitting on line 1 of y, store the total homography that we have applied... self.offset_x = -min_x self.offset_y = -line hg = numpy.eye(3, dtype=numpy.float32) hg[0, 2] = self.offset_x hg[1, 2] = self.offset_y self.left_x += self.offset_x self.right_x += self.offset_x self.lg.transform(hg) self.transform = numpy.dot(hg, ihg) # Set as empty its before and after glyphs - None if there is no adjacency, or a tuple if there is: (glyph, list of connecting (link glyph, shared vertex in this, shared vertex in glyph, vertex in link glyph on this side, vertex in link glyph on glyph side), empty if none.)... self.left = None self.right = None # Extract the character this glyph represents... tags = self.lg.get_tags() codes = [ t[0] for t in tags if len(filter(lambda c: c != '_', t[0])) == 1 ] self.key = codes[0] if len(codes) != 0 else None self.code = -id(self) # Cache stuff... self.mass = None self.center = None self.feat = None self.v_offset = None
class Glyph: """Represents a glyph, that has been transformed into a suitable coordinate system; includes connectivity information.""" def __init__(self, lg, seg, hg, extra = 0.4, bias = None): """Given a segmented LineGraph and segment number this extracts it, transforms it into the standard coordinate system and stores the homography used to get there. (hg transforms from line space, where there is a line for each y=integer, to the space of the original pixels.) Also records its position on its assigned line and line number so it can be ordered suitably. Does not store connectivity information - that is done later. extra is used for infering the line position, and is extra falloff to have either side of a line voting for it - a smoothing term basically. bias is an optional dictionary indexed by line number that gives a weight to assign to being assigned to that line - used to utilise the fact that data collection asks the writter to use every-other line, which helps avoid misassigned dropped j's for instance.""" if lg==None: return # Extract the line graph... self.lg = LineGraph() self.adjacent = self.lg.from_segment(lg, seg) self.seg = seg # Tranform it to line space... ihg = la.inv(hg) self.lg.transform(ihg, True) # Check if which line its on is tagged - exists as an override for annoying glyphs... line = None for tag in self.lg.get_tags(): if tag[0]=='line': # We have a tag of line - its position specifies the line the glyph is on... point = self.lg.get_point(tag[1], tag[2]) line = int(numpy.floor(point[1])) break # Record which line it is on and its position along the line... # (Works by assuming that the line is the one below the space where most of the mass of the glyph is. Takes it range to be that within the space, so crazy tails are cut off.) min_x, max_x, min_y, max_y = self.lg.get_bounds() self.source = (0.5 * (min_x + max_x), 0.5 * (min_y + max_y)) if line==None: best_mass = 0.0 self.left_x = min_x self.right_x = max_x line = 0 start = int(numpy.trunc(min_y)) for pl in xrange(start, int(numpy.ceil(max_y))): mass = 0.0 low_y = float(pl) - extra high_y = float(pl+1) + extra left_x = None right_x = None for es in self.lg.within(min_x, max_x, low_y, high_y): for ei in xrange(*es.indices(self.lg.edge_count)): edge = self.lg.get_edge(ei) vf = self.lg.get_vertex(edge[0]) vt = self.lg.get_vertex(edge[1]) if vf[1]>low_y and vf[1]<high_y and vt[1]>low_y and vt[1]<high_y: dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass += (vf[5] + vt[5]) * numpy.sqrt(dx*dx + dy*dy) if left_x==None: left_x = min(vf[0], vt[0]) else: left_x = min(vf[0], vt[0], left_x) if right_x==None: right_x = max(vf[0], vt[0]) else: right_x = max(vf[0], vt[0], right_x) mass *= 1.0/(1.0+pl - start) # Bias to choosing higher, for tails. if bias!=None: mass *= bias[pl] if mass>best_mass: best_mass = mass self.left_x = left_x self.right_x = right_x line = pl # Transform it so it is positioned to be sitting on line 1 of y, store the total homography that we have applied... self.offset_x = -min_x self.offset_y = -line hg = numpy.eye(3, dtype=numpy.float32) hg[0,2] = self.offset_x hg[1,2] = self.offset_y self.left_x += self.offset_x self.right_x += self.offset_x self.lg.transform(hg) self.transform = numpy.dot(hg, ihg) # Set as empty its before and after glyphs - None if there is no adjacency, or a tuple if there is: (glyph, list of connecting (link glyph, shared vertex in this, shared vertex in glyph, vertex in link glyph on this side, vertex in link glyph on glyph side), empty if none.)... self.left = None self.right = None # Extract the character this glyph represents... tags = self.lg.get_tags() codes = [t[0] for t in tags if len(filter(lambda c: c!='_', t[0]))==1] self.key = codes[0] if len(codes)!=0 else None self.code = -id(self) # Cache stuff... self.mass = None self.center = None self.feat = None self.v_offset = None def clone(self): """Returns a clone of this Glyph.""" ret = Glyph(None, None, None) ret.lg = self.lg ret.adjacent = self.adjacent ret.seg = self.seg ret.source = self.source ret.left_x = self.left_x ret.right_x = self.right_x ret.offset_x = self.offset_x ret.offset_y = self.offset_y ret.transform = self.transform ret.left = self.left ret.right = self.right ret.key = self.key ret.code = self.code ret.mass = None if self.mass==None else self.mass.copy() ret.center = None if self.center==None else self.center.copy() ret.feat = None if self.feat==None else map(lambda a: a.copy(), self.feat) ret.v_offset = self.v_offset return ret def get_linegraph(self): return self.lg def orig_left_x(self): return self.left_x - self.offset_x def orig_right_x(self): return self.right_x - self.offset_x def get_mass(self): """Returns a vector of [average density, average radius] - used for matching adjacent glyphs.""" if self.mass==None: self.mass = numpy.zeros(2, dtype=numpy.float32) weight = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) weight += 1.0 self.mass += (numpy.array([info[6], info[5]]) - self.mass) / weight return self.mass def get_center(self): """Returns the 'center' of the glyph - its density weighted in an attempt to make it robust to crazy tails.""" if self.center==None: self.center = numpy.zeros(2, dtype=numpy.float32) weight = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[6] # Radius squared * density - proportional to quantity of ink, assuming (correctly as rest of system currently works) even sampling. if w>1e-6: weight += w mult = w / weight self.center[0] += (info[0] - self.center[0]) * mult self.center[1] += (info[1] - self.center[1]) * mult return self.center def get_voffset(self): """Calculates and returns the vertical offset to apply to the glyph that corrects for any systematic bias in its flow calculation.""" if self.v_offset==None: self.v_offset = 0.0 weight = 0.0 truth = self.get_center()[1] # Calculate the estimated offsets from the left side and update the estimate, correctly factoring in the variance of the offset... if self.left!=None: diff, sd = costs.glyph_pair_offset(self.left[0], self, 0.2, True) estimate = self.left[0].get_center()[1] + diff offset = truth - estimate est_weight = 1.0 / (sd**2.0) weight += est_weight self.v_offset += (offset - self.v_offset) * est_weight / weight # Again from the right side... if self.right!=None: diff, sd = costs.glyph_pair_offset(self, self.right[0], 0.2, True) estimate = self.right[0].get_center()[1] - diff offset = truth - estimate est_weight = 1.0 / (sd**2.0) weight += est_weight self.v_offset += (offset - self.v_offset) * est_weight / weight return self.v_offset def most_left(self): """Returns the coordinate of the furthest left vertex in the glyph.""" info = self.lg.get_vertex(0) best_x = info[0] best_y = info[1] for i in xrange(1,self.lg.vertex_count): info = self.lg.get_vertex(0) if info[0]<best_x: best_x = info[0] best_y = info[1] return (best_x, best_y) def most_right(self): """Returns the coordinate of the furthest right vertex in the glyph.""" info = self.lg.get_vertex(0) best_x = info[0] best_y = info[1] for i in xrange(1,self.lg.vertex_count): info = self.lg.get_vertex(0) if info[0]>best_x: best_x = info[0] best_y = info[1] return (best_x, best_y) def get_feat(self): """Calculates and returns a feature for the glyph, or, more accuratly two features, representing (left, right), so some tricks can be done to make their use side dependent (For learning a function for matching to adjacent glyphs.).""" if self.feat==None: # First build a culumative distribution over the x axis range of the glyph... min_x, max_x, min_y, max_y = self.lg.get_bounds() culm = numpy.ones(32, dtype=numpy.float32) culm *= 1e-2 min_x -= 1e-3 max_x += 1e-3 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[6] t = (info[0] - min_x) / (max_x - min_x) t *= (culm.shape[0]-1) low = int(t) high = low + 1 t -= low culm[low] += (1.0 - t) * w culm[high] += t * w culm /= culm.sum() culm = numpy.cumsum(culm) # Now extract all the per sample features... feat_param = {'dir_travel':0.1, 'travel_max':1.0, 'travel_bins':6, 'travel_ratio':0.8, 'pos_bins':3, 'pos_ratio':0.9, 'radius_bins':1, 'density_bins':3} fv = self.lg.features(**feat_param) # Combine them into the two halves, by weighting by the culumative; include density and radius as well... left = numpy.zeros(fv.shape[1]+2, dtype=numpy.float32) right = numpy.zeros(fv.shape[1]+2, dtype=numpy.float32) left_total = 0.0 right_total = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[6] t = (info[0] - min_x) / (max_x - min_x) t *= (culm.shape[0]-1) low = int(t) high = low + 1 t -= low right_w = (1.0-t) * culm[low] + t * culm[high] left_w = 1.0 - right_w left[0] += left_w * info[5] right[0] += right_w * info[5] left[1] += left_w * info[6] right[1] += right_w * info[6] left[2:] += w * left_w * fv[i,:] right[2:] += w * right_w * fv[i,:] left_total += left_w right_total += right_w left[:2] /= left_total right[:2] /= right_total left[2:] /= max(left[2:].sum(), 1e-6) right[2:] /= max(right[2:].sum(), 1e-6) self.feat = (left, right) return self.feat def __str__(self): l = self.left[0].key if self.left!=None else 'None' r = self.right[0].key if self.right!=None else 'None' return 'Glyph %i: key = %s (%s|%s)' % (self.code, self.key, l, r)
def add(self, fn): """Given the filename for a LineGraph file this loads it in and splits it into Glyphs, which it dumps into the db. Does nothing if the file is already loaded. returns the number of glyphs added.""" if fn in self.fnl: return 0 self.fnl.append(fn) # Load the LineGraph from the given filename, and get the homography... f = open(fn, 'r') data = ply2.read(f) f.close() lg = LineGraph() lg.from_dict(data) lg.segment() hg = data['element']['homography']['v'].reshape((3,3)) texture = os.path.normpath(os.path.join(os.path.dirname(fn), data['meta']['image'])) # Create a line bias object to be used in the next step... bias = gen_bias(lg, hg) # First pass - create each glyph object... glyphs = [] for s in xrange(lg.segments): g = Glyph(lg, s, hg, bias = bias) glyphs.append(g if '_' not in map(lambda t: t[0], g.lg.get_tags()) else None) # Second pass - fill in the connectivity information supported by adjacency... link_glyphs = [] for seg, glyph in enumerate(glyphs): if glyph==None: continue if glyph.key==None: continue # Brute force search to find a left partner... if glyph.left==None: best = None for g in glyphs: # Check it satisfies the conditions... if g==None: continue if g.key==None: continue; if id(g)==id(glyph): continue if g.offset_y!=glyph.offset_y: continue if (g.right_x - g.offset_x) > (glyph.right_x - glyph.offset_x): continue # Check its better than the current best... if best==None or (best.right_x - best.offset_x) < (g.right_x - g.offset_x): best = g if best!=None: glyph.left = (best, []) # Brute force search to find a right partner... if glyph.right==None: best = None for g in glyphs: # Check it satisfies the conditions... if g==None: continue if g.key==None: continue; if id(g)==id(glyph): continue if g.offset_y!=glyph.offset_y: continue if (g.left_x - g.offset_x) < (glyph.left_x - glyph.offset_x): continue # Check its better than the current best... if best==None or (best.left_x - best.offset_x) > (g.left_x - g.offset_x): best = g if best!=None: glyph.right = (best, []) # Now we have the best matches find common glyphs to link them, and record them... for other, out in [g for g in [glyph.left, glyph.right] if g!=None]: shared_seg = set([a[1] for a in glyph.adjacent]) & set([a[1] for a in other.adjacent]) for seg in shared_seg: g = glyphs[seg] if g==None: continue # We have a linking glyph - extract the information... glyph_vert = [a[0] for a in glyph.adjacent if a[1]==seg] other_vert = [a[0] for a in other.adjacent if a[1]==seg] link_glyph = [a[0] for a in g.adjacent if a[1]==glyph.seg] link_other = [a[0] for a in g.adjacent if a[1]==other.seg] # Check if we have a multi-link scenario - if so choose links... if len(glyph_vert)>1 or len(other_vert)>1: gv_y = map(lambda v: glyph.lg.get_vertex(v)[1], glyph_vert) ov_y = map(lambda v: other.lg.get_vertex(v)[1], other_vert) if (max(gv_y) - min(ov_y)) > (max(ov_y) - min(gv_y)): glyph_vert = glyph_vert[numpy.argmax(gv_y)] other_vert = other_vert[numpy.argmin(ov_y)] else: glyph_vert = glyph_vert[numpy.argmin(gv_y)] other_vert = other_vert[numpy.argmax(ov_y)] lg_y = map(lambda v: g.lg.get_vertex(v)[1], link_glyph) lo_y = map(lambda v: g.lg.get_vertex(v)[1], link_other) if (max(lg_y) - min(lo_y)) > (max(lo_y) - min(lg_y)): link_glyph = link_glyph[numpy.argmax(lg_y)] link_other = link_other[numpy.argmin(lo_y)] else: link_glyph = link_glyph[numpy.argmin(lg_y)] link_other = link_other[numpy.argmax(lo_y)] else: # Simple scenario... glyph_vert = glyph_vert[0] other_vert = other_vert[0] link_glyph = link_glyph[0] link_other = link_other[0] # Recreate the link as a simple path - its the only way to be safe!.. try: g = g.clone() nlg = LineGraph() link_glyph, link_other = nlg.from_path(g.lg, link_glyph, link_other) g.lg = nlg link_glyphs.append(g) except: continue # Line is broken in the centre - don't use it. # Add the tuple to the storage... out.append((g, glyph_vert, other_vert, link_glyph, link_other)) # Third pass - enforce consistancy... for glyph in glyphs: if glyph==None: continue if glyph.left!=None: if glyph.left[0].right==None or id(glyph.left[0].right[0])!=id(glyph): # Inconsistancy - break it... glyph.left[0].right = None glyph.left = None if glyph.right!=None: if glyph.right[0].left==None or id(glyph.right[0].left[0])!=id(glyph): # Inconsistancy - break it... glyph.right[0].left = None glyph.right = None # Forth pass - add filename tags and add to db (Count and return the glyph count)... count = 0 for glyph in (glyphs+link_glyphs): if glyph==None: continue glyph.lg.add_tag(0, 0.1, 'file:%s'%fn) glyph.lg.add_tag(0, 0.2, 'texture:%s'%texture) glyph.lg.add_tag(0, 0.3, 'link:left:%s'%('n' if glyph.left==None else ('g' if len(glyph.left[1])==0 else 'l'))) glyph.lg.add_tag(0, 0.4, 'link:right:%s'%('n' if glyph.right==None else ('g' if len(glyph.right[1])==0 else 'l'))) glyph.lg.add_tag(0, 0.5, 'source:x:%f' % glyph.source[0]) glyph.lg.add_tag(0, 0.6, 'source:y:%f' % glyph.source[1]) if glyph.key!=None: count += 1 if glyph.key in self.db: self.db[glyph.key].append(glyph) else: self.db[glyph.key] = [glyph] glyph.code = len(self.by_code) glyph.lg.add_tag(0, 0.5, 'code:%i'%glyph.code) self.by_code.append(glyph) return count
def render(lg, border=8, textures=TextureCache(), cleverness=0, radius_growth=3.0, stretch_weight=0.5, edge_weight=0.5, smooth_weight=2.0, alpha_weight=1.0, unary_mult=1.0, overlap_weight=0.0, use_linear=True): """Given a line_graph this will render it, returning a numpy array that represents an image (As the first element in a tuple - second element is how many graph cut problems it solved.). It will transform the entire linegraph to obtain a suitable border. The cleverness parameter indicates how it merges the many bits - 0 means last layer (stupid), 1 means averaging; 2 selecting a border using max flow; 3 using graph cuts to take into account weight as well.""" # Setup the compositor... comp = Composite() min_x, max_x, min_y, max_y = lg.get_bounds() do_transform = False offset_x = 0.0 offset_y = 0.0 if min_x < border: do_transform = True offset_x = border - min_x if min_y < border: do_transform = True offset_y = border - min_y if do_transform: hg = numpy.eye(3, dtype=numpy.float32) hg[0, 2] = offset_x hg[1, 2] = offset_y lg.transform(hg) max_x += offset_x max_y += offset_y comp.set_size(int(max_x + border), int(max_y + border)) # Break the lg into segments, as each can have its own image - draw & paint each in turn... lg.segment() duplicate_sets = dict() for s in xrange(lg.segments): slg = LineGraph() slg.from_segment(lg, s) part = comp.draw_line_graph(slg, radius_growth, stretch_weight) done = False fn = filter(lambda t: t[0].startswith('texture:'), slg.get_tags()) if len(fn) != 0: fn = fn[0][0][len('texture:'):] else: fn = None for pair in filter(lambda t: t[0].startswith('duplicate:'), slg.get_tags()): key = pair[0][len('duplicate:'):] if key in duplicate_sets: duplicate_sets[key].append(part) else: duplicate_sets[key] = [part] tex = textures[fn] if tex is not None: if use_linear: comp.paint_texture_linear(tex, part) else: comp.paint_texture_nearest(tex, part) done = True if not done: comp.paint_test_pattern(part) # Bias towards pixels that are opaque... comp.inc_weight_alpha(alpha_weight) # Arrange for duplicate pairs to have complete overlap, by adding transparent pixels, so graph cuts doesn't create a feather effect... if overlap_weight > 1e-6: for values in duplicate_sets.itervalues(): for i, part1 in enumerate(values): for part2 in values[i:]: comp.draw_pair(part1, part2, overlap_weight) # If requested use maxflow to find optimal cuts, to avoid any real blending... count = 0 if cleverness == 2: count = comp.maxflow_select(edge_weight, smooth_weight, maxflow) elif cleverness == 3: count = comp.graphcut_select(edge_weight, smooth_weight, unary_mult, maxflow) if cleverness == 0: render = comp.render_last() else: render = comp.render_average() # Return the rendered image (If cleverness==0 this will actually do some averaging, otherwise it will just create an image)... return render, count
def stitch_connect(glyph_layout, soft=True, half=False, pair_base=0): """Converts a glyph layout to a linegraph layout. This stitches together the glyphs when it has sufficient information to do so.""" ret = [] # First copy over the actual glyphs... for pair in glyph_layout: if pair is not None: hg, glyph = pair ret.append((hg, glyph.lg)) # Now loop through and identify all pairs that can be stitched together, and stitch them... pair_code = 0 for i in xrange(len(glyph_layout) - 1): # Can't stitch spaces... if glyph_layout[i] is not None and glyph_layout[i + 1] is not None: l_hg, l_glyph = glyph_layout[i] r_hg, r_glyph = glyph_layout[i + 1] matches = costs.match_links(l_glyph, r_glyph) # Iterate and do each pairing in turn... for ml, mr in matches: # Calculate the homographies to put the two line graphs into position... lc_hg = numpy.dot( l_hg, numpy.dot(l_glyph.transform, la.inv(ml[0].transform))) rc_hg = numpy.dot( r_hg, numpy.dot(r_glyph.transform, la.inv(mr[0].transform))) # Copy the links, applying the homographies... lc = LineGraph() lc.from_many(lc_hg, ml[0].lg) rc = LineGraph() rc.from_many(rc_hg, mr[0].lg) # Extract the merge points... blend = [(ml[3], 0.0, mr[4]), (ml[4], 1.0, mr[3])] # Do the blending... lc.blend(rc, blend, soft) # Record via tagging that the two parts are the same entity... pair = 'duplicate:%i,%i' % (pair_base, pair_code) lc.add_tag(0, 0.5, pair) rc.add_tag(0, 0.5, pair) pair_code += 1 # Store the pair of line graphs in the return, with identity homographies... ret.append((numpy.eye(3), lc)) if not half: ret.append((numpy.eye(3), rc)) return ret
def render(lg, border = 8, textures = TextureCache(), cleverness = 0, radius_growth = 3.0, stretch_weight = 0.5, edge_weight = 0.5, smooth_weight = 2.0, alpha_weight = 1.0, unary_mult = 1.0, overlap_weight = 0.0, use_linear = True): """Given a line_graph this will render it, returning a numpy array that represents an image (As the first element in a tuple - second element is how many graph cut problems it solved.). It will transform the entire linegraph to obtain a suitable border. The cleverness parameter indicates how it merges the many bits - 0 means last layer (stupid), 1 means averaging; 2 selecting a border using max flow; 3 using graph cuts to take into account weight as well.""" # Setup the compositor... comp = Composite() min_x, max_x, min_y, max_y = lg.get_bounds() do_transform = False offset_x = 0.0 offset_y = 0.0 if min_x<border: do_transform = True offset_x = border-min_x if min_y<border: do_transform = True offset_y = border-min_y if do_transform: hg = numpy.eye(3, dtype=numpy.float32) hg[0,2] = offset_x hg[1,2] = offset_y lg.transform(hg) max_x += offset_x max_y += offset_y comp.set_size(int(max_x+border), int(max_y+border)) # Break the lg into segments, as each can have its own image - draw & paint each in turn... lg.segment() duplicate_sets = dict() for s in xrange(lg.segments): slg = LineGraph() slg.from_segment(lg, s) part = comp.draw_line_graph(slg, radius_growth, stretch_weight) done = False fn = filter(lambda t: t[0].startswith('texture:'), slg.get_tags()) if len(fn)!=0: fn = fn[0][0][len('texture:'):] else: fn = None for pair in filter(lambda t: t[0].startswith('duplicate:'), slg.get_tags()): key = pair[0][len('duplicate:'):] if key in duplicate_sets: duplicate_sets[key].append(part) else: duplicate_sets[key] = [part] tex = textures[fn] if tex!=None: if use_linear: comp.paint_texture_linear(tex, part) else: comp.paint_texture_nearest(tex, part) done = True if not done: comp.paint_test_pattern(part) # Bias towards pixels that are opaque... comp.inc_weight_alpha(alpha_weight) # Arrange for duplicate pairs to have complete overlap, by adding transparent pixels, so graph cuts doesn't create a feather effect... if overlap_weight>1e-6: for values in duplicate_sets.itervalues(): for i, part1 in enumerate(values): for part2 in values[i:]: comp.draw_pair(part1, part2, overlap_weight) # If requested use maxflow to find optimal cuts, to avoid any real blending... count = 0 if cleverness==2: count = comp.maxflow_select(edge_weight, smooth_weight, maxflow) elif cleverness==3: count = comp.graphcut_select(edge_weight, smooth_weight, unary_mult, maxflow) if cleverness==0: render = comp.render_last() else: render = comp.render_average() # Return the rendered image (If cleverness==0 this will actually do some averaging, otherwise it will just create an image)... return render, count
def add(self, fn): """Given the filename for a LineGraph file this loads it in and splits it into Glyphs, which it dumps into the db. Does nothing if the file is already loaded. returns the number of glyphs added.""" if fn in self.fnl: return 0 self.fnl.append(fn) # Load the LineGraph from the given filename, and get the homography... f = open(fn, 'r') data = ply2.read(f) f.close() lg = LineGraph() lg.from_dict(data) lg.segment() hg = data['element']['homography']['v'].reshape((3, 3)) texture = os.path.normpath( os.path.join(os.path.dirname(fn), data['meta']['image'])) # Create a line bias object to be used in the next step... bias = gen_bias(lg, hg) # First pass - create each glyph object... glyphs = [] for s in xrange(lg.segments): g = Glyph(lg, s, hg, bias=bias) glyphs.append(g if '_' not in map(lambda t: t[0], g.lg.get_tags()) else None) # Second pass - fill in the connectivity information supported by adjacency... link_glyphs = [] for seg, glyph in enumerate(glyphs): if glyph == None: continue if glyph.key == None: continue # Brute force search to find a left partner... if glyph.left == None: best = None for g in glyphs: # Check it satisfies the conditions... if g == None: continue if g.key == None: continue if id(g) == id(glyph): continue if g.offset_y != glyph.offset_y: continue if (g.right_x - g.offset_x) > (glyph.right_x - glyph.offset_x): continue # Check its better than the current best... if best == None or (best.right_x - best.offset_x) < ( g.right_x - g.offset_x): best = g if best != None: glyph.left = (best, []) # Brute force search to find a right partner... if glyph.right == None: best = None for g in glyphs: # Check it satisfies the conditions... if g == None: continue if g.key == None: continue if id(g) == id(glyph): continue if g.offset_y != glyph.offset_y: continue if (g.left_x - g.offset_x) < (glyph.left_x - glyph.offset_x): continue # Check its better than the current best... if best == None or (best.left_x - best.offset_x) > ( g.left_x - g.offset_x): best = g if best != None: glyph.right = (best, []) # Now we have the best matches find common glyphs to link them, and record them... for other, out in [ g for g in [glyph.left, glyph.right] if g != None ]: shared_seg = set([a[1] for a in glyph.adjacent]) & set( [a[1] for a in other.adjacent]) for seg in shared_seg: g = glyphs[seg] if g == None: continue # We have a linking glyph - extract the information... glyph_vert = [a[0] for a in glyph.adjacent if a[1] == seg] other_vert = [a[0] for a in other.adjacent if a[1] == seg] link_glyph = [ a[0] for a in g.adjacent if a[1] == glyph.seg ] link_other = [ a[0] for a in g.adjacent if a[1] == other.seg ] # Check if we have a multi-link scenario - if so choose links... if len(glyph_vert) > 1 or len(other_vert) > 1: gv_y = map(lambda v: glyph.lg.get_vertex(v)[1], glyph_vert) ov_y = map(lambda v: other.lg.get_vertex(v)[1], other_vert) if (max(gv_y) - min(ov_y)) > (max(ov_y) - min(gv_y)): glyph_vert = glyph_vert[numpy.argmax(gv_y)] other_vert = other_vert[numpy.argmin(ov_y)] else: glyph_vert = glyph_vert[numpy.argmin(gv_y)] other_vert = other_vert[numpy.argmax(ov_y)] lg_y = map(lambda v: g.lg.get_vertex(v)[1], link_glyph) lo_y = map(lambda v: g.lg.get_vertex(v)[1], link_other) if (max(lg_y) - min(lo_y)) > (max(lo_y) - min(lg_y)): link_glyph = link_glyph[numpy.argmax(lg_y)] link_other = link_other[numpy.argmin(lo_y)] else: link_glyph = link_glyph[numpy.argmin(lg_y)] link_other = link_other[numpy.argmax(lo_y)] else: # Simple scenario... glyph_vert = glyph_vert[0] other_vert = other_vert[0] link_glyph = link_glyph[0] link_other = link_other[0] # Recreate the link as a simple path - its the only way to be safe!.. try: g = g.clone() nlg = LineGraph() link_glyph, link_other = nlg.from_path( g.lg, link_glyph, link_other) g.lg = nlg link_glyphs.append(g) except: continue # Line is broken in the centre - don't use it. # Add the tuple to the storage... out.append( (g, glyph_vert, other_vert, link_glyph, link_other)) # Third pass - enforce consistancy... for glyph in glyphs: if glyph == None: continue if glyph.left != None: if glyph.left[0].right == None or id( glyph.left[0].right[0]) != id(glyph): # Inconsistancy - break it... glyph.left[0].right = None glyph.left = None if glyph.right != None: if glyph.right[0].left == None or id( glyph.right[0].left[0]) != id(glyph): # Inconsistancy - break it... glyph.right[0].left = None glyph.right = None # Forth pass - add filename tags and add to db (Count and return the glyph count)... count = 0 for glyph in (glyphs + link_glyphs): if glyph == None: continue glyph.lg.add_tag(0, 0.1, 'file:%s' % fn) glyph.lg.add_tag(0, 0.2, 'texture:%s' % texture) glyph.lg.add_tag( 0, 0.3, 'link:left:%s' % ('n' if glyph.left == None else ('g' if len(glyph.left[1]) == 0 else 'l'))) glyph.lg.add_tag( 0, 0.4, 'link:right:%s' % ('n' if glyph.right == None else ('g' if len(glyph.right[1]) == 0 else 'l'))) glyph.lg.add_tag(0, 0.5, 'source:x:%f' % glyph.source[0]) glyph.lg.add_tag(0, 0.6, 'source:y:%f' % glyph.source[1]) if glyph.key != None: count += 1 if glyph.key in self.db: self.db[glyph.key].append(glyph) else: self.db[glyph.key] = [glyph] glyph.code = len(self.by_code) glyph.lg.add_tag(0, 0.5, 'code:%i' % glyph.code) self.by_code.append(glyph) return count
def __init__(self, lg, seg, hg, extra = 0.4, bias = None): """Given a segmented LineGraph and segment number this extracts it, transforms it into the standard coordinate system and stores the homography used to get there. (hg transforms from line space, where there is a line for each y=integer, to the space of the original pixels.) Also records its position on its assigned line and line number so it can be ordered suitably. Does not store connectivity information - that is done later. extra is used for infering the line position, and is extra falloff to have either side of a line voting for it - a smoothing term basically. bias is an optional dictionary indexed by line number that gives a weight to assign to being assigned to that line - used to utilise the fact that data collection asks the writter to use every-other line, which helps avoid misassigned dropped j's for instance.""" if lg==None: return # Extract the line graph... self.lg = LineGraph() self.adjacent = self.lg.from_segment(lg, seg) self.seg = seg # Tranform it to line space... ihg = la.inv(hg) self.lg.transform(ihg, True) # Check if which line its on is tagged - exists as an override for annoying glyphs... line = None for tag in self.lg.get_tags(): if tag[0]=='line': # We have a tag of line - its position specifies the line the glyph is on... point = self.lg.get_point(tag[1], tag[2]) line = int(numpy.floor(point[1])) break # Record which line it is on and its position along the line... # (Works by assuming that the line is the one below the space where most of the mass of the glyph is. Takes it range to be that within the space, so crazy tails are cut off.) min_x, max_x, min_y, max_y = self.lg.get_bounds() self.source = (0.5 * (min_x + max_x), 0.5 * (min_y + max_y)) if line==None: best_mass = 0.0 self.left_x = min_x self.right_x = max_x line = 0 start = int(numpy.trunc(min_y)) for pl in xrange(start, int(numpy.ceil(max_y))): mass = 0.0 low_y = float(pl) - extra high_y = float(pl+1) + extra left_x = None right_x = None for es in self.lg.within(min_x, max_x, low_y, high_y): for ei in xrange(*es.indices(self.lg.edge_count)): edge = self.lg.get_edge(ei) vf = self.lg.get_vertex(edge[0]) vt = self.lg.get_vertex(edge[1]) if vf[1]>low_y and vf[1]<high_y and vt[1]>low_y and vt[1]<high_y: dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass += (vf[5] + vt[5]) * numpy.sqrt(dx*dx + dy*dy) if left_x==None: left_x = min(vf[0], vt[0]) else: left_x = min(vf[0], vt[0], left_x) if right_x==None: right_x = max(vf[0], vt[0]) else: right_x = max(vf[0], vt[0], right_x) mass *= 1.0/(1.0+pl - start) # Bias to choosing higher, for tails. if bias!=None: mass *= bias[pl] if mass>best_mass: best_mass = mass self.left_x = left_x self.right_x = right_x line = pl # Transform it so it is positioned to be sitting on line 1 of y, store the total homography that we have applied... self.offset_x = -min_x self.offset_y = -line hg = numpy.eye(3, dtype=numpy.float32) hg[0,2] = self.offset_x hg[1,2] = self.offset_y self.left_x += self.offset_x self.right_x += self.offset_x self.lg.transform(hg) self.transform = numpy.dot(hg, ihg) # Set as empty its before and after glyphs - None if there is no adjacency, or a tuple if there is: (glyph, list of connecting (link glyph, shared vertex in this, shared vertex in glyph, vertex in link glyph on this side, vertex in link glyph on glyph side), empty if none.)... self.left = None self.right = None # Extract the character this glyph represents... tags = self.lg.get_tags() codes = [t[0] for t in tags if len(filter(lambda c: c!='_', t[0]))==1] self.key = codes[0] if len(codes)!=0 else None self.code = -id(self) # Cache stuff... self.mass = None self.center = None self.feat = None self.v_offset = None
class Glyph: """Represents a glyph, that has been transformed into a suitable coordinate system; includes connectivity information.""" def __init__(self, lg, seg, hg, extra=0.4, bias=None): """Given a segmented LineGraph and segment number this extracts it, transforms it into the standard coordinate system and stores the homography used to get there. (hg transforms from line space, where there is a line for each y=integer, to the space of the original pixels.) Also records its position on its assigned line and line number so it can be ordered suitably. Does not store connectivity information - that is done later. extra is used for infering the line position, and is extra falloff to have either side of a line voting for it - a smoothing term basically. bias is an optional dictionary indexed by line number that gives a weight to assign to being assigned to that line - used to utilise the fact that data collection asks the writter to use every-other line, which helps avoid misassigned dropped j's for instance.""" if lg == None: return # Extract the line graph... self.lg = LineGraph() self.adjacent = self.lg.from_segment(lg, seg) self.seg = seg # Tranform it to line space... ihg = la.inv(hg) self.lg.transform(ihg, True) # Check if which line its on is tagged - exists as an override for annoying glyphs... line = None for tag in self.lg.get_tags(): if tag[0] == 'line': # We have a tag of line - its position specifies the line the glyph is on... point = self.lg.get_point(tag[1], tag[2]) line = int(numpy.floor(point[1])) break # Record which line it is on and its position along the line... # (Works by assuming that the line is the one below the space where most of the mass of the glyph is. Takes it range to be that within the space, so crazy tails are cut off.) min_x, max_x, min_y, max_y = self.lg.get_bounds() self.source = (0.5 * (min_x + max_x), 0.5 * (min_y + max_y)) if line == None: best_mass = 0.0 self.left_x = min_x self.right_x = max_x line = 0 start = int(numpy.trunc(min_y)) for pl in xrange(start, int(numpy.ceil(max_y))): mass = 0.0 low_y = float(pl) - extra high_y = float(pl + 1) + extra left_x = None right_x = None for es in self.lg.within(min_x, max_x, low_y, high_y): for ei in xrange(*es.indices(self.lg.edge_count)): edge = self.lg.get_edge(ei) vf = self.lg.get_vertex(edge[0]) vt = self.lg.get_vertex(edge[1]) if vf[1] > low_y and vf[1] < high_y and vt[ 1] > low_y and vt[1] < high_y: dx = vt[0] - vf[0] dy = vt[1] - vf[1] mass += (vf[5] + vt[5]) * numpy.sqrt(dx * dx + dy * dy) if left_x == None: left_x = min(vf[0], vt[0]) else: left_x = min(vf[0], vt[0], left_x) if right_x == None: right_x = max(vf[0], vt[0]) else: right_x = max(vf[0], vt[0], right_x) mass *= 1.0 / (1.0 + pl - start ) # Bias to choosing higher, for tails. if bias != None: mass *= bias[pl] if mass > best_mass: best_mass = mass self.left_x = left_x self.right_x = right_x line = pl # Transform it so it is positioned to be sitting on line 1 of y, store the total homography that we have applied... self.offset_x = -min_x self.offset_y = -line hg = numpy.eye(3, dtype=numpy.float32) hg[0, 2] = self.offset_x hg[1, 2] = self.offset_y self.left_x += self.offset_x self.right_x += self.offset_x self.lg.transform(hg) self.transform = numpy.dot(hg, ihg) # Set as empty its before and after glyphs - None if there is no adjacency, or a tuple if there is: (glyph, list of connecting (link glyph, shared vertex in this, shared vertex in glyph, vertex in link glyph on this side, vertex in link glyph on glyph side), empty if none.)... self.left = None self.right = None # Extract the character this glyph represents... tags = self.lg.get_tags() codes = [ t[0] for t in tags if len(filter(lambda c: c != '_', t[0])) == 1 ] self.key = codes[0] if len(codes) != 0 else None self.code = -id(self) # Cache stuff... self.mass = None self.center = None self.feat = None self.v_offset = None def clone(self): """Returns a clone of this Glyph.""" ret = Glyph(None, None, None) ret.lg = self.lg ret.adjacent = self.adjacent ret.seg = self.seg ret.source = self.source ret.left_x = self.left_x ret.right_x = self.right_x ret.offset_x = self.offset_x ret.offset_y = self.offset_y ret.transform = self.transform ret.left = self.left ret.right = self.right ret.key = self.key ret.code = self.code ret.mass = None if self.mass == None else self.mass.copy() ret.center = None if self.center == None else self.center.copy() ret.feat = None if self.feat == None else map(lambda a: a.copy(), self.feat) ret.v_offset = self.v_offset return ret def get_linegraph(self): return self.lg def orig_left_x(self): return self.left_x - self.offset_x def orig_right_x(self): return self.right_x - self.offset_x def get_mass(self): """Returns a vector of [average density, average radius] - used for matching adjacent glyphs.""" if self.mass == None: self.mass = numpy.zeros(2, dtype=numpy.float32) weight = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) weight += 1.0 self.mass += (numpy.array([info[6], info[5]]) - self.mass) / weight return self.mass def get_center(self): """Returns the 'center' of the glyph - its density weighted in an attempt to make it robust to crazy tails.""" if self.center == None: self.center = numpy.zeros(2, dtype=numpy.float32) weight = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[ 6] # Radius squared * density - proportional to quantity of ink, assuming (correctly as rest of system currently works) even sampling. if w > 1e-6: weight += w mult = w / weight self.center[0] += (info[0] - self.center[0]) * mult self.center[1] += (info[1] - self.center[1]) * mult return self.center def get_voffset(self): """Calculates and returns the vertical offset to apply to the glyph that corrects for any systematic bias in its flow calculation.""" if self.v_offset == None: self.v_offset = 0.0 weight = 0.0 truth = self.get_center()[1] # Calculate the estimated offsets from the left side and update the estimate, correctly factoring in the variance of the offset... if self.left != None: diff, sd = costs.glyph_pair_offset(self.left[0], self, 0.2, True) estimate = self.left[0].get_center()[1] + diff offset = truth - estimate est_weight = 1.0 / (sd**2.0) weight += est_weight self.v_offset += (offset - self.v_offset) * est_weight / weight # Again from the right side... if self.right != None: diff, sd = costs.glyph_pair_offset(self, self.right[0], 0.2, True) estimate = self.right[0].get_center()[1] - diff offset = truth - estimate est_weight = 1.0 / (sd**2.0) weight += est_weight self.v_offset += (offset - self.v_offset) * est_weight / weight return self.v_offset def most_left(self): """Returns the coordinate of the furthest left vertex in the glyph.""" info = self.lg.get_vertex(0) best_x = info[0] best_y = info[1] for i in xrange(1, self.lg.vertex_count): info = self.lg.get_vertex(0) if info[0] < best_x: best_x = info[0] best_y = info[1] return (best_x, best_y) def most_right(self): """Returns the coordinate of the furthest right vertex in the glyph.""" info = self.lg.get_vertex(0) best_x = info[0] best_y = info[1] for i in xrange(1, self.lg.vertex_count): info = self.lg.get_vertex(0) if info[0] > best_x: best_x = info[0] best_y = info[1] return (best_x, best_y) def get_feat(self): """Calculates and returns a feature for the glyph, or, more accuratly two features, representing (left, right), so some tricks can be done to make their use side dependent (For learning a function for matching to adjacent glyphs.).""" if self.feat == None: # First build a culumative distribution over the x axis range of the glyph... min_x, max_x, min_y, max_y = self.lg.get_bounds() culm = numpy.ones(32, dtype=numpy.float32) culm *= 1e-2 min_x -= 1e-3 max_x += 1e-3 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[6] t = (info[0] - min_x) / (max_x - min_x) t *= (culm.shape[0] - 1) low = int(t) high = low + 1 t -= low culm[low] += (1.0 - t) * w culm[high] += t * w culm /= culm.sum() culm = numpy.cumsum(culm) # Now extract all the per sample features... feat_param = { 'dir_travel': 0.1, 'travel_max': 1.0, 'travel_bins': 6, 'travel_ratio': 0.8, 'pos_bins': 3, 'pos_ratio': 0.9, 'radius_bins': 1, 'density_bins': 3 } fv = self.lg.features(**feat_param) # Combine them into the two halves, by weighting by the culumative; include density and radius as well... left = numpy.zeros(fv.shape[1] + 2, dtype=numpy.float32) right = numpy.zeros(fv.shape[1] + 2, dtype=numpy.float32) left_total = 0.0 right_total = 0.0 for i in xrange(self.lg.vertex_count): info = self.lg.get_vertex(i) w = info[5] * info[5] * info[6] t = (info[0] - min_x) / (max_x - min_x) t *= (culm.shape[0] - 1) low = int(t) high = low + 1 t -= low right_w = (1.0 - t) * culm[low] + t * culm[high] left_w = 1.0 - right_w left[0] += left_w * info[5] right[0] += right_w * info[5] left[1] += left_w * info[6] right[1] += right_w * info[6] left[2:] += w * left_w * fv[i, :] right[2:] += w * right_w * fv[i, :] left_total += left_w right_total += right_w left[:2] /= left_total right[:2] /= right_total left[2:] /= max(left[2:].sum(), 1e-6) right[2:] /= max(right[2:].sum(), 1e-6) self.feat = (left, right) return self.feat def __str__(self): l = self.left[0].key if self.left != None else 'None' r = self.right[0].key if self.right != None else 'None' return 'Glyph %i: key = %s (%s|%s)' % (self.code, self.key, l, r)
# Load and process each in turn, to create a big database of feature/class... train = [] for fn_num, fn in enumerate(lg_fn): print 'Processing %s: (%i of %i)' % (fn, fn_num+1, len(lg_fn)) # Load line graph... print ' Loading...' f = open(fn, 'r') data = ply2.read(f) f.close() lg = LineGraph() lg.from_dict(data) lg.segment() # Extract features... print ' Extracting features...' fv = lg.features(**feat_param) # Extract labels, to match the features... segs = lg.get_segs() def seg_to_label(seg): tags = lg.get_tags(seg) if len(tags)==0: return 0 # Its a ligament for tag in tags:
print 'Found %i line graphs' % len(lg_fn) # Load and process each in turn, to create a big database of feature/class... train = [] for fn_num, fn in enumerate(lg_fn): print 'Processing %s: (%i of %i)' % (fn, fn_num + 1, len(lg_fn)) # Load line graph... print ' Loading...' f = open(fn, 'r') data = ply2.read(f) f.close() lg = LineGraph() lg.from_dict(data) lg.segment() # Extract features... print ' Extracting features...' fv = lg.features(**feat_param) # Extract labels, to match the features... segs = lg.get_segs() def seg_to_label(seg): tags = lg.get_tags(seg) if len(tags) == 0: return 0 # Its a ligament for tag in tags:
def stitch_connect(glyph_layout, soft = True, half = False, pair_base = 0): """Converts a glyph layout to a linegraph layout. This stitches together the glyphs when it has sufficient information to do so.""" ret = [] # First copy over the actual glyphs... for pair in glyph_layout: if pair!=None: hg, glyph = pair ret.append((hg, glyph.lg)) # Now loop through and identify all pairs that can be stitched together, and stitch them... pair_code = 0 for i in xrange(len(glyph_layout)-1): # Can't stitch spaces... if glyph_layout[i]!=None and glyph_layout[i+1]!=None: l_hg, l_glyph = glyph_layout[i] r_hg, r_glyph = glyph_layout[i+1] matches = costs.match_links(l_glyph, r_glyph) # Iterate and do each pairing in turn... for ml, mr in matches: # Calculate the homographies to put the two line graphs into position... lc_hg = numpy.dot(l_hg, numpy.dot(l_glyph.transform, la.inv(ml[0].transform))) rc_hg = numpy.dot(r_hg, numpy.dot(r_glyph.transform, la.inv(mr[0].transform))) # Copy the links, applying the homographies... lc = LineGraph() lc.from_many(lc_hg, ml[0].lg) rc = LineGraph() rc.from_many(rc_hg, mr[0].lg) # Extract the merge points... blend = [(ml[3], 0.0, mr[4]), (ml[4], 1.0, mr[3])] # Do the blending... lc.blend(rc, blend, soft) # Record via tagging that the two parts are the same entity... pair = 'duplicate:%i,%i' % (pair_base, pair_code) lc.add_tag(0, 0.5, pair) rc.add_tag(0, 0.5, pair) pair_code += 1 # Store the pair of line graphs in the return, with identity homographies... ret.append((numpy.eye(3), lc)) if not half: ret.append((numpy.eye(3), rc)) return ret