Commit 1edf1375 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Removed all dependencies on scipy.

Merge branch 'make_scipy_optional' into develop
parents 00418e49 34802c8a
......@@ -23,6 +23,7 @@ pyqtgraph-0.9.9 [unreleased]
- Added ViewBox.setLimits() method
- Adde ImageItem downsampling
- New HDF5 example for working with very large datasets
- Removed all dependency on scipy
- Added Qt.loadUiType function for PySide
- Simplified Profilers; can be activated with environmental variables
- Added Dock.raiseDock() method
......
......@@ -11,7 +11,6 @@ a 2D plane and interpolate data along that plane to generate a slice image
import initExample
import numpy as np
import scipy
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
......
......@@ -12,7 +12,6 @@ from pyqtgraph.flowchart.library.common import CtrlNode
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
import numpy as np
import scipy.ndimage
app = QtGui.QApplication([])
......@@ -44,7 +43,7 @@ win.show()
## generate random input data
data = np.random.normal(size=(100,100))
data = 25 * scipy.ndimage.gaussian_filter(data, (5,5))
data = 25 * pg.gaussianFilter(data, (5,5))
data += np.random.normal(size=(100,100))
data[40:60, 40:60] += 15.0
data[30:50, 30:50] += 15.0
......@@ -90,7 +89,7 @@ class ImageViewNode(Node):
## CtrlNode is just a convenience class that automatically creates its
## control widget based on a simple data structure.
class UnsharpMaskNode(CtrlNode):
"""Return the input data passed through scipy.ndimage.gaussian_filter."""
"""Return the input data passed through pg.gaussianFilter."""
nodeName = "UnsharpMask"
uiTemplate = [
('sigma', 'spin', {'value': 1.0, 'step': 1.0, 'range': [0.0, None]}),
......@@ -110,7 +109,7 @@ class UnsharpMaskNode(CtrlNode):
# CtrlNode has created self.ctrls, which is a dict containing {ctrlName: widget}
sigma = self.ctrls['sigma'].value()
strength = self.ctrls['strength'].value()
output = dataIn - (strength * scipy.ndimage.gaussian_filter(dataIn, (sigma,sigma)))
output = dataIn - (strength * pg.gaussianFilter(dataIn, (sigma,sigma)))
return {'dataOut': output}
......
......@@ -12,7 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
import pyqtgraph as pg
import numpy as np
import scipy.ndimage as ndi
app = QtGui.QApplication([])
w = gl.GLViewWidget()
......@@ -22,8 +21,8 @@ w.setWindowTitle('pyqtgraph example: GLImageItem')
## create volume data set to slice three images from
shape = (100,100,70)
data = ndi.gaussian_filter(np.random.normal(size=shape), (4,4,4))
data += ndi.gaussian_filter(np.random.normal(size=shape), (15,15,15))*15
data = pg.gaussianFilter(np.random.normal(size=shape), (4,4,4))
data += pg.gaussianFilter(np.random.normal(size=shape), (15,15,15))*15
## slice out three planes, convert to RGBA for OpenGL texture
levels = (-0.08, 0.08)
......
......@@ -10,7 +10,6 @@ import initExample
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
import pyqtgraph.opengl as gl
import scipy.ndimage as ndi
import numpy as np
## Create a GL View widget to display data
......@@ -29,7 +28,7 @@ w.addItem(g)
## Simple surface plot example
## x, y values are not specified, so assumed to be 0:50
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
z = pg.gaussianFilter(np.random.normal(size=(50,50)), (1,1))
p1 = gl.GLSurfacePlotItem(z=z, shader='shaded', color=(0.5, 0.5, 1, 1))
p1.scale(16./49., 16./49., 1.0)
p1.translate(-18, 2, 0)
......@@ -46,7 +45,7 @@ w.addItem(p2)
## Manually specified colors
z = ndi.gaussian_filter(np.random.normal(size=(50,50)), (1,1))
z = pg.gaussianFilter(np.random.normal(size=(50,50)), (1,1))
x = np.linspace(-12, 12, 50)
y = np.linspace(-12, 12, 50)
colors = np.ones((50,50,4), dtype=float)
......
......@@ -7,7 +7,6 @@ Use a HistogramLUTWidget to control the contrast / coloration of an image.
import initExample
import numpy as np
import scipy.ndimage as ndi
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
......@@ -34,7 +33,7 @@ l.addWidget(v, 0, 0)
w = pg.HistogramLUTWidget()
l.addWidget(w, 0, 1)
data = ndi.gaussian_filter(np.random.normal(size=(256, 256)), (20, 20))
data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20))
for i in range(32):
for j in range(32):
data[i*8, j*8] += .1
......
......@@ -14,7 +14,6 @@ displaying and analyzing 2D and 3D data. ImageView provides:
import initExample
import numpy as np
import scipy
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph as pg
......@@ -29,7 +28,7 @@ win.show()
win.setWindowTitle('pyqtgraph example: ImageView')
## Create random 3D data set with noisy signals
img = scipy.ndimage.gaussian_filter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100
img = pg.gaussianFilter(np.random.normal(size=(200, 200)), (5, 5)) * 20 + 100
img = img[np.newaxis,:,:]
decay = np.exp(-np.linspace(0,0.3,100))[:,np.newaxis,np.newaxis]
data = np.random.normal(size=(100, 200, 200))
......
......@@ -13,7 +13,6 @@ import initExample ## Add path to library (just for examples; you do not need th
from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE
import numpy as np
import pyqtgraph as pg
import scipy.ndimage as ndi
import pyqtgraph.ptime as ptime
if USE_PYSIDE:
......@@ -95,10 +94,11 @@ def mkData():
if ui.rgbCheck.isChecked():
data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale)
data = ndi.gaussian_filter(data, (0, 6, 6, 0))
data = pg.gaussianFilter(data, (0, 6, 6, 0))
else:
data = np.random.normal(size=(frames,width,height), loc=loc, scale=scale)
data = ndi.gaussian_filter(data, (0, 6, 6))
data = pg.gaussianFilter(data, (0, 6, 6))
pg.image(data)
if dtype[0] != 'float':
data = np.clip(data, 0, mx)
data = data.astype(dt)
......
......@@ -14,7 +14,6 @@ import initExample
## This example uses a ViewBox to create a PlotWidget-like interface
#from scipy import random
import numpy as np
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg
......
......@@ -7,7 +7,6 @@ the mouse.
import initExample ## Add path to library (just for examples; you do not need this)
import numpy as np
import scipy.ndimage as ndi
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
from pyqtgraph.Point import Point
......@@ -33,8 +32,8 @@ p1.setAutoVisible(y=True)
#create numpy arrays
#make the numbers large to show that the xrange shows data from 10000 to all the way 0
data1 = 10000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
data2 = 15000 + 15000 * ndi.gaussian_filter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
data1 = 10000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
data2 = 15000 + 15000 * pg.gaussianFilter(np.random.random(size=10000), 10) + 3000 * np.random.random(size=10000)
p1.plot(data1, pen="r")
p1.plot(data2, pen="g")
......
......@@ -10,7 +10,6 @@ import initExample ## Add path to library (just for examples; you do not need th
from pyqtgraph.Qt import QtGui, QtCore
import numpy as np
import pyqtgraph as pg
import scipy.ndimage as ndi
app = QtGui.QApplication([])
......@@ -18,7 +17,7 @@ app = QtGui.QApplication([])
frames = 200
data = np.random.normal(size=(frames,30,30), loc=0, scale=100)
data = np.concatenate([data, data], axis=0)
data = ndi.gaussian_filter(data, (10, 10, 10))[frames/2:frames + frames/2]
data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2]
data[:, 15:16, 15:17] += 1
win = pg.GraphicsWindow()
......
......@@ -4,7 +4,6 @@ from .Vector import Vector
from .Transform3D import Transform3D
from .Vector import Vector
import numpy as np
import scipy.linalg
class SRTTransform3D(Transform3D):
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
......@@ -118,11 +117,13 @@ class SRTTransform3D(Transform3D):
The input matrix must be affine AND have no shear,
otherwise the conversion will most likely fail.
"""
import numpy.linalg
for i in range(4):
self.setRow(i, m.row(i))
m = self.matrix().reshape(4,4)
## translation is 4th column
self._state['pos'] = m[:3,3]
self._state['pos'] = m[:3,3]
## scale is vector-length of first three columns
scale = (m[:3,:3]**2).sum(axis=0)**0.5
## see whether there is an inversion
......@@ -132,9 +133,9 @@ class SRTTransform3D(Transform3D):
self._state['scale'] = scale
## rotation axis is the eigenvector with eigenvalue=1
r = m[:3, :3] / scale[:, np.newaxis]
r = m[:3, :3] / scale[np.newaxis, :]
try:
evals, evecs = scipy.linalg.eig(r)
evals, evecs = numpy.linalg.eig(r)
except:
print("Rotation matrix: %s" % str(r))
print("Scale: %s" % str(scale))
......
import numpy as np
import scipy.interpolate
from .Qt import QtGui, QtCore
class ColorMap(object):
......@@ -64,8 +63,8 @@ class ColorMap(object):
ignored. By default, the mode is entirely RGB.
=============== ==============================================================
"""
self.pos = pos
self.color = color
self.pos = np.array(pos)
self.color = np.array(color)
if mode is None:
mode = np.ones(len(pos))
self.mode = mode
......@@ -92,15 +91,24 @@ class ColorMap(object):
else:
pos, color = self.getStops(mode)
data = np.clip(data, pos.min(), pos.max())
# don't need this--np.interp takes care of it.
#data = np.clip(data, pos.min(), pos.max())
if not isinstance(data, np.ndarray):
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0]
# Interpolate
# TODO: is griddata faster?
# interp = scipy.interpolate.griddata(pos, color, data)
if np.isscalar(data):
interp = np.empty((color.shape[1],), dtype=color.dtype)
else:
interp = scipy.interpolate.griddata(pos, color, data)
if mode == self.QCOLOR:
if not isinstance(data, np.ndarray):
data = np.array(data)
interp = np.empty(data.shape + (color.shape[1],), dtype=color.dtype)
for i in range(color.shape[1]):
interp[...,i] = np.interp(data, pos, color[:,i])
# Convert to QColor if requested
if mode == self.QCOLOR:
if np.isscalar(data):
return QtGui.QColor(*interp)
else:
return [QtGui.QColor(*x) for x in interp]
......
......@@ -399,7 +399,9 @@ class Profiler(object):
only the initial "pyqtgraph." prefix from the module.
"""
_profilers = os.environ.get("PYQTGRAPHPROFILE", "")
_profilers = os.environ.get("PYQTGRAPHPROFILE", None)
_profilers = _profilers.split(",") if _profilers is not None else []
_depth = 0
_msgs = []
......@@ -415,38 +417,36 @@ class Profiler(object):
_disabledProfiler = DisabledProfiler()
if _profilers:
_profilers = _profilers.split(",")
def __new__(cls, msg=None, disabled='env', delayed=True):
"""Optionally create a new profiler based on caller's qualname.
"""
if disabled is True:
return cls._disabledProfiler
# determine the qualified name of the caller function
caller_frame = sys._getframe(1)
try:
caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
else: # we are in a method
qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_name
if func_qualname not in cls._profilers: # don't do anything
return cls._disabledProfiler
# create an actual profiling object
cls._depth += 1
obj = super(Profiler, cls).__new__(cls)
obj._name = msg or func_qualname
obj._delayed = delayed
obj._markCount = 0
obj._finished = False
obj._firstTime = obj._lastTime = ptime.time()
obj._newMsg("> Entering " + obj._name)
return obj
else:
def __new__(cls, delayed=True):
return lambda msg=None: None
def __new__(cls, msg=None, disabled='env', delayed=True):
"""Optionally create a new profiler based on caller's qualname.
"""
if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
return cls._disabledProfiler
# determine the qualified name of the caller function
caller_frame = sys._getframe(1)
try:
caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
else: # we are in a method
qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_name
if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
return cls._disabledProfiler
# create an actual profiling object
cls._depth += 1
obj = super(Profiler, cls).__new__(cls)
obj._name = msg or func_qualname
obj._delayed = delayed
obj._markCount = 0
obj._finished = False
obj._firstTime = obj._lastTime = ptime.time()
obj._newMsg("> Entering " + obj._name)
return obj
#else:
#def __new__(cls, delayed=True):
#return lambda msg=None: None
def __call__(self, msg=None):
"""Register or print a new message with timing information.
......
# -*- coding: utf-8 -*-
from ...Qt import QtCore, QtGui
from ..Node import Node
from scipy.signal import detrend
from scipy.ndimage import median_filter, gaussian_filter
#from ...SignalProxy import SignalProxy
from . import functions
from ... import functions as pgfn
from .common import *
import numpy as np
......@@ -119,7 +117,11 @@ class Median(CtrlNode):
@metaArrayWrapper
def processData(self, data):
return median_filter(data, self.ctrls['n'].value())
try:
import scipy.ndimage
except ImportError:
raise Exception("MedianFilter node requires the package scipy.ndimage.")
return scipy.ndimage.median_filter(data, self.ctrls['n'].value())
class Mode(CtrlNode):
"""Filters data by taking the mode (histogram-based) of a sliding window"""
......@@ -156,7 +158,11 @@ class Gaussian(CtrlNode):
@metaArrayWrapper
def processData(self, data):
return gaussian_filter(data, self.ctrls['sigma'].value())
try:
import scipy.ndimage
except ImportError:
raise Exception("GaussianFilter node requires the package scipy.ndimage.")
return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
class Derivative(CtrlNode):
......@@ -189,6 +195,10 @@ class Detrend(CtrlNode):
@metaArrayWrapper
def processData(self, data):
try:
from scipy.signal import detrend
except ImportError:
raise Exception("DetrendFilter node requires the package scipy.signal.")
return detrend(data)
......
import scipy
import numpy as np
from ...metaarray import MetaArray
......@@ -47,6 +46,11 @@ def downsample(data, n, axis=0, xvals='subsample'):
def applyFilter(data, b, a, padding=100, bidir=True):
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
and/or run the filter in both directions."""
try:
import scipy.signal
except ImportError:
raise Exception("applyFilter() requires the package scipy.signal.")
d1 = data.view(np.ndarray)
if padding > 0:
......@@ -67,6 +71,11 @@ def applyFilter(data, b, a, padding=100, bidir=True):
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
"""return data passed through bessel filter"""
try:
import scipy.signal
except ImportError:
raise Exception("besselFilter() requires the package scipy.signal.")
if dt is None:
try:
tvals = data.xvals('Time')
......@@ -85,6 +94,11 @@ def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
def butterworthFilter(data, wPass, wStop=None, gPass=2.0, gStop=20.0, order=1, dt=None, btype='low', bidir=True):
"""return data passed through bessel filter"""
try:
import scipy.signal
except ImportError:
raise Exception("butterworthFilter() requires the package scipy.signal.")
if dt is None:
try:
tvals = data.xvals('Time')
......@@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4):
def adaptiveDetrend(data, x=None, threshold=3.0):
"""Return the signal with baseline removed. Discards outliers from baseline measurement."""
try:
import scipy.signal
except ImportError:
raise Exception("adaptiveDetrend() requires the package scipy.signal.")
if x is None:
x = data.xvals(0)
......
......@@ -34,17 +34,6 @@ import decimal, re
import ctypes
import sys, struct
try:
import scipy.ndimage
HAVE_SCIPY = True
if getConfigOption('useWeave'):
try:
import scipy.weave
except ImportError:
setConfigOptions(useWeave=False)
except ImportError:
HAVE_SCIPY = False
from . import debug
def siScale(x, minVal=1e-25, allowUnicode=True):
......@@ -383,7 +372,7 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
"""
Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data.
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates (see the scipy documentation for more information about this).
The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used.
For a graphical interface to this function, see :func:`ROI.getArrayRegion <pyqtgraph.ROI.getArrayRegion>`
......@@ -422,8 +411,12 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
"""
if not HAVE_SCIPY:
raise Exception("This function requires the scipy library, but it does not appear to be importable.")
try:
import scipy.ndimage
have_scipy = True
except ImportError:
have_scipy = False
have_scipy = False
# sanity check
if len(shape) != len(vectors):
......@@ -445,7 +438,6 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
#print "tr1:", tr1
## dims are now [(slice axes), (other axes)]
## make sure vectors are arrays
if not isinstance(vectors, np.ndarray):
vectors = np.array(vectors)
......@@ -461,12 +453,18 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
#print "X values:"
#print x
## iterate manually over unused axes since map_coordinates won't do it for us
extraShape = data.shape[len(axes):]
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
for inds in np.ndindex(*extraShape):
ind = (Ellipsis,) + inds
#print data[ind].shape, x.shape, output[ind].shape, output.shape
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
if have_scipy:
extraShape = data.shape[len(axes):]
output = np.empty(tuple(shape) + extraShape, dtype=data.dtype)
for inds in np.ndindex(*extraShape):
ind = (Ellipsis,) + inds
output[ind] = scipy.ndimage.map_coordinates(data[ind], x, order=order, **kargs)
else:
# map_coordinates expects the indexes as the first axis, whereas
# interpolateArray expects indexes at the last axis.
tr = tuple(range(1,x.ndim)) + (0,)
output = interpolateArray(data, x.transpose(tr))
tr = list(range(output.ndim))
trb = []
......@@ -483,6 +481,117 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False,
else:
return output
def interpolateArray(data, x, default=0.0):
"""
N-dimensional interpolation similar scipy.ndimage.map_coordinates.
This function returns linearly-interpolated values sampled from a regular
grid of data.
*data* is an array of any shape containing the values to be interpolated.
*x* is an array with (shape[-1] <= data.ndim) containing the locations
within *data* to interpolate.
Returns array of shape (x.shape[:-1] + data.shape)
For example, assume we have the following 2D image data::
>>> data = np.array([[1, 2, 4 ],
[10, 20, 40 ],
[100, 200, 400]])
To compute a single interpolated point from this data::
>>> x = np.array([(0.5, 0.5)])
>>> interpolateArray(data, x)
array([ 8.25])
To compute a 1D list of interpolated locations::
>>> x = np.array([(0.5, 0.5),
(1.0, 1.0),
(1.0, 2.0),
(1.5, 0.0)])
>>> interpolateArray(data, x)
array([ 8.25, 20. , 40. , 55. ])
To compute a 2D array of interpolated locations::
>>> x = np.array([[(0.5, 0.5), (1.0, 2.0)],
[(1.0, 1.0), (1.5, 0.0)]])
>>> interpolateArray(data, x)
array([[ 8.25, 40. ],
[ 20. , 55. ]])
..and so on. The *x* argument may have any shape as long as
```x.shape[-1] <= data.ndim```. In the case that
```x.shape[-1] < data.ndim```, then the remaining axes are simply
broadcasted as usual. For example, we can interpolate one location
from an entire row of the data::
>>> x = np.array([[0.5]])
>>> interpolateArray(data, x)
array([[ 5.5, 11. , 22. ]])
This is useful for interpolating from arrays of colors, vertexes, etc.
"""
prof = debug.Profiler()
result = np.empty(x.shape[:-1] + data.shape, dtype=data.dtype)
nd = data.ndim
md = x.shape[-1]
# First we generate arrays of indexes that are needed to
# extract the data surrounding each point
fields = np.mgrid[(slice(0,2),) * md]
xmin = np.floor(x).astype(int)
xmax = xmin + 1
indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]])
fieldInds = []