Chris, The advantage of the SpriteMulti method is that there is only one Shape requiring transformation and projection matrix multiplicaiton. It can have thousands of vertices each one with a different image, or part of an image mapped to a 'point' on each one (gl_PointSize can be as big as you want, even filling the whole screen). However the Raspberry Pi 3 can do this kind of thing pretty fast - the numpy module seems significantly faster, maybe it's utilising the extra processors. So if you can render 50 ImageSprites at 60fps, and that's enough to do what you want, I would stick with that (20fps is pretty much perfect animation on the RPi, much smoother than 60fps on this i5 laptop). To animate the texture mapping the best way is to alter the uniform variables passed to the shader from the Buffer umult, vmult, u_off, v_off to render a different part of a texture atlas image. This takes virtually no processing effort compared with reloading a new texture even from a preloaded numpy array, which is quite slow.
I will make a very simple animated texture example to show you how to use the u_off, v_off values.
Purely for interest if you had 16 128x128 images in a directory called im00.png, im01.png ... im32.png, im33.png then you could generate a numpy texture atlas something like this:
img = np.zeros((512,512,4),dtype=np.uint8)
for i in range(4):
for j in range(4):
img[i*128:(i+1)*128, j*128:(j+1)*128] = np.array(Image.open('im{}{}.png'.format(i,j)))
# then
tex = pi3d.Texture(img)
# or quicker for a pre-existing texture
tex.update_ndarray(img)