Qwen-Image-Edit-2509教程:一键生成电商多角度产品图(下)

2025-11-14 17:52:31
文章摘要
这篇文章教你用一张普通产品图,生成一整套专业级多角度照片。读完之后,你会明白如何用Qwen图像编辑模型和多角度LoRA,轻松做出左转45°、俯拍、广角、特写等效果,全程AI完成,几乎零成本。让你懂得模型加载、角度控制、画幅适配、加速推理这些关键知识,个人卖家也能拥有工作室级的产品图生产能力。

国内部署方案

方案一、魔塔社区本地部署

国内用户访问Hugging Fac可能不稳定,可以使用魔塔社区部署

1.安装依赖

# 安装魔塔SDK和Diffusers
pip install modelscope -U
pip install git+https://github.com/huggingface/diffusers
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install transformers accelerate safetensors pillow opencv-python-headless gradio


2.完整应用代码

import torch
from PIL import Image
from modelscope import QwenImageEditPlusPipeline # 注意:从modelscope导入
import gradio as gr

# ============= GPU配置(保持不变) =============
def get_gpu_config():
    if not torch.cuda.is_available():
        return {
            'device''cpu',
            'dtype': torch.float32,
            'gpu_name''CPU',
            'vram_gb'0,
            'max_batch'1,
            'enable_attention_slicing'True,
            'enable_vae_slicing'True,
        }
    
    gpu_name = torch.cuda.get_device_name(0)
    vram_gb = torch.cuda.get_device_properties(0).total_memory / 1e9
    
    if 'T4' in gpu_name or vram_gb < 20:
        return {
            'device''cuda',
            'dtype': torch.bfloat16,
            'gpu_name': gpu_name,
            'vram_gb': vram_gb,
            'max_batch'1,
            'enable_attention_slicing'True,
            'enable_vae_slicing'True,
        }
    else:
        return {
            'device''cuda',
            'dtype': torch.bfloat16,
            'gpu_name': gpu_name,
            'vram_gb': vram_gb,
            'max_batch'2,
            'enable_attention_slicing'False,
            'enable_vae_slicing'False,
        }

gpu_config = get_gpu_config()
print(f"检测到GPU: {gpu_config['gpu_name']} ({gpu_config['vram_gb']:.1f}GB)")

# ============= 模型配置(魔塔版本) =============
ANGLE_MACROS = {
    "Wide-angle""将镜头转为广角镜头",
    "Close-up""将镜头转为特写镜头",
    "Forward""将镜头向前移动",
    "Left""将镜头向左移动",
    "Right""将镜头向右移动",
    "Down""将镜头向下移动",
    "Rotate 45° Left""将镜头向左旋转45度",
    "Rotate 45° Right""将镜头向右旋转45度",
    "Top-down""将镜头转为俯视",
}

BACKGROUND_PRESETS = {
    "(None)"None,
    "Pure Studio (white seamless)""in a professional studio with seamless white background, soft shadows, product centered",
    "Soft Gray Studio""in a professional studio with seamless soft gray background, gentle vignette, softbox lighting",
    "Lifestyle (cozy desk)""on a cozy wooden desk near a window, soft natural light, minimal props",
    "Lifestyle (marble)""on a clean white marble surface, bright daylight, subtle reflections",
    "Lifestyle (outdoor)""outdoors on a neutral table, soft shade, bokeh background",
}

ASPECT_RATIOS = {
    "1:1 (Square)": (10241024),
    "4:3 (Standard)": (1024768),
    "3:4 (Portrait)": (7681024),
    "16:9 (Widescreen)": (1024576),
    "9:16 (Mobile)": (5761024),
    "3:2 (Photo)": (1024683),
    "2:3 (Portrait Photo)": (6831024),
}

# ============= 加载模型(魔塔社区) =============
print("正在从魔塔社区加载模型...")
pipe = QwenImageEditPlusPipeline.from_pretrained(
    "Qwen/Qwen-Image-Edit-2509",  # 魔塔模型ID
    torch_dtype=gpu_config['dtype']
)
print("✅ 模型加载完成")

pipe.to(gpu_config['device'])

if gpu_config['enable_attention_slicing']:
    pipe.enable_attention_slicing()
if gpu_config['enable_vae_slicing']:
    pipe.enable_vae_slicing()

