Alright. Previously we got our arm all set up to perform operational space control, accepting commands through Python. In this post we’re going to set it up with a set of spiking cameras for eyes, train it to learn the mapping between camera coordinates and end-effector coordinates, and have it track an LED target.
What is a spiking camera?
Good question! Spiking cameras are awesome, and they come from Dr. Jorg Conradt’s lab. Basically what they do is return you information about movement from the environment. They’re event-driven, instead of clock-driven like most hardware, which means that they have no internal clock that’s dictating when they send information (i.e. they’re asynchronous). They send information out as soon as they receive it. Additionally, they only send out information about the part of the image that has changed. This means that they have super fast response times and their output bandwidth is really low. Dr. Terry Stewart of our lab has written a bunch of code that can be used for interfacing with spiking cameras, which can all be found up on his GitHub.
Let’s use his code to see through a spiking camera’s eye. After cloning his repo and running
python setup.py you can plug in a spiking camera through USB, and with the following code have a Matplotlib figure pop-up with the camera output:
eye = nstbot.RetinaBot()
The important parts here are the creation of an instance of the
RetinaBot, connecting it to the proper USB port, and calling the
show_image function. Pretty easy, right? Here’s some example output, this is me waving my hand and snapping my fingers:
How cool is that? Now, you may be wondering how or why we’re going to use a spiking camera instead of a regular camera. The main reason that I’m using it here is because it makes tracking targets super easy. We just set up an LED that blinks at say 100Hz, and then we look for that frequency in the spiking camera output by recording the rate of change of each of the pixels and averaging over all pixel locations changing at the target frequency. So, to do this with the above code we simply add
And now we can track the location of an LED blinking at 100Hz! The visualization code place a blue dot at the estimated target location, and this all looks like:
Alright! Easily decoded target location complete.
Transforming between camera coordinates and end-effector coordinates
Now that we have a system that can track a target location, we need to transform that position information into end-effector coordinates for the arm to move to. There are a few ways to go about this. One is by very carefully positioning the camera and measuring the distances between the robot’s origin reference frame and working through the trig etc etc. Another, much less pain-in-the-neck way is to instead record some sample points of the robot end-effector at different positions in both end-effector and camera coordinates, and then use a function approximator to generalize over the rest of space.
We’ll do the latter, because it’s exactly the kind of thing that neurons are great for. We have some weird function, and we want to learn to approximate it. Populations of neurons are awesome function approximators. Think of all the crazy mappings your brain learns. To perform function approximation with neurons we’re going to use the Neural Engineering Framework (NEF). If you’re not familiar with the NEF, the basic idea is to using the response curves of neurons as a big set of basis function to decode some signal in some vector space. So we look at the responses of the neurons in the population as we vary our input signal, and then determine a set of decoders (using least-squares or somesuch) that specify the contribution of each neuron to the different dimensions of the function we want to approximate.
Here’s how this is going to work.
- We’re going to attach the LED to the head of the robot,
- we specify a set of coordinates that we send to the robot’s controller,
- when the robot moves to each point, record the LED location from the camera as well as the end-effector’s coordinate,
- create a population of neurons that we train up to learn the mapping from camera locations to end-effector locations
- use this information to tell the robot where to move.
A detail that should be mentioned here is that a single camera only provides 2D output. To get a 3D location we’re going to use two separate cameras. One will provide information, and the other will provide information.
Once we’ve taped (expertly) the LED onto the robot arm, the following script to generate the information we to approximate the function transforming from camera to end-effector space:
from eye import Eye # this is just a spiking camera wrapper class
import numpy as np
# connect to the spiking cameras
eye0 = Eye(port='/dev/ttyUSB2')
eye1 = Eye(port='/dev/ttyUSB1')
eyes = [eye0, eye1]
# connect to the robot
rob = robot.robotArm()
# define the range of values to test
min_x = -10.0
max_x = 10.0
x_interval = 5.0
min_y = -15.0
max_y = -5.0
y_interval = 5.0
min_z = 10.0
max_z = 20.0
z_interval = 5.0
x_space = np.arange(min_x, max_x, x_interval)
y_space = np.arange(min_y, max_y, y_interval)
z_space = np.arange(min_z, max_z, z_interval)
num_samples = 10 # how many camera samples to average over
out_file0 = open('eye_map_0.csv', 'w')
out_file1 = open('eye_map_1.csv', 'w')
for i, x_val in enumerate(x_space):
for j, y_val in enumerate(y_space):
for k, z_val in enumerate(z_space):
time.sleep(2) # time for the robot to move
# take a bunch of samples and average the input to get
# the approximation of the LED in camera coordinates
eye_data0 = np.zeros(2)
for k in range(num_samples):
eye_data0 += eye0.position(0)[:2]
eye_data0 /= num_samples
out_file0.write('%0.2f, %0.2f, %0.2f, %0.2f\n' %
(y_val, z_val, eye_data0, eye_data0))
eye_data1 = np.zeros(2)
for k in range(num_samples):
eye_data1 += eye1.position(0)[:2]
eye_data1 /= num_samples
out_file1.write('%0.2f, %0.2f, %0.2f, %0.2f\n' %
(x_val, z_val, eye_data1, eye_data1))
This script connects to the cameras, defines some rectangle in end-effector space to sample, and then works through each of the points writing the data to file. The results of this code can be seen in the animation posted in part 2 of this series.
OK! So now we have all the information we need to train up our neural population. It’s worth noting that we’re only using 36 sample points to train up our neurons, I did this mostly because I didn’t want to wait around. You can of course use more, though, and the more sample points you have the more accurate your function approximation will be.
Implementing a controller using Nengo
The neural simulation software (which implements the NEF) that we’re going to be using to generate and train our neural population is called Nengo. It’s free to use for non-commercial use, and I highly recommend checking out the introduction and tutorials if you have any interest in neural modeling.
What we need to do now is generate two neural populations, one for each camera, that will receives input from the spiking camera and transform the target’s location information into end-effector coordinates. We will then combine the estimates from the two populations, and send that information out to the robot to tell it where to move. I’ll paste the code in here, and then we’ll step through it below.
from eye import Eye
from nengo.utils.connection import target_function
import numpy as np
# connect to robot
rob = robot.robotArm()
model = nengo.Network()
def eyeNet(port='/dev/ttyUSB0', filename='eye_map.csv', n_neurons=1000,
# connect to eye
spiking_cam = Eye(port=port)
# read in eval points and target output
eval_points = 
targets = 
file_obj = open(filename, 'r')
file_data = file_obj.readlines()
for line in file_data:
line_data = map(float, line.strip().split(','))
eval_points = np.array(eval_points)
targets = np.array(targets)
# create subnetwork for eye
net = nengo.Network(label=label)
net.input = nengo.Node(output=eye_input, size_out=2)
net.map_ens = nengo.Ensemble(n_neurons, dimensions=2)
net.output = nengo.Node(size_in=2)
nengo.Connection(net.input, net.map_ens, synapse=None)
nengo.Connection(net.map_ens, net.output, synapse=None,
# create network for spiking camera 0
eye0 = eyeNet(port='/dev/ttyUSB2', filename='eye_map_0.csv', label='eye0')
# create network for spiking camera 1
eye1 = eyeNet(port='/dev/ttyUSB1', filename='eye_map_1.csv', label='eye1')
def eyes_func(t, yzxz):
x = yzxz # x coordinate coded from eye1
y = yzxz # y coordinate coded from eye0
z = (yzxz + yzxz) / 2.0 # z coordinate average from eye0 and eye1
eyes = nengo.Node(output=eyes_func, size_in=4)
# create output node for sending instructions to arm
def arm_func(t, x):
if t < .05: return # don't move arm during startup (avoid transients)
armNode = nengo.Node(output=arm_func, size_in=3, size_out=0)
sim = nengo.Simulator(model)
The first thing we’re doing is defining a function (
eyeNet) to create our neural population that takes input from a spiking camera, and decodes out an end-effector location. In here, we read in from the file the information we just recorded about the camera positions that will serve as the input signal to the neurons (
eval_points) and the corresponding set of function output (
targets). We create a Nengo network,
net, and then a couple of nodes for connecting the input (
net.input) and projecting the output (
net.output). The population of neurons that we’ll use to approximate our function is called
net.map_ens. To specify the function we want to approximate using the
targets arrays, we create a connection from
net.output and use
**target_function(eval_points, targets). So this is probably a little weird to parse if you haven’t used Nengo before, but hopefully it’s clear enough that you can get the gist of what’s going on.
In the main part of the code, we create another Nengo network. We call this one
model because that’s convention for the top-level network in Nengo. We then create two networks using the
eyeNet function to hook up to the two cameras. At this point we create a node called
eyes, and the role of this node is simply to amalgamate the information from the two cameras from and into . This node is then hooked up to another node called
armNode, and all
armNode does is call the robot arm’s
move_to_xyz function, which we defined in the last post.
Finally, we create a
model, which compiles the neural network we just specified above, and we run the simulation. The result of all of this then looks something like the following:
And there we go! Project complete! We have a controller for a 6DOF arm that uses spiking cameras to train up a neural population and track an LED, that requires almost no set up time. I gave a demo of this at the end of the summer school and there’s no real positioning of the cameras relative to the arm required, just have to tape the cameras up somewhere, run the training script, and go!
From here there are a bunch of fun ways to go about extending this. We could add another LED blinking at a different frequency that the arm needs to avoid, using an obstacle avoidance algorithm like the one in this post, add in another dimension of the task involving the gripper, implement a null-space controller to keep the arm near resting joint angles as it tracks the target, and on and on!
Another thing that I’ve looked at is including learning on the system to fine tune our function approximation online. As is, the controller is able to extrapolate and move the arm to target locations that are outside of the range of space sampled during training, but it’s not super accurate. It would be much better to be constantly refining the estimate using learning. I was able to implement a basic version that works, but getting the learning and the tracking running at the same time turns out to be a bit trickier, so I haven’t had the chance to get it all running yet. Hopefully there will be some more down-time in the near future, however, and be able to finish implementing it.
For now, though, we still have a pretty neat target tracker for our robot arm!