From 64dd05d563e3b1a1cb6a2182c506b492b07bc82d Mon Sep 17 00:00:00 2001 From: speillet Date: Thu, 23 Apr 2020 20:04:29 +0200 Subject: [PATCH 01/11] ENH: add function to load model --- config.ini | 19 +++++++++++ gui/NbLabelDialog.py | 23 +++++++++++++ qdeeplandia.py | 80 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 113 insertions(+), 9 deletions(-) create mode 100644 config.ini create mode 100644 gui/NbLabelDialog.py diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..bad56fd --- /dev/null +++ b/config.ini @@ -0,0 +1,19 @@ +[status] +status = dev + +[running] +processes = 1 + +[symlink] +predicted = /path/to/predicted/images/ +shapes = /path/to/shape/dataset/ +mapillary = /path/to/mapillary/dataset/ +mapillary_agg = /path/to/agregated/mapillary/dataset/ +aerial = /path/to/aerial/dataset/ +tanzania = /home/speillet/OpenSourceProject/deeposlandia/tests/data/tanzania + +[folder] +project_folder = /home/speillet/OpenSourceProject/deeposlandia/deeposlandia/projet + +[key] +secret_key = enter-your-app-key diff --git a/gui/NbLabelDialog.py b/gui/NbLabelDialog.py new file mode 100644 index 0000000..5633192 --- /dev/null +++ b/gui/NbLabelDialog.py @@ -0,0 +1,23 @@ +from qgis.PyQt.QtWidgets import QDialog, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QSpinBox, QDialogButtonBox + +class NbLabelDialog(QDialog): + def __init__(self,parent): + super(NbLabelDialog, self).__init__() + + self.VL = QVBoxLayout(self) + self.HL = QHBoxLayout() + self.VL.addLayout(self.HL) + + self.label = QLabel(self.tr('Number of label : ')) + self.HL.addWidget(self.label) + + self.spinbox = QSpinBox() + self.HL.addWidget(self.spinbox) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + self.VL.addWidget(self.buttonBox) + + def param(self): + return self.spinbox.value() \ No newline at end of file diff --git a/qdeeplandia.py b/qdeeplandia.py index 9b06ad2..350e20e 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -17,31 +17,64 @@ import os +from qgis.core import Qgis + +from qgis.PyQt.QtCore import QSettings, QCoreApplication from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction +from qgis.PyQt.QtWidgets import QAction, QFileDialog, QWidget, QHBoxLayout, QVBoxLayout, QMessageBox + +os.environ['DEEPOSL_CONFIG']=os.path.join(os.path.dirname(__file__),'config.ini') +from .deeposlandia import postprocess +from .gui.NbLabelDialog import NbLabelDialog -class QDeeplandiaPlugin: +def tr(message): + """Get the translation for a string using Qt translation API. + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate('@default', message) + +class QDeeplandiaPlugin(QWidget): + """ Major class of QDeeplandia plugin """ def __init__(self, iface): + """Constructor + + :param iface: qgis interface + :type iface:QgisInterface + """ + super(QDeeplandiaPlugin, self).__init__() self.iface = iface + self.model = None + + locale = QSettings().value('locale/userLocale') or 'en_USA' + locale= locale[0:2] + locale_path = os.path.join( + os.path.dirname(__file__), + 'i18n', + 'thyrsis_{}.qm'.format(locale)) + + if os.path.exists(locale_path): + self.translator = QTranslator() + self.translator.load(locale_path, 'qdeeplandia') + QCoreApplication.installTranslator(self.translator) + print("TRANSLATION LOADED", locale_path) def initGui(self): # Select a trained model on the file system - load_model_msg = "Load a trained model" + load_model_msg = tr("Load a trained model") load_icon = QIcon(os.path.join(os.path.dirname(__file__), "img/load.svg")) self.model_loading = QAction(load_icon, load_model_msg, self.iface.mainWindow()) - self.model_loading.triggered.connect(self.load_trained_model) - self.iface.addPluginToMenu("QDeeplandia", self.model_loading) self.model_loading.triggered.connect(lambda: self.load_trained_model()) + self.iface.addPluginToMenu("QDeeplandia", self.model_loading) self.iface.addToolBarIcon(self.model_loading) # Run-an-inference process - run_inference_msg = "Run an inference" + run_inference_msg = tr("Run an inference") run_icon = QIcon(os.path.join(os.path.dirname(__file__), "img/run.svg")) self.inference = QAction(run_icon, run_inference_msg, self.iface.mainWindow()) - self.inference.triggered.connect(self.infer) - self.iface.addPluginToMenu("QDeeplandia", self.inference) self.inference.triggered.connect(lambda: self.infer()) + self.iface.addPluginToMenu("QDeeplandia", self.inference) self.iface.addToolBarIcon(self.inference) + self.inference.setEnabled(False) def unload(self): # Select a trained model on the file system @@ -53,8 +86,37 @@ def unload(self): self.iface.removeToolBarIcon(self.inference) self.inference.setParent(None) + def tr(message): + """Get the translation for a string using Qt translation API. + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate('@default', message) + def load_trained_model(self): - pass + fil, __ = QFileDialog.getOpenFileName(None, + tr("Load best-model-*.h5 file"), + os.path.abspath("."), + tr("h5 file (*.h5)")) + + nbLabelDlg = NbLabelDialog(self) + + if nbLabelDlg.exec(): + nb_labels = nbLabelDlg.param() + else : + return + + datapath = os.path.abspath(os.path.join(os.path.dirname(fil), '..', '..', '..', '..')) + dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(fil), '..', '..', '..'))) + image_size = os.path.splitext(os.path.basename(fil))[0].split('-')[-1] + print(datapath, dataset, image_size, nb_labels) + try : + self.model = postprocess.get_trained_model(datapath, dataset, int(image_size), int(nb_labels)) + except ValueError as e: + self.iface.messageBar().pushMessage(tr("Critical"), + str(e), level=Qgis.Critical) + + if self.model : + self.inference.setEnabled(True) def infer(self): pass From b3e87cd38a470304c968d9e8d6defea42cb16a38 Mon Sep 17 00:00:00 2001 From: speillet Date: Fri, 24 Apr 2020 17:10:36 +0200 Subject: [PATCH 02/11] begin processing utils --- gui/__init__.py | 0 metadata.txt | 1 + processing_provider/__init__.py | 0 processing_provider/inference.py | 211 +++++++++++++++++++++++++++++++ processing_provider/provider.py | 33 +++++ qdeeplandia.py | 36 +++++- 6 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 gui/__init__.py create mode 100644 processing_provider/__init__.py create mode 100644 processing_provider/inference.py create mode 100644 processing_provider/provider.py diff --git a/gui/__init__.py b/gui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/metadata.txt b/metadata.txt index 916aad4..f3d1562 100644 --- a/metadata.txt +++ b/metadata.txt @@ -6,3 +6,4 @@ qgisMinimumVersion=3.00 qgisMaximumVersion=3.99 author=Oslandia email=infos@oslandia.com +hasProcessingProvider=yes \ No newline at end of file diff --git a/processing_provider/__init__.py b/processing_provider/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/processing_provider/inference.py b/processing_provider/inference.py new file mode 100644 index 0000000..ee1b9e5 --- /dev/null +++ b/processing_provider/inference.py @@ -0,0 +1,211 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** +* * +* 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 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import (QgsProcessing, + QgsFeatureSink, + QgsProcessingException, + QgsProcessingAlgorithm, + QgsProcessingParameterFeatureSource, + QgsProcessingParameterFeatureSink, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterFile) +from qgis import processing + + +class InferenceQDeepLandiaProcessingAlgorithm(QgsProcessingAlgorithm): + """ + This is an example algorithm that takes a vector layer and + creates a new identical one. + + It is meant to be used as an example of how to create your own + algorithms and explain methods and variables used to do it. An + algorithm like this will be available in all elements, and there + is not need for additional work. + + All Processing algorithms should extend the QgsProcessingAlgorithm + class. + """ + + # Constants used to refer to parameters and outputs. They will be + # used when calling the algorithm from another algorithm, or when + # calling from the QGIS console. + + INPUT = 'INPUT' + MODEL = 'MODEL' + OUTPUT = 'OUTPUT' + + def tr(self, string): + """ + Returns a translatable string with the self.tr() function. + """ + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return InferenceQDeepLandiaProcessingAlgorithm() + + def name(self): + """ + Returns the algorithm name, used for identifying the algorithm. This + string should be fixed for the algorithm, and must not be localised. + The name should be unique within each provider. Names should contain + lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'InferenceQDeepLandia' + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr('Inference') + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr('QDeepLandia') + + def groupId(self): + """ + Returns the unique ID of the group this algorithm belongs to. This + string should be fixed for the algorithm, and must not be localised. + The group id should be unique within each provider. Group id should + contain lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'QDeepLandia' + + def shortHelpString(self): + """ + Returns a localised short helper string for the algorithm. This string + should provide a basic description about what the algorithm does and the + parameters and outputs associated with it.. + """ + return self.tr("Do inference according to the loaded model") + + def initAlgorithm(self, config=None): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + # We add the input vector features source. It can have any kind of + # geometry. + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT, + self.tr('Input layer') + ) + ) + + self.addParameter( + QgsProcessingParameterFile( + self.MODEL, + self.tr('Input model'), + extension="h5" + ) + ) + + # We add a feature sink in which to store our processed features (this + # usually takes the form of a newly created vector layer when the + # algorithm is run in QGIS). + self.addParameter( + QgsProcessingParameterFeatureSink( + self.OUTPUT, + self.tr('Output layer') + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + # Retrieve the feature source and sink. The 'dest_id' variable is used + # to uniquely identify the feature sink, and must be included in the + # dictionary returned by the processAlgorithm function. + # source = self.parameterAsSource( + # parameters, + # self.INPUT, + # context + # ) + + # # If source was not found, throw an exception to indicate that the algorithm + # # encountered a fatal error. The exception text can be any string, but in this + # # case we use the pre-built invalidSourceError method to return a standard + # # helper text for when a source cannot be evaluated + # if source is None: + # raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) + + # (sink, dest_id) = self.parameterAsSink( + # parameters, + # self.OUTPUT, + # context, + # source.fields(), + # source.wkbType(), + # source.sourceCrs() + # ) + + # # Send some information to the user + # feedback.pushInfo('CRS is {}'.format(source.sourceCrs().authid())) + + # # If sink was not created, throw an exception to indicate that the algorithm + # # encountered a fatal error. The exception text can be any string, but in this + # # case we use the pre-built invalidSinkError method to return a standard + # # helper text for when a sink cannot be evaluated + # if sink is None: + # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) + + # # Compute the number of steps to display within the progress bar and + # # get features from source + # total = 100.0 / source.featureCount() if source.featureCount() else 0 + # features = source.getFeatures() + + # for current, feature in enumerate(features): + # # Stop the algorithm if cancel button has been clicked + # if feedback.isCanceled(): + # break + + # # Add a feature in the sink + # sink.addFeature(feature, QgsFeatureSink.FastInsert) + + # # Update the progress bar + # feedback.setProgress(int(current * total)) + + # # To run another Processing algorithm as part of this algorithm, you can use + # # processing.run(...). Make sure you pass the current context and feedback + # # to processing.run to ensure that all temporary layer outputs are available + # # to the executed algorithm, and that the executed algorithm can send feedback + # # reports to the user (and correctly handle cancellation and progress reports!) + # if False: + # buffered_layer = processing.run("native:buffer", { + # 'INPUT': dest_id, + # 'DISTANCE': 1.5, + # 'SEGMENTS': 5, + # 'END_CAP_STYLE': 0, + # 'JOIN_STYLE': 0, + # 'MITER_LIMIT': 2, + # 'DISSOLVE': False, + # 'OUTPUT': 'memory:' + # }, context=context, feedback=feedback)['OUTPUT'] + + # Return the results of the algorithm. In this case our only result is + # the feature sink which contains the processed features, but some + # algorithms may return multiple feature sinks, calculated numeric + # statistics, etc. These should all be included in the returned + # dictionary, with keys matching the feature corresponding parameter + # or output names. + return #{self.OUTPUT: dest_id} diff --git a/processing_provider/provider.py b/processing_provider/provider.py new file mode 100644 index 0000000..80abe51 --- /dev/null +++ b/processing_provider/provider.py @@ -0,0 +1,33 @@ +from qgis.core import QgsProcessingProvider + +from .inference import InferenceQDeepLandiaProcessingAlgorithm + + +class QDeepLandiaProvider(QgsProcessingProvider): + + def loadAlgorithms(self, *args, **kwargs): + self.addAlgorithm(InferenceQDeepLandiaProcessingAlgorithm()) + # add additional algorithms here + # self.addAlgorithm(MyOtherAlgorithm()) + + def id(self, *args, **kwargs): + """The ID of your plugin, used for identifying the provider. + + This string should be a unique, short, character only string, + eg "qgis" or "gdal". This string should not be localised. + """ + return 'QDeepLandia' + + def name(self, *args, **kwargs): + """The human friendly name of your plugin in Processing. + + This string should be as short as possible (e.g. "Lastools", not + "Lastools version 1.0.1 64-bit") and localised. + """ + return self.tr('QDeepLandia') + + def icon(self): + """Should return a QIcon which is used for your provider inside + the Processing toolbox. + """ + return QgsProcessingProvider.icon(self) \ No newline at end of file diff --git a/qdeeplandia.py b/qdeeplandia.py index 350e20e..0917a43 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -17,14 +17,15 @@ import os -from qgis.core import Qgis +from qgis.core import Qgis, QgsRasterDataProvider, QgsApplication -from qgis.PyQt.QtCore import QSettings, QCoreApplication +from qgis.PyQt.QtCore import QSettings, QCoreApplication, pyqtSignal from qgis.PyQt.QtGui import QIcon from qgis.PyQt.QtWidgets import QAction, QFileDialog, QWidget, QHBoxLayout, QVBoxLayout, QMessageBox os.environ['DEEPOSL_CONFIG']=os.path.join(os.path.dirname(__file__),'config.ini') from .deeposlandia import postprocess +from .processing_provider.provider import QDeepLandiaProvider from .gui.NbLabelDialog import NbLabelDialog @@ -36,6 +37,9 @@ def tr(message): class QDeeplandiaPlugin(QWidget): """ Major class of QDeeplandia plugin """ + + isready = pyqtSignal() + def __init__(self, iface): """Constructor @@ -44,7 +48,10 @@ def __init__(self, iface): """ super(QDeeplandiaPlugin, self).__init__() self.iface = iface + self.mapCanvas = self.iface.mapCanvas() self.model = None + self.deepOprovider = None + self.layer = self.updateLayer() locale = QSettings().value('locale/userLocale') or 'en_USA' locale= locale[0:2] @@ -59,8 +66,12 @@ def __init__(self, iface): QCoreApplication.installTranslator(self.translator) print("TRANSLATION LOADED", locale_path) + self.mapCanvas.currentLayerChanged.connect(self.updateLayer) + self.isready.connect(self.ready) + def initGui(self): # Select a trained model on the file system + self.initProcessing() load_model_msg = tr("Load a trained model") load_icon = QIcon(os.path.join(os.path.dirname(__file__), "img/load.svg")) self.model_loading = QAction(load_icon, load_model_msg, self.iface.mainWindow()) @@ -76,6 +87,10 @@ def initGui(self): self.iface.addToolBarIcon(self.inference) self.inference.setEnabled(False) + def initProcessing(self): + self.deepOprovider = QDeepLandiaProvider() + QgsApplication.processingRegistry().addProvider(self.deepOprovider) + def unload(self): # Select a trained model on the file system self.iface.removePluginMenu("QDeeplandia", self.model_loading) @@ -85,6 +100,7 @@ def unload(self): self.iface.removePluginMenu("QDeeplandia", self.inference) self.iface.removeToolBarIcon(self.inference) self.inference.setParent(None) + QgsApplication.processingRegistry().removeProvider(self.deepOprovider) def tr(message): """Get the translation for a string using Qt translation API. @@ -116,7 +132,21 @@ def load_trained_model(self): str(e), level=Qgis.Critical) if self.model : - self.inference.setEnabled(True) + self.isready.emit() def infer(self): pass + + def updateLayer(self): + layer = self.mapCanvas.currentLayer() + if layer : + if isinstance(layer.dataProvider(), QgsRasterDataProvider): + self.layer = layer + else : + self.layer = None + self.isready.emit() + + def ready(self) : + print (self.layer, self.model) + if self.layer and self.model : + self.inference.setEnabled(True) \ No newline at end of file From d3d5d05ea5a01e439ffa9a1c31b0417353db23f6 Mon Sep 17 00:00:00 2001 From: speillet Date: Mon, 27 Apr 2020 18:15:45 +0200 Subject: [PATCH 03/11] First operational workflow --- processing_provider/datagen.py | 183 +++++++++++++++++++++++++++ processing_provider/inference.py | 208 ++++++++++++++++++------------- processing_provider/provider.py | 2 + qdeeplandia.py | 59 +++++++-- 4 files changed, 352 insertions(+), 100 deletions(-) create mode 100644 processing_provider/datagen.py diff --git a/processing_provider/datagen.py b/processing_provider/datagen.py new file mode 100644 index 0000000..808a9f8 --- /dev/null +++ b/processing_provider/datagen.py @@ -0,0 +1,183 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** +* * +* 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 2 of the License, or * +* (at your option) any later version. * +* * +*************************************************************************** +""" + +import os +import shutil +import subprocess + +from qgis.PyQt.QtCore import QCoreApplication +from qgis.core import (QgsProcessing, + QgsFeatureSink, + QgsProcessingException, + QgsProcessingAlgorithm, + QgsProcessingParameterFolderDestination, + QgsProcessingParameterRasterLayer, + QgsProcessingParameterFile, + QgsProcessingParameterString, + QgsProcessingParameterNumber) +from qgis import processing + + + +class DatagenQDeepLandiaProcessingAlgorithm(QgsProcessingAlgorithm): + """ + """ + + # Constants used to refer to parameters and outputs. They will be + # used when calling the algorithm from another algorithm, or when + # calling from the QGIS console. + + INPUT = 'INPUT' + DATASET = 'DATASET' + SHAPE = 'SHAPE' + OUTPUT = 'OUTPUT' + + def tr(self, string): + """ + Returns a translatable string with the self.tr() function. + """ + return QCoreApplication.translate('Processing', string) + + def createInstance(self): + return DatagenQDeepLandiaProcessingAlgorithm() + + def name(self): + """ + Returns the algorithm name, used for identifying the algorithm. This + string should be fixed for the algorithm, and must not be localised. + The name should be unique within each provider. Names should contain + lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'DatagenQDeepLandia' + + def displayName(self): + """ + Returns the translated algorithm name, which should be used for any + user-visible display of the algorithm name. + """ + return self.tr('Datageneration') + + def group(self): + """ + Returns the name of the group this algorithm belongs to. This string + should be localised. + """ + return self.tr('QDeepLandia') + + def groupId(self): + """ + Returns the unique ID of the group this algorithm belongs to. This + string should be fixed for the algorithm, and must not be localised. + The group id should be unique within each provider. Group id should + contain lowercase alphanumeric characters only and no spaces or other + formatting characters. + """ + return 'QDeepLandia' + + def shortHelpString(self): + """ + Returns a localised short helper string for the algorithm. This string + should provide a basic description about what the algorithm does and the + parameters and outputs associated with it.. + """ + return self.tr("Preprocess layer into predictable tiles") + + def initAlgorithm(self, config=None): + """ + Here we define the inputs and output of the algorithm, along + with some other properties. + """ + + # We add the input vector features source. It can have any kind of + # geometry. + self.addParameter( + QgsProcessingParameterRasterLayer( + self.INPUT, + self.tr('Input layer') + ) + ) + + # We add a feature sink in which to store our processed features (this + # usually takes the form of a newly created vector layer when the + # algorithm is run in QGIS). + self.addParameter( + QgsProcessingParameterString( + self.DATASET, + self.tr('Dataset name') + ) + ) + + self.addParameter( + QgsProcessingParameterNumber( + self.SHAPE, + self.tr('Number of pixel for the side of tiles'), + type = QgsProcessingParameterNumber.Integer, + defaultValue = 512, + minValue = 16 + ) + ) + + self.addParameter( + QgsProcessingParameterFolderDestination( + self.OUTPUT, + self.tr('Output folder') + ) + ) + + def processAlgorithm(self, parameters, context, feedback): + """ + Here is where the processing itself takes place. + """ + + raster_in = self.parameterAsRasterLayer( + parameters, + self.INPUT, + context + ) + + dest_path = self.parameterAsString( + parameters, + self.OUTPUT, + context + ) + + dataset = self.parameterAsString( + parameters, + self.DATASET, + context + ) + + shape = self.parameterAsInt( + parameters, + self.SHAPE, + context + ) + + # To do clip on canvas + path='' + for i in [dest_path, dataset, 'input','images']: + path = os.path.join(path, i) + if not os.path.exists(path): + os.mkdir(path) + + if os.path.exists(os.path.join( path, os.path.basename( raster_in.source()))): + os.remove(os.path.join( path, os.path.basename( raster_in.source()))) + shutil.copy( raster_in.source(), os.path.join( path, os.path.basename( raster_in.source()))) + + cmd = ['deepo', 'datagen', '-D', dataset, '-s', str(shape), '-P', dest_path, '-T', '1'] + subprocess.run(cmd) + + output_folder = os.path.join(dest_path, dataset, 'preprocessed', str(shape), 'testing', 'images') + + return {self.OUTPUT: output_folder} \ No newline at end of file diff --git a/processing_provider/inference.py b/processing_provider/inference.py index ee1b9e5..ac838b9 100644 --- a/processing_provider/inference.py +++ b/processing_provider/inference.py @@ -11,6 +11,13 @@ *************************************************************************** """ +import os +import sys +import glob +import gdal +import shutil +import numpy as np + from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, QgsFeatureSink, @@ -19,22 +26,19 @@ QgsProcessingParameterFeatureSource, QgsProcessingParameterFeatureSink, QgsProcessingParameterRasterLayer, - QgsProcessingParameterFile) + QgsProcessingParameterFile, + QgsProcessingParameterNumber, + QgsProcessingParameterFileDestination) from qgis import processing +from deeposlandia.inference import predict +from deeposlandia.postprocess import get_trained_model, extract_images, \ + extract_coordinates_from_filenames, \ + build_full_labelled_image, get_labels, \ + assign_label_colors, draw_grid class InferenceQDeepLandiaProcessingAlgorithm(QgsProcessingAlgorithm): """ - This is an example algorithm that takes a vector layer and - creates a new identical one. - - It is meant to be used as an example of how to create your own - algorithms and explain methods and variables used to do it. An - algorithm like this will be available in all elements, and there - is not need for additional work. - - All Processing algorithms should extend the QgsProcessingAlgorithm - class. """ # Constants used to refer to parameters and outputs. They will be @@ -43,8 +47,13 @@ class InferenceQDeepLandiaProcessingAlgorithm(QgsProcessingAlgorithm): INPUT = 'INPUT' MODEL = 'MODEL' + LABELS = 'LABELS' OUTPUT = 'OUTPUT' + def __init__(self, model=None): + super().__init__() + self.model=model + def tr(self, string): """ Returns a translatable string with the self.tr() function. @@ -52,7 +61,7 @@ def tr(self, string): return QCoreApplication.translate('Processing', string) def createInstance(self): - return InferenceQDeepLandiaProcessingAlgorithm() + return InferenceQDeepLandiaProcessingAlgorithm(self.model) def name(self): """ @@ -119,13 +128,24 @@ def initAlgorithm(self, config=None): ) ) + self.addParameter( + QgsProcessingParameterNumber( + self.LABELS, + self.tr('Number of labels used for the inference'), + type = QgsProcessingParameterNumber.Integer, + defaultValue = 4, + minValue = 2, + optional = True + ) + ) + # We add a feature sink in which to store our processed features (this # usually takes the form of a newly created vector layer when the # algorithm is run in QGIS). self.addParameter( - QgsProcessingParameterFeatureSink( + QgsProcessingParameterFileDestination( self.OUTPUT, - self.tr('Output layer') + self.tr('Output file') ) ) @@ -134,78 +154,88 @@ def processAlgorithm(self, parameters, context, feedback): Here is where the processing itself takes place. """ - # Retrieve the feature source and sink. The 'dest_id' variable is used - # to uniquely identify the feature sink, and must be included in the - # dictionary returned by the processAlgorithm function. - # source = self.parameterAsSource( - # parameters, - # self.INPUT, - # context - # ) - - # # If source was not found, throw an exception to indicate that the algorithm - # # encountered a fatal error. The exception text can be any string, but in this - # # case we use the pre-built invalidSourceError method to return a standard - # # helper text for when a source cannot be evaluated - # if source is None: - # raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT)) - - # (sink, dest_id) = self.parameterAsSink( - # parameters, - # self.OUTPUT, - # context, - # source.fields(), - # source.wkbType(), - # source.sourceCrs() - # ) - - # # Send some information to the user - # feedback.pushInfo('CRS is {}'.format(source.sourceCrs().authid())) - - # # If sink was not created, throw an exception to indicate that the algorithm - # # encountered a fatal error. The exception text can be any string, but in this - # # case we use the pre-built invalidSinkError method to return a standard - # # helper text for when a sink cannot be evaluated - # if sink is None: - # raise QgsProcessingException(self.invalidSinkError(parameters, self.OUTPUT)) - - # # Compute the number of steps to display within the progress bar and - # # get features from source - # total = 100.0 / source.featureCount() if source.featureCount() else 0 - # features = source.getFeatures() - - # for current, feature in enumerate(features): - # # Stop the algorithm if cancel button has been clicked - # if feedback.isCanceled(): - # break - - # # Add a feature in the sink - # sink.addFeature(feature, QgsFeatureSink.FastInsert) - - # # Update the progress bar - # feedback.setProgress(int(current * total)) - - # # To run another Processing algorithm as part of this algorithm, you can use - # # processing.run(...). Make sure you pass the current context and feedback - # # to processing.run to ensure that all temporary layer outputs are available - # # to the executed algorithm, and that the executed algorithm can send feedback - # # reports to the user (and correctly handle cancellation and progress reports!) - # if False: - # buffered_layer = processing.run("native:buffer", { - # 'INPUT': dest_id, - # 'DISTANCE': 1.5, - # 'SEGMENTS': 5, - # 'END_CAP_STYLE': 0, - # 'JOIN_STYLE': 0, - # 'MITER_LIMIT': 2, - # 'DISSOLVE': False, - # 'OUTPUT': 'memory:' - # }, context=context, feedback=feedback)['OUTPUT'] - - # Return the results of the algorithm. In this case our only result is - # the feature sink which contains the processed features, but some - # algorithms may return multiple feature sinks, calculated numeric - # statistics, etc. These should all be included in the returned - # dictionary, with keys matching the feature corresponding parameter - # or output names. - return #{self.OUTPUT: dest_id} + raster_in = self.parameterAsRasterLayer( + parameters, + self.INPUT, + context + ) + + if not self.model: + model_path = self.parameterAsString( + parameters, + self.MODEL, + context + ) + nb_labels = self.parameterAsInt( + parameters, + self.LABELS, + context + ) + datapath = os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..', '..')) + dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..'))) + image_size = os.path.splitext(os.path.basename(model_path))[0].split('-')[-1] + try : + model = get_trained_model(datapath, dataset, int(image_size), int(nb_labels)) + except: + sys.exit() + + else : + model = self.model['model'] + model_path = self.model['path'] + datapath = os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..', '..')) + dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..'))) + image_size = os.path.splitext(os.path.basename(model_path))[0].split('-')[-1] + + param = { 'INPUT': raster_in.id(), 'OUTPUT': datapath, 'DATASET': dataset, 'SHAPE': image_size} + + out = processing.run('QDeepLandia:DatagenQDeepLandia', param, feedback=feedback) + + raster_list = glob.glob(os.path.join(out['OUTPUT'],'*.png')) + + images = extract_images(raster_list) + coordinates = extract_coordinates_from_filenames(raster_list) + labels = get_labels(datapath, dataset, image_size) + + data = build_full_labelled_image( + images, + coordinates, + model, + int(image_size), + int(raster_in.width()), + int(raster_in.height()), + 128 + ) + + colored_data = assign_label_colors(data, labels) + colored_data = draw_grid( + colored_data, int(raster_in.width()), int(raster_in.height()), int(image_size) + ) + predicted_label_folder = os.path.join( + datapath, + dataset, + "output", + "semseg", + "predicted_labels" + ) + os.makedirs(predicted_label_folder, exist_ok=True) + predicted_label_file = os.path.join( + predicted_label_folder, + os.path.basename(os.path.splitext(raster_in.source())[0]) + "_" + str(image_size) + ".tif", + ) + ds = gdal.Open(raster_in.source()) + CreateGeoTiff(predicted_label_file, colored_data, ds.GetGeoTransform(), ds.GetProjection()) + shutil.copy(predicted_label_file, self.OUTPUT) + return {self.OUTPUT: predicted_label_file} + +def CreateGeoTiff(outRaster, data, geo_transform, projection): + driver = gdal.GetDriverByName('GTiff') + rows, cols, no_bands = data.shape + DataSet = driver.Create(outRaster, cols, rows, no_bands, gdal.GDT_Byte) + DataSet.SetGeoTransform(geo_transform) + DataSet.SetProjection(projection) + + data = np.moveaxis(data, -1, 0) + + for i, image in enumerate(data, 1): + DataSet.GetRasterBand(i).WriteArray(image) + DataSet = None \ No newline at end of file diff --git a/processing_provider/provider.py b/processing_provider/provider.py index 80abe51..ea4986d 100644 --- a/processing_provider/provider.py +++ b/processing_provider/provider.py @@ -1,12 +1,14 @@ from qgis.core import QgsProcessingProvider from .inference import InferenceQDeepLandiaProcessingAlgorithm +from .datagen import DatagenQDeepLandiaProcessingAlgorithm class QDeepLandiaProvider(QgsProcessingProvider): def loadAlgorithms(self, *args, **kwargs): self.addAlgorithm(InferenceQDeepLandiaProcessingAlgorithm()) + self.addAlgorithm(DatagenQDeepLandiaProcessingAlgorithm()) # add additional algorithms here # self.addAlgorithm(MyOtherAlgorithm()) diff --git a/qdeeplandia.py b/qdeeplandia.py index 0917a43..296691b 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -17,7 +17,10 @@ import os -from qgis.core import Qgis, QgsRasterDataProvider, QgsApplication +from qgis.core import Qgis, QgsRasterDataProvider, QgsApplication, \ + QgsProcessingFeedback, QgsMessageLog, QgsProcessingContext + +import processing from qgis.PyQt.QtCore import QSettings, QCoreApplication, pyqtSignal from qgis.PyQt.QtGui import QIcon @@ -26,6 +29,7 @@ os.environ['DEEPOSL_CONFIG']=os.path.join(os.path.dirname(__file__),'config.ini') from .deeposlandia import postprocess from .processing_provider.provider import QDeepLandiaProvider +from .processing_provider.inference import InferenceQDeepLandiaProcessingAlgorithm from .gui.NbLabelDialog import NbLabelDialog @@ -52,6 +56,9 @@ def __init__(self, iface): self.model = None self.deepOprovider = None self.layer = self.updateLayer() + self.model_path = None + self.datapath = None + self.dataset = None locale = QSettings().value('locale/userLocale') or 'en_USA' locale= locale[0:2] @@ -109,11 +116,14 @@ def tr(message): return QCoreApplication.translate('@default', message) def load_trained_model(self): - fil, __ = QFileDialog.getOpenFileName(None, + self.model_path, __ = QFileDialog.getOpenFileName(None, tr("Load best-model-*.h5 file"), os.path.abspath("."), tr("h5 file (*.h5)")) + if not self.model_path : + return + nbLabelDlg = NbLabelDialog(self) if nbLabelDlg.exec(): @@ -121,21 +131,28 @@ def load_trained_model(self): else : return - datapath = os.path.abspath(os.path.join(os.path.dirname(fil), '..', '..', '..', '..')) - dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(fil), '..', '..', '..'))) - image_size = os.path.splitext(os.path.basename(fil))[0].split('-')[-1] - print(datapath, dataset, image_size, nb_labels) + self.datapath = os.path.abspath(os.path.join(os.path.dirname(self.model_path), '..', '..', '..', '..')) + self.dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(self.model_path), '..', '..', '..'))) + self.image_size = os.path.splitext(os.path.basename(self.model_path))[0].split('-')[-1] try : - self.model = postprocess.get_trained_model(datapath, dataset, int(image_size), int(nb_labels)) + self.model = postprocess.get_trained_model(self.datapath, self.dataset, int(self.image_size), int(nb_labels)) except ValueError as e: self.iface.messageBar().pushMessage(tr("Critical"), str(e), level=Qgis.Critical) if self.model : - self.isready.emit() + self.updateLayer() def infer(self): - pass + feedback = Feedback(self.iface) + infer_algorithm = 'QDeepLandia:InferenceQDeepLandia' + param = { 'INPUT' : self.layer, 'OUTPUT' : '/home/speillet/tmp/tmp.tif', 'MODEL' : self.model_path } + inference_alg = InferenceQDeepLandiaProcessingAlgorithm(model={'path' : self.model_path ,'model' : self.model}) + out = inference_alg.run(param, context=QgsProcessingContext(), feedback=feedback) + + if os.path.exists(out[0]['OUTPUT']): + self.iface.addRasterLayer(out[0]['OUTPUT'], "labelled_output") + return def updateLayer(self): layer = self.mapCanvas.currentLayer() @@ -144,9 +161,29 @@ def updateLayer(self): self.layer = layer else : self.layer = None + else : + self.layer = None self.isready.emit() def ready(self) : - print (self.layer, self.model) if self.layer and self.model : - self.inference.setEnabled(True) \ No newline at end of file + self.inference.setEnabled(True) + +class Feedback(QgsProcessingFeedback): + """To provide feedback to the message bar from the express tools""" + + def __init__(self, iface): + super().__init__() + self.iface = iface + self.fatal_errors = [] + + def reportError(self, error, fatalError=False): + QgsMessageLog.logMessage(str(error), "QDeeplandia") + if fatalError: + self.fatal_errors.append(error) + + def pushToUser(self, exception): + QgsMessageLog.logMessage(str(exception), "QDeeplandia") + self.iface.messageBar().pushMessage( + "Error", ", ".join(self.fatal_errors), level=Qgis.Critical, duration=0 + ) \ No newline at end of file From 51475d3023b173f75b226ad64abdfabf4104f8b0 Mon Sep 17 00:00:00 2001 From: speillet Date: Tue, 28 Apr 2020 12:25:02 +0200 Subject: [PATCH 04/11] ENH : workflow refac with QgsTask + canvas extent use --- feedback.py | 20 +++ img/load.svg | 70 ++++++++ img/run.svg | 263 +++++++++++++++++++++++++++++++ inferenceTask.py | 34 ++++ processing_provider/datagen.py | 13 +- processing_provider/inference.py | 87 ++++++---- qdeeplandia.py | 70 ++++---- 7 files changed, 483 insertions(+), 74 deletions(-) create mode 100644 feedback.py create mode 100644 inferenceTask.py diff --git a/feedback.py b/feedback.py new file mode 100644 index 0000000..4eee0d0 --- /dev/null +++ b/feedback.py @@ -0,0 +1,20 @@ +from qgis.core import Qgis, QgsProcessingFeedback, QgsMessageLog + +class Feedback(QgsProcessingFeedback): + """To provide feedback to the message bar from the express tools""" + + def __init__(self, iface): + super().__init__() + self.iface = iface + self.fatal_errors = [] + + def reportError(self, error, fatalError=False): + QgsMessageLog.logMessage(str(error), "QDeeplandia") + if fatalError: + self.fatal_errors.append(error) + + def pushToUser(self, exception): + QgsMessageLog.logMessage(str(exception), "QDeeplandia") + self.iface.messageBar().pushMessage( + "Error", ", ".join(self.fatal_errors), level=Qgis.Critical, duration=0 + ) \ No newline at end of file diff --git a/img/load.svg b/img/load.svg index e69de29..8e90d7b 100644 --- a/img/load.svg +++ b/img/load.svg @@ -0,0 +1,70 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/img/run.svg b/img/run.svg index e69de29..5993738 100644 --- a/img/run.svg +++ b/img/run.svg @@ -0,0 +1,263 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + Play + + + play + playback + start + begin + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inferenceTask.py b/inferenceTask.py new file mode 100644 index 0000000..fc5b975 --- /dev/null +++ b/inferenceTask.py @@ -0,0 +1,34 @@ +import os + +from .feedback import Feedback +from qgis.core import Qgis, QgsTask, QgsMessageLog, QgsProcessingContext +import processing + +from qgis.PyQt.QtCore import pyqtSignal + +from .processing_provider.inference import InferenceQDeepLandiaProcessingAlgorithm + + +class InferenceTask(QgsTask): + """This shows how to subclass QgsTask""" + + terminated = pyqtSignal(str) + + def __init__(self, description, iface, layer, nb_label, model_path, extent=None): + super().__init__(description, QgsTask.CanCancel) + self.feedback = Feedback(iface) + self.param = { 'INPUT' : layer.id(), 'OUTPUT' : '/home/speillet/temp/tmp.tif', 'LABELS' : nb_label, 'MODEL' : model_path } + if extent : + self.param['EXTENT'] = extent + + def run(self): + out = processing.run('QDeepLandia:InferenceQDeepLandia', self.param, feedback=self.feedback) + if os.path.exists(out['OUTPUT']): + self.terminated.emit(out['OUTPUT']) + return True + + def cancel(self): + QgsMessageLog.logMessage( + 'Task "{name}" was canceled'.format( + name=self.description()), Qgis.Info) + super().cancel() diff --git a/processing_provider/datagen.py b/processing_provider/datagen.py index 808a9f8..23c469e 100644 --- a/processing_provider/datagen.py +++ b/processing_provider/datagen.py @@ -164,20 +164,21 @@ def processAlgorithm(self, parameters, context, feedback): context ) - # To do clip on canvas path='' - for i in [dest_path, dataset, 'input','images']: + for i in [dest_path, dataset, 'input','testing','images']: path = os.path.join(path, i) if not os.path.exists(path): os.mkdir(path) - if os.path.exists(os.path.join( path, os.path.basename( raster_in.source()))): - os.remove(os.path.join( path, os.path.basename( raster_in.source()))) + for file in os.listdir(path): + os.remove(os.path.join(path,file)) + shutil.copy( raster_in.source(), os.path.join( path, os.path.basename( raster_in.source()))) + output_folder = os.path.join(dest_path, dataset, 'preprocessed', str(shape), 'testing', 'images') + shutil.rmtree(os.path.join(dest_path, dataset, 'preprocessed', str(shape))) + cmd = ['deepo', 'datagen', '-D', dataset, '-s', str(shape), '-P', dest_path, '-T', '1'] subprocess.run(cmd) - output_folder = os.path.join(dest_path, dataset, 'preprocessed', str(shape), 'testing', 'images') - return {self.OUTPUT: output_folder} \ No newline at end of file diff --git a/processing_provider/inference.py b/processing_provider/inference.py index ac838b9..0c37a93 100644 --- a/processing_provider/inference.py +++ b/processing_provider/inference.py @@ -20,6 +20,7 @@ from qgis.PyQt.QtCore import QCoreApplication from qgis.core import (QgsProcessing, + QgsRasterLayer, QgsFeatureSink, QgsProcessingException, QgsProcessingAlgorithm, @@ -28,7 +29,8 @@ QgsProcessingParameterRasterLayer, QgsProcessingParameterFile, QgsProcessingParameterNumber, - QgsProcessingParameterFileDestination) + QgsProcessingParameterFileDestination, + QgsProcessingParameterExtent) from qgis import processing from deeposlandia.inference import predict @@ -46,13 +48,13 @@ class InferenceQDeepLandiaProcessingAlgorithm(QgsProcessingAlgorithm): # calling from the QGIS console. INPUT = 'INPUT' + EXTENT = 'EXTENT' MODEL = 'MODEL' LABELS = 'LABELS' OUTPUT = 'OUTPUT' def __init__(self, model=None): super().__init__() - self.model=model def tr(self, string): """ @@ -61,7 +63,7 @@ def tr(self, string): return QCoreApplication.translate('Processing', string) def createInstance(self): - return InferenceQDeepLandiaProcessingAlgorithm(self.model) + return InferenceQDeepLandiaProcessingAlgorithm() def name(self): """ @@ -120,6 +122,15 @@ def initAlgorithm(self, config=None): ) ) + self.addParameter( + QgsProcessingParameterExtent( + self.EXTENT, + self.tr('Input extent'), + defaultValue= None, + optional=True + ) + ) + self.addParameter( QgsProcessingParameterFile( self.MODEL, @@ -160,38 +171,50 @@ def processAlgorithm(self, parameters, context, feedback): context ) - if not self.model: - model_path = self.parameterAsString( - parameters, - self.MODEL, - context - ) - nb_labels = self.parameterAsInt( - parameters, - self.LABELS, - context - ) - datapath = os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..', '..')) - dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..'))) - image_size = os.path.splitext(os.path.basename(model_path))[0].split('-')[-1] - try : - model = get_trained_model(datapath, dataset, int(image_size), int(nb_labels)) - except: - sys.exit() - - else : - model = self.model['model'] - model_path = self.model['path'] - datapath = os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..', '..')) - dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..'))) - image_size = os.path.splitext(os.path.basename(model_path))[0].split('-')[-1] + output_path = self.parameterAsString( + parameters, + self.OUTPUT, + context + ) - param = { 'INPUT': raster_in.id(), 'OUTPUT': datapath, 'DATASET': dataset, 'SHAPE': image_size} + model_path = self.parameterAsString( + parameters, + self.MODEL, + context + ) + nb_labels = self.parameterAsInt( + parameters, + self.LABELS, + context + ) + + datapath = os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..', '..')) + dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(model_path), '..', '..', '..'))) + image_size = os.path.splitext(os.path.basename(model_path))[0].split('-')[-1] + try : + model = get_trained_model(datapath, dataset, int(image_size), int(nb_labels)) + except: + sys.exit() + extent = self.parameterAsExtent( + parameters, + self.EXTENT, + context + ) + + param = { 'INPUT': raster_in.id(), 'OUTPUT': datapath, 'DATASET': dataset, 'SHAPE': image_size} + if extent.xMinimum() != 0 and extent.xMaximum() != 0: + if ((extent.xMaximum() - extent.xMinimum())/raster_in.rasterUnitsPerPixelX() >= int(image_size) and \ + (extent.yMaximum() - extent.yMinimum())/raster_in.rasterUnitsPerPixelY() >= int(image_size)): + clipped = os.path.join(os.path.dirname(output_path), 'clipped.tif') + param = { 'INPUT': raster_in.id(), 'PROJWIN': extent, 'OUTPUT': clipped} + out = processing.run('gdal:cliprasterbyextent', param, feedback=feedback) + param = { 'INPUT': out['OUTPUT'], 'OUTPUT': datapath, 'DATASET': dataset, 'SHAPE': image_size} + raster_in = QgsRasterLayer(out['OUTPUT'],'clipped', 'gdal') + out = processing.run('QDeepLandia:DatagenQDeepLandia', param, feedback=feedback) raster_list = glob.glob(os.path.join(out['OUTPUT'],'*.png')) - images = extract_images(raster_list) coordinates = extract_coordinates_from_filenames(raster_list) labels = get_labels(datapath, dataset, image_size) @@ -224,8 +247,8 @@ def processAlgorithm(self, parameters, context, feedback): ) ds = gdal.Open(raster_in.source()) CreateGeoTiff(predicted_label_file, colored_data, ds.GetGeoTransform(), ds.GetProjection()) - shutil.copy(predicted_label_file, self.OUTPUT) - return {self.OUTPUT: predicted_label_file} + shutil.copy(predicted_label_file, output_path) + return {self.OUTPUT: output_path} def CreateGeoTiff(outRaster, data, geo_transform, projection): driver = gdal.GetDriverByName('GTiff') diff --git a/qdeeplandia.py b/qdeeplandia.py index 296691b..1359105 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -24,15 +24,18 @@ from qgis.PyQt.QtCore import QSettings, QCoreApplication, pyqtSignal from qgis.PyQt.QtGui import QIcon -from qgis.PyQt.QtWidgets import QAction, QFileDialog, QWidget, QHBoxLayout, QVBoxLayout, QMessageBox +from qgis.PyQt.QtWidgets import QAction, QFileDialog, QWidget, \ + QHBoxLayout, QVBoxLayout, QMessageBox, \ + QToolBar, QLabel, QCheckBox os.environ['DEEPOSL_CONFIG']=os.path.join(os.path.dirname(__file__),'config.ini') from .deeposlandia import postprocess from .processing_provider.provider import QDeepLandiaProvider -from .processing_provider.inference import InferenceQDeepLandiaProcessingAlgorithm from .gui.NbLabelDialog import NbLabelDialog +from .inferenceTask import InferenceTask + def tr(message): """Get the translation for a string using Qt translation API. """ @@ -56,6 +59,7 @@ def __init__(self, iface): self.model = None self.deepOprovider = None self.layer = self.updateLayer() + self.nb_labels = None self.model_path = None self.datapath = None self.dataset = None @@ -79,21 +83,34 @@ def __init__(self, iface): def initGui(self): # Select a trained model on the file system self.initProcessing() + + self.toolbar = QToolBar("QDeepLandia_toolbar") + self.toolbar.setObjectName("QDeepLandia_toolbar") + # self.toolbar.setMaximumWidth(180) + self.toolbar.addWidget(QLabel("QDeeplandia")) + self.iface.addToolBar(self.toolbar) + + # Load model process load_model_msg = tr("Load a trained model") load_icon = QIcon(os.path.join(os.path.dirname(__file__), "img/load.svg")) self.model_loading = QAction(load_icon, load_model_msg, self.iface.mainWindow()) self.model_loading.triggered.connect(lambda: self.load_trained_model()) self.iface.addPluginToMenu("QDeeplandia", self.model_loading) - self.iface.addToolBarIcon(self.model_loading) + self.toolbar.addAction(self.model_loading) + # Run-an-inference process run_inference_msg = tr("Run an inference") run_icon = QIcon(os.path.join(os.path.dirname(__file__), "img/run.svg")) self.inference = QAction(run_icon, run_inference_msg, self.iface.mainWindow()) self.inference.triggered.connect(lambda: self.infer()) self.iface.addPluginToMenu("QDeeplandia", self.inference) - self.iface.addToolBarIcon(self.inference) + self.toolbar.addAction(self.inference) self.inference.setEnabled(False) + # Use canvas parameters + self.canvasCheckbox = QCheckBox('Use canvas extent') + self.toolbar.addWidget(self.canvasCheckbox) + def initProcessing(self): self.deepOprovider = QDeepLandiaProvider() QgsApplication.processingRegistry().addProvider(self.deepOprovider) @@ -101,11 +118,10 @@ def initProcessing(self): def unload(self): # Select a trained model on the file system self.iface.removePluginMenu("QDeeplandia", self.model_loading) - self.iface.removeToolBarIcon(self.model_loading) + self.toolbar.setParent(None) self.model_loading.setParent(None) # Run-an-inference process self.iface.removePluginMenu("QDeeplandia", self.inference) - self.iface.removeToolBarIcon(self.inference) self.inference.setParent(None) QgsApplication.processingRegistry().removeProvider(self.deepOprovider) @@ -127,7 +143,7 @@ def load_trained_model(self): nbLabelDlg = NbLabelDialog(self) if nbLabelDlg.exec(): - nb_labels = nbLabelDlg.param() + self.nb_labels = nbLabelDlg.param() else : return @@ -135,7 +151,7 @@ def load_trained_model(self): self.dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(self.model_path), '..', '..', '..'))) self.image_size = os.path.splitext(os.path.basename(self.model_path))[0].split('-')[-1] try : - self.model = postprocess.get_trained_model(self.datapath, self.dataset, int(self.image_size), int(nb_labels)) + self.model = postprocess.get_trained_model(self.datapath, self.dataset, int(self.image_size), int(self.nb_labels)) except ValueError as e: self.iface.messageBar().pushMessage(tr("Critical"), str(e), level=Qgis.Critical) @@ -144,15 +160,16 @@ def load_trained_model(self): self.updateLayer() def infer(self): - feedback = Feedback(self.iface) - infer_algorithm = 'QDeepLandia:InferenceQDeepLandia' - param = { 'INPUT' : self.layer, 'OUTPUT' : '/home/speillet/tmp/tmp.tif', 'MODEL' : self.model_path } - inference_alg = InferenceQDeepLandiaProcessingAlgorithm(model={'path' : self.model_path ,'model' : self.model}) - out = inference_alg.run(param, context=QgsProcessingContext(), feedback=feedback) + extent = None + if self.canvasCheckbox.checkState() : + extent = self.mapCanvas.extent() - if os.path.exists(out[0]['OUTPUT']): - self.iface.addRasterLayer(out[0]['OUTPUT'], "labelled_output") - return + def addOutput(layer): + self.iface.addRasterLayer(layer) + + task = InferenceTask('Inference', self.iface, self.layer, self.nb_labels, self.model_path, extent) + task.terminated.connect(addOutput) + QgsApplication.taskManager().addTask(task) def updateLayer(self): layer = self.mapCanvas.currentLayer() @@ -167,23 +184,4 @@ def updateLayer(self): def ready(self) : if self.layer and self.model : - self.inference.setEnabled(True) - -class Feedback(QgsProcessingFeedback): - """To provide feedback to the message bar from the express tools""" - - def __init__(self, iface): - super().__init__() - self.iface = iface - self.fatal_errors = [] - - def reportError(self, error, fatalError=False): - QgsMessageLog.logMessage(str(error), "QDeeplandia") - if fatalError: - self.fatal_errors.append(error) - - def pushToUser(self, exception): - QgsMessageLog.logMessage(str(exception), "QDeeplandia") - self.iface.messageBar().pushMessage( - "Error", ", ".join(self.fatal_errors), level=Qgis.Critical, duration=0 - ) \ No newline at end of file + self.inference.setEnabled(True) \ No newline at end of file From 92f74f0af15158c53e5a451736511b594b753a8a Mon Sep 17 00:00:00 2001 From: speillet Date: Fri, 22 May 2020 10:27:14 +0200 Subject: [PATCH 05/11] use tmp folder --- inferenceTask.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/inferenceTask.py b/inferenceTask.py index fc5b975..b466399 100644 --- a/inferenceTask.py +++ b/inferenceTask.py @@ -1,8 +1,10 @@ import os +import sys from .feedback import Feedback from qgis.core import Qgis, QgsTask, QgsMessageLog, QgsProcessingContext import processing +import random, string from qgis.PyQt.QtCore import pyqtSignal @@ -10,14 +12,19 @@ class InferenceTask(QgsTask): - """This shows how to subclass QgsTask""" + """InferenceTask is a QgsTask subclass""" terminated = pyqtSignal(str) def __init__(self, description, iface, layer, nb_label, model_path, extent=None): super().__init__(description, QgsTask.CanCancel) self.feedback = Feedback(iface) - self.param = { 'INPUT' : layer.id(), 'OUTPUT' : '/home/speillet/temp/tmp.tif', 'LABELS' : nb_label, 'MODEL' : model_path } + if sys.platform == 'windows' : + tmp_folder = os.path.join(os.environ['LOCALAPPDATA'], 'QGIS') + else : + tmp_folder = '/tmp' + tmp_name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(16)) + '.tif' + self.param = { 'INPUT' : layer.id(), 'OUTPUT' : os.path.join(tmp_folder,tmp_name), 'LABELS' : nb_label, 'MODEL' : model_path } if extent : self.param['EXTENT'] = extent @@ -30,5 +37,6 @@ def run(self): def cancel(self): QgsMessageLog.logMessage( 'Task "{name}" was canceled'.format( - name=self.description()), Qgis.Info) + name=self.description()), "QDeeplandia") + self.terminated.emit(None) super().cancel() From 0b87259b3c34ffff1c75732c2054a904a815e048 Mon Sep 17 00:00:00 2001 From: speillet Date: Fri, 22 May 2020 10:38:43 +0200 Subject: [PATCH 06/11] better method to get tmp dir --- inferenceTask.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/inferenceTask.py b/inferenceTask.py index b466399..0529c95 100644 --- a/inferenceTask.py +++ b/inferenceTask.py @@ -19,12 +19,8 @@ class InferenceTask(QgsTask): def __init__(self, description, iface, layer, nb_label, model_path, extent=None): super().__init__(description, QgsTask.CanCancel) self.feedback = Feedback(iface) - if sys.platform == 'windows' : - tmp_folder = os.path.join(os.environ['LOCALAPPDATA'], 'QGIS') - else : - tmp_folder = '/tmp' - tmp_name = ''.join(random.choice(string.ascii_uppercase + string.ascii_lowercase + string.digits) for _ in range(16)) + '.tif' - self.param = { 'INPUT' : layer.id(), 'OUTPUT' : os.path.join(tmp_folder,tmp_name), 'LABELS' : nb_label, 'MODEL' : model_path } + tmp_name = processing.getTempFilename() + '.tif' + self.param = { 'INPUT' : layer.id(), 'OUTPUT' : os.path.join(tmp_name), 'LABELS' : nb_label, 'MODEL' : model_path } if extent : self.param['EXTENT'] = extent From d3717818d0af03264f3578fe3cf593b671364623 Mon Sep 17 00:00:00 2001 From: speillet Date: Fri, 22 May 2020 10:39:23 +0200 Subject: [PATCH 07/11] block inference button during inference task --- qdeeplandia.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/qdeeplandia.py b/qdeeplandia.py index 1359105..ff3edc3 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -84,10 +84,10 @@ def initGui(self): # Select a trained model on the file system self.initProcessing() - self.toolbar = QToolBar("QDeepLandia_toolbar") + self.toolbar = QToolBar(tr("QDeepLandia_toolbar")) self.toolbar.setObjectName("QDeepLandia_toolbar") # self.toolbar.setMaximumWidth(180) - self.toolbar.addWidget(QLabel("QDeeplandia")) + self.toolbar.addWidget(QLabel(tr("QDeeplandia"))) self.iface.addToolBar(self.toolbar) # Load model process @@ -108,7 +108,7 @@ def initGui(self): self.inference.setEnabled(False) # Use canvas parameters - self.canvasCheckbox = QCheckBox('Use canvas extent') + self.canvasCheckbox = QCheckBox(tr('Use canvas extent')) self.toolbar.addWidget(self.canvasCheckbox) def initProcessing(self): @@ -132,6 +132,7 @@ def tr(message): return QCoreApplication.translate('@default', message) def load_trained_model(self): + """Load a h5 model""" self.model_path, __ = QFileDialog.getOpenFileName(None, tr("Load best-model-*.h5 file"), os.path.abspath("."), @@ -160,18 +161,23 @@ def load_trained_model(self): self.updateLayer() def infer(self): + """Launch inference on the current layer""" extent = None if self.canvasCheckbox.checkState() : extent = self.mapCanvas.extent() def addOutput(layer): - self.iface.addRasterLayer(layer) + self.inference.setEnabled(True) + if layer : + self.iface.addRasterLayer(layer) task = InferenceTask('Inference', self.iface, self.layer, self.nb_labels, self.model_path, extent) task.terminated.connect(addOutput) + self.inference.setEnabled(False) QgsApplication.taskManager().addTask(task) def updateLayer(self): + """Update the current layer""" layer = self.mapCanvas.currentLayer() if layer : if isinstance(layer.dataProvider(), QgsRasterDataProvider): From 165208aeb06547ed0e8b8aefac1a1d9057d32440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Delhome?= Date: Thu, 28 May 2020 08:44:38 +0200 Subject: [PATCH 08/11] config: consider a config sample, that each user must tune --- .gitignore | 4 +++- config.ini | 19 ------------------- config.ini.sample | 12 ++++++++++++ 3 files changed, 15 insertions(+), 20 deletions(-) delete mode 100644 config.ini create mode 100644 config.ini.sample diff --git a/.gitignore b/.gitignore index e4e5f6c..f5e5e8f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -*~ \ No newline at end of file +*~ +config.ini +data \ No newline at end of file diff --git a/config.ini b/config.ini deleted file mode 100644 index bad56fd..0000000 --- a/config.ini +++ /dev/null @@ -1,19 +0,0 @@ -[status] -status = dev - -[running] -processes = 1 - -[symlink] -predicted = /path/to/predicted/images/ -shapes = /path/to/shape/dataset/ -mapillary = /path/to/mapillary/dataset/ -mapillary_agg = /path/to/agregated/mapillary/dataset/ -aerial = /path/to/aerial/dataset/ -tanzania = /home/speillet/OpenSourceProject/deeposlandia/tests/data/tanzania - -[folder] -project_folder = /home/speillet/OpenSourceProject/deeposlandia/deeposlandia/projet - -[key] -secret_key = enter-your-app-key diff --git a/config.ini.sample b/config.ini.sample new file mode 100644 index 0000000..b56755e --- /dev/null +++ b/config.ini.sample @@ -0,0 +1,12 @@ +[status] +status = dev + +[running] +processes = 1 + +[symlink] +aerial = /path/to/aerial/dataset/ +tanzania = /path/to/tanzania/dataset/ + +[folder] +project_folder = /path/to/static/files/ From 93fcffcb541765d9557c0f81741b10042a7c1ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Delhome?= Date: Thu, 28 May 2020 10:22:36 +0200 Subject: [PATCH 09/11] packaging: update Makefile --- Makefile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0cdad25..fe42686 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,13 @@ endif SOURCES=__init__.py \ metadata.txt \ - qdeeplandia.py + qdeeplandia.py \ + config.ini \ + inferenceTask.py \ + feedback.py \ + gui \ + img \ + processing_provider ZIP_FILE=$(PLUGIN_NAME)-$(VERSION).zip From 107f22d4a66bc6a0cebcc95a96e8a7204d415cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Delhome?= Date: Thu, 28 May 2020 10:24:47 +0200 Subject: [PATCH 10/11] minor pep8 fixes --- processing_provider/datagen.py | 8 ++++---- qdeeplandia.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/processing_provider/datagen.py b/processing_provider/datagen.py index 23c469e..86b6a52 100644 --- a/processing_provider/datagen.py +++ b/processing_provider/datagen.py @@ -164,8 +164,8 @@ def processAlgorithm(self, parameters, context, feedback): context ) - path='' - for i in [dest_path, dataset, 'input','testing','images']: + path = '' + for i in [dest_path, dataset, 'input', 'testing', 'images']: path = os.path.join(path, i) if not os.path.exists(path): os.mkdir(path) @@ -177,8 +177,8 @@ def processAlgorithm(self, parameters, context, feedback): output_folder = os.path.join(dest_path, dataset, 'preprocessed', str(shape), 'testing', 'images') shutil.rmtree(os.path.join(dest_path, dataset, 'preprocessed', str(shape))) - + cmd = ['deepo', 'datagen', '-D', dataset, '-s', str(shape), '-P', dest_path, '-T', '1'] subprocess.run(cmd) - return {self.OUTPUT: output_folder} \ No newline at end of file + return {self.OUTPUT: output_folder} diff --git a/qdeeplandia.py b/qdeeplandia.py index ff3edc3..8ee99b6 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -33,9 +33,9 @@ from .processing_provider.provider import QDeepLandiaProvider from .gui.NbLabelDialog import NbLabelDialog - from .inferenceTask import InferenceTask + def tr(message): """Get the translation for a string using Qt translation API. """ @@ -65,7 +65,7 @@ def __init__(self, iface): self.dataset = None locale = QSettings().value('locale/userLocale') or 'en_USA' - locale= locale[0:2] + locale = locale[0:2] locale_path = os.path.join( os.path.dirname(__file__), 'i18n', @@ -184,10 +184,10 @@ def updateLayer(self): self.layer = layer else : self.layer = None - else : + else : self.layer = None self.isready.emit() def ready(self) : if self.layer and self.model : - self.inference.setEnabled(True) \ No newline at end of file + self.inference.setEnabled(True) From 0e5a51a7ef94ac496489f7747ebb999cf06e9122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Delhome?= Date: Thu, 28 May 2020 10:25:43 +0200 Subject: [PATCH 11/11] load_model: simplify the model loading with respect to deeposlandia 0.6.2 --- qdeeplandia.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/qdeeplandia.py b/qdeeplandia.py index 8ee99b6..dcf75d5 100644 --- a/qdeeplandia.py +++ b/qdeeplandia.py @@ -28,10 +28,10 @@ QHBoxLayout, QVBoxLayout, QMessageBox, \ QToolBar, QLabel, QCheckBox -os.environ['DEEPOSL_CONFIG']=os.path.join(os.path.dirname(__file__),'config.ini') -from .deeposlandia import postprocess -from .processing_provider.provider import QDeepLandiaProvider +os.environ['DEEPOSL_CONFIG'] = os.path.join(os.path.dirname(__file__), 'config.ini') +from deeposlandia.postprocess import get_trained_model +from .processing_provider.provider import QDeepLandiaProvider from .gui.NbLabelDialog import NbLabelDialog from .inferenceTask import InferenceTask @@ -61,8 +61,6 @@ def __init__(self, iface): self.layer = self.updateLayer() self.nb_labels = None self.model_path = None - self.datapath = None - self.dataset = None locale = QSettings().value('locale/userLocale') or 'en_USA' locale = locale[0:2] @@ -147,12 +145,10 @@ def load_trained_model(self): self.nb_labels = nbLabelDlg.param() else : return - - self.datapath = os.path.abspath(os.path.join(os.path.dirname(self.model_path), '..', '..', '..', '..')) - self.dataset = os.path.basename(os.path.abspath(os.path.join(os.path.dirname(self.model_path), '..', '..', '..'))) + self.image_size = os.path.splitext(os.path.basename(self.model_path))[0].split('-')[-1] try : - self.model = postprocess.get_trained_model(self.datapath, self.dataset, int(self.image_size), int(self.nb_labels)) + self.model = get_trained_model(self.model_path, int(self.image_size), int(self.nb_labels)) except ValueError as e: self.iface.messageBar().pushMessage(tr("Critical"), str(e), level=Qgis.Critical)