8000 Add a fast PixelMap-like class by jepler · Pull Request #7191 · adafruit/circuitpython · GitHub
[go: up one dir, main page]

Skip to content

Add a fast PixelMap-like class #7191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send 8000 you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Dec 1, 2022
Merged

Add a fast PixelMap-like class #7191

merged 9 commits into from
Dec 1, 2022

Conversation

jepler
Copy link
@jepler jepler commented Nov 10, 2022

LED Animation Library has a class called PixelMap.

This is inspired by PixelMap, a little different, but close! With adafruit/Adafruit_CircuitPython_LED_Animation#103 it can support the following tested animations (I think that's all of 'em):

  • Comet
  • RainbowComet
  • Chase
  • RainbowChase
  • Blink
  • RainbowSparkle
  • SparklePulse
  • Solid
  • plain Sparkle

@jepler
Copy link
Author
jepler commented Nov 10, 2022

This branch has some unrelated stuff in it, I'll rebase it out before bringing out of draft.

@jepler jepler force-pushed the fastpixelmap branch 2 times, most recently from 7c323ba to 513c116 Compare November 11, 2022 00:43
.. a fast helper for animations. It is similar to and inspired by the
PixelMap helper in Adafruit LED Animation library, but with an extremely
fast 'paste' method for setting a series of pixels. This is a common
operation for many animations, and can give a substantial speed improvement.

It's named `adafruit_pixelmap` so that we can package a compatible version
in pure Python for systems that can't fit it in C in flash, or for
Blinka.

This is a proof of concept and can make a very fast comet animation:
```python
import time
import adafruit_pixelbuf
import adafruti_pixelmap
import board
import neopixel
from supervisor import ticks_ms
from adafruit_led_animation.animation.solid import Solid
from adafruit_led_animation import color

pixel_pin = board.GP0
pixel_num = 96

pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=1, auto_write=False, pixel_order="RGB")

evens = adafruit_pixelmap.PixelMap(pixels, tuple(range(0, pixel_num, 2)))
odd_indices = tuple((i, i+2) for i in range(1, pixel_num, 4))
print(odd_indices)
odds = adafruit_pixelbuf.PixelMap(pixels, odd_indices)
assert len(odds) == len(odd_indices)


comet_length = 16

comet1 = [color.calculate_intensity(color.GREEN, ((1+i) / comet_length) ** 2.4)
        for i in range(comet_length)]
comet2 = [color.calculate_intensity(color.PURPLE, ((1+i) / comet_length) ** 2.4)
        for i in range(comet_length)]

pos1 = 0
pos2 = 96//4

while True:
    evens.paste(comet1, pos1, wrap=True, reverse=False, others=0)
    pos1 = (pos1 + 1) % len(evens)

    odds.paste(comet2, pos2, wrap=True, reverse=True, others=0)
    pos2 = (pos2 - 1) % len(odds)
    pixels.show()

    m = ticks_ms()
    if m % 2000 > 1000:
        time.sleep(.02)
```
@jepler jepler marked this pull request as ready for review November 11, 2022 20:41
@FoamyGuy
Copy link
Collaborator

I tested this successfully on a NeoTrellis M4 using this modified version of the pixelmap example from the led_animation repo:

import board
import neopixel
from adafruit_led_animation.animation.comet import Comet
from adafruit_led_animation.animation.rainbowcomet import RainbowComet
from adafruit_led_animation.animation.rainbowchase import RainbowChase
from adafruit_led_animation.animation.chase import Chase
from adafruit_led_animation.animation.rainbow import Rainbow
from adafruit_led_animation.sequence import AnimationSequence
import adafruit_pixelmap
from adafruit_led_animation.color import PURPLE, JADE, AMBER

pixel_pin = board.NEOPIXEL
pixel_num = 32

pixels = neopixel.NeoPixel(pixel_pin, pixel_num, brightness=0.5, auto_write=False)

pixel_wing_vertical = adafruit_pixelmap.PixelMap(pixels, ((0, 8, 16, 24), (1, 9, 17, 25), (2, 10, 18, 26), (3, 11, 19, 27), (4, 12, 20, 28), (5, 13, 21, 29), (6, 14, 22, 30), (7, 15, 23, 31)))
pixel_wing_horizontal = adafruit_pixelmap.PixelMap(pixels, ((0, 1, 2, 3, 4, 5, 6, 7), (8, 9, 10, 11, 12, 13, 14, 15), (16, 17, 18, 19, 20, 21, 22, 23), (24, 25, 26, 27, 28, 29, 30, 31)))

