Animated interactive vibrating string using python - python

I would like to plot an animated vibrating string using python but to be able to play it and to control the parameters used during the vibration (much like this Desmos calculation).
So far, this is my code:
from __future__ import print_function
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, interactive, fixed, interact_manual
import ipywidgets as widgets
import matplotlib as mpl
%matplotlib inline
def f(n=1, v=0.2, L=2, t=0):
x = np.linspace(0, L, 2001)
func = np.sin((n*np.pi*x)/L)*np.cos((n*np.pi*v*t)/L)
plt.figure(figsize=(6,6))
ax1 = plt.plot(x, func)
plt.show()
interactive_plot = interactive(f, n=(0, 10, 1), v=(0.2, 5, 0.1), L=(0.2, 2, 0.1), t=(0, 10, 1))
output = interactive_plot.children[-1]
interactive_plot
I can control the wavefunction and all parameters, but I am not sure about what is the easiest way to animate it.
So far, I know that matplotlib can do it, but I am wondering if we have a more straightforward way to do animated interactive plots (using another package, maybe?).
Thanks in advance for any help.

Here my version of your script useing opencv and numpy.
You can set all params in realtime using keyboard.
More info in the code
import numpy as np
import cv2
def f(im,n=1, v=0.2, L=2, t=0):
x = np.linspace(0, L, 2001)
func = np.sin( (n*np.pi*x) / L ) * np.cos( (n*np.pi*v*t) / L )
ww2 = int(win_w/2)
wh2 = int(win_h/2)
scale = 100
for i in range(len(x)):
ix=x[i]
iy=func[i]
p = (int(ww2 + ix*scale) , int(wh2 + iy*scale))
cv2.circle( im, p ,1, (255,255,255) )
win_w=640
win_h=480
#params={
# "n":(0, 10, 1),
# "v":(0.2, 5, 0.1),
# "L":(0.2, 2, 0.1),
# "t":(0, 10, 1)
#}
params={
"n":[10],
"v":[5],
"L":[2],
"t":[0]
}
while True:
im = np.zeros( (win_h,win_w,3), dtype="uint8")
for i in range(len(params["n"])):
n=params["n"][i]
v=params["v"][i]
L=params["L"][i]
t=params["t"][i]
f(im,n,v,L,t)
cv2.imshow("f",im)
k = cv2.waitKey(33) & 0xFF
if k==ord('q'):break
# Parameter setting with the keyboard
# comment / uncomment this for animation:
params["t"][0]+=.001
# n param setting +,- use n,b
if k==ord('n'): params["n"][0]+=.001
if k==ord('b'): params["n"][0]-=.001
# v param setting +,- use v,c
if k==ord('v'): params["v"][0]+=.001
if k==ord('c'): params["v"][0]-=.001
# L param setting +,- use l,k
if k==ord('l'): params["L"][0]+=.001
if k==ord('k'): params["L"][0]-=.001
# t param setting +,- use t,r
if k==ord('t'): params["t"][0]+=.001
if k==ord('r'): params["t"][0]-=.001
print(params)
cv2.destroyAllWindows()

Take a look at the gif python package. I have only used it for simple gif saves. But it can be integrated with plotly or Altair, which may give better interactions.

Here is a simple plot I just made based on this YouTube video.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button
x = np.arange(0,np.pi,0.01)
y_base = 0.5*np.sin(x)
y = y_base+1
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.1, bottom=0.35)
p, = plt.plot(x,y, linewidth=2, color='blue')
plt.axis([0,np.pi,0,2])
axSlider = plt.axes([0.1,0.2,0.8,0.05])
slider1 = Slider(axSlider, "Slider", valmin=-100, valmax=100)
def val_update(val):
yval = slider1.val/50
p.set_ydata(yval*y_base+1)
plt.draw()
slider1.on_changed(val_update)
plt.show()

Related

How to display audio at the right side of matplotlib

