Skip to content

Commit 96545cd

Browse files
Miyamura80claude
andcommitted
🔨 Improve logo generation quality and consistency
Fix patchy/pixelated edges by removing aggressive color quantization and implementing gentler greenscreen removal. Extract icons from wordmark instead of generating separately to ensure perfect visual consistency. Generate separate icon-light.png and icon-dark.png files for better theme support. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 602b924 commit 96545cd

7 files changed

Lines changed: 76 additions & 122 deletions

File tree

docs/public/favicon.ico

-1.25 KB
Binary file not shown.

docs/public/icon-dark.png

46.2 KB
Loading

docs/public/icon-light.png

44.9 KB
Loading

docs/public/icon.png

-93.7 KB
Binary file not shown.

docs/public/logo-dark.png

911 KB
Loading

docs/public/logo-light.png

911 KB
Loading

init/generate_logo.py

Lines changed: 76 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -22,61 +22,33 @@ class WordmarkDescription(dspy.Signature):
2222
)
2323

2424

25-
class IconDescription(dspy.Signature):
26-
"""Generate a creative description for a square icon (no text). The icon should be clean, modern, and recognizable."""
27-
28-
project_name: str = dspy.InputField()
29-
suggestion: str = dspy.InputField(
30-
desc="Optional suggestion to guide the icon description generation"
31-
)
32-
is_dark_mode: bool = dspy.InputField(
33-
desc="Whether this is for dark mode (light colors) or light mode (dark colors)"
34-
)
35-
icon_description: str = dspy.OutputField(
36-
desc="A creative description for a square icon without any text. Focus on simple shapes, clean lines, and a professional look."
37-
)
38-
39-
4025
client = genai.Client(api_key=global_config.GEMINI_API_KEY)
4126

4227

4328
def remove_greenscreen(img: Image.Image, tolerance: int = 60) -> Image.Image:
44-
"""Remove lime green/greenscreen background using aggressive chroma key removal."""
29+
"""Remove greenscreen with better edge preservation."""
4530
if img.mode != "RGBA":
4631
img = img.convert("RGBA")
4732

4833
data = np.array(img, dtype=np.float32)
4934
r, g, b, alpha = data[:, :, 0], data[:, :, 1], data[:, :, 2], data[:, :, 3]
5035

51-
# Step 1: Identify obvious greenscreen pixels (more aggressive threshold)
52-
green_high = g > 160 # Lowered from 180
53-
green_dominant = (g > r + tolerance) & (g > b + tolerance)
54-
red_blue_low = (r < 200) & (b < 200) # Raised from 180
55-
greenscreen_mask = green_high & green_dominant & red_blue_low
36+
# More conservative greenscreen detection
37+
green_high = g > 180 # Higher threshold
38+
green_dominant = (g > r + tolerance + 20) & (g > b + tolerance + 20)
39+
greenscreen_mask = green_high & green_dominant
5640

5741
# Set alpha to 0 for greenscreen pixels
5842
alpha[greenscreen_mask] = 0
5943

60-
# Step 2: Aggressively remove green spill from ALL pixels with green tint
61-
visible = alpha > 0
62-
has_green_tint = g > r + 10 # Very low threshold
63-
has_strong_green_tint = g > b + 10
64-
65-
green_tinted = visible & has_green_tint & has_strong_green_tint
44+
# Gentler green spill removal - only on visible pixels
45+
visible = alpha > 128 # Only strong pixels
46+
has_green_tint = (g > r + 20) & (g > b + 20)
47+
green_tinted = visible & has_green_tint
6648

