Part 1: From GANs to Generative AI — Why and How the Migration Happened

Introduction

This is the sequel to the META FIT series, which documented the construction of a virtual try-on system using GANs. That series ended with Part 5, acknowledging fundamental limitations in body diversity, processing speed, and garment fidelity.

This new series picks up where that story left off. Over the course of three articles, I will document the migration from a GAN-based pipeline to generative AI — the technical decisions, the implementation, and what the results actually look like.

Part 1 covers the why: how generative AI changed the virtual try-on landscape, the motivation to test it against our own system, and the architectural redesign that replaced an entire GPU pipeline with API calls.

The complete source code is available at github.com/matu79go/metafit.


Generative AI Changed Virtual Try-On

The primary motivation for this migration was straightforward: generative AI had begun to transform virtual try-on from a research problem into a production-ready capability.

In 2024, Google launched AI-powered virtual try-on in Google Shopping. When a shopper selects a garment, the system generates realistic images of diverse body types wearing that item — in real time, powered by generative AI. The very functionality I had spent years building with GANs was already in production behind an API.

That raised an obvious question: could the same technology replace the entire PASTA-GAN++ pipeline I had built? The body diversity problem, the processing speed problem, the infrastructure problem — could generative AI solve all of them at once?

This technical investigation is what started the project.


The License Problem: A Secondary Push

Beyond the technical motivation, a license audit added urgency to the migration.

A careful review of the PASTA-GAN++ codebase revealed that five core components carried non-commercial licenses:

ComponentLicenseFunction in Pipeline
PASTA-GAN++Non-commercial researchCore try-on generation
StyleGAN2 (NVIDIA)NVIDIA Source Code License-NCGenerator backbone (torch_utils/, dnnlib/)
OpenPose (CMU)Academic non-commercialPose detection (18 keypoints)
PF-AFNNon-commercial researchWarping module
FlowNet2 (Freiburg)Research-onlyOptical flow estimation

Some components had commercial licensing options — OpenPose, for example, was available through CMU FlintBox for approximately $25,000/year — but the cost and complexity of licensing every component made this path impractical.

More critically, the license restriction applies to the entire pipeline, not just the individual component. Even using OpenPose as a preprocessing step for a commercially licensed model would constitute commercial use of OpenPose itself:

Pattern A (clean):
  Photo → Gemini API → Try-on result
  → Only Gemini's license applies

Pattern B (violation):
  Photo → OpenPose (keypoints) → Gemini API → Try-on result
  → OpenPose's non-commercial license is violated

The remaining commercially licensed components — Graphonomy (MIT), OpenCV (Apache 2.0), PyTorch (BSD) — were useful but could not function without the non-commercial components that formed the core pipeline.

The technical motivation to explore generative AI, combined with these license constraints, made the decision clear: rather than incremental component replacements, the entire approach needed to change.


Two Paths Forward: Gemini and Vertex AI

Research into Google’s generative AI offerings revealed two distinct approaches to virtual try-on:

Gemini Image Generation (Nano Banana)

The Gemini API’s image generation capability — internally known as Nano Banana — is a general-purpose image editing model. It accepts images and text prompts as input, and generates modified images as output.

For virtual try-on, this means sending a person image and a clothing image along with a prompt describing the desired transformation. The model interprets the instruction and generates the result.

from google import genai

client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

response = client.models.generate_content(
    model="gemini-3-pro-image-preview",
    contents=[prompt, person_image, clothing_image],
    config=types.GenerateContentConfig(
        response_modalities=["IMAGE", "TEXT"],
    ),
)

Strengths: Extremely flexible. The prompt can specify exactly what to do — extract clothing from one person and apply it to another, preserve specific body features, maintain background. No pre-trained try-on-specific model needed.

Limitations: Results depend heavily on prompt engineering. The model may interpret clothing differently than intended, especially for distinctive designs like dresses.

Vertex AI Virtual Try-On

Google also offers a dedicated virtual try-on model (virtual-try-on-001) through Vertex AI. Unlike Gemini’s general-purpose approach, this model is purpose-built for fitting product images onto person photos.

# REST API call to Vertex AI
payload = {
    "instances": [{
        "personImage": {
            "image": {"bytesBase64Encoded": person_b64}
        },
        "productImages": [{
            "image": {"bytesBase64Encoded": clothing_b64}
        }]
    }],
    "parameters": {"sampleCount": 1}
}

Strengths: Superior fidelity for product images. Color accuracy, garment structure, and proportions are well-preserved. Designed specifically for EC applications.

Limitations: Cannot perform person-to-person transfer. Expects flat product images (white background, no model). Requires GCP project setup with service account authentication.

The Hybrid Strategy

The two engines have complementary strengths:

