Skip to main content

Qwen-Image-Edit-2509 Tutorial: Generate Studio-Ready Product Angles

Transform one photo into multiple product shots with Qwen-Image-Edit-2509 and LoRA. Generate consistent studio angles for Amazon, Etsy, and online stores in seconds.
Nov 12, 2025  · 9 min read

Solo sellers and small brands rarely have the budget for a full product photoshoot. In this guide, we’ll build and run a Gradio app that transforms a single input image into e-commerce-ready, multi-angle product renders (wide, macro, ±45°, top-down) using Qwen-Image-Edit-2509 with LoRA-based virtual camera controls for reproducible AI product photography.

Under the hood, we use:

  • Base model: Qwen/Qwen-Image-Edit-2509 (Apache-2.0)
  • Angle control LoRA: dx8152/Qwen-Edit-2509-Multiple-angles
  • Optional speed LoRA: lightx2v/Qwen-Image-Lightning

In this tutorial, you’ll learn how to:

  • Load the Qwen edit pipeline and stack multiple LoRAs.
  • Drive camera moves with robust angle macros.
  • Build a clean Gradio UI for creators and ecommerce teams.
  • Export all results as a ZIP for easy handoff.

If you’re keen to learn more about how image data is processed in deep learning, check out the Image Processing in Python skill track

Why Use Qwen Image Edit with Multiple Angles LoRA?

Qwen-Image-Edit-2509 is the latest iteration of Qwen’s image-editing model with enhanced consistency and multi-image editing. The Multiple-Angles LoRA injects reliable “virtual camera” controls so you can say:

  • 将镜头转为广角镜头 (turn to wide-angle)
  • 将镜头转为特写镜头 (turn to close-up)
  • 将镜头向左旋转45度 (rotate 45° left)
  • 将镜头转为俯视 (top-down view)

These commands consistently reframe the same product without hallucinating to a new product.

Qwen Image Edit Step-By-Step Tutorial: Building a Product Shot Booster 

In this guide, we’ll build an app that transforms a single product image into a studio-ready angle set (including angles like wide, close-up, 45° left/right, and top-down), with optional lifestyle presets and fast preview mode. It uses Qwen/Qwen-Image-Edit-2509 model and a Multiple-Angles LoRA wrapped in a Gradio UI.

We’ll go from environment setup to model loading, generation helpers, and a web UI.

Step 1: Prerequisites

First, install all runtime dependencies. We use PyTorch with CUDA wheels, Hugging Face tooling, Diffusers library, Gradio for the UI, and Pillow/OpenCV for image handling.

pip install -q torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
pip install -q transformers>=4.44.0 accelerate>=0.33.0 safetensors>=0.4.3
pip install -q pillow>=10.2.0 huggingface_hub>=0.24.0 opencv-python-headless>=4.9.0.80
pip install -q numpy>=1.26.0 gradio>=4.0.0
pip install -q git+https://github.com/huggingface/diffusers

Here is what each prerequisite does:

  • CUDA Wheels: The PyTorch line points to NVIDIA CUDA 12.1 wheels for GPU acceleration.
  • Transformers and Diffusers: Transformers manages configs and tokenizers while Diffusers provides the QwenImageEditPlusPipeline.
  • Safetensors: They provide a secure tensor format (faster and safer than Pickle).
  • OpenCV-headless and Pillow: It provides robust image I/O and resizing for pre/post-processing.
  • Gradio: It is a rapid web UI that works locally and in Colab.

With the environment ready, we can authenticate to Hugging Face (optional) and load models.

Step 2: Hugging Face authentication

Some models or adapters may require authentication from HuggingFace. This step logs you in once for the session.

from huggingface_hub import login
import os
HF_TOKEN = "Your_HF_Token"  # @param {type:"string"}
if HF_TOKEN:
    login(token=HF_TOKEN)
    print("Logged in to Hugging Face")
else:
    print("WARNING: No token provided. Continuing without authentication")

