Adafruit is open and shipping! Let's build back better, together!
0

[Show n tell] Matrix portal animated eyes
Moderators: adafruit_support_bill, adafruit

Please be positive and constructive with your questions and comments.

[Show n tell] Matrix portal animated eyes

by dearmash on Wed Oct 14, 2020 2:50 am

https://youtu.be/ySICYe0UjsE

This is just the perfect halloween display I can think of.

Basically took the two examples and mashed them together, and I have to say the effect is quite impressive.

https://learn.adafruit.com/pixel-art-ma ... -animation
https://learn.adafruit.com/matrix-portal-creature-eyes

The images are likely copywritten, so not completely comfortable sharing everything on forums.

The changes are roughly as follows:

*Take the creature eyes project, and load it as is.
*For the new animated eyes, add a sprite sheet rather than a single image
*data.py I delete the code regarding the lid images (later fix references in code)
*data.py add two new entries 'animated':True and 'animation_delay':0.05 or whatever suits
*the code.py changes are pasted below
Code: Select all | TOGGLE FULL SIZE
"""
RASTER EYES for Adafruit Matrix Portal: animated spooky eyes.
"""

print("Main loop")

# pylint: disable=import-error
import math
import random
import time
import displayio
import adafruit_imageload
from adafruit_matrixportal.matrix import Matrix

# TO LOAD DIFFERENT EYE DESIGNS: change the middle word here (between
# 'eyes.' and '.data') to one of the folder names inside the 'eyes' folder:
#from eyes.werewolf.data import EYE_DATA
#from eyes.cyclops.data import EYE_DATA
#from eyes.kobold.data import EYE_DATA
from eyes.pumpkin.data import EYE_DATA

# UTILITY FUNCTIONS AND CLASSES --------------------------------------------

# pylint: disable=too-few-public-methods
class Sprite(displayio.TileGrid):
    """Single-tile-with-bitmap TileGrid subclass, adds a height element
       because TileGrid doesn't appear to have a way to poll that later,
       object still functions in a displayio.Group.
    """
    def __init__(self, filename, transparent=None):
        """Create Sprite object from color-paletted BMP file, optionally
           set one color to transparent (pass as RGB tuple or list to locate
           nearest color, or integer to use a known specific color index).
        """
        bitmap, palette = adafruit_imageload.load(
            filename, bitmap=displayio.Bitmap, palette=displayio.Palette)
        if isinstance(transparent, (tuple, list)): # Find closest RGB match
            closest_distance = 0x1000000           # Force first match
            for color_index, color in enumerate(palette): # Compare each...
                delta = (transparent[0] - ((color >> 16) & 0xFF),
                         transparent[1] - ((color >> 8) & 0xFF),
                         transparent[2] - (color & 0xFF))
                rgb_distance = (delta[0] * delta[0] +
                                delta[1] * delta[1] +
                                delta[2] * delta[2]) # Actually dist^2
                if rgb_distance < closest_distance:  # but adequate for
                    closest_distance = rgb_distance  # compare purposes,
                    closest_index = color_index      # no sqrt needed
            palette.make_transparent(closest_index)
        elif isinstance(transparent, int):
            palette.make_transparent(transparent)
        super(Sprite, self).__init__(bitmap, pixel_shader=palette, tile_height=MATRIX.display.height)
        self.height = bitmap.height


# ONE-TIME INITIALIZATION --------------------------------------------------

MATRIX = Matrix(bit_depth=6)
DISPLAY = MATRIX.display

# Order in which sprites are added determines the 'stacking order' and
# visual priority. Lower lid is added before the upper lid so that if they
# overlap, the upper lid is 'on top' (e.g. if it has eyelashes or such).
SPRITES = displayio.Group()

EYE_SPRITE = Sprite(EYE_DATA['eye_image']); # Base image is opaque
SPRITES.append(EYE_SPRITE)

LOWER_LID_SPRITE = None
if EYE_DATA['lower_lid_image']:
    LOWER_LID_SPRITE = Sprite(EYE_DATA['lower_lid_image'], EYE_DATA['transparent'])
    SPRITES.append(LOWER_LID_SPRITE)

UPPER_LID_SPRITE = None
if EYE_DATA['upper_lid_image']:
    UPPER_LID_SPRITE = Sprite(EYE_DATA['upper_lid_image'], EYE_DATA['transparent'])
    SPRITES.append(LOWER_LID_SPRITE)

STENCIL_SPRITE = Sprite(EYE_DATA['stencil_image'], EYE_DATA['transparent'])
SPRITES.append(STENCIL_SPRITE)

DISPLAY.show(SPRITES)

EYE_CENTER = ((EYE_DATA['eye_move_min'][0] +           # Pixel coords of eye
               EYE_DATA['eye_move_max'][0]) / 2,       # image when centered
              (EYE_DATA['eye_move_min'][1] +           # ('neutral' position)
               EYE_DATA['eye_move_max'][1]) / 2)
EYE_RANGE = (abs(EYE_DATA['eye_move_max'][0] -         # Max eye image motion
                 EYE_DATA['eye_move_min'][0]) / 2,     # delta from center
             abs(EYE_DATA['eye_move_max'][1] -
                 EYE_DATA['eye_move_min'][1]) / 2)
if UPPER_LID_SPRITE:
    UPPER_LID_MIN = (min(EYE_DATA['upper_lid_open'][0],    # Motion bounds of
                         EYE_DATA['upper_lid_closed'][0]), # upper and lower
                     min(EYE_DATA['upper_lid_open'][1],    # eyelids
                         EYE_DATA['upper_lid_closed'][1]))
    UPPER_LID_MAX = (max(EYE_DATA['upper_lid_open'][0],
                         EYE_DATA['upper_lid_closed'][0]),
                     max(EYE_DATA['upper_lid_open'][1],
                         EYE_DATA['upper_lid_closed'][1]))
if LOWER_LID_SPRITE:
    LOWER_LID_MIN = (min(EYE_DATA['lower_lid_open'][0],
                         EYE_DATA['lower_lid_closed'][0]),
                     min(EYE_DATA['lower_lid_open'][1],
                         EYE_DATA['lower_lid_closed'][1]))
    LOWER_LID_MAX = (max(EYE_DATA['lower_lid_open'][0],
                         EYE_DATA['lower_lid_closed'][0]),
                     max(EYE_DATA['lower_lid_open'][1],
                         EYE_DATA['lower_lid_closed'][1]))
EYE_PREV = EYE_CENTER
EYE_NEXT = EYE_CENTER
MOVE_STATE = False                                     # Initially stationary
MOVE_EVENT_DURATION = random.uniform(0.1, 3)           # Time to first move
BLINK_STATE = 2                                        # Start eyes closed
BLINK_EVENT_DURATION = random.uniform(0.25, 0.5)       # Time for eyes to open
TIME_OF_LAST_MOVE_EVENT = TIME_OF_LAST_BLINK_EVENT = time.monotonic()

CURRENT_FRAME = 0
FRAME_COUNT = 1
LAST_FRAME_TIME = 0
if EYE_DATA['animated']:
    FRAME_COUNT = int(EYE_SPRITE.height / MATRIX.display.height)

# MAIN LOOP ----------------------------------------------------------------

while True:
    NOW = time.monotonic()

    # Eye movement ---------------------------------------------------------

    if NOW - TIME_OF_LAST_MOVE_EVENT > MOVE_EVENT_DURATION:
        TIME_OF_LAST_MOVE_EVENT = NOW # Start new move or pause
        MOVE_STATE = not MOVE_STATE   # Toggle between moving & stationary
        if MOVE_STATE:                # Starting a new move?
            MOVE_EVENT_DURATION = random.uniform(0.08, 0.17) # Move time
            ANGLE = random.uniform(0, math.pi * 2)
            EYE_NEXT = (math.cos(ANGLE) * EYE_RANGE[0], # (0,0) in center,
                        math.sin(ANGLE) * EYE_RANGE[1]) # NOT pixel coords
        else:                         # Starting a new pause
            MOVE_EVENT_DURATION = random.uniform(0.04, 3)    # Hold time
            EYE_PREV = EYE_NEXT

    # Fraction of move elapsed (0.0 to 1.0), then ease in/out 3*e^2-2*e^3
    RATIO = (NOW - TIME_OF_LAST_MOVE_EVENT) / MOVE_EVENT_DURATION
    RATIO = 3 * RATIO * RATIO - 2 * RATIO * RATIO * RATIO
    EYE_POS = (EYE_PREV[0] + RATIO * (EYE_NEXT[0] - EYE_PREV[0]),
               EYE_PREV[1] + RATIO * (EYE_NEXT[1] - EYE_PREV[1]))

    # Blinking -------------------------------------------------------------

    if NOW - TIME_OF_LAST_BLINK_EVENT > BLINK_EVENT_DURATION:
        TIME_OF_LAST_BLINK_EVENT = NOW # Start change in blink
        BLINK_STATE += 1               # Cycle paused/closing/opening
        if BLINK_STATE == 1:           # Starting a new blink (closing)
            BLINK_EVENT_DURATION = random.uniform(0.03, 0.07)
        elif BLINK_STATE == 2:         # Starting de-blink (opening)
            BLINK_EVENT_DURATION *= 2
        else:                          # Blink ended,
            BLINK_STATE = 0            # paused
            BLINK_EVENT_DURATION = random.uniform(BLINK_EVENT_DURATION * 3, 4)

    if BLINK_STATE: # Currently in a blink?
        # Fraction of closing or opening elapsed (0.0 to 1.0)
        RATIO = (NOW - TIME_OF_LAST_BLINK_EVENT) / BLINK_EVENT_DURATION
        if BLINK_STATE == 2:    # Opening
            RATIO = 1.0 - RATIO # Flip ratio so eye opens instead of closes
    else:           # Not blinking
        RATIO = 0

    # Eyelid tracking ------------------------------------------------------

    if UPPER_LID_SPRITE:
        # Initial estimate of 'tracked' eyelid positions
        UPPER_LID_POS = (EYE_DATA['upper_lid_center'][0] + EYE_POS[0],
                         EYE_DATA['upper_lid_center'][1] + EYE_POS[1])
        # Then constrain these to the upper/lower lid motion bounds
        UPPER_LID_POS = (min(max(UPPER_LID_POS[0],
                                 UPPER_LID_MIN[0]), UPPER_LID_MAX[0]),
                         min(max(UPPER_LID_POS[1],
                                 UPPER_LID_MIN[1]), UPPER_LID_MAX[1]))
        # Then interpolate between bounded tracked position to closed position
        UPPER_LID_POS = (UPPER_LID_POS[0] + RATIO *
                         (EYE_DATA['upper_lid_closed'][0] - UPPER_LID_POS[0]),
                         UPPER_LID_POS[1] + RATIO *
                         (EYE_DATA['upper_lid_closed'][1] - UPPER_LID_POS[1]))

    if LOWER_LID_SPRITE:
        LOWER_LID_POS = (EYE_DATA['lower_lid_center'][0] + EYE_POS[0],
                         EYE_DATA['lower_lid_center'][1] + EYE_POS[1])
        LOWER_LID_POS = (min(max(LOWER_LID_POS[0],
                                 LOWER_LID_MIN[0]), LOWER_LID_MAX[0]),
                         min(max(LOWER_LID_POS[1],
                                 LOWER_LID_MIN[1]), LOWER_LID_MAX[1]))
        LOWER_LID_POS = (LOWER_LID_POS[0] + RATIO *
                         (EYE_DATA['lower_lid_closed'][0] - LOWER_LID_POS[0]),
                         LOWER_LID_POS[1] + RATIO *
                         (EYE_DATA['lower_lid_closed'][1] - LOWER_LID_POS[1]))

    # Move eye sprites -----------------------------------------------------

    EYE_SPRITE.x, EYE_SPRITE.y = (int(EYE_CENTER[0] + EYE_POS[0] + 0.5),
                                  int(EYE_CENTER[1] + EYE_POS[1] + 0.5))
    if UPPER_LID_SPRITE:
        UPPER_LID_SPRITE.x, UPPER_LID_SPRITE.y = (int(UPPER_LID_POS[0] + 0.5),
                                                  int(UPPER_LID_POS[1] + 0.5))
    if LOWER_LID_SPRITE:
        LOWER_LID_SPRITE.x, LOWER_LID_SPRITE.y = (int(LOWER_LID_POS[0] + 0.5),
                                                  int(LOWER_LID_POS[1] + 0.5))

    if EYE_DATA['animated'] and time.monotonic() - LAST_FRAME_TIME > EYE_DATA['animation_delay']:
        LAST_FRAME_TIME = time.monotonic()
        CURRENT_FRAME = (CURRENT_FRAME + 1) % FRAME_COUNT
        EYE_SPRITE[0] = CURRENT_FRAME


