Statistics
| Branch: | Tag: | Revision:

root / gr-wxgui / src / python / plotter / waterfall_plotter.py @ 36649d4e

History | View | Annotate | Download (9.1 kB)

1
#
2
# Copyright 2008 Free Software Foundation, Inc.
3
#
4
# This file is part of GNU Radio
5
#
6
# GNU Radio is free software; you can redistribute it and/or modify
7
# it under the terms of the GNU General Public License as published by
8
# the Free Software Foundation; either version 3, or (at your option)
9
# any later version.
10
#
11
# GNU Radio is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with GNU Radio; see the file COPYING.  If not, write to
18
# the Free Software Foundation, Inc., 51 Franklin Street,
19
# Boston, MA 02110-1301, USA.
20
#
21
22
import wx
23
from plotter_base import grid_plotter_base
24
from OpenGL.GL import *
25
from gnuradio.wxgui import common
26
import numpy
27
import gltext
28
import math
29
30
LEGEND_LEFT_PAD = 7
31
LEGEND_NUM_BLOCKS = 256
32
LEGEND_NUM_LABELS = 9
33
LEGEND_WIDTH = 8
34
LEGEND_FONT_SIZE = 8
35
LEGEND_BORDER_COLOR_SPEC = (0, 0, 0) #black
36
PADDING = 35, 60, 40, 60 #top, right, bottom, left
37
38
ceil_log2 = lambda x: 2**int(math.ceil(math.log(x)/math.log(2)))
39
40
def _get_rbga(red_pts, green_pts, blue_pts, alpha_pts=[(0, 0), (1, 0)]):
41
        """!
42
        Get an array of 256 rgba values where each index maps to a color.
43
        The scaling for red, green, blue, alpha are specified in piece-wise functions.
44
        The piece-wise functions consist of a set of x, y coordinates.
45
        The x and y values of the coordinates range from 0 to 1.
46
        The coordinates must be specified so that x increases with the index value.
47
        Resulting values are calculated along the line formed between 2 coordinates.
48
        @param *_pts an array of x,y coordinates for each color element
49
        @return array of rbga values (4 bytes) each
50
        """
51
        def _fcn(x, pw):
52
                for (x1, y1), (x2, y2) in zip(pw, pw[1:]):
53
                        #linear interpolation
54
                        if x <= x2: return float(y1 - y2)/(x1 - x2)*(x - x1) + y1
55
                raise Exception
56
        return [numpy.array(map(
57
                        lambda pw: int(255*_fcn(i/255.0, pw)),
58
                        (red_pts, green_pts, blue_pts, alpha_pts),
59
                ), numpy.uint8).tostring() for i in range(0, 256)
60
        ]
61
62
COLORS = {
63
        'rgb1': _get_rbga( #http://www.ks.uiuc.edu/Research/vmd/vmd-1.7.1/ug/img47.gif
64
                red_pts = [(0, 0), (.5, 0), (1, 1)],
65
                green_pts = [(0, 0), (.5, 1), (1, 0)],
66
                blue_pts = [(0, 1), (.5, 0), (1, 0)],
67
        ),
68
        'rgb2': _get_rbga( #http://xtide.ldeo.columbia.edu/~krahmann/coledit/screen.jpg
69
                red_pts = [(0, 0), (3.0/8, 0), (5.0/8, 1), (7.0/8, 1), (1, .5)],
70
                green_pts = [(0, 0), (1.0/8, 0), (3.0/8, 1), (5.0/8, 1), (7.0/8, 0), (1, 0)],
71
                blue_pts = [(0, .5), (1.0/8, 1), (3.0/8, 1), (5.0/8, 0), (1, 0)],
72
        ),
73
        'rgb3': _get_rbga(
74
                red_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 0), (1, 1)],
75
                green_pts = [(0, 0), (1.0/3.0, 0), (2.0/3.0, 1), (1, 0)],
76
                blue_pts = [(0, 0), (1.0/3.0, 1), (2.0/3.0, 0), (1, 0)],
77
        ),