ScenarioBest EngineWhy
EC site: show product on customerVertex AI VTODesigned for product-to-person; highest fidelity
Social: “I want to wear what she’s wearing”Gemini (Nano Banana)Only option that can extract clothing from person images
Cross-gender / body type adaptationGemini (Nano Banana)Prompt-based control over body shape preservation

Rather than choosing one, the optimal architecture uses both — selecting the engine based on the input type and use case.


Implementation: From Pipeline to API Call

The Old Architecture

The PASTA-GAN++ pipeline, as documented in Part 3 and Part 4 of the original series, required multiple sequential stages:

Input Image
  → OpenPose: Extract 18 body keypoints → JSON
  → Graphonomy: Generate 20-class body segmentation → PNG
  → PASTA-GAN++:
      → style_encoding(clothing, retain_mask) → style vectors
      → const_encoding(pose_tensor) → pose features
      → mapping(z, style) → w vectors
      → synthesis(w, pose, clothing_features) → output image

Each stage had its own model, its own preprocessing requirements, and its own failure modes. The inference code in test.py shows the complexity:

# Load the StyleGAN2-based generator
with dnnlib.util.open_url(config["network"]) as f:
    G = legacy.load_network_pkl(f)["G_ema"].to(device)

# Process each sample through the full pipeline
for data in dataloader:
    image, clothes, pose = data[0], data[1], data[2]
    norm_img, norm_img_lower = data[4], data[5]
    retain_mask, skin_average = data[10], data[11]

    # Style encoding from clothing appearance
    gen_c, cat_feat_list = G.style_encoding(
        torch.cat([norm_img, norm_img_lower], dim=1),
        retain_mask
    )

    # Pose encoding from skeleton
    gen_z = torch.randn(1, G.z_dim, device=device)
    ws = G.mapping(gen_z, gen_c)
    pose_feat = G.const_encoding(pose)

    # Final synthesis
    gen_imgs = G.synthesis(ws, pose_feat, cat_feat_list, ...)

This required: Docker with NVIDIA CUDA, ~4GB GPU memory, pre-computed keypoints and segmentation masks, and images normalized to exactly 320x512 pixels.

The New Architecture

The Gemini-based implementation in try_on_test.py reduces the entire pipeline to a single API call:

def run_tryon(person_path, clothing_path, prompt, mode, use_preprocess):
    load_dotenv()
    client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))

    # Load images as API-compatible parts
    person_part = load_image_as_part(person_path)
    clothing_part = load_image_as_part(clothing_path)

    contents = [prompt, person_part, clothing_part]

    response = client.models.generate_content(
        model="gemini-3-pro-image-preview",
        contents=contents,
        config=types.GenerateContentConfig(
            response_modalities=["IMAGE", "TEXT"],
        ),
    )

    # Extract and save generated image
    for part in response.candidates[0].content.parts:
        if part.inline_data:
            image_bytes = part.inline_data.data
            # Save to file...

No GPU. No Docker. No preprocessing pipeline. The model handles pose understanding, body segmentation, garment extraction, and image composition internally.

Two Modes, One Interface

The script supports two modes through different prompts:

Clothing mode — applying a product image to a person:

PROMPT_CLOTHING = """You are a virtual fitting AI model.
Given the person image and the clothing product image,
generate a new image of THE SAME PERSON wearing THE GIVEN CLOTHING.

The person's face MUST remain EXACTLY identical to the input.
Do NOT regenerate or modify the face in any way.
Preserve the exact same: body shape, pose, background, hair, accessories.
Only change the clothing to match the provided product image."""

Transfer mode — extracting clothing from one person and applying to another:

PROMPT_TRANSFER = """You are a virtual fitting AI model.
The first image is the TARGET person.
The second image is the SOURCE person wearing the clothes to transfer.
Extract only the clothing design, color, pattern, and style from
the SOURCE person, and generate a new image of the TARGET person
wearing those exact clothes.

The TARGET person's face MUST remain EXACTLY identical.
Do NOT regenerate or modify the face in any way.
Maintain the TARGET person's body shape and pose exactly."""

The transfer mode is particularly significant — it replicates the functionality of PASTA-GAN++ (which required OpenPose + Graphonomy + a trained GAN) using nothing but a text prompt and two images.


The MediaPipe Experiment: Unnecessary Complexity

An early hypothesis was that providing explicit body information — keypoints and face landmarks — would improve results. The implementation added optional MediaPipe preprocessing:

def preprocess_images(person_path, clothing_path, mode):
    # MediaPipe Face Landmark detection
    face_result = face_detector.detect(mp_image)
    if face_result.face_landmarks:
        landmarks = face_result.face_landmarks[0]
        # Extract face bounding box from normalized coordinates
        face_left = min(lm.x for lm in landmarks)
        face_top = min(lm.y for lm in landmarks)
        # ... crop face for additional reference

    # MediaPipe Pose Landmark detection
    pose_result = pose_detector.detect(mp_image)
    if pose_result.pose_landmarks:
        # Extract 13 body proportions
        shoulder_width = abs(landmarks[11].x - landmarks[12].x)
        hip_width = abs(landmarks[23].x - landmarks[24].x)
        # ... calculate body measurements

