- Generate 18 classic eye variants in 3 styles (realistic, anime, cartoon) with 6 colors each (blue, green, brown, hazel, grey, amber) - Add scripts for generating, converting, and importing eyes: - generate_classic_eyes_ppm.py: Generate PPM images (no dependencies) - download_classic_eyes.py: Full-featured generator with PIL - startup_import_eyes.py: Import eyes on backend startup - import_saved_eyes.py: Import from saved directory - Update docker-compose to mount scripts directory - Include metadata.json for tracking eye properties
263 lines
8.2 KiB
Python
263 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Generate classic eye images using only the Python standard library.
|
|
Creates PPM format images that can be converted to PNG when PIL is available.
|
|
|
|
This script requires NO external dependencies - it uses the PPM image format
|
|
which is a simple text/binary format readable by most image tools.
|
|
|
|
Usage:
|
|
python scripts/generate_classic_eyes_ppm.py
|
|
|
|
The generated PPM files can be converted to PNG using:
|
|
- PIL/Pillow: Image.open('eye.ppm').save('eye.png')
|
|
- ImageMagick: convert eye.ppm eye.png
|
|
- GIMP: Open and export as PNG
|
|
"""
|
|
|
|
import os
|
|
import math
|
|
from pathlib import Path
|
|
|
|
|
|
def create_ppm(width: int, height: int) -> list:
|
|
"""Create an empty PPM image as a 2D list of (R, G, B) tuples."""
|
|
return [[(0, 0, 0) for _ in range(width)] for _ in range(height)]
|
|
|
|
|
|
def set_pixel(img: list, x: int, y: int, color: tuple):
|
|
"""Set a pixel in the image, with bounds checking."""
|
|
height = len(img)
|
|
width = len(img[0]) if height > 0 else 0
|
|
if 0 <= x < width and 0 <= y < height:
|
|
img[y][x] = color
|
|
|
|
|
|
def draw_filled_circle(img: list, cx: int, cy: int, radius: int, color: tuple):
|
|
"""Draw a filled circle."""
|
|
for y in range(-radius, radius + 1):
|
|
for x in range(-radius, radius + 1):
|
|
if x*x + y*y <= radius*radius:
|
|
set_pixel(img, cx + x, cy + y, color)
|
|
|
|
|
|
def draw_circle_ring(img: list, cx: int, cy: int, radius: int, color: tuple, thickness: int = 1):
|
|
"""Draw a circle outline."""
|
|
for y in range(-radius - thickness, radius + thickness + 1):
|
|
for x in range(-radius - thickness, radius + thickness + 1):
|
|
dist_sq = x*x + y*y
|
|
if (radius - thickness)**2 <= dist_sq <= (radius + thickness)**2:
|
|
set_pixel(img, cx + x, cy + y, color)
|
|
|
|
|
|
def blend_color(c1: tuple, c2: tuple, ratio: float) -> tuple:
|
|
"""Blend two colors. ratio=0 gives c1, ratio=1 gives c2."""
|
|
return (
|
|
int(c1[0] * (1 - ratio) + c2[0] * ratio),
|
|
int(c1[1] * (1 - ratio) + c2[1] * ratio),
|
|
int(c1[2] * (1 - ratio) + c2[2] * ratio)
|
|
)
|
|
|
|
|
|
def save_ppm(img: list, filepath: str):
|
|
"""Save image as PPM format (P6 binary)."""
|
|
height = len(img)
|
|
width = len(img[0]) if height > 0 else 0
|
|
|
|
with open(filepath, 'wb') as f:
|
|
# PPM header
|
|
f.write(f"P6\n{width} {height}\n255\n".encode())
|
|
# Pixel data
|
|
for row in img:
|
|
for r, g, b in row:
|
|
f.write(bytes([r, g, b]))
|
|
|
|
|
|
def save_ppm_text(img: list, filepath: str):
|
|
"""Save image as PPM format (P3 text - more portable)."""
|
|
height = len(img)
|
|
width = len(img[0]) if height > 0 else 0
|
|
|
|
with open(filepath, 'w') as f:
|
|
f.write(f"P3\n{width} {height}\n255\n")
|
|
for row in img:
|
|
line = ' '.join(f"{r} {g} {b}" for r, g, b in row)
|
|
f.write(line + '\n')
|
|
|
|
|
|
def create_classic_eye(
|
|
size: int = 200,
|
|
iris_color: tuple = (70, 130, 180),
|
|
style: str = "realistic"
|
|
) -> list:
|
|
"""
|
|
Generate a classic stylized eye image.
|
|
|
|
Args:
|
|
size: Image size (square)
|
|
iris_color: RGB color for the iris
|
|
style: "realistic", "anime", "cartoon"
|
|
|
|
Returns:
|
|
2D list of (R, G, B) tuples
|
|
"""
|
|
img = create_ppm(size, size)
|
|
|
|
cx, cy = size // 2, size // 2
|
|
eye_radius = size // 2 - 5
|
|
iris_radius = int(eye_radius * 0.7)
|
|
pupil_radius = int(iris_radius * 0.35)
|
|
|
|
if style == "realistic":
|
|
# White of the eye (sclera)
|
|
sclera_color = (250, 245, 240)
|
|
draw_filled_circle(img, cx, cy, eye_radius, sclera_color)
|
|
|
|
# Sclera outline
|
|
draw_circle_ring(img, cx, cy, eye_radius, (180, 160, 150), 2)
|
|
|
|
# Iris with gradient effect (simplified)
|
|
for r in range(iris_radius, 0, -1):
|
|
ratio = r / iris_radius
|
|
color = blend_color((40, 40, 40), iris_color, ratio)
|
|
draw_circle_ring(img, cx, cy, r, color, 1)
|
|
|
|
# Pupil
|
|
draw_filled_circle(img, cx, cy, pupil_radius, (10, 10, 10))
|
|
|
|
# Highlight
|
|
hl_x = cx - pupil_radius // 2
|
|
hl_y = cy - pupil_radius // 2
|
|
hl_r = pupil_radius // 3
|
|
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
|
|
|
|
elif style == "anime":
|
|
# Larger iris for anime style
|
|
iris_radius = int(eye_radius * 0.85)
|
|
|
|
# Sclera
|
|
draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255))
|
|
draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 3)
|
|
|
|
# Large iris
|
|
draw_filled_circle(img, cx, cy, iris_radius, iris_color)
|
|
|
|
# Pupil
|
|
pupil_radius = int(iris_radius * 0.25)
|
|
draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0))
|
|
|
|
# Large highlight
|
|
hl_x = cx - iris_radius // 3
|
|
hl_y = cy - iris_radius // 3
|
|
hl_r = iris_radius // 3
|
|
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
|
|
|
|
# Secondary highlight
|
|
hl2_x = cx + iris_radius // 4
|
|
hl2_y = cy + iris_radius // 4
|
|
hl2_r = iris_radius // 6
|
|
draw_filled_circle(img, hl2_x, hl2_y, hl2_r, (220, 220, 220))
|
|
|
|
elif style == "cartoon":
|
|
# Simple cartoon eye
|
|
draw_filled_circle(img, cx, cy, eye_radius, (255, 255, 255))
|
|
draw_circle_ring(img, cx, cy, eye_radius, (0, 0, 0), 4)
|
|
|
|
draw_filled_circle(img, cx, cy, iris_radius, iris_color)
|
|
draw_circle_ring(img, cx, cy, iris_radius, (0, 0, 0), 2)
|
|
|
|
draw_filled_circle(img, cx, cy, pupil_radius, (0, 0, 0))
|
|
|
|
# Highlight
|
|
hl_x = cx - pupil_radius
|
|
hl_y = cy - pupil_radius
|
|
hl_r = pupil_radius // 2
|
|
draw_filled_circle(img, hl_x, hl_y, hl_r, (255, 255, 255))
|
|
|
|
return img
|
|
|
|
|
|
def main():
|
|
"""Generate a set of classic eye images."""
|
|
output_dir = Path(__file__).parent.parent / "data" / "classic_eyes_ppm"
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
print("=" * 60)
|
|
print("Classic Eye Generator (PPM Format)")
|
|
print("=" * 60)
|
|
print(f"Output directory: {output_dir}")
|
|
|
|
# Eye colors
|
|
colors = {
|
|
"blue": (70, 130, 180),
|
|
"green": (60, 140, 90),
|
|
"brown": (139, 90, 43),
|
|
"hazel": (150, 120, 70),
|
|
"grey": (120, 130, 140),
|
|
"amber": (180, 130, 50),
|
|
}
|
|
|
|
styles = ["realistic", "anime", "cartoon"]
|
|
|
|
metadata = []
|
|
generated = 0
|
|
|
|
print("\nGenerating eyes...")
|
|
|
|
for style in styles:
|
|
for color_name, color_rgb in colors.items():
|
|
name = f"classic_{style}_{color_name}"
|
|
filename = f"{name}.ppm"
|
|
filepath = output_dir / filename
|
|
|
|
img = create_classic_eye(size=200, iris_color=color_rgb, style=style)
|
|
save_ppm(img, str(filepath))
|
|
|
|
metadata.append({
|
|
'filename': filename.replace('.ppm', '.png'), # For after conversion
|
|
'ppm_filename': filename,
|
|
'name': f"Classic {style.title()} Eye - {color_name.title()}",
|
|
'description': f"A {style} style eye with {color_name} iris color",
|
|
'tags': f"eye,classic,{style},{color_name},medium",
|
|
'width': 200,
|
|
'height': 200
|
|
})
|
|
|
|
generated += 1
|
|
print(f" Generated: {name}.ppm")
|
|
|
|
# Save metadata
|
|
import json
|
|
metadata_path = output_dir / "metadata.json"
|
|
with open(metadata_path, 'w') as f:
|
|
json.dump(metadata, f, indent=2)
|
|
|
|
print(f"\n Metadata saved to: {metadata_path}")
|
|
print(f"\nGenerated {generated} eye images in PPM format.")
|
|
|
|
# Create conversion script
|
|
convert_script = output_dir / "convert_to_png.py"
|
|
with open(convert_script, 'w') as f:
|
|
f.write('''#!/usr/bin/env python3
|
|
"""Convert PPM files to PNG using PIL."""
|
|
from pathlib import Path
|
|
from PIL import Image
|
|
|
|
ppm_dir = Path(__file__).parent
|
|
for ppm_file in ppm_dir.glob("*.ppm"):
|
|
png_file = ppm_file.with_suffix('.png')
|
|
img = Image.open(ppm_file)
|
|
img.save(png_file, 'PNG')
|
|
print(f"Converted: {ppm_file.name} -> {png_file.name}")
|
|
''')
|
|
|
|
print(f"\n Conversion script: {convert_script}")
|
|
print("\nTo convert PPM to PNG, run inside the backend container:")
|
|
print(f" python {convert_script}")
|
|
print("\nOr use ImageMagick:")
|
|
print(f" cd {output_dir} && for f in *.ppm; do convert \"$f\" \"${{f%.ppm}}.png\"; done")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|