def run(self): ''' Phase 1: remap the relevant source image areas onto a canvas Note that nona will load ALL of the images (one at a time) but will only generate output for those that matter Each one takes a noticible amount of time but its relatively small compared to the time spent actually mapping images ''' print print 'Supertile phase 1: remapping (nona)' if self.out.find('.') < 0: raise Exception('Require image extension') # Hugin likes to use the base filename as the intermediates, lets do the sames out_name_base = self.out[0:self.out.find('.')].split('/')[-1] print "out name: %s, base: %s" % (self.out, out_name_base) #ssadf if out_name_base is None or len(out_name_base) == 0 or out_name_base == '.' or out_name_base == '..': raise Exception('Bad output file base "%s"' % str(out_name_base)) # Scope of these files is only here # We only produce the single output file, not the intermediates managed_temp_dir = ManagedTempDir.get() # without the slash they go into the parent directory with that prefix out_name_prefix = managed_temp_dir.file_name + "/" pto = self.pto.copy() print 'Making absolute' pto.make_absolute() print 'Cropping...' #sys.exit(1) pl = pto.get_panorama_line() # It is fine to go out of bounds, it will be black filled #pl.set_bounds(x, min(x + self.tw(), pto.right()), y, min(y + self.th(), pto.bottom())) pl.set_crop(self.bounds) remapper = Remapper(pto, out_name_prefix) remapper.remap() ''' Phase 2: blend the remapped images into an output image ''' print print 'Supertile phase 2: blending (enblend)' blender = Blender(remapper.get_output_files(), self.out) blender.run() # We are done with these files, they should be nuked if not config.keep_temp_files(): for f in remapper.get_output_files(): os.remove(f) print 'Supertile ready!'
class CommonStitch: def __init__(self): self.output_image_file_name = None self.project = None self.remapper = None self.photometric_optimizer = None self.optimize = True self.optimizer = None self.cleaner = None # Used before init, later ignore for project.file_name self.output_project_file_name = None self.image_file_names = None self.control_point_gen = None # Images have predictable separation? self.regular = False # Only used if regular image self.subimage_control_points = True # TODO: parse these from scan.json # and fix scan.json to invert these to match these values self.x_overlap = 1.0 / 3.0 self.y_overlap = 1.0 / 3.0 self.dry = False # Each filename as the key #self.failures = FailedImages() self.failures = None def set_dry(self, d): self.dry = d def set_regular(self, regular): self.regular = regular def set_output_project_file_name(self, file_name): self.output_project_file_name = file_name def set_output_image_file_name(self, file_name): self.output_image_file_name = file_name def init_failures(self): pass def run(self): if self.dry: print 'Dry run abort' return if not self.output_project_file_name and not self.output_image_file_name: raise Exception("need either project or image file") #if not self.output_project_file_name: #self.project_temp_file = ManagedTempFile.get() #self.output_project_file_name = self.project_temp_file.file_name print 'Beginning stitch' print 'output project file name: %s' % self.output_project_file_name print 'output image file name: %s' % self.output_image_file_name #sys.exit(1) self.init_failures() # Generate control points and merge them into a master project self.control_point_gen = ControlPointGenerator() # How many rows and cols to go to each side # If you hand took the pictures, this might suit you self.project = PTOProject.from_blank() if self.output_project_file_name: self.project.set_file_name(self.output_project_file_name) if os.path.exists(self.output_project_file_name): # Otherwise, we merge into it print 'WARNING: removing old project file: %s' % self.output_project_file_name os.remove(self.output_project_file_name) else: self.project.get_a_file_name(None, "_master.pto") self.project.image_file_names = self.image_file_names try: ''' Generate control points ''' self.generate_control_points() if False: self.photometric_optimizer = PhotometricOptimizer(self.project) self.photometric_optimizer.run() # Remove statistically unpleasant points if False: self.cleaner = PTOClean(self.project) self.cleaner.run() print 'Post stitch fixup...' optimize_xy_only(self.project) fixup_i_lines(self.project) fixup_p_lines(self.project) if 0: center_anchor(self.project) print print '***PTO project baseline final (%s / %s) data length %d***' % (self.project.file_name, self.output_project_file_name, len(self.project.get_text())) print if self.failures: print 'Writing failure JSON' cc = self.failures.critical_count() print '%d pairs failed to make %d images critical' % (self.failures.pair_count(), cc) if cc: print '******WARNING WARNING WARING******' print '%d images are not connected' % cc print '******WARNING WARNING WARING******' open('stitch_failures.json', 'w').write(str(self.failures)) print # Make dead sure its saved up to date self.project.save() # having issues with this.. if self.output_project_file_name and not self.project.file_name == self.output_project_file_name: raise Exception('project file name changed %s %s', self.project.file_name, self.output_project_file_name) self.optimize = False if self.optimize: self.optimizer = optimizer.PTOptimizer(self.project) self.optimizer.run() center(self.project) # TODO: missing calc opt size/width/height/fov and crop # Did we request an actual stitch? if self.output_image_file_name: print 'Stitching...' self.remapper = Remapper(self.project) self.remapper.remap(self.output_image_file_name) else: print 'NOT stitching (common stitch)' except Exception as e: print print 'WARNING: stitch FAILED' try: fn = self.project.file_name + ".failed" print 'Attempting to save intermediate result to %s' % fn self.project.save_as(fn) except: print 'WARNING: failed intermediate save' raise e def control_points_by_subimage(self, pair, image_fn_pair, subimage_factor = None): '''Stitch two images together by cropping to restrict overlap''' # subimage_factor: (y, x) overlap percent tuple or none for default # pair: pair of row/col or coordinate positions (used to determine relative positions) # (0, 0) at upper left # image_fn_pair: pair of image file names print 'Preparing subimage stitch on %s:%s' % (image_fn_pair[0], image_fn_pair[1]) ''' Just work on the overlap section, maybe even less ''' images = [PImage.from_file(image_file_name) for image_file_name in image_fn_pair] ''' image_0 used as reference 4 basic situations: left, right, up right 8 extended: 4 basic + corners Pairs should be sorted, which simplifies the logic ''' sub_image_0_x_delta = 0 sub_image_0_y_delta = 0 sub_image_1_x_end = images[1].width() sub_image_1_y_end = images[1].height() if subimage_factor: y_overlap = subimage_factor[0] x_overlap = subimage_factor[1] else: x_overlap = self.x_overlap y_overlap = self.y_overlap # image 0 left of image 1? if pair.first.col < pair.second.col: # Keep image 0 right, image 1 left sub_image_0_x_delta = int(images[0].width() * (1.0 - x_overlap)) sub_image_1_x_end = int(images[1].width() * x_overlap) # image 0 above image 1? if pair.first.row < pair.second.row: # Keep image 0 top, image 1 bottom sub_image_0_y_delta = int(images[0].height() * (1.0 - y_overlap)) sub_image_1_y_end = int(images[1].height() * y_overlap) ''' print 'image 0 x delta: %d, y delta: %d' % (sub_image_0_x_delta, sub_image_0_y_delta) Note y starts at top in PIL ''' sub_image_0 = images[0].subimage(sub_image_0_x_delta, None, sub_image_0_y_delta, None) sub_image_1 = images[1].subimage(None, sub_image_1_x_end, None, sub_image_1_y_end) sub_image_0_file = ManagedTempFile.get(None, '.jpg') sub_image_1_file = ManagedTempFile.get(None, '.jpg') print 'sub image 0: width=%d, height=%d, name=%s' % (sub_image_0.width(), sub_image_0.height(), sub_image_0_file.file_name) print 'sub image 1: width=%d, height=%d, name=%s' % (sub_image_1.width(), sub_image_1.height(), sub_image_0_file.file_name) #sys.exit(1) sub_image_0.image.save(sub_image_0_file.file_name) sub_image_1.image.save(sub_image_1_file.file_name) sub_image_fn_pair = (sub_image_0_file.file_name, sub_image_1_file.file_name) # subimage file name symbolic link to subimage file name # this should be taken care of inside of control point actually #sub_link_to_sub = dict() # subimage to the image it came from sub_to_real = dict() sub_to_real[sub_image_0_file.file_name] = image_fn_pair[0] sub_to_real[sub_image_1_file.file_name] = image_fn_pair[1] # Returns a pto project object fast_pair_project = self.control_point_gen.generate_core(sub_image_fn_pair) if fast_pair_project is None: print 'WARNING: failed to gen control points @ %s' % repr(pair) return None oto_text = str(fast_pair_project) if 0: print oto_text # are we actually doing anything useful here? # The original intention was to make dead sure we had the right file order # but I'm pretty sure its consistent and we don't need to parse the comments final_pair_project = ajpto2pto_text(oto_text, sub_image_0_file, sub_image_1_file, sub_image_0_x_delta, sub_image_0_y_delta, sub_to_real) # Filenames become absolute #sys.exit(1) return final_pair_project def try_control_points_with_position(self, pair, image_fn_pair, subimage_factor = None): '''Try to stitch two images together without any (high level) image processing other than cropping''' # If images are arranged in a regular grid and we are allowed to crop do it if self.regular and self.subimage_control_points: return self.control_points_by_subimage(pair, image_fn_pair, subimage_factor) # Otherwise run stitches on the full image else: print 'Full image stitch (not partial w/ regular %d and subimage control %d)' % (self.regular, self.subimage_control_points) return self.control_point_gen.generate_core(image_fn_pair) # Control point generator wrapper entry def generate_control_points_by_pair(self, pair, image_fn_pair): ret = self.do_generate_control_points_by_pair(pair, image_fn_pair) # If it failed and they were adjacent it is a "critical pair" if self.failures and pair.adjacent(): if ret: self.failures.add_success(image_fn_pair) else: self.failures.add_failure(image_fn_pair) return ret def do_generate_control_points_by_pair(self, pair, image_fn_pair): '''high level function uses by sub-stitches. Given a pair of images make a best effort to return a .pto object''' ''' pair: ImageCoordinatePair() object image_fn_pair: tuple of strings Algorithm: First try to stitch normally (either whole image or partial depending on the mode) If that doesn't succeed and softening is enabled try up to three times to soften to produce a match If that still doesn't produce a decent solution return None and let higher levels deal with ''' soften_iterations = 3 print print #print 'Generating project for image pair (%s / %s, %s / %s)' % (image_fn_pair[0], str(pair[0]), image_fn_pair[1], str(pair[1])) print 'Generating project for image pair (%s, %s)' % (image_fn_pair[0], image_fn_pair[1]) if True: # Try raw initially print 'Attempting sharp match...' ret_project = self.try_control_points_with_position(pair, image_fn_pair) if ret_project: return ret_project print 'WARNING: bad project, attempting soften...' soften_image_file_0_managed = ManagedTempFile.from_same_extension(image_fn_pair[0]) soften_image_file_1_managed = ManagedTempFile.from_same_extension(image_fn_pair[1]) softener = Softener() first_run = True for i in range(0, soften_iterations): # And then start screwing with it # Wonder if we can combine features from multiple soften passes? # Or at least take the maximum # Do features get much less accurate as the soften gets up there? print 'Attempting soften %d / %d' % (i + 1, soften_iterations) if first_run: softener.run(image_fn_pair[0], soften_image_file_0_managed.file_name) softener.run(image_fn_pair[1], soften_image_file_1_managed.file_name) else: softener.run(soften_image_file_0_managed.file_name) softener.run(soften_image_file_1_managed.file_name) pair_soften_image_file_names = (soften_image_file_0_managed.file_name, soften_image_file_1_managed.file_name) ret_project = self.try_control_points_with_position(pair, pair_soften_image_file_names) # Did we win? if ret_project: # Fixup the project to reflect the correct file names text = str(ret_project) print print 'Before sub' print print str(ret_project) print print print print '%s => %s' % (soften_image_file_0_managed.file_name, image_fn_pair[0]) text = text.replace(soften_image_file_0_managed.file_name, image_fn_pair[0]) print '%s => %s' % (soften_image_file_1_managed.file_name, image_fn_pair[1]) text = text.replace(soften_image_file_1_managed.file_name, image_fn_pair[1]) ret_project.set_text(text) print print 'After sub' print print str(ret_project) print print print #sys.exit(1) return ret_project first_run = False print 'WARNING: gave up on generating control points!' return None