Monday, March 6, 2017

Arduino OLED BitMap Animation

Summary

On occasion, I bump up against a little tech challenge that just ticks me off enough that I won't let go until I have defeated it.  While making a special purpose remote-control for a camera aimer, I wanted to use a tiny, inexpensive OLED as a feedback indicator showing which direction the remote device was pointing.  I thought it would be simple enough to display a little bitmap depiction of the camera, rotated to correspond with the direction of the actual camera.  However, it wasn't that simple.

Challenges

  • Creating the initial bitmap was a bit tedious. (...to me anyway.  I suspect my only real solution for that would be more artistic talent.)
  • Converting the bitmap to C++ code required some web searching
    • Found option 1 (online): http://manytools.org/hacker-tools/image-to-byte-array/
    • Found option 2 (Windows): http://en.radzio.dxp.pl/bitmap_converter/
  • Displaying a rotated bitmap wasn't part of the library API for the OLED display
    • and it isn't trivial to just write a rotation function
      • https://forum.arduino.cc/index.php?topic=420182.0
    • and it isn't really quick enough
      • see post #12 of the previous forum thread.
    • and I doubt the result would have looked very good anyway.
  • Each 64x64 bitmap requires about 1/2 KB of the limited 32 KB program memory on an Arduino (ouch).
    • so I realized I'd have to compromise and only include a bitmap for each 10 degree increment, using a total of about 18 KB (36 images @ 0.5 KB each).
      • as it turns out, that's probably good enough, but it's still a trade-off.  I would have preferred a little more granularity.

Abandoned the First Attempt to Create all 36 Bitmaps

After deciding that using individual bitmaps encoded as a C++ char array was really the most practical option, I started doing the rotation task in Photoshop.  The process was promising to be very tedious.  I don't like tedious.  Even after transforming and saving each 10 degree rotation as a separate image, I would still need to upload every image file, one at a time, to the "image-to-byte-array" web site to convert it to C++ code.  The Photoshop processing could have been done with a recorded macro I guess but it was taking about 10 minutes to scale, rotate, color-reduce, and clean up extraneous bits.  I really didn't want to spend the next 5 hours doing the rest of the images this way, so I spent a few hours trying to find another way.

ImageMagick to the Rescue

After a short time, I remembered a command-line tool that I have found very handy for tasks like this in the past, ImageMagick.  While I was reading the ImageMagick docs, examples, and forum-posts explaining how to rotate an image, which, frankly, was all I had expected I'd get from the command line tool, I noticed that it was capable of doing a reasonably good job of interpolating the right pixels for a 2-color off-center rotation of the bitmap too (using the Scale Rotate and Translate / SRT function).  I was then really excited to find that ImageMagick could convert an image file to a C/C++ header file.  After a bit more web searching for various examples, I managed to boil the whole process down to 3 ImageMagick commands to produce a header file (C/C++ code) for each rotated image. 

The commands are (using a 10 degree rotation as an example):
  1. magick original_bitmap.png -antialias -interpolate Spline -virtual-pixel transparent -size 64x64 -distort SRT 10 rotated_10_deg_bitmap.png
  2. magick rotated_10_deg_bitmap.png -channel alpha -auto-level -threshold 50% two_color_10_deg_bitmap.png
  3. magick two_color_10deg_bitmap.png -define h:format=gray -depth 1 -size 64x64 -alpha extract bitmap_10deg.h
Using a Windows batch/cmd script (which was easier than writing a *nix shell script since I was on a Windows machine anyway), I could have a script quickly produce the full set of header files.  Using the "for /L" command and inserting variable references in a few key places, the script loops through the 10-degree increments and creates a C/C++ char array with hex- encoded (i.e.  0x0E, 0x00, etc.) data, representing each image.

All that was required to finish automating the process was to:
  • add a few lines for #ifndef, #define and #endif (to avoid build issues with multiple includes),
  • and use a Windows port of the "sed" command to customize the default variable declaration (static const unsigned char MagickImage[]) with a distinct name and extra keywords (PROGMEM).

Other Possibilities

Before moving on to the actual example script, it's worth noting that image rotation isn't the only way to use ImageMagick to "pre-formulate" bitmaps for an OLED (or other single color displays).  ImageMagick is capable of a multitude of other "distortions" to show movement or perceived effects like 3D flipping.   If rotating an image isn't exactly what you want, you may find your answer by reading through documentation pages like this one: http://www.imagemagick.org/Usage/distorts/

The final Windows command script is as follows:

@echo off
set MAGICK_CMD=c:\win32app\ImageMagick-7.0.5-Q16\magick.exe
set SED_CMD=c:\win32app\unixgnu\sed.exe
set HEADER_OUT_DIR=..\

for /L %%i in (0,10,350) DO (
    %MAGICK_CMD%
original_bitmap.png -antialias -interpolate Spline -virtual-pixel transparent -size 64x64 -distort SRT %%i rotated_%%i_deg_bitmap.png
    %MAGICK_CMD%
rotated_%%i_deg_bitmap.png -channel alpha -auto-level -threshold 50%%
two_color_%%i_deg_bitmap.png

     %MAGICK_CMD% two_color_%%i_deg_bitmap.png -define h:format=gray -depth 1 -size 64x64 -alpha extract bitmap_%%i_deg.h
    echo #ifndef ICON%%i > %HEADER_OUT_DIR%\bitmap_%%i_deg.h
    echo #define ICON%%i >> %HEADER_OUT_DIR%\bitmap_%%i_deg.h
    %SED_CMD% -e "s/char/char PROGMEM/g; s/MagickImage/bitmap_data_%%i/g"
bitmap_%%i_deg.h >> %HEADER_OUT_DIR%\bitmap_%%i_deg.h
    echo #endif >> %HEADER_OUT_DIR%\
bitmap_%%i_deg.h
)

Notes on Magick command options used:

Some of these explanations are not quite right.  This represents the best understanding I had time to obtain, so if any of it is a bit off, please leave a comment with a better explanation.
  • Converting from original PNG (saved "For Web and Devices" from PSD file in photoshop as 2-color PNG8) to rotated PNG
    • -antialias produces an image that has fuzzy edges that are a better approximation of what the rotated image should look like
    • -interpolate Spline gives the best results for translating the lines and spots in the original image
    • -virtual-pixel transparent fills in the alpha-channel transparency for pixels that are set on an edge (instead of the pixel's color)
    • -size 64x64 saves dimensional info in the output image so the next step doesn't whine about %h and %w being missing
    • -distort SRT is the number of degrees to "scale rotate translate" which basically accomplishes an in-place rotation without clipping
  • Converting from rotated PNG to the BW (black an white) PNG
    • -channel alpha tells ImageMagick to use the alpha channel instead of one of the color channels to pick the output pixels
      This is necessary because the rotated image is essentially a gray-scale image with a transparent background
    • -threshold 50% yields a good final pixel on/off choice, based on the transparency/alpha values.
  • Converting from the BW PNG to the C/C++ header file
    • -define h:format=gray tells ImageMagick to output to just image bit data bytes without GIF or PNG header info included
    • -depth 1 constrains the output to 1 bit per pixel as required for the OLED (each pixel is either on or off)
    • -size 64x64 (may not be required  TODO: experiment)
    • -alpha extract tells ImageMagick to use only the alpha channel info in the PNG instead of every color channel.

Conclusion

What would have been a lot of tedious work creating derivative images, with Photoshop (or a similar image editor) and various other online/GUI based tools, was accomplished with a bit of scripting and a spectacularly useful (and free) command line tool.  Hope this comes in handy for something you're working on.  Please leave a comment and let me know if you found it useful.