Merge pull request #2014 from arjones6/master

Add wxDC.DrawLinesFromBuffer to draw directly from numpy array memory using the Buffer protocol
This commit is contained in:
Robin Dunn
2021-12-15 18:55:41 -08:00
committed by GitHub
5 changed files with 273 additions and 1 deletions

131
demo/DrawLinesFromBuffer.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/env python
import time
import wx
npts = 100000
def TestLinesFromBuffer(dc, log):
global npts
try:
import numpy as np
start = time.time()
w, h = dc.GetSize()
vs = np.linspace(0, 1, npts).reshape((npts, 1))
x1 = np.cos(vs * 12 * np.pi) * w/2 * vs + w/2
x2 = np.cos(vs * 16 * np.pi) * w/2 * vs + w/2
y1 = np.sin(vs * 12 * np.pi) * w/2 * vs + h/2
y2 = np.sin(vs * 16 * np.pi) * w/2 * vs + h/2
# Data has to be the same size as a C integer
pts1 = np.append(x1, y1, 1).astype('intc')
pts2 = np.append(x2, y2, 1).astype('intc')
dc.SetPen(wx.BLACK_PEN)
t1 = time.time()
dc.DrawLines(pts1)
t2 = time.time()
dc.SetPen(wx.RED_PEN)
t3 = time.time()
dc.DrawLinesFromBuffer(pts2)
t4 = time.time()
log.write("%s pts: %s seconds with DrawLines %s seconds with DrawLinesFromBuffer\n" % (npts, t2 - t1, t4 - t3))
except ImportError:
log.write("Couldn't import numpy")
pass
# Class used for all the various sample pages; the mechanics are the same
# for each one with regards to the notebook. The only difference is
# the function we use to draw on it.
class DrawPanel(wx.Panel):
def __init__(self, parent, drawFun, log):
wx.Panel.__init__(self, parent, -1)
self.SetBackgroundColour(wx.WHITE)
self.log = log
self.drawFun = drawFun
self.Bind(wx.EVT_PAINT, self.OnPaint)
def OnSize(self, evt):
self.Refresh()
def OnPaint(self, evt):
dc = wx.PaintDC(self)
dc.Clear()
self.drawFun(dc,self.log)
#----------------------------------------------------------------------
def runTest(frame, nb, log):
global npts
panel = wx.Panel(nb, -1)
vsizer = wx.BoxSizer(wx.VERTICAL)
hsizer = wx.BoxSizer(wx.HORIZONTAL)
hsizer.Add(wx.StaticText(panel, -1, "# of Points"), 0, wx.ALIGN_CENTER|wx.ALL, 5)
npts_ctrl = wx.TextCtrl(panel, -1, str(npts))
hsizer.Add(npts_ctrl, 0, wx.ALIGN_CENTER|wx.ALL, 5)
button = wx.Button(panel, -1, "Refresh")
hsizer.Add(button, 0, wx.ALIGN_CENTER|wx.ALL, 5)
vsizer.Add(hsizer, 0, wx.ALIGN_CENTER, 0)
win = DrawPanel(panel, TestLinesFromBuffer, log)
vsizer.Add(win, 1, wx.GROW, 0)
panel.SetSizer(vsizer)
def update_npts(evt):
global npts
val = npts_ctrl.GetValue()
try:
npts = int(val)
except ValueError:
log.write("Error converting %s to an int" % (val))
win.Refresh()
button.Bind(wx.EVT_BUTTON, update_npts)
return panel
#----------------------------------------------------------------------
overview = """\
The DrawLinesFromBuffer function has been added to wx.DC to provide
a way to draw directly from a numpy array or other object that implements
the python buffer protocol.
<pre>
DrawLinesFromBuffer(pyBuff)
</pre>
The buffer object needs to provide an array of C integers organized as
x, y point pairs. The size of a C integer is platform dependent.
With numpy, the intc data type will provide the appropriate element size.
If called with an object that doesn't support
the python buffer protocol, or if the underlying element size does not
match the size of a C integer, a TypeError exception is raised. If
the buffer provided has float data with the same element size as a
C integer, no error will be raised, but the lines will not be drawn
in the appropriate places.
"""
if __name__ == '__main__':
import sys,os
import run
run.main(['', os.path.basename(sys.argv[0])] + sys.argv[1:])

View File

