图生图:Python 和 C++ 两种实现方式
一、完整代码
1.1 Python 版本
python
"""
图生图 (Image-to-Image) Demo
使用火山引擎 Ark API / 豆包 Seedream 4.5 模型
"""
import os
import argparse
import base64
import urllib.request
from openai import OpenAI
MODEL = "doubao-seedream-4-5-251128"
client = OpenAI(
base_url="https://ark.cn-beijing.volces.com/api/v3",
api_key="6e69285a-1461-459e-954e-a492db0d8d3b",
)
def img2img(image_path: str, prompt: str, size: str = "2K", watermark: bool = True):
"""本地图片 Base64 编码后传入图生图接口"""
with open(image_path, "rb") as f:
image_b64 = base64.b64encode(f.read()).decode("utf-8")
ext = os.path.splitext(image_path)[1].lower()
mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp"}
mime = mime_map.get(ext, "image/png")
response = client.images.generate(
model=MODEL,
prompt=prompt,
size=size,
response_format="url",
extra_body={
"image": f"data:{mime};base64,{image_b64}",
"watermark": watermark,
},
)
return response.data[0].url
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="图生图 Demo - 豆包 Seedream 4.5")
parser.add_argument("-i", "--input", required=True, help="输入图片路径")
parser.add_argument("-o", "--output", default="output.png", help="输出图片路径 (默认: output.png)")
parser.add_argument("-p", "--prompt", required=True, help="提示词")
parser.add_argument("-s", "--size", default="2K", help="输出尺寸 (默认: 2K)")
parser.add_argument("--no-watermark", action="store_true", help="去除水印")
args = parser.parse_args()
url = img2img(
image_path=args.input,
prompt=args.prompt,
size=args.size,
watermark=not args.no_watermark,
)
print(f"下载中: {url}")
urllib.request.urlretrieve(url, args.output)
print(f"已保存到: {args.output}")
用法:
bash
python img2img_demo.py -i ./test.png -p "转化为油画风格"
python img2img_demo.py -i ./test.png -p "赛博朋克" -o cyber.png --no-watermark
1.2 C++ 版本
cpp
/**
* 图生图 (Image-to-Image) Demo - C++ 版本
* 使用火山引擎 Ark API / 豆包 Seedream 4.5 模型
*
* 编译: cmake -B build && cmake --build build
* 使用: ./img2img -i input.png -p "提示词" -o output.png
*/
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <cstring>
#include <curl/curl.h>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
const std::string MODEL = "doubao-seedream-4-5-251128";
const std::string BASE_URL = "https://ark.cn-beijing.volces.com/api/v3";
const std::string API_KEY = "6e69285a-1461-459e-954e-a492db0d8d3b";
// ============================================================
// Base64 编码
// ============================================================
const std::string BASE64_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string base64_encode(const unsigned char* data, size_t len) {
std::string result;
result.reserve(4 * ((len + 2) / 3));
for (size_t i = 0; i < len; i += 3) {
unsigned int n = (unsigned int)data[i] << 16;
if (i + 1 < len) n |= (unsigned int)data[i + 1] << 8;
if (i + 2 < len) n |= (unsigned int)data[i + 2];
result.push_back(BASE64_CHARS[(n >> 18) & 0x3F]);
result.push_back(BASE64_CHARS[(n >> 12) & 0x3F]);
result.push_back((i + 1 < len) ? BASE64_CHARS[(n >> 6) & 0x3F] : '=');
result.push_back((i + 2 < len) ? BASE64_CHARS[n & 0x3F] : '=');
}
return result;
}
// ============================================================
// 读取文件为字节数组
// ============================================================
std::vector<unsigned char> read_file(const std::string& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
std::cerr << "无法打开文件: " << path << std::endl;
exit(1);
}
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
// ============================================================
// 根据扩展名推断 MIME 类型
// ============================================================
std::string get_mime(const std::string& path) {
auto dot = path.find_last_of('.');
if (dot == std::string::npos) return "image/png";
std::string ext = path.substr(dot);
if (ext == ".jpg" || ext == ".jpeg") return "image/jpeg";
if (ext == ".webp") return "image/webp";
return "image/png";
}
// ============================================================
// libcurl 回调:将响应写入 string
// ============================================================
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
}
// ============================================================
// 调用图生图 API,返回结果图片 URL
// ============================================================
std::string img2img(const std::string& image_path,
const std::string& prompt,
const std::string& size = "2K",
bool watermark = true) {
// 1. 读取图片并 Base64 编码
auto raw = read_file(image_path);
std::string b64 = base64_encode(raw.data(), raw.size());
std::string mime = get_mime(image_path);
std::string data_uri = "data:" + mime + ";base64," + b64;
// 2. 构造 JSON 请求体
json body;
body["model"] = MODEL;
body["prompt"] = prompt;
body["size"] = size;
body["response_format"] = "url";
body["image"] = data_uri;
body["watermark"] = watermark;
std::string body_str = body.dump();
// 3. 发送 HTTP POST
CURL* curl = curl_easy_init();
if (!curl) {
std::cerr << "curl 初始化失败" << std::endl;
exit(1);
}
std::string response_data;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, ("Authorization: Bearer " + API_KEY).c_str());
curl_easy_setopt(curl, CURLOPT_URL, (BASE_URL + "/images/generations").c_str());
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_str.size());
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
std::cerr << "请求失败: " << curl_easy_strerror(res) << std::endl;
exit(1);
}
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
if (http_code != 200) {
std::cerr << "HTTP " << http_code << ": " << response_data << std::endl;
exit(1);
}
// 4. 解析返回的图片 URL
auto resp = json::parse(response_data);
return resp["data"][0]["url"];
}
// ============================================================
// 下载图片到本地
// ============================================================
void download(const std::string& url, const std::string& output_path) {
CURL* curl = curl_easy_init();
if (!curl) {
std::cerr << "curl 初始化失败" << std::endl;
exit(1);
}
std::ofstream out(output_path, std::ios::binary);
if (!out) {
std::cerr << "无法创建文件: " << output_path << std::endl;
exit(1);
}
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L);
auto file_callback = [](void* ptr, size_t size, size_t nmemb, void* stream) -> size_t {
auto* ofs = (std::ofstream*)stream;
ofs->write((char*)ptr, size * nmemb);
return size * nmemb;
};
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, +file_callback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out);
CURLcode res = curl_easy_perform(curl);
curl_easy_cleanup(curl);
if (res != CURLE_OK) {
std::cerr << "下载失败: " << curl_easy_strerror(res) << std::endl;
exit(1);
}
}
// ============================================================
// CLI
// ============================================================
struct Args {
std::string input;
std::string output = "output.png";
std::string prompt;
std::string size = "2K";
bool no_watermark = false;
};
Args parse_args(int argc, char* argv[]) {
Args args;
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if ((arg == "-i" || arg == "--input") && i + 1 < argc) {
args.input = argv[++i];
} else if ((arg == "-o" || arg == "--output") && i + 1 < argc) {
args.output = argv[++i];
} else if ((arg == "-p" || arg == "--prompt") && i + 1 < argc) {
args.prompt = argv[++i];
} else if ((arg == "-s" || arg == "--size") && i + 1 < argc) {
args.size = argv[++i];
} else if (arg == "--no-watermark") {
args.no_watermark = true;
}
}
return args;
}
int main(int argc, char* argv[]) {
if (argc < 2) {
std::cout << "图生图 Demo - 豆包 Seedream 4.5\n"
<< "用法: ./img2img -i <输入图> -p <提示词> [-o <输出>] [-s <尺寸>] [--no-watermark]\n"
<< "示例: ./img2img -i test.png -p \"转化为油画风格\"\n"
<< " ./img2img -i test.png -p \"赛博朋克\" -o cyber.png --no-watermark\n";
return 1;
}
auto args = parse_args(argc, argv);
if (args.input.empty()) {
std::cerr << "错误: 缺少 -i/--input" << std::endl;
return 1;
}
if (args.prompt.empty()) {
std::cerr << "错误: 缺少 -p/--prompt" << std::endl;
return 1;
}
std::string url = img2img(args.input, args.prompt, args.size, !args.no_watermark);
std::cout << "下载中: " << url << std::endl;
download(url, args.output);
std::cout << "已保存到: " << args.output << std::endl;
return 0;
}
编译和用法:
bash
# 安装依赖
sudo apt install libcurl4-openssl-dev nlohmann-json3-dev cmake g++
# 编译
cmake -B build && cmake --build build
# 运行
./build/img2img -i ./test.png -p "转化为油画风格"
./build/img2img -i ./test.png -p "赛博朋克" -o cyber.png --no-watermark
二、什么是图生图 / 这个代码在做什么
2.1 图生图(Image-to-Image)
图生图就是给 AI 一张参考图 + 一段文字描述,让 AI 基于参考图生成新图片。
- 文生图:只给文字 → AI 凭空画
- 图生图:给图片 + 文字 → AI 照着图片改、换风格、换背景
2.2 整体流程
无论 Python 还是 C++,走的都是同一条链路:
┌──────────┐ ┌──────────┐ ┌───────────────┐ ┌──────────┐ ┌──────────┐
│ 本地图片 │ ──→ │ Base64 │ ──→ │ JSON body │ ──→ │ HTTP POST│ ──→ │ 火山引擎 │
│ test.png │ │ 编码 │ │ {image, prompt}│ │ │ │ Ark API │
└──────────┘ └──────────┘ └───────────────┘ └──────────┘ └────┬─────┘
│
返回结果图片 URL
│
┌─────────────┘
│
┌─────▼──────┐
│ 下载到本地 │
│ output.png │
└────────────┘
为什么要 Base64 编码: JSON 只能传文本,图片是二进制。Base64 把图片的每一个字节翻译成可打印字符,这样图片就能"塞进"JSON 字符串里,和 API Key、prompt 一起打包发给服务器。
三、Python 方式的实现
Python 方式的核心优势:有现成的 OpenAI SDK。
3.1 SDK 做了什么
python
client = OpenAI(
base_url="https://ark.cn-beijing.volces.com/api/v3",
api_key="...",
)
这行创建了一个 HTTP 客户端对象。关键是 base_url 指向火山引擎,而不是 OpenAI 官网------火山引擎的 API 协议兼容 OpenAI 格式,所以能用同一个 SDK。
之后调用 client.images.generate(model=..., prompt=..., extra_body={...}) 时,SDK 在背后做了这些事:
- 把参数和
extra_body合并,转成 JSON 字符串 - 拼完整 URL:
https://ark.cn-beijing.volces.com/api/v3/images/generations - 加上
Authorization: Bearer xxx请求头 - 加上
Content-Type: application/json - 发 HTTP POST 请求
- 解析返回的 JSON 响应
- 检查状态码,非 200 则抛异常
- 返回封装好的 Python 对象
你只写了一行代码,SDK 代劳了七步。
3.2 依赖的标准库
Python 版除了 openai SDK,其余的 base64、argparse、urllib 全是标准库自带,不需要额外安装:
| 库 | 用途 |
|---|---|
base64 |
图片二进制 → Base64 文本 |
argparse |
解析 -i / -p / -o 命令行参数 |
urllib.request |
下载生成的图片到本地 |
四、C++ 方式的实现
C++ 没有 OpenAI SDK,也没有内置 Base64,也没有内置 JSON 库,也没有内置 HTTP 库。所有东西要么手写,要么用第三方库。
4.1 依赖
| Python | C++ | |
|---|---|---|
| HTTP 通信 | openai SDK(内部用 httpx/urllib) |
libcurl |
| JSON | 内置 dict |
nlohmann/json(第三方头文件库) |
| Base64 | 标准库 import base64 |
手写 ~15 行(或用 OpenSSL) |
| 文件读写 | open() + f.read() |
std::ifstream + istreambuf_iterator |
| CLI 参数 | argparse(标准库) |
手写 argv 循环 |
4.2 关键差异:HTTP 请求
这是两个版本最大的不同点。Python 的 client.images.generate() 一行,在 C++ 里对应的是整套 libcurl 操作流程(下面有完整章节讲)。
4.3 关键差异:Base64
Python 有 base64.b64encode() 标准库,C++ 没有。要么自己写 ~15 行(如本项目),要么引入 OpenSSL、Boost 等第三方库。
五、C++ 中的 curl 深度解析
5.1 curl 是什么
curl 是一个发送网络请求的工具/库。它有两个形态:
| 形态 | 叫什么 | 怎么用 | 本项目用到 |
|---|---|---|---|
| 命令行工具 | curl |
终端里敲 curl -X POST ... |
没有 |
| C 语言库 | libcurl |
代码里 #include <curl/curl.h> |
全部用它 |
它们是同一个项目(同名、同作者、同代码基础),只是调用层面不同。libcurl 就是命令行 curl 的 C 语言内核 ,你在终端敲的每个 curl 命令,底层都是通过 libcurl 的 API 执行的。
5.2 命令行 curl vs 代码中的 libcurl
如果把 C++ 代码做的事翻译成你熟悉的命令行 curl,就是:
bash
curl -X POST \
"https://ark.cn-beijing.volces.com/api/v3/images/generations" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer 6e69285a-..." \
-d '{"model":"doubao-seedream-4-5-251128","prompt":"转化为油画风格","image":"data:image/png;base64,iVBORw0KGgo...","size":"2K","response_format":"url","watermark":true}'
代码里的每一行 curl_easy_setopt,在命令行 curl 里都有对应的参数:
命令行的 -X POST → curl_easy_setopt(curl, CURLOPT_POST, 1L)
命令行的 -H "..." → curl_slist_append(headers, "...")
命令行的 -d '...' → curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body_str.c_str())
命令行的 https://... → curl_easy_setopt(curl, CURLOPT_URL, "https://...")
区别只是:命令行 curl 帮你完成了 init / perform / cleanup,而代码里你得手动做。
5.3 libcurl 的四步固定流程
每次用 libcurl 发请求,必须走这四个步骤:
curl_easy_init() ← 创建会话对象
│
▼
curl_easy_setopt() × N ← 配置各种参数
│
▼
curl_easy_perform() ← 真正执行,阻塞等待
│
▼
curl_easy_cleanup() ← 销毁会话,释放资源
步骤 1 --- curl_easy_init():
在堆上创建一个 curl 会话对象(句柄)。"easy" 不是"简单"的意思 ,是相对于 curl_multi_(多路并发接口)而言的"单次同步请求模式"。
步骤 2 --- curl_easy_setopt():
这是配置的核心。curl 用一个统一的函数来设置所有参数,通过第二个参数(option)来区分"你要设什么东西":
cpp
curl_easy_setopt(curl, CURLOPT_URL, "..."); // 设目标 URL
curl_easy_setopt(curl, CURLOPT_POST, 1L); // 设 HTTP 方法为 POST
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); // 设请求头
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "..."); // 设 body 内容
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 123); // 设 body 长度
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback);// 设接收回调
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); // 设回调参数
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 120L); // 设超时
每个 CURLOPT_* 控制一个维度。整个配置阶段就是不断调 setopt,把你关心的事情都设好。
步骤 3 --- curl_easy_perform():
真正的执行。调用后阻塞当前线程 ,直到请求完成、超时、或出错。返回值是 CURLcode 枚举,表示网络层面的结果(不是 HTTP 状态码)。
步骤 4 --- curl_easy_cleanup():
销毁会话,释放 libcurl 内部持有的所有资源(连接池、缓存、cookie 等)。
5.4 回调机制:libcurl 最核心的设计
这是使用 libcurl 最容易困惑的地方。大多数 HTTP 库都是"调一个函数,返回完整响应",libcurl 不是。
libcurl 的做法是:服务器返回数据是分块到达的,每收到一块,libcurl 就调一次你注册的回调函数,把这一小块数据塞给你。
cpp
// 注册回调
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); // "用这个函数收数据"
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response_data); // "这个指针传给回调"
// 回调的实现
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
// contents → 刚收到的一小块数据
// size * nmemb → 这一小块有多大
// userp → 就是上面设置的 &response_data
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
}
为什么不直接返回完整结果: 网络数据不是一次到齐的。TCP 是流式协议,服务器可能先发 1460 字节,再发 1460 字节,再发 800 字节。回调让你每一块到达时立刻可以处理,不用等全部收完。对于大文件下载(比如图片),可以一块一块直接写磁盘,不占满内存。
WRITEDATA 和 WRITEFUNCTION 的关系:
curl 内部每收到一块数据:
write_callback(
contents, ← 数据块地址
size, ← 每元素大小 (通常是 1)
nmemb, ← 元素个数
&response_data ← 你通过 CURLOPT_WRITEDATA 传入的指针
)
WRITEFUNCTION 决定"谁来处理",WRITEDATA 决定"处理时把什么传进去"。
5.5 两次 curl 调用的回调对比
本项目中共用了两次 libcurl------一次发图生图请求,一次下载结果图片。它们的回调设计不同:
| img2img(发 POST) | download(GET 下载) | |
|---|---|---|
| 目的 | 收 JSON 响应文本 | 收二进制图片数据 |
| 写到哪里 | std::string(内存) |
std::ofstream(直接写磁盘) |
| 为什么 | JSON 很小,需要解析后取 URL | 图片可能很大,不占内存 |
| 实现方式 | static 普通函数 |
C++ lambda 表达式 |
cpp
// POST 时:数据拼进 string,后续 json::parse() 解析
static size_t write_callback(void* contents, size_t size, size_t nmemb, void* userp) {
((std::string*)userp)->append((char*)contents, size * nmemb);
return size * nmemb;
}
// 下载时:数据直接写磁盘
auto file_callback = [](void* ptr, size_t size, size_t nmemb, void* stream) -> size_t {
auto* ofs = (std::ofstream*)stream;
ofs->write((char*)ptr, size * nmemb);
return size * nmemb;
};
5.6 错误处理的两个层次
cpp
// 第一层:网络错误
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
// "网线拔了"、"DNS 挂了"、"SSL 过期"、"超时"
// 服务器根本没收到请求
}
// 第二层:HTTP 错误
long http_code = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code);
if (http_code != 200) {
// 服务器收到了请求,但拒绝了:
// 400 → 参数错误
// 401 → API Key 无效
// 500 → 服务器内部错误
}
必须先检查第一层再检查第二层------网络通才有 HTTP 状态码可言。
5.7 资源释放顺序
cpp
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); // 1. 先读数据
curl_slist_free_all(headers); // 2. 释放请求头链表
curl_easy_cleanup(curl); // 3. 销毁会话
顺序不能乱:
getinfo必须在cleanup之前(会话销毁了就读不到了)free_all可以在getinfo后任意时机(headers 不属于会话内部状态)
六、两个版本的关键对应关系
| 环节 | Python | C++ |
|---|---|---|
| 读文件 | open(path, "rb").read() |
std::ifstream + istreambuf_iterator |
| Base64 | base64.b64encode()(标准库) |
base64_encode()(手写 ~15 行) |
| 拼 Data URI | f"data:{mime};base64,{b64}" |
"data:" + mime + ";base64," + b64 |
| 构造 JSON | dict 字面量 |
nlohmann::json,语法几乎一样 |
| 序列化 JSON | SDK 内部 json.dumps() |
body.dump() |
| HTTP 请求 | client.images.generate() 一行 |
curl_easy_* 约 20 行 |
| 设置鉴权 | api_key="..." 参数 |
手动拼 Authorization 请求头 |
| 解析响应 | .data[0].url |
json::parse()["data"][0]["url"] |
| 下载图片 | urllib.request.urlretrieve() |
curl_easy_* GET + ofstream |
| CLI 参数 | argparse(标准库) |
手写 argv for 循环 |
七、总结
Python 版本适合快速验证、demo。 60 行代码,SDK 代劳了大量底层操作,你可以专注于业务逻辑(拼 prompt、调参数)。
C++ 版本适合嵌入更大的 C++ 项目。 代码量大约是 Python 的 4 倍,但你能完全控制每一步------用哪个 HTTP 库、JSON 怎么解析、内存如何分配。curl 虽然用起来啰嗦,但它是一个稳定了 25 年的 C 库,几乎每台 Linux 机器上都有,没有额外的运行时依赖。
两个版本发出的 HTTP 请求是完全一样的,服务器无法区分请求是 Python 发的还是 C++ 发的------它们等价。