def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = PyQt4.QtCore.QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'SatEx_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = PyQt4.QtCore.QTranslator() self.translator.load(locale_path) if PyQt4.QtCore.qVersion() > '4.3.3': PyQt4.QtCore.QCoreApplication.installTranslator( self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&GFZ SatEx') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SatEx') self.toolbar.setObjectName(u'SatEx') #create dialogs and keep reference self.Pdlg = PreprocessingDialog() self.Cdlg = ClassificationDialog() #gui interactions #Preprocessing self.Pdlg.lineEdit.clear() self.Pdlg.pushButton.clicked.connect(self.select_input_raster) self.Pdlg.lineEdit_2.clear() self.Pdlg.pushButton_2.clicked.connect(self.select_roi) self.Pdlg.lineEdit_3.clear() self.Pdlg.pushButton_3.clicked.connect(self.select_Poutput_name) self.Pdlg.toolButton.clicked.connect(self.show_help) #Classification self.Cdlg.lineEdit.clear() self.Cdlg.pushButton.clicked.connect(self.select_input_pan) self.Cdlg.lineEdit_2.clear() self.Cdlg.pushButton_2.clicked.connect(self.select_training) self.Cdlg.lineEdit_3.clear() self.Cdlg.pushButton_3.clicked.connect(self.select_Coutput_name) self.Cdlg.checkBox.clicked.connect(self.switch_external_SVM) self.Cdlg.checkBox_3.clicked.connect(self.switch_sieve) self.Cdlg.pushButton_4.clicked.connect(self.select_CSVM) self.Cdlg.lineEdit_6.setText('4') self.Cdlg.toolButton.clicked.connect(self.show_help) #setup subrpocess differently for windows self.startupinfo = None if os.name == 'nt': self.startupinfo = subprocess.STARTUPINFO() self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #Prefent bug in directory search (added / on linux results in searching the root directory) self.Pdlg.lineEdit.setText(' ')
def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = PyQt4.QtCore.QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'SatEx_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = PyQt4.QtCore.QTranslator() self.translator.load(locale_path) if PyQt4.QtCore.qVersion() > '4.3.3': PyQt4.QtCore.QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&GFZ SatEx') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SatEx') self.toolbar.setObjectName(u'SatEx') #create dialogs and keep reference self.Pdlg = PreprocessingDialog() self.Cdlg = ClassificationDialog() #gui interactions #Preprocessing self.Pdlg.lineEdit.clear() self.Pdlg.pushButton.clicked.connect(self.select_input_raster) self.Pdlg.lineEdit_2.clear() self.Pdlg.pushButton_2.clicked.connect(self.select_roi) self.Pdlg.lineEdit_3.clear() self.Pdlg.pushButton_3.clicked.connect(self.select_Poutput_name) self.Pdlg.toolButton.clicked.connect(self.show_help) #Classification self.Cdlg.lineEdit.clear() self.Cdlg.pushButton.clicked.connect(self.select_input_pan) self.Cdlg.lineEdit_2.clear() self.Cdlg.pushButton_2.clicked.connect(self.select_training) self.Cdlg.lineEdit_3.clear() self.Cdlg.pushButton_3.clicked.connect(self.select_Coutput_name) self.Cdlg.checkBox.clicked.connect(self.switch_external_SVM) self.Cdlg.checkBox_3.clicked.connect(self.switch_sieve) self.Cdlg.pushButton_4.clicked.connect(self.select_CSVM) self.Cdlg.lineEdit_6.setText('4') self.Cdlg.toolButton.clicked.connect(self.show_help) #setup subrpocess differently for windows self.startupinfo = None if os.name == 'nt': self.startupinfo = subprocess.STARTUPINFO() self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #Prefent bug in directory search (added / on linux results in searching the root directory) self.Pdlg.lineEdit.setText(' ')
class SatEx: """QGIS Plugin Implementation.""" def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = PyQt4.QtCore.QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'SatEx_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = PyQt4.QtCore.QTranslator() self.translator.load(locale_path) if PyQt4.QtCore.qVersion() > '4.3.3': PyQt4.QtCore.QCoreApplication.installTranslator( self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&GFZ SatEx') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SatEx') self.toolbar.setObjectName(u'SatEx') #create dialogs and keep reference self.Pdlg = PreprocessingDialog() self.Cdlg = ClassificationDialog() #gui interactions #Preprocessing self.Pdlg.lineEdit.clear() self.Pdlg.pushButton.clicked.connect(self.select_input_raster) self.Pdlg.lineEdit_2.clear() self.Pdlg.pushButton_2.clicked.connect(self.select_roi) self.Pdlg.lineEdit_3.clear() self.Pdlg.pushButton_3.clicked.connect(self.select_Poutput_name) self.Pdlg.toolButton.clicked.connect(self.show_help) #Classification self.Cdlg.lineEdit.clear() self.Cdlg.pushButton.clicked.connect(self.select_input_pan) self.Cdlg.lineEdit_2.clear() self.Cdlg.pushButton_2.clicked.connect(self.select_training) self.Cdlg.lineEdit_3.clear() self.Cdlg.pushButton_3.clicked.connect(self.select_Coutput_name) self.Cdlg.checkBox.clicked.connect(self.switch_external_SVM) self.Cdlg.checkBox_3.clicked.connect(self.switch_sieve) self.Cdlg.pushButton_4.clicked.connect(self.select_CSVM) self.Cdlg.lineEdit_6.setText('4') self.Cdlg.toolButton.clicked.connect(self.show_help) #setup subrpocess differently for windows self.startupinfo = None if os.name == 'nt': self.startupinfo = subprocess.STARTUPINFO() self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #Prefent bug in directory search (added / on linux results in searching the root directory) self.Pdlg.lineEdit.setText(' ') #TODO:defaults for development #self.Pdlg.lineEdit.setText('C:\Users\michael\Desktop\plugin data\LC81680542015357LGN00') #self.Pdlg.lineEdit_2.setText('C:/Users/michael/Desktop/plugin data/adisababa.shp') #self.Pdlg.lineEdit_3.setText('C:/Users/michael/Desktop/test.vrt') #self.Pdlg.lineEdit.setText('/home/mhaas/PhD/Routines/rst/plugin/data/LC81680542015357LGN00/') #self.Pdlg.lineEdit_2.setText('/home/mhaas/PhD/Routines/rst/plugin/data/adisababa.shp') #self.Pdlg.lineEdit_3.setText('/home/mhaas/test/test.vrt') #TODO:defaults for development #self.Cdlg.lineEdit.setText('Path to vrt') #self.Cdlg.lineEdit_2.setText('Path to training shapefile') #self.Cdlg.lineEdit_3.setText('Path to output-tif') #self.Cdlg.lineEdit_5.setText('Training class label') # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return PyQt4.QtCore.QCoreApplication.translate('SatEx', message) def add_action(self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = PyQt4.QtGui.QIcon(icon_path) action = PyQt4.QtGui.QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToMenu(self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SatEx/icon_preprocessing.png' self.add_action(icon_path, text=self.tr(u'Preprocessing'), callback=self.run_preprocessing, parent=self.iface.mainWindow()) icon_path = ':/plugins/SatEx/icon_classification.png' self.add_action(icon_path, text=self.tr(u'Classification'), callback=self.run_classification, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginMenu(self.tr(u'&GFZ SatEx'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def updatePForm(self): #get user edits self.ls_path = self.Pdlg.lineEdit.text() + '/' self.roi = self.Pdlg.lineEdit_2.text() self.out_fname = self.Pdlg.lineEdit_3.text() if (self.ls_path == ' /' or self.roi == '' or self.out_fname == ''): return False else: return True def select_input_raster(self): dirname = PyQt4.QtGui.QFileDialog.getExistingDirectory( self.Pdlg, "Select input directory ", "", PyQt4.QtGui.QFileDialog.ShowDirsOnly) self.Pdlg.lineEdit.setText(dirname) def select_roi(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName( self.Pdlg, "Select region of interest ", "", "Shapefile (*.shp)") self.Pdlg.lineEdit_2.setText(filename) def select_Poutput_name(self): filename = PyQt4.QtGui.QFileDialog.getSaveFileName( self.Pdlg, "Select output file ", "", "Virtual Raster Tile (*.vrt)") if filename.split('.')[-1] != 'vrt': filename = filename + '.vrt' self.Pdlg.lineEdit_3.setText(filename) def updateCForm(self): #get user edits self.raster = self.Cdlg.lineEdit.text() self.in_train = self.Cdlg.lineEdit_2.text() self.out_fname = self.Cdlg.lineEdit_3.text() self.label = self.Cdlg.lineEdit_5.text() self.sieve = self.Cdlg.lineEdit_6.text() self.external = False if self.Cdlg.checkBox.isChecked(): self.external = True #in case an external SVM is provided the testing is optional if self.external: if (self.raster == '' or self.out_fname == '' or self.sieve == ''): return False else: return True else: if (self.raster == '' or self.in_train == '' or self.out_fname == '' or self.label == '' or self.sieve == ''): return False else: return True def select_input_pan(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName( self.Cdlg, "Select input layerstacked virtual raster tile", "", "*.vrt") self.Cdlg.lineEdit.setText(filename) def select_training(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName( self.Cdlg, "Select training vector ", "", "*.shp") self.Cdlg.lineEdit_2.setText(filename) def select_CSVM(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName( self.Cdlg, "Select Support Vector Model file", "", "") self.Cdlg.lineEdit_4.setText(filename) def select_Coutput_name(self): filename = PyQt4.QtGui.QFileDialog.getSaveFileName( self.Cdlg, "Select output file ", "", "GeoTIFF (*.tif)") suffix = filename.split('.')[-1] if suffix not in ['tif', 'TIF']: filename = filename + '.tif' self.Cdlg.lineEdit_3.setText(filename) def switch_external_SVM(self): ''' Activates the external SVM dialog parts ''' if self.Cdlg.checkBox.isChecked(): self.Cdlg.lineEdit_4.setEnabled(True) self.Cdlg.pushButton_4.setEnabled(True) self.Cdlg.label_5.setEnabled(True) self.external = True else: self.Cdlg.lineEdit_4.setDisabled(True) self.Cdlg.pushButton_4.setDisabled(True) self.Cdlg.label_5.setDisabled(True) self.external = False def switch_sieve(self): if self.Cdlg.checkBox_3.isChecked(): self.Cdlg.lineEdit_6.setEnabled(True) else: self.Cdlg.lineEdit_6.setEnabled(False) def show_help(self): import webbrowser import sys import os source = sys.modules['SatEx'].__file__ path = os.path.dirname(source) helpfile = path + '/help/index.html' url = "file://" + helpfile webbrowser.open(url, new=2) def errorMsg(self, msg): self.iface.messageBar().pushMessage('Error: ' + msg, self.iface.messageBar().CRITICAL) def run_preprocessing(self): """Run method that performs all the real work""" self.Pdlg.setModal(False) self.Pdlg.show() #Dialog event loop result = self.Pdlg.exec_() if result: self.processed = 0 self.percentage = 0 #TODO:fix self.ntasks = 3 #Get user edits and check if not empty valid_input = self.updatePForm() import utils import traceback import qgis.core import ogr try: import otbApplication except: self.errorMsg('Plugin requires installation of OrfeoToolbox') #find the number of different L8 scenes #by reading all TIFs splitting off '_Bxy.TIF' and getting unique strings e = 'Unspecified error' #instantiate utilities function ut = utils.utils() try: try: #check if input is not empty string 1 / valid_input except ZeroDivisionError: e = str('Please fill all required input fields') raise Exception try: #delete any old tmp files that might be in the directory from a killed task old = ut.delete_tmps(self.ls_path) if old > 0: qgis.core.QgsMessageLog.logMessage( 'Old *satexTMP* files were present. They were deleted.' ) except: e = str( 'Could not delete old *satexTMP* files. Function utils.delete_tmps.' ) raise Exception try: pattern = '*.TIF' scenes = set([ '_'.join(s.split('_')[:1]) for s in ut.findFiles(self.ls_path, pattern) ]) if len(scenes) == 0: pattern = '*.tif' scenes = set([ '_'.join(s.split('_')[:1]) for s in ut.findFiles(self.ls_path, pattern) ]) 1 / len(scenes) except ZeroDivisionError: e = str('Found no scene in {}'.format(self.ls_path)) raise Exception else: qgis.core.QgsMessageLog.logMessage( str('Found {} scene(s) in {}'.format( len(scenes), self.ls_path))) #check shapefile roi try: driver = ogr.GetDriverByName('ESRI Shapefile') dataSource = driver.Open(self.roi, 0) layer = dataSource.GetLayer() qgis.core.QgsMessageLog.logMessage( str('Using {} as ROI'.format(self.roi))) except AttributeError: e = str('Could not open {}'.format(self.roi)) raise Exception #loop through all scenes out_files = [] for scene in scenes: #find all bands for scene exclude quality band BQA and B8 try: bands = [ b for b in ut.findFiles(self.ls_path, scene + '*_B' + pattern) if '_BQA' not in b ] bands = [b for b in bands if '_B8' not in b] #in case of multiple scenes (and not first scene is processed) check if nr of bands are equal try: #only if more than one scene and at least second scene nr_bands except: if len(bands) == 0: e = str('Found no bands for scene {}.'.format( scene)) raise Exception else: #store number of bands for potential additonal scenes nr_bands = len(bands) qgis.core.QgsMessageLog.logMessage( str('Found {} bands (if present, excluding B8 and BQA) for scene {} ' .format(nr_bands, scene))) else: if len(bands) != nr_bands: e = str( 'Found {} instead of {} bands (excluding B8 and BQA) for scene {}. If multiple scenes are provided in the input directory, ensure they have equal bands!' .format(len(bands), nr_bands, scene)) else: qgis.core.QgsMessageLog.logMessage( str('Found {} bands (if present, excluding B8 and BQA) for scene {} ' .format(len(bands), scene))) except: raise Exception #Check if ROI and scene overlap try: error, overlap = ut.vector_raster_overlap( self.roi, self.ls_path + bands[0]) except: e = str( 'Unspecified error while trying to execute utils.vector_raster_overlap function with {} and {}' .format(self.roi, bands[0])) raise Exception if error != 'SUCCESS': e = error raise Exception else: try: 1 / overlap except ZeroDivisionError: e = str( 'The provided ROI {} does not overlap with scene {}' .format(self.roi, scene)) raise Exception #use gdalwarp to cut bands to roi try: #go through bands for band in bands: cmd = [ 'gdalwarp', '-overwrite', '-q', '-cutline', self.roi, '-crop_to_cutline', self.ls_path + band, self.ls_path + band[:-4] + '_satexTMP_ROI' + pattern[1:] ] subprocess.check_call(cmd, startupinfo=self.startupinfo) qgis.core.QgsMessageLog.logMessage( str('Cropped band {} to ROI'.format(band))) except subprocess.CalledProcessError: e = str( 'Could not execute gdalwarp cmd: {}.\nError is:{}'. format(' '.join(cmd), error)) raise Exception # Layerstack try: #respect order B1,B2,B3,B4,B5,B6,B7,B9,B10,B11 in_files = [ str(self.ls_path + b[:-4] + '_satexTMP_ROI' + pattern[1:]) for b in bands ] in_files.sort() if nr_bands == 10: # For Landsat 8 B10,B11 considered smaller --> resort in_files = in_files[2:] + in_files[0:2] out_file = str( os.path.dirname(self.out_fname) + '/' + scene + '_satex_mul' + pattern[1:]) #call otb wrapper error = ut.otb_concatenate(in_files, out_file) if error != 'success': raise ZeroDivisionError #append file to list out_files.append(out_file) qgis.core.QgsMessageLog.logMessage( str('Concatenated bands for scene {}'.format( scene))) except ZeroDivisionError: e = str( 'Could not execute OTB ConcatenateImages for scene: {}\nin_files: {}\nout_file: {}. \nError is: {}' .format(scene, in_files, out_file, error)) raise Exception # after all scenes were processed combine them to a virtual raster tile try: cmd = [ "gdalbuildvrt", "-q", "-srcnodata", "0", "-overwrite", self.out_fname ] for f in out_files: cmd.append(f) subprocess.check_call(cmd, startupinfo=self.startupinfo) qgis.core.QgsMessageLog.logMessage( str('Merged {} different scenes to {}'.format( len(out_files), self.out_fname))) except subprocess.CalledProcessError: e = str('Could not execute gdalbuildvrt cmd: {}'.format( ' '.join(cmd))) raise Exception #add to map canvas if checked if self.Pdlg.checkBox.isChecked(): try: self.iface.addRasterLayer(str(self.out_fname), "SatEx_vrt") except: e = str('Could not add {} to the layer canvas'.format( self.out_fname)) raise Exception except: self.errorMsg(e) qgis.core.QgsMessageLog.logMessage( str('Exception: {}'.format(e))) qgis.core.QgsMessageLog.logMessage( str('Exception occurred...deleting temporary files')) ut.delete_tmps(self.ls_path) else: qgis.core.QgsMessageLog.logMessage( str('Processing sucessfully completed')) qgis.core.QgsMessageLog.logMessage( str('Deleting temporary files')) self.iface.messageBar().pushMessage( 'Processing successfully completed, see log for details', self.iface.messageBar().SUCCESS, duration=3) ut.delete_tmps(self.ls_path) def run_classification(self): """Run method that performs all the real work""" self.Cdlg.setModal(False) self.Cdlg.show() #Dialog event loop result = self.Cdlg.exec_() if result: import utils import traceback import qgis.core import ogr #import subprocess #Get user edits valid_input = self.updateCForm() #TODO:fix self.classification_type = 'libsvm' self.svmModel = self.in_train[:-4] + '_svmModel.svm' self.ConfMatrix = self.in_train[:-4] + '_CM.csv' try: import otbApplication except: self.errorMsg('Plugin requires installation of OrfeoToolbox') e = 'Unspecified error' try: #instantiate utilities functions ut = utils.utils() try: #check if input is not empty string 1 / valid_input except ZeroDivisionError: e = str('Please fill all required input fields') raise Exception #check if training fields overlap with raster if not self.external: try: error, overlap = ut.vector_raster_overlap( self.in_train, self.raster) except: e = str( 'Unspecified error while trying to execute utils.vector_raster_overlap function' ) raise Exception if error != 'SUCCESS': e = error raise Exception else: try: 1 / overlap except ZeroDivisionError: e = str( 'At least one feature in {} does not overlap with {}' .format(self.in_train, self.raster)) raise Exception #generate image statistics try: self.stats = str(self.raster[:-4] + '_stats.xml') error = ut.otb_image_statistics(str(self.raster), str(self.stats)) if error != 'success': raise ZeroDivisionError qgis.core.QgsMessageLog.logMessage( str('Calculated image statistics {} for {}'.format( self.stats, self.raster))) except ZeroDivisionError: e = str( 'Could not execute OTB Image Statistics on: {}. \nError is:{}' .format(self.raster, error)) raise Exception #differntiate two cases case 1) external SVM provided an case 2) on the fly SVM training if self.external: if self.in_train != '': #use full training set for testing self.test = self.in_train #get SVM filename self.svmModel = self.Cdlg.lineEdit_4.text() else: #split training dataset in 80% train 20% testing [self.error, self.test, self.train] = ut.split_train(self.in_train, self.label, self.startupinfo) if self.error != 'success': e = self.error raise Exception else: qgis.core.QgsMessageLog.logMessage( str('Splitted ground truth data set in {} (~80%) and {} (~20%)' .format(self.train, self.test))) #train classifier #on the fly (wrong) confusion matrix gets overwritten later try: error = ut.otb_train_classifier( self.raster, self.train, self.stats, self.classification_type, self.label, self.svmModel, self.ConfMatrix) if error != 'success': raise ZeroDivisionError qgis.core.QgsMessageLog.logMessage( str('Trained image classifier using {} and {}'. format(self.raster, self.train))) except ZeroDivisionError: e = 'Could not execute OTB TrainClassifiers with {} {} {} {} {} {} {}. \nError is:{}'.format( self.raster, self.train, self.stats, self.classification_type, self.label, self.svmModel, self.ConfMatrix, error) raise Exception #classify image try: error = ut.otb_classification(self.raster, self.stats, self.svmModel, self.out_fname) if error != 'success': raise ZeroDivisionError qgis.core.QgsMessageLog.logMessage( str('Image {} classified as {}'.format( self.raster, self.out_fname))) except ZeroDivisionError: e = 'Could not execute OTB Classifier with {}, {}, {}, {}. \n Error is: {}'.format( self.raster, self.stats, self.svmModel, self.out_fname, error) raise Exception #confusion matrix try: #testing is optional in case of externally provided SVM if self.in_train != '': error = ut.otb_confusion_matrix( self.out_fname, self.ConfMatrix, self.test, self.label) if error != 'success': raise ZeroDivisionError qgis.core.QgsMessageLog.logMessage( str('Confusion matrix calculated on classified image {} with test set {} saved as {}' .format(self.out_fname, self.test, self.ConfMatrix))) except ZeroDivisionError: e = 'Could not execute OTB Confusion Matrix with {}, {}, {}, {}. \nError is: {}'.format( self.out_fname, self.ConfMatrix, self.test, self.label) raise Exception #if sieving is asked perform sieving if self.Cdlg.checkBox_3.isChecked(): try: if os.name == 'nt': cmd = [ 'gdal_sieve.bat', '-q', '-st', str(self.sieve), '-8', str(self.out_fname) ] else: cmd = [ 'gdal_sieve.py', '-q', '-st', str(self.sieve), '-8', str(self.out_fname) ] subprocess.check_call(cmd, startupinfo=self.startupinfo) except subprocess.CalledProcessError: e = 'Could not execute {}'.format(cmd) raise Exception #add to map canvas if checked if self.Cdlg.checkBox_2.isChecked(): try: self.iface.addRasterLayer(str(self.out_fname), "SatEx_classified_scene") except: e = str('Could not add {} to the layer canvas'.format( self.out_fname)) raise Exception except: self.errorMsg(e) qgis.core.QgsMessageLog.logMessage(e) else: qgis.core.QgsMessageLog.logMessage(str('Processing completed')) self.iface.messageBar().pushMessage( 'Processing successfully completed, see log for details', self.iface.messageBar().SUCCESS, duration=3)
class SatEx: """QGIS Plugin Implementation.""" def __init__(self, iface): """Constructor. :param iface: An interface instance that will be passed to this class which provides the hook by which you can manipulate the QGIS application at run time. :type iface: QgsInterface """ # Save reference to the QGIS interface self.iface = iface # initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # initialize locale locale = PyQt4.QtCore.QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'SatEx_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = PyQt4.QtCore.QTranslator() self.translator.load(locale_path) if PyQt4.QtCore.qVersion() > '4.3.3': PyQt4.QtCore.QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr(u'&GFZ SatEx') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'SatEx') self.toolbar.setObjectName(u'SatEx') #create dialogs and keep reference self.Pdlg = PreprocessingDialog() self.Cdlg = ClassificationDialog() #gui interactions #Preprocessing self.Pdlg.lineEdit.clear() self.Pdlg.pushButton.clicked.connect(self.select_input_raster) self.Pdlg.lineEdit_2.clear() self.Pdlg.pushButton_2.clicked.connect(self.select_roi) self.Pdlg.lineEdit_3.clear() self.Pdlg.pushButton_3.clicked.connect(self.select_Poutput_name) self.Pdlg.toolButton.clicked.connect(self.show_help) #Classification self.Cdlg.lineEdit.clear() self.Cdlg.pushButton.clicked.connect(self.select_input_pan) self.Cdlg.lineEdit_2.clear() self.Cdlg.pushButton_2.clicked.connect(self.select_training) self.Cdlg.lineEdit_3.clear() self.Cdlg.pushButton_3.clicked.connect(self.select_Coutput_name) self.Cdlg.checkBox.clicked.connect(self.switch_external_SVM) self.Cdlg.checkBox_3.clicked.connect(self.switch_sieve) self.Cdlg.pushButton_4.clicked.connect(self.select_CSVM) self.Cdlg.lineEdit_6.setText('4') self.Cdlg.toolButton.clicked.connect(self.show_help) #setup subrpocess differently for windows self.startupinfo = None if os.name == 'nt': self.startupinfo = subprocess.STARTUPINFO() self.startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW #Prefent bug in directory search (added / on linux results in searching the root directory) self.Pdlg.lineEdit.setText(' ') #TODO:defaults for development #self.Pdlg.lineEdit.setText('/home/mhaas/PhD/Routines/rst/plugin/data/LC81680542015357LGN00/') #self.Pdlg.lineEdit_2.setText('/home/mhaas/PhD/Routines/rst/plugin/data/adisababa.shp') #self.Pdlg.lineEdit_3.setText('/home/mhaas/test/test.vrt') #TODO:defaults for development #self.Cdlg.lineEdit.setText('Path to vrt') #self.Cdlg.lineEdit_2.setText('Path to training shapefile') #self.Cdlg.lineEdit_3.setText('Path to output-tif') #self.Cdlg.lineEdit_5.setText('Training class label') # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return PyQt4.QtCore.QCoreApplication.translate('SatEx', message) def add_action( self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = PyQt4.QtGui.QIcon(icon_path) action = PyQt4.QtGui.QAction(icon, text, parent) action.triggered.connect(callback) action.setEnabled(enabled_flag) if status_tip is not None: action.setStatusTip(status_tip) if whats_this is not None: action.setWhatsThis(whats_this) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToMenu( self.menu, action) self.actions.append(action) return action def initGui(self): """Create the menu entries and toolbar icons inside the QGIS GUI.""" icon_path = ':/plugins/SatEx/icon.png' self.add_action( icon_path, text=self.tr(u'Preprocessing'), callback=self.run_preprocessing, parent=self.iface.mainWindow()) icon_path = ':/plugins/SatEx/icon.png' self.add_action( icon_path, text=self.tr(u'Classification'), callback=self.run_classification, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginMenu( self.tr(u'&GFZ SatEx'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def updatePForm(self): #get user edits self.ls_path = self.Pdlg.lineEdit.text()+'/' self.roi = self.Pdlg.lineEdit_2.text() self.out_fname = self.Pdlg.lineEdit_3.text() if (self.ls_path ==' /' or self.roi == '' or self.out_fname == ''): return False else: return True def select_input_raster(self): dirname = PyQt4.QtGui.QFileDialog.getExistingDirectory(self.Pdlg, "Select input directory ","",PyQt4.QtGui.QFileDialog.ShowDirsOnly) self.Pdlg.lineEdit.setText(dirname) def select_roi(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName(self.Pdlg, "Select region of interest ","","Shapefile (*.shp)") self.Pdlg.lineEdit_2.setText(filename) def select_Poutput_name(self): filename = PyQt4.QtGui.QFileDialog.getSaveFileName(self.Pdlg, "Select output file ","","Virtual Raster Tile (*.vrt)") if filename.split('.')[-1]!='vrt': filename = filename+'.vrt' self.Pdlg.lineEdit_3.setText(filename) def updateCForm(self): #get user edits self.raster = self.Cdlg.lineEdit.text() self.in_train = self.Cdlg.lineEdit_2.text() self.out_fname = self.Cdlg.lineEdit_3.text() self.label = self.Cdlg.lineEdit_5.text() self.sieve = self.Cdlg.lineEdit_6.text() self.external = False if self.Cdlg.checkBox.isChecked(): self.external=True #in case an external SVM is provided the testing is optional if self.external: if (self.raster =='' or self.out_fname == '' or self.sieve == ''): return False else: return True else: if (self.raster =='' or self.in_train == '' or self.out_fname == '' or self.label == '' or self.sieve == ''): return False else: return True def select_input_pan(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName(self.Cdlg, "Select input layerstacked virtual raster tile","","*.vrt") self.Cdlg.lineEdit.setText(filename) def select_training(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName(self.Cdlg, "Select training vector ","","*.shp") self.Cdlg.lineEdit_2.setText(filename) def select_CSVM(self): filename = PyQt4.QtGui.QFileDialog.getOpenFileName(self.Cdlg, "Select Support Vector Model file","","") self.Cdlg.lineEdit_4.setText(filename) def select_Coutput_name(self): filename = PyQt4.QtGui.QFileDialog.getSaveFileName(self.Cdlg, "Select output file ","","GeoTIFF (*.tif)") if filename.split('.')[-1]!='tif': filename = filename+'.tif' self.Cdlg.lineEdit_3.setText(filename) #def calculate_progress(self): # self.processed = self.processed + 1 # percentage_new = (self.processed * 100) / self.ntasks # if percentage_new > self.percentage: # self.percentage = percentage_new #def updateTextbox(self,msg): # self.textBrowser.append(msg) def switch_external_SVM(self): ''' Activates the external SVM dialog parts ''' if self.Cdlg.checkBox.isChecked(): self.Cdlg.lineEdit_4.setEnabled(True) self.Cdlg.pushButton_4.setEnabled(True) self.Cdlg.label_5.setEnabled(True) #self.Cdlg.lineEdit_5.setDisabled(True) #self.Cdlg.label_6.setDisabled(True) self.external=True else: self.Cdlg.lineEdit_4.setDisabled(True) self.Cdlg.pushButton_4.setDisabled(True) self.Cdlg.label_5.setDisabled(True) #self.Cdlg.lineEdit_5.setEnabled(True) #self.Cdlg.label_6.setEnabled(True) self.external=False def switch_sieve(self): if self.Cdlg.checkBox_3.isChecked(): self.Cdlg.lineEdit_6.setEnabled(True) else: self.Cdlg.lineEdit_6.setEnabled(False) def show_help(self): import webbrowser import inspect source = inspect.currentframe().f_back.f_code.co_filename #sys.modules['SatEx'].__file__ path = os.path.dirname(source) helpfile = os.path.join(path, 'index-en.html') url = "file://" + helpfile webbrowser.open(url,new=2) #qgis.utils.showPluginHelp(packageName='SatEx') def errorMsg(self,msg): self.iface.messageBar().pushMessage('Error: '+ msg,self.iface.messageBar().CRITICAL) def run_preprocessing(self): """Run method that performs all the real work""" self.Pdlg.setModal(False) self.Pdlg.show() #Dialog event loop result = self.Pdlg.exec_() if result: self.processed = 0 self.percentage = 0 #TODO:fix self.ntasks = 3 #Get user edits and check if not empty valid_input=self.updatePForm() #self.Pdlg.startWorker(self.iface, self.ls_path, self.roi, self.out_fname) import utils import traceback import qgis.core import ogr #import subprocess try: import otbApplication except: self.errorMsg('Plugin requires installation of OrfeoToolbox') #find the number of different L8 scenes #by reading all TIFs splitting off '_Bxy.TIF' and getting unique strings e = 'unspecified error' #instantiate utilities function ut = utils.utils() try: try: #check if input is not empty string 1/valid_input except Exception as e: e = str('Please fill all required input fields') raise Exception try: #delete any old tmp files that might be in the directory from a killed task old=ut.delete_tmps(self.ls_path) if old > 0: qgis.core.QgsMessageLog.logMessage('Old *satexTMP* files were present. They were deleted.') scenes = set(['_'.join(s.split('_')[:1]) for s in ut.findFiles(self.ls_path,'*.TIF')]) #adjust number of tasks self.ntasks = self.ntasks*len(scenes) qgis.core.QgsMessageLog.logMessage(str('Found {} Landsat 8 scene(s) in {}'.format(len(scenes),self.ls_path))) except Exception as e: e = str('Found no Landsat 8 scene in {}'.format(self.ls_path)) raise Exception #check shapefile roi try: driver = ogr.GetDriverByName('ESRI Shapefile') dataSource = driver.Open(self.roi,0) layer = dataSource.GetLayer() qgis.core.QgsMessageLog.logMessage(str('Using {} as ROI'.format(self.roi))) except Exception as e: e = str('Could not open {}'.format(self.roi)) raise Exception #loop through all scenes for scene in scenes: #find all bands for scene exclude quality band BQA and B8 try: bands = [b for b in ut.findFiles(self.ls_path,scene+'*_B*.TIF') if '_BQA' not in b] bands = [b for b in bands if '_B8' not in b] #check if there are 10 bands #if len(bands)!=11: if len(bands)!=10: e = str('Found {} instead of 10 bands (excluding B8 and BQA) for scene {}'.format(len(bands),scene)) raise Exception else: #self.status.emit('Found all 11 bands for scene {}'.format(scene)) qgis.core.QgsMessageLog.logMessage(str('Found all 10 bands (excluding B8 and BQA) for scene {} '.format(scene))) except Exception as e: e = str('Could not find all 10 bands (excluding B8 and BQA) for scene {}'.format(scene)) raise Exception #Check if ROI and scene overlap try: 1/ut.vector_raster_overlap(self.roi,self.ls_path+scene+'_B1.TIF') except: e = str('The provided ROI {} does not overlap with scene {}'.format(self.roi,scene)) raise Exception #use gdalwarp to cut bands to roi try: #go through bands for band in bands: #self.status.emit('Cropping band {} to ROI'.format(band)) qgis.core.QgsMessageLog.logMessage(str('Cropping band {} to ROI'.format(band))) cmd = ['gdalwarp','-overwrite','-q','-cutline',self.roi,'-crop_to_cutline',self.ls_path+band,self.ls_path+band[:-4]+'_satexTMP_ROI.TIF'] subprocess.check_call(cmd,startupinfo=self.startupinfo) # self.calculate_progress() except Exception as e: e = str('Could not execute gdalwarp cmd: {}'.format(' '.join(cmd))) raise Exception # Layerstack try: #respect order B1,B2,B3,B4,B5,B6,B7,B9,B10,B11 #in_files = [str(self.ls_path+b[:-4]+'_satexTMP_ROI.TIF') for b in bands if '_B8' not in b] in_files = [str(self.ls_path+b[:-4]+'_satexTMP_ROI.TIF') for b in bands] in_files.sort() #B10,B11 considered smaller --> resort in_files = in_files[2:] + in_files[0:2] out_file = str(self.ls_path+scene+'_satex_mul.TIF') #call otb wrapper #self.status.emit('Concatenating bands for pansharpening scene {}'.format(scene)) qgis.core.QgsMessageLog.logMessage(str('Concatenate bands for pansharpening scene {}'.format(scene))) ut.otb_concatenate(in_files,out_file) #self.calculate_progress() except Exception as e: e = str('Could not execute OTB ConcatenateImages for scene: {}\nin_files: {}\nout_file: {}'.format(scene,in_files,out_file)) raise Exception # after all scenes were processed combine them to a virtual raster tile try: cmd = ["gdalbuildvrt","-q","-srcnodata","0","-overwrite",self.out_fname] files = [f for f in ut.findFiles(self.ls_path,'*satex_mul.TIF')] for f in files: cmd.append(str(self.ls_path+f)) subprocess.check_call(cmd,startupinfo=self.startupinfo) qgis.core.QgsMessageLog.logMessage(str('Merged {} different L8 scenes to {}'.format(len(files),self.out_fname))) #self.calculate_progress() except: e = str('Could not execute gdalbuildvrt cmd: {}'.format(' '.join(cmd))) raise Exception #add to map canvas if checked if self.Pdlg.checkBox.isChecked(): self.iface.addRasterLayer(str(self.out_fname), "SatEx_vrt") except: self.errorMsg(e) qgis.core.QgsMessageLog.logMessage(str('Exception: {}'.format(e))) qgis.core.QgsMessageLog.logMessage(str('Exception: Deleting temporary files')) ut.delete_tmps(self.ls_path) else: qgis.core.QgsMessageLog.logMessage(str('Processing sucessfully completed')) qgis.core.QgsMessageLog.logMessage(str('Deleting temporary files')) self.iface.messageBar().pushMessage('Processing successfully completed, see log for details',self.iface.messageBar().SUCCESS,duration=3) ut.delete_tmps(self.ls_path) def run_classification(self): """Run method that performs all the real work""" self.Cdlg.setModal(False) self.Cdlg.show() #Dialog event loop result = self.Cdlg.exec_() if result: import utils import traceback import qgis.core import ogr #import subprocess self.processed = 0 self.percentage = 0 #TODO:fix self.ntasks = 3 #Get user edits valid_input=self.updateCForm() #TODO:fix self.classification_type='libsvm' self.svmModel = self.in_train[:-4]+'_svmModel.svm' self.ConfMatrix = self.in_train[:-4]+'_CM.csv' try: import otbApplication except: self.errorMsg('Plugin requires installation of OrfeoToolbox') e = 'unspecified error' try: #instantiate utilities functions ut = utils.utils() try: #check if input is not empty string 1/valid_input except Exception as e: e = str('Please fill all required input fields') raise Exception #check if training fields overlap with raster if not self.external: try: 1/ut.vector_raster_overlap(self.in_train,self.raster) except: e = str('At least one feature in {} does not overlap with {}'.format(self.in_train,self.raster)) raise Exception #generate image statistics try: self.stats = str(self.raster[:-4]+'_stats.xml') ut.otb_image_statistics(str(self.raster),str(self.stats)) qgis.core.QgsMessageLog.logMessage(str('Calculated image statistics {} for {}'.format(self.stats,self.raster))) #self.calculate_progress() except: e = str('Could not execute OTB Image Statistics on: {}'.format(self.raster)) raise Exception #differntiate two cases case 1) external SVM provided an case 2) on the fly SVM training if self.external: if self.in_train!='': #use full training set for testing self.test = self.in_train #get SVM filename self.svmModel = self.Cdlg.lineEdit_4.text() else: #split training dataset in 80% train 20% testing [self.error,self.test,self.train] = ut.split_train(self.in_train,self.label,self.startupinfo) if self.error != 'success': e=self.error raise Exception else: qgis.core.QgsMessageLog.logMessage(str('Splitted ground truth data set in {} (~80%) and {} (~20%)'.format(self.train,self.test))) #train classifier #on the fly (wrong) confusion matrix gets overwritten later try: ut.otb_train_classifier(self.raster, self.train, self.stats, self.classification_type, self.label, self.svmModel, self.ConfMatrix) qgis.core.QgsMessageLog.logMessage(str('Trained image classifier using {} and {}'.format(self.raster,self.train))) except Exception as e: e = 'Could not execute OTB TrainClassifiers with {} {} {} {} {} {} {}'.format(self.raster, self.train, self.stats, self.classification_type, self.label, self.svmModel, self.ConfMatrix) raise Exception #classify image try: ut.otb_classification(self.raster, self.stats, self.svmModel, self.out_fname) qgis.core.QgsMessageLog.logMessage(str('Image {} classified as {}'.format(self.raster,self.out_fname))) except Exception as e: e = 'Could not execute OTB Classifier with {}, {}, {}, {}'.format(self.raster, self.stats, self.svmModel, self.out_fname) raise Exception #confusion matrix try: #testing is optional in case of externally provided SVM if self.in_train!='': ut.otb_confusion_matrix(self.out_fname,self.ConfMatrix,self.test,self.label) qgis.core.QgsMessageLog.logMessage(str('Confusion matrix calcualted on classified image {} with test set {} saved as {}'.format(self.out_fname,self.test,self.ConfMatrix))) except Exception as e: e = 'Could not execute OTB Confusion Matrix with {}, {}, {}, {}'.format(self.out_fname, self.ConfMatrix, self.test, self.label) raise Exception #if sieving is asked perform sieving if self.Cdlg.checkBox_3.isChecked(): try: if os.name=='nt': cmd = ['gdal_sieve.bat','-q','-st',str(self.sieve),'-8',str(self.out_fname)] else: cmd = ['gdal_sieve.py','-q','-st',str(self.sieve),'-8',str(self.out_fname)] subprocess.check_call(cmd,startupinfo=self.startupinfo) except Exception as e: e = 'Could not execute {}'.format(cmd) raise Exception #add to map canvas if checked if self.Cdlg.checkBox_2.isChecked(): self.iface.addRasterLayer(str(self.out_fname), "SatEx_classified_scene") except: self.errorMsg(e) qgis.core.QgsMessageLog.logMessage(e) qgis.core.QgsMessageLog.logMessage(str('Exception: Deleting temporary files')) #ut.delete_tmps(self.ls_path) else: qgis.core.QgsMessageLog.logMessage(str('Processing completed')) qgis.core.QgsMessageLog.logMessage(str('Deleting temporary files')) self.iface.messageBar().pushMessage('Processing successfully completed, see log for details',self.iface.messageBar().SUCCESS,duration=3)