Source code for vacumm.misc.filters

# -*- coding: utf8 -*-
"""Various 1d and 2D filters"""

# Copyright or © or Copr. Actimar/IFREMER (2010-2015)
#
# This software is a computer program whose purpose is to provide
# utilities for handling oceanographic and atmospheric data,
# with the ultimate goal of validating the MARS model from IFREMER.
#
# This software is governed by the CeCILL license under French law and
# abiding by the rules of distribution of free software.  You can  use,
# modify and/ or redistribute the software under the terms of the CeCILL
# license as circulated by CEA, CNRS and INRIA at the following URL
# "http://www.cecill.info".
#
# As a counterpart to the access to the source code and  rights to copy,
# modify and redistribute granted by the license, users are provided only
# with a limited warranty  and the software's author,  the holder of the
# economic rights,  and the successive licensors  have only  limited
# liability.
#
# In this respect, the user's attention is drawn to the risks associated
# with loading,  using,  modifying and/or developing or reproducing the
# software by the user in light of its specific status of free software,
# that may mean  that it is complicated to manipulate,  and  that  also
# therefore means  that it is reserved for developers  and  experienced
# professionals having in-depth computer knowledge. Users are therefore
# encouraged to load and test the software's suitability as regards their
# requirements in conditions enabling the security of their systems and/or
# data to be ensured and,  more generally, to use and operate it in the
# same conditions as regards security.
#
# The fact that you are presently reading this means that you have had
# knowledge of the CeCILL license and that you accept its terms.
#
__all__ = ['generic1d', 'shapiro1d', 'gaussian1d', 'hamming1d','generic2d', 'shapiro2d', 'gaussian2d', 'deriv', 'deriv2d',
    'norm_atan','running_average', 'bartlett1d', 'kaiser1d', 'hanning1d', 'blackman1d']
__all__.sort()

import numpy as N,MV2, cdms2
from genutil.filters import *
MA = N.ma
MV = MV2
cdms = cdms2
import scipy.signal
from scipy.signal import convolve2d

from misc import cp_atts
from phys.units import deg2m
from pylab import meshgrid
from axes import islon,islat
import warnings

try:
    from numpy import ComplexWarning
    warnings.filterwarnings('ignore', 'Casting complex values', ComplexWarning)
except ImportError: pass


