Convert images to Lego mosaic patterns with perceptual color matching
Project description
legopic
Convert images to LEGO mosaic patterns with perceptual color matching.
Installation
pip install legopic
Quick Start
Basic Conversion
from legopic import ConversionSession, ConvertConfig, Palette, load_image
# Load image and palette
image = load_image("photo.jpg")
palette = Palette.from_set(31197) # Andy Warhol's Marilyn Monroe set
# Create session and convert
session = ConversionSession(image, palette, canvas_size=(48, 48))
session.convert()
# Access result
print(f"Similarity score: {session.similarity_score:.2f}")
rgb_array = session.canvas.to_array() # numpy array (48, 48, 3)
for row in session.canvas.cells:
for cell in row:
print(f"({cell.x}, {cell.y}): {cell.color.name}")
Using All Standard LEGO Colors
from legopic import ConversionSession, Palette, load_image
# Load all 41 standard (opaque) LEGO colors
palette = Palette.from_set() # No set_id = all standard colors
image = load_image("photo.jpg")
session = ConversionSession(image, palette, (48, 48))
session.convert()
Using a Custom Palette
from legopic import ConversionSession, Palette, Color, load_image
# Create a custom palette
palette = Palette([
Color((255, 0, 0), "Red"),
Color((0, 255, 0), "Green"),
Color((0, 0, 255), "Blue"),
Color((255, 255, 255), "White"),
Color((0, 0, 0), "Black"),
])
image = load_image("photo.jpg")
session = ConversionSession(image, palette, canvas_size=(48, 48))
session.convert()
Different Downsampling Methods
from legopic import ConversionSession, ConvertConfig, Palette, load_image
image = load_image("photo.jpg")
palette = Palette.from_set(31198) # The Beatles
session = ConversionSession(image, palette, (48, 48))
# Use different methods via ConvertConfig
config = ConvertConfig(method='match_then_mode') # Best for sharp edges
session.convert(config)
# Re-convert with different method
session.reconvert(ConvertConfig(method='mean_then_match')) # Best for gradients
Interactive Editing Workflow
from legopic import ConversionSession, ConvertConfig, Palette, Color, load_image
image = load_image("photo.jpg")
palette = Palette.from_set(31197)
session = ConversionSession(image, palette, (48, 48))
# Initial conversion
session.convert(ConvertConfig(method='match_then_mode'))
print(f"Initial similarity: {session.similarity_score:.2f}")
# Pin a cell and change its color
custom_blue = Color((0, 100, 200), "Custom Blue")
session.pin(5, 10, custom_blue) # Pin cell at (5, 10) to custom blue
# Bulk swap a color throughout the canvas
old_red = Color((179, 0, 6), "Red")
new_orange = Color((255, 126, 20), "Orange")
count = session.swap_color(old_red, new_orange)
print(f"Swapped {count} cells from red to orange")
# Re-convert with a different method, preserving pinned cells
session.reconvert(ConvertConfig(method='mean_then_match'), keep_pins=True)
# Get pinned cells
pinned = session.get_pinned_cells()
print(f"Pinned cells: {pinned}")
Inventory-Limited Conversion
from legopic import ConversionSession, ConvertConfig, Palette, load_image
image = load_image("photo.jpg")
palette = Palette.from_set(31197) # Has specific element counts
session = ConversionSession(image, palette, (48, 48))
# Enable inventory limits - cells fall back to next-best color when preferred runs out
config = ConvertConfig(
method='match_then_mode',
limit_inventory=True,
algorithm='priority_greedy' # Fast heuristic
)
session.convert(config)
Exporting for Building Guide
from legopic import ConversionSession, Palette, load_image
image = load_image("photo.jpg")
palette = Palette.from_set(31197)
session = ConversionSession(image, palette, (48, 48))
session.convert()
# Bill of Materials
bom = session.get_bill_of_materials()
for entry in bom:
status = "✓" if entry.in_palette else "⚠ custom"
print(f"{entry.color.name}: {entry.count_needed} tiles {status}")
# Grid data for rendering
grid = session.get_grid_data()
for y, row in enumerate(grid):
for x, cell_data in enumerate(row):
print(f"({x},{y}): {cell_data.color.name}, ΔE={cell_data.delta_e:.1f}")
# Similarity map (identify problem areas)
sim_map = session.get_similarity_map()
max_delta_e = max(max(row) for row in sim_map)
print(f"Worst color match: ΔE={max_delta_e:.1f}")
Exporting to External Platforms
Export your parts list directly to BrickLink or Rebrickable for easy ordering:
from legopic import ConversionSession, Palette, load_image
image = load_image("photo.jpg")
palette = Palette.from_set(31197)
session = ConversionSession(image, palette, (48, 48))
session.convert()
# Export to BrickLink XML (for wanted list upload)
xml = session.export_bricklink_xml()
with open("wanted_list.xml", "w") as f:
f.write(xml)
# Export to Rebrickable CSV (for parts list import)
csv = session.export_rebrickable_csv()
with open("parts_list.csv", "w") as f:
f.write(csv)
The exports include platform-specific color IDs so you can directly upload to:
- BrickLink — Upload XML as a Wanted List
- Rebrickable — Import CSV as a Parts List
Available LEGO Sets
The package includes data for official LEGO Art sets:
| Set ID | Name | Canvas Size |
|---|---|---|
| 31197 | Andy Warhol's Marilyn Monroe | 48×48 |
| 31198 | The Beatles | 48×48 |
| 31202 | Disney's Mickey Mouse | 48×48 |
| 31203 | World Map | 128×80 |
| 31204 | Elvis Presley "The King" | 48×48 |
| 21226 | Art Project – Create Together | 48×48 |
from legopic.data import list_available_sets, get_set_dimensions
# List all sets
for set_id, name in list_available_sets():
width, height = get_set_dimensions(set_id)
print(f"{set_id}: {name} ({width}×{height})")
Features
Color Matching
Uses the Delta E (CIE2000) perceptual color distance metric via basic_colormath, which accounts for non-linearities in human vision perception.
Downsampling Methods
| Method | Description | Best For |
|---|---|---|
mean_then_match |
Average pixel colors, then match to palette | Smooth gradients |
match_then_mean |
Match each pixel, then average results | Balanced approach |
match_then_mode |
Match each pixel, take most common (default) | Sharp edges, distinct colors |
Dimension Validation
For uniform stride downsampling, image and canvas dimensions must satisfy:
image_width // canvas_width == image_height // canvas_height
This ensures every canvas cell maps to image pixels with a uniform block size.
Valid examples:
- Image 100×100 → Canvas 10×10 ✓
- Image 100×91 → Canvas 10×10 ✓ (last row has 1 pixel)
Invalid examples:
- Image 100×90 → Canvas 10×10 ✗ (height stride differs)
- Image 92×101 → Canvas 10×10 ✗ (width stride differs)
About LEGO Mosaics
What Are LEGO Mosaics?
LEGO mosaics are pixel-art style creations built using 1×1 round tiles (commonly called "studs" or "dots"). Each tile represents a single pixel, and when arranged on a baseplate, they form a cohesive image — similar to pointillism or cross-stitch patterns.
The official LEGO Art line (sets like 31197, 31198, etc.) popularized this technique, offering curated color palettes and building instructions for iconic portraits and artwork.
The 1×1 Round Tile (Element 98138)
The primary building block for LEGO mosaics is the 1×1 round plate with design ID 98138. This element:
- Has a smooth, circular top surface
- Sits flat on baseplates and standard LEGO bricks
- Comes in 40+ official colors
- Creates the characteristic "dotted" mosaic appearance
Where to Buy LEGO Tiles
Once you've designed your mosaic with legopic, you'll need to source the actual bricks. Here are the main marketplaces:
| Marketplace | Description |
|---|---|
| BrickLink | The largest secondary LEGO marketplace. Search by element ID (98138) to find tiles in any color. |
| BrickOwl | Alternative marketplace with competitive pricing and international sellers. |
| LEGO Pick-a-Brick | Official LEGO store. Limited color selection but guaranteed authenticity. |
Pro tip: Use session.export_bricklink_xml() to generate a ready-to-upload wanted list for BrickLink, or session.export_rebrickable_csv() for Rebrickable. No manual counting needed!
Contributing New Colors & Sets
The LEGO color palette evolves over time, and new Art sets are released regularly. We welcome community contributions!
If you'd like to add:
- New colors — Add entries to
src/legopic/data/colors.csv - New sets — Add set info to
sets.csvand element mappings toelements.csv
See the Contributing section for validation rules and submit a PR. The CI will automatically verify data integrity.
API Reference
Main API
| Class | Description |
|---|---|
ConversionSession(image, palette, canvas_size) |
Main workflow manager for conversion |
ConvertConfig(method, limit_inventory, algorithm) |
Soft parameters for conversion |
Models
| Class | Description |
|---|---|
Color(rgb, name=None) |
RGB color with optional name |
Element(element_id, design_id, variant_id, count=None) |
LEGO element variant for inventory tracking |
Cell(color, x=None, y=None) |
Single pixel/block with position |
Image(array) |
Input image wrapper |
Canvas(width, height) |
Output mosaic grid |
Palette(colors) |
Collection of available colors with element variants |
BOMEntry |
Bill of materials entry for export |
CellData |
Cell data for grid export |
ConversionSession Methods
| Method | Description |
|---|---|
convert(config=None) |
Run initial conversion |
reconvert(config=None, keep_pins=True) |
Re-convert preserving pinned cells |
pin(x, y, new_color=None) |
Pin a cell, optionally changing its color |
unpin(x, y) |
Unpin a cell |
swap_color(old, new, pin=True) |
Bulk swap all cells of one color |
get_pinned_cells() |
Get list of (x, y) pinned coordinates |
get_bill_of_materials() |
Get BOM for building guide |
get_grid_data() |
Get 2D cell data for rendering |
get_similarity_map() |
Get per-cell Delta E values |
export_bricklink_xml() |
Export BOM as BrickLink XML wanted list |
export_rebrickable_csv() |
Export BOM as Rebrickable CSV parts list |
ConversionSession Properties
| Property | Description |
|---|---|
image |
Source image (read-only) |
palette |
Available colors (read-only) |
canvas_size |
Target dimensions (read-only) |
canvas |
Current conversion result |
config |
Current configuration |
similarity_score |
Average Delta E across all cells |
Palette Methods
| Method | Description |
|---|---|
Palette.from_set(set_id=None, standard_only=True) |
Load from LEGO set or all colors |
palette.colors |
List of unique Color objects |
palette.elements |
List of all Element objects |
palette.get_elements_for_color(color) |
Get element variants for a color |
Canvas Methods
| Method | Description |
|---|---|
Canvas.from_set(set_id) |
Create empty canvas with set dimensions |
canvas.get_cell(x, y) |
Get cell at coordinates |
canvas.to_array() |
Convert to numpy RGB array |
Utility Functions
| Function | Description |
|---|---|
load_image(source) |
Load from file path or URL |
downsize(image, palette, width, height, method) |
Low-level resize with color matching |
match_color(targets, palette) |
Raw color matching utility |
export_bricklink_xml(bom) |
Export BOM list to BrickLink XML format |
export_rebrickable_csv(bom) |
Export BOM list to Rebrickable CSV format |
Development
Setup
# Clone the repository
git clone https://github.com/zl3311/lego_image_converter.git
cd lego_image_converter
# Install with uv (recommended)
uv sync --all-groups
# Or with pip
pip install -e ".[dev]"
Testing
# Run all tests
uv run pytest
# Run with coverage
uv run pytest --cov=legopic --cov-report=term-missing
Code Quality
# Lint
uv run ruff check src/ tests/
# Format
uv run ruff format src/ tests/
# Type check
uv run mypy src/legopic/
Contributing New Colors or Sets
When adding new LEGO colors or sets via PR, the CI automatically validates data integrity. The following checks run on every PR:
colors.csv Validation
| Check | Description |
|---|---|
Unique element_id |
Primary key must be unique |
| No exact duplicates | Same (design_id, name, r, g, b, variant_id) not allowed |
| Consistent RGB per name | Same color name must have same RGB values |
| Valid RGB range | Values must be integers in [0, 255] |
Valid is_standard |
Must be "true" or "false" |
Valid variant_id |
Must be a positive integer |
sets.csv Validation
| Check | Description |
|---|---|
Unique set_id |
Primary key must be unique |
| Unique names | Set names should not duplicate |
| Valid dimensions | Width and height must be positive integers |
| Dimension limits | Canvas cannot exceed 1024×1024 studs |
elements.csv Validation
| Check | Description |
|---|---|
| Unique pairs | (set_id, element_id) must be unique |
Valid element_id |
Must exist in colors.csv |
Valid set_id |
Must exist in sets.csv |
| Valid count | Must be a positive integer |
Cross-file Consistency
| Check | Description |
|---|---|
| Sets have elements | Every set must have at least one element |
Consistent design_id |
Must match between colors.csv and elements.csv |
Run these checks locally before submitting:
uv run pytest tests/unit/test_data_integrity.py -v
Background
This project was born from a passion for LEGO Art and the desire to create custom mosaic portraits.
Originally created for LEGO Art Project 21226 — a collaborative set designed for creative freedom — this package helps you prototype LEGO mosaic designs by:
- Converting any image to the constrained color palette of available LEGO tiles
- Downsizing intelligently to your target canvas dimensions (e.g., 48×48 studs)
- Using perceptually accurate color matching via the CIEDE2000 algorithm
Whether you're recreating a family photo, a pet portrait, or pixel art, legopic lets you preview exactly how your design will look before ordering hundreds of tiles. No more guesswork — just load an image, pick a palette, and see the result instantly.
Why Perceptual Color Matching?
Simple RGB distance doesn't account for how humans actually perceive color. Two colors might be mathematically similar but look completely different to our eyes. legopic uses the Delta E (CIE2000) metric, which models human vision to find the closest perceptual match — resulting in mosaics that look right, not just mathematically correct.
License
MIT License - see LICENSE for details.
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 legopic-0.4.0.tar.gz.
File metadata
- Download URL: legopic-0.4.0.tar.gz
- Upload date:
- Size: 806.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82f41d6d014f67e2fc8a84ddf43747c685f4313a028c1a0af980e480e7e4496f
|
|
| MD5 |
dcd58aa9de1d44843758d1a25f5888d8
|
|
| BLAKE2b-256 |
fd7b379b10083e5d1e60d64a03783087ac5142eb92b4398a3c58cba7db700c54
|
Provenance
The following attestation bundles were made for legopic-0.4.0.tar.gz:
Publisher:
release.yml on zl3311/lego_image_converter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
legopic-0.4.0.tar.gz -
Subject digest:
82f41d6d014f67e2fc8a84ddf43747c685f4313a028c1a0af980e480e7e4496f - Sigstore transparency entry: 752828626
- Sigstore integration time:
-
Permalink:
zl3311/lego_image_converter@83da3c1fa93c750d8bf7423faf4049e28440d91f -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/zl3311
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@83da3c1fa93c750d8bf7423faf4049e28440d91f -
Trigger Event:
push
-
Statement type:
File details
Details for the file legopic-0.4.0-py3-none-any.whl.
File metadata
- Download URL: legopic-0.4.0-py3-none-any.whl
- Upload date:
- Size: 44.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9b5b3ea0f46d4c7ee6820a35c13225e9ac1b80511530a30ce4fedd6914652af7
|
|
| MD5 |
5bc54817d5a93821995f78ea62cfaaaf
|
|
| BLAKE2b-256 |
00b03394cf5d199780349d0e59a6239b74216c67858a0cfea8c446fa2433d29a
|
Provenance
The following attestation bundles were made for legopic-0.4.0-py3-none-any.whl:
Publisher:
release.yml on zl3311/lego_image_converter
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
legopic-0.4.0-py3-none-any.whl -
Subject digest:
9b5b3ea0f46d4c7ee6820a35c13225e9ac1b80511530a30ce4fedd6914652af7 - Sigstore transparency entry: 752828635
- Sigstore integration time:
-
Permalink:
zl3311/lego_image_converter@83da3c1fa93c750d8bf7423faf4049e28440d91f -
Branch / Tag:
refs/tags/v0.4.0 - Owner: https://github.com/zl3311
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@83da3c1fa93c750d8bf7423faf4049e28440d91f -
Trigger Event:
push
-
Statement type: