Customizing the Look

Changing the Python Code

The installer script places the code and data in the directory /boot/Pi_Eyes. If you’re not familiar with Linux text editors and so forth, you can move the SD card over to a regular Windows or Mac system, where the “boot” partition appears as a drive and you can edit these files with your text editor of choice.

If using TFT or OLED screens on a Snake Eyes Bonnet board, the Python script of interest is eyes.py. If using HDMI output (with or without the bonnet), look for cyclops.py (so named because it only renders one eye).

After making changes to the code, rather than tracking down and killing processes, it’s often easier just to reboot the Pi. After a minute or so, the revised code will run on startup.

This section near the top of the code (previously mentioned on the Hardware page) includes a couple of items that are relevant to the appearance of the eyes:

# INPUT CONFIG for eye motion ----------------------------------------------

JOYSTICK_X_IN   = -1    # Analog input for eye horiz pos (-1 = auto)
JOYSTICK_Y_IN   = -1    # Analog input for eye vert position (")
PUPIL_IN        = -1    # Analog input for pupil control (-1 = auto)
JOYSTICK_X_FLIP = False # If True, reverse stick X axis
JOYSTICK_Y_FLIP = False # If True, reverse stick Y axis
PUPIL_IN_FLIP   = False # If True, reverse reading from PUPIL_IN
TRACKING        = True  # If True, eyelid tracks pupil
PUPIL_SMOOTH    = 16    # If > 0, filter input from PUPIL_IN
PUPIL_MIN       = 0.0   # Lower analog range from PUPIL_IN
PUPIL_MAX       = 1.0   # Upper "
WINK_L_PIN      = 22    # GPIO pin for LEFT eye wink button
BLINK_PIN       = 23    # GPIO pin for blink button (BOTH eyes)
WINK_R_PIN      = 24    # GPIO pin for RIGHT eye wink button
AUTOBLINK       = True  # If True, eyes blink autonomously

Most relevant here are AUTOBLINK and TRACKING. If AUTOBLINK is changed to False, the eyes will stop their automatic blinking (responding only to button presses, if you have something like that wired up). If TRACKING is changed to False, the eyelids will no longer follow the pupils as they move around (this is an interesting thing that real eyes actually do, but sometimes you want a continuous wide-eyed stare).

