Skip to main content

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

Project description

๐ŸŽจ Emosaic

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.

emosaic 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, Emosaic falls back to Pillow's built-in bitmap font, which renders ASCII approximations. The output still works, but won't look as good.


Installation

pip install emosaic

With pipx (isolated environment)

pipx install emosaic

Verify

emosaic --help
emosaic --help

Usage

Basic

# Output defaults to emojioutput.mp4
emosaic input.mp4

# Explicit output name
emosaic input.mp4 output.mp4

# GIF input
emosaic animation.gif emoji_animation.mp4

Quality modes

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

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

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

Grid resolution

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

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

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

Speed control

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

# Double speed (timelapse)
emosaic 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
emosaic input.mp4 out.mp4 --no-audio

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

Preview (single frame)

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

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

The output path suffix is replaced with .png automatically.

Custom emoji palette

emosaic 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)
emosaic input.mp4 out.mp4 --faces

Video quality

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

Debugging

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

FFmpeg Commands Explained

Emosaic 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.

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.6.tar.gz (24.3 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.6-py3-none-any.whl (19.4 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: emosaic-1.0.6.tar.gz
  • Upload date:
  • Size: 24.3 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.6.tar.gz
Algorithm Hash digest
SHA256 ed5dc89dbce9966936fe6c45cd2994b6ebd1c12193b60b2745d51523e3b2bd3a
MD5 da590b3b154bef31c5621a79b74006a8
BLAKE2b-256 712b2591d5005adbf3c9829fd3132d6ba3beb8e42e13a5208c010ad3c563707a

See more details on using hashes here.

File details

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

File metadata

  • Download URL: emosaic-1.0.6-py3-none-any.whl
  • Upload date:
  • Size: 19.4 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.6-py3-none-any.whl
Algorithm Hash digest
SHA256 be5f947c12f258cccbd59f395bb5c4f9e21f47bbfd4a1e2af00d2d853a775255
MD5 9c575f0d98d35fb19c19b6e9fe9bcedf
BLAKE2b-256 09904e9d30179e82128a0337223cf9f784863f6de32239e27cf43234e2bb28a4

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