The following code display the image and audio in the top-bottom style:
Here is the test code:
import librosa
import matplotlib.pyplot as plt
import IPython.display as ipd
def plot_it(name, audio, sample_rate):
plt.figure(figsize=(8, 1))
plt.plot(audio)
plt.gca().set_title(name)
plt.show()
ipd.display(ipd.Audio(data=audio, rate=sample_rate))
Is it possible for changing the "top-bottom" style to "left-right" style for displaying the audio at the right side of the plt figure?
You can use a GridspecLayout which is similar to matplotlib's GridSpec. In order to direct to output into the needed grid cells, you can capture it using the Output widget:
import librosa
import matplotlib.pyplot as plt
import IPython.display as ipd
from ipywidgets import Output, GridspecLayout
def plot_it(name, audio, sample_rate):
grid = GridspecLayout(1, 2, align_items='center')
out = Output()
with out:
fig, ax = plt.subplots(figsize=(8, 1))
ax.plot(audio)
ax.set_title(name)
plt.close(fig)
ipd.display(ax.figure)
grid[0, 0] = out
out = Output()
with out:
ipd.display(ipd.Audio(data=audio, rate=sample_rate))
grid[0, 1] = out
ipd.display(grid)
name = 'nutcracker'
filename = librosa.example(name)
y, sr = librosa.load(filename)
plot_it(name, y, sr)
(It is essential to close the figure, otherwise you'll have double output of the figure. This is easier to do this using the OOP than the pyplot interface, that's why I changed your matplotlib code a bit)

matplotlib custom path markers not displaying correctly

just wondering if anybody has experience with matplotlib custom markers
I want each marker in my plot to be a pie chart. To achieve this, my strategy was to create custom markers using the path class, method wedge.
https://matplotlib.org/stable/api/path_api.html
However is not displaying correctly, in particular with wedges defined with angles in the left quadrants. However, the path defined by the wedge class method seems to be correct and wedges are displayed correctly if using PathPatch and .add_patch()
See example below
import numpy as np
import math
import matplotlib.path as mpath
import matplotlib.cm
import matplotlib.pyplot as plt
import matplotlib.patches as patches
#Create wedges from angles
angles = np.array( [0,140,160,360] ) #Wedges angles
wedges=[]
for i in range(len(angles)-1):
angle0= angles[i]
angle1= angles[i+1]
dangle = angle1-angle0
wedge0=None
if dangle>0:
wedge0= mpath.Path.wedge(angle0, angle1)
wedge0= mpath.Path.wedge(angle0, angle1)
wedges.append(wedge0)
fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot(121)
ax1.set_xlim(-1, 1)
ax1.set_ylim(-1, 1)
ax2 = fig.add_subplot(122)
ax2.set_xlim(-2, 2)
ax2.set_ylim(-2, 2)
tab10 = matplotlib.cm.get_cmap('tab10')
for i, w0 in enumerate(wedges):
ax1.scatter(0,0, marker=w0, c = [tab10(i)], s=20000) #Use path markers
patch = patches.PathPatch(w0, color=tab10(i)) #Use patch
ax2.add_patch(patch)
plt.show()
Notice that the wedge on the left plot is sticking out, which is not supposed to.
Is this a bug in the matplotlib markers' code?
I managed to get the pie charts to display correctly.
Scaling by doing affine transforms does not help because the path markaers are all resized, as in
line 495 of markers.py .
def _set_custom_marker(self, path):
rescale = np.max(np.abs(path.vertices)) # max of x's and y's.
self._transform = Affine2D().scale(0.5 / rescale)
self._path = path
My solution is to modify the vertices in the created wedges by inserting new vertices that define a bounding box, slightly larger than the circle with radius 1.
Here is the modified code
import numpy as np
import matplotlib.path as mpath
import matplotlib.cm
import matplotlib.pyplot as plt
import matplotlib.patches as patches
def getBoundedWedge(angle0, angle1):
wedge0= mpath.Path.wedge(angle0, angle1)
#print(f"wedge0:{wedge0}")
vertices = wedge0.vertices
codes = wedge0.codes
#Add ghost vertices to define bounding box
vertices= np.insert( vertices, 0, [[1.1,1.1], [-1.1,1.1] , [-1.1,-1.1], [1.1,-1.1]] , axis=0)
codes = np.insert( codes, 0, [1,1,1,1])
wedgeextra = mpath.Path(vertices, codes)
return wedgeextra
#Create wedges from angles
angles = np.array( [0,140,160,360] ) #Wedges angles
wedges=[]
for i in range(len(angles)-1):
angle0= angles[i]
angle1= angles[i+1]
dangle = angle1-angle0
wedge0=None
if dangle>0:
wedge0= getBoundedWedge(angle0, angle1)
wedges.append(wedge0)
fig = plt.figure(figsize=(10,5))
ax1 = fig.add_subplot(121)
ax1.set_xlim(-1, 1)
ax1.set_ylim(-1, 1)
ax2 = fig.add_subplot(122)
ax2.set_xlim(-2, 2)
ax2.set_ylim(-2, 2)
tab10 = matplotlib.cm.get_cmap('tab10')
for i, w0 in enumerate(wedges):
ax1.scatter(0,0, marker=w0, c = [tab10(i)], s=20000) #Use path markers
patch = patches.PathPatch(w0, color=tab10(i)) #Use patch
ax2.add_patch(patch)
plt.show()
And the output is as follows

