ginput: blocking call for mouse input

Hi all,

A while ago (a year or so), I was looking for a ginput-like function with
matplotlib. For those who don't know what I am talking about, it is a
blocking call that can be used in a script to ask the user to enter
coordinnate by clicking on the figure. This is incredibly handy, as it
allows some simple user interaction without worrying about events loop
and Co.

At the time I gave up on that. I have made progress with GUI and event
loops, and a recent post on the matplotlib-user list put me back to work
on this (
http://www.nabble.com/ginput-is-here!-(for-wx-anyway)-to14960506.html
).

I have a preliminary patch to add this feature to matplotlib that I
attach. I add a method to the backend canvases, to be able to deal with
event-loop-handling, and following Jack Gurkesaft I create my own a small
event-loop to poll for events inside my function. This is a kludge, but it
allows to block the function while still processing events. If you have
better ideas.

I have implemented the functionnality for all the interactive backends I
could test (and thus not cocoa), but I am not too sure my gtk code is
kosher, I wouldn't mind advice on this one.

The code of ginput itself is currently in a seperate file
(matplotlib.ginput, that you can run for a demo) because I wasn't too
sure where to add it.

This is a first version of the patch. I would like comments on it,
especially where to add the code itself, and if the proposed additions to
the backends look all right.

Once I have comments, I plan to:

    * Make right click cancel an input
    * Allow infinite number of inputs, terminated by right-click
    * Add optional display of inputs
    * Rework docstrings

More suggestions welcomed,

This could (and should, IMHO) open the road to other blocking calls for
user interaction, like the selection of a window, as they make life
really easy for people wanting to hack quick data analysis scripts.

Cheers,

Ga�l

ginput.diff (5.61 KB)

Ooops, I had forgotten to add the Wx backend. Here is a new patch.

By the way, with the wx backend, there seems to be a simple mistake in
the "show" method of the figure manager, to reproduce the traceback do
(with a recent ipython):

ipython -wthread -pylab
In [1]: f = figure()

In [2]: f.show()

Cheers,

Ga�l

ginput.diff (6.11 KB)

···

On Wed, Jan 30, 2008 at 03:05:13AM +0100, Gael Varoquaux wrote:

Hi all,

A while ago (a year or so), I was looking for a ginput-like function with
matplotlib. For those who don't know what I am talking about, it is a
blocking call that can be used in a script to ask the user to enter
coordinnate by clicking on the figure. This is incredibly handy, as it
allows some simple user interaction without worrying about events loop
and Co.

At the time I gave up on that. I have made progress with GUI and event
loops, and a recent post on the matplotlib-user list put me back to work
on this (
http://www.nabble.com/ginput-is-here!-(for-wx-anyway)-to14960506.html
).

I have a preliminary patch to add this feature to matplotlib that I
attach. I add a method to the backend canvases, to be able to deal with
event-loop-handling, and following Jack Gurkesaft I create my own a small
event-loop to poll for events inside my function. This is a kludge, but it
allows to block the function while still processing events. If you have
better ideas.

I have implemented the functionnality for all the interactive backends I
could test (and thus not cocoa), but I am not too sure my gtk code is
kosher, I wouldn't mind advice on this one.

The code of ginput itself is currently in a seperate file
(matplotlib.ginput, that you can run for a demo) because I wasn't too
sure where to add it.

This is a first version of the patch. I would like comments on it,
especially where to add the code itself, and if the proposed additions to
the backends look all right.

Once I have comments, I plan to:

    * Make right click cancel an input
    * Allow infinite number of inputs, terminated by right-click
    * Add optional display of inputs
    * Rework docstrings

More suggestions welcomed,

This could (and should, IMHO) open the road to other blocking calls for
user interaction, like the selection of a window, as they make life
really easy for people wanting to hack quick data analysis scripts.

Cheers,

Ga�l

Index: trunk/matplotlib/lib/matplotlib/backend_bases.py