I could definitely see this being expandable to the eye stencil animating in addition to the eyes themselves.

PS the case is a custom 3d print as well, turned out way better than I would have expected, fits like a glove. Large flat surfaces take forever to print (the back, the front wasn't too bad) The LED diffusion acrylic is super cool. It's two sandwich halves and the internal acrylic and matrix slide in on rails and secure with screws.

dearmash
 
Posts: 32
Joined: Mon Nov 28, 2011 4:20 pm

Re: [Show n tell] Matrix portal animated eyes

by johnpark on Fri Oct 16, 2020 2:10 pm

That is awesome! I can't wait to try this out, thanks so much for posting.

johnpark
 
Posts: 822
Joined: Wed Mar 25, 2009 2:15 pm

Re: [Show n tell] Matrix portal animated eyes

by dearmash on Fri Oct 23, 2020 11:31 am

FYI, two things that have been invaluable for this project,

https://learn.adafruit.com/image-correc ... d-matrices

pumpkinportal.jpg
pumpkinportal.jpg (859.47 KiB) Viewed 98 times


Color correction for the matrices. Something that I wanted to fix since the beginning, the simple imagemagick convert command was supremely welcome. Any time the learn guides can suggest convert commands / params, it's very appreciated. The tool can do so much of the converting / file format / tilling / etc. automatically, and it's supported everywhere for free.

I kept track of mine for posterity as well.

Code: Select all | TOGGLE FULL SIZE
Convert the gif from compressed motion to full frame by frame to make conversion easier
convert fire.gif -coalesce fire-c.gif

Add in black borders to make it square
convert fire-sm.gif -bordercolor Black -border 5x0 fire-border.gif

Extract frames
convert fire-border.gif fire.bmp

Delete every other frame because there were duplicates

Double up frames to make 64x32, offsetting frames so they aren't copies in each eye, and tile down for the animation format.
montage -tile 2 -geometry +0+0 fire-1.bmp fire-17.bmp fire-3.bmp fire-19.bmp fire-5.bmp fire-21.bmp fire-7.bmp fire-23.bmp fire-9.bmp fire-25.bmp fire-11.bmp fire-27.bmp fire-13.bmp fire-29.bmp fire-15.bmp fire-31.bmp fire-17.bmp fire-33.bmp fire-19.bmp fire-1.bmp fire-21.bmp fire-3.bmp fire-23.bmp fire-5.bmp fire-25.bmp fire-7.bmp fire-27.bmp fire-9.bmp fire-29.bmp fire-11.bmp fire-31.bmp fire-13.bmp fire-33.bmp fire-15.bmp fireout.bmp && open fireout.bmp

Need to also convert things to work nicely on portal re bmps
convert fireout.bmp -compress none -type palette fireout-convert.bmp

Additionally, the black levels needed tweaking
convert fireout.bmp -black-threshold 2% -compress none -type palette fireout-convert-2.bmp

Then the levels tweaking
convert -gamma 0.4 fireout.bmp fireout-proc.bmp


The other thing is the inline usbc power switch. I got it for a raspi from another store, and it's been way more useful for the matrixportal than the raspi (which is always on anyway).

Next, need to get off my lazy self and make the portal only turn on after sunset for X hours.

dearmash
 
Posts: 32
Joined: Mon Nov 28, 2011 4:20 pm

Re: [Show n tell] Matrix portal animated eyes

by johnpark on Fri Oct 23, 2020 12:20 pm

Excellent update, thanks. I'm a big fan of USB-C on/off switches, too. I've got this one https://www.canakit.com/raspberry-pi-4- ... witch.html

johnpark
 
Posts: 822
Joined: Wed Mar 25, 2009 2:15 pm

Please be positive and constructive with your questions and comments.