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()
|