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}")
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)
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 |
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 |
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
Originally created for LEGO Art Project 21226, this package helps prototype LEGO mosaic designs by:
- Converting any image to the constrained color palette of available LEGO bricks
- Downsizing to the target canvas dimensions (e.g., 48×48 studs)
- Using perceptually accurate color matching
This saves time when planning custom LEGO Art builds, allowing you to preview the result before committing to a design.
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.3.0.tar.gz.
File metadata
- Download URL: legopic-0.3.0.tar.gz
- Upload date:
- Size: 800.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
afb4fc52bbe0ca46744b16c04b7530d6e59d8ae07524f875bbf249259cd26117
|
|
| MD5 |
0ce82c65c2c99439cfff89bd20f71f0b
|
|
| BLAKE2b-256 |
037298bc440573e7cb52294de3d18ab930cbbedc3c8bfd074664620ef2462233
|
File details
Details for the file legopic-0.3.0-py3-none-any.whl.
File metadata
- Download URL: legopic-0.3.0-py3-none-any.whl
- Upload date:
- Size: 39.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.9.15 {"installer":{"name":"uv","version":"0.9.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
095827208e84f37cf8e6c472ead0934179537afcc50527d68268a1f8298d9cd9
|
|
| MD5 |
7e47a9e591e227042ee48285d0527a33
|
|
| BLAKE2b-256 |
9351f963b36b7739102fa74a3056fdf949ca627c281adb7cebaf6f90ec793f72
|