@@ -50,6 +50,7 @@ _treeList = [
'ActivityIndicator',
'GenericCheckBox',
'CheckListCtrl',
'DrawLinesFromBuffer',
]),
# managed windows == things with a (optional) caption you can close
@@ -275,6 +276,7 @@ _treeList = [
'Cairo_Snippets',
'ColourDB',
'DragScroller',
'DrawLinesFromBuffer',
'DrawXXXList',
'FileHistory',
'FontEnumerator',

View File

@@ -310,6 +310,9 @@ def run():
'(PyObject* textList, PyObject* pyPoints, PyObject* foregroundList, PyObject* backgroundList)',
body="return wxPyDrawTextList(*self, textList, pyPoints, foregroundList, backgroundList);")
c.addCppMethod('PyObject*', '_DrawLinesFromBuffer',
'(PyObject* pyBuff)',
body="return wxPyDrawLinesFromBuffer(*self, pyBuff);")
c.addPyMethod('DrawPointList', '(self, points, pens=None)',
doc="""\
@@ -475,6 +478,28 @@ def run():
""")
c.addPyMethod('DrawLinesFromBuffer', '(self, pyBuff)',
doc="""\
Implementation of DrawLines that can use numpy arrays, or anything else that uses the
python buffer protocol directly without any element conversion. This provides a
significant performance increase over the standard DrawLines function.
The pyBuff argument needs to provide an array of C integers organized as
x, y point pairs. The size of a C integer is platform dependent.
With numpy, the intc data type will provide the appropriate element size.
If called with an object that doesn't support
the python buffer protocol, or if the underlying element size does not
match the size of a C integer, a TypeError exception is raised. If
the buffer provided has float data with the same element size as a
C integer, no error will be raised, but the lines will not be drawn
in the appropriate places.
:param pyBuff: A python buffer containing integer pairs
""",
body="""\
return self._DrawLinesFromBuffer(pyBuff)
""")
#-----------------------------------------------------------------

View File

@@ -9,7 +9,6 @@
// Licence: wxWindows license
//--------------------------------------------------------------------------
typedef bool (*wxPyDrawListOp_t)(wxDC& dc, PyObject* coords);
PyObject* wxPyDrawXXXList(wxDC& dc, wxPyDrawListOp_t doDraw,
@@ -466,3 +465,53 @@ error0:
PyErr_SetString(PyExc_TypeError, "Expected a sequence of length-2 sequences or wx.Points.");
return NULL;
}
PyObject* wxPyDrawLinesFromBuffer(wxDC& dc, PyObject* pyBuff)
{
wxPyBlock_t blocked = wxPyBeginBlockThreads();
Py_buffer view;
PyObject* retval;
if (!PyObject_CheckBuffer(pyBuff)) {
goto err0;
}
if (PyObject_GetBuffer(pyBuff, &view, PyBUF_CONTIG) < 0) {
goto err1;
}
if (view.itemsize * 2 != sizeof(wxPoint)) {
goto err2;
}
dc.DrawLines(view.len / view.itemsize / 2, (wxPoint *)view.buf);
PyBuffer_Release(&view);
Py_INCREF(Py_None);
retval = Py_None;
goto exit;
err0:
PyErr_SetString(PyExc_TypeError, "Expected a buffer object");
retval = NULL;
goto exit;
err1:
// PyObject_GetBuffer raises exception already
retval = NULL;
goto exit;
err2:
PyErr_SetString(PyExc_TypeError, "Item size does not match wxPoint size");
retval = NULL;
goto exit;
exit:
wxPyEndBlockThreads(blocked);
return retval;
}

View File

@@ -0,0 +1,65 @@
import unittest
from unittests import wtc
import wx
import random
try:
import numpy as np
haveNumpy = True
except ImportError:
haveNumpy = False
#---------------------------------------------------------------------------
w = 600
h = 400
def makeRandomLines():
lines = []
for i in range(num):
x1 = random.randint(0, w)
y1 = random.randint(0, h)
x2 = random.randint(0, w)
y2 = random.randint(0, h)
lines.append( (x1,y1, x2,y2) )
return lines
#---------------------------------------------------------------------------
class dcDrawLists_Tests(wtc.WidgetTestCase):
@unittest.skipIf(not haveNumpy, "Numpy required for this test")
def test_dcDrawLinesFromBuffer(self):
pnl = wx.Panel(self.frame)
self.frame.SetSize((w,h))
dc = wx.ClientDC(pnl)
dc.SetPen(wx.Pen("BLACK", 1))
xs = np.linspace(0, w, w + 1)
ys = np.sin(xs / w * 2 * np.pi)
xs.shape = xs.size, 1
ys.shape = ys.size, 1
pts = np.append(xs, ys, 1)
dc.DrawLinesFromBuffer(pts.astype('intc'))
self.assertRaises(TypeError, dc.DrawLinesFromBuffer,
pts.astype('int64') if np.intc(1).nbytes != np.int64(1).nbytes else pts.astype('int32'))
self.assertRaises(TypeError, dc.DrawLinesFromBuffer, pts.tolist())
del dc
#---------------------------------------------------------------------------
if __name__ == '__main__':
unittest.main()