# ⚠️ 注意:魔塔版本暂不支持LoRA加载,以下代码可以注释掉
# 如果需要角度控制功能,建议使用方案二(硅基流动API)
# pipe.load_lora_weights(LORA_ANGLES, adapter_name="angles")

print("Pipeline准备就绪!")

# ============= 工具函数(保持不变) =============
def compose_prompt(angle_phrase: str,
                   bg_preset_text: Optional[str],
                   custom_scene: str,
                   extra_style: str) -> str:
    parts = [angle_phrase]
    if bg_preset_text:
        parts.append(f"{bg_preset_text}")
    if custom_scene.strip():
        parts.append(custom_scene.strip())
    if extra_style.strip():
        parts.append(extra_style.strip())
    return " | ".join(parts)

def resize_image(img: Image.Image, target_size: Tuple[int, int]) -> Image.Image:
    target_w, target_h = target_size
    orig_w, orig_h = img.size
    scale = max(target_w / orig_w, target_h / orig_h)
    new_w = int(orig_w * scale)
    new_h = int(orig_h * scale)
    img = img.resize((new_w, new_h), Image.Resampling.LANCZOS)
    left = (new_w - target_w) // 2
    top = (new_h - target_h) // 2
    img = img.crop((left, top, left + target_w, top + target_h))
    return img

# ============= 生成函数(魔塔版本简化) =============
def generate_images(
    source_img: Image.Image,
    angle_keys: List[str],
    bg_key: str,
    custom_scene: str,
    extra_style: str,
    aspect_ratio: str,
    seed: int,
    steps: int,
    guidance_scale: float,
    true_cfg_scale: float,
    images_per_prompt: int,
    progress=gr.Progress()
) -> List[Image.Image]:
    
    if source_img is None:
        return [], "⚠️ 请先上传产品图!"
    if not angle_keys:
        return [], "⚠️ 请至少选择一个角度!"
    
    target_size = ASPECT_RATIOS[aspect_ratio]
    source_img = resize_image(source_img, target_size)
    
    results = []
    generator = torch.manual_seed(seed)
    bg_preset_text = BACKGROUND_PRESETS.get(bg_key)
    
    total_angles = len(angle_keys)
    for idx, angle_name in enumerate(angle_keys):
        progress((idx + 1) / total_angles, f"正在生成 {angle_name}...")
        
        angle_phrase = ANGLE_MACROS[angle_name]
        full_prompt = compose_prompt(angle_phrase, bg_preset_text, custom_scene, extra_style)
        
        inputs = {
            "image": [source_img],
            "prompt": full_prompt,
            "generator": generator,
            "true_cfg_scale": true_cfg_scale,
            "negative_prompt"" ",
            "num_inference_steps": steps,
            "guidance_scale": guidance_scale,
            "num_images_per_prompt": images_per_prompt,
            "height": target_size[1],
            "width": target_size[0],
        }
        
        with torch.inference_mode():
            out = pipe(**inputs)
        
        for img_idx, im in enumerate(out.images):
            results.append(im)
        
        if 'T4' in gpu_config['gpu_name']:
            torch.cuda.empty_cache()
    
    return results

def create_zip(images: List[Image.Image]) -> Optional[str]:
    if not images:
        return None
    import zipfile
    import io
    
    zip_path = "/tmp/product_shot_booster.zip"
    with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("manifest.txt""产品图生成器导出\n生成时间: {}\n".format(
            __import__('datetime').datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        ))
        for idx, img in enumerate(images):
            buf = io.BytesIO()
            img.save(buf, format="PNG")
            zf.writestr(f"angle_{idx+1:03d}.png", buf.getvalue())
    return zip_path

# ============= Gradio UI(保持不变,省略重复代码) =============
# ... (使用前文的完整Gradio代码)



方案二、硅基流动API服务(推荐轻量化场景)

如果本地GPU不足,可以使用硅基流动的API服务

1.注册并获取API Key

网址https://siliconflow.cn

API密钥 → 新建密钥


2. 完整API调用代码

import requests
import base64
import json
from PIL import Image
from io import BytesIO
from typing import ListOptional
import gradio as gr

# ============= 配置API密钥 =============
SILICONFLOW_API_KEY = "sk-xxxxxxxxxxxxxx"  # 替换为你的密钥
API_BASE_URL = "https://api.siliconflow.cn/v1"

