Spaces:
Sleeping
Sleeping
| import numpy as np | |
| from rembg import remove, new_session | |
| from PIL import Image, ImageFilter, ImageOps, ImageChops, ImageColor, ImageEnhance | |
| from moviepy.editor import ImageClip, CompositeVideoClip, ColorClip | |
| import math | |
| # Load AI Model | |
| session = new_session("u2net") | |
| class IconAnimator: | |
| def __init__(self, input_path, output_path): | |
| self.input_path = input_path | |
| self.output_path = output_path | |
| def hex_to_rgb(self, hex_color): | |
| try: | |
| return ImageColor.getcolor(hex_color, "RGB") | |
| except: | |
| return (255, 255, 255) | |
| def get_edges(self, img, thickness=2): | |
| """Extracts the outline of the shape to create a 'Neon Tube' effect""" | |
| # Get alpha channel | |
| a = img.split()[-1] | |
| # Dilate (expand) and Erode (shrink) to find edges | |
| # Simple trick: Edge = Alpha - Eroded_Alpha | |
| eroded = a.filter(ImageFilter.MinFilter(3)) | |
| edges = ImageChops.difference(a, eroded) | |
| # Boost visibility | |
| edges = edges.point(lambda p: 255 if p > 50 else 0) | |
| return edges | |
| def process_image(self, remove_bg=True, keep_original=False, icon_color_hex="#FFFFFF"): | |
| with open(self.input_path, 'rb') as i: | |
| input_data = i.read() | |
| # 1. Background Removal | |
| if remove_bg: | |
| try: | |
| # alpha_matting=False is faster and safer for icons | |
| output_data = remove(input_data, session=session, alpha_matting=False) | |
| from io import BytesIO | |
| img = Image.open(BytesIO(output_data)).convert("RGBA") | |
| except: | |
| img = Image.open(self.input_path).convert("RGBA") | |
| else: | |
| img = Image.open(self.input_path).convert("RGBA") | |
| # 2. Resize (High Quality) | |
| if max(img.size) > 512: | |
| ratio = 512 / max(img.size) | |
| new_size = (int(img.width * ratio), int(img.height * ratio)) | |
| img = img.resize(new_size, Image.LANCZOS) | |
| # 3. Create Visual Style | |
| if keep_original: | |
| # Just sharpen it a bit | |
| img = ImageEnhance.Sharpness(img).enhance(1.2) | |
| base_icon = img | |
| else: | |
| # Create a "Neon Tube" look | |
| # A. Solid Fill | |
| r, g, b, alpha = img.split() | |
| target_rgb = self.hex_to_rgb(icon_color_hex) | |
| solid_fill = Image.new("RGB", img.size, target_rgb) | |
| fill_layer = Image.merge("RGBA", (*solid_fill.split(), alpha)) | |
| # B. White Edge (The Tube) | |
| edge_mask = self.get_edges(img) | |
| white_fill = Image.new("RGB", img.size, (255, 255, 255)) | |
| edge_layer = Image.merge("RGBA", (*white_fill.split(), edge_mask)) | |
| # Composite Edge on top of Fill | |
| base_icon = Image.alpha_composite(fill_layer, edge_layer) | |
| return base_icon | |
| def create_deep_glow(self, img, color_hex): | |
| """ | |
| Creates a 'Deep Glow' by stacking 4 layers of varying blur radii. | |
| This mimics professional compositing software. | |
| """ | |
| padding = 100 | |
| w, h = img.size | |
| new_size = (w + padding*2, h + padding*2) | |
| # 1. Prepare Base Glow Shape | |
| canvas = Image.new("RGBA", new_size, (0, 0, 0, 0)) | |
| canvas.paste(img, (padding, padding), img) | |
| # Colorize to Glow Color | |
| r, g, b, a = canvas.split() | |
| target_rgb = self.hex_to_rgb(color_hex) | |
| glow_fill = Image.new("RGB", new_size, target_rgb) | |
| glow_base = Image.merge("RGBA", (*glow_fill.split(), a)) | |
| # 2. Generate Stacked Blurs (The "Deep" Effect) | |
| # Radii: Tight, Mid, Wide, Atmosphere | |
| radii = [5, 15, 40, 80] | |
| opacities = [0.8, 0.6, 0.4, 0.2] | |
| composite_glow = Image.new("RGBA", new_size, (0, 0, 0, 0)) | |
| for radius, opacity in zip(radii, opacities): | |
| # Blur | |
| layer = glow_base.filter(ImageFilter.GaussianBlur(radius)) | |
| # Adjust Opacity | |
| r, g, b, a = layer.split() | |
| a = a.point(lambda p: int(p * opacity)) | |
| layer = Image.merge("RGBA", (r, g, b, a)) | |
| # Additive Blend (Screen) | |
| composite_glow = ImageChops.add(composite_glow, layer) | |
| return composite_glow, padding | |
| def create_reflection(self, img): | |
| reflection = ImageOps.flip(img) | |
| width, height = img.size | |
| # Non-linear fade for more realistic floor | |
| gradient = Image.new('L', (1, height), color=0xFF) | |
| for y in range(height): | |
| # Exponential falloff | |
| factor = (y / height) ** 0.5 | |
| alpha = 120 - int(factor * 255) | |
| if alpha < 0: alpha = 0 | |
| gradient.putpixel((0, y), alpha) | |
| alpha_mask = gradient.resize((width, height)) | |
| r, g, b, a = reflection.split() | |
| a = ImageChops.multiply(a, alpha_mask) | |
| reflection.putalpha(a) | |
| return reflection | |
| def generate_animation(self, icon_color="#FFFFFF", glow_color="#00E5FF", speed=2.0, intensity=1.2, | |
| use_original_colors=False, add_reflection=True, remove_bg=True): | |
| # 1. Prepare Base | |
| base_pil = self.process_image(remove_bg, use_original_colors, icon_color) | |
| w, h = base_pil.size | |
| # 2. Dimensions & Reflection | |
| reflection_h = 0 | |
| reflection_img = None | |
| if add_reflection: | |
| reflection_img = self.create_reflection(base_pil) | |
| reflection_h = int(h * 0.6) | |
| pad = 100 | |
| final_w = w + (pad * 2) | |
| final_h = h + (pad * 2) + reflection_h | |
| # 3. Create The Deep Glow Layer | |
| glow_pil, _ = self.create_deep_glow(base_pil, glow_color) | |
| # 4. Setup Video Clips | |
| clips_to_render = [] | |
| # Floor (Invisible Transparency Fix) | |
| bg_clip = ColorClip(size=(final_w, final_h), color=(0,0,0), duration=speed).set_opacity(0) | |
| clips_to_render.append(bg_clip) | |
| # A. The Glow Animation (Pulse) | |
| glow_clip = ImageClip(np.array(glow_pil)).set_duration(speed).set_position((0, 0)) | |
| def pulse_glow(t): | |
| # A heartbeat rhythm: fast up, slow down | |
| # Sine wave shifted to be always positive | |
| wave = math.sin(2 * math.pi * t / speed) | |
| # Map -1..1 to 0..1 | |
| norm = (wave + 1) / 2 | |
| # Intensity Modulation: Base 0.5, Max varies by intensity | |
| return 0.5 + (0.5 * norm * intensity) | |
| glow_clip.mask = glow_clip.mask.fl(lambda gf, t: gf(t) * pulse_glow(t)) | |
| clips_to_render.append(glow_clip) | |
| # B. The Reflection | |
| if add_reflection and reflection_img: | |
| ref_clip = ImageClip(np.array(reflection_img)).set_duration(speed) | |
| ref_clip = ref_clip.set_position((pad, pad + h - 5)) | |
| clips_to_render.append(ref_clip) | |
| # C. The Base Icon (Float Animation) | |
| base_clip = ImageClip(np.array(base_pil)).set_duration(speed) | |
| # Add a subtle "Hover" effect (bobbing up and down) | |
| def hover_pos(t): | |
| y_offset = 5 * math.sin(2 * math.pi * t / speed) | |
| return (pad, pad + y_offset) | |
| base_clip = base_clip.set_position(hover_pos) | |
| clips_to_render.append(base_clip) | |
| # 5. Composite | |
| final = CompositeVideoClip(clips_to_render, size=(final_w, final_h), bg_color=None) | |
| # 6. Export WEBM (High Quality) | |
| final.write_videofile( | |
| self.output_path, | |
| fps=24, | |
| codec='libvpx-vp9', | |
| ffmpeg_params=['-pix_fmt', 'yuva420p', '-auto-alt-ref', '0'], | |
| logger=None | |
| ) | |
| return self.output_path |