Optimizing GIFs
Kamoji's animations are GIF files. Smaller GIFs take less of Kamoji's storage, transfer faster (a photo or GIF sent from the app goes over Bluetooth at about 12 KB per second), and load sooner. This page explains how to make a GIF smaller without changing a single pixel, and gives you a script that does it.
Applied to all 32 of Kamoji's built-in animations, this took them from 3.29 MB to 2.55 MB (22.4% smaller), every one showing exactly the same picture as before.
GIFs optimised this way need Kamoji firmware 0.1.56 or later. Older firmware shows stripes or wrong colours in the parts of the picture that do not move.
What Kamoji needs
- 240 × 240 pixels. Kamoji draws a GIF at its own size from the top-left corner. It does not scale or centre it.
- A looping GIF. It plays at the frame delays stored in the file.
- Any colours. One colour table for the whole GIF or one per frame both work. Transparent pixels show black.
The technique
Most frames of an animation look almost exactly like the frame before them. A lossless optimiser stores only what really changes:
- Changed rectangles. Each frame stores only the smallest rectangle around the pixels that changed. The rest of the screen stays as it was.
- Transparency for unchanged pixels. Inside that rectangle, every pixel that is exactly the same as before becomes transparent. Long runs of transparent pixels compress to almost nothing.
- Repeated frames merged. A frame identical to the one before is dropped, and its time is added to the previous frame's delay.
- Tighter compression. The image data is re-compressed with the best settings.
gifsicle does all four with -O3:
gifsicle -O3 input.gif -o output.gif
What to avoid
- Lossy optimisers. Settings such as "lossy", "fuzz" or a lower gifski quality treat pixels that are almost unchanged as unchanged. Near-black pixels from the previous position of something that moves are then never cleared, and leave a faint trail behind it. On a computer screen the trail is hard to see; Kamoji's display shows dark tones clearly, so it stands out. The lossless steps above never do this.
- Dithered sources. Dithering scatters noise over the picture that changes every frame, so almost no pixel stays identical and the frame deltas save little. Export without dithering if your tool offers it.
- Resizing afterwards. Export your animation at 240 × 240 in the first place. Resizing a finished GIF blends edge colours differently on every frame, which also makes it larger.
The script
Download optimize_gif.py. For each GIF it:
- fits it to 240 × 240 if it is another size (centre crop and resize, with one colour table for all frames);
- re-encodes it with
gifsicle -O3; - checks that the result shows exactly the same picture as the input at every moment of the loop, and keeps it only if it is also smaller.
It needs Python with Pillow, and gifsicle:
pip install pillow
brew install gifsicle # macOS
sudo apt install gifsicle # Debian / Ubuntu / Raspberry Pi OS
On Windows, download gifsicle from lcdf.org/gifsicle
and pass its path with --gifsicle.
python optimize_gif.py dance.gif # writes dance.kamoji.gif
python optimize_gif.py *.gif --out optimized/ # many at once, into a folder
dance.gif: 271.6 KB -> 210.4 KB (-22.5%), lossless: identical picture -> dance.kamoji.gif
The check, in Python
The script compares what is on screen over time, not frame by frame: the optimiser may merge repeated frames, so the frame count can change while the animation stays the same. If you build your own pipeline, this is the test:
import hashlib
from PIL import Image, ImageSequence
def visible_frames(path):
"""[(start ms, end ms, picture hash)], transparent pixels counted as black."""
out, t = [], 0
for frame in ImageSequence.Iterator(Image.open(path)):
rgba = frame.convert("RGBA")
shown = Image.new("RGB", rgba.size)
shown.paste(rgba.convert("RGB"), mask=rgba.split()[3])
key = hashlib.md5(shown.tobytes()).hexdigest()
delay = frame.info.get("duration", 0)
if out and out[-1][2] == key:
out[-1] = (out[-1][0], t + delay, key) # same picture: one longer frame
else:
out.append((t, t + delay, key))
t += delay
return out
assert visible_frames("dance.gif") == visible_frames("dance.kamoji.gif")
Putting it on Kamoji
- From the app: the Photo page (My Kamoji → Manage → Photo) sends a GIF of up to 250 KB and plays it on a loop for the time you choose. A GIF that is already 240 × 240 and small enough is sent as it is; anything else the app resizes and optimises itself.
- As one of Kamoji's own animations: see Custom animations. Keep the file name of the animation you replace.