Start by logging in to your HuggingFace account and looking for Access Tokens within your account settings. Click on Create new token and generate a new token with Read token type. Select the required permissions and generate a new token.

HF Access code

In production/Colab, do not hardcode tokens, instead use environment variables or Colab “Secrets.”

Step 3: Load model and set up pipeline

In this section, we set up the Qwen-Image-Edit pipeline, load the angles LoRA, and add a Fast toggle with GPU-aware settings. Note that this code runs efficiently on a high-RAM A100 GPU.

Step 3.1:  Imports and GPU config

Now, we’ll import core libraries and detect the GPU to set precision, attention slicing, and VAE slicing automatically for stability.

import io
import zipfile
from typing import List, Tuple, Optional
import gc
import torch
from PIL import Image
from diffusers import QwenImageEditPlusPipeline
import gradio as gr
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()
HF_BASE_MODEL = "Qwen/Qwen-Image-Edit-2509"
LORA_ANGLES = "dx8152/Qwen-Edit-2509-Multiple-angles"
LORA_LIGHTNING = "lightx2v/Qwen-Image-Lightning"
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)": (1024, 1024),
    "4:3 (Standard)": (1024, 768),
    "3:4 (Portrait)": (768, 1024),
    "16:9 (Widescreen)": (1024, 576),
    "9:16 (Mobile)": (576, 1024),
    "3:2 (Photo)": (1024, 683),
    "2:3 (Portrait Photo)": (683, 1024),
}

Here are the key components of the above code blocks:

  • GPU auto-config (get_gpu_config function): We detect the available accelerator and return a small config dictionary that is used to choose device (cpu/cuda), dtype (like bfloat16 on GPU), and memory behaviors:
    • On CPU, we default to float32 and enable attention/VAE slicing for safety.
    • On T4 / <20 GB VRAM, we keep bfloat16 but enable slicing to prevent OOMs.
    • On 20 GB+ GPUs (like A100), we disable slicing and allow a larger batch for speed.
  • Model IDs and adapters: We load Qwen-Image-Edit-2509 as the core editor,along with Multiple-angles LoRA for camera control, and optionally blend the Lightning LoRA when you toggle to Fast Mode.  
    • HF_BASE_MODEL = "Qwen/Qwen-Image-Edit-2509" is the main image-editing backbone.
    • LORA_ANGLES = "dx8152/Qwen-Edit-2509-Multiple-angles" adds “virtual camera” controls.
    • LORA_LIGHTNING = "lightx2v/Qwen-Image-Lightning" is an optional speed adapter for fast previews.

Note: for HQ Mode, only the angles adapter stays active.

  • Angle macros: ANGLE_MACROS maps labels like  “Wide-angle” or “Top-down” to equivalent Chinese commands that the angles LoRA understands reliably. These commands consistently moves the virtual camera (move/rotate/zoom) without changing identity.
  • Background presets: BACKGROUND_PRESETS are short English fragments like “marble surface”, “soft gray studio”, that we merge into the final prompt. They are orthogonal to the angle macros and is responsible for controling scene.
  • Aspect ratios (ASPECT_RATIOS): These allow us to generate canvas (1:1, 4:3, 16:9, etc.) and resize or center-crop the input prior to inference so outputs don’t need a post generation cropping.

We now have GPU andadapters set up. Let’s instantiate the Diffusers pipeline and attach the LoRAs next.

Step 3.2: Pipeline

Next, we load the base Qwen Edit pipeline, enable memory optimizations according to the GPU, and stack the Multiple-Angles LoRA. We also implement a mode switch to optionally add the Lightning LoRA for fast previews.

