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 Nsets emoji columns (rows auto-scale) - Three quality modes โ
fast/standard/hq - Speed control โ
--speed 0.5or--speed 2.0(adjusts both video and audio) - Custom emoji palettes โ JSON file with your own emoji + RGB definitions
- Single-frame preview โ
--previewoutputs 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 copyis used when no scaling/speed is applied-vsync 0is critical for GIFs which have variable frame timingsetpts=FACTOR*PTSadjusts 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
- Source pixel block averaged to one
(R, G, B)triple - Quantized to nearest 8 (reduces cache misses by ~8ร)
- Converted to CIE L*a*b* (perceptual color space)
- CIE ฮE76 distance computed against every palette entry's pre-computed Lab value
- Minimum-distance entry wins โ its emoji is rendered
- 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 fastor a lower--resolution - For long videos, split into segments and render in parallel (shell
&orxargs) - A GPU-accelerated emoji renderer (via
torchorcupy) 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file emosaic-1.0.4.tar.gz.
File metadata
- Download URL: emosaic-1.0.4.tar.gz
- Upload date:
- Size: 23.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8bcf0aa7f7b4124a1f9e9c7745b65ba7afa9cd7f2c004cdc13f90b275245648d
|
|
| MD5 |
23974523c651560076577dbb268ba401
|
|
| BLAKE2b-256 |
20d3bbf98cac1a792f15759746c1bfe22e8e2b59b010e2e1074b2143ad47ba97
|
File details
Details for the file emosaic-1.0.4-py3-none-any.whl.
File metadata
- Download URL: emosaic-1.0.4-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e3320e2e7fc8c007bd53502e854dc2eb823dc967ce9e98af603f0a31de5cf64
|
|
| MD5 |
18e13c97ce364517a29816cd914060c3
|
|
| BLAKE2b-256 |
1a37b58ee1a6be91e9687374e3765606fbc6d6296b2ebf5b6729368193f79624
|