def parse_line(self, line): # Ignore empty lines if len(line) == 0: return k = line[0] if k == '#': # Ignore comments and option lines for now # They have position dependencies and usually can be ignored anyway for my purposes # They are mostly used by Hugin #print 'WARNING: ignoring comment line: %s' % line self.comment_lines.append(line) # EOF marker, used for PToptimizer to indicate end of original project elif k == '*': pass # Panorama line elif k == "p": self.panorama_line = PanoramaLine(line, self) # additional options elif k == "m": self.mode_line = ModeLine(line, self) # Image line elif k == "i": self.image_lines.append(ImageLine(line, self)) # Optimization (variable) line elif k == "v": self.variable_lines.append(VariableLine(line, self)) # Control point line elif k == "c": self.control_point_lines.append(ControlPointLine(line, self)) elif k == 'C': self.absolute_control_point_lines.append( AbsoluteControlPointLine(line, self)) # Generated by PToptimizer elif k == "o": self.optimizer_lines.append(OptimizerLine(line, self)) else: print 'WARNING: unknown line type: %s' % line self.misc_lines.append(line)
def parse_line(self, line): # Ignore empty lines if len(line) == 0: return k = line[0] if k == '#': # Ignore comments and option lines for now # They have position dependencies and usually can be ignored anyway for my purposes # They are mostly used by Hugin #print 'WARNING: ignoring comment line: %s' % line self.comment_lines.append(line) # EOF marker, used for PToptimizer to indicate end of original project elif k == '*': pass # Panorama line elif k == "p": self.panorama_line = PanoramaLine(line, self) # additional options elif k == "m": self.mode_line = ModeLine(line, self) # Image line elif k == "i": self.image_lines.append(ImageLine(line, self)) # Optimization (variable) line elif k == "v": self.variable_lines.append(VariableLine(line, self)) # Control point line elif k == "c": self.control_point_lines.append(ControlPointLine(line, self)) elif k == 'C': self.absolute_control_point_lines.append(AbsoluteControlPointLine(line, self)) # Generated by PToptimizer elif k == "o": self.optimizer_lines.append(OptimizerLine(line, self)) else: print 'WARNING: unknown line type: %s' % line self.misc_lines.append(line)
def set_pano_line_by_text(self, line): self.parse() self.panorama_line = PanoramaLine(line, self)
class PTOProject: def __init__(self): # File name, if one exists self.file_name = None # If this is a temporary project, have it delete upon destruction self.temp_file = None # Raw project text, None is not loaded self.text = None self.unparse() def unparse(self): # 'p' line self.panorama_line = None # 'm' line self.mode_line = None # Those started with #hugin_ # option_lines = None # Raw strings self.comment_lines = None # c N1 X1225.863118 Y967.737131 n0 t0 x1444.778035 y233.74261 self.control_point_lines = None self.absolute_control_point_lines = None self.image_lines = None self.img_fn2il = None self.il2i = None self.optimizer_lines = None ''' I bet lone v lines can be omitted # Variable lines v v d1 e1 p1 r1 y1 v d2 e2 p2 r2 y2 v d3 e3 p3 r3 y3 v ''' self.variable_lines = None # Raw strings, we don't know what these are self.misc_lines = list() # Has this been loaded from the file? self.parsed = False def remove_file_name(self): '''Unbound this from the filesystem''' self.ensure_text_loaded() self.file_name = None def copy(self, control_points=True): '''Return an unsaved but identical project''' # slow #return PTOProject.from_text(self.get_text()) # maybe...not sure about this cp_tmp = self.control_point_lines cp_tmp_abs = self.absolute_control_point_lines if not control_points: self.control_point_lines = [] self.absolute_control_point_lines = [] ret = copy.deepcopy(self) ret.file_name = None ret.temp_file = None #return self.to_str_core(False) if not control_points: self.control_point_lines = cp_tmp self.absolute_control_point_lines = cp_tmp_abs return ret def i2img(self, index): '''Given index return image object''' lines = self.get_image_lines() if index >= len(lines): raise IndexError('index: %d, items: %d' % (index, len(lines))) return lines[index] def i2i(self, project, i): '''Return index in this project given i in other project''' return self.img_fn2i(project.i2img(i).get_name()) def img_fn2i(self, fn): '''Given image file name return image index''' for i, il in enumerate(self.get_image_lines()): if fn == il.get_name(): return i return None def img_fn2l(self, fn): '''Given image file name return image line''' for il in self.get_image_lines(): if fn == il.get_name(): return il return None def assert_uniform_images(self): '''All images have same width and height''' w = None h = None for i in self.get_image_lines(): if w is None: w = i.width() if h is None: h = i.height() if w != i.width(): raise Exception('Mismatched width') if h != i.height(): raise Exception('Mismatched height') def get_comment_lines(self): self.parse() return self.comment_lines def get_image(self, n): '''Return image object n''' return self.get_image_lines()[n] def build_image_fn_map(self): self.img_fn2il = {} for il in self.get_image_lines(): self.img_fn2il[il.get_name()] = il def build_il2i(self): self.il2i = {} for i, il in enumerate(self.image_lines): self.il2i[il] = i def get_image_by_fn(self, fn): if self.img_fn2il: return self.img_fn2il.get(fn, None) for i in self.get_image_lines(): if fn == i.get_name(): return i return None def add_image(self, image_fn, calc_dim=True, def_opt=False): self.parse() il = ImageLine('i n"%s"' % image_fn, self) # set w/h if calc_dim: calc_il_dim(il) if def_opt: il.set_variable('f', 0) # default FOV il.set_variable('v', 51) # x/y position not yet calculated il.set_variable('d', 0) il.set_variable('e', 0) self.image_lines.append(il) def del_images(self, ils): '''Delete image as well as coresponding control point lines''' # added to support image sub-projects for fast preview ils_i = [il.get_index() for il in ils] new_image_lines = [] for i, il in enumerate(self.image_lines): if not i in ils_i: new_image_lines.append(il) # Build a map of old image index to new img_fn2ii_old = {} for i, il in enumerate(self.image_lines): img_fn2ii_old[il.get_name()] = i img_fn2ii_new = {} for i, il in enumerate(new_image_lines): img_fn2ii_new[il.get_name()] = i ii_old2new = {} for fn, ii_new in img_fn2ii_new.iteritems(): try: ii_old2new[img_fn2ii_old[fn]] = ii_new except: print 'ERROR' print fn print img_fn2ii_old print img_fn2ii_new print ii_old2new raise # remove unneeded control points # and replace image indices new_control_point_lines = [] for cpl in self.control_point_lines: n = cpl.getv('n') N = cpl.getv('N') if n not in ils_i and N not in ils_i: # Translate indices cpl.setv('n', ii_old2new[cpl.getv('n')]) cpl.setv('N', ii_old2new[cpl.getv('N')]) # and add to the keep set new_control_point_lines.append(cpl) # shift in new control point set self.control_point_lines = new_control_point_lines # FIXME: hack since I don't need variables for intended purpose of this function # variable lines are messy since technically you might have to split it self.variable_lines = [] # shift in new image line set self.image_lines = new_image_lines # Invalidate the index cache, if any self.img_fn2il = None def get_image_lines(self): self.parse() return self.image_lines def nimages(self): return len(self.get_image_lines()) def get_control_point_lines(self): self.parse() return self.control_point_lines def add_control_point_line(self, cl): self.parse() if self.control_point_lines is None: self.control_point_lines = [] self.control_point_lines.append(cl) def remove_control_point_line(self, cl): self.parse() assert self.control_point_lines is not None self.control_point_lines.remove(cl) def add_control_point_line_by_text(self, cl): self.add_control_point_line(ControlPointLine(cl, self)) def set_pano_line_by_text(self, line): self.parse() self.panorama_line = PanoramaLine(line, self) def set_mode_line_by_text(self, line): self.parse() self.mode_line = ModeLine(line, self) def add_image_line(self, il): self.parse() if self.image_lines is None: self.image_lines = [] self.image_lines.append(il) def add_image_line_by_text(self, il_text): il = ImageLine(il_text, self) self.add_image_line(il) def get_optimizer_lines(self): self.parse() return self.optimizer_lines def get_panorama_line(self): #print 'getting p line, parsed: %d' % self.parsed self.parse() return self.panorama_line def get_variable_lines(self): self.parse() return self.variable_lines @staticmethod def from_file_name(file_name, is_temporary=False): ret = PTOProject() ret.file_name = file_name if is_temporary: ret.temp_file = ManagedTempFile.from_existing(file_name) ret.parse() return ret @staticmethod def from_temp_file(temp_file): ret = PTOProject() ret.file_name = temp_file.file_name ret.temp_file = temp_file ret.parse() return ret @staticmethod def from_text(text): if text is None: raise Exception('Require text') ret = PTOProject() ret.text = text ret.reparse() return ret @staticmethod def from_blank(): return PTOProject.from_text('') @staticmethod def from_simple(): return PTOProject.from_text(''' p m ''') @staticmethod def from_default(): return PTOProject.from_text(''' p f0 v179 n"PSD_mask" E0.0 R0 m g1.0 i0 f0 m2 ''') @staticmethod def from_default2(): return PTOProject.from_text(''' p f0 v179 n"TIFF_m c:LZW" E0.0 R0 m g1.0 i0 f0 m2 ''') def parse(self): '''Parse if not already parsed''' if not self.parsed: self.reparse() # We should now be using the intermediate form # Force a regen if someone wants text self.text = None def reparse(self): '''Force a parse''' if False: print 'WARNING: pto parsing disabled' return self.unparse() self.panorama_line = None self.mode_line = None self.comment_lines = list() self.variable_lines = list() self.control_point_lines = list() self.absolute_control_point_lines = list() self.image_lines = list() self.misc_lines = list() self.optimizer_lines = list() if self.text is None: self.text = open(self.file_name).read() #print self.text dbg('Beginning split on text of len %d' % (len(self.text))) for line in self.text.split('\n'): dbg('Processing line: %s' % line) # Single * is end of file # Any comments / garbage is allowed to follow #if line.strip() == '*': # break # In practice this is PTOptimizer output I want # Add an option later if needed to override self.parse_line(line) dbg() #print 'Finished reparse' self.parsed = True def parse_line(self, line): # Ignore empty lines if len(line) == 0: return k = line[0] if k == '#': # Ignore comments and option lines for now # They have position dependencies and usually can be ignored anyway for my purposes # They are mostly used by Hugin #print 'WARNING: ignoring comment line: %s' % line self.comment_lines.append(line) # EOF marker, used for PToptimizer to indicate end of original project elif k == '*': pass # Panorama line elif k == "p": self.panorama_line = PanoramaLine(line, self) # additional options elif k == "m": self.mode_line = ModeLine(line, self) # Image line elif k == "i": self.image_lines.append(ImageLine(line, self)) # Optimization (variable) line elif k == "v": self.variable_lines.append(VariableLine(line, self)) # Control point line elif k == "c": self.control_point_lines.append(ControlPointLine(line, self)) elif k == 'C': self.absolute_control_point_lines.append( AbsoluteControlPointLine(line, self)) # Generated by PToptimizer elif k == "o": self.optimizer_lines.append(OptimizerLine(line, self)) else: print 'WARNING: unknown line type: %s' % line self.misc_lines.append(line) # These functions are fragile....should probably just stick to the get string versions def regen_hugin(self): self.set_text(self.to_str_core(False)) def regen_pto(self): self.set_text(self.to_str_core(True)) def regen(self): self.regen_pto() def to_str_core(self, ptoptimizer_form): self.build_il2i() text = '' text += '# Generated by xystitch\n' #print 'Pano line: %s' % self.panorama_line if ptoptimizer_form: print 'generating ptopt form' key_blacklist = None if ptoptimizer_form: key_blacklist = 'E R S'.split() if self.panorama_line: text += self.panorama_line.regen(key_blacklist) if self.mode_line: text += self.mode_line.regen() for line in self.image_lines: key_blacklist = None if ptoptimizer_form: key_blacklist = 'Eb Eev Er Ra Rb Rc Rd Re Va Vb Vc Vd Vx Vy'.split( ) text += line.regen(key_blacklist) for line in self.variable_lines: text += line.regen() for line in self.control_point_lines: text += line.regen() for line in self.absolute_control_point_lines: text += line.regen() for line in self.comment_lines: #text += line.regen() text += line + '\n' self.il2i = None return text def __str__(self): # Might make this diff from get_text to show parser info at some point return self.get_text() def get_text(self): # If parsed then convert the intermediate repr since we may have modified from the saved value if self.parsed: #print 'get_text: constructed version' return self.to_str_core(False) else: #print 'get_text: file/text version' # Not parsed? Then just directly load from the file self.ensure_text_loaded() return self.text def ensure_text_loaded(self): if self.text is None: self.load_text() def load_text(self): self.text = open(self.file_name).read() self.parsed = False def make_absolute(self, to=None): '''Make all image paths absolute''' #print 'Making %d images absolute' % len(self.get_image_lines()) for i in self.get_image_lines(): i.make_absolute(to) def make_relative(self, to=None): '''Make all image paths relative''' for i in self.get_image_lines(): i.make_relative(to) def get_a_file_name(self, prefix=None, postfix=None): '''Return a file name that has current .pto contents''' '''If doesn't have a real file, create a temp file''' if self.file_name: return self.file_name if postfix is None: postfix = ".pto" self.temp_file = ManagedTempFile.get(prefix, postfix) self.file_name = self.temp_file.file_name # hmmm... self.save() return self.file_name def set_file_name(self, file_name): self.file_name = file_name def set_text(self, text): self.text = text if self.file_name: self.save() self.parsed = False def merge_into(self, ptos): '''Merge project into this one. Output file is updated''' print 'merge_into: others: %d' % len(ptos) this = [] if self.file_name and os.path.exists(self.file_name): this = [self] # Merge is based on filename # Must be synced to disk self.save() # Merging modifies the project structure so make sure that a dummy merge occurs if nothing else m = Merger(this + ptos) temp = m.run() shutil.move(temp.file_name, self.file_name) self.reopen() def merge(self, ptos): '''Return a project containing both control points''' ''' Does not allow in place replace, we have to move things around [mcmaster@gespenst bin]$ pto_merge --help pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f Usage: pto_merge [options] input.pto input2.pto ... Options: -o, --output=file.pto Output Hugin PTO file. Default: <filename>_merge.pto -h, --help Shows this help ''' if len(ptos) == 0: print 'WARNING: skipping merge due to no other files' raise Exception('Nothing to merge') return None this = [] if self.file_name and os.path.exists(self.file_name): this = [self] # Merge is based on filename # Must be synced to disk self.save() m = Merger(this + ptos) return m.run() def save(self): ''' I considered invalidating text here but for most operations I'm doing (PToptimizer excluded) only I modify the project import traceback import sys sys.stdout.flush() sys.stderr.flush() traceback.print_stack() sys.stdout.flush() sys.stderr.flush() ''' if self.file_name is None: raise Exception( 'Cannot save a project that was never assigned a filename') self.save_as(self.file_name) def save_as(self, file_name, is_new_filename=False): with open(file_name + '.tmp', 'w') as f: f.write(self.get_text()) shutil.move(file_name + '.tmp', file_name) if is_new_filename: self.file_name = file_name # reload is a builtin...not sure if it would conflict def reopen(self): f = open(self.file_name, 'r') self.text = f.read() self.reparse() def get_file_names(self): '''Get image file names''' return [i.get_name() for i in self.get_image_lines()] def hugin_form(self): ''' This is used when merging through fortify stitch Something is causing pto_merge to hang, but NOT ptomerge Only occurs if I wrap my commands in a script... The script doesn't do any fancy I/O redirection clear rm -rf /tmp/xystitch_* pr0nstitch *.jpg out.pto pto_merge produces nicer output than ptomerge While ptomerge produces the fields I need, it leaves some other junk I think pto_merge also calculates width/heigh attributes part of Hugin [mcmaster@gespenst first]$ pto_merge Warning: pto_merge requires at least 2 project files pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f part of perl-Panotools-Script [mcmaster@gespenst first]$ ptomerge --help cannot read-open --help at /usr/share/perl5/Panotools/Script.pm line 91. man ptomerge ... ptomerge infile1.pto infile2.pto infile3.pto [...] outfile.pto ... ''' # However, this tool also generates an archaic .pto format that pto can parse, but I don't want to # pretend to merge into an empty project to force Hugin to clean it up # pto_merge --output=temp.pto /dev/null temp.pto if False: args = list() args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) (rc, output) = Execute.with_output("ptomerge", args) else: args = list() args.append("--output=%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) if False: args.append("/dev/null") else: empty_file = ManagedTempFile.get(None, ".pto") open(empty_file.file_name, 'w').write('') args.append(empty_file.file_name) (rc, output) = Execute.with_output("pto_merge", args) if not rc == 0: print print print if rc == 35072: # ex: empty projects seem to cause this print 'Out of memory, expect malformed project file' print 'output:%s' % output raise Exception('Bad rc: %d' % rc) self.reopen()
class PTOProject: def __init__(self): # File name, if one exists self.file_name = None # Raw project text, None is not loaded self.text = None # If this is a temporary project, have it delete upon destruction self.temp_file = None # Could be a mix of temp and non-temp, so don't make any ordering assumptions self.temp_image_files = set() # 'p' line self.panorama_line = None # 'm' line self.mode_line = None # Those started with #hugin_ # option_lines = None # Raw strings self.comment_lines = None # c N1 X1225.863118 Y967.737131 n0 t0 x1444.778035 y233.74261 self.control_point_lines = None self.absolute_control_point_lines = None self.image_lines = None self.optimizer_lines = None ''' I bet lone v lines can be omitted # Variable lines v v d1 e1 p1 r1 y1 v d2 e2 p2 r2 y2 v d3 e3 p3 r3 y3 v ''' self.variable_lines = None # Raw strings, we don't know what these are self.misc_lines = list() # Has this been loaded from the file? self.parsed = False def remove_file_name(self): '''Unbound this from the filesystem''' self.ensure_text_loaded() self.file_name = None def copy(self): '''Return an unsaved but identical project''' return PTOProject.from_text(self.get_text()) def index_to_image(self, index): lines = self.get_image_lines() if index >= len(lines): raise IndexError('index: %d, items: %d' % (index, len(lines))) return lines[index] def assert_uniform_images(self): '''All images have same width and height''' w = None h = None for i in self.get_image_lines(): if w is None: w = i.width() if h is None: h = i.height() if w != i.width(): raise Exception('Mismatched width') if h != i.height(): raise Exception('Mismatched height') def get_comment_lines(self): self.parse() return self.comment_lines def get_image(self, n): '''Return image object n''' return self.get_image_lines()[n] def get_image_by_fn(self, fn): for i in self.get_image_lines(): if fn == i.get_name(): return i return None def add_image(self, image_fn, calc_dim = True): self.parse() il = ImageLine('i n"%s"' % image_fn, self) if calc_dim: calc_il_dim(il) self.image_lines.append(il) def get_image_lines(self): self.parse() return self.image_lines def nimages(self): return len(self.get_image_lines()) def get_control_point_lines(self): self.parse() return self.control_point_lines def add_control_point_line(self, cl): self.parse() if self.control_point_lines is None: self.control_point_lines = [] self.control_point_lines.append(cl) def add_control_point_line_by_text(self, cl): self.add_control_point_line(ControlPointLine(cl, self)) def add_image_line(self, il): self.parse() if self.image_lines is None: self.image_lines = [] self.image_lines.append(il) def add_image_line_by_text(self, il_text): il = ImageLine(il_text, self) self.add_image_line(il) def get_optimizer_lines(self): self.parse() return self.optimizer_lines def get_panorama_line(self): #print 'getting p line, parsed: %d' % self.parsed self.parse() return self.panorama_line def get_variable_lines(self): self.parse() return self.variable_lines @staticmethod def from_file_name(file_name, is_temporary = False): ret = PTOProject() ret.file_name = file_name if is_temporary: ret.temp_file = ManagedTempFile.from_existing(file_name) return ret @staticmethod def parse_from_file_name(file_name, is_temporary = False): ret = PTOProject() ret.file_name = file_name if is_temporary: ret.temp_file = ManagedTempFile.from_existing(file_name) ret.parse() return ret @staticmethod def from_temp_file(temp_file): ret = PTOProject() ret.file_name = temp_file.file_name ret.temp_file = temp_file return ret @staticmethod def from_text(text): ret = PTOProject() if text is None: raise Excetpion('No text is invalid') ret.text = text #ret.reparse() return ret @staticmethod def from_blank(): return PTOProject.from_text('') @staticmethod def from_simple(): return PTOProject.from_text(''' p m ''') @staticmethod def from_default(): return PTOProject.from_text(''' p f0 v179 n"PSD_mask" E0.0 R0 m g1.0 i0 f0 m2 ''') def parse(self): '''Parse if not already parsed''' if not self.parsed: self.reparse() # We should now be using the intermediate form # Force a regen if someone wants text self.text = None def reparse(self): '''Force a parse''' if False: print 'WARNING: pto parsing disabled' return self.panorama_line = None self.mode_line = None self.comment_lines = list() self.variable_lines = list() self.control_point_lines = list() self.absolute_control_point_lines = list() self.image_lines = list() self.misc_lines = list() self.optimizer_lines = list() #print self.text print_debug('Beginning split on text of len %d' % (len(self.get_text()))) for line in self.get_text().split('\n'): print_debug('Processing line: %s' % line) # Single * is end of file # Any comments / garbage is allowed to follow #if line.strip() == '*': # break # In practice this is PTOptimizer output I want # Add an option later if needed to override self.parse_line(line) print_debug() print 'Finished reparse' self.parsed = True def parse_line(self, line): # Ignore empty lines if len(line) == 0: return k = line[0] if k == '#': # Ignore comments and option lines for now # They have position dependencies and usually can be ignored anyway for my purposes # They are mostly used by Hugin #print 'WARNING: ignoring comment line: %s' % line self.comment_lines.append(line) # EOF marker, used for PToptimizer to indicate end of original project elif k == '*': pass # Panorama line elif k == "p": self.panorama_line = PanoramaLine(line, self) # additional options elif k == "m": self.mode_line = ModeLine(line, self) # Image line elif k == "i": self.image_lines.append(ImageLine(line, self)) # Optimization (variable) line elif k == "v": self.variable_lines.append(VariableLine(line, self)) # Control point line elif k == "c": self.control_point_lines.append(ControlPointLine(line, self)) elif k == 'C': self.absolute_control_point_lines.append(AbsoluteControlPointLine(line, self)) # Generated by PToptimizer elif k == "o": self.optimizer_lines.append(OptimizerLine(line, self)) else: print 'WARNING: unknown line type: %s' % line self.misc_lines.append(line) # These functions are fragile....should probably just stick to the get string versions def regen_hugin(self): self.set_text(self.to_str_core(False)) def regen_pto(self): self.set_text(self.to_str_core(True)) # XXX: a lot of this logic was moved out since it was more complicated than anticipated def to_ptoptimizer(self): '''Create a new, unusaved version compatible with PToptimizer''' ''' FIXME: this was a hack Really utilities should create a new PToptimizer compatible project and then return the string repr if users want it Illegal token in 'p'-line [83] [S] [S"103,21061,28,16889"] ''' return PTOProject.from_text(self.to_str_core(True)) def regen(self): self.regen_pto() def to_str_core(self, ptoptimizer_form): text = '' text += '# Generated by pr0ntools\n' #print 'Pano line: %s' % self.panorama_line if ptoptimizer_form: print 'generating ptopt form' key_blacklist = None if ptoptimizer_form: key_blacklist = 'E R S'.split() pass text += self.panorama_line.regen(key_blacklist) text += self.mode_line.regen() for line in self.image_lines: key_blacklist = None if ptoptimizer_form: key_blacklist = 'Eb Eev Er Ra Rb Rc Rd Re Va Vb Vc Vd Vx Vy'.split() text += line.regen(key_blacklist) for line in self.variable_lines: text += line.regen() for line in self.control_point_lines: text += line.regen() for line in self.absolute_control_point_lines: text += line.regen() for line in self.comment_lines: #text += line.regen() text += line + '\n' return text def __str__(self): # Might make this diff from get_text to show parser info at some point return self.get_text() def get_text(self): # If parsed then convert the intermediate repr since we may have modified from the saved value if self.parsed: #print 'get_text: constructed version' return self.to_str_core(False) else: #print 'get_text: file/text version' # Not parsed? Then just directly load from the file self.ensure_text_loaded() return self.text def ensure_text_loaded(self): if self.text is None: self.load_text() def load_text(self): self.text = open(self.file_name).read() self.parsed = False def make_absolute(self, to = None): '''Make all image paths absolute''' print 'Making %d images absolute' % len(self.get_image_lines()) for i in self.get_image_lines(): i.make_absolute(to) def make_relative(self, to = None): '''Make all image paths relative''' for i in self.get_image_lines(): i.make_relative(to) def do_get_a_file_name(self, prefix = None, postfix = None): '''If doesn't have a real file, create a temp file''' if self.file_name: return self.file_name if postfix is None: postfix = ".pto" self.temp_file = ManagedTempFile.get(prefix, postfix) self.file_name = self.temp_file.file_name return self.file_name def get_a_file_name(self, prefix = None, postfix = None): self.do_get_a_file_name(prefix, postfix) self.save() return self.file_name def set_file_name(self, file_name): self.file_name = file_name def set_text(self, text): self.text = text if self.file_name: self.save() self.parsed = False def merge_into(self, others): '''Merge project into this one''' print 'merge_into: others: %d' % len(others) # Merging modifies the project structure so make sure that a dummy merge occurs if nothing else temp = self.merge(others) self.text = str(temp) print 'merge_into: text len: %d' % len(self.text) if self.file_name: print 'merge_into: saving' self.save() def merge(self, others): '''Return a project containing both control points''' ''' Does not allow in place replace, we have to move things around [mcmaster@gespenst bin]$ pto_merge --help pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f Usage: pto_merge [options] input.pto input2.pto ... Options: -o, --output=file.pto Output Hugin PTO file. Default: <filename>_merge.pto -h, --help Shows this help ''' if len(others) == 0: print 'WARNING: skipping merge due to no other files' raise Exception('Nothing to merge') return None # Make sure that current project gets included # if empty we should still do this so that a merge can happen # as "merging" causes panotools project transforms tools may expect self.save() m = Merger(others) m.pto = self return m.run() def save(self): ''' I considered invalidating text here but for most operations I'm doing (PToptimizer excluded) only I modify the project import traceback import sys sys.stdout.flush() sys.stderr.flush() traceback.print_stack() sys.stdout.flush() sys.stderr.flush() ''' if self.file_name is None: raise Exception('Cannot save a project that was never assigned a filename') self.save_as(self.file_name) def save_as(self, file_name, is_new_filename = False): open(file_name, 'w').write(self.get_text()) if is_new_filename: self.file_name = file_name # reload is a builtin...not sure if it would conflict def reopen(self): f = open(self.file_name, 'r') self.text = f.read() self.parsed = False def get_file_names(self): '''Get image file names''' return [i.get_name() for i in self.get_image_lines()] def hugin_form(self): ''' This is used when merging through fortify stitch Something is causing pto_merge to hang, but NOT ptomerge Only occurs if I wrap my commands in a script... The script doesn't do any fancy I/O redirection clear rm -rf /tmp/pr0ntools_* pr0nstitch *.jpg out.pto pto_merge produces nicer output than ptomerge While ptomerge produces the fields I need, it leaves some other junk I think pto_merge also calculates width/heigh attributes part of Hugin [mcmaster@gespenst first]$ pto_merge Warning: pto_merge requires at least 2 project files pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f part of perl-Panotools-Script [mcmaster@gespenst first]$ ptomerge --help cannot read-open --help at /usr/share/perl5/Panotools/Script.pm line 91. man ptomerge ... ptomerge infile1.pto infile2.pto infile3.pto [...] outfile.pto ... ''' # However, this tool also generates an archaic .pto format that pto can parse, but I don't want to # pretend to merge into an empty project to force Hugin to clean it up # pto_merge --output=temp.pto /dev/null temp.pto if False: args = list() args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) (rc, output) = Execute.with_output("ptomerge", args) else: args = list() args.append("--output=%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) if False: args.append("/dev/null") else: empty_file = ManagedTempFile.get(None, ".pto") open(empty_file.file_name, 'w').write('') args.append(empty_file.file_name) (rc, output) = Execute.with_output("pto_merge", args) if not rc == 0: print print print if rc == 35072: # ex: empty projects seem to cause this print 'Out of memory, expect malformed project file' print 'output:%s' % output raise Exception('Bad rc: %d' % rc) self.reopen()
class PTOProject: def __init__(self): # File name, if one exists self.file_name = None # If this is a temporary project, have it delete upon destruction self.temp_file = None # Raw project text, None is not loaded self.text = None self.unparse() def unparse(self): # 'p' line self.panorama_line = None # 'm' line self.mode_line = None # Those started with #hugin_ # option_lines = None # Raw strings self.comment_lines = None # c N1 X1225.863118 Y967.737131 n0 t0 x1444.778035 y233.74261 self.control_point_lines = None self.absolute_control_point_lines = None self.image_lines = None self.img_fn2il = None self.il2i = None self.optimizer_lines = None ''' I bet lone v lines can be omitted # Variable lines v v d1 e1 p1 r1 y1 v d2 e2 p2 r2 y2 v d3 e3 p3 r3 y3 v ''' self.variable_lines = None # Raw strings, we don't know what these are self.misc_lines = list() # Has this been loaded from the file? self.parsed = False def remove_file_name(self): '''Unbound this from the filesystem''' self.ensure_text_loaded() self.file_name = None def copy(self, control_points=True): '''Return an unsaved but identical project''' # slow #return PTOProject.from_text(self.get_text()) # maybe...not sure about this cp_tmp = self.control_point_lines cp_tmp_abs = self.absolute_control_point_lines if not control_points: self.control_point_lines = [] self.absolute_control_point_lines = [] ret = copy.deepcopy(self) ret.file_name = None ret.temp_file = None #return self.to_str_core(False) if not control_points: self.control_point_lines = cp_tmp self.absolute_control_point_lines = cp_tmp_abs return ret def i2img(self, index): '''Given index return image object''' lines = self.get_image_lines() if index >= len(lines): raise IndexError('index: %d, items: %d' % (index, len(lines))) return lines[index] def i2i(self, project, i): '''Return index in this project given i in other project''' return self.img_fn2i(project.i2img(i).get_name()) def img_fn2i(self, fn): '''Given image file name return image index''' for i, il in enumerate(self.get_image_lines()): if fn == il.get_name(): return i return None def img_fn2l(self, fn): '''Given image file name return image line''' for il in self.get_image_lines(): if fn == il.get_name(): return il return None def assert_uniform_images(self): '''All images have same width and height''' w = None h = None for i in self.get_image_lines(): if w is None: w = i.width() if h is None: h = i.height() if w != i.width(): raise Exception('Mismatched width') if h != i.height(): raise Exception('Mismatched height') def get_comment_lines(self): self.parse() return self.comment_lines def get_image(self, n): '''Return image object n''' return self.get_image_lines()[n] def build_image_fn_map(self): self.img_fn2il = {} for il in self.get_image_lines(): self.img_fn2il[il.get_name()] = il def build_il2i(self): self.il2i = {} for i, il in enumerate(self.image_lines): self.il2i[il] = i def get_image_by_fn(self, fn): if self.img_fn2il: return self.img_fn2il.get(fn, None) for i in self.get_image_lines(): if fn == i.get_name(): return i return None def add_image(self, image_fn, calc_dim=True, def_opt=False): self.parse() il = ImageLine('i n"%s"' % image_fn, self) # set w/h if calc_dim: calc_il_dim(il) if def_opt: il.set_variable('f', 0) # default FOV il.set_variable('v', 51) # x/y position not yet calculated il.set_variable('d', 0) il.set_variable('e', 0) self.image_lines.append(il) def del_images(self, ils): '''Delete image as well as coresponding control point lines''' # added to support image sub-projects for fast preview ils_i = [il.get_index() for il in ils] new_image_lines = [] for i, il in enumerate(self.image_lines): if not i in ils_i: new_image_lines.append(il) # Build a map of old image index to new img_fn2ii_old = {} for i, il in enumerate(self.image_lines): img_fn2ii_old[il.get_name()] = i img_fn2ii_new = {} for i, il in enumerate(new_image_lines): img_fn2ii_new[il.get_name()] = i ii_old2new = {} for fn, ii_new in img_fn2ii_new.iteritems(): try: ii_old2new[img_fn2ii_old[fn]] = ii_new except: print 'ERROR' print fn print img_fn2ii_old print img_fn2ii_new print ii_old2new raise # remove unneeded control points # and replace image indices new_control_point_lines = [] for cpl in self.control_point_lines: n = cpl.getv('n') N = cpl.getv('N') if n not in ils_i and N not in ils_i: # Translate indices cpl.setv('n', ii_old2new[cpl.getv('n')]) cpl.setv('N', ii_old2new[cpl.getv('N')]) # and add to the keep set new_control_point_lines.append(cpl) # shift in new control point set self.control_point_lines = new_control_point_lines # FIXME: hack since I don't need variables for intended purpose of this function # variable lines are messy since technically you might have to split it self.variable_lines = [] # shift in new image line set self.image_lines = new_image_lines # Invalidate the index cache, if any self.img_fn2il = None def get_image_lines(self): self.parse() return self.image_lines def nimages(self): return len(self.get_image_lines()) def get_control_point_lines(self): self.parse() return self.control_point_lines def add_control_point_line(self, cl): self.parse() if self.control_point_lines is None: self.control_point_lines = [] self.control_point_lines.append(cl) def add_control_point_line_by_text(self, cl): self.add_control_point_line(ControlPointLine(cl, self)) def set_pano_line_by_text(self, line): self.parse() self.panorama_line = PanoramaLine(line, self) def set_mode_line_by_text(self, line): self.parse() self.mode_line = ModeLine(line, self) def add_image_line(self, il): self.parse() if self.image_lines is None: self.image_lines = [] self.image_lines.append(il) def add_image_line_by_text(self, il_text): il = ImageLine(il_text, self) self.add_image_line(il) def get_optimizer_lines(self): self.parse() return self.optimizer_lines def get_panorama_line(self): #print 'getting p line, parsed: %d' % self.parsed self.parse() return self.panorama_line def get_variable_lines(self): self.parse() return self.variable_lines @staticmethod def from_file_name(file_name, is_temporary = False): ret = PTOProject() ret.file_name = file_name if is_temporary: ret.temp_file = ManagedTempFile.from_existing(file_name) ret.parse() return ret @staticmethod def from_temp_file(temp_file): ret = PTOProject() ret.file_name = temp_file.file_name ret.temp_file = temp_file ret.parse() return ret @staticmethod def from_text(text): if text is None: raise Exception('Require text') ret = PTOProject() ret.text = text ret.reparse() return ret @staticmethod def from_blank(): return PTOProject.from_text('') @staticmethod def from_simple(): return PTOProject.from_text(''' p m ''') @staticmethod def from_default(): return PTOProject.from_text(''' p f0 v179 n"PSD_mask" E0.0 R0 m g1.0 i0 f0 m2 ''') @staticmethod def from_default2(): return PTOProject.from_text(''' p f0 v179 n"TIFF_m c:LZW" E0.0 R0 m g1.0 i0 f0 m2 ''') def parse(self): '''Parse if not already parsed''' if not self.parsed: self.reparse() # We should now be using the intermediate form # Force a regen if someone wants text self.text = None def reparse(self): '''Force a parse''' if False: print 'WARNING: pto parsing disabled' return self.unparse() self.panorama_line = None self.mode_line = None self.comment_lines = list() self.variable_lines = list() self.control_point_lines = list() self.absolute_control_point_lines = list() self.image_lines = list() self.misc_lines = list() self.optimizer_lines = list() if self.text is None: self.text = open(self.file_name).read() #print self.text dbg('Beginning split on text of len %d' % (len(self.text))) for line in self.text.split('\n'): dbg('Processing line: %s' % line) # Single * is end of file # Any comments / garbage is allowed to follow #if line.strip() == '*': # break # In practice this is PTOptimizer output I want # Add an option later if needed to override self.parse_line(line) dbg() #print 'Finished reparse' self.parsed = True def parse_line(self, line): # Ignore empty lines if len(line) == 0: return k = line[0] if k == '#': # Ignore comments and option lines for now # They have position dependencies and usually can be ignored anyway for my purposes # They are mostly used by Hugin #print 'WARNING: ignoring comment line: %s' % line self.comment_lines.append(line) # EOF marker, used for PToptimizer to indicate end of original project elif k == '*': pass # Panorama line elif k == "p": self.panorama_line = PanoramaLine(line, self) # additional options elif k == "m": self.mode_line = ModeLine(line, self) # Image line elif k == "i": self.image_lines.append(ImageLine(line, self)) # Optimization (variable) line elif k == "v": self.variable_lines.append(VariableLine(line, self)) # Control point line elif k == "c": self.control_point_lines.append(ControlPointLine(line, self)) elif k == 'C': self.absolute_control_point_lines.append(AbsoluteControlPointLine(line, self)) # Generated by PToptimizer elif k == "o": self.optimizer_lines.append(OptimizerLine(line, self)) else: print 'WARNING: unknown line type: %s' % line self.misc_lines.append(line) # These functions are fragile....should probably just stick to the get string versions def regen_hugin(self): self.set_text(self.to_str_core(False)) def regen_pto(self): self.set_text(self.to_str_core(True)) def regen(self): self.regen_pto() def to_str_core(self, ptoptimizer_form): self.build_il2i() text = '' text += '# Generated by pr0ntools\n' #print 'Pano line: %s' % self.panorama_line if ptoptimizer_form: print 'generating ptopt form' key_blacklist = None if ptoptimizer_form: key_blacklist = 'E R S'.split() if self.panorama_line: text += self.panorama_line.regen(key_blacklist) if self.mode_line: text += self.mode_line.regen() for line in self.image_lines: key_blacklist = None if ptoptimizer_form: key_blacklist = 'Eb Eev Er Ra Rb Rc Rd Re Va Vb Vc Vd Vx Vy'.split() text += line.regen(key_blacklist) for line in self.variable_lines: text += line.regen() for line in self.control_point_lines: text += line.regen() for line in self.absolute_control_point_lines: text += line.regen() for line in self.comment_lines: #text += line.regen() text += line + '\n' self.il2i = None return text def __str__(self): # Might make this diff from get_text to show parser info at some point return self.get_text() def get_text(self): # If parsed then convert the intermediate repr since we may have modified from the saved value if self.parsed: #print 'get_text: constructed version' return self.to_str_core(False) else: #print 'get_text: file/text version' # Not parsed? Then just directly load from the file self.ensure_text_loaded() return self.text def ensure_text_loaded(self): if self.text is None: self.load_text() def load_text(self): self.text = open(self.file_name).read() self.parsed = False def make_absolute(self, to = None): '''Make all image paths absolute''' #print 'Making %d images absolute' % len(self.get_image_lines()) for i in self.get_image_lines(): i.make_absolute(to) def make_relative(self, to = None): '''Make all image paths relative''' for i in self.get_image_lines(): i.make_relative(to) def get_a_file_name(self, prefix = None, postfix = None): '''Return a file name that has current .pto contents''' '''If doesn't have a real file, create a temp file''' if self.file_name: return self.file_name if postfix is None: postfix = ".pto" self.temp_file = ManagedTempFile.get(prefix, postfix) self.file_name = self.temp_file.file_name # hmmm... self.save() return self.file_name def set_file_name(self, file_name): self.file_name = file_name def set_text(self, text): self.text = text if self.file_name: self.save() self.parsed = False def merge_into(self, ptos): '''Merge project into this one. Output file is updated''' print 'merge_into: others: %d' % len(ptos) this = [] if self.file_name and os.path.exists(self.file_name): this = [self] # Merge is based on filename # Must be synced to disk self.save() # Merging modifies the project structure so make sure that a dummy merge occurs if nothing else m = Merger(this + ptos) temp = m.run() shutil.move(temp.file_name, self.file_name) self.reopen() def merge(self, ptos): '''Return a project containing both control points''' ''' Does not allow in place replace, we have to move things around [mcmaster@gespenst bin]$ pto_merge --help pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f Usage: pto_merge [options] input.pto input2.pto ... Options: -o, --output=file.pto Output Hugin PTO file. Default: <filename>_merge.pto -h, --help Shows this help ''' if len(ptos) == 0: print 'WARNING: skipping merge due to no other files' raise Exception('Nothing to merge') return None this = [] if self.file_name and os.path.exists(self.file_name): this = [self] # Merge is based on filename # Must be synced to disk self.save() m = Merger(this + ptos) return m.run() def save(self): ''' I considered invalidating text here but for most operations I'm doing (PToptimizer excluded) only I modify the project import traceback import sys sys.stdout.flush() sys.stderr.flush() traceback.print_stack() sys.stdout.flush() sys.stderr.flush() ''' if self.file_name is None: raise Exception('Cannot save a project that was never assigned a filename') self.save_as(self.file_name) def save_as(self, file_name, is_new_filename = False): with open(file_name + '.tmp', 'w') as f: f.write(self.get_text()) shutil.move(file_name + '.tmp', file_name) if is_new_filename: self.file_name = file_name # reload is a builtin...not sure if it would conflict def reopen(self): f = open(self.file_name, 'r') self.text = f.read() self.reparse() def get_file_names(self): '''Get image file names''' return [i.get_name() for i in self.get_image_lines()] def hugin_form(self): ''' This is used when merging through fortify stitch Something is causing pto_merge to hang, but NOT ptomerge Only occurs if I wrap my commands in a script... The script doesn't do any fancy I/O redirection clear rm -rf /tmp/pr0ntools_* pr0nstitch *.jpg out.pto pto_merge produces nicer output than ptomerge While ptomerge produces the fields I need, it leaves some other junk I think pto_merge also calculates width/heigh attributes part of Hugin [mcmaster@gespenst first]$ pto_merge Warning: pto_merge requires at least 2 project files pto_merge: merges several project files pto_merge version 2010.4.0.854952d82c8f part of perl-Panotools-Script [mcmaster@gespenst first]$ ptomerge --help cannot read-open --help at /usr/share/perl5/Panotools/Script.pm line 91. man ptomerge ... ptomerge infile1.pto infile2.pto infile3.pto [...] outfile.pto ... ''' # However, this tool also generates an archaic .pto format that pto can parse, but I don't want to # pretend to merge into an empty project to force Hugin to clean it up # pto_merge --output=temp.pto /dev/null temp.pto if False: args = list() args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) (rc, output) = Execute.with_output("ptomerge", args) else: args = list() args.append("--output=%s" % self.get_a_file_name()) args.append("%s" % self.get_a_file_name()) if False: args.append("/dev/null") else: empty_file = ManagedTempFile.get(None, ".pto") open(empty_file.file_name, 'w').write('') args.append(empty_file.file_name) (rc, output) = Execute.with_output("pto_merge", args) if not rc == 0: print print print if rc == 35072: # ex: empty projects seem to cause this print 'Out of memory, expect malformed project file' print 'output:%s' % output raise Exception('Bad rc: %d' % rc) self.reopen()