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 = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'FFTConvolution_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Create the dialog (after translation) and keep reference self.dlg = FFTConvolutionDialog() # Declare instance attributes self.actions = [] self.menu = self.tr(u'&FFT COnvolution Filters') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'FFTConvolution') self.toolbar.setObjectName(u'FFTConvolution') self.dlg.output_file.clear() self.dlg.output_file_button.clicked.connect(self.select_output_file)
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 = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'FFTConvolution_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Create the dialog (after translation) and keep reference self.dlg = FFTConvolutionDialog() # Declare instance attributes self.actions = [] self.menu = self.tr(u'&FFT COnvolution Filters') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'FFTConvolution') self.toolbar.setObjectName(u'FFTConvolution') self.dlg.output_file.clear() self.dlg.output_file_button.clicked.connect(self.select_output_file)
class FFTConvolution: """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 = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join(self.plugin_dir, 'i18n', 'FFTConvolution_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Create the dialog (after translation) and keep reference self.dlg = FFTConvolutionDialog() # Declare instance attributes self.actions = [] self.menu = self.tr(u'&FFT COnvolution Filters') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'FFTConvolution') self.toolbar.setObjectName(u'FFTConvolution') self.dlg.output_file.clear() self.dlg.output_file_button.clicked.connect(self.select_output_file) # 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 QCoreApplication.translate('FFTConvolution', 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 = QIcon(icon_path) action = 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.addPluginToRasterMenu(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/FFTConvolution/icon.png' self.add_action(icon_path, text=self.tr(u'FFT Convolution filters'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginRasterMenu( self.tr(u'&FFT COnvolution Filters'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def select_output_file(self): filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ", "", '*.tif') self.dlg.output_file.setText(filename) def run(self): """Run method that performs all the real work""" # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: if self.dlg.smoothing.isChecked(): edge = False else: edge = True #call the function linking to real work #the input is translated from the GUI input to correct format here self.fft_convolution(in_layer=self.dlg.input_layer.currentLayer(), out_path=self.dlg.output_file.text(), size=self.dlg.size.text(), edge=edge, new_crs=self.dlg.crs.crs(), tiled=self.dlg.window.isChecked(), tilerows=self.dlg.window_rows.text(), tilecols=self.dlg.window_cols.text(), add_layer=self.dlg.check_add.isChecked()) #this function parses the arguments, calls the appropriate functions and displays the new layer if needed def fft_convolution(self, in_layer, out_path, size=10, edge=False, new_crs=None, tiled=False, tilerows=0, tilecols=0, add_layer=True): #if the file has no extension, add '.tif' ext = os.path.splitext(out_path)[-1].lower() if ext == '': out_path = out_path + '.tif' #if the file already exists, ask the user if os.path.isfile(out_path): reply = QMessageBox.question(None, 'File exists!', 'File exists - overwite it?', QMessageBox.Yes, QMessageBox.No) if reply == QMessageBox.No: return False #we need the CRS as EPSG code, or None if invalid if new_crs.isValid(): new_crs = new_crs.authid() else: new_crs = None #preprocessing the input layer's path in_path = in_layer.dataProvider().dataSourceUri() #QMessageBox.information(None, "DEBUG:", str(in_path)) if in_path.find('=') > -1: QMessageBox.information(None, "Sorry!", "WMS support wasn't implemented yet!") return False #the main computation layer = self.gaussian_filter(in_path=in_path, out_path=out_path, size=int(re.sub(r"\D", "", size)), edge=edge, tiled=tiled, tilerows=tilerows, tilecols=tilecols, new_crs=new_crs) if add_layer: QgsMapLayerRegistry.instance().addMapLayers([layer]) qgis.utils.iface.mapCanvas().refresh() #returns number of array dimensions def __array_rank(self, arr): return len(arr.shape) #loads the newly created layer def __load_layer(self, path): fileName = path fileInfo = QFileInfo(fileName) baseName = fileInfo.baseName() rlayer = QgsRasterLayer(fileName, baseName) if not rlayer.isValid(): raise Exception( "Computation finished, but layer failed to load. Inspect the path zou specified for the output layer." ) return rlayer #pads the window to process its original extend accurately def __extend_window(self, window, size, height, width): minrow = max(window[0][0] - size, 0) maxrow = min(window[0][1] + size, height) mincol = max(window[1][0] - size, 0) maxcol = min(window[1][1] + size, width) return ((minrow, maxrow), (mincol, maxcol)) #creates a window generator, in the same format as it is returned by block_windows method def __generate_windows(self, height, width, tilerows, tilecols): rownum = int(math.ceil(float(height) / tilerows)) colnum = int(math.ceil(float(width) / tilecols)) for i in range(rownum): #last row's and column's dimensions are computed by modulo - they are smaller than regular tiles if i == rownum - 1: rowsize = height % tilerows else: rowsize = tilerows for j in range(colnum): if j == colnum - 1: colsize = width % tilecols else: colsize = tilecols cell = ((i, j), ((i * tilerows, i * tilerows + rowsize), (j * tilecols, j * tilecols + colsize))) yield cell #like __generate_windows(), but returns an array def __generate_window_array(self, height, width, tilerows, tilecols): rownum = int(math.ceil(float(height) / tilerows)) colnum = int(math.ceil(float(width) / tilecols)) windows = np.asarray(np.zeros((height, width), dtype=object)) for i in range(rownum): #last row's and column's dimensions are computed by modulo - they are smaller than regular tiles if i == rownum - 1: rowsize = height % tilerows else: rowsize = tilerows for j in range(colnum): if j == colnum - 1: colsize = width % tilecols else: colsize = tilecols windows[i][j] = ((i * tilerows, i * tilerows + rowsize), (j * tilecols, j * tilecols + colsize)) return windows #processes the window parameters #returns the windows as a generator or an array (specified in the generator parameter) def __compute_windows(self, in_raster, height, width, size, tilerows=0, tilecols=0, generator=True): #input validation size = int(size) try: tilerows = int(tilerows) except ValueError: tilerows = 0 try: tilecols = int(tilecols) except ValueError: tilecols = 0 #when raster's dimensions are modified due to reprojection, we must adjust the windows as well #this algorithm is quick'n'dirty - we just sompute one ratio and make all tiles larger/smaller #reprojecting each tile node would be better - perhaps in some future version if height != in_raster.height or width != in_raster.width: hratio = float(height) / in_raster.height wratio = float(width) / in_raster.width else: hratio = 1 wratio = 1 windows = [] #if only one of tile's dimension was set, we assume a square if tilecols == 0 and tilerows > 0: tilecols = tilerows elif tilerows == 0 and tilecols > 0: tilerows = tilecols #if tiles are too small (including default 0 length), we make them automatically #"2*size" is the total padding length and also an arbitrarily chosen minimum #"size" would be a minimum to get accurate tiles, but this way only 1/9 of the tile would be useful => inefficient #"2*size" means at least 50% efficiency if min(tilerows, tilecols) <= 2 * size: #if the raster has blocks and they are big enough, we use them blocks = in_raster.block_shapes block = blocks[0] if min(block[0], block[1]) > 2 * size: #if we compute the original raster, use the block as-is #otherwise use the dimensions and continue if hratio == 1 and wratio == 1: return in_raster.block_windows(1) else: tilerows = block[0] tilecols = block[1] else: #"2*size + 100" is an arbitrary constant #it's quite efficient on smaller rasters and shouldn't make any memory issues #really small rasters shouldn't be computed by tiles anyway tilerows = 2 * size + 100 tilecols = 2 * size + 100 #we transform the dimensions if needed tilerows = int(hratio * tilerows) tilecols = int(wratio * tilecols) #if the tiles are too big (more than half of any dimension of the raster), #we switch to the untiled algorithm if 2 * tilerows >= height or 2 * tilecols >= width: return False #if windows are not read from the raster, we must make them if generator: windows = self.__generate_windows(height, width, tilerows, tilecols) else: windows = self.__generate_window_array(height, width, tilerows, tilecols) return windows #computes the affine transformation, raster dimensions and metadata for the new raster def __compute_transform(self, in_raster, new_crs): affine, width, height = calculate_default_transform( in_raster.crs, new_crs, in_raster.width, in_raster.height, *in_raster.bounds) kwargs = in_raster.meta.copy() kwargs.update({ 'driver': 'GTiff', 'crs': new_crs, 'transform': affine, 'affine': affine, 'width': width, 'height': height }) return affine, height, width, kwargs #calls reproject() function for every band def __reproject(self, in_raster, out_raster, affine, new_crs): for k in range(1, in_raster.count + 1): reproject(source=rasterio.band(in_raster, k), destination=rasterio.band(out_raster, k), src_transform=affine, src_crs=in_raster.crs, dst_transform=affine, dst_crs=new_crs, resampling=RESAMPLING.nearest) return out_raster #the original MikeT's function from http://gis.stackexchange.com/a/10467 #my addition is the conversion of the padded array to float to avoid errors with integer rasters #the intended input is a single band raster array def __gaussian_blur1d(self, in_array, size): #check validity try: if 0 in in_array.shape: raise Exception("Null array can't be processed!") except TypeError: raise Exception("Null array can't be processed!") # expand in_array to fit edge of kernel padded_array = np.pad(in_array, size, 'symmetric').astype(float) # build kernel x, y = np.mgrid[-size:size + 1, -size:size + 1] g = np.exp(-(x**2 / float(size) + y**2 / float(size))) g = (g / g.sum()).astype(float) # do the Gaussian blur out_array = fftconvolve(padded_array, g, mode='valid') return out_array.astype(in_array.dtype) #the intended input is an array with various number of bands #returns a numpy array def __gaussian_blur(self, in_array, size): #make sure the input is a numpy array in_array = np.asarray(in_array) #find number of dimensions - 2 for single-band or 3 for multiband rasters rank = self.__array_rank(in_array) if rank == 2: return self.__gaussian_blur1d(in_array, size).astype(in_array.dtype) elif rank > 3 or rank == 1: raise TypeError("Invalid number of dimensions!") #continue to multiband count = in_array.shape[0] out = [] for i in range(count): band = in_array[i] out_band = self.__gaussian_blur1d(band, size) #.astype(in_array.dtype) #if out_band != False: out.append(out_band) return np.asarray(out) #the real work is done here #filters a raster specified by the file's path (in_path) and writes it to another file (out_path) def gaussian_filter(self, in_path, out_path, size, edge=False, tiled=False, tilerows=0, tilecols=0, new_crs=None): with rasterio.drivers(): with rasterio.open(in_path, 'r') as in_raster: if new_crs == None: new_crs = in_raster.crs affine, height, width, kwargs = self.__compute_transform( in_raster, new_crs) if tiled: #we make two sets of tiles, for the old and the new raster #this is important in case of reprojection old_windows = self.__compute_windows( in_raster=in_raster, height=in_raster.height, width=in_raster.width, size=size, tilerows=tilerows, tilecols=tilecols) #windows for the new raster are made in two steps: generator and array new_windows = self.__compute_windows(in_raster=in_raster, height=height, width=width, size=size, tilerows=tilerows, tilecols=tilecols, generator=False) #if windows are too big or invalid, we process the raster without them try: iter(old_windows) iter(new_windows) except TypeError: tiled = False with rasterio.open(out_path, 'w', **kwargs) as out_raster: out_raster = self.__reproject(in_raster, out_raster, affine, new_crs) if tiled: for index, window in old_windows: oldbigwindow = self.__extend_window( window, size, in_raster.height, in_raster.width) in_array = in_raster.read(window=oldbigwindow) out_array = self.__gaussian_blur(in_array, size) #for edge detection we subtract the output array from the original #this may produce some artifacts when the raster is reprojected #or extensive and with degree coordinates if edge: out_array = np.subtract(in_array, out_array) #now compute the window for writing into the new raster nwindow = new_windows[index[0]][index[1]] newbigwindow = self.__extend_window( nwindow, size, height, width) out_raster.write(out_array, window=newbigwindow) else: in_array = in_raster.read() out_array = self.__gaussian_blur(in_array, size) if edge: out_array = out_array = np.subtract( in_array, out_array) out_raster.write(out_array) return self.__load_layer(out_path)
class FFTConvolution: """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 = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', 'FFTConvolution_{}.qm'.format(locale)) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) if qVersion() > '4.3.3': QCoreApplication.installTranslator(self.translator) # Create the dialog (after translation) and keep reference self.dlg = FFTConvolutionDialog() # Declare instance attributes self.actions = [] self.menu = self.tr(u'&FFT COnvolution Filters') # TODO: We are going to let the user set this up in a future iteration self.toolbar = self.iface.addToolBar(u'FFTConvolution') self.toolbar.setObjectName(u'FFTConvolution') self.dlg.output_file.clear() self.dlg.output_file_button.clicked.connect(self.select_output_file) # 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 QCoreApplication.translate('FFTConvolution', 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 = QIcon(icon_path) action = 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.addPluginToRasterMenu( 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/FFTConvolution/icon.png' self.add_action( icon_path, text=self.tr(u'FFT Convolution filters'), callback=self.run, parent=self.iface.mainWindow()) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" for action in self.actions: self.iface.removePluginRasterMenu( self.tr(u'&FFT COnvolution Filters'), action) self.iface.removeToolBarIcon(action) # remove the toolbar del self.toolbar def select_output_file(self): filename = QFileDialog.getSaveFileName(self.dlg, "Select output file ","", '*.tif') self.dlg.output_file.setText(filename) def run(self): """Run method that performs all the real work""" # show the dialog self.dlg.show() # Run the dialog event loop result = self.dlg.exec_() # See if OK was pressed if result: if self.dlg.smoothing.isChecked(): edge = False else: edge = True #call the function linking to real work #the input is translated from the GUI input to correct format here self.fft_convolution( in_layer=self.dlg.input_layer.currentLayer(), out_path=self.dlg.output_file.text(), size=self.dlg.size.text(), edge=edge, new_crs=self.dlg.crs.crs(), tiled=self.dlg.window.isChecked(), tilerows=self.dlg.window_rows.text(), tilecols=self.dlg.window_cols.text(), add_layer=self.dlg.check_add.isChecked() ) #this function parses the arguments, calls the appropriate functions and displays the new layer if needed def fft_convolution( self, in_layer, out_path, size=10, edge=False, new_crs=None, tiled=False, tilerows=0, tilecols=0, add_layer=True ): #if the file has no extension, add '.tif' ext = os.path.splitext(out_path)[-1].lower() if ext == '': out_path = out_path + '.tif' #if the file already exists, ask the user if os.path.isfile(out_path): reply = QMessageBox.question( None,'File exists!','File exists - overwite it?', QMessageBox.Yes, QMessageBox.No ) if reply == QMessageBox.No: return False #we need the CRS as EPSG code, or None if invalid if new_crs.isValid(): new_crs = new_crs.authid() else: new_crs = None #preprocessing the input layer's path in_path = in_layer.dataProvider().dataSourceUri() #QMessageBox.information(None, "DEBUG:", str(in_path)) if in_path.find('=') > -1: QMessageBox.information(None, "Sorry!", "WMS support wasn't implemented yet!") return False #the main computation layer = self.gaussian_filter( in_path = in_path, out_path=out_path, size = int(re.sub(r"\D", "", size)), edge=edge, tiled = tiled, tilerows = tilerows, tilecols = tilecols, new_crs = new_crs ) if add_layer: QgsMapLayerRegistry.instance().addMapLayers([layer]) qgis.utils.iface.mapCanvas().refresh() #returns number of array dimensions def __array_rank(self, arr): return len(arr.shape) #loads the newly created layer def __load_layer(self, path): fileName = path fileInfo = QFileInfo(fileName) baseName = fileInfo.baseName() rlayer = QgsRasterLayer(fileName, baseName) if not rlayer.isValid(): raise Exception("Computation finished, but layer failed to load. Inspect the path zou specified for the output layer.") return rlayer #pads the window to process its original extend accurately def __extend_window(self, window,size,height,width): minrow = max(window[0][0]-size,0) maxrow = min(window[0][1]+size,height) mincol = max(window[1][0]-size,0) maxcol = min(window[1][1]+size,width) return ((minrow,maxrow),(mincol,maxcol)) #creates a window generator, in the same format as it is returned by block_windows method def __generate_windows(self, height, width, tilerows, tilecols): rownum = int(math.ceil(float(height)/tilerows)) colnum = int(math.ceil(float(width)/tilecols)) for i in range(rownum): #last row's and column's dimensions are computed by modulo - they are smaller than regular tiles if i == rownum-1: rowsize = height%tilerows else: rowsize = tilerows for j in range(colnum): if j == colnum-1: colsize = width%tilecols else: colsize = tilecols cell = ((i,j),((i*tilerows,i*tilerows+rowsize),(j*tilecols,j*tilecols+colsize))) yield cell #like __generate_windows(), but returns an array def __generate_window_array(self, height, width, tilerows, tilecols): rownum = int(math.ceil(float(height)/tilerows)) colnum = int(math.ceil(float(width)/tilecols)) windows = np.asarray(np.zeros((height, width),dtype=object)) for i in range(rownum): #last row's and column's dimensions are computed by modulo - they are smaller than regular tiles if i == rownum-1: rowsize = height%tilerows else: rowsize = tilerows for j in range(colnum): if j == colnum-1: colsize = width%tilecols else: colsize = tilecols windows[i][j]=((i*tilerows,i*tilerows+rowsize),(j*tilecols,j*tilecols+colsize)) return windows #processes the window parameters #returns the windows as a generator or an array (specified in the generator parameter) def __compute_windows(self, in_raster, height, width, size, tilerows=0, tilecols=0, generator=True): #input validation size = int(size) try: tilerows = int(tilerows) except ValueError: tilerows = 0 try: tilecols = int(tilecols) except ValueError: tilecols = 0 #when raster's dimensions are modified due to reprojection, we must adjust the windows as well #this algorithm is quick'n'dirty - we just sompute one ratio and make all tiles larger/smaller #reprojecting each tile node would be better - perhaps in some future version if height != in_raster.height or width != in_raster.width: hratio = float(height)/in_raster.height wratio = float(width)/in_raster.width else: hratio = 1 wratio = 1 windows = [] #if only one of tile's dimension was set, we assume a square if tilecols == 0 and tilerows > 0: tilecols = tilerows elif tilerows == 0 and tilecols > 0: tilerows = tilecols #if tiles are too small (including default 0 length), we make them automatically #"2*size" is the total padding length and also an arbitrarily chosen minimum #"size" would be a minimum to get accurate tiles, but this way only 1/9 of the tile would be useful => inefficient #"2*size" means at least 50% efficiency if min(tilerows, tilecols) <= 2*size: #if the raster has blocks and they are big enough, we use them blocks = in_raster.block_shapes block = blocks[0] if min(block[0],block[1]) > 2*size: #if we compute the original raster, use the block as-is #otherwise use the dimensions and continue if hratio==1 and wratio==1: return in_raster.block_windows(1) else: tilerows = block[0] tilecols = block[1] else: #"2*size + 100" is an arbitrary constant #it's quite efficient on smaller rasters and shouldn't make any memory issues #really small rasters shouldn't be computed by tiles anyway tilerows = 2*size + 100 tilecols = 2*size + 100 #we transform the dimensions if needed tilerows = int(hratio * tilerows) tilecols = int(wratio * tilecols) #if the tiles are too big (more than half of any dimension of the raster), #we switch to the untiled algorithm if 2*tilerows >= height or 2*tilecols >= width: return False #if windows are not read from the raster, we must make them if generator: windows = self.__generate_windows(height, width, tilerows, tilecols) else: windows = self.__generate_window_array(height, width, tilerows, tilecols) return windows #computes the affine transformation, raster dimensions and metadata for the new raster def __compute_transform(self, in_raster, new_crs): affine, width, height = calculate_default_transform( in_raster.crs, new_crs, in_raster.width, in_raster.height, *in_raster.bounds ) kwargs = in_raster.meta.copy() kwargs.update({ 'driver':'GTiff', 'crs': new_crs, 'transform': affine, 'affine': affine, 'width': width, 'height': height }) return affine, height, width, kwargs #calls reproject() function for every band def __reproject(self, in_raster, out_raster, affine, new_crs): for k in range(1, in_raster.count + 1): reproject( source=rasterio.band(in_raster, k), destination=rasterio.band(out_raster, k), src_transform=affine, src_crs=in_raster.crs, dst_transform=affine, dst_crs=new_crs, resampling=RESAMPLING.nearest) return out_raster #the original MikeT's function from http://gis.stackexchange.com/a/10467 #my addition is the conversion of the padded array to float to avoid errors with integer rasters #the intended input is a single band raster array def __gaussian_blur1d(self, in_array, size): #check validity try: if 0 in in_array.shape: raise Exception("Null array can't be processed!") except TypeError: raise Exception("Null array can't be processed!") # expand in_array to fit edge of kernel padded_array = np.pad(in_array, size, 'symmetric').astype(float) # build kernel x, y = np.mgrid[-size:size + 1, -size:size + 1] g = np.exp(-(x**2 / float(size) + y**2 / float(size))) g = (g / g.sum()).astype(float) # do the Gaussian blur out_array = fftconvolve(padded_array, g, mode='valid') return out_array.astype(in_array.dtype) #the intended input is an array with various number of bands #returns a numpy array def __gaussian_blur(self, in_array, size): #make sure the input is a numpy array in_array = np.asarray(in_array) #find number of dimensions - 2 for single-band or 3 for multiband rasters rank = self.__array_rank(in_array) if rank == 2: return self.__gaussian_blur1d(in_array, size).astype(in_array.dtype) elif rank > 3 or rank == 1: raise TypeError("Invalid number of dimensions!") #continue to multiband count = in_array.shape[0] out = [] for i in range(count): band = in_array[i] out_band = self.__gaussian_blur1d(band, size)#.astype(in_array.dtype) #if out_band != False: out.append( out_band ) return np.asarray(out) #the real work is done here #filters a raster specified by the file's path (in_path) and writes it to another file (out_path) def gaussian_filter(self, in_path, out_path, size, edge=False, tiled=False, tilerows=0, tilecols=0, new_crs=None): with rasterio.drivers(): with rasterio.open(in_path,'r') as in_raster: if new_crs == None: new_crs = in_raster.crs affine, height, width, kwargs = self.__compute_transform(in_raster, new_crs) if tiled: #we make two sets of tiles, for the old and the new raster #this is important in case of reprojection old_windows = self.__compute_windows( in_raster=in_raster, height=in_raster.height, width=in_raster.width, size=size, tilerows=tilerows, tilecols=tilecols ) #windows for the new raster are made in two steps: generator and array new_windows = self.__compute_windows( in_raster=in_raster, height=height, width=width, size=size, tilerows=tilerows, tilecols=tilecols, generator=False ) #if windows are too big or invalid, we process the raster without them try: iter(old_windows) iter(new_windows) except TypeError: tiled = False with rasterio.open(out_path,'w',**kwargs) as out_raster: out_raster = self.__reproject(in_raster, out_raster, affine, new_crs) if tiled: for index, window in old_windows: oldbigwindow = self.__extend_window(window,size,in_raster.height,in_raster.width) in_array = in_raster.read(window=oldbigwindow) out_array = self.__gaussian_blur(in_array, size) #for edge detection we subtract the output array from the original #this may produce some artifacts when the raster is reprojected #or extensive and with degree coordinates if edge: out_array = np.subtract(in_array, out_array) #now compute the window for writing into the new raster nwindow = new_windows[index[0]][index[1]] newbigwindow = self.__extend_window(nwindow,size,height,width) out_raster.write(out_array,window=newbigwindow) else: in_array = in_raster.read() out_array = self.__gaussian_blur(in_array, size) if edge: out_array = out_array = np.subtract(in_array, out_array) out_raster.write(out_array) return self.__load_layer(out_path)