예제 #1
0
파일: project.py 프로젝트: vonj/xystitch
    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)
예제 #2
0
	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)
예제 #3
0
파일: project.py 프로젝트: vonj/xystitch
 def set_pano_line_by_text(self, line):
     self.parse()
     self.panorama_line = PanoramaLine(line, self)
예제 #4
0
파일: project.py 프로젝트: vonj/xystitch
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()
예제 #5
0
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()		
예제 #6
0
 def set_pano_line_by_text(self, line):
     self.parse()
     self.panorama_line = PanoramaLine(line, self)
예제 #7
0
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()