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

pip install emosaic

With pipx (isolated environment)

pipx install emosaic

Verify

emojiart --help
emosaic --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.

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.5.tar.gz (22.9 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.5-py3-none-any.whl (18.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: emosaic-1.0.5.tar.gz
  • Upload date:
  • Size: 22.9 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.5.tar.gz
Algorithm Hash digest
SHA256 fb2db858fb0cb4a6d4b27f4f343b02b66cc2af7769dce30f9410a4adb6168711
MD5 c5b240a3cac6866940ba12fbf30cc99a
BLAKE2b-256 ed55be1794cb122fb5cb14151d9a01ad356dc0b490e50ad21d10c0f5d689740d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: emosaic-1.0.5-py3-none-any.whl
  • Upload date:
  • Size: 18.1 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.5-py3-none-any.whl
Algorithm Hash digest
SHA256 7deeae01654026b0af624b4e2c04cae7fd1b5f954fee36c01d71689c7a046596
MD5 8aec0401ed27c57b29995847322dee71
BLAKE2b-256 d46b08c7eec1d920e32697eca192ff67c109dce8c5aa356958f3fd4f95845a36

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