summaryrefslogtreecommitdiff
path: root/silx/gui/_utils.py
blob: d91a5725f8fa636d23d6e24464001b3f59111aad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module provides convenient functions to use with Qt objects.

It provides:
- conversion between numpy and QImage:
  :func:`convertArrayToQImage`, :func:`convertQImageToArray`
- Execution of function in Qt main thread: :func:`submitToQtMainThread`
"""

from __future__ import division


__authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "16/01/2017"


import sys
import numpy

from silx.third_party.concurrent_futures import Future

from . import qt


def convertArrayToQImage(image):
    """Convert an array-like RGB888 image to a QImage.

    The created QImage is using a copy of the array data.

    Limitation: Only supports RGB888 format.

    :param image: Array-like image data
    :type image: numpy.ndarray of uint8 of dimension HxWx3
    :return: Corresponding Qt image
    :rtype: QImage
    """
    # Possible extension: add a format argument to support more formats

    image = numpy.array(image, copy=False, order='C', dtype=numpy.uint8)

    height, width, depth = image.shape
    assert depth == 3

    qimage = qt.QImage(
        image.data,
        width,
        height,
        image.strides[0],  # bytesPerLine
        qt.QImage.Format_RGB888)

    return qimage.copy()  # Making a copy of the image and its data


def convertQImageToArray(image):
    """Convert a RGB888 QImage to a numpy array.

    Limitation: Only supports RGB888 format.
    If QImage is not RGB888 it gets converted to this format.

    :param QImage: The QImage to convert.
    :return: The image array
    :rtype: numpy.ndarray of uint8 of shape HxWx3
    """
    # Possible extension: avoid conversion to support more formats

    if image.format() != qt.QImage.Format_RGB888:
        # Convert to RGB888 if needed
        image = image.convertToFormat(qt.QImage.Format_RGB888)

    ptr = image.bits()
    if qt.BINDING not in ('PySide', 'PySide2'):
        ptr.setsize(image.byteCount())
        if qt.BINDING == 'PyQt4' and sys.version_info[0] == 2:
            ptr = ptr.asstring()
    elif sys.version_info[0] == 3:  # PySide with Python3
        ptr = ptr.tobytes()

    array = numpy.fromstring(ptr, dtype=numpy.uint8)

    # Lines are 32 bits aligned: remove padding bytes
    array = array.reshape(image.height(), -1)[:, :image.width() * 3]
    array.shape = image.height(), image.width(), 3
    return array


class _QtExecutor(qt.QObject):
    """Executor of tasks in Qt main thread"""

    __sigSubmit = qt.Signal(Future, object, tuple, dict)
    """Signal used to run tasks."""

    def __init__(self):
        super(_QtExecutor, self).__init__(parent=None)

        # Makes sure the executor lives in the main thread
        app = qt.QApplication.instance()
        assert app is not None
        mainThread = app.thread()
        if self.thread() != mainThread:
            self.moveToThread(mainThread)

        self.__sigSubmit.connect(self.__run)

    def submit(self, fn, *args, **kwargs):
        """Submit fn(*args, **kwargs) to Qt main thread

        :param callable fn: Function to call in main thread
        :return: Future object to retrieve result
        :rtype: concurrent.future.Future
        """
        future = Future()
        self.__sigSubmit.emit(future, fn, args, kwargs)
        return future

    def __run(self, future, fn, args, kwargs):
        """Run task in Qt main thread

        :param concurrent.future.Future future:
        :param callable fn: Function to run
        :param tuple args: Arguments
        :param dict kwargs: Keyword arguments
        """
        if not future.set_running_or_notify_cancel():
            return

        try:
            result = fn(*args, **kwargs)
        except BaseException as e:
            future.set_exception(e)
        else:
            future.set_result(result)


_executor = None
"""QObject running the tasks in main thread"""


def submitToQtMainThread(fn, *args, **kwargs):
    """Run fn(*args, **kwargs) in Qt's main thread.

    If not called from the main thread, this is run asynchronously.

    :param callable fn: Function to call in main thread.
    :return: A future object to retrieve the result
    :rtype: concurrent.future.Future
    """
    global _executor
    if _executor is None:  # Lazy-loading
        _executor = _QtExecutor()

    return _executor.submit(fn, *args, **kwargs)