Skip to main content

Convert GIF/MP4 video to emoji mosaic art video using FFmpeg

Project description

๐ŸŽจ EmojiArt

Convert any GIF or MP4 video into an emoji mosaic art video using FFmpeg.

Each frame is broken into a configurable grid of pixel blocks. Every block maps to the closest-matching emoji by perceptual color distance (CIE L*a*b* ฮ”E). The processed frames are stitched back into an MP4 with the original frame rate preserved, and optional audio passthrough for MP4 inputs.

emojiart input.mp4 output.mp4
๐ŸŸฅ๐ŸŸฅ๐ŸŸง๐ŸŸจ๐ŸŸฉ๐ŸŸฆ๐ŸŸช โ† each cell = one emoji
๐ŸŸฅ๐ŸŸซ๐ŸŸจ๐ŸŸฉ๐ŸŸฆ๐ŸŸช๐ŸŸฅ
๐ŸŸง๐ŸŸจ๐ŸŸฉ๐ŸŸฆ๐ŸŸช๐ŸŸฅ๐ŸŸง

Features

  • GIF & MP4 input โ€” any format FFmpeg can read (.gif, .mp4, .mov, .mkv, .webm, .avi)
  • MP4 output โ€” H.264, yuv420p, maximum compatibility
  • Audio passthrough โ€” copies or re-encodes audio from MP4 inputs
  • Perceptual color matching โ€” CIE L*a*b* ฮ”E76 for accurate emoji selection
  • Configurable grid โ€” --resolution N sets emoji columns (rows auto-scale)
  • Three quality modes โ€” fast / standard / hq
  • Speed control โ€” --speed 0.5 or --speed 2.0 (adjusts both video and audio)
  • Custom emoji palettes โ€” JSON file with your own emoji + RGB definitions
  • Single-frame preview โ€” --preview outputs a PNG before committing to full render
  • Progress bar โ€” real-time frame progress with FPS counter
  • Prints every FFmpeg command โ€” copy and run manually if needed
  • Zero network calls โ€” fully offline, no API keys

Requirements

Dependency Version Notes
Python โ‰ฅ 3.9
FFmpeg โ‰ฅ 4.4 Must be on PATH
Pillow โ‰ฅ 10.0 Installed automatically
numpy โ‰ฅ 1.24 Installed automatically

Optional (strongly recommended): A system color emoji font for best visual output.

OS Font Install
macOS Apple Color Emoji Pre-installed
Windows Segoe UI Emoji Pre-installed
Linux Noto Color Emoji sudo apt install fonts-noto-color-emoji
Linux Twemoji Download from twitter/twemoji

Without a color emoji font, EmojiArt falls back to Pillow's built-in bitmap font, which renders ASCII approximations. The output still works, but won't look as good.


Installation

cd emojiart pip install -e .


### With pipx (isolated environment)

