#!/usr/bin/env python from __future__ import print_function gpl = r""" crop_cp.py - remove CPs from parts of the panorama Copyright (C) 2011 Kay F. Jahnke This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ # @category Control Points # @name Crop Control Points # @api-min 2011.1 # @api-max 2011.2 # crop_cps will function as a hugin plugin and as a standalone # Python script. It can also serve as a template for a slightly # more involved plugin than the ones in the Examples section. # Please note that this script tries to use wxPython. This should # work on Linux systems that have wxPython istalled, but as far as # I know currently it won't work on Windows, and certainly not on # Mac OS, where there is currently no hsi/hpi suport. If you want # to switch off the use of wxPython, set the default parameter in # the entry function to a value different from None, i.e. change # def entry ( pano , inside = None ) : # to # def entry ( pano , inside = False ) : import os import sys import hsi import math # we make an exception for run time errors in this script: class CropCpError ( Exception ) : def __init__ ( self , text ) : self.text = text def __str__ ( self ) : return self.text # some global variables to reduce overhead - we just reuse the same # FDiff2D objects every time we call the transforms transform_in = hsi.FDiff2D(0,0) transform_out = hsi.FDiff2D(0,0) # transform point coordinates using panotools transforms # and the global variables for source and target class to_pano : def __init__ ( self , pano , image ) : pano_options = pano.getOptions() img = pano.getImage ( image ) w = img.getWidth() h = img.getHeight() self.wh = w / 2.0 self.hh = h / 2.0 # image to pano self.tf = hsi.Transform() self.tf.createInvTransform ( img , pano_options ) def transform ( self , p ) : # pixel coordinates come in, but have to use center-origin transform_in.x = p[0] - self.wh transform_in.y = p[1] - self.hh if self.tf.transform ( transform_out , transform_in ) : return ( transform_out.x , transform_out.y ) # KFJ 2011-06-19 I've noticed the transform failing # with images that have X, Y or Z values different from 0. # I'm not sure if this is a bug in the transform. print ( 'transform failed for %s' % str ( p ) ) return None # the crop_cps routine is the workhorse of this script. It does the # actual work of looking at the CPs and removing the unwanted ones. def crop_cps ( pano , inside ) : pano_options = pano.getOptions() size = pano_options.getSize() # we need these offsets because pano coordinates are center-origin wh = size.width() / 2.0 hh = size.height() / 2.0 # that's the ROI of the panorama, in corner-origin roi = pano_options.getROI() # out limits, now in center-origin left = roi.left() - wh right = roi.right() - wh top = roi.top() - hh bottom = roi.bottom() - hh ni = pano.getNrOfImages() tflist = [] # we get a list of CPs in pano coordinates. Note that the pano # coordinates are taken from the coordinates in the left image only. for img in range ( ni ) : tflist.append ( to_pano ( pano , img ) ) # we take the CPVector: cpv = pano.getCtrlPoints() print ( 'found %d CPs' % len ( cpv ) ) # We want to put the remaining CPs into this CPVector: ncpv = hsi.CPVector() for cp in cpv : p = ( cp.x1 , cp.y1 ) img = cp.image1Nr tfp = tflist[img].transform ( p ) # KFJ 2011-06-17 If the transform fails, keep the CP # - best to be defensive. if tfp is None : ncpv.append ( cp ) elif inside : # remove CPs inside ROI (or, keep those outside) if not ( ( left <= tfp[0] <= right ) and ( top <= tfp[1] <= bottom ) ) : ncpv.append ( cp ) else : if ( left <= tfp[0] <= right ) and ( top <= tfp[1] <= bottom ) : ncpv.append ( cp ) pano.setCtrlPoints ( ncpv ) print ( 'removed %d CPs' % ( len ( cpv ) - len ( ncpv ) ) ) return 0 # the remainder of the script is administration and deals with running # it either as a plugin from hugin or as a standalone CLI program. # first is the entry routine which is called when this script is used # as a plugin from hugin. If the inside parameter is not passed # (or passed as 'None') - a dialog will ask whether to remove CPs # inside or outside of the ROI. If inside is passed as True, CPs # inside the ROI are removed, otherwise those outside. # Note that the 'GUI' is primitive - it offers no help or about info, # no tool tips etc. - the help could be made from the docstrings of # the CLI version below. def entry ( pano , inside = None ) : if inside is None: # if we've been called as a plugin (we're not main) and inside is # None, the default, we assume that we're supposed to ask the user # whether to remove the CPs from inside or outside the ROI. If # inside is passed as True or False, we execute without further ado. import wx # we need a simple dialog box for this purpose which displays # a radio button for the inside/outside choice, plus Okay and Cancel. class crop_cp_dialog ( wx.Dialog ) : # excuse my clumsy wxPython ;-) def __init__(self, parent, title): wx.Dialog.__init__(self, parent, -1, title, size = ( 200 , 150 ) ) choices = [ 'outside ROI' , 'inside ROI' ] # default choice is to remove CPs outdide ROI self.choice = 0 # we create and position the UI elements sizer = wx.BoxSizer ( wx.VERTICAL ) rb = wx.RadioBox( self, -1, "", wx.DefaultPosition, wx.DefaultSize, choices, 2, wx.RA_SPECIFY_COLS ) self.Bind(wx.EVT_RADIOBOX, self.EvtRadioBox, rb) sizer.Add(rb, 0, wx.ALL, 20) btnsizer = wx.StdDialogButtonSizer() btn = wx.Button(self, wx.ID_OK) btn.SetDefault() btnsizer.AddButton(btn) btn = wx.Button(self, wx.ID_CANCEL) btnsizer.AddButton(btn) btnsizer.Realize() sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) self.SetSizer(sizer) # what to do when the radio button is operated: def EvtRadioBox(self, event): self.choice = event.GetInt() # make the dialog and show it modally: ccd = crop_cp_dialog ( None , 'Crop CPs' ) retval = ccd.ShowModal() # only if Okay was clicked the state of the radio button is used if retval == wx.ID_OK : if ccd.choice == 0 : inside = False else : inside = True # otherwise we cancel the operation and return. else : return 0 # now the preliminaries are sorted, we can call the workhorse routine. return crop_cps ( pano , inside ) # the main routine is what's called if this script has been called from # the command line. In this case we do the CLI thing, so we parse the # argument vector and act accordingly. def main() : # when called from the command line, we import the argparse module. # This may not be part of the standard library on your system. import argparse # and create an argument parser. parser = argparse.ArgumentParser ( formatter_class=argparse.RawDescriptionHelpFormatter , description = gpl + ''' crop_cp will remove all CPs which are either outside the panorama's ROI or inside it's ROI if the inside flag is set ''' ) parser.add_argument('-o', '--output', metavar='', default = None, type=str, help='write output to a different file') parser.add_argument('-i', '--inside', action='store_true', default = False, help='remove CPs inside ROI') parser.add_argument('input' , metavar = '' , type = str , help = 'pto file to be processed' ) if len ( sys.argv ) < 2 : parser.print_help() return # we parse the arguments into a global variable so we can access # them from everywhere without having to pass them around global args args = parser.parse_args() if args.output : print ( 'output: ' , args.output ) print ( 'ptofile:' , args.input ) # first we see if we can open the input file ifs = hsi.ifstream ( args.input ) if not ifs.good() : raise CropCpError ( 'cannot open input file %s' % args.input ) pano = hsi.Panorama() success = pano.readData ( ifs ) del ifs if success != hsi.DocumentData.SUCCESSFUL : raise CropCpError ( 'input file %s contains invalid data' % args.input ) # if a separate output file was chosen, we open it now to avoid # later disappointments if args.output: ofs = hsi.ofstream ( args.output ) if not ofs.good() : raise CropCpError ( 'cannot open output file %s' % args.output ) # we've checked the args, now we call crop_cps() crop_cps ( pano , args.inside ) # if no different output file was specified, overwrite the input if not args.output : ofs = hsi.ofstream ( args.input ) if not ofs.good() : raise CropCpError ( 'cannot open file %s for output' % args.input ) success = pano.writeData ( ofs ) del ofs if success != hsi.DocumentData.SUCCESSFUL : raise CropCpError ( 'error writing pano to %s' % args.input ) # done. return 0 # are we main? This means we've been called from the command line # as a standalone program, so we have to go through the main() function. if __name__ == "__main__": try: main() except CropCpError as e : print ( 'Run Time Error: %s' % e )