Cairo data плоттер в PyGTK: я должен использовать pixbuffer? - PullRequest
1 голос
/ 20 апреля 2011

Я пытаюсь создать многоканальный график, очень похожий на те, которые используются для редактирования аудио, но для медицинских данных.

Этот вид программы должен использоваться человеком, который должен (среди прочего) масштабировать и панорамировать горизонтально по графику данных, чтобы найти и классифицировать некоторые значимые события.

Итак, у меня есть поток данных (список из нескольких десятков тысяч образцов), который я строю на gtk.DrawingArea с использованием Cairo, с начальным «масштабом», основанным на первом и последнем индексах данных для построения, и отношение ширины между интервалом данных для построения графика и шириной в пикселях области рисования. Я создал несколько событий мыши для «перетаскивания» данных, как это делают большинство программ просмотра изображений и даже Карты Google (но сейчас я работаю только по горизонтальной оси).

Дело в том, что перерисовка при панорамировании довольно медленная, и я думаю, что это происходит из-за функции перерисовки, поскольку она зависит от длины наносимого интервала (в связи с установленным мною "масштабированием", показывая более плотный интервал данных). Интересно, должен ли я отрисовывать весь график в (большом) пиксельном буфере и только переставлять этот пиксельный буфер, фиксировать соответствующую часть в области рисования окна.

Итак, мои вопросы: Msgstr "Как в 2D-графике обычно выполняется построение такого вида двухмерных данных с панорамированием / масштабированием? Есть ли «стандартный» способ сделать это? Должен ли я создать огромный пиксельный буфер, который я мог бы использовать в качестве источника Каира, переводя его и «штампуя» на поверхности Каира области рисования? "

Ниже следует сокращенная часть моего кода:

class DataView(gtk.DrawingArea):
    """ Plots a 'rectangle' of the data, depending on predefined horizontal and vertical ranges """
    def __init__(self, channel):
        gtk.DrawingArea.__init__(self)
        self.connect("expose_event", self.expose)
        self.channel = dados.channel_content[channel]

        self.top = int(self.channel['pmax'])
        self.bottom = int(self.channel['pmin'])

        # this part defines position and size of the plotting
        self.x_offset = 0
        self.y_offset = 0
        self.x_scale = 1
        self.y_scale = 0.01

    def expose(self, widget, event):
        cr = widget.window.cairo_create()
        rect = self.get_allocation()
        w = rect.width
        h = rect.height

        cr.translate(0, h/2)
        cr.scale(1,-1)

        cr.save()
        self.x_scale = 1.*w/(signalpanel.end - signalpanel.start)
        cr.translate(self.x_offset, self.y_offset)
        cr.scale(self.x_scale, self.y_scale)

        step = 5
        # here I select a slice of my full data list
        stream = self.channel['recording'][signalpanel.start:signalpanel.end:step]

        # here I draw
        cr.move_to(0, stream[0])
        for n,s in enumerate(stream[1:]):
            cr.line_to((n+1)*step, s)
        cr.restore()
        cr.set_source_rgb(0,0,0)
        cr.set_line_width(1)
        cr.stroke()

class ChannelView(gtk.HBox):
    """ contains a DataView surrounded by all other satellite widgets """
    def __init__(self, channel):
        gtk.HBox.__init__(self)
        labelpanel = gtk.VBox()
        labelpanel.set_size_request(100, 100)
        dataview = DataView(channel)
        dataview.connect("motion_notify_event", onmove)
        dataview.connect("button_press_event", onpress)
        dataview.connect("button_release_event", onrelease)
        dataview.connect("destroy", gtk.main_quit)
        dataview.add_events(gtk.gdk.EXPOSURE_MASK
                    | gtk.gdk.LEAVE_NOTIFY_MASK
                    | gtk.gdk.BUTTON_PRESS_MASK
                    | gtk.gdk.BUTTON_RELEASE_MASK
                    | gtk.gdk.POINTER_MOTION_MASK
                    | gtk.gdk.POINTER_MOTION_HINT_MASK)
        self.pack_end(dataview, True, True)
        self.pack_end(gtk.VSeparator(), False, False)

        #populate labelpanel
        """ a lot of widget-creating code (ommited) """

# three functions to pan the data with the mouse
def onpress(widget, event):
    if event.button == 1:
        signalpanel.initial_position = event.x
        signalpanel.start_x = signalpanel.start
        signalpanel.end_x = signalpanel.end
    signalpanel.queue_draw()

def onmove(widget, event):
    if signalpanel.initial_position:
        signalpanel.start = max(0, int((signalpanel.start_x - (event.x-signalpanel.initial_position))*widget.x_scale))
        signalpanel.end = int((signalpanel.end_x - (event.x-signalpanel.initial_position))*widget.x_scale)
        print signalpanel.start, signalpanel.end
    signalpanel.queue_draw()

def onrelease(widget, event):
    signalpanel.initial_position = None
    signalpanel.queue_draw()

class PlotterPanel(gtk.VBox):
    """ Defines a vertical panel with special features to manage multichannel plots """
    def __init__(self):
        gtk.VBox.__init__(self)

        self.initial_position = None

        # now these are the indexes defining the slice to plot
        self.start = 0
        self.end = 20000 # full list has 120000 values

if __name__ == "__main__":
    folder = os.path.expanduser('~/Dropbox/01MIOTEC/06APNÉIA/Samples')
    dados = EDF_Reader(folder, 'Osas2002plusQRS.rec') # the file from where the data come from
    window = gtk.Window()
    signalpanel = PlotterPanel()
    signalpanel.pack_start(ChannelView('Resp abdomen'), True, True)
    window.add(signalpanel)
    window.connect("delete-event", gtk.main_quit)
    window.set_position(gtk.WIN_POS_CENTER)
    window.show_all()
    gtk.main()

Кроме того, если у кого-то есть другие советы о других способах достижения той же цели, я был бы очень рад ее получить.

Спасибо за чтение

РЕДАКТИРОВАТЬ: я изменил код, чтобы сделать переменную step зависимой от пропорции между доступными пикселями для построения и интервалом длины данных для построения. Таким образом, если окно имеет только, скажем, 1000 пикселей, будет взят «срез» всего интервала, который имеет только 1000 выборочных значений. Результат не такой плавный, но он довольно быстрый, и, если требуется больше деталей, его можно увеличить, чтобы увеличить разрешение (таким образом, пересчитав шаг)

Ответы [ 2 ]

1 голос
/ 27 апреля 2011

Я изменил код, чтобы переменная step зависела от пропорции между доступными пикселями для построения и интервалом между данными, которые должны быть построены. Таким образом, если окно имеет только, скажем, 1000 пикселей, будет взят «срез» всего интервала, который имеет только 1000 выборочных значений. Результат не такой плавный, но он довольно быстрый, и, если нужно больше деталей, его можно увеличить, чтобы увеличить разрешение (таким образом, пересчитывая шаг):

step = int(self.samples/float(w)) if step >= 1 else 1
stream = self.channel['recording'][startsample:endsample:step]
0 голосов
/ 20 апреля 2011

Если производительность не имеет большого значения, я предлагаю вам использовать matplotlib .Он очень совершенен и работает с несколькими бэкэндами, включая GtkEgg (насколько я помню)

...