pipe = QwenImageEditPlusPipeline.from_pretrained(
    HF_BASE_MODEL,
    torch_dtype=gpu_config['dtype'],
)
pipe = 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()
pipe.load_lora_weights(LORA_ANGLES, adapter_name="angles")
current_mode = {"fast": False}
def set_pipeline_mode(use_lightning: bool):
    global current_mode  
    if use_lightning and not current_mode["fast"]:
        pipe.load_lora_weights(LORA_LIGHTNING, adapter_name="lightning")
        pipe.set_adapters(["angles", "lightning"], adapter_weights=[1.0, 1.0])
        current_mode["fast"] = True
    elif not use_lightning and current_mode["fast"]:
        pipe.set_adapters(["angles"], adapter_weights=[1.0])
        current_mode["fast"] = False
    elif not use_lightning and not current_mode["fast"]:
        pipe.set_adapters(["angles"], adapter_weights=[1.0])
set_pipeline_mode(False)

The above code is the pipeline setup layer for the Product Shot Booster app, which performs the following functions:

  • Pipeline initialization: It instantiates the QwenImageEditPlusPipeline from the base model (Qwen/Qwen-Image-Edit-2509), moves it to the detected device (CPU/GPU), and sets the tensor precision (torch_dtype) based on your hardware.
  • Memory safety: On smaller GPUs, it enables attention and VAE slicing to reduce peak VRAM and avoid out-of-memory errors while trading a small amount of speed for stability.
  • LoRA composition: It loads the Multiple-angles LoRA to provide repeatable “virtual camera” controls (move/rotate/zoom) and optionally blends the Lightning LoRA for faster previews.
  • Mode switching logic: The set_pipeline_mode() helper function and a current_mode flag manage HQ vs Fast states, preventing redundant Lightning reloads and letting you flip adapters.

Now the pipeline is ready. Let’s move on to wiring generation functions and the Gradio UI.

Step 4: Generation functions

Generation functions power generation by building prompts, resizing to the target aspect ratio, running the pipeline across selected angles, and packaging results for download in a zip file.

Step 4.1: Prompt Composer and  Image Resize Helpers

Before we invoke the model, we normalize inputs with two stateless helpers:

  • compose_prompt() function: This function assembles a single instruction string from camera angle, background preset, custom scene, and style notes. Concise clauses helps to keep Qwen’s instruction parsing stable and avoids over-weighting any one part.
  • resize_image() function: It fits the source image to the target aspect ratio using a scale-to-cover strategy, Lanczos resampling helps with high-quality down/up-scaling, and a center crop so key subject matter stays in frame.
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

The above two functions perform the following tasks:

  • Composable prompt: We join parts with " | " to keep clauses visually distinct, while Qwen-Edit handles punctuation and yields consistent parsing without biasing toward the last clause. We also use .strip() with user text to avoid empty fragments.
  • Scale-to-cover math: This guarantees that the resized image fully covers the target canvas in both dimensions.
  • Lanczos resampling: It is a high-quality filter that preserves fine details when downsampling and reduces aliasing when upscaling.
  • Center crop: After resizing, we crop the exact target canvas from the center to preserve typical composition and avoid unexpected off-center trims.

With helpers ready, we can now run a multi-angle generation loop and package results.

Step 4.2: Generate images 

Next, we validate inputs, conform the image to the chosen aspect, loop over selected angles,  and run the pipeline in inference_mode. Finally, we return a zip file with all generated images.

