# app.py # -*- coding: utf-8 -*- """ 雨效增强器(Rain FX)- Gradio 应用(增强版:支持手动控制雨丝粗细) 功能: - 为上传图片叠加“下雨”效果 - 可设置:风向(°)、雨的大小(长度&粗细一体化) - 新增:可选“手动设置雨丝粗细(像素)”,做极细/特粗风格 - 保持原图分辨率与尺寸不变 - 界面美观、布局合理,适合直接部署到 Hugging Face Spaces(CPU) 实现思路(核心算法简述): 1) 生成稀疏随机噪声作为“雨滴种子”(密度与强度相关)。 2) 通过自定义的“运动模糊卷积核”(可旋转的细线核)在指定角度方向拉伸成雨线。 3) 对雨线图做归一化与轻微模糊,使线条更自然。 4) 与原图采用加权叠加(仅对雨线区域变亮),最终得到雨景效果。 5) 全流程不改变输入图片的宽高(分辨率)与像素比例。 注意: - 角度约定:0° 代表“垂直向下”的雨;正值表示“向右偏”,负值表示“向左偏”。 - “雨的大小”同时影响雨线长度与粗细;你也可以调“雨量”控制密度/亮度强度。 - 本增强版将默认粗细区间提升为 2~5 px,并提供可选“手动粗细(1~9 px)”。 """ import math import random from typing import Optional import cv2 # 建议使用 opencv-python-headless import gradio as gr import numpy as np from PIL import Image # ---------------------------- # 工具函数:创建可旋转的运动模糊卷积核(雨线核) # ---------------------------- def motion_kernel(length: int, angle_deg: float, thickness: int = 1) -> np.ndarray: """ 创建一个 size=length x length 的卷积核,其中包含一条可旋转的直线,用于产生“运动模糊”效果。 - length: 雨线长度(卷积核尺寸,需为奇数以保证中心对齐) - angle_deg: 旋转角度,0 表示竖直向下;正值向右倾斜,负值向左倾斜 - thickness: 线条粗细(像素) 返回: 归一化后的 float32 卷积核(和为 1) """ # 确保 length 为 >=3 的奇数,避免旋转后中心漂移 length = int(max(3, length)) if length % 2 == 0: length += 1 k = np.zeros((length, length), dtype=np.float32) # 在正中画一条竖线(先默认竖直方向),稍后整体旋转到目标角度 center = length // 2 cv2.line( k, (center, 0), # 起点(上中) (center, length - 1), # 终点(下中) color=1.0, thickness=int(max(1, thickness)), lineType=cv2.LINE_AA ) # 围绕中心旋转到指定角度 M = cv2.getRotationMatrix2D((center, center), angle_deg, 1.0) k_rot = cv2.warpAffine( k, M, (length, length), flags=cv2.INTER_LINEAR, borderMode=cv2.BORDER_CONSTANT, borderValue=0 ) s = k_rot.sum() if s <= 1e-6: # 极端情况下(很细的线且角度接近 45° 时),避免全零 k_rot[center, center] = 1.0 s = 1.0 k_rot /= s return k_rot.astype(np.float32) # ---------------------------- # 核心:为图片添加雨效 # ---------------------------- def add_rain_effect( img: Image.Image, angle_deg: float = 0.0, size_level: int = 50, rain_intensity: int = 50, seed: Optional[int] = 0, use_thickness_override: bool = False, # 新增:是否手动指定粗细 thickness_px: int = 3 # 新增:手动粗细像素(1~9) ) -> Image.Image: """ 为输入 PIL 图片添加雨效(不改变分辨率)。 参数: - img: 输入图片(PIL.Image),支持 RGB/RGBA/JPEG/PNG 等 - angle_deg: 风向角度(°)。0=垂直下落;>0 向右偏;<0 向左偏 - size_level: 雨的大小(1~100),同时影响雨线长度与粗细(默认映射 2~5 px) - rain_intensity: 雨量(0~100),影响雨滴密度与亮度 - seed: 随机种子(相同种子可复现;0 表示随机) - use_thickness_override: 是否启用“手动粗细像素” - thickness_px: 手动粗细(像素),仅在 use_thickness_override=True 时生效 返回: - PIL.Image:与输入同尺寸的结果图(保持分辨率不变) """ if img is None: raise gr.Error("请先上传一张图片。") # 统一到 RGBA,便于保留透明通道(若原图无 alpha 也不会改变分辨率) pil_rgba = img.convert("RGBA") w, h = pil_rgba.size # numpy 格式(OpenCV 使用 BGR 或灰度) rgba = np.array(pil_rgba) bgr = cv2.cvtColor(rgba, cv2.COLOR_RGBA2BGR) alpha_ch = rgba[:, :, 3] # 原图 alpha,稍后原样带回 # 随机数发生器(可复现) rng = np.random.default_rng(None if (seed is None or int(seed) == 0) else int(seed)) # ---------- 参数映射 ---------- # 雨量 -> 噪声密度(越大越密) # 经验范围:0.0015 ~ 0.035(避免全屏白) p_noise = float(np.interp(rain_intensity, [0, 100], [0.0015, 0.035])) # 雨大小 -> 雨线长度与(默认)粗细 # 长度:7 ~ 48 像素;默认粗细:*提升为* 2 ~ 5 像素(较原版更粗) line_len = int(np.interp(size_level, [1, 100], [7, 48])) mapped_thick = int(np.interp(size_level, [1, 100], [2, 5])) # 若启用手动粗细,则覆盖默认映射;并进行安全夹取 if use_thickness_override: line_thick = int(np.clip(thickness_px, 1, 9)) else: line_thick = int(np.clip(mapped_thick, 1, 9)) # 雨量 -> 叠加强度(亮度权重),避免过曝 blend_alpha = float(np.interp(rain_intensity, [0, 100], [0.15, 0.65])) # ---------- 生成稀疏噪声(雨滴种子) ---------- # True/False 的布尔图,True 代表一个潜在雨滴种子 noise_mask = (rng.random((h, w)) < p_noise).astype(np.uint8) * 255 # 0/255 灰度 # ---------- 构建雨线卷积核并拉伸成条 ---------- kernel = motion_kernel(length=line_len, angle_deg=angle_deg, thickness=line_thick) streaks = cv2.filter2D(noise_mask, ddepth=-1, kernel=kernel) # 归一化到 0~255,并轻微平滑,降低颗粒感 streaks = cv2.normalize(streaks, None, 0, 255, cv2.NORM_MINMAX) streaks = cv2.GaussianBlur(streaks, (3, 3), 0) # 可选:轻微对比增强,让雨线更有层次 # 使用简单的 gamma 矫正(gamma < 1 变亮) gamma = 0.9 streaks_f = (streaks / 255.0) ** gamma streaks = np.clip(streaks_f * 255.0, 0, 255).astype(np.uint8) # 叠加为 3 通道白色雨线层(黑底) rain_bgr = cv2.cvtColor(streaks, cv2.COLOR_GRAY2BGR) # ---------- 与原图叠加(仅雨线区域亮化),不改变原图尺寸 ---------- # 使用 addWeighted:rain_bgr 的黑底对背景几乎无影响,白色线条按权重亮化 out_bgr = cv2.addWeighted(bgr, 1.0, rain_bgr, blend_alpha, 0.0) # 还原回 RGBA,并带回原 alpha 通道(不改变尺寸) out_rgba = cv2.cvtColor(out_bgr, cv2.COLOR_BGR2RGBA) out_rgba[:, :, 3] = alpha_ch # 保持输入透明度 out_img = Image.fromarray(out_rgba) return out_img # ---------------------------- # Gradio UI # ---------------------------- APP_TITLE = "🌧️ 雨效增强器(Rain FX)" APP_DESC = """ **给图片一键加“下雨”效果**,支持调节**风向**与**雨的大小**,不改变原图分辨率。 - 角度约定:`0° = 垂直向下`;正数 = 向右偏;负数 = 向左偏 - “雨的大小”同时影响雨线长度与**默认粗细(现已提升为更粗)** - “雨量”影响密度与亮度强度 - **新增**:可勾选“手动设置雨丝粗细(像素)”,范围 1~9 px """ CUSTOM_CSS = """ /* 简洁卡片风格 */ .gradio-container {max-width: 1080px !important; margin: auto;} #rainfx-head h1 {margin-bottom: .25rem} #rainfx-head p {opacity: .9} """ theme = gr.themes.Soft( primary_hue="blue", radius_size=gr.themes.sizes.radius_lg, ) with gr.Blocks(theme=theme, css=CUSTOM_CSS, title="Rain FX - Gradio") as demo: with gr.Column(): gr.Markdown(f"
{APP_DESC}