How to draw animation by taking snapshot with matplotlib?

In my project, I have many polygons to draw for each time step.
At each step, the number of polygons varies, thus it is difficult to keep Axes.patchs and translate them to make the animation.
I want to create animation with final figures (show after calling matplotlib.pyplot.show()), how to do this?
We take the sin curve as example:
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
fig = plt.figure()
ims = []
x = np.linspace(0, 2 * np.pi, 100)
y = np.sin(x)
z = np.cos(x)
for i in range(1,100):
tmpx = x[:i]
tmpy = y[:i]
tmpz = z[:i]
plt.plot(tmpx, tmpz)
im = plt.plot(tmpx, tmpy)
ims.append(im)
ani = animation.ArtistAnimation(fig, ims, interval=200)
ani.save('/home/test.gif', writer='imagemagick')
plt.show()
There are two curves: animated-sin-curve and static-cos-curve.
the sin-curve is kept as Line2D objects for each step
the cos-curve stay static for each step.
In this way, we show different Artist object for each step.
But I want to keep the rasterized Line2D figure for each step.
I find classes of AxesImage/FigureImage, but I don't know how to save the rasterized figure and make them work.
I tried to convert figure.canvas to AxesImage with following code :
def fig2AxesImage(fig):
import PIL.Image as Image
fig.canvas.draw()
w, h = fig.canvas.get_width_height()
buf = numpy.fromstring(fig.canvas.tostring_argb(), dtype=numpy.uint8)
buf.shape = (w, h, 4)
# canvas.tostring_argb give pixmap in ARGB mode. Roll the ALPHA channel to have it in RGBA mode
buf = numpy.roll(buf, 3, axis=2)
image = Image.frombytes("RGBA", (w, h), buf.tostring())
image = numpy.asarray(image)
return plt.imshow(image, animated=True)
but with this way, I have to clear canvas at start of next frame, which make the final animation a blank video. (but the .jpg figures I output for each step get the right content)
Does anyone have done this before that save rasterized canvas-figures of matplotlib.pyplot.figure() as a animation Vedio?
celluloid for python 2.7
''' copy from celluloid'''
# from typing import Dict, List # not supported by python 2.7. So comment it
from collections import defaultdict
from matplotlib.figure import Figure
from matplotlib.artist import Artist
from matplotlib.animation import ArtistAnimation
__version__ = '0.2.0'
class Camera:
def __init__(self, figure):
self.figure_ = figure
self.offsets_ = { k:defaultdict(int) \
for k in ['collections', 'patches', 'lines', 'texts', 'artists', 'images']
}
self.photos_ = []
def snap(self):
frame_artists = []
for i, axis in enumerate(self.figure_.axes):
if axis.legend_ is not None:
axis.add_artist(axis.legend_)
for name in self.offsets_:
new_artists = getattr(axis, name)[self.offsets_[name][i]:]
frame_artists += new_artists
self.offsets_[name][i] += len(new_artists)
self.photos_.append(frame_artists)
def animate(self):
return ArtistAnimation(self.figure_, self.photos_)

Change format string of axis picker in matplotlib [duplicate]

