Spaces:
Sleeping
Sleeping
| #!/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() |