All scipy imports in the library are now optional (need to test each of these changes)
Several examples still require scipy.
This commit is contained in:
parent
00418e4921
commit
816069c020
@ -14,7 +14,6 @@ import initExample
|
|||||||
|
|
||||||
## This example uses a ViewBox to create a PlotWidget-like interface
|
## This example uses a ViewBox to create a PlotWidget-like interface
|
||||||
|
|
||||||
#from scipy import random
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from pyqtgraph.Qt import QtGui, QtCore
|
from pyqtgraph.Qt import QtGui, QtCore
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
@ -4,7 +4,6 @@ from .Vector import Vector
|
|||||||
from .Transform3D import Transform3D
|
from .Transform3D import Transform3D
|
||||||
from .Vector import Vector
|
from .Vector import Vector
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy.linalg
|
|
||||||
|
|
||||||
class SRTTransform3D(Transform3D):
|
class SRTTransform3D(Transform3D):
|
||||||
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
"""4x4 Transform matrix that can always be represented as a combination of 3 matrices: scale * rotate * translate
|
||||||
@ -118,6 +117,7 @@ class SRTTransform3D(Transform3D):
|
|||||||
The input matrix must be affine AND have no shear,
|
The input matrix must be affine AND have no shear,
|
||||||
otherwise the conversion will most likely fail.
|
otherwise the conversion will most likely fail.
|
||||||
"""
|
"""
|
||||||
|
import numpy.linalg
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
self.setRow(i, m.row(i))
|
self.setRow(i, m.row(i))
|
||||||
m = self.matrix().reshape(4,4)
|
m = self.matrix().reshape(4,4)
|
||||||
@ -134,7 +134,7 @@ class SRTTransform3D(Transform3D):
|
|||||||
## rotation axis is the eigenvector with eigenvalue=1
|
## rotation axis is the eigenvector with eigenvalue=1
|
||||||
r = m[:3, :3] / scale[:, np.newaxis]
|
r = m[:3, :3] / scale[:, np.newaxis]
|
||||||
try:
|
try:
|
||||||
evals, evecs = scipy.linalg.eig(r)
|
evals, evecs = numpy.linalg.eig(r)
|
||||||
except:
|
except:
|
||||||
print("Rotation matrix: %s" % str(r))
|
print("Rotation matrix: %s" % str(r))
|
||||||
print("Scale: %s" % str(scale))
|
print("Scale: %s" % str(scale))
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import scipy.interpolate
|
|
||||||
from .Qt import QtGui, QtCore
|
from .Qt import QtGui, QtCore
|
||||||
|
|
||||||
class ColorMap(object):
|
class ColorMap(object):
|
||||||
@ -84,6 +83,11 @@ class ColorMap(object):
|
|||||||
qcolor Values are returned as an array of QColor objects.
|
qcolor Values are returned as an array of QColor objects.
|
||||||
=========== ===============================================================
|
=========== ===============================================================
|
||||||
"""
|
"""
|
||||||
|
try:
|
||||||
|
import scipy.interpolate
|
||||||
|
except:
|
||||||
|
raise Exception("Colormap.map() requires the package scipy.interpolate, but it could not be imported.")
|
||||||
|
|
||||||
if isinstance(mode, basestring):
|
if isinstance(mode, basestring):
|
||||||
mode = self.enumMap[mode.lower()]
|
mode = self.enumMap[mode.lower()]
|
||||||
|
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from ...Qt import QtCore, QtGui
|
from ...Qt import QtCore, QtGui
|
||||||
from ..Node import Node
|
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
|
||||||
from .common import *
|
from .common import *
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -119,7 +116,11 @@ class Median(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
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):
|
class Mode(CtrlNode):
|
||||||
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
"""Filters data by taking the mode (histogram-based) of a sliding window"""
|
||||||
@ -156,7 +157,11 @@ class Gaussian(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
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 scipy.ndimage.gaussian_filter(data, self.ctrls['sigma'].value())
|
||||||
|
|
||||||
|
|
||||||
class Derivative(CtrlNode):
|
class Derivative(CtrlNode):
|
||||||
@ -189,6 +194,10 @@ class Detrend(CtrlNode):
|
|||||||
|
|
||||||
@metaArrayWrapper
|
@metaArrayWrapper
|
||||||
def processData(self, data):
|
def processData(self, data):
|
||||||
|
try:
|
||||||
|
from scipy.signal import detrend
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("DetrendFilter node requires the package scipy.signal.")
|
||||||
return detrend(data)
|
return detrend(data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import scipy
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from ...metaarray import MetaArray
|
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):
|
def applyFilter(data, b, a, padding=100, bidir=True):
|
||||||
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
"""Apply a linear filter with coefficients a, b. Optionally pad the data before filtering
|
||||||
and/or run the filter in both directions."""
|
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)
|
d1 = data.view(np.ndarray)
|
||||||
|
|
||||||
if padding > 0:
|
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):
|
def besselFilter(data, cutoff, order=1, dt=None, btype='low', bidir=True):
|
||||||
"""return data passed through bessel filter"""
|
"""return data passed through bessel filter"""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("besselFilter() requires the package scipy.signal.")
|
||||||
|
|
||||||
if dt is None:
|
if dt is None:
|
||||||
try:
|
try:
|
||||||
tvals = data.xvals('Time')
|
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):
|
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"""
|
"""return data passed through bessel filter"""
|
||||||
|
try:
|
||||||
|
import scipy.signal
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("butterworthFilter() requires the package scipy.signal.")
|
||||||
|
|
||||||
if dt is None:
|
if dt is None:
|
||||||
try:
|
try:
|
||||||
tvals = data.xvals('Time')
|
tvals = data.xvals('Time')
|
||||||
@ -175,6 +189,11 @@ def denoise(data, radius=2, threshold=4):
|
|||||||
|
|
||||||
def adaptiveDetrend(data, x=None, threshold=3.0):
|
def adaptiveDetrend(data, x=None, threshold=3.0):
|
||||||
"""Return the signal with baseline removed. Discards outliers from baseline measurement."""
|
"""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:
|
if x is None:
|
||||||
x = data.xvals(0)
|
x = data.xvals(0)
|
||||||
|
|
||||||
|
@ -34,17 +34,6 @@ import decimal, re
|
|||||||
import ctypes
|
import ctypes
|
||||||
import sys, struct
|
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
|
from . import debug
|
||||||
|
|
||||||
def siScale(x, minVal=1e-25, allowUnicode=True):
|
def siScale(x, minVal=1e-25, allowUnicode=True):
|
||||||
@ -422,7 +411,9 @@ 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))
|
affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3))
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
try:
|
||||||
|
import scipy.ndimage
|
||||||
|
except ImportError:
|
||||||
raise Exception("This function requires the scipy library, but it does not appear to be importable.")
|
raise Exception("This function requires the scipy library, but it does not appear to be importable.")
|
||||||
|
|
||||||
# sanity check
|
# sanity check
|
||||||
@ -579,15 +570,14 @@ def solve3DTransform(points1, points2):
|
|||||||
Find a 3D transformation matrix that maps points1 onto points2.
|
Find a 3D transformation matrix that maps points1 onto points2.
|
||||||
Points must be specified as a list of 4 Vectors.
|
Points must be specified as a list of 4 Vectors.
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
import numpy.linalg
|
||||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
|
||||||
A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)])
|
A = np.array([[points1[i].x(), points1[i].y(), points1[i].z(), 1] for i in range(4)])
|
||||||
B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)])
|
B = np.array([[points2[i].x(), points2[i].y(), points2[i].z(), 1] for i in range(4)])
|
||||||
|
|
||||||
## solve 3 sets of linear equations to determine transformation matrix elements
|
## solve 3 sets of linear equations to determine transformation matrix elements
|
||||||
matrix = np.zeros((4,4))
|
matrix = np.zeros((4,4))
|
||||||
for i in range(3):
|
for i in range(3):
|
||||||
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
||||||
|
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
@ -600,8 +590,7 @@ def solveBilinearTransform(points1, points2):
|
|||||||
|
|
||||||
mapped = np.dot(matrix, [x*y, x, y, 1])
|
mapped = np.dot(matrix, [x*y, x, y, 1])
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
import numpy.linalg
|
||||||
raise Exception("This function depends on the scipy library, but it does not appear to be importable.")
|
|
||||||
## A is 4 rows (points) x 4 columns (xy, x, y, 1)
|
## A is 4 rows (points) x 4 columns (xy, x, y, 1)
|
||||||
## B is 4 rows (points) x 2 columns (x, y)
|
## B is 4 rows (points) x 2 columns (x, y)
|
||||||
A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)])
|
A = np.array([[points1[i].x()*points1[i].y(), points1[i].x(), points1[i].y(), 1] for i in range(4)])
|
||||||
@ -610,7 +599,7 @@ def solveBilinearTransform(points1, points2):
|
|||||||
## solve 2 sets of linear equations to determine transformation matrix elements
|
## solve 2 sets of linear equations to determine transformation matrix elements
|
||||||
matrix = np.zeros((2,4))
|
matrix = np.zeros((2,4))
|
||||||
for i in range(2):
|
for i in range(2):
|
||||||
matrix[i] = scipy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
matrix[i] = numpy.linalg.solve(A, B[:,i]) ## solve Ax = B; x is one row of the desired transformation matrix
|
||||||
|
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
@ -629,6 +618,10 @@ def rescaleData(data, scale, offset, dtype=None):
|
|||||||
try:
|
try:
|
||||||
if not getConfigOption('useWeave'):
|
if not getConfigOption('useWeave'):
|
||||||
raise Exception('Weave is disabled; falling back to slower version.')
|
raise Exception('Weave is disabled; falling back to slower version.')
|
||||||
|
try:
|
||||||
|
import scipy.weave
|
||||||
|
except ImportError:
|
||||||
|
raise Exception('scipy.weave is not importable; falling back to slower version.')
|
||||||
|
|
||||||
## require native dtype when using weave
|
## require native dtype when using weave
|
||||||
if not data.dtype.isnative:
|
if not data.dtype.isnative:
|
||||||
@ -671,68 +664,13 @@ def applyLookupTable(data, lut):
|
|||||||
Uses values in *data* as indexes to select values from *lut*.
|
Uses values in *data* as indexes to select values from *lut*.
|
||||||
The returned data has shape data.shape + lut.shape[1:]
|
The returned data has shape data.shape + lut.shape[1:]
|
||||||
|
|
||||||
Uses scipy.weave to improve performance if it is available.
|
|
||||||
Note: color gradient lookup tables can be generated using GradientWidget.
|
Note: color gradient lookup tables can be generated using GradientWidget.
|
||||||
"""
|
"""
|
||||||
if data.dtype.kind not in ('i', 'u'):
|
if data.dtype.kind not in ('i', 'u'):
|
||||||
data = data.astype(int)
|
data = data.astype(int)
|
||||||
|
|
||||||
## using np.take appears to be faster than even the scipy.weave method and takes care of clipping as well.
|
|
||||||
return np.take(lut, data, axis=0, mode='clip')
|
return np.take(lut, data, axis=0, mode='clip')
|
||||||
|
|
||||||
### old methods:
|
|
||||||
#data = np.clip(data, 0, lut.shape[0]-1)
|
|
||||||
|
|
||||||
#try:
|
|
||||||
#if not USE_WEAVE:
|
|
||||||
#raise Exception('Weave is disabled; falling back to slower version.')
|
|
||||||
|
|
||||||
### number of values to copy for each LUT lookup
|
|
||||||
#if lut.ndim == 1:
|
|
||||||
#ncol = 1
|
|
||||||
#else:
|
|
||||||
#ncol = sum(lut.shape[1:])
|
|
||||||
|
|
||||||
### output array
|
|
||||||
#newData = np.empty((data.size, ncol), dtype=lut.dtype)
|
|
||||||
|
|
||||||
### flattened input arrays
|
|
||||||
#flatData = data.flatten()
|
|
||||||
#flatLut = lut.reshape((lut.shape[0], ncol))
|
|
||||||
|
|
||||||
#dataSize = data.size
|
|
||||||
|
|
||||||
### strides for accessing each item
|
|
||||||
#newStride = newData.strides[0] / newData.dtype.itemsize
|
|
||||||
#lutStride = flatLut.strides[0] / flatLut.dtype.itemsize
|
|
||||||
#dataStride = flatData.strides[0] / flatData.dtype.itemsize
|
|
||||||
|
|
||||||
### strides for accessing individual values within a single LUT lookup
|
|
||||||
#newColStride = newData.strides[1] / newData.dtype.itemsize
|
|
||||||
#lutColStride = flatLut.strides[1] / flatLut.dtype.itemsize
|
|
||||||
|
|
||||||
#code = """
|
|
||||||
|
|
||||||
#for( int i=0; i<dataSize; i++ ) {
|
|
||||||
#for( int j=0; j<ncol; j++ ) {
|
|
||||||
#newData[i*newStride + j*newColStride] = flatLut[flatData[i*dataStride]*lutStride + j*lutColStride];
|
|
||||||
#}
|
|
||||||
#}
|
|
||||||
#"""
|
|
||||||
#scipy.weave.inline(code, ['flatData', 'flatLut', 'newData', 'dataSize', 'ncol', 'newStride', 'lutStride', 'dataStride', 'newColStride', 'lutColStride'])
|
|
||||||
#newData = newData.reshape(data.shape + lut.shape[1:])
|
|
||||||
##if np.any(newData != lut[data]):
|
|
||||||
##print "mismatch!"
|
|
||||||
|
|
||||||
#data = newData
|
|
||||||
#except:
|
|
||||||
#if USE_WEAVE:
|
|
||||||
#debug.printExc("Error; disabling weave.")
|
|
||||||
#USE_WEAVE = False
|
|
||||||
#data = lut[data]
|
|
||||||
|
|
||||||
#return data
|
|
||||||
|
|
||||||
|
|
||||||
def makeRGBA(*args, **kwds):
|
def makeRGBA(*args, **kwds):
|
||||||
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
"""Equivalent to makeARGB(..., useRGBA=True)"""
|
||||||
@ -1473,7 +1411,11 @@ def traceImage(image, values, smooth=0.5):
|
|||||||
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
|
If image is RGB or RGBA, then the shape of values should be (nvals, 3/4)
|
||||||
The parameter *smooth* is expressed in pixels.
|
The parameter *smooth* is expressed in pixels.
|
||||||
"""
|
"""
|
||||||
import scipy.ndimage as ndi
|
try:
|
||||||
|
import scipy.ndimage as ndi
|
||||||
|
except ImportError:
|
||||||
|
raise Exception("traceImage() requires the package scipy.ndimage, but it is not importable.")
|
||||||
|
|
||||||
if values.ndim == 2:
|
if values.ndim == 2:
|
||||||
values = values.T
|
values = values.T
|
||||||
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
values = values[np.newaxis, np.newaxis, ...].astype(float)
|
||||||
@ -1967,14 +1909,16 @@ def invertQTransform(tr):
|
|||||||
bugs in that method. (specifically, Qt has floating-point precision issues
|
bugs in that method. (specifically, Qt has floating-point precision issues
|
||||||
when determining whether a matrix is invertible)
|
when determining whether a matrix is invertible)
|
||||||
"""
|
"""
|
||||||
if not HAVE_SCIPY:
|
try:
|
||||||
|
import numpy.linalg
|
||||||
|
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
|
||||||
|
inv = numpy.linalg.inv(arr)
|
||||||
|
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
||||||
|
except ImportError:
|
||||||
inv = tr.inverted()
|
inv = tr.inverted()
|
||||||
if inv[1] is False:
|
if inv[1] is False:
|
||||||
raise Exception("Transform is not invertible.")
|
raise Exception("Transform is not invertible.")
|
||||||
return inv[0]
|
return inv[0]
|
||||||
arr = np.array([[tr.m11(), tr.m12(), tr.m13()], [tr.m21(), tr.m22(), tr.m23()], [tr.m31(), tr.m32(), tr.m33()]])
|
|
||||||
inv = scipy.linalg.inv(arr)
|
|
||||||
return QtGui.QTransform(inv[0,0], inv[0,1], inv[0,2], inv[1,0], inv[1,1], inv[1,2], inv[2,0], inv[2,1])
|
|
||||||
|
|
||||||
|
|
||||||
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
||||||
|
@ -655,7 +655,10 @@ class PlotDataItem(GraphicsObject):
|
|||||||
dx = np.diff(x)
|
dx = np.diff(x)
|
||||||
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
|
uniform = not np.any(np.abs(dx-dx[0]) > (abs(dx[0]) / 1000.))
|
||||||
if not uniform:
|
if not uniform:
|
||||||
import scipy.interpolate as interp
|
try:
|
||||||
|
import scipy.interpolate as interp
|
||||||
|
except:
|
||||||
|
raise Exception('Fourier transform of irregularly-sampled data requires the package scipy.interpolate.')
|
||||||
x2 = np.linspace(x[0], x[-1], len(x))
|
x2 = np.linspace(x[0], x[-1], len(x))
|
||||||
y = interp.griddata(x, y, x2, method='linear')
|
y = interp.griddata(x, y, x2, method='linear')
|
||||||
x = x2
|
x = x2
|
||||||
|
@ -13,11 +13,8 @@ of how to build an ROI at the bottom of the file.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from ..Qt import QtCore, QtGui
|
from ..Qt import QtCore, QtGui
|
||||||
#if not hasattr(QtCore, 'Signal'):
|
|
||||||
#QtCore.Signal = QtCore.pyqtSignal
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy.linalg import norm
|
#from numpy.linalg import norm
|
||||||
import scipy.ndimage as ndimage
|
|
||||||
from ..Point import *
|
from ..Point import *
|
||||||
from ..SRTTransform import SRTTransform
|
from ..SRTTransform import SRTTransform
|
||||||
from math import cos, sin
|
from math import cos, sin
|
||||||
|
Loading…
Reference in New Issue
Block a user