comet_h = Comet(
    pixel_wing_horizontal, speed=0.1, color=PURPLE, tail_length=3, bounce=True
)
comet_v = Comet(pixel_wing_vertical, speed=0.1, color=AMBER, tail_length=6, bounce=True)
chase_h = Chase(pixel_wing_horizontal, speed=0.1, size=3, spacing=6, color=JADE)
rainbow_chase_v = RainbowChase(
    pixel_wing_vertical, speed=0.1, size=3, spacing=2, step=8
)
rainbow_comet_v 
8000
= RainbowComet(
    pixel_wing_vertical, speed=0.1, tail_length=7, bounce=True
)
rainbow_v = Rainbow(pixel_wing_vertical, speed=0.1, period=2)
rainbow_chase_h = RainbowChase(pixel_wing_horizontal, speed=0.1, size=3, spacing=3)

animations = AnimationSequence(
    rainbow_v,
    comet_h,
    rainbow_comet_v,
    chase_h,
    rainbow_chase_v,
    comet_v,
    rainbow_chase_h,
    advance_interval=5,
)

while True:
    animations.animate()

This version does run successfully and the animations appear visually the same to the stock version of the example which utilizes the python PixelMap implementation. In that regards this seems good to me. However I did notice a few differences in the supported features that I wanted to mention because I wasn't sure if this is intended to be a full drop-in replacement for the python implementation or not.

This core module does not have the vertical_lines() and horizontal_lines() functions that the python implementation does

Those were used by the original example that I started from, they feel like variations of the initializer to me. They create an instance in a certain way and return it. For testing I replaced their usage with hardcoded tuples that contain the indexes for my 8x4 grid on the NeoTrellis.

I attempted having the python implementation extend the core implementation but that didn't work due to one other difference which is the individual_pixels keyword argument. The python implementation has this argument that allows you to indicate your tuples are literal indexes rather than ranges. It appears the core implementation behaves as though this is always true not allowing the ranges, only individual pixels. I didn't make any attempts but the python implementation could possibly be tweaked perhaps to extend or make use of the core implementation and drop the duplicated functions eventually, keeping the functionality it has now, but making use of the new core class internally.

@jepler
Copy link
Author
jepler commented Nov 14, 2022

The testing is appreciated!

This is not intended to be a 100% replacement for the adafruit_led_animation.helper.PixelMap class. You've identified a number of things that are not implemented (pretty thoroughly actually!)

I chose not to implement everything as a way to keep the code size smaller. I'd prefer if only the part that benefits the most from speed is in C and as much as possible is in Python.

So, for instance, those class methods would just shift to being non-class functions in helper. Similarly, unless it turns out to be an important RAM optimization, so can the support for ranges.

It is worth taking time to decide how to structure this. First I said I wanted to call it adafruit_pixelmap so it could be paired with a pure-Python implementation. But now we are talking about always having helper functions coded in Python, which points towards making it a _pixelmap instead, a module that would not be used directly but is intended to always be used through its helper module adafruit_pixelmap (which implements handling of pixel ranges, lines, gridmap, etc)

@tannewt tannewt requested a review from FoamyGuy November 15, 2022 22:51
Copy link
Collaborator
@FoamyGuy FoamyGuy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think it sounds good to have it under _pixelmap and then having the accompanying python code be adafruit_pixelmap library with the helper functions that is meant for use by user code and other libraries.

If we do want to go that way, I can work this week on cookie cutting a library for it and migrating the existing code from led_animation and making the changes needed to work with new core module.

@gamblor21
Copy link
Member

Two potential issues I found:

  1. If I have a pixelmap and access outside of the bounds CP crashes
  2. If I try to get a slice e.g. pixelmap[0:20] I get TypeError: can't convert slice to int

As to the helper code, I'm not sure if a _pixelmap vs adafruit_pixelmap with helper functions is best. For my 300 grid array it alternates up/down so I had to write a quick function to create the indices in the correct order where the animation library does have some of that in it.

Also I have not done any speed comparisons but will try to do that as well.

@jepler
Copy link
Author
jepler commented Nov 17, 2022

If I have a pixelmap and access outside of the bounds CP crashes

This one should be fixed for sure

