youneeds's picture
Update app.py
c2e8c28 verified
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import gradio as gr
import requests
import base64
import json
import os
import gc
import numpy as np
import cv2
from PIL import Image
from io import BytesIO
# ========== 可选依赖导入(云端API)==========
try:
import dashscope
from dashscope import ImageSynthesis
DASHSCOPE_AVAILABLE = True
except ImportError:
DASHSCOPE_AVAILABLE = False
try:
import google.generativeai as genai
GOOGLE_API_AVAILABLE = True
except ImportError:
GOOGLE_API_AVAILABLE = False
# ================== 配置 ==================
SILICONFLOW_API_BASE = "https://api.siliconflow.cn/v1"
# 图像编辑模型(SiliconFlow)
DEFAULT_EDIT_MODEL_SF = "Qwen/Qwen-Image-Edit"
# 文本图像生成模型(通义千问)
DEFAULT_TEXT2IMG_MODEL_TONGYI = "wanx-v1"
# 图像生成模型(Gemini)
DEFAULT_TEXT2IMG_MODEL_GEMINI = "gemini-1.5-pro" # Gemini 实际上通过 generate_content 生成图像,需具体实现
BASE_NEGATIVE = "worst quality, low quality, ugly, deformed, blurry, messy, watermark, signature"
# ================== 风格预设(与之前一致)==================
def load_style_presets():
default_presets = {
"自定义": {"positive": "", "negative": BASE_NEGATIVE},
"🎨 二次元动漫": {"positive": "anime style, manga, vibrant colors, cel shading, studio ghibli, high quality, detailed background", "negative": "photorealistic, realistic, 3d render, blurry, low quality, ugly"},
"📸 写实摄影": {"positive": "photorealistic, 8k uhd, professional photography, sharp focus, natural lighting, high detail, canon r5", "negative": "anime, cartoon, illustration, painting, blurry, low quality"},
"🌆 赛博朋克": {"positive": "cyberpunk city, neon lights, dark alley, rain, glowing signs, intricate details, blade runner style", "negative": "bright daylight, rural, low quality, ugly"},
"🖌️ 油画风格": {"positive": "oil painting, thick brushstrokes, canvas texture, impressionist, rich colors, artistic", "negative": "photorealistic, smooth, digital art, 3d render, blurry"},
"💧 水彩风格": {"positive": "watercolor painting, soft edges, paper texture, flowing colors, artistic, delicate", "negative": "oil painting, thick strokes, high contrast, ugly"},
"🔮 梦幻奇幻": {"positive": "fantasy art, ethereal, glowing magical atmosphere, dreamlike, detailed, intricate, soft colors", "negative": "realistic, modern, city, ugly, low quality"},
"🔥 火焰/灯光特效增强": {"positive": "dramatic lighting, cinematic light, glowing fire, warm rim light, high contrast, volumetric light", "negative": "flat lighting, no shadows, dull, low contrast"},
}
json_path = os.path.join(os.path.dirname(__file__), "prompts.json")
if os.path.exists(json_path):
try:
with open(json_path, "r", encoding="utf-8") as f:
user = json.load(f)
default_presets.update(user)
except Exception as e:
print(f"加载 prompts.json 失败: {e}")
return default_presets
STYLE_PRESETS = load_style_presets()
# ================== 工具函数 ==================
def encode_image_to_base64(pil_image, format="PNG"):
buffered = BytesIO()
pil_image.save(buffered, format=format)
return base64.b64encode(buffered.getvalue()).decode("utf-8")
def pil_to_data_url(pil_image):
return f"data:image/png;base64,{encode_image_to_base64(pil_image)}"
# ================== 云端API调用函数 ==================
def siliconflow_image_edit(content_img, prompt, negative_prompt, api_key, strength=0.3):
"""调用 SiliconFlow Qwen-Image-Edit API"""
if not api_key:
return None, "请提供 SiliconFlow API Key"
if content_img is None:
return None, "请上传内容图像"
try:
img_url = pil_to_data_url(content_img)
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
payload = {
"model": DEFAULT_EDIT_MODEL_SF,
"image": img_url,
"prompt": prompt,
"negative_prompt": negative_prompt if negative_prompt else "",
"num_inference_steps": 30,
"guidance_scale": 7.0,
"strength": strength,
"output_format": "url"
}
response = requests.post(f"{SILICONFLOW_API_BASE}/images/edits", headers=headers, json=payload, timeout=120)
if response.status_code == 200:
data = response.json()
img_url = data['images'][0]['url']
img_resp = requests.get(img_url, timeout=60)
if img_resp.status_code == 200:
return Image.open(BytesIO(img_resp.content)), "SiliconFlow 重绘成功"
else:
return None, f"下载图片失败: {img_resp.status_code}"
else:
return None, f"API 错误 {response.status_code}: {response.text}"
except Exception as e:
return None, f"异常: {str(e)}"
def tongyi_text2img(prompt, negative_prompt, api_key):
"""通义千问文生图(无法编辑原图,仅根据提示词生成新图)"""
if not api_key:
return None, "请提供通义千问 API Key"
if not DASHSCOPE_AVAILABLE:
return None, "请安装 dashscope: pip install dashscope"
try:
dashscope.api_key = api_key
response = ImageSynthesis.call(model=DEFAULT_TEXT2IMG_MODEL_TONGYI,
prompt=prompt,
negative_prompt=negative_prompt,
n=1,
size='1024*1024')
if response.status_code == 200:
img_url = response.output.results[0].url
img_resp = requests.get(img_url, timeout=60)
if img_resp.status_code == 200:
return Image.open(BytesIO(img_resp.content)), "通义千问生成成功"
else:
return None, f"下载图片失败: {img_resp.status_code}"
else:
return None, f"API 错误: {response.message}"
except Exception as e:
return None, f"异常: {str(e)}"
def gemini_text2img(prompt, negative_prompt, api_key):
"""Gemini 文生图(需要支持图像输出的模型)"""
if not api_key:
return None, "请提供 Gemini API Key"
if not GOOGLE_API_AVAILABLE:
return None, "请安装 google-generativeai: pip install google-generativeai"
try:
genai.configure(api_key=api_key)
model = genai.GenerativeModel(DEFAULT_TEXT2IMG_MODEL_GEMINI)
# Gemini 目前图像生成需要特殊模型,此处简单占位
# 实际上 Gemini 图像生成需使用 Imagen 模型,这里简化处理
response = model.generate_content(f"Generate an image: {prompt}")
# 解析响应获取图像(假设返回图片base64)
# 实际情况需根据Gemini API最新文档调整
return None, "Gemini 文生图暂未完全实现,请使用 SiliconFlow 或通义千问"
except Exception as e:
return None, f"异常: {str(e)}"
def cloud_relight(service, content_img, prompt, negative_prompt, api_key, strength):
"""根据服务选择调用相应API"""
if service == "SiliconFlow":
return siliconflow_image_edit(content_img, prompt, negative_prompt, api_key, strength)
elif service == "通义千问":
if content_img is not None:
# 通义千问暂不支持图生图,可提示用户未实现
return None, "通义千问 API 目前仅支持文生图,不会保留原图结构。建议使用 SiliconFlow。"
else:
return tongyi_text2img(prompt, negative_prompt, api_key)
elif service == "Gemini":
return gemini_text2img(prompt, negative_prompt, api_key)
else:
return None, "未知服务"
# ================== 保留原有本地处理功能 ==================
def lighting_transfer_advanced(image, exposure, temp, contrast, intensity):
if image is None:
return None, "无图像"
img = np.array(image).astype(np.float32) / 255.0
lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)
L, a, b = cv2.split(lab)
if exposure != 0:
gamma = 1.0 / (1.0 + exposure * 0.8) if exposure >= 0 else 1.0 / (1.0 - exposure * 0.6)
L = np.power(L, gamma)
L = np.clip(L, 0, 1)
if contrast != 1.0:
L_mid = 0.5
L = (L - L_mid) * contrast + L_mid
L = np.clip(L, 0, 1)
if temp != 0:
a = a + temp * 0.1
b = b - temp * 0.05
a = np.clip(a, -128, 127)
b = np.clip(b, -128, 127)
lab_adjusted = cv2.merge([L, a, b])
rgb_adjusted = cv2.cvtColor(lab_adjusted, cv2.COLOR_LAB2RGB)
rgb_adjusted = np.clip(rgb_adjusted, 0, 1)
if intensity < 1.0:
rgb_adjusted = cv2.addWeighted(img, 1-intensity, rgb_adjusted, intensity, 0)
result = (rgb_adjusted * 255).astype(np.uint8)
return Image.fromarray(result), f"滑块调整完成 (曝光{exposure:+.1f}, 色温{temp:+.1f}, 对比度{contrast:.2f}, 强度{intensity:.2f})"
def apply_lighting_effect(image, lighting_type):
if image is None:
return None, "无图像"
img = np.array(image)
if lighting_type == "电影级灯光":
img = cv2.convertScaleAbs(img, alpha=1.2, beta=5)
hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV).astype(np.float32)
hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.3, 0, 255)
img = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
elif lighting_type == "柔和漫射光":
img = cv2.convertScaleAbs(img, alpha=0.9, beta=20)
img = cv2.GaussianBlur(img, (3,3), 0.5)
elif lighting_type == "边缘轮廓光":
lab = cv2.cvtColor(img, cv2.COLOR_RGB2LAB)
l, a, b = cv2.split(lab)
l = cv2.addWeighted(l, 1.2, np.zeros_like(l), 0, 10)
lab = cv2.merge([l, a, b])
img = cv2.cvtColor(lab, cv2.COLOR_LAB2RGB)
elif lighting_type == "霓虹光效":
hsv = cv2.cvtColor(img, cv2.COLOR_RGB2HSV).astype(np.float32)
hsv[:,:,0] = (hsv[:,:,0] + 30) % 180
hsv[:,:,1] = np.clip(hsv[:,:,1] * 1.5, 0, 255)
hsv[:,:,2] = np.clip(hsv[:,:,2] * 0.7, 0, 255)
img = cv2.cvtColor(hsv.astype(np.uint8), cv2.COLOR_HSV2RGB)
elif lighting_type == "黄金时刻光":
b, g, r = cv2.split(img)
r = np.clip(r * 1.3, 0, 255).astype(np.uint8)
g = np.clip(g * 1.1, 0, 255).astype(np.uint8)
b = (b * 0.8).astype(np.uint8)
img = cv2.merge([r, g, b])
elif lighting_type == "影棚标准光":
img = cv2.convertScaleAbs(img, alpha=1.0, beta=10)
return Image.fromarray(img), f"已应用滤镜: {lighting_type}"
def color_transfer(content_img, style_img, intensity=0.8):
if content_img is None or style_img is None:
return None, "请同时上传内容图和参考图"
content_np = np.array(content_img).astype(np.float32) / 255.0
style_np = np.array(style_img.resize(content_img.size)).astype(np.float32) / 255.0
content_lab = cv2.cvtColor(content_np, cv2.COLOR_RGB2LAB)
style_lab = cv2.cvtColor(style_np, cv2.COLOR_RGB2LAB)
for i in range(1,3):
c_mean = content_lab[:,:,i].mean()
c_std = content_lab[:,:,i].std()
s_mean = style_lab[:,:,i].mean()
s_std = style_lab[:,:,i].std()
content_lab[:,:,i] = (content_lab[:,:,i] - c_mean) * (s_std / (c_std + 1e-8)) + s_mean
transferred = cv2.cvtColor(content_lab, cv2.COLOR_LAB2RGB)
result = cv2.addWeighted(content_np, 1-intensity, transferred, intensity, 0)
result = np.clip(result * 255, 0, 255).astype(np.uint8)
return Image.fromarray(result), f"颜色迁移完成 (强度={intensity})"
def relight_by_reference(content_img, style_img, strength=0.8, preserve_luminance=True):
if content_img is None:
return None, "无内容图像"
if style_img is None:
return content_img, "未提供参考图,返回原图"
content_np = np.array(content_img).astype(np.float32) / 255.0
style_np = np.array(style_img.resize(content_img.size)).astype(np.float32) / 255.0
content_lab = cv2.cvtColor(content_np, cv2.COLOR_RGB2LAB)
style_lab = cv2.cvtColor(style_np, cv2.COLOR_RGB2LAB)
for i in range(1,3):
c_mean = content_lab[:,:,i].mean()
c_std = content_lab[:,:,i].std()
s_mean = style_lab[:,:,i].mean()
s_std = style_lab[:,:,i].std()
content_lab[:,:,i] = (content_lab[:,:,i] - c_mean) * (s_std / (c_std + 1e-8)) + s_mean
if not preserve_luminance:
L_content = content_lab[:,:,0]
L_style = style_lab[:,:,0]
Lc = (L_content - L_content.min()) / (L_content.max() - L_content.min() + 1e-8)
Ls = (L_style - L_style.min()) / (L_style.max() - L_style.min() + 1e-8)
hist_c, bins = np.histogram(Lc.flatten(), 256, [0,1])
hist_s, _ = np.histogram(Ls.flatten(), 256, [0,1])
cdf_c = hist_c.cumsum() / hist_c.sum()
cdf_s = hist_s.cumsum() / hist_s.sum()
mapping = np.interp(cdf_c, cdf_s, np.linspace(0,1,256))
L_matched = np.interp(Lc.flatten(), bins[:-1], mapping).reshape(Lc.shape)
content_lab[:,:,0] = L_matched * (L_content.max() - L_content.min()) + L_content.min()
else:
content_lab[:,:,0] = content_lab[:,:,0]
transferred = cv2.cvtColor(content_lab, cv2.COLOR_LAB2RGB)
result = cv2.addWeighted(content_np, 1-strength, transferred, strength, 0)
result = np.clip(result * 255, 0, 255).astype(np.uint8)
return Image.fromarray(result), f"参考图迁移完成 (强度={strength:.2f})"
def clear_cache():
gc.collect()
return "内存已清理"
def update_prompts_from_style(style_name):
preset = STYLE_PRESETS.get(style_name, STYLE_PRESETS["自定义"])
return preset["positive"], preset["negative"]
# ================== Gradio 界面 ==================
with gr.Blocks(title="AI 光影工坊 - 云端增强版", theme=gr.themes.Soft()) as demo:
gr.Markdown("# 🎨 AI 光影工坊 - 云端重绘增强版")
gr.Markdown("**保留所有原有本地功能,新增「云端AI重绘」模块,支持白天转黑夜、专业灯光效果。**")
with gr.Row():
with gr.Column():
content_img = gr.Image(label="📷 内容图像", type="pil")
style_img = gr.Image(label="🎨 风格参考图(用于参考图迁移)", type="pil")
style_selector = gr.Dropdown(choices=list(STYLE_PRESETS.keys()), value="自定义", label="✨ 风格预设")
prompt_text = gr.Textbox(label="📝 正提示词", lines=2)
negative_text = gr.Textbox(label="🚫 负提示词", lines=2, value=BASE_NEGATIVE)
# 原有本地滑块调整
gr.Markdown("### 🌟 手动滑块调整 (结构不变)")
with gr.Row():
exposure_slider = gr.Slider(label="曝光补偿", minimum=-0.8, maximum=0.8, value=0.0, step=0.05)
temp_slider = gr.Slider(label="色温", minimum=-0.8, maximum=0.8, value=0.0, step=0.05)
with gr.Row():
contrast_slider = gr.Slider(label="对比度", minimum=0.6, maximum=1.8, value=1.0, step=0.05)
intensity_slider = gr.Slider(label="整体强度", minimum=0.0, maximum=1.0, value=0.8, step=0.05)
btn_local = gr.Button("🌟 应用滑块调整", variant="secondary")
# 参考图迁移
gr.Markdown("### 🎨 参考图光影迁移")
ref_strength = gr.Slider(label="迁移强度", minimum=0.0, maximum=1.0, value=0.8, step=0.05)
preserve_lum = gr.Checkbox(label="保留原图亮度结构", value=True)
btn_reference = gr.Button("🎨 参考图光影迁移", variant="secondary")
# 快速滤镜和颜色迁移放在右侧列?
with gr.Column():
output_img = gr.Image(label="结果", type="pil", interactive=False, height=450)
lighting_type = gr.Dropdown(choices=["电影级灯光","柔和漫射光","边缘轮廓光","霓虹光效","黄金时刻光","影棚标准光"], value="电影级灯光", label="💡 快速滤镜")
btn_filter = gr.Button("🎬 应用快速滤镜", variant="secondary")
color_intensity = gr.Slider(label="颜色迁移强度", minimum=0.0, maximum=1.0, value=0.7, step=0.05)
btn_color = gr.Button("🎨 纯颜色迁移", variant="secondary")
btn_clear = gr.Button("🗑️ 清理缓存", variant="secondary")
# 云端AI重绘区域(新增)
gr.Markdown("---\n### 🌐 云端AI重绘(保留结构 + 专业灯光)")
cloud_service = gr.Radio(choices=["SiliconFlow", "通义千问", "Gemini"], label="选择服务", value="SiliconFlow")
with gr.Column(visible=True) as sf_key_col:
sf_key = gr.Textbox(label="SiliconFlow API Key", type="password", placeholder="sk-...")
with gr.Column(visible=False) as tongyi_key_col:
tongyi_key = gr.Textbox(label="通义千问 API Key", type="password", placeholder="sk-...")
with gr.Column(visible=False) as gemini_key_col:
gemini_key = gr.Textbox(label="Gemini API Key", type="password", placeholder="...")
cloud_strength = gr.Slider(label="重绘强度(仅SiliconFlow有效)", minimum=0.1, maximum=0.6, value=0.3, step=0.01,
info="值越低越保留原图结构")
btn_cloud = gr.Button("☁️ 开始云端重绘", variant="primary")
output_info = gr.Textbox(label="状态", interactive=False)
# 动态显示API Key输入框
def update_cloud_visibility(service):
return {
sf_key_col: gr.update(visible=(service == "SiliconFlow")),
tongyi_key_col: gr.update(visible=(service == "通义千问")),
gemini_key_col: gr.update(visible=(service == "Gemini"))
}
cloud_service.change(fn=update_cloud_visibility, inputs=cloud_service, outputs=[sf_key_col, tongyi_key_col, gemini_key_col])
# 事件绑定
style_selector.change(fn=update_prompts_from_style, inputs=style_selector, outputs=[prompt_text, negative_text])
btn_local.click(fn=lighting_transfer_advanced,
inputs=[content_img, exposure_slider, temp_slider, contrast_slider, intensity_slider],
outputs=[output_img, output_info])
btn_reference.click(fn=relight_by_reference,
inputs=[content_img, style_img, ref_strength, preserve_lum],
outputs=[output_img, output_info])
btn_filter.click(fn=apply_lighting_effect, inputs=[content_img, lighting_type], outputs=[output_img, output_info])
btn_color.click(fn=color_transfer, inputs=[content_img, style_img, color_intensity], outputs=[output_img, output_info])
btn_clear.click(fn=clear_cache, inputs=[], outputs=[output_info])
# 云端重绘:根据服务选择对应的API Key
def cloud_relight_router(service, content_img, prompt, negative, sf_key, tongyi_key, gemini_key, strength):
if service == "SiliconFlow":
api_key = sf_key
elif service == "通义千问":
api_key = tongyi_key
elif service == "Gemini":
api_key = gemini_key
else:
return None, "未知服务"
return cloud_relight(service, content_img, prompt, negative, api_key, strength)
btn_cloud.click(fn=cloud_relight_router,
inputs=[cloud_service, content_img, prompt_text, negative_text, sf_key, tongyi_key, gemini_key, cloud_strength],
outputs=[output_img, output_info])
demo.load(fn=lambda: update_prompts_from_style("自定义"), outputs=[prompt_text, negative_text])
if __name__ == "__main__":
demo.launch()