[docs]def generic1d(data, weights, axis=0, mask='same', copy=True, cyclic=False): """Generic 1D filter applied to :mod:`MV2` variables using convolution. :Params: - **data**: Atleast 1D :mod:`MV2` variable. - **weights**: integer, 1D weights. They are expected to be symmetric and of odd sizes. - **axis**, optional: axis on which to operate - **mask**, optional: mode of masking. The mask is also filtered, and its value helps defining the new mask, depending on this parameter: - If a float is provided, data are masked if the mask value is greater than this parameter. - ``"minimal"``: Equivalent to ``1.``. Data is not masked if a valid value was used to compute data. - ``"maximal"``: Equivalent to ``0.``. Data is masked if a invalid value was used to compute data. - ``"same"``: Mask is the same as input mask. - **copy**, optional: Copy variable before filtering it. :Return: - The filtered :mod:`MV2` variable. :Example: >>> generic1d(data, 3.) # running mean using a 3-points block >>> generic1d(data, [1.,2.,1], axis=2) # shapiro filter on third axis :See also: :func:`scipy.signal.convolve2d` """ # Setup # - data data = MV2.asarray(data) assert data.ndim, 'Input data array must be at least 1D' if axis<0: axis += data.ndim if axis!=data.ndim-1: init_order = data.getOrder(ids=1) data = data.reorder('...%i'%axis) datan = data.filled(0.).copy() nx = datan.shape[-1] datan.shape = -1, nx # - weights if isinstance(weights, int): weights = N.ones(weights) else: weights = N.asarray(weights) assert weights.ndim, 'Input weights array must be at least 1D' assert weights.shape[-1] % 2 == 1, 'Shape of weights must be of odd size' ww = (~N.ma.getmaskarray(data)).astype('i') # = good 2D nw = weights.shape[-1] nw2 = weights.shape[-1] / 2 weights.shape = 1, -1 ww.shape = datan.shape if data.mask is not MV2.nomask: one2d = N.ones(datan.shape, 'i') # Cyclic case if cyclic: mode = 'valid' fdatan = N.concatenate((datan[:, -nw2:], datan, datan[:, :nw2]), axis=1) fww = N.concatenate((ww[:, -nw2:], ww, ww[:, :nw2]), axis=1) one1d = N.ones(datan.shape[-1]+nw2*2, 'i') else: mode = 'same' fdatan = datan fww = ww one1d = N.ones(datan.shape[-1], 'i') # Filter kwf = dict(mode=mode) datan = convolve2d(fdatan, weights, **kwf) one2d = N.resize(N.convolve(one1d, weights[0], **kwf), datan.shape) del one1d if data.mask is MV2.nomask: ww = one2d else: ww = convolve2d(fww, weights, **kwf) del fdatan, fww bad = ww==0 ww[bad] = 1 datan[:] = N.where(bad, datan, datan/ww.astype('d')) ww[bad] = 0 # Set if copy: datao = data.clone() else: datao = data datan.shape = data.shape bad.shape = data.shape if data.mask is not MV2.nomask: ww.shape = one2d.shape = data.shape if mask is 'same': bad = data.mask else: if mask == 'minimal': mask = 1. elif mask == 'maximal': mask = 0. else: mask = float(mask) bad |= (ww/one2d)<(1-mask) # threshold datao[:] = N.ma.masked_where(bad, datan, copy=False) del ww, one2d, bad else: datao[:] = datan if axis!=data.ndim-1: init_order = cdms2.order2index(datao.getAxisList(), init_order) return datao.reorder(init_order) return datao
[docs]def shapiro1d(data, **kwargs): """Shapiro (121) 1D filter :Params: - **data**: A :mod:`MV2` variable. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.array([1.,2.,1.],data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def hamming1d(data, M, **kwargs): """Hamming 1D filter :Params: - **data**: A :mod:`MV2` variable. - **M**: Size of the Hamming window. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.hamming(M).astype(data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def hanning1d(data, M, **kwargs): """Hanning 1D filter :Params: - **data**: A :mod:`MV2` variable. - **M**: Size of the Hanning window. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.hanning(M).astype(data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def bartlett1d(data, M, **kwargs): """Bartlett 1D filter :Params: - **data**: A :mod:`MV2` variable. - **M**: Size of the Bartlett window. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.bartlett(M).astype(data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def blackman1d(data, M, **kwargs): """Blackman 1D filter :Params: - **data**: A :mod:`MV2` variable. - **M**: Size of the Blackman window. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.blackman(M).astype(data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def kaiser1d(data, M, beta, **kwargs): """Kaiser 1D filter :Params: - **data**: A :mod:`MV2` variable. - **M**: Size of the Kaiser window. - **beta**: Shape of the window. - Keywords are passed to :func:`generic1d`. :Return: - A :mod:`MV2` variable """ weights = N.kaiser(M, beta).astype(data.dtype.char) return generic1d(data, weights, **kwargs)
[docs]def gaussian1d(data,nxw,**kwargs): """Gaussian 1D filter - **data**: Data array - **nxw**: Size of gaussian weights array along X - Other keywords are passed to :func:`generic1d` """ assert nxw % 2 == 1 , 'nxw must be an odd number' tc = data.dtype.char xx = N.arange(nxw)-nxw/2. return generic1d(data, N.exp(-xx**2/nxw**2),**kwargs)
[docs]def generic2d(data, weights, mask='same', copy=True): """Generic 2D filter applied to 2D (or more) :mod:`MV2` variables using convolution. :Params: - **data**: Atleast 2D :mod:`MV2` variable. - **weights**: integer, 2D weights. They are expected to be symmetric and of odd sizes. If an integer is provided, a ``(weights,weights)`` array of ones is used. - **mask**, optional: mode of masking. The mask is also filtered, and its value helps defining the new mask, depending on this parameter: - If a float is provided, data are masked if the mask value is greater than this parameter. - ``"minimal"``: Equivalent to ``1.``. Data is not masked if a valid value was used to compute data. - ``"maximal"``: Equivalent to ``0.``. Data is masked if a invalid value was used to compute data. - ``"same"``: Mask is the same as input mask. - **copy**, optional: Copy variable before filtering it. :Return: - The filtered :mod:`MV2` variable. :Example: >>> generic2d(data, 3.) # running mean using a 3x3 block >>> generic2d(data, N.ones(3,3)) # the same >>> generic2d(data, N.ones(3,3), weights, mode='minimal') # crop coasts :See also: :func:`scipy.signal.convolve2d` """ # Setup data = MV2.asarray(data) assert data.ndim>1, 'Input data array must be at least 2D' if isinstance(weights, int): weights = N.ones((weights, weights)) else: weights = N.asarray(weights) assert weights.ndim, 'Input weights array must be at least 2D' for i in -2,-1: assert weights.shape[i] % 2 == 1, \ 'Shape of weights must be of odd size in the two directions' ww = -N.ma.getmaskarray(data).astype('f')+1 datan = data.filled(0.).copy() if datan.ndim > 2: ny, nx = datan.shape[-2:] datan.shape = datan.size/(nx*ny), ny, nx elif datan.ndim == 2: datan.shape = (1,) + datan.shape[-2:] ww.shape = datan.shape one2d = N.ones(datan.shape[-2:]) # Filter kwf = dict(mode='same', boundary='fill', fillvalue=0.) for i in xrange(datan.shape[0]): # TODO: multiproc filter2d datan[i] = scipy.signal.convolve2d(datan[i], weights, **kwf) if data.mask is MV2.nomask: ww[i] = scipy.signal.convolve2d(one2d, weights, **kwf) else: ww[i] = scipy.signal.convolve2d(ww[i], weights, **kwf) if data.mask is not MV2.nomask: one3d = scipy.signal.convolve2d(one2d, weights, **kwf) one3d = N.resize(one3d, datan.shape) bad = ww==0 ww[bad]=1. datan[:] = N.where(bad, datan, datan/ww) ww[bad]=0 # del bad # Set if copy: datao = data.clone() else: datao = data datan.shape = data.shape if data.mask is not MV2.nomask: ww.shape = bad.shape = one3d.shape = data.shape # print 'mask crit', ww/one3d if mask is 'same': bad = data.mask else: if mask == 'minimal': mask = 1. elif mask == 'maximal': mask = 0. else: mask = float(mask) bad |= (ww/one3d) < (1-mask) datao[:] = N.ma.masked_where(bad, datan, copy=False) del ww, one3d else: datao[:] = datan del one2d return datao
def generic2d_old(data,weights,fast=False,fill_value=None,min_valid=0): """Generic 2D filter - **data**: 2D variable. - **weights**: Weights of the filter as 2D array of odd sizes. """ if not cdms.isVariable(data): data = MV.masked_array(data, data.mask) assert data.rank() == 2, 'You need a 2D data set' assert len(weights.shape) == 2, 'You need to apply 2D weights' for i in 0,1: assert weights.shape[i] % 2 == 1, \ 'Shape of weights must be of odd size in the two directions' weights = weights.astype(data.dtype.char) nyw,nxw = weights.shape dxw = nxw/2 dyw = nyw/2 ny,nx = data.shape data_filt = data.clone() if data.mask is MV.nomask: fast = True data_min = data.min() data_max = data.max() if fast: if fill_value is None: if data.getMissing() is None: data.setMissing(data.max()*1.e20) fill_value = data.getMissing() data_to_use = data.filled() else: data_to_use = data.filled(fill_value) mm = N else: data_to_use = data mm = MV for j in xrange(ny): yyrange = N.clip([j-dyw,j+dyw],0,ny-1) jminw = min(yyrange)-j+dyw+1 jmaxw = max(yyrange)-j+dyw+1 for i in xrange(nx): xxrange = N.clip([i-dxw,i+dxw],0,nx-1) this_data = data_to_use[yyrange[0]:yyrange[1],xxrange[0]:xxrange[1]] if min_valid and MV.count(this_data) < min_valid: if not fast: data_filt[j,i] = MV.masked else: data_filt[j,i] = fill_value continue iminw = min(xxrange)-i+dxw+1 imaxw = max(xxrange)-i+dxw+1 data_filt[j,i] = mm.average(this_data, weights = weights[jminw:jmaxw,iminw:imaxw]) if fast and data.mask is not MV.masked: data_filt[:] = MV.masked_greater(data_filt,data_max) data_filt[:] = MV.masked_less(data_filt,data_min) if data.mask is not MV.nomask: data_filt[:] = MV.masked_where(data.mask, data_filt, copy=0) return data_filt
[docs]def shapiro2d(data, corners=.5, **kwargs): """Shapiro (121) 2D filter :Params: - **data**: A :mod:`MV2` variable. - **corner**, optional: Value in (4) corners. - Keywords are passed to :func:`generic2d`. :Return: - A :mod:`MV2` variable """ weights = N.empty((3,3),data.dtype.char) weights[:] = corners weights[1,:] = [1.,2.,1.] weights[:,1] = [1.,2.,1.] return generic2d(data, weights, **kwargs)
def shapiro2d_old(data,**kwargs): """Shapiro (121) 2D filter - **data**: Data array - Keywords are passed to :func:`generic2d` """ ## print 'shap2d' weights = N.zeros((3,3),data.dtype.char) weights[1,:] = [1.,2.,1.] weights[:,1] = [1.,2.,1.] for j in 1,-1: for i in 1,-1: weights[j,i] = 0.5 shap = generic2d(data,weights,**kwargs) for i in 0, -1: shap[i] = data[i] shap[:, i] = data[:, i] return shap
[docs]def gaussian2d(data, nxw, nyw=None, sxw=1/3., syw=1/3., rmax=3., **kwargs): """Gaussian 2D filter - **data**: Data array - **nxw**: Size of gaussian weights array along X (and Y if nyw not given) - *nyw*: Size of gaussian weights array along Y [default: nxw] - *sxw*: Standard deviation of the gaussian distribution along X. If <1, its size is relative to nxw. If > 1, it is directly expressed in grid steps. - *syw*: Same as sxw for Y direction. - *rmax*: Distance relative to sqrt(sxw**2+syw**2) after with weights are nullified. - Other keywords are passed to :func:`generic2d` """ if nyw is None: nyw = nxw assert nxw % 2 == 1 and nyw % 2 == 1, 'nxw and nyw must be odd numbers' assert sxw > 0 and syw > 0, 'sxw and syw must be positive' xx,yy = meshgrid(N.arange(nxw)-nxw/2, N.arange(nyw)-nyw/2) if sxw < 1: sxw *= nxw/2 if syw < 1: syw *= nyw/2 weights = N.exp(-(xx**2/sxw**2 + yy**2/syw**2)) weights[weights<N.exp(-rmax**2)] = 0. return generic2d(data, weights, **kwargs)
[docs]def deriv(data, axis=0, fast=True, fill_value=None, physical=True, lat=None): """Derivative along a given axis - **data**: Data array (converted to MV array if needed) - *axis*: Axis on which the derivative is performed [default: 0] - *fast*: Filled masked array before derivating, so use Numeric which is faster than MA or MV [*WARNING* default: True] - *physical*: Try physical derivative, taking axis units into account [default: True] - *lat*: Latitude for geographical deriviative to convert positions in degrees to meters """ ## print 'deriv2d' data = MV.masked_array(data) # Reordering if needed if axis: init_order = data.getOrder(ids=1) data = data.reorder(str(axis)+'...') data_deriv = data.clone() data_deriv.id += '_deriv%i'%axis # cdms or Numeric variable to work on? if data.mask is MV.nomask: fast = True if fast: if fill_value is None: data_to_use = data.filled() else: data_to_use = data.filled(fill_value) else: data_to_use = data # Derivative data_deriv[1:-1] = data_to_use[2:]-data_to_use[:-2] data_deriv[0] = data_to_use[1]-data_to_use[0] data_deriv[-1] = data_to_use[-2]-data_to_use[-1] # if not fast: # for i in 0,-1: data_deriv[i] = MV.masked # Physical derivative if physical: pos = N.resize(data.getAxis(0)[:],data.shape[::-1]).transpose() if islon(data.getAxis(0)): if lat is None: for i,lataxis in enumerate(data.getAxisList()): if islat(lataxis): sh = list(data.shape) if i != data.ndim-1: olen = sh[-1] sh[-1] = len(lataxis) sh[i] = olen lat = N.swapaxes(N.resize(lataxis[:],sh),i,-1) else: lat = N.resize(lataxis[:],sh) break if islon(data.getAxis(0)) or islat(data.getAxis(0)): pos = deg2m(pos,lat) units = 'm-1' else: units = getattr(data.getAxis(0),'units',None) data_deriv[1:-1] /= (pos[2:]-pos[:-2]) data_deriv[0] /= (pos[1]-pos[0]) data_deriv[-1] /= (pos[-2]-pos[-1]) else: data_deriv[1:-1] = data_deriv[1:-1] * .5 units = None # Mask if fast: mask = MV.getmaskarray(data) dmask = mask.copy() dmask[1:-1] = N.logical_or(mask[2:],mask[:-2]) # for i in 0,-1: dmask[i] = 1 data_deriv[:] = MV.masked_array(data_deriv,mask=dmask) else: for i in 0,-1: data_deriv[i] = MV.masked # Reordering back if axis: init_order = cdms2.order2index(data_deriv.getAxisList(), init_order) data_deriv = data_deriv.reorder(init_order) # Units if units is not None: data_units = getattr(data_deriv,'units',None) if data_units is None: data_deriv.units = units else: data_deriv.units += ' '+units if data_deriv.units == 'm m-1': del data_deriv.units return data_deriv
[docs]def deriv2d(data,direction=None,**kwargs): """Derivative in a 2D space - **data**: 2D variable - *direction*: If not None, derivative is computed in this direction, else the module is returned [default: None] - Other keywords are passed to deriv() """ data = MV.masked_array(data) data_deriv = data.clone() data_deriv.id += '_deriv' assert data.ndim == 2, 'You need a 2D data set' xdata_deriv = deriv(data,1,**kwargs) ydata_deriv = deriv(data,0,**kwargs) ## print '2d der' if direction is None: data_deriv[:] = MV.sqrt(xdata_deriv**2+ydata_deriv**2) else: aa = direction*MV.pi/180. data_deriv[:] = xdata_deriv*MV.cos(aa) + ydata_deriv*MV.sin(aa) if hasattr(xdata_deriv,'units'): data_deriv.units = xdata_deriv.units elif hasattr(data_deriv,'units'): del data_deriv.units return data_deriv
[docs]def norm_atan(var,stretch=1.): """Normalize using arctan (arctan(strecth*var/std(var)) - *stretch*: If stretch close to 1, saturates values [default: 1] Return: Value in [-1,1] """ if cdms2.isVariable(var): var_norm = var.clone() var_norm.id += '_norm' mm = MV elif MA.isMA(var): var_norm = var.copy() mm = MA else: var_norm = N.array(var) mm = N std = var.std() var_norm[:] = mm.arctan(stretch*var/var.std())*2./N.pi return var_norm
[docs]def running_average(x, l, d = 0, w = None, keep_mask = True): """Perform a running average on an masked array. Average is linearly reduced near bounds, so that the input and output have the same size. :Params: - **x**: Masked array - **l**: Window size - *d*: Dimension over which the average is performed (0) - *w*: Weights (1...) - *keep_mask*: Apply mask from x to output array :Example: >>> running_average(x, l, d = 0, w = None, keep_mask = 1) Returns Average array (same size as x) .. warning:: This function deprecated. Please use :func:`generic1d` instead. """ s= x.shape if cdms.isVariable(x): AV = MV out = x.clone() else: AV = N out = N.zeros(s,x.dtype.char) keep_mask = False ns = len(s) if d > (ns-1) or d < 0: raise '[running_average] bad dimension', d n = s[d] #if w is None: # w = N.ones(n,'f') ii = [] io = [] for id in range(ns): if id == d: ii.append('i') io.append('imin:imax') else: ii.append(':') io.append(':') for i in range(n): imin = max(0, i-l/2) imax = min(n-1,i+l/2)+1 exec 'out['+','.join(ii)+'] = AV.average(x['+','.join(io)+'], d)' if keep_mask: out = MV.masked_array(out,mask=MV.getmask(x)) return out
#print 'importing filters 1' #def smooth1d(var): # # Smooth # varan = vara.filled() # try: # len(smooth) # except: # smooth = [1., 2., 1.] # vara[:] = N.convolve(varan, smooth, mode='same') # vara[:] /= N.convolve(N.ones(len(vara)), smooth, mode='same') # if vara.mask is not MV2.nomask: # mask = N.convolve(vara.mask.astype('f'), smooth, mode='same') # vara[:] = MV2.masked_where(mask!=0., vara, copy=0)