If I try to get a slice e.g. pixelmap[0:20] I get TypeError: can't convert slice to int

This one was deliberately not included initially, but now that I used such slices to e.g., quickly scroll in the 5x5 neopixel bff example code I guess it should be added

@FoamyGuy
Copy link
Collaborator
  1. If I have a pixelmap and access outside of the bounds CP crashes

@gamblor21 do you raise an exception like IndexError? or crash more severely than that like a hardfault crash or something?

I'm seeing this when I attempt to access outside of the bounds, and it seems like reasonable / expected behavior to me for the error case.

Traceback (most recent call last):
  File "code.py", line 26, in <module>
IndexError: index out of range
8000

@jepler
Copy link
Author
jepler commented Nov 23, 2022

There are at least two things that could be out of range

  • the indices used to construct the PixelMap
  • the indices used to index into the PixelMap
    it wasn't clear which one of the two was causing the problem .. an example program could be helpful!

@jepler
Copy link
Author
jepler commented Nov 23, 2022

Recommend simply disabling the new module on this board...


Error: Build bluemicro833 for ru took 31.40s and failed
make: Entering directory '/home/runner/work/circuitpython/circuitpython/ports/nrf'
Use make V=1, make V=2 or set BUILD_VERBOSE similarly in your environment to increase build verbosity.

237588 bytes used, -20 bytes free in flash firmware space out of 237568 bytes (232.0kB).

@gamblor21
Copy link
Member

I had my matrix temporarily mounted in my window. I plan to take it down tomorrow and will do some more testing to find out how to reproduce the error I saw.

It was not just an exception but some type of hard fault (or infinite loop as I lost USB).

@FoamyGuy
Copy link
Collaborator

The latest commits disable this on the device that was too large, and move it to _pixelmap. I've also made a first draft version of the python layer code that integrates with this new core class.

I think the remaining open items for consideration are:

  • The python layer's home. I've currently left it in adafruit_led_animation.helper but perhaps we want to create adafruit_pixelmap for it? If anyone has thoughts on this I'm happy for input. I can cookie cut a new library and move the new python layer class to it if we do want to go that route.
  • The get slice functionality is currently implemented in the python code only. I'm not sure if that needs to or should be moved into the core. @jepler do you have a sense of whether this portion of the original helper is worth implementing in the core to speed up? or is it something that won't benefit as much and is better to save the space instead?
  • Negative indexing is currently implemented in the python layer. Not sure if it makes a difference or if the core class should support that or not.
  • Possible out of range exception mentioned by Gamblor. I haven't managed to get anything worse than raising an exception, but definitely want to fix if there is something that does lead to a hard crash.

I have tested the current versions of these successfully on a NeoTrellis M4 as well as Feather S2 TFT + Neopixel Featherwign. running the led_animation_pixel_map.py example from the led_animation library as well as a more simplified version of that script that just creates the maps and manipulates them directly without animations.

@gamblor21
Copy link
Member

Sorry for the delay in testing, I hope to get to it tomorrow again.

Copy link
Member
@gamblor21 gamblor21 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a pixel string N long the range checks were lettings pixels[n] go through (and crash) where pixels[n-1] is the last valid value. Two range checks were inclusive of the top index value.

Other out of bound errors were being caught properly.

@gamblor21
Copy link
Member

Comment on slices: I did figure out how to implement the get slice, I could try to (figure out how to and) push a commit to this branch. I think it is worthwhile in the core. It was a 25% speed increase over doing it in Python.

FoamyGuy and others added 2 commits November 27, 2022 17:02
Co-authored-by: Mark <56205165+gamblor21@users.noreply.github.com>
Co-authored-by: Mark <56205165+gamblor21@users.noreply.github.com>
@dhalbert dhalbert requested a review from gamblor21 November 30, 2022 16:21
@dhalbert
Copy link
Collaborator

@jepler I think you have passed this on, but is it meeting with your approval with the current changes?

Copy link
Author
@jepler jepler left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other folks' changes look good, no testing performed

@gamblor21
Copy link
Member

I tried it last night quickly and it worked. Was going to do a little more testing tonight if I have the time but seems good to me.

Copy link
Collaborator
@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems fine to merge now. Further testing is good but it would be good to get it out for people to try. Since it's an underscore module, we don't guarantee about the API will remain stable.

@dhalbert dhalbert merged commit 082b0d1 into adafruit:main Dec 1, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
0