# ============= 角度和背景配置(同前文) =============
ANGLE_MACROS = {
    "Wide-angle""将镜头转为广角镜头",
    "Close-up""将镜头转为特写镜头",
    "Forward""将镜头向前移动",
    "Left""将镜头向左移动",
    "Right""将镜头向右移动",
    "Down""将镜头向下移动",
    "Rotate 45° Left""将镜头向左旋转45度",
    "Rotate 45° Right""将镜头向右旋转45度",
    "Top-down""将镜头转为俯视",
}

BACKGROUND_PRESETS = {
    "(None)"None,
    "Pure Studio (white seamless)""in a professional studio with seamless white background, soft shadows, product centered",
    "Soft Gray Studio""in a professional studio with seamless soft gray background, gentle vignette, softbox lighting",
    "Lifestyle (cozy desk)""on a cozy wooden desk near a window, soft natural light, minimal props",
    "Lifestyle (marble)""on a clean white marble surface, bright daylight, subtle reflections",
}

ASPECT_RATIOS = {
    "1:1 (Square)": (10241024),
    "4:3 (Standard)": (1024768),
    "3:4 (Portrait)": (7681024),
}

# ============= 工具函数 =============
def compose_prompt(angle_phrase: str,
                   bg_preset_text: Optional[str],
                   custom_scene: str,
                   extra_style: str) -> str:
    parts = [angle_phrase]
    if bg_preset_text:
        parts.append(f"{bg_preset_text}")
    if custom_scene.strip():
        parts.append(custom_scene.strip())
    if extra_style.strip():
        parts.append(extra_style.strip())
    return " | ".join(parts)

def image_to_base64(img: Image.Image) -> str:
    """将PIL图像转为Base64字符串"""
    buffered = BytesIO()
    img.save(buffered, format="PNG")
    return base64.b64encode(buffered.getvalue()).decode()

def base64_to_image(b64_str: str) -> Image.Image:
    """将Base64字符串转为PIL图像"""
    img_data = base64.b64decode(b64_str)
    return Image.open(BytesIO(img_data))

# ============= 核心API调用函数 =============
def call_siliconflow_api(
    image: Image.Image,
    prompt: str,
    width: int = 1024,
    height: int = 1024,
    steps: int = 28,
    guidance_scale: float = 1.0,
    seed: int = 123
) -> Image.Image:
    """
    调用硅基流动API生成图像
    
    Args:
        image: 输入图像(PIL格式)
        prompt: 编辑指令
        width/height: 输出尺寸
        steps: 推理步数
        guidance_scale: CFG引导强度
        seed: 随机种子
    
    Returns:
        生成的PIL图像
    """
    
    # 1. 图像转Base64
    img_base64 = image_to_base64(image)
    
    # 2. 构造请求
    url = f"{API_BASE_URL}/image/edit"
    headers = {
        "Authorization"f"Bearer {SILICONFLOW_API_KEY}",
        "Content-Type""application/json"
    }
    
    payload = {
        "model""Qwen/Qwen-Image-Edit-2509",
        "image": img_base64,
        "prompt": prompt,
        "width": width,
        "height": height,
        "num_inference_steps": steps,
        "guidance_scale": guidance_scale,
        "seed": seed,
        "response_format""b64_json"  # 返回Base64格式
    }
    
    # 3. 发送请求
    try:
        response = requests.post(url, json=payload, headers=headers, timeout=60)
        response.raise_for_status()
        result = response.json()
        
        # 4. 解析返回结果
        if "data" in result and len(result["data"]) > 0:
            b64_img = result["data"][0]["b64_json"]
            return base64_to_image(b64_img)
        else:
            raise Exception(f"API返回异常: {result}")
    
    except requests.exceptions.RequestException as e:
        raise Exception(f"API调用失败: {str(e)}")

