PyGTK 中的开罗数据绘图仪:我应该使用像素缓冲区吗?
我正在尝试创建一个与用于音频编辑的多通道图非常相似的多通道图,但用于医疗数据。
这种程序供那些应该(除其他外)在数据图上水平缩放和平移的人使用,以便找到一些有意义的事件并对其进行分类。
因此,我有一个数据流(包含数万个样本的列表),我使用 Cairo 在 gtk.DrawingArea 上绘制该数据流,其初始“比例”基于要绘制的数据的第一个和最后一个索引,以及宽度绘图的数据间隔与绘图区域的像素宽度之间的比率。我创建了一些鼠标事件来“拖动”数据,就像大多数图像查看器甚至 Google 地图所做的那样(但我现在只在水平轴上工作)。
事实是:平移时重绘非常慢,我认为这是因为重绘功能,因为它取决于绘制的间隔的长度(与我设置的“缩放”相关,显示更密集的数据间隔) 。我想知道是否应该将整个绘图渲染到一个(大)pixbuffer,并且只重新定位该pixbuffer,将相应的部分提交到窗口绘图区域。
所以,我的问题是: “这种使用平移/缩放的二维数据绘图通常是如何在 Pygtk 中完成的? 有“标准”的方法吗? 我应该创建一个巨大的像素缓冲区,我可以将其用作开罗源,翻译它并在绘图区域开罗表面上“标记”吗?”
我的代码的缩小部分如下:
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 个样本值,结果不是那么平滑,但速度相当快,如果想要更多细节,可以将其放大以提高分辨率(从而重新计算步骤)
I am trying to create a multichannel plot very similar to those used for audio editing, but for medical data.
This kind of program is to be used by a person who should (among other things) zoom and pan horizontally over the dataplot, in order to find and classify some meaningful events.
So, I have a data stream (a list of many tens of thousands of samples) which I plot on a gtk.DrawingArea using Cairo, with an initial "scale" based on first and last indexes of the data to plot, and a width ratio between the data interval to plot and the pixel width of the drawing area. I created some mouse events to "drag" the data around, much as most image viewers and even Google Maps do (but I am only working on the horizontal axis by now).
The fact is: redrawing while panning is quite slow, and I think it is because of the redrawing function, since it depends on the lengh of the interval being plotted (related to the "zoom" I set, showing a more dense data interval). I wonder if I should render the whole plot to a (big) pixbuffer, and only reposition this pixbuffer commit the corresponding part to the window drawing area.
So, my questions are:
"How is this kind of 2d data plotting with pan/zoom usually done in Pygtk?
Is there a 'standard' way of doing it?
Should I create a huge pixbuffer which I could use as a cairo source, translating it and 'stamping' on the drawing area cairo surface?"
A shrinked part of my code follows:
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()
Also, if anyone has any other tip on other ways of achieving the same goal, I would be very glad to receive it.
Thanks for reading
EDIT: I changed the code to make the variable step
dependant on the proportion between the available pixels to plot and the interval lenght of the data do be plot. This way, if the window has only, say, 1000 pixels, a "slice" of the whole interval will be taken, which have only 1000 sample values. The result is not so smooth, but it's quite fast, and if one wants more detail, it could be zoomed in to increase resolution (thus recalculating the step)
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
我更改了代码,使变量
step
取决于要绘制的可用像素与绘制数据的间隔长度之间的比例。这样,如果窗口只有 1000 个像素,则将获取整个间隔的“切片”,其中只有 1000 个样本值。结果不太平滑,但速度相当快,如果想要更多细节,可以放大它以提高分辨率(从而重新计算步骤):I changed the code to make the variable
step
dependant on the proportion between the available pixels to plot and the interval lenght of the data do be plot. This way, if the window has only, say, 1000 pixels, a "slice" of the whole interval will be taken, which have only 1000 sample values. The result is not so smooth, but it's quite fast, and if one wants more detail, it could be zoomed in to increase resolution (thus recalculating the step):如果性能不太重要,我建议您使用 matplotlib。它非常完美,并且可以与包括 GtkEgg 在内的多个后端一起使用(只要我记得)
If performance does not much matter, I suggest you to use matplotlib. Its very perfect, and works with several backends including GtkEgg (as long as I remember)