def generate_images(
    source_img: Image.Image,
    angle_keys: List[str],
    bg_key: str,
    custom_scene: str,
    extra_style: str,
    aspect_ratio: str,
    use_lightning: bool,
    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 [], "WARNING: Please upload an image first!"    
    if not angle_keys:
        return [], "WARNING: Please select at least one angle!"  
    target_size = ASPECT_RATIOS[aspect_ratio]
    source_img = resize_image(source_img, target_size)  
    set_pipeline_mode(use_lightning)
    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"Generating {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
    zip_path = "/content/product_shot_booster.zip"
    with zipfile.ZipFile(zip_path, mode="w", compression=zipfile.ZIP_DEFLATED) as zf:
        zf.writestr("manifest.txt", "Product Shot Booster Export\nGenerated angles exported as PNG files.\n")
        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

The generate_images() function implements multi-angle generation and export by:

  • Validate, normalize, and configure: It checks for an uploaded image and selected angles, then fits the image to the chosen aspect (resize and center crop), and toggles HQ vs Fast mode via set_pipeline_mode(use_lightning)
  • Efficient inference: For each angle, it builds a single instruction string including angle macro, background preset, scene and style. It then prepares the Diffusers inputs and executes under torch.inference_mode() to save memory. On low-VRAM GPUs (like a T4), calls like torch.cuda.empty_cache() per angle help to reduce fragmentation.
  • Aggregate results with UI feedback: It finally updates Gradio progress as each angle completes and collects all outputs into one list for downstream zipping.

The create_zip(images) function provides a portable export which writes a zip file with a brief manifest.txtalong with encoding images via in-memory (BytesIO) for lossless files and returns None if no images are present.

With generation and packaging finalized, the UI can trigger a multi-angle pass and offer a one-click download of the complete angle pack.

Step 5: Gradio UI

This UI allows users to upload a product image, pick angles and aspect ratios, choose a background preset, set style notes, and generate multiple-angle images on the go. 

with gr.Blocks(title="Product Shot Booster", theme=gr.themes.Soft()) as demo:
    gr.Markdown("<h1 style='text-align: center;font-weight: bold;'>Product Shot Booster</h1>")
    with gr.Row():
        with gr.Column(scale=1):
            input_image = gr.Image(
                label="Product Image",
                type="pil",
                height=300
            ) 
            angle_choices = gr.CheckboxGroup(
                choices=list(ANGLE_MACROS.keys()),
                value=["Wide-angle", "Close-up", "Rotate 45° Left", "Rotate 45° Right", "Top-down"],
                label="Camera Angles"
            ) 
            aspect_ratio = gr.Dropdown(
                choices=list(ASPECT_RATIOS.keys()),
                value="1:1 (Square)",
                label="Aspect Ratio"
            )
            bg_preset = gr.Dropdown(
                choices=list(BACKGROUND_PRESETS.keys()),
                value="(None)",
                label="Background"
            ) 
            custom_scene = gr.Textbox(
                label="Custom Scene",
                placeholder="e.g., 'on a matte black table with soft reflections'",
                lines=2
            )    
            extra_style = gr.Textbox(
                label="Style Notes",
                value="studio-grade lighting, high clarity, ecommerce-ready composition",
                lines=2
            )
        with gr.Column(scale=2):
            output_gallery = gr.Gallery(
                  label="Generated Images",
                  show_label=True,
                  columns=3,
                  height="auto",
                  object_fit="contain"
              )

            info_output = gr.Markdown("")
            zip_output = gr.File(label="Download ZIP")
    with gr.Accordion("Advanced Settings", open=False):
        with gr.Row():
            use_lightning = gr.Checkbox(
                label="Fast Mode (Lightning LoRA)",
                value=('T4' in gpu_config['gpu_name']),
                info="Enable for faster generation (recommended for T4)"
            )   
            seed = gr.Number(
                label="Seed",
                value=123,
                precision=0
            )  
            steps = gr.Slider(
                label="Inference Steps",
                minimum=10,
                maximum=60,
                value=28,
                step=1
            )
        with gr.Row():
            guidance_scale = gr.Slider(
                label="Guidance Scale",
                minimum=0.0,
                maximum=8.0,
                value=1.0,
                step=0.1
            )
            true_cfg_scale = gr.Slider(
                label="True CFG Scale",
                minimum=0.0,
                maximum=10.0,
                value=4.0,
                step=0.1
            ) 
            images_per_prompt = gr.Slider(
                label="Images per Angle",
                minimum=1,
                maximum=4,
                value=1,
                step=1
            )
    generate_btn = gr.Button("Generate", variant="primary", size="lg")
    def generate_and_zip(*args):
      images = generate_images(*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,
            use_lightning,
            seed,
            steps,
            guidance_scale,
            true_cfg_scale,
            images_per_prompt
        ],
        outputs=[output_gallery, zip_output]
    )
