|
|
|
|
|
""" |
|
雨效增强器(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 |
|
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 = 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: |
|
|
|
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 |
|
) -> 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("请先上传一张图片。") |
|
|
|
|
|
pil_rgba = img.convert("RGBA") |
|
w, h = pil_rgba.size |
|
|
|
|
|
rgba = np.array(pil_rgba) |
|
bgr = cv2.cvtColor(rgba, cv2.COLOR_RGBA2BGR) |
|
alpha_ch = rgba[:, :, 3] |
|
|
|
|
|
rng = np.random.default_rng(None if (seed is None or int(seed) == 0) else int(seed)) |
|
|
|
|
|
|
|
|
|
p_noise = float(np.interp(rain_intensity, [0, 100], [0.0015, 0.035])) |
|
|
|
|
|
|
|
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])) |
|
|
|
|
|
|
|
noise_mask = (rng.random((h, w)) < p_noise).astype(np.uint8) * 255 |
|
|
|
|
|
kernel = motion_kernel(length=line_len, angle_deg=angle_deg, thickness=line_thick) |
|
streaks = cv2.filter2D(noise_mask, ddepth=-1, kernel=kernel) |
|
|
|
|
|
streaks = cv2.normalize(streaks, None, 0, 255, cv2.NORM_MINMAX) |
|
streaks = cv2.GaussianBlur(streaks, (3, 3), 0) |
|
|
|
|
|
|
|
gamma = 0.9 |
|
streaks_f = (streaks / 255.0) ** gamma |
|
streaks = np.clip(streaks_f * 255.0, 0, 255).astype(np.uint8) |
|
|
|
|
|
rain_bgr = cv2.cvtColor(streaks, cv2.COLOR_GRAY2BGR) |
|
|
|
|
|
|
|
out_bgr = cv2.addWeighted(bgr, 1.0, rain_bgr, blend_alpha, 0.0) |
|
|
|
|
|
out_rgba = cv2.cvtColor(out_bgr, cv2.COLOR_BGR2RGBA) |
|
out_rgba[:, :, 3] = alpha_ch |
|
out_img = Image.fromarray(out_rgba) |
|
|
|
return out_img |
|
|
|
|
|
|
|
|
|
|
|
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"<div id='rainfx-head'><h1>{APP_TITLE}</h1><p>{APP_DESC}</p></div>") |
|
|
|
with gr.Row(): |
|
with gr.Column(scale=1): |
|
in_img = gr.Image( |
|
label="上传图片(保持原尺寸)", |
|
type="pil", |
|
image_mode="RGBA", |
|
height=360, |
|
) |
|
angle = gr.Slider( |
|
label="风向角度(°)", |
|
minimum=-80, |
|
maximum=80, |
|
value=0, |
|
step=1, |
|
info="0=垂直下落;正值向右偏;负值向左偏", |
|
) |
|
size_level = gr.Slider( |
|
label="雨的大小(影响长度&默认粗细)", |
|
minimum=1, |
|
maximum=100, |
|
value=50, |
|
step=1, |
|
info="长度 7~48 px;默认粗细已提升为 2~5 px", |
|
) |
|
rain_intensity = gr.Slider( |
|
label="雨量(密度&亮度)", |
|
minimum=0, |
|
maximum=100, |
|
value=50, |
|
step=1, |
|
info="越大越密、越明显。温和建议:40~70", |
|
) |
|
seed = gr.Number( |
|
label="随机种子(0=每次随机)", |
|
value=0, |
|
precision=0, |
|
) |
|
|
|
|
|
use_thickness_override = gr.Checkbox( |
|
label="手动设置雨丝粗细(像素)", |
|
value=False, |
|
) |
|
thickness_px = gr.Slider( |
|
label="雨丝粗细(像素)", |
|
minimum=1, |
|
maximum=9, |
|
value=3, |
|
step=1, |
|
info="仅在上方勾选时生效;数值越大线条越粗", |
|
) |
|
|
|
with gr.Row(): |
|
run_btn = gr.Button("✨ 生成效果", variant="primary") |
|
clear_btn = gr.ClearButton([in_img], value="清空图片") |
|
|
|
with gr.Column(scale=1): |
|
out_img = gr.Image( |
|
label="效果预览(可下载)", |
|
type="pil", |
|
image_mode="RGBA", |
|
show_download_button=True, |
|
height=360, |
|
) |
|
|
|
|
|
run_btn.click( |
|
fn=add_rain_effect, |
|
inputs=[in_img, angle, size_level, rain_intensity, seed, use_thickness_override, thickness_px], |
|
outputs=[out_img], |
|
api_name="rainfx", |
|
) |
|
|
|
|
|
for c in (in_img, angle, size_level, rain_intensity, seed, use_thickness_override, thickness_px): |
|
c.change( |
|
fn=add_rain_effect, |
|
inputs=[in_img, angle, size_level, rain_intensity, seed, use_thickness_override, thickness_px], |
|
outputs=[out_img], |
|
) |
|
|
|
if __name__ == "__main__": |
|
|
|
demo.launch(server_name="0.0.0.0", server_port=7860) |
|
|