如何调用 Google Nano Banana 做图片抠图

基于一段实际可运行的 Python 源码,介绍如何调用 Google Nano Banana 做商品图抠图,并保留完整源码。

这篇文章用一段实际可运行的 Python 脚本,演示如何调用 Google 的 Nano Banana 图像编辑能力来做商品图抠图。

当前这份实现的目标很明确:

  • 读取一个目录里的商品图片
  • 调用 Google 图像模型执行背景移除
  • 对返回图片再做一次本地透明背景清理
  • 最终输出为透明底 PNG

如果你手里已经有一批白底商品图、耳机图、线材图,想快速生成电商可用的透明背景图,这种方式会很直接。

这段代码做了什么

这份脚本主要分成 4 个部分:

  1. 定义提示词,让模型知道要执行“去背景、保留主体、不要加阴影”
  2. 调用 google-genai 的图像生成接口
  3. 从模型响应中提取图片结果
  4. 再用本地逻辑把边缘浅色背景转成透明,减少残边

也就是说,它不是单纯把图片丢给模型就结束,而是把“模型编辑 + 本地后处理”串起来了。

运行前准备

先安装依赖:

1
.\.venv\Scripts\python.exe -m pip install google-genai pillow

如何获取 GEMINI_API_KEY

GEMINI_API_KEY 就是调用 Gemini API 时使用的密钥。根据 Google 官方 quickstart,如果你还没有 key,可以直接在 Google AI Studio 创建。

获取步骤如下:

  1. 打开 Google AI Studio。
  2. 登录你的 Google 账号。
  3. 找到 Get API keyAPI keys 页面。
  4. 创建一个新的 API key。
  5. 复制生成出来的 key。
  6. 把它配置到本地环境变量里,供脚本读取。

如果页面里还没有可用项目,通常需要先完成项目初始化,然后再回到 API Key 页面创建密钥。

拿到 key 之后,再配置环境变量:

1
$env:GEMINI_API_KEY="your_api_key"

如果你用的是 cmd,可以写成:

1
set GEMINI_API_KEY=your_api_key

如果你同时设置了 GEMINI_API_KEYGOOGLE_API_KEY,实际运行时通常会优先读取 GOOGLE_API_KEY,所以建议只保留一个,避免混淆。

目录结构示例

脚本接收两个参数:

  • input_dir:输入图片目录
  • output_dir:输出图片目录

例如:

1
2
3
4
5
images/
  product1.jpg
  product2.png

output/

如何运行

假设脚本文件名是 cutout.py,运行方式如下:

1
.\.venv\Scripts\python.exe .\cutout.py .\images .\output

如果你想换模型,也可以显式传参:

1
.\.venv\Scripts\python.exe .\cutout.py .\images .\output --model gemini-2.5-flash-image

脚本会遍历输入目录中这些格式的文件:

  • .jpg
  • .jpeg
  • .png
  • .webp

处理完成后,会在输出目录中生成同名的透明底 PNG 文件。

核心调用流程

真正调用 Google Nano Banana 的关键代码在这里:

1
2
3
4
response = client.models.generate_content(
    model=model,
    contents=[PROMPT, image],
)

这里传入了两个内容:

  • 一段文本提示词 PROMPT
  • 一张 PIL.Image

提示词内容是让模型把整张商品图背景移除,只保留主体,并强调几件事:

  • 保留完整商品
  • 保留细线和线缆细节
  • 清理内部空洞和环形区域
  • 不要新增物体
  • 不要添加阴影

这类提示词对抠图质量影响很大,尤其是耳机线、透明边缘、空洞区域这类细节。

为什么还要做一次本地后处理

模型返回结果后,脚本没有直接保存,而是又执行了 make_transparent_from_borders(image)

这一步的思路是:

  • 从图片四周边界开始找浅色背景像素
  • 用广度优先搜索把连通的浅色区域全部标记出来
  • 最后把这些区域统一改成透明

这样做的好处是可以进一步清掉一些残留白边、浅灰背景和不够干净的边缘区域。

判断“是不是背景”的条件在这里:

1
2
3
4
def is_light_background_pixel(r: int, g: int, b: int) -> bool:
    brightness = (r + g + b) / 3
    spread = max(r, g, b) - min(r, g, b)
    return brightness >= 170 and spread <= 35

简单理解就是:

  • 颜色整体足够亮
  • RGB 三通道差异不能太大

这比较适合处理白底、浅灰底、接近纯色的商品图背景。

完整源码

下面保留当前这份完整源码,方便你直接复用或二次修改:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
from __future__ import annotations

import argparse
import os
from pathlib import Path
from collections import deque

from PIL import Image

try:
    from google import genai
except ImportError as exc:  # pragma: no cover
    raise SystemExit(
        "Missing dependency: google-genai. Install it with "
        r"'.\.venv\Scripts\python.exe -m pip install google-genai'."
    ) from exc