demo.launch(share=True, debug=True, show_error=True)

Demo

The above code builds an interactive Gradio app that wires a single generate action to the inference and exposes a shareable web UI, by using:

  • Inputs (left column): The left pane collects everything the model needs using a gr.Image to upload product photo, gr.CheckboxGroup for camera angles, two gr.Dropdowns for aspect ratio and background presets and two gr.Textbox fields for custom scene and style notes to fine-tune the look.
  • Outputs (right column): A gr.Gallery method shows all generated angles in a grid so you can compare compositions at a glance, and a gr.File component offers ZIP of PNGs for quick download.
  • Advanced settings : A gr.Checkbox toggles Fast Mode (Lightning LoRA) for quick previews on T4-class GPUs, while Seed and Steps adjust reproducibility and the quality/time trade-off. Guidance and True-CFG steer edit strength versus identity stability, and Images per Angle lets you produce multiple variants per camera macro in one go.
  • Interface wiring (action): A single Generate button connects all inputs to generate_images() function(which runs Qwen Edit with the selected adapters) and then to create_zip() function(which packages outputs), returning both the gallery and the ZIP back to the interface.
  • App launch: Finally, demo.launch() method spins up the web app and provides a public share URL—perfect for Colab sessions, quick demos, or sending stakeholders a link to try the tool themselves.

Finally, hit Generate and watch a full-angle pack render. 

Here are the results from my experiments with this model. First, you can see how the products are displayed based on the different parameters set.

In the next video, you can see how I can interact with the interface, the various settings, and the results generated from our original image. 

Final Thoughts

You now have a Product Shot Booster Gradio application that turns a single photo into multi-angle renders using Qwen-Image-Edit-2509 and LoRA “virtual camera” controls. It’s GPU-aware (A100 optimal, but smaller GPUs use slicing), reproducible, and supports a Fast preview to HQ export workflow.

Here’s  what you can try next:

  • Ship it: Add auth and quotas, and include one-click preset exports for Amazon/Etsy/PDP image specs.
  • Scale it: Expose a /generate API and support batch jobs (CSV/JSON of SKUs) so teams can render angle packs at scale.

If you’re keen to get more hands-on with creating AI-powered applications, I recommend the Developing AI Applications skill track

=

Topics
Related

Tutorial

Qwen (Alibaba Cloud) Tutorial: Introduction and Fine-Tuning

Qwen is a family of large language and multimodal models developed by Alibaba Cloud, designed for various tasks like text generation, image understanding, and conversation.
Dr Ana Rojo-Echeburúa's photo

Dr Ana Rojo-Echeburúa

Tutorial

Qwen's Wan 2.1: A Guide With Practical Examples

Learn how to use Wan 2.1, Alibaba's open-source video generation model, to create videos from text and images.
Dr Ana Rojo-Echeburúa's photo

Dr Ana Rojo-Echeburúa

Tutorial

Qwen-Agent: A Guide With Demo Project

Learn how to use Qwen-Agent and Qwen3 to build a real-time webpage summarizer extension.
Aashi Dutt's photo

Aashi Dutt

Tutorial

Qwen 2.5 Coder: A Guide With Examples

Learn about the Qwen2.5-Coder series by building an AI code review assistant using Qwen 2.5-Coder-32B-Instruct and Gradio.
Aashi Dutt's photo

Aashi Dutt

Tutorial

How to Use Qwen2.5-VL Locally

Learn about the new flagship vision-language model and run it on a laptop with 8GB VRAM.
Abid Ali Awan's photo

Abid Ali Awan

Tutorial

Fine-Tuning Qwen3: A Step-by-Step Guide

A step-by-step guide for fine-tuning the Qwen3-32B model on the medical reasoning dataset within an hour.
Abid Ali Awan's photo

Abid Ali Awan

See MoreSee More