Short version: is there a Python method for displaying an image which shows, in real time, the pixel indices and intensities? So that as I move the cursor over the image, I have a continually updated display such as pixel[103,214] = 198 (for grayscale) or pixel[103,214] = (138,24,211) for rgb?
Long version:
Suppose I open a grayscale image saved as an ndarray im and display it with imshow from matplotlib:
im = plt.imread('image.png')
plt.imshow(im,cm.gray)
What I get is the image, and in the bottom right of the window frame, an interactive display of the pixel indices. Except that they're not quite, as the values are not integers: x=134.64 y=129.169 for example.
If I set the display with correct resolution:
plt.axis('equal')
the x and y values are still not integers.
The imshow method from the spectral package does a better job:
import spectral as spc
spc.imshow(im)
Then in the bottom right I now have pixel=[103,152] for example.
However, none of these methods also shows the pixel values. So I have two questions:
Can the imshow from matplotlib (and the imshow from scikit-image) be coerced into showing the correct (integer) pixel indices?
Can any of these methods be extended to show the pixel values as well?
There a couple of different ways to go about this.
You can monkey-patch ax.format_coord, similar to this official example. I'm going to use a slightly more "pythonic" approach here that doesn't rely on global variables. (Note that I'm assuming no extent kwarg was specified, similar to the matplotlib example. To be fully general, you need to do a touch more work.)
import numpy as np
import matplotlib.pyplot as plt
class Formatter(object):
def __init__(self, im):
self.im = im
def __call__(self, x, y):
z = self.im.get_array()[int(y), int(x)]
return 'x={:.01f}, y={:.01f}, z={:.01f}'.format(x, y, z)
data = np.random.random((10,10))
fig, ax = plt.subplots()
im = ax.imshow(data, interpolation='none')
ax.format_coord = Formatter(im)
plt.show()
Alternatively, just to plug one of my own projects, you can use mpldatacursor for this. If you specify hover=True, the box will pop up whenever you hover over an enabled artist. (By default it only pops up when clicked.) Note that mpldatacursor does handle the extent and origin kwargs to imshow correctly.
import numpy as np
import matplotlib.pyplot as plt
import mpldatacursor
data = np.random.random((10,10))
fig, ax = plt.subplots()
ax.imshow(data, interpolation='none')
mpldatacursor.datacursor(hover=True, bbox=dict(alpha=1, fc='w'))
plt.show()
Also, I forgot to mention how to show the pixel indices. In the first example, it's just assuming that i, j = int(y), int(x). You can add those in place of x and y, if you'd prefer.
With mpldatacursor, you can specify them with a custom formatter. The i and j arguments are the correct pixel indices, regardless of the extent and origin of the image plotted.
For example (note the extent of the image vs. the i,j coordinates displayed):
import numpy as np
import matplotlib.pyplot as plt
import mpldatacursor
data = np.random.random((10,10))
fig, ax = plt.subplots()
ax.imshow(data, interpolation='none', extent=[0, 1.5*np.pi, 0, np.pi])
mpldatacursor.datacursor(hover=True, bbox=dict(alpha=1, fc='w'),
formatter='i, j = {i}, {j}\nz = {z:.02g}'.format)
plt.show()
An absolute bare-bones "one-liner" to do this: (without relying on datacursor)
def val_shower(im):
return lambda x,y: '%dx%d = %d' % (x,y,im[int(y+.5),int(x+.5)])
plt.imshow(image)
plt.gca().format_coord = val_shower(ims)
It puts the image in closure so makes sure if you have multiple images each will display its own values.
All of the examples that I have seen only work if your x and y extents start from 0. Here is code that uses your image extents to find the z value.
import numpy as np
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
d = np.array([[i+j for i in range(-5, 6)] for j in range(-5, 6)])
im = ax.imshow(d)
im.set_extent((-5, 5, -5, 5))
def format_coord(x, y):
"""Format the x and y string display."""
imgs = ax.get_images()
if len(imgs) > 0:
for img in imgs:
try:
array = img.get_array()
extent = img.get_extent()
# Get the x and y index spacing
x_space = np.linspace(extent[0], extent[1], array.shape[1])
y_space = np.linspace(extent[3], extent[2], array.shape[0])
# Find the closest index
x_idx= (np.abs(x_space - x)).argmin()
y_idx= (np.abs(y_space - y)).argmin()
# Grab z
z = array[y_idx, x_idx]
return 'x={:1.4f}, y={:1.4f}, z={:1.4f}'.format(x, y, z)
except (TypeError, ValueError):
pass
return 'x={:1.4f}, y={:1.4f}, z={:1.4f}'.format(x, y, 0)
return 'x={:1.4f}, y={:1.4f}'.format(x, y)
# end format_coord
ax.format_coord = format_coord
If you are using PySide/PyQT here is an example to have a mouse hover tooltip for the data
import matplotlib
matplotlib.use("Qt4Agg")
matplotlib.rcParams["backend.qt4"] = "PySide"
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
# Mouse tooltip
from PySide import QtGui, QtCore
mouse_tooltip = QtGui.QLabel()
mouse_tooltip.setFrameShape(QtGui.QFrame.StyledPanel)
mouse_tooltip.setWindowFlags(QtCore.Qt.ToolTip)
mouse_tooltip.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
mouse_tooltip.show()
def show_tooltip(msg):
msg = msg.replace(', ', '\n')
mouse_tooltip.setText(msg)
pos = QtGui.QCursor.pos()
mouse_tooltip.move(pos.x()+20, pos.y()+15)
mouse_tooltip.adjustSize()
fig.canvas.toolbar.message.connect(show_tooltip)
# Show the plot
plt.show()
with Jupyter you can do so either with datacursor(myax)or by ax.format_coord.
Sample code:
%matplotlib nbagg
import numpy as np
import matplotlib.pyplot as plt
X = 10*np.random.rand(5,3)
fig,ax = plt.subplots()
myax = ax.imshow(X, cmap=cm.jet,interpolation='nearest')
ax.set_title('hover over the image')
datacursor(myax)
plt.show()
the datacursor(myax) can also be replaced with ax.format_coord = lambda x,y : "x=%g y=%g" % (x, y)
In case you, like me, work on Google Colab, this solutions do not work as Colab disabled interactive feature of images for matplotlib.
Then you might simply use Plotly:
https://plotly.com/python/imshow/
import plotly.express as px
import numpy as np
img_rgb = np.array([[[255, 0, 0], [0, 255, 0], [0, 0, 255]],
[[0, 255, 0], [0, 0, 255], [255, 0, 0]]
], dtype=np.uint8)
fig = px.imshow(img_rgb)
fig.show()
Matplotlib has built-in interactive plot which logs pixel values at the corner of the screen.
To setup first install pip install ipympl
Then use either %matplotlib notebook or %matplotlib widget instead of %matplotlib inline
The drawback with plotly or Bokeh is that they don't work on Pycharm.
For more information take a look at the doc
To get interactive pixel information of an image use the module imagetoolbox
To download the module open the command prompt and write
pip install imagetoolbox
Write the given code to get interactive pixel information of an image
enter image description here
Output:enter image description here

IPython notebook interactive function: how to set the slider range

I wrote the code below in Ipython notebook to generate a sigmoid function controlled by parameters a which defines the position of the sigmoid center, and b which defines its width:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
def sigmoid(x,a,b):
#sigmoid function with parameters a = center; b = width
s= 1/(1+np.exp(-(x-a)/b))
return 100.0*(s-min(s))/(max(s)-min(s)) # normalize sigmoid to 0-100
x = np.linspace(0,10,256)
sigm = sigmoid(x, a=5, b=1)
fig = plt.figure(figsize=(24,6))
ax1 = fig.add_subplot(2, 1, 1)
ax1.set_xticks([])
ax1.set_xticks([])
plt.plot(x,sigm,lw=2,color='black')
plt.xlim(x.min(), x.max())
I wanted to add interactivity for parameters a and b so I re-wrote the function as below:
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from IPython.html.widgets import interactive
from IPython.display import display
def sigmoid_demo(a=5,b=1):
x = np.linspace(0,10,256)
s = 1/(1+np.exp(-(x-a)/(b+0.1))) # +0.1 to avoid dividing by 0
sn = 100.0*(s-min(s))/(max(s)-min(s)) # normalize sigmoid to 0-100
fig = plt.figure(figsize=(24,6))
ax1 = fig.add_subplot(2, 1, 1)
ax1.set_xticks([])
ax1.set_yticks([])
plt.plot(x,sn,lw=2,color='black')
plt.xlim(x.min(), x.max())
w=widgets.interactive(sigmoid_demo,a=5,b=1)
display(w)
Is there any way to se the range of the sliders to be symmetrical (for example around zero)? It does not seem to me to be possible by just setting the starting value for the parameters.
You can create widgets manually and bind them to variables in the interactive function. This way you are much more flexible and can tailor those widgets to your needs.
This example creates two different sliders and sets their max, min, stepsize and initial value and uses them in the interactive function.
a_slider = widgets.IntSliderWidget(min=-5, max=5, step=1, value=0)
b_slider = widgets.FloatSliderWidget(min=-5, max=5, step=0.3, value=0)
w=widgets.interactive(sigmoid_demo,a=a_slider,b=b_slider)
display(w)

Categories

Resources