78
        'gray': _get_rbga(
79
                red_pts = [(0, 0), (1, 1)],
80
                green_pts = [(0, 0), (1, 1)],
81
                blue_pts = [(0, 0), (1, 1)],
82
        ),
83
}
84
85
##################################################
86
# Waterfall Plotter
87
##################################################
88
class waterfall_plotter(grid_plotter_base):
89
        def __init__(self, parent):
90
                """!
91
                Create a new channel plotter.
92
                """
93
                #init
94
                grid_plotter_base.__init__(self, parent, PADDING)
95
                self._resize_texture(False)
96
                self._minimum = 0
97
                self._maximum = 0
98
                self._fft_size = 1
99
                self._buffer = list()
100
                self._pointer = 0
101
                self._counter = 0
102
                self.set_num_lines(0)
103
                self.set_color_mode(COLORS.keys()[0])
104
105
        def _gl_init(self):
106
                """!
107
                Run gl initialization tasks.
108
                """
109
                self._grid_compiled_list_id = glGenLists(1)
110
                self._waterfall_texture = glGenTextures(1)
111
112
        def draw(self):
113
                """!
114
                Draw the grid and waveforms.
115
                """
116
                self.lock()
117
                #resize texture
118
                self._resize_texture()
119
                #store the grid drawing operations
120
                if self.changed():
121
                        glNewList(self._grid_compiled_list_id, GL_COMPILE)
122
                        self._draw_grid()
123
                        self._draw_legend()
124
                        glEndList()
125
                        self.changed(False)
126
                self.clear()
127
                #draw the grid
128
                glCallList(self._grid_compiled_list_id)
129
                self._draw_waterfall()
130
                self._draw_point_label()
131
                #swap buffer into display
132
                self.SwapBuffers()
133
                self.unlock()
134
135
        def _draw_waterfall(self):
136
                """!
137
                Draw the waterfall from the texture.
138
                The texture is circularly filled and will wrap around.
139
                Use matrix modeling to shift and scale the texture onto the coordinate plane.
140
                """
141
                #setup texture
142
                glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
143
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
144
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
145
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
146
                glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE)
147
                #write the buffer to the texture
148
                while self._buffer:
149
                        glTexSubImage2D(GL_TEXTURE_2D, 0, 0, self._pointer, self._fft_size, 1, GL_RGBA, GL_UNSIGNED_BYTE, self._buffer.pop(0))
150
                        self._pointer = (self._pointer + 1)%self._num_lines
151
                #begin drawing
152
                glEnable(GL_TEXTURE_2D)
153
                glPushMatrix()
154
                #matrix scaling
155
                glTranslatef(self.padding_left+1, self.padding_top, 0)
156
                glScalef(
157
                        float(self.width-self.padding_left-self.padding_right-1),
158
                        float(self.height-self.padding_top-self.padding_bottom-1),
159
                        1.0,
160
                )
161
                #draw texture with wrapping
162
                glBegin(GL_QUADS)
163
                prop_y = float(self._pointer)/(self._num_lines-1)
164
                prop_x = float(self._fft_size)/ceil_log2(self._fft_size)
165
                off = 1.0/(self._num_lines-1)
166
                glTexCoord2f(0, prop_y+1-off)
167
                glVertex2f(0, 1)
168
                glTexCoord2f(prop_x, prop_y+1-off)
169
                glVertex2f(1, 1)
170
                glTexCoord2f(prop_x, prop_y)
171
                glVertex2f(1, 0)
172
                glTexCoord2f(0, prop_y)
173
                glVertex2f(0, 0)
174
                glEnd()
175
                glPopMatrix()
176
                glDisable(GL_TEXTURE_2D)
177
178
        def _populate_point_label(self, x_val, y_val):
179
                """!
180
                Get the text the will populate the point label.
181
                Give the X value for the current point.
182
                @param x_val the current x value
183
                @param y_val the current y value
184
                @return a value string with units
185
                """
186
                return '%s: %s %s'%(self.x_label, common.label_format(x_val), self.x_units)
187
188
        def _draw_legend(self):
189
                """!
190
                Draw the color scale legend.
191
                """
192
                if not self._color_mode: return
193
                legend_height = self.height-self.padding_top-self.padding_bottom
