diff --git a/.gitignore b/.gitignore index 78309170..8a81fee3 100644 --- a/.gitignore +++ b/.gitignore @@ -107,3 +107,4 @@ rtr.cvs # ctags .tags* +.asv/ diff --git a/asv.conf.json b/asv.conf.json new file mode 100644 index 00000000..bb7d805d --- /dev/null +++ b/asv.conf.json @@ -0,0 +1,140 @@ +{ + // The version of the config file format. Do not change, unless + // you know what you are doing. + "version": 1, + + // The name of the project being benchmarked + "project": "pyqtgraph", + + // The project's homepage + "project_url": "http://pyqtgraph.org/", + + // The URL or local path of the source code repository for the + // project being benchmarked + "repo": ".", + + // List of branches to benchmark. If not provided, defaults to "master" + // (for git) or "default" (for mercurial). + "branches": ["master"], // for git + // "branches": ["default"], // for mercurial + + // The DVCS being used. If not set, it will be automatically + // determined from "repo" by looking at the protocol in the URL + // (if remote), or by looking for special directories, such as + // ".git" (if local). + // "dvcs": "git", + + // The tool to use to create environments. May be "conda", + // "virtualenv" or other value depending on the plugins in use. + // If missing or the empty string, the tool will be automatically + // determined by looking for tools on the PATH environment + // variable. + "environment_type": "conda", + + // timeout in seconds for installing any dependencies in environment + // defaults to 10 min + //"install_timeout": 600, + + // the base URL to show a commit for the project. + "show_commit_url": "http://github.com/pyqtgraph/pyqtgraph/commit/", + + // The Pythons you'd like to test against. If not provided, defaults + // to the current version of Python used to run `asv`. + "pythons": ["2.7", "3.8"], + + // The matrix of dependencies to test. Each key is the name of a + // package (in PyPI) and the values are version numbers. An empty + // list or empty string indicates to just test against the default + // (latest) version. null indicates that the package is to not be + // installed. If the package to be tested is only available from + // PyPi, and the 'environment_type' is conda, then you can preface + // the package name by 'pip+', and the package will be installed via + // pip (with all the conda available packages installed first, + // followed by the pip installed packages). + // + "matrix": { + "numpy": [], + "numba": [], + "pyqt": ["4", "5"], + }, + + // Combinations of libraries/python versions can be excluded/included + // from the set to test. Each entry is a dictionary containing additional + // key-value pairs to include/exclude. + // + // An exclude entry excludes entries where all values match. The + // values are regexps that should match the whole string. + // + // An include entry adds an environment. Only the packages listed + // are installed. The 'python' key is required. The exclude rules + // do not apply to includes. + // + // In addition to package names, the following keys are available: + // + // - python + // Python version, as in the *pythons* variable above. + // - environment_type + // Environment type, as above. + // - sys_platform + // Platform, as in sys.platform. Possible values for the common + // cases: 'linux2', 'win32', 'cygwin', 'darwin'. + // + "exclude": [ + {"python": "3.8", "pyqt": "4"}, + ], + // + // "include": [ + // // additional env for python2.7 + // {"python": "2.7", "numpy": "1.8"}, + // // additional env if run on windows+conda + // {"platform": "win32", "environment_type": "conda", "python": "2.7", "libpython": ""}, + // ], + + // The directory (relative to the current directory) that benchmarks are + // stored in. If not provided, defaults to "benchmarks" + "benchmark_dir": "benchmarks", + + // The directory (relative to the current directory) to cache the Python + // environments in. If not provided, defaults to "env" + "env_dir": ".asv/env", + + // The directory (relative to the current directory) that raw benchmark + // results are stored in. If not provided, defaults to "results". + "results_dir": ".asv/results", + + // The directory (relative to the current directory) that the html tree + // should be written to. If not provided, defaults to "html". + "html_dir": ".asv/html", + + // The number of characters to retain in the commit hashes. + // "hash_length": 8, + + // `asv` will cache wheels of the recent builds in each + // environment, making them faster to install next time. This is + // number of builds to keep, per environment. + "build_cache_size": 5 + + // The commits after which the regression search in `asv publish` + // should start looking for regressions. Dictionary whose keys are + // regexps matching to benchmark names, and values corresponding to + // the commit (exclusive) after which to start looking for + // regressions. The default is to start from the first commit + // with results. If the commit is `null`, regression detection is + // skipped for the matching benchmark. + // + // "regressions_first_commits": { + // "some_benchmark": "352cdf", // Consider regressions only after this commit + // "another_benchmark": null, // Skip regression detection altogether + // } + + // The thresholds for relative change in results, after which `asv + // publish` starts reporting regressions. Dictionary of the same + // form as in ``regressions_first_commits``, with values + // indicating the thresholds. If multiple entries match, the + // maximum is taken. If no entry matches, the default is 5%. + // + // "regressions_thresholds": { + // "some_benchmark": 0.01, // Threshold of 1% + // "another_benchmark": 0.5, // Threshold of 50% + // } +} diff --git a/benchmarks/__init__.py b/benchmarks/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/benchmarks/__init__.py @@ -0,0 +1 @@ + diff --git a/benchmarks/makeARGB.py b/benchmarks/makeARGB.py new file mode 100644 index 00000000..48656c52 --- /dev/null +++ b/benchmarks/makeARGB.py @@ -0,0 +1,72 @@ +import numpy as np + +from pyqtgraph.functions import makeARGB + + +class TimeSuite(object): + def __init__(self): + self.c_map = None + self.float_data = None + self.uint8_data = None + self.uint8_lut = None + self.uint16_data = None + self.uint16_lut = None + + def setup(self): + size = (500, 500) + + self.float_data = { + 'data': np.random.normal(size=size), + 'levels': [-4., 4.], + } + + self.uint16_data = { + 'data': np.random.randint(100, 4500, size=size).astype('uint16'), + 'levels': [250, 3000], + } + + self.uint8_data = { + 'data': np.random.randint(0, 255, size=size).astype('ubyte'), + 'levels': [20, 220], + } + + self.c_map = np.array([ + [-500., 255.], + [-255., 255.], + [0., 500.], + ]) + + self.uint8_lut = np.zeros((256, 4), dtype='ubyte') + for i in range(3): + self.uint8_lut[:, i] = np.clip(np.linspace(self.c_map[i][0], self.c_map[i][1], 256), 0, 255) + self.uint8_lut[:, 3] = 255 + + self.uint16_lut = np.zeros((2 ** 16, 4), dtype='ubyte') + for i in range(3): + self.uint16_lut[:, i] = np.clip(np.linspace(self.c_map[i][0], self.c_map[i][1], 2 ** 16), 0, 255) + self.uint16_lut[:, 3] = 255 + + +def make_test(dtype, use_levels, lut_name, func_name): + def time_test(self): + data = getattr(self, dtype + '_data') + makeARGB( + data['data'], + lut=getattr(self, lut_name + '_lut', None), + levels=use_levels and data['levels'], + ) + + time_test.__name__ = func_name + return time_test + + +for dt in ['float', 'uint16', 'uint8']: + for levels in [True, False]: + for ln in [None, 'uint8', 'uint16']: + name = f'time_makeARGB_{dt}_{"" if levels else "no"}levels_{ln or "no"}lut' + setattr(TimeSuite, name, make_test(dt, levels, ln, name)) + + +if __name__ == "__main__": + ts = TimeSuite() + ts.setup() diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index e7ca0e32..6bcf8113 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1000,7 +1000,7 @@ def makeRGBA(*args, **kwds): def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, output=None): - """ + """ Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc. @@ -1091,7 +1091,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, output=None dtype = xp.min_scalar_type(lut.shape[0]-1) # awkward, but fastest numpy native nan evaluation - # nanMask = None if data.dtype.kind == 'f' and xp.isnan(data.min()): nanMask = xp.isnan(data) @@ -1128,7 +1127,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False, output=None if lut is not None: data = applyLookupTable(data, lut) else: - if data.dtype is not xp.ubyte: + if data.dtype != xp.ubyte: data = xp.clip(data, 0, 255).astype(xp.ubyte) profile('apply lut') @@ -1189,10 +1188,14 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): ============== =================================================================== **Arguments:** - imgData Array of data to convert. Must have shape (width, height, 3 or 4) - and dtype=ubyte. The order of values in the 3rd axis must be - (b, g, r, a). - alpha If True, the QImage returned will have format ARGB32. If False, + imgData Array of data to convert. Must have shape (height, width), + (height, width, 3), or (height, width, 4). If transpose is + True, then the first two axes are swapped. The array dtype + must be ubyte. For 2D arrays, the value is interpreted as + greyscale. For 3D arrays, the order of values in the 3rd + axis must be (b, g, r, a). + alpha If the input array is 3D and *alpha* is True, the QImage + returned will have format ARGB32. If False, the format will be RGB32. By default, _alpha_ is True if array.shape[2] == 4. copy If True, the data is copied before converting to QImage. @@ -1208,30 +1211,35 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): ## create QImage from buffer profile = debug.Profiler() - ## If we didn't explicitly specify alpha, check the array shape. - if alpha is None: - alpha = (imgData.shape[2] == 4) - copied = False - if imgData.shape[2] == 3: ## need to make alpha channel (even if alpha==False; QImage requires 32 bpp) - if copy is True: - d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype) - d2[:,:,:3] = imgData - d2[:,:,3] = 255 - imgData = d2 - copied = True + if imgData.ndim == 2: + imgFormat = QtGui.QImage.Format_Grayscale8 + elif imgData.ndim == 3: + # If we didn't explicitly specify alpha, check the array shape. + if alpha is None: + alpha = (imgData.shape[2] == 4) + + if imgData.shape[2] == 3: # need to make alpha channel (even if alpha==False; QImage requires 32 bpp) + if copy is True: + d2 = np.empty(imgData.shape[:2] + (4,), dtype=imgData.dtype) + d2[:,:,:3] = imgData + d2[:,:,3] = 255 + imgData = d2 + copied = True + else: + raise Exception('Array has only 3 channels; cannot make QImage without copying.') + + profile("add alpha channel") + + if alpha: + imgFormat = QtGui.QImage.Format_ARGB32 else: - raise Exception('Array has only 3 channels; cannot make QImage without copying.') - - if alpha: - imgFormat = QtGui.QImage.Format_ARGB32 + imgFormat = QtGui.QImage.Format_RGB32 else: - imgFormat = QtGui.QImage.Format_RGB32 + raise TypeError("Image array must have ndim = 2 or 3.") if transpose: - imgData = imgData.transpose((1, 0, 2)) ## QImage expects the row/column order to be opposite - - profile() + imgData = imgData.transpose((1, 0, 2)) # QImage expects row-major order if not imgData.flags['C_CONTIGUOUS']: if copy is False: @@ -1240,9 +1248,12 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): imgData = np.ascontiguousarray(imgData) copied = True + profile("ascontiguousarray") + if copy is True and copied is False: imgData = imgData.copy() + profile("copy") if QT_LIB == 'PySide': ch = ctypes.c_char.from_buffer(imgData, 0) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index a1bf49f0..8737c541 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -50,7 +50,6 @@ class ImageItem(GraphicsObject): self.qimage = None ## rendered image for display self.paintMode = None - self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None self.autoDownsample = False