```bash
pipx install .

Verify

emojiart --help

Usage

Basic

# Output defaults to emojioutput.mp4
emojiart input.mp4

# Explicit output name
emojiart input.mp4 output.mp4

# GIF input
emojiart animation.gif emoji_animation.mp4

Quality modes

# Fast mode: 16-column grid, minimal palette โ€” good for quick previews
emojiart input.mp4 out.mp4 --mode fast

# Standard mode: 32-column grid, 26-emoji palette (default)
emojiart input.mp4 out.mp4 --mode standard

# High quality: 48+ column grid, 47-emoji palette
emojiart input.mp4 out.mp4 --mode hq

Grid resolution

# 16 columns = chunky / pixel-art look
emojiart input.mp4 out.mp4 --resolution 16

# 64 columns = fine mosaic, slower to render
emojiart input.mp4 out.mp4 --resolution 64

# Custom cell pixel size (overrides auto-sizing)
emojiart input.mp4 out.mp4 --resolution 32 --cell-size 24

Speed control

# Half speed (slow motion)
emojiart input.mp4 out.mp4 --speed 0.5

# Double speed (timelapse)
emojiart input.mp4 out.mp4 --speed 2.0

Audio is adjusted proportionally via atempo when speed โ‰  1.0. For speed outside the [0.5, 2.0] range, chain multiple atempo filters manually.

Audio

# Strip audio entirely
emojiart input.mp4 out.mp4 --no-audio

# Audio is preserved by default when input has audio
emojiart input_with_audio.mp4 out.mp4

Preview (single frame)

# Saves frame_001.png instead of a full video โ€” fast feedback loop
emojiart input.mp4 frame_001 --preview

# Preview with HQ settings
emojiart input.mp4 preview --preview --mode hq --resolution 48

The output path suffix is replaced with .png automatically.

Custom emoji palette

emojiart input.mp4 out.mp4 --palette examples/nature_palette.json

Custom palette JSON format:

[
  {"emoji": "๐ŸŒŠ", "name": "ocean-blue",   "rgb": [28,  107, 186]},
  {"emoji": "๐ŸŒฟ", "name": "forest-green", "rgb": [68,  148, 74]},
  {"emoji": "๐Ÿ”ฅ", "name": "fire-orange",  "rgb": [220, 80,  0]},
  {"emoji": "โฌ›", "name": "black",        "rgb": [23,  23,  23]},
  {"emoji": "โฌœ", "name": "white",        "rgb": [230, 230, 230]}
]

Face emojis

# Adds ๐Ÿ˜€ ๐Ÿ˜ ๐Ÿ˜ก ๐Ÿ˜ข ๐Ÿฅฐ to the palette (mapped to skin-tone/yellow zones)
emojiart input.mp4 out.mp4 --faces

Video quality

# CRF 0 = lossless, 51 = worst quality, 18 = default (visually near-lossless)
emojiart input.mp4 out.mp4 --crf 23

Debugging

# Keep the raw and processed frame directories in /tmp/emojiart_*/
emojiart input.mp4 out.mp4 --keep-temp

FFmpeg Commands Explained

EmojiArt prints every FFmpeg command it runs. Here's what they mean:

Frame extraction

ffmpeg -y \
  -i input.mp4 \
  -vf "setpts=0.5000*PTS" \   # speed: 0.5 = half speed (2ร— more frames)
  -vsync 0 \                   # prevent duplicate/dropped frames
  -frame_pts 1 \               # embed PTS in output filenames
  /tmp/emojiart_xxx/raw_frames/frame_%06d.png
  • -vf copy is used when no scaling/speed is applied
  • -vsync 0 is critical for GIFs which have variable frame timing
  • setpts=FACTOR*PTS adjusts timing: factor < 1 = faster, > 1 = slower

Video rebuild

ffmpeg -y \
  -framerate 25.0000 \         # output FPS (original ร— speed multiplier)
  -i frame_%06d.png \          # processed emoji frames
  -i input.mp4 \               # original (for audio mux, if applicable)
  -c:v libx264 \               # H.264 encoder
  -crf 18 \                    # quality (lower = better)
  -preset medium \             # encoding speed vs compression tradeoff
  -pix_fmt yuv420p \           # max player compatibility
  -movflags +faststart \       # web-optimized: moov atom at front
  -c:a copy \                  # copy audio stream (or 'aac' + atempo for speed)
  -map 0:v:0 -map 1:a:0 \     # explicit stream mapping
  -shortest \                  # end when shortest stream ends
  output.mp4

Architecture

emojiart/
โ”œโ”€โ”€ __init__.py          # Package exports
โ”œโ”€โ”€ __main__.py          # python -m emojiart entry point
โ”œโ”€โ”€ cli.py               # Argument parsing + orchestration pipeline
โ”œโ”€โ”€ ffmpeg_utils.py      # FFmpeg subprocess wrappers
โ”‚                           probe_video()   โ†’ metadata dict
โ”‚                           extract_frames() โ†’ PNG files
โ”‚                           rebuild_video()  โ†’ MP4 output
โ”œโ”€โ”€ palette.py           # Color science + emoji mapping
โ”‚                           EmojiEntry       โ†’ dataclass
โ”‚                           load_palette()   โ†’ List[EmojiEntry]
โ”‚                           EmojiMapper      โ†’ RGB โ†’ emoji (CIE Lab ฮ”E)
โ””โ”€โ”€ renderer.py          # Per-frame image rendering
                            EmojiFrameRenderer.render() โ†’ PIL Image
                            render_frames_batch()        โ†’ List[Path]

Data flow

Input file
    โ”‚
    โ–ผ
[ffprobe] probe_video()
    โ†’ fps, resolution, has_audio
    โ”‚
    โ–ผ
[FFmpeg] extract_frames()
    โ†’ /tmp/.../raw_frames/frame_000001.png ...
    โ”‚
    โ–ผ
[Pillow] EmojiFrameRenderer.render() ร— N frames
    For each cell (col ร— row):
        avg_color(region) โ†’ (R, G, B)
        EmojiMapper.map(R, G, B) โ†’ emoji via CIE ฮ”E
        draw.rectangle(fill=avg_color)
        draw.text(emoji, font=emoji_font)
    โ†’ /tmp/.../emoji_frames/frame_000001.png ...
    โ”‚
    โ–ผ
[FFmpeg] rebuild_video()
    โ†’ output.mp4 (H.264 + audio)

Color matching algorithm

  1. Source pixel block averaged to one (R, G, B) triple
  2. Quantized to nearest 8 (reduces cache misses by ~8ร—)
  3. Converted to CIE L*a*b* (perceptual color space)
  4. CIE ฮ”E76 distance computed against every palette entry's pre-computed Lab value
  5. Minimum-distance entry wins โ†’ its emoji is rendered
  6. Result cached โ€” typical video needs only ~200โ€“500 unique cache entries

Performance

Resolution Cell px Frames/sec (CPU) Notes
16 32 ~80 fps Fast mode, great for quick drafts
32 16 ~30 fps Standard mode default
48 16 ~15 fps HQ mode
64 12 ~8 fps Very detailed, slow

Measurements on a modern laptop (Apple M2 / Ryzen 7). Performance scales linearly with grid size (O(cols ร— rows) per frame).

To speed up rendering:

  • Use --mode fast or a lower --resolution
  • For long videos, split into segments and render in parallel (shell & or xargs)
  • A GPU-accelerated emoji renderer (via torch or cupy) would be the next optimization step

Edge Cases & Known Limitations

Situation Behavior
GIF with variable frame timing FFmpeg normalizes to constant FPS via vsync 0
Input has no audio Audio flags are ignored silently
--speed outside [0.5, 2.0] Audio uses clamped atempo; video speed is unaffected
No emoji font on system Falls back to Pillow bitmap font (visible ASCII-like glyphs)
Very small input (< 16px) Grid auto-reduces to avoid zero-size cells
Unicode filenames Fully supported on Python 3.9+ on all platforms
Transparent GIF frames Converted to RGB (transparency โ†’ black background)
Very long video (> 30min) Works, but may use significant disk space in /tmp for frames
CRF 0 (lossless) Produces very large files; --crf 18 is recommended

Custom Palette Examples

Neon / cyberpunk

[
  {"emoji": "๐ŸŸฅ", "name": "hot-pink",    "rgb": [255, 0, 128]},
  {"emoji": "๐ŸŸฆ", "name": "cyber-cyan",  "rgb": [0, 255, 220]},
  {"emoji": "๐ŸŸจ", "name": "neon-yellow", "rgb": [220, 255, 0]},
  {"emoji": "๐ŸŸช", "name": "electric-purple", "rgb": [180, 0, 255]},
  {"emoji": "โฌ›", "name": "void-black",  "rgb": [5, 0, 20]},
  {"emoji": "โฌœ", "name": "grid-white",  "rgb": [200, 220, 255]}
]

Grayscale

[
  {"emoji": "โฌ›", "name": "black",  "rgb": [0,   0,   0]},
  {"emoji": "๐Ÿ–ค", "name": "dark",   "rgb": [50,  50,  50]},
  {"emoji": "๐Ÿฉถ", "name": "gray",   "rgb": [128, 128, 128]},
  {"emoji": "๐Ÿค", "name": "light",  "rgb": [200, 200, 200]},
  {"emoji": "โฌœ", "name": "white",  "rgb": [255, 255, 255]}
]

License

MIT โ€” see LICENSE.

if the "emojiart" command doesnt work try python -m emojiart input.mp4

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

emosaic-1.0.0.tar.gz (22.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

emosaic-1.0.0-py3-none-any.whl (17.8 kB view details)

Uploaded Python 3

File details

Details for the file emosaic-1.0.0.tar.gz.

File metadata

  • Download URL: emosaic-1.0.0.tar.gz
  • Upload date:
  • Size: 22.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for emosaic-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d3fd717059aaf4442bd2854eb06eddfdb55811a91c726792dda6f86733ec6474
MD5 ca872681a3828542fc342b19376300f2
BLAKE2b-256 157600978bc55e908066117942846fbb130bffb6334119d337b853915f9aef8e

See more details on using hashes here.

File details

Details for the file emosaic-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: emosaic-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 17.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for emosaic-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7a25d6e7767f79258339c6e87e50ba6e3edf32fa220b782cc279037513b045e4
MD5 d74195544d18991538bd2e490b84cf81
BLAKE2b-256 ead8db4eab5882840b8038cac1e57dfc0b26de8e75a9da9074b33e7f4d921eed

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page