194
                #draw each legend block
195
                block_height = float(legend_height)/LEGEND_NUM_BLOCKS
196
                x = self.width - self.padding_right + LEGEND_LEFT_PAD
197
                for i in range(LEGEND_NUM_BLOCKS):
198
                        color = COLORS[self._color_mode][int(255*i/float(LEGEND_NUM_BLOCKS-1))]
199
                        glColor4f(*map(lambda c: ord(c)/255.0, color))
200
                        y = self.height - (i+1)*block_height - self.padding_bottom
201
                        self._draw_rect(x, y, LEGEND_WIDTH, block_height)
202
                #draw rectangle around color scale border
203
                glColor3f(*LEGEND_BORDER_COLOR_SPEC)
204
                self._draw_rect(x, self.padding_top, LEGEND_WIDTH, legend_height, fill=False)
205
                #draw each legend label
206
                label_spacing = float(legend_height)/(LEGEND_NUM_LABELS-1)
207
                x = self.width - (self.padding_right - LEGEND_LEFT_PAD - LEGEND_WIDTH)/2
208
                for i in range(LEGEND_NUM_LABELS):
209
                        proportion = i/float(LEGEND_NUM_LABELS-1)
210
                        dB = proportion*(self._maximum - self._minimum) + self._minimum
211
                        y = self.height - i*label_spacing - self.padding_bottom
212
                        txt = gltext.Text('%ddB'%int(dB), font_size=LEGEND_FONT_SIZE, centered=True)
213
                        txt.draw_text(wx.Point(x, y))
214
215
        def _resize_texture(self, flag=None):
216
                """!
217
                Create the texture to fit the fft_size X num_lines.
218
                @param flag the set/unset or update flag
219
                """
220
                if flag is not None: 
221
                        self._resize_texture_flag = flag
222
                        return
223
                if not self._resize_texture_flag: return
224
                self._buffer = list()
225
                self._pointer = 0
226
                if self._num_lines and self._fft_size:
227
                        glBindTexture(GL_TEXTURE_2D, self._waterfall_texture)
228
                        data = numpy.zeros(self._num_lines*self._fft_size*4, numpy.uint8).tostring()
229
                        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, ceil_log2(self._fft_size), self._num_lines, 0, GL_RGBA, GL_UNSIGNED_BYTE, data)
230
                self._resize_texture_flag = False
231
232
        def set_color_mode(self, color_mode):
233
                """!
234
                Set the color mode.
235
                New samples will be converted to the new color mode.
236
                Old samples will not be recolorized.
237
                @param color_mode the new color mode string
238
                """
239
                self.lock()
240
                if color_mode in COLORS.keys():
241
                        self._color_mode = color_mode
242
                        self.changed(True)
243
                self.update()
244
                self.unlock()
245
246
        def set_num_lines(self, num_lines):
247
                """!
248
                Set number of lines.
249
                Powers of two only.
250
                @param num_lines the new number of lines
251
                """
252
                self.lock()
253
                self._num_lines = num_lines
254
                self._resize_texture(True)
255
                self.update()
256
                self.unlock()
257
258
        def set_samples(self, samples, minimum, maximum):
259
                """!
260
                Set the samples to the waterfall.
261
                Convert the samples to color data.
262
                @param samples the array of floats
263
                @param minimum the minimum value to scale
264
                @param maximum the maximum value to scale
265
                """
266
                self.lock()
267
                #set the min, max values
268
                if self._minimum != minimum or self._maximum != maximum:
269
                        self._minimum = minimum
270
                        self._maximum = maximum
271
                        self.changed(True)
272
                if self._fft_size != len(samples):
273
                        self._fft_size = len(samples)
274
                        self._resize_texture(True)
275
                #normalize the samples to min/max
276
                samples = (samples - minimum)*float(255/(maximum-minimum))
277
                samples = numpy.clip(samples, 0, 255) #clip
278
                samples = numpy.array(samples, numpy.uint8)
279
                #convert the samples to RGBA data
280
                data = numpy.choose(samples, COLORS[self._color_mode]).tostring()
281
                self._buffer.append(data)
282
                self.unlock()