2011-04-19 4 views
1

J'essaie de créer un tracé multicanal très similaire à ceux utilisés pour l'édition audio, mais pour les données médicales.Traceur de données Cairo dans PyGTK: devrais-je utiliser un pixbuffer?

Ce type de programme doit être utilisé par une personne qui devrait (entre autres) zoomer et faire un panoramique horizontalement sur la parcelle de données, afin de trouver et de classifier certains événements significatifs. Donc, j'ai un flux de données (une liste de plusieurs dizaines de milliers d'échantillons) que je trace sur un gtk.DrawingArea en utilisant Cairo, avec une "échelle" initiale basée sur le premier et le dernier index des données à tracer et un rapport de largeur entre l'intervalle de données à tracer et la largeur de pixel de la zone de dessin. J'ai créé des événements souris pour "faire glisser" les données, comme le font la plupart des visionneuses d'images et même Google Maps (mais je ne travaille que sur l'axe horizontal maintenant). Le fait est: redessiner alors que le panoramique est assez lent, et je pense que c'est à cause de la fonction de redessiner, car cela dépend de la longueur de l'intervalle étant tracé (lié au "zoom" intervalle de données dense). Je me demande si je devrais rendre tout le tracé à un (grand) pixbuffer, et seulement repositionner ce pixbuffer commettre la partie correspondante dans la zone de dessin de la fenêtre.

Alors, mes questions sont les suivantes:? « Comment est ce genre de données 2D complotant avec pan/zoom fait habituellement dans Pygtk est-il un moyen « standard » de le faire Dois-je créer un énorme pixbuffer que je pourrait utiliser comme une source de cairo, en le traduisant et «estampillage» sur la surface de dessin cairo surface? "

Une Shrinked partie de mon code suit:

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

Aussi, si quelqu'un a une autre astuce sur d'autres façons d'atteindre le même objectif, je serais très heureux de le recevoir.

Merci pour la lecture

EDIT: J'ai changé le code pour la variable dépendante step sur la proportion entre les pixels disponibles pour tracer et l'intervalle longueur des données ne soient parcelle. De cette façon, si la fenêtre a seulement, disons, 1000 pixels, une "tranche" de l'intervalle entier sera prise, qui n'a que 1000 valeurs d'échantillon. Le résultat n'est pas si lisse, mais il est assez rapide, et si on veut plus de détails, on peut zoomer pour augmenter la résolution (donc recalculer l'étape)

Répondre

1

J'ai changé le code pour faire la variable step dépend de la proportion entre les pixels disponibles à tracer et la longueur d'intervalle des données être terrain. De cette façon, si la fenêtre a seulement, disons, 1000 pixels, une "tranche" de l'intervalle entier sera prise, qui n'a que 1000 valeurs d'échantillon. Le résultat est pas si lisse, mais il est assez rapide, et si l'on veut plus de détails, il pourrait être zoomé pour augmenter la résolution (recalcul ainsi l'étape):

step = int(self.samples/float(w)) if step >= 1 else 1 
stream = self.channel['recording'][startsample:endsample:step] 
+0

J'ai répondu à ma propre question parce que c'est ce que j'ai obtenu jusqu'à présent (et la réponse @ilius n'a pas résolu le problème de vitesse), mais bien sûr toute autre réponse est la bienvenue. – heltonbiker

0

Si la performance n'a pas d'importance, je vous suggère d'utiliser matplotlib . C'est très parfait, et fonctionne avec plusieurs backends y compris GtkEgg (aussi longtemps que je me souviens)

+0

Je pensais à ce sujet, mais en effet je une 'planifier' de toujours préférer le mode de dessin de niveau inférieur dans mes programmes d'assurance-chômage. Par exemple, quand le zoom/panoramique est correct, je vais devoir cliquer sur un point de la courbe et dessiner un rectangle transparent de 10 secondes vers la droite, calculer la moyenne mobile des 5 dernières minutes de données et dessiner cette ligne, dessiner marqueurs dans un autre canal ... En tout cas, merci pour votre réponse! – heltonbiker