File: image_complex.py

package info (click to toggle)
python-vispy 0.14.3-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 8,840 kB
  • sloc: python: 59,436; javascript: 6,800; makefile: 69; sh: 6
file content (130 lines) | stat: -rw-r--r-- 5,273 bytes parent folder | download | duplicates (2)
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
from .image import ImageVisual, _APPLY_CLIM_FLOAT, _APPLY_GAMMA_FLOAT
import numpy as np
from .shaders import Function, FunctionChain

# In a complex Image, the texture will be rg32f, where:
# data.r contains the real component
# data.g contains the imaginary component
COMPLEX_TRANSFORMS = {
    "real": "float cplx2float(vec4 data) { return data.r; }",
    "imaginary": "float cplx2float(vec4 data) { return data.g; }",
    "magnitude": "float cplx2float(vec4 data) { return length(vec2(data)); }",
    "phase": "float cplx2float(vec4 data) { return atan(data.g, data.r); }",
}
CPU_COMPLEX_TRANSFORMS = {
    "magnitude": np.abs,
    "phase": np.angle,
    "real": np.real,
    "imaginary": np.imag,
}


class ComplexImageVisual(ImageVisual):
    """:class:`~vispy.visuals.ImageVisual` subclass displaying a complex-valued image.

    This class handles complex values by using an rg32f float texture behind the scenes,
    storing the real component in the "r"  value and the imaginary in the "g" value.

    Parameters
    ----------
    data : ndarray
        Complex valued ImageVisual data.  Should be a two dimensional array with a dtype
        of np.complex64 or np.complex128.
    complex_mode : str
        The mode used to convert the complex value in each pixel into a scalar:
            * 'real': show only the real component.
            * 'imaginary': show only the imaginary component.
            * 'magnitude': show the magnitude (`np.abs`) of the complex value.
            * 'phase': show the phase (`np.angle`) of the complex value.
    """
    COMPLEX_MODES = set(COMPLEX_TRANSFORMS)

    def __init__(self, data=None, complex_mode="magnitude", **kwargs):
        if complex_mode not in self.COMPLEX_MODES:
            raise ValueError(
                "complex_mode must be one of %s" % ", ".join(self.COMPLEX_MODES)
            )
        self._data_is_complex = np.iscomplexobj(data)
        self._complex_mode = complex_mode

        if kwargs.get("clim", "auto") == "auto" and self._data_is_complex:
            kwargs["clim"] = self._calc_complex_clim(data)

        kwargs["texture_format"] = "r32f" if self._data_is_complex else "r32f"
        if self._data_is_complex:
            data = self._convert_complex_to_float_view(data)
        super().__init__(data=data, **kwargs)

    def _init_texture(self, data, texture_format, **texture_kwargs):
        texture_kwargs = {}
        if self._data_is_complex:
            texture_kwargs["format"] = "rg"
        return super()._init_texture(data, texture_format, **texture_kwargs)

    def set_data(self, image):
        data = np.asarray(image)
        if np.iscomplexobj(data):
            #  Turn the texture into an rg32f texture
            # where r = 'real' and g = 'imag'
            self._data_is_complex = True
            # FUTURE: Add formal way of defining texture format from set_data
            self._texture._format = "rg"
            data = self._convert_complex_to_float_view(data)
        elif data.ndim == 3 and data.shape[-1] == 2:
            # data was complex but was already converted to 32-bit float
            # should really only occur from __init__
            self._data_is_complex = True
        else:
            self._texture._format = None
        return super().set_data(data)

    @staticmethod
    def _convert_complex_to_float_view(complex_arr):
        # turn complex128 into complex64 if needed
        complex64_arr = complex_arr.astype(np.complex64, copy=False)
        float_view_arr = complex64_arr.view(dtype=np.float32).reshape((complex64_arr.shape + (2, )))
        return float_view_arr

    @property
    def complex_mode(self):
        return self._data_is_complex and self._complex_mode

    @complex_mode.setter
    def complex_mode(self, value):
        if value not in self.COMPLEX_MODES:
            raise ValueError(
                "complex_mode must be one of %s" % ", ".join(self.COMPLEX_MODES)
            )
        if self._complex_mode != value:
            self._complex_mode = value
            self._need_colortransform_update = True
            self.update()

    def _build_color_transform(self):
        if self.complex_mode:
            fclim = Function(_APPLY_CLIM_FLOAT)
            fgamma = Function(_APPLY_GAMMA_FLOAT)
            chain = [
                Function(COMPLEX_TRANSFORMS[self.complex_mode]),
                fclim,
                fgamma,
                Function(self.cmap.glsl_map),
            ]
            fun = FunctionChain(None, chain)
            fclim["clim"] = self._texture.clim_normalized
            fgamma["gamma"] = self.gamma
            return fun
        return super()._build_color_transform()

    @ImageVisual.clim.setter
    def clim(self, clim):
        if clim == "auto" and self.complex_mode:
            clim = self._calc_complex_clim()
        super(ComplexImageVisual, type(self)).clim.fset(self, clim)

    def _calc_complex_clim(self, data=None):
        # it would be nice if this could be done in the scalable texture mixin,
        # but that would require the mixin knowing about the complex mode.
        func = CPU_COMPLEX_TRANSFORMS[self.complex_mode]
        _rendered = func(self._data if data is None else data)
        return (_rendered.min(), _rendered.max())