6749
if np.any(green_tinted):
68-
# Reduce green channel more aggressively
6950
avg_rb = (r[green_tinted] + b[green_tinted]) / 2
70-
# Take whichever is lower: 30% of original green, or average of R&B
71-
g[green_tinted] = np.minimum(g[green_tinted] * 0.3, avg_rb)
72-
73-
# Step 3: Make any remaining greenish pixels more transparent
74-
still_greenish = (alpha > 0) & (g > r + 5) & (g > b + 5)
75-
alpha[still_greenish] *= 0.5 # Make them much more transparent
76-
77-
# Step 4: Completely remove very faint greenish pixels
78-
very_faint_green = (alpha > 0) & (alpha < 150) & (g > np.maximum(r, b))
79-
alpha[very_faint_green] = 0
51+
g[green_tinted] = np.minimum(g[green_tinted] * 0.6, avg_rb) # Less aggressive
8052

8153
data[:, :, 0] = r
8254
data[:, :, 1] = g
@@ -121,19 +93,23 @@ def invert_colors(img: Image.Image) -> Image.Image:
12193
async def generate_logo(
12294
project_name: str, suggestion: str | None = None, output_dir: Path | None = None
12395
) -> dict[str, Image.Image]:
124-
"""Generate logo assets using the new pipeline:
125-
1. Generate light mode wordmark
126-
2. Reduce color variance
127-
3. Invert colors for dark mode
128-
4. Generate square icons for both light and dark modes
96+
"""Generate logo assets using AI-powered pipeline with consistent branding:
97+
1. Generate light mode wordmark with greenscreen
98+
2. Extract icon from wordmark (removes text, keeps icon)
99+
3. Remove greenscreen from both wordmark and icon
100+
4. Invert colors for dark mode wordmark
101+
5. Invert colors for dark mode icon
102+
6. Save all assets including favicon
103+
104+
This ensures the icon in the wordmark matches the standalone icon perfectly.
129105
130106
Args:
131107
project_name: Name of the project
132108
suggestion: Optional suggestion to guide the logo generation
133109
output_dir: Output directory for the generated images. Defaults to docs/public/
134110
135111
Returns:
136-
Dictionary of generated images
112+
Dictionary of generated images (wordmark_light, wordmark_dark, icon_light, icon_dark, favicon)
137113
"""
138114
# Determine output directory
139115
if output_dir is None:
@@ -175,106 +151,70 @@ async def generate_logo(
175151
if light_img is None:
176152
raise ValueError("No light mode wordmark generated")
177153

178-
print("Removing greenscreen...")
179-
light_img = remove_greenscreen(light_img)
180-
181-
# ============================================================
182-
# 2. Reduce color variance
183-
# ============================================================
184-
print("\n=== Step 2: Reducing Color Variance ===")
185-
print("Quantizing colors to reduce variance...")
186-
light_img = reduce_color_variance(light_img, colors=6)
187-
188-
light_path = output_dir / "logo-light.png"
189-
light_img.save(light_path)
190-
print(f"✓ Light mode wordmark saved to: {light_path}")
191-
results["wordmark_light"] = light_img
192-
193154
# ============================================================
194-
# 3. Generate dark mode by inverting colors
155+
# 2. Extract icon from wordmark (before greenscreen removal)
195156
# ============================================================
196-
print("\n=== Step 3: Generating Dark Mode (Invert Colors) ===")
197-
print("Inverting colors from light mode...")
198-
dark_img = invert_colors(light_img)
157+
print("\n=== Step 2: Extracting Square Icon from Wordmark ===")
158+
print("Asking AI to remove text and preserve only the icon...")
199159

200-
dark_path = output_dir / "logo-dark.png"
201-
dark_img.save(dark_path)
202-
print(f"✓ Dark mode wordmark saved to: {dark_path}")
203-
results["wordmark_dark"] = dark_img
204-
205-
# ============================================================
206-
# 4. Generate square icons for light mode
207-
# ============================================================
208-
print("\n=== Step 4: Generating Square Icon (Light Mode) ===")
209-
icon_inf = DSPYInference(pred_signature=IconDescription, observe=False)
210-
icon_light_result = await icon_inf.run(
211-
project_name=project_name,
212-
suggestion=suggestion or "",
213-
is_dark_mode=False,
214-
)
160+
icon_extract_prompt = f"Remove ALL TEXT from this image. Keep ONLY the icon/symbol on the left side. Output a SQUARE 1:1 aspect ratio image with the icon centered. Preserve the BRIGHT LIME GREEN (#00FF00) GREENSCREEN background exactly as it is. Do not change any colors of the icon itself - keep them identical to the original. Just remove the text '{project_name}' and center the icon in a square format."
215161

216-
print(f"Light icon description: {icon_light_result.icon_description}")
217-
218-
icon_light_prompt = f"{icon_light_result.icon_description}. Create a SQUARE 1:1 aspect ratio icon/symbol. NO TEXT should appear. Use DARK colors (black, dark gray, dark blue, etc.) suitable for light backgrounds. The icon should be bold, simple, and instantly recognizable. Center it with minimal padding. CRITICAL: Use a BRIGHT LIME GREEN (#00FF00) GREENSCREEN background. Do not use lime green in the icon itself."
219-
220-
print("Generating light mode icon with Gemini...")
221-
icon_light_resp = client.models.generate_content(
162+
print("Generating square icon by extracting from wordmark...")
163+
icon_extract_resp = client.models.generate_content(
222164
model="gemini-3-pro-image-preview",
223-
contents=[icon_light_prompt],
165+
contents=[icon_extract_prompt, light_img],
224166
config=types.GenerateContentConfig(response_modalities=["TEXT", "IMAGE"]),
225167
)
226168

227169
icon_light_img = None
228-
for part in icon_light_resp.candidates[0].content.parts: # type: ignore
170+
for part in icon_extract_resp.candidates[0].content.parts: # type: ignore
229171
if part.inline_data and part.inline_data.mime_type.startswith("image/"): # type: ignore
230172
icon_light_img = Image.open(BytesIO(part.inline_data.data)) # type: ignore
231173
break
232174

233175
if icon_light_img is None:
234-
raise ValueError("No light mode icon generated")
176+
raise ValueError("No light mode icon extracted")
235177

236-
print("Removing greenscreen from light icon...")
178+
print("Removing greenscreen from extracted icon...")
237179
icon_light_img = remove_greenscreen(icon_light_img)
238180

239181
# ============================================================
240-
# 5. Generate square icons for dark mode
182+
# 3. Remove greenscreen from wordmark
241183
# ============================================================
242-
print("\n=== Step 5: Generating Square Icon (Dark Mode) ===")
243-
icon_dark_result = await icon_inf.run(
244-
project_name=project_name,
245-
suggestion=suggestion or "",
246-
is_dark_mode=True,
247-
)
248-
249-
print(f"Dark icon description: {icon_dark_result.icon_description}")
250-
251-
icon_dark_prompt = f"{icon_dark_result.icon_description}. Create a SQUARE 1:1 aspect ratio icon/symbol. NO TEXT should appear. Use LIGHT colors (white, light gray, light cyan, etc.) suitable for dark backgrounds. The icon should be bold, simple, and instantly recognizable. Center it with minimal padding. CRITICAL: Use a BRIGHT LIME GREEN (#00FF00) GREENSCREEN background. Do not use lime green in the icon itself."
184+
print("\n=== Step 3: Removing Greenscreen from Wordmark ===")
185+
print("Removing greenscreen...")
186+
light_img = remove_greenscreen(light_img)
252187

253-
print("Generating dark mode icon with Gemini...")
254-
icon_dark_resp = client.models.generate_content(
255-
model="gemini-3-pro-image-preview",
256-
contents=[icon_dark_prompt],
257-
config=types.GenerateContentConfig(response_modalities=["TEXT", "IMAGE"]),
258-
)
188+
light_path = output_dir / "logo-light.png"
189+
light_img.save(light_path)
190+
print(f"✓ Light mode wordmark saved to: {light_path}")
191+
results["wordmark_light"] = light_img
259192

260-
icon_dark_img = None
261-
for part in icon_dark_resp.candidates[0].content.parts: # type: ignore
262-
if part.inline_data and part.inline_data.mime_type.startswith("image/"): # type: ignore
263-
icon_dark_img = Image.open(BytesIO(part.inline_data.data)) # type: ignore
264-
break
193+
# ============================================================
194+
# 4. Generate dark mode by inverting colors
195+
# ============================================================
196+
print("\n=== Step 4: Generating Dark Mode (Invert Colors) ===")
197+
print("Inverting colors from light mode for wordmark...")
198+
dark_img = invert_colors(light_img)
265199

266-
if icon_dark_img is None:
267-
raise ValueError("No dark mode icon generated")
200+
dark_path = output_dir / "logo-dark.png"
201+
dark_img.save(dark_path)
202+
print(f"✓ Dark mode wordmark saved to: {dark_path}")
203+
results["wordmark_dark"] = dark_img
268204

269-
print("Removing greenscreen from dark icon...")
270-
icon_dark_img = remove_greenscreen(icon_dark_img)
205+
# ============================================================
206+
# 5. Generate dark mode icon by inverting
207+
# ============================================================
208+
print("\n=== Step 5: Inverting Icon for Dark Mode ===")
209+
print("Inverting colors from light mode icon...")
210+
icon_dark_img = invert_colors(icon_light_img)
271211

272212
# ============================================================
273213
# 6. Save icon versions (use light mode icon for favicon)
274214
# ============================================================
275215
print("\n=== Saving Icon Versions ===")
276216

277-
# Ensure icon is square
217+
# Ensure light icon is square
278218
width, height = icon_light_img.size
279219
if width != height:
280220
size = max(width, height)
@@ -284,22 +224,36 @@ async def generate_logo(
284224
new_icon.paste(icon_light_img, (paste_x, paste_y))
285225
icon_light_img = new_icon
286226

287-
# Generate favicon sizes
227+
# Ensure dark icon is square
228+
width, height = icon_dark_img.size
229+
if width != height:
230+
size = max(width, height)
231+
new_icon = Image.new("RGBA", (size, size), (255, 255, 255, 0))
232+
paste_x = (size - width) // 2
233+
paste_y = (size - height) // 2
234+
new_icon.paste(icon_dark_img, (paste_x, paste_y))
235+
icon_dark_img = new_icon
236+
237+
# Generate favicon and icon sizes
288238
favicon_32 = icon_light_img.resize((32, 32), Image.Resampling.LANCZOS)
289-
icon_512 = icon_light_img.resize((512, 512), Image.Resampling.LANCZOS)
239+
icon_light_512 = icon_light_img.resize((512, 512), Image.Resampling.LANCZOS)
240+
icon_dark_512 = icon_dark_img.resize((512, 512), Image.Resampling.LANCZOS)
290241

291242
# Save icon versions
292-
icon_path = output_dir / "icon.png"
243+
icon_light_path = output_dir / "icon-light.png"
244+
icon_dark_path = output_dir / "icon-dark.png"
293245
favicon_path = output_dir / "favicon.ico"
294246

295-
icon_512.save(icon_path)
247+
icon_light_512.save(icon_light_path)
248+
icon_dark_512.save(icon_dark_path)
296249
favicon_32.save(favicon_path, format="ICO")
297250

298-
print(f"✓ Icon saved to: {icon_path}")
251+
print(f"✓ Light icon saved to: {icon_light_path}")
252+
print(f"✓ Dark icon saved to: {icon_dark_path}")
299253
print(f"✓ Favicon saved to: {favicon_path}")
300254

301-
results["icon_light"] = icon_light_img
302-
results["icon_dark"] = icon_dark_img
255+
results["icon_light"] = icon_light_512
256+
results["icon_dark"] = icon_dark_512
303257
results["favicon"] = favicon_32
304258

305259
print("\n=== All assets generated successfully! ===")

0 commit comments

Comments
 (0)