The function generated a supplementary prompt describing the person’s body proportions, and a cropped face image as an additional reference.

Testing revealed this was unnecessary. With high-resolution input images (1000px+), Gemini alone produced results equal to or better than the preprocessed version. The MediaPipe step added latency and complexity without meaningful quality improvement.

This was a key finding: the generative AI model already understands human anatomy well enough that explicit pose/body information adds no value. The entire field of pose estimation and body parsing — the subjects of Part 4 in the original series — became optional auxiliary information rather than required pipeline stages.


Face Restoration: A Solved Problem

One concern with any image generation approach is face preservation. Early tests showed occasional face quality degradation, particularly with low-resolution inputs. The implementation includes a face restoration postprocessor:

def postprocess_face_restore(original_path, generated_path):
    # Detect face in both images
    orig_face = detect_face_region(original_image)
    gen_face = detect_face_region(generated_image)

    # LAB color space correction
    orig_lab = cv2.cvtColor(orig_crop, cv2.COLOR_BGR2LAB)
    gen_lab = cv2.cvtColor(gen_crop, cv2.COLOR_BGR2LAB)
    for ch in range(3):
        gen_ch = gen_lab[:,:,ch].astype(float)
        gen_ch = (gen_ch - gen_ch.mean()) / (gen_ch.std() + 1e-6)
        gen_ch = gen_ch * orig_lab[:,:,ch].std() + orig_lab[:,:,ch].mean()
        result_lab[:,:,ch] = np.clip(gen_ch, 0, 255)

    # Elliptical feather mask for seamless blending
    feather_size = max(face_h, face_w) // 3
    mask = create_elliptical_mask(face_h, face_w)
    mask = cv2.GaussianBlur(mask, (feather_size*2+1, feather_size*2+1), 0)

    # Blend original face onto generated image
    result = original_face * mask + generated_face * (1 - mask)

However, this too proved unnecessary for high-resolution inputs. The prompt-based approach (“The person’s face MUST remain EXACTLY identical”) was sufficient when the input image had adequate resolution.

The pattern was consistent: resolution is the single most important quality factor. Low-resolution images (320px, the size PASTA-GAN++ operated at) require helper processing. High-resolution images (1000px+) need nothing but the API call.


Vertex AI VTO: The Dedicated Alternative

For product-to-person try-on, Vertex AI offers a specialized model. The implementation in compare_vto.py and test_vertex_vto.py uses the REST API with service account authentication:

def get_vertex_token():
    credentials = service_account.Credentials.from_service_account_file(
        os.getenv("GOOGLE_APPLICATION_CREDENTIALS"),
        scopes=["https://www.googleapis.com/auth/cloud-platform"]
    )
    credentials.refresh(google.auth.transport.requests.Request())
    return credentials.token

def run_vertex_vto(person_path, clothing_path):
    token = get_vertex_token()
    endpoint = (
        f"https://{LOCATION}-aiplatform.googleapis.com/v1/"
        f"projects/{PROJECT_ID}/locations/{LOCATION}/"
        f"publishers/google/models/virtual-try-on-001:predict"
    )

    payload = {
        "instances": [{
            "personImage": {"image": {"bytesBase64Encoded": person_b64}},
            "productImages": [{"image": {"bytesBase64Encoded": clothing_b64}}]
        }],
        "parameters": {"sampleCount": 1}
    }

    response = requests.post(
        endpoint,
        headers={"Authorization": f"Bearer {token}"},
        json=payload,
        timeout=120
    )

A practical note: setting up GCP authentication required working around version incompatibilities. The gcloud auth application-default login approach failed due to scope errors in an older gcloud CLI version, and gcloud components update stalled. The solution was to create a dedicated service account (metafit-vto) with the Vertex AI User role and use its JSON key file directly.


What Changed: A Before/After Summary

AspectPASTA-GAN++ (Before)Gemini + Vertex AI (After)
InfrastructureDocker + NVIDIA GPUAPI key
Processing pipeline3-stage (OpenPose → Graphonomy → GAN)Single API call
Image resolutionFixed 320×512Any resolution
Body diversityDegraded on underrepresented typesHandles all body types
Commercial license5 non-commercial componentsFully commercial
Processing costGPU computeAPI pricing (~$0.02-0.04/image)
Code complexity~500 lines of pipeline orchestration~100 lines of API interaction
Transfer modeRequired trained GAN modelPrompt-based, no training

What Comes Next

Part 1 has covered the why and the how of the migration. Part 2 documents the systematic testing of Nano Banana across 16 test cases — from initial experiments with noisy images through high-resolution action poses — and the discovery that resolution, not preprocessing, is the key to quality.


META FIT GenAI Series:

Share this article

Related Posts