PROMPT = (
    "Remove the entire background from this product photo and return only the product "
    "on a fully transparent background as a PNG. Keep the full product intact, preserve "
    "thin cable details, clean the inner loops and holes, and do not add any new objects "
    "or shadows."
)


def is_light_background_pixel(r: int, g: int, b: int) -> bool:
    brightness = (r + g + b) / 3
    spread = max(r, g, b) - min(r, g, b)
    return brightness >= 170 and spread <= 35


def to_pil_image(image_obj) -> Image.Image:
    if isinstance(image_obj, Image.Image):
        return image_obj
    pil_image = getattr(image_obj, "_pil_image", None)
    if isinstance(pil_image, Image.Image):
        return pil_image
    as_pil = getattr(image_obj, "pil_image", None)
    if isinstance(as_pil, Image.Image):
        return as_pil
    raise TypeError(f"Unsupported image object type: {type(image_obj)!r}")


def make_transparent_from_borders(image: Image.Image) -> Image.Image:
    rgba = image.convert("RGBA")
    width, height = rgba.size
    pixels = rgba.load()

    visited: set[tuple[int, int]] = set()
    queue: deque[tuple[int, int]] = deque()

    def push_if_bg(x: int, y: int) -> None:
        if (x, y) in visited:
            return
        r, g, b, _ = pixels[x, y]
        if is_light_background_pixel(r, g, b):
            visited.add((x, y))
            queue.append((x, y))

    for x in range(width):
        push_if_bg(x, 0)
        push_if_bg(x, height - 1)
    for y in range(height):
        push_if_bg(0, y)
        push_if_bg(width - 1, y)

    while queue:
        x, y = queue.popleft()
        for nx, ny in ((x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)):
            if 0 <= nx < width and 0 <= ny < height:
                push_if_bg(nx, ny)

    for x, y in visited:
        pixels[x, y] = (0, 0, 0, 0)

    return rgba


def save_first_image_part(response, dst: Path) -> None:
    parts = getattr(response, "parts", None)
    if parts is None and getattr(response, "candidates", None):
        parts = response.candidates[0].content.parts

    if not parts:
        raise RuntimeError("Model returned no content parts.")

    for part in parts:
        inline_data = getattr(part, "inline_data", None)
        if inline_data is None and isinstance(part, dict):
            inline_data = part.get("inline_data")

        if inline_data is None:
            continue

        if hasattr(part, "as_image"):
            image = to_pil_image(part.as_image())
            dst.parent.mkdir(parents=True, exist_ok=True)
            make_transparent_from_borders(image).save(dst)
            return

        data = getattr(inline_data, "data", None)
        mime_type = getattr(inline_data, "mime_type", "")
        if data:
            dst.parent.mkdir(parents=True, exist_ok=True)
            with open(dst, "wb") as handle:
                handle.write(data)
            with Image.open(dst) as img:
                processed = make_transparent_from_borders(img)
                processed.save(dst.with_suffix(".png"))
            if dst.suffix.lower() != ".png":
                dst.unlink(missing_ok=True)
            return

    raise RuntimeError("Model returned text only and no edited image.")


def process_image(src: Path, dst: Path, client, model: str) -> None:
    with Image.open(src).convert("RGBA") as image:
        response = client.models.generate_content(
            model=model,
            contents=[PROMPT, image],
        )
    save_first_image_part(response, dst)


def main() -> None:
    parser = argparse.ArgumentParser(description="Use Nano Banana / Gemini image editing to cut out product images.")
    parser.add_argument("input_dir", type=Path)
    parser.add_argument("output_dir", type=Path)
    parser.add_argument("--model", default="gemini-2.5-flash-image")
    args = parser.parse_args()

    api_key = os.environ.get("GEMINI_API_KEY")
    if not api_key:
        raise SystemExit("Missing GEMINI_API_KEY environment variable.")

    client = genai.Client(api_key=api_key)
    exts = {".jpg", ".jpeg", ".png", ".webp"}

    for src in sorted(args.input_dir.iterdir()):
        if not src.is_file() or src.suffix.lower() not in exts:
            continue
        dst = args.output_dir / f"{src.stem}.png"
        process_image(src, dst, client, args.model)
        print(dst)


if __name__ == "__main__":
    main()

适合继续优化的地方

如果你准备把这段脚本继续用于批量生产,后面还可以继续补这些能力:

  • 增加失败重试,避免单张图片报错后整批中断
  • 记录日志,方便定位是哪张图处理失败
  • 对不同背景阈值做可配置化
  • 支持递归扫描子目录
  • 增加原图与结果图的对比预览

小结

如果你只想快速理解“怎么调用 Google Nano Banana 做抠图”,其实核心就三步:

  1. 安装 google-genaiPillow
  2. 设置 GEMINI_API_KEY
  3. client.models.generate_content() 传入提示词和图片

而这份代码的价值在于,它不只是调用模型,还补上了透明背景后处理,更适合直接拿去做商品图抠图任务。

记录并分享
使用 Hugo 构建
主题 StackJimmy 设计