--- trunk/matplotlib/lib/matplotlib/backend_bases.py (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backend_bases.py (working copy)
@@ -1151,7 +1151,13 @@
         """
         return self.callbacks.disconnect(cid)

+ def flush_events(self):
+ """ Flush the GUI events for the figure. Implemented only for
+ backends with GUIs.
+ """
+ raise NotImplementedError

+
class FigureManagerBase:
     """
     Helper class for matlab mode, wraps everything up into a neat bundle
Index: trunk/matplotlib/lib/matplotlib/ginput.py

--- trunk/matplotlib/lib/matplotlib/ginput.py (revision 0)
+++ trunk/matplotlib/lib/matplotlib/ginput.py (revision 0)
@@ -0,0 +1,80 @@
+
+from matplotlib.pyplot import gcf
+import time
+
+class BlockingMouseInput(object):
+ """ Class that creates a callable object to retrieve mouse click
+ in a blocking way, a la MatLab.
+ """
+
+ callback = None
+ verbose = False
+
+ def on_click(self, event):
+ """ Event handler that will be passed to the current figure to
+ retrieve clicks.
+ """
+ # if it's a valid click, append the coordinates to the list
+ if event.inaxes:
+ self.clicks.append((event.xdata, event.ydata))
+ if self.verbose:
+ print "input %i: %f,%f" % (len(self.clicks),
+ event.xdata, event.ydata)
+
+ def __call__(self, fig, n=1, timeout=30, verbose=False):
+ """ Blocking call to retrieve n coordinate pairs through mouse
+ clicks.
+ """
+
+ assert isinstance(n, int), "Requires an integer argument"
+
+ # Ensure that the current figure is shown
+ fig.show()
+ # connect the click events to the on_click function call
+ self.callback = fig.canvas.mpl_connect('button_press_event',
+ self.on_click)
+
+ # initialize the list of click coordinates
+ self.clicks = []
+
+ self.verbose = verbose
+
+ # wait for n clicks
+ counter = 0
+ while len(self.clicks) < n:
+ fig.canvas.flush_events()
+ # rest for a moment
+ time.sleep(0.01)
+
+ # check for a timeout
+ counter += 1
+ if timeout > 0 and counter > timeout/0.01:
+ print "ginput timeout";
+ break;
+
+ # All done! Disconnect the event and return what we have
+ fig.canvas.mpl_disconnect(self.callback)
+ self.callback = None
+ return self.clicks
+
+
+def ginput(n=1, timeout=30, verbose=False):
+ """
+ Blocking call to interact with the figure.
+
+ This will wait for n clicks from the user and return a list of the
+ coordinates of each click.
+
+ If timeout is negative, does not timeout.
+ """
+
+ blocking_mouse_input = BlockingMouseInput()
+ return blocking_mouse_input(gcf(), n, timeout, verbose=verbose)
+
+if __name__ == "__main__":
+ import pylab
+ t = pylab.arange(10)
+ pylab.plot(t, pylab.sin(t))
+ print "Please click"
+ ginput(3, verbose=True)
+
Index: trunk/matplotlib/lib/matplotlib/backends/backend_qt.py

--- trunk/matplotlib/lib/matplotlib/backends/backend_qt.py (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_qt.py (working copy)
@@ -175,6 +175,9 @@

         return key

+ def flush_events(self):
+ qt.qApp.processEvents()
+
class FigureManagerQT( FigureManagerBase ):
     """
     Public attributes
Index: trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py

--- trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_gtk.py (working copy)
@@ -386,6 +386,13 @@
     def get_default_filetype(self):
         return 'png'

+ def flush_events(self):
+ gtk.gdk.threads_enter()
+ while gtk.events_pending():
+ gtk.main_iteration(True)
+ gtk.gdk.flush()
+ gtk.gdk.threads_leave()
+

class FigureManagerGTK(FigureManagerBase):
     """
Index: trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py

--- trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_tkagg.py (working copy)
@@ -269,8 +269,9 @@
         key = self._get_key(event)
         FigureCanvasBase.key_release_event(self, key, guiEvent=event)

+ def flush_events(self):
+ self._master.update()

-
class FigureManagerTkAgg(FigureManagerBase):
     """
     Public attributes
Index: trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py

--- trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py (revision 4908)
+++ trunk/matplotlib/lib/matplotlib/backends/backend_qt4.py (working copy)
@@ -13,7 +13,7 @@
from matplotlib.mathtext import MathTextParser
from matplotlib.widgets import SubplotTool

-from PyQt4 import QtCore, QtGui
+from PyQt4 import QtCore, QtGui, Qt

backend_version = "0.9.1"
def fn_name(): return sys._getframe(1).f_code.co_name
@@ -174,6 +174,9 @@

         return key

+ def flush_events(self):
+ Qt.qApp.processEvents()
+
class FigureManagerQT( FigureManagerBase ):
     """
     Public attributes

-------------------------------------------------------------------------
This SF.net email is sponsored by: Microsoft
Defy all challenges. Microsoft(R) Visual Studio 2008.
http://clk.atdmt.com/MRT/go/vse0120000070mrt/direct/01/
_______________________________________________
Matplotlib-devel mailing list
Matplotlib-devel@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/matplotlib-devel

--
  Gael Varoquaux,
  Quantum degenerate gases group
  European Laboratory for Non-Linear Spectroscopy
    University of Florence, Polo Scientifico
    Via Nello Carrara 1, I-50019-Sesto-Fiorentino (Firenze) Italy
    Phone: ++ 390-55-457242145 Fax: ++ 390-55-4572451
  ++ and ++
  Groupe d'optique atomique,
  Laboratoire Charles Fabry de l'Institut d'Optique
    Campus Polytechnique, RD 128, 91127 Palaiseau cedex FRANCE
    Tel : 33 (0) 1 64 53 33 23 - Fax : 33 (0) 1 64 53 31 01
    Labs: 33 (0) 1 64 53 33 63 - 33 (0) 1 64 53 33 62

Hey Gael -- this is really cool. As you know, this has been a much
requested feature, and the hard part is to get something working
across backends, which it looks like you've done.

I suggest a minor reorganization. Get rid of ginput.py altogether,
add the BlockingMouseInput code to either backend_bases.py or
figure.py. Make a figure.Figure.ginput method, so folks can use it
outside of pylab, and then add a ginput method to pyplot.py which is a
thin wrapper to the Figure.ginput method.

If this seems like a good organization to you, I'll wait for a new
patch and then contribute that.

Thanks!
JDH

···

On Jan 29, 2008 8:15 PM, Gael Varoquaux <gael.varoquaux@...427...> wrote:

Ooops, I had forgotten to add the Wx backend. Here is a new patch.

By the way, with the wx backend, there seems to be a simple mistake in
the "show" method of the figure manager, to reproduce the traceback do
(with a recent ipython):

As you know, this has been a much requested feature,

I know. I have wanted it pretty badly.

and the hard part is to get something working across backends, which it
looks like you've done.

Looks like it works OK. I would appreciate heads up from people who know
GTK, as I am not too sure of possibly garbbling the mainloop with my
kludge. But I have tested it quit extensively and it seems to work.

I suggest a minor reorganization. Get rid of ginput.py altogether,
add the BlockingMouseInput code to either backend_bases.py or
figure.py. Make a figure.Figure.ginput method, so folks can use it
outside of pylab, and then add a ginput method to pyplot.py which is a
thin wrapper to the Figure.ginput method.

Yup, this seems like a good solution.

If this seems like a good organization to you, I'll wait for a new
patch and then contribute that.

Give me a few days, but it will come.

Cheers,

Ga�l

···

On Thu, Jan 31, 2008 at 08:20:37AM -0600, John Hunter wrote:

Here is the new patch. I added visual feedback when accumulating points.
I hope the docstrings are clear.

Cheers,

Ga�l

ginput.diff (7.63 KB)

···

On Thu, Jan 31, 2008 at 04:41:41PM +0100, Gael Varoquaux wrote:

> If this seems like a good organization to you, I'll wait for a new
> patch and then contribute that.

Give me a few days, but it will come.

Great -- thanks again. I applied this patch and created a new example
ginput_demo.py

Tested on GTKAgg and TkAgg on my system, and looks good so far.

JDH

···

On Feb 2, 2008 8:48 AM, Gael Varoquaux <gael.varoquaux@...427...> wrote:

Here is the new patch. I added visual feedback when accumulating points.
I hope the docstrings are clear.

Jack replied to me offlist so I am going to paste in his post below.
Perhaps you and Gael can consult on the ideal functionality of ginput
vis-a-vis optional line segment drawing, etc...

From Jack Sankey <jack.sankey@...149...>
to John Hunter <jdh2358@...149...>,
date Feb 5, 2008 4:02 PM
subject Re: [matplotlib-devel] ginput: blocking call for mouse input
mailed-by gmail.com
  
Woa, it's working on GTKAgg using wx.Yield()? You must have added some voodoo :slight_smile:

Also, my version of GaelInput has seemed to stop evolving. This
version has the option to draw lines between clicks, which I use a
lot. Also, the default timeout is 0 now, since you can always
right-click to abort.

-Jack

class GaelInput(object):
   """
   Class that create a callable object to retrieve mouse click in a
   blocking way, a la MatLab. Based on Gael Varoquaux's almost-working
   object. Thanks Gael! I've wanted to get this working for years!

   -Jack
   """

   debug = False
   cid = None # event connection object
   clicks = [] # list of click coordinates
   n = 1 # number of clicks we're waiting for
   lines = False # if we should draw the lines

   def on_click(self, event):
       """
       Event handler that will be passed to the current figure to
       retrieve clicks.
       """

       # write the debug information if we're supposed to
       if self.debug: print "button "+str(event.button)+":
"+str(event.xdata)+", "+str(event.ydata)

       # if this event's a right click we're done
       if event.button == 3:
           self.done = True
           return

       # if it's a valid click (and this isn't an extra event
       # in the queue), append the coordinates to the list
       if event.inaxes and not self.done:
           self.clicks.append([event.xdata, event.ydata])

           # now if we're supposed to draw lines, do so
           if self.lines and len(self.clicks) > 1:
               event.inaxes.plot([self.clicks[-1][0], self.clicks[-2][0]],
                                 [self.clicks[-1][1], self.clicks[-2][1]],
                                 color='w', linewidth=2.0,
scalex=False, scaley=False)
               event.inaxes.plot([self.clicks[-1][0], self.clicks[-2][0]],
                                 [self.clicks[-1][1], self.clicks[-2][1]],
                                 color='k', linewidth=1.0,
scalex=False, scaley=False)
               _pylab.draw()

       # if we have n data points, we're done
       if len(self.clicks) >= self.n and self.n is not 0:
           self.done = True
           return

   def __call__(self, n=1, timeout=0, debug=False, lines=False):
       """
       Blocking call to retrieve n coordinate pairs through mouse clicks.

       n=1 number of clicks to collect. Set n=0 to keep collecting
                       points until you click with the right mouse button.

       timeout=30 maximum number of seconds to wait for clicks
before giving up.
                       timeout=0 to disable

       debug=False show each click event coordinates

       lines=False draw lines between clicks
       """

       # just for printing the coordinates
       self.debug = debug

       # for drawing lines
       self.lines = lines

       # connect the click events to the on_click function call
       self.cid = _pylab.connect('button_press_event', self.on_click)

       # initialize the list of click coordinates
       self.clicks = []

       # wait for n clicks
       self.n = n
       self.done = False
       t = 0.0
       while not self.done:
           # key step: yield the processor to other threads
           _wx.Yield();
           _time.sleep(0.1)

           # check for a timeout
           t += 0.1
           if timeout and t > timeout: print "ginput timeout"; break;

       # All done! Disconnect the event and return what we have
       _pylab.disconnect(self.cid)
       self.cid = None
       return _numpy.array(self.clicks)

def ginput(n=1, timeout=0, show=True, lines=False):
   """
   Simple functional call for physicists. This will wait for n clicks
from the user and
   return a list of the coordinates of each click.

   n=1 number of clicks to collect
   timeout=30 maximum number of seconds to wait for clicks
before giving up.
                   timeout=0 to disable
   show=True print the clicks as they are received
   lines=False draw lines between clicks
   """

   x = GaelInput()
   return x(n, timeout, show, lines)

···

On Feb 5, 2008 3:58 PM, John Hunter <jdh2358@...149...> wrote:

On Feb 2, 2008 8:48 AM, Gael Varoquaux <gael.varoquaux@...427...> wrote:

> Here is the new patch. I added visual feedback when accumulating points.
> I hope the docstrings are clear.

Great -- thanks again. I applied this patch and created a new example
ginput_demo.py

I'm not at all attached to any particular functionality. Feel free to
mangle it as you see fit!

···

On Feb 5, 2008 5:11 PM, John Hunter <jdh2358@...149...> wrote:

On Feb 5, 2008 3:58 PM, John Hunter <jdh2358@...149...> wrote:
> On Feb 2, 2008 8:48 AM, Gael Varoquaux <gael.varoquaux@...427...> wrote:
>
> > Here is the new patch. I added visual feedback when accumulating points.
> > I hope the docstrings are clear.
>
> Great -- thanks again. I applied this patch and created a new example
> ginput_demo.py

Jack replied to me offlist so I am going to paste in his post below.
Perhaps you and Gael can consult on the ideal functionality of ginput
vis-a-vis optional line segment drawing, etc...

From Jack Sankey <jack.sankey@...149...>
to John Hunter <jdh2358@...149...>,
date Feb 5, 2008 4:02 PM
subject Re: [matplotlib-devel] ginput: blocking call for mouse input
mailed-by gmail.com

Woa, it's working on GTKAgg using wx.Yield()? You must have added some voodoo :slight_smile:

Also, my version of GaelInput has seemed to stop evolving. This
version has the option to draw lines between clicks, which I use a
lot. Also, the default timeout is 0 now, since you can always
right-click to abort.

-Jack

class GaelInput(object):
   """
   Class that create a callable object to retrieve mouse click in a
   blocking way, a la MatLab. Based on Gael Varoquaux's almost-working
   object. Thanks Gael! I've wanted to get this working for years!

   -Jack
   """

   debug = False
   cid = None # event connection object
   clicks = [] # list of click coordinates
   n = 1 # number of clicks we're waiting for
   lines = False # if we should draw the lines

   def on_click(self, event):
       """
       Event handler that will be passed to the current figure to
       retrieve clicks.
       """

       # write the debug information if we're supposed to
       if self.debug: print "button "+str(event.button)+":
"+str(event.xdata)+", "+str(event.ydata)

       # if this event's a right click we're done
       if event.button == 3:
           self.done = True
           return

       # if it's a valid click (and this isn't an extra event
       # in the queue), append the coordinates to the list
       if event.inaxes and not self.done:
           self.clicks.append([event.xdata, event.ydata])

           # now if we're supposed to draw lines, do so
           if self.lines and len(self.clicks) > 1:
               event.inaxes.plot([self.clicks[-1][0], self.clicks[-2][0]],
                                 [self.clicks[-1][1], self.clicks[-2][1]],
                                 color='w', linewidth=2.0,
scalex=False, scaley=False)
               event.inaxes.plot([self.clicks[-1][0], self.clicks[-2][0]],
                                 [self.clicks[-1][1], self.clicks[-2][1]],
                                 color='k', linewidth=1.0,
scalex=False, scaley=False)
               _pylab.draw()

       # if we have n data points, we're done
       if len(self.clicks) >= self.n and self.n is not 0:
           self.done = True
           return

   def __call__(self, n=1, timeout=0, debug=False, lines=False):
       """
       Blocking call to retrieve n coordinate pairs through mouse clicks.

       n=1 number of clicks to collect. Set n=0 to keep collecting
                       points until you click with the right mouse button.

       timeout=30 maximum number of seconds to wait for clicks
before giving up.
                       timeout=0 to disable

       debug=False show each click event coordinates

       lines=False draw lines between clicks
       """

       # just for printing the coordinates
       self.debug = debug

       # for drawing lines
       self.lines = lines

       # connect the click events to the on_click function call
       self.cid = _pylab.connect('button_press_event', self.on_click)

       # initialize the list of click coordinates
       self.clicks = []

       # wait for n clicks
       self.n = n
       self.done = False
       t = 0.0
       while not self.done:
           # key step: yield the processor to other threads
           _wx.Yield();
           _time.sleep(0.1)

           # check for a timeout
           t += 0.1
           if timeout and t > timeout: print "ginput timeout"; break;

       # All done! Disconnect the event and return what we have
       _pylab.disconnect(self.cid)
       self.cid = None
       return _numpy.array(self.clicks)

def ginput(n=1, timeout=0, show=True, lines=False):
   """
   Simple functional call for physicists. This will wait for n clicks
from the user and
   return a list of the coordinates of each click.

   n=1 number of clicks to collect
   timeout=30 maximum number of seconds to wait for clicks
before giving up.
                   timeout=0 to disable
   show=True print the clicks as they are received
   lines=False draw lines between clicks
   """

   x = GaelInput()
   return x(n, timeout, show, lines)

You can still use this behavoir, using a timeout of zero, n = 0, and the
middle click to end (I use right_click to cancel points). I am not sure
what the default timeout should be. If you have strong opinions about it
being 0, that's fine with me.

As far as n = 0 being the default, I think this is a bad idea. First of
all, it break matlab-compatibility for no good reasons, second in most
cases, IMHO, the naive programmer only wants one point, and puts some
logics afterwards. He will not read the doc, and wont understand why his
function is not returning after one click (so many people don't even know
how to read docstring, I am not kidding).

As for the lines, having lines implies that there is a connection order
in your points, which is not always the case. I suggest thus adding a
optional (oof by default) keyword argument for this behavior.

I am working late hours currently to try to improve the mayavi2 docs
before we do a major release, I can't work on this patchs. Jack, do you
feel like writing it. It should be pretty trivial by simply extending
what I wrote for displaying points, and cleaning them afterward, which is
the hardest part.

Sorry not to propose to do it myself, but I already put myself behind the
release schedule by hacking on this on saturday, and I am not even
talking about my day work.

Cheers,

Ga�l

···

On Tue, Feb 05, 2008 at 04:11:59PM -0600, John Hunter wrote:

Also, my version of GaelInput has seemed to stop evolving. This
version has the option to draw lines between clicks, which I use a
lot. Also, the default timeout is 0 now, since you can always
right-click to abort.