Sometimes you may want no eyelids at all…just a full unblinking hemisphere. You can achieve this by simply commenting out the two lines that render the eyelids (put a “#” character at the start of each line). This is much later in the code, near line 430:

        upperEyelid.draw()
        lowerEyelid.draw()

The other settings above are mostly related to hardware stuff.

Changing Graphics

Certain aspects (such as iris color) can be changed by substituting different graphics files. These are in the directory /boot/Pi_Eyes/graphics. One could just overwrite the files that are there, but I prefer to keep the originals around for reference and assign new names to the changed files. To make the code load these changed files, look for this section starting around line 122:

# Load texture maps --------------------------------------------------------

irisMap   = pi3d.Texture("graphics/iris.jpg"  , mipmap=False,
              filter=pi3d.GL_LINEAR)
scleraMap = pi3d.Texture("graphics/sclera.png", mipmap=False,
              filter=pi3d.GL_LINEAR, blend=True)

Most common graphics formats (JPG, GIF, PNG — the latter with or without transparency) are supported. The images are square to make the OpenGL library happy.

The graphics are stored flat and unrolled, like a map projection. The horizontal (X) axis works like the longitude, or angle around the eye, while the vertical (Y) axis is the latitude.

The iris (file iris.jpg) is what we think of as the “color” of the eye and is most often what you’ll want to edit. Sometimes you just need to edit the hue & saturation in a program like Photoshop, or you can make something totally custom if you’re after a particular look.

The sclera (file sclera.png) is the “white” of the eye…which really isn’t that white at all. There’s veins and blotches and gross stuff!

This file is a transparent PNG to simulate the transition where the sclera meets the lens…it’s not an abrupt transition, there’s a slight “fuzziness” to it. When editing, try to preserve that transparency (and note the couple of transparent rows at the bottom, necessary because of the way OpenGL interpolates these images).

(The third file, lidMap, probably shouldn’t be changed. It’s complicated.)

Around line 79 in the code is this:

# Load SVG file, extract paths & convert to point lists --------------------

dom               = parse("graphics/eye.svg")

eye.svg (or cyclops-eye.svg for the single-eye code) is a Scalable Vector Graphics (SVG) file that determines the size and shape of various elements. Suppose you want Krampus eyes, with that freaky horizontal goat pupil? Or dragon eyes with a vertical slit pupil? (In fact there’s an example file there for that — dragon-eye.svg.)

InkScape and Adobe Illustrator (among others) can both load and save SVG files. But…editing / substituting this file is fairly tricky, as the names of individual paths are used in the Python code. If your graphics editor of choice does not maintain these element names exactly when changing or saving the file, the Python code will fail to run.

In Illustrator, you can see the path names by toggling “Layer 1” open:

The largest blue circle there, which extends to the edges of the document, can mostly be ignored…it’s there for reference and represents the outer bounds of the eye ball (which can’t be changed).

The iris path (which needs to remain a circle, though you can change its size) determines the size of the outer edge of the iris relative to the whole eye.

pupilMin and pupilMax are the size and shape of the pupil in its most contracted and most dilated positions, as it responds to light. This doesn’t need to be a circle! Have a look at dragon-eye.svg for example.

Two additional blue circles — scleraFront and scleraBack — are used in determining the “sweep” of the white of the eye. Notice it overlaps the outer edge of the iris slightly, and is open at the back (we have no need for the back of the eye, so it’s not modeled or rendered).

Red paths are used for animating the eyelids. upperLidOpen and upperLidClosed are the shape of the upper eyelid in its fully-open and fully-closed positions. lowerLidOpen and lowerLidClosed are the same for the lower eyelid. upperLidEdge and lowerLidEdge are used by the software to generate the other edge of the eyelid mesh geometry, which is really a 2D polygon that occludes the eye behind it.

You’ll notice the default eye is not symmetrical. Eyes are interesting things and have a unique shape left and right! Looking at the SVG graphics, the other eye (and nose, if we had one) would be to the left. cyclops-eye.svg is left/right symmetrical…it’s designed for the Pi’s single HDMI output, which might be split to two identical displays…the asymmetrical eye would look weird and lopsided in that case, so we go for the naïve “football shape.”

(At some point I hope to add a translucent nictitating membrane to the dragon eye, but this hasn’t happened yet.)

Replacing Everything

Well, almost everything.

Our Python code generates OpenGL animation that goes to the Pi’s normal HDMI video framebuffer, whether or not there’s actually an HDMI display attached. A separate program — fbx2 — continuously copies two sections of the framebuffer to the TFT or OLED displays attached to the Snake Eyes Bonnet.

This means, if you really want, the entire eye-animating program could be replaced with code of your own design in whatever language you like. As long as your code positions graphics in the right places, fbx2 will present that on the TFT/OLED screens. (I’ve even tried installing fbx2 over RetroPie — yes, you can play Doom on these eyes!)

The pi-eyes.sh installer script configures the Pi’s video output resolution to either 640x480 pixels (for OLED or TFT) or 1280x720 pixels (for IPS). It could be nearly any resolution, but these sizes were chosen for a reason, explained in a moment…

Picture the screen split evenly; two rectangular regions side-by-side. The eyes will be centered within these regions, regardless their size or aspect ratio. The size of the rendered eyes is specified on the command line to the Python code using the --radius option, for example:

python eyes.py --radius 128

The fbx2 program performs repeated screen captures, scaling it by 50% (providing a smooth 2:1 downsampling in the process) and then copying two squares centered on these regions to the connected SPI screens. For OLED or TFT, the screens are 128x128 pixels, so the sections of the screen copied are 256x256 pixels, and the eyes have a radius of 128 pixels. IPS screens are 240x240 pixels, so the copied sections are 480x480 pixels and the eyes have a radius of 240 pixels.

There’s a lot of unused blank space around the eyes; this is normal and by design. If developing your own code, this allows you to put debugging or status information in the margins of the HDMI display, but not have it copied to the SPI screens. Also, the eyelids are rendered by dynamically generating 2D meshes, and these need to be a little larger than the eyes to fully block things out. If the eyes were right up against each other, these would overlap.

Last updated on Apr 24, 2018 Published on Jan 11, 2017