# ============= 批量生成函数 =============
def generate_images_api(
    source_img: Image.Image,
    angle_keys: List[str],
    bg_key: str,
    custom_scene: str,
    extra_style: str,
    aspect_ratio: str,
    seed: int,
    steps: int,
    guidance_scale: float,
    progress=gr.Progress()
) -> List[Image.Image]:
    """使用硅基流动API批量生成多角度图像"""
    
    if source_img is None:
        return []
    if not angle_keys:
        return []
    
    # 调整图像尺寸
    target_size = ASPECT_RATIOS[aspect_ratio]
    source_img = source_img.resize(target_size, Image.Resampling.LANCZOS)
    
    results = []
    bg_preset_text = BACKGROUND_PRESETS.get(bg_key)
    
    total_angles = len(angle_keys)
    for idx, angle_name in enumerate(angle_keys):
        progress((idx + 1) / total_angles, f"正在生成 {angle_name}...")
        
        # 组装提示词
        angle_phrase = ANGLE_MACROS[angle_name]
        full_prompt = compose_prompt(angle_phrase, bg_preset_text, custom_scene, extra_style)
        
        # 调用API
        try:
            result_img = call_siliconflow_api(
                image=source_img,
                prompt=full_prompt,
                width=target_size[0],
                height=target_size[1],
                steps=steps,
                guidance_scale=guidance_scale,
                seed=seed
            )
            results.append(result_img)
            print(f"✅ {angle_name} 生成成功")
        
        except Exception as e:
            print(f"❌ {angle_name} 生成失败: {str(e)}")
            # 失败时返回原图(避免中断流程)
            results.append(source_img.copy())
    
    return results

def create_zip(images: List[Image.Image]) -> Optional[str]:
    """打包ZIP文件"""
    if not images:
        return None
    import zipfile
    import io
    
    zip_path = "/tmp/product_shot_api.zip"
    with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
        for idx, img in enumerate(images):
            buf = io.BytesIO()
            img.save(buf, format="PNG")
            zf.writestr(f"angle_{idx+1:03d}.png", buf.getvalue())
    return zip_path

# ============= Gradio UI =============
with gr.Blocks(title="产品图生成器(API版)", theme=gr.themes.Soft()) as demo:
    gr.Markdown("# AI产品图多角度生成器(硅基流动API版)")
    gr.Markdown(" 无需GPU,按需付费,新用户赠送免费额度")
    
    with gr.Row():
        with gr.Column(scale=1):
            input_image = gr.Image(label="上传产品图"type="pil", height=300)
            
            angle_choices = gr.CheckboxGroup(
                choices=list(ANGLE_MACROS.keys()),
                value=["Wide-angle""Close-up""Top-down"],
                label="📐 相机角度(可多选)"
            )
            
            aspect_ratio = gr.Dropdown(
                choices=list(ASPECT_RATIOS.keys()),
                value="1:1 (Square)",
                label="画幅比例"
            )
            
            bg_preset = gr.Dropdown(
                choices=list(BACKGROUND_PRESETS.keys()),
                value="(None)",
                label="背景预设"
            )
            
            custom_scene = gr.Textbox(
                label="自定义场景(可选)",
                placeholder="例如: 放在深色木质桌面上",
                lines=2
            )
            
            extra_style = gr.Textbox(
                label="风格补充",
                value="studio-grade lighting, high clarity",
                lines=2
            )
        
        with gr.Column(scale=2):
            output_gallery = gr.Gallery(
                label="生成结果",
                columns=3,
                height="auto"
            )
            zip_output = gr.File(label="打包下载")
    
    with gr.Accordion("⚙️ 高级设置"open=False):
        with gr.Row():
            seed = gr.Number(label="随机种子", value=123, precision=0)
            steps = gr.Slider(label="推理步数", minimum=10, maximum=50, value=28, step=1)
            guidance_scale = gr.Slider(label="CFG强度", minimum=0.0, maximum=5.0, value=1.0, step=0.1)
    
    generate_btn = gr.Button("开始生成", variant="primary", size="lg")
    
    def generate_and_zip(*args):
        images = generate_images_api(*args)
        zip_file = create_zip(images) if images else None
        return images, zip_file
    
    generate_btn.click(
        fn=generate_and_zip,
        inputs=[
            input_image, angle_choices, bg_preset, custom_scene,
            extra_style, aspect_ratio, seed, steps, guidance_scale
        ],
        outputs=[output_gallery, zip_output]
    )

demo.launch(share=True, debug=True)



写在最后

AI图像生成技术正在重塑电商视觉生产流程,从雇摄影师拍5000元到AI生成成本5元。

Qwen-Image-Edit + 多角度LoRA的方案,代表了当前开源+可控+低成本的实践。

希望本文能帮助你快速上手,并在实际业务中落地应用。


声明:该内容由作者自行发布,观点内容仅供参考,不代表平台立场;如有侵权,请联系平台删除。