文章目录
- 【116.Python+AI】用Python调用GPT-4V分析图片:一句话让它描述、分类、检测你的图像
-
- 导入语
- [1 ~> Hello World:30行看图说话](#1 ~> Hello World:30行看图说话)
-
- [1.1 调用链路](#1.1 调用链路)
- [2 ~> 传图两式:URL vs Base64](#2 ~> 传图两式:URL vs Base64)
- [3 ~> 多图同传:对比类任务的打开方式](#3 ~> 多图同传:对比类任务的打开方式)
- [4 ~> 结构化输出:让视觉结果进流水线](#4 ~> 结构化输出:让视觉结果进流水线)
-
- [4.1 Schema 设计的两个经验](#4.1 Schema 设计的两个经验)
- [4.2 别忘了校验](#4.2 别忘了校验)
- [5 ~> 生产三件套:重试、缓存、批量](#5 ~> 生产三件套:重试、缓存、批量)
- [思考 && 总结](#思考 && 总结)
- 结尾
【116.Python+AI】用Python调用GPT-4V分析图片:一句话让它描述、分类、检测你的图像
📖 文章简介: 本文系统讲解用Python调用多模态模型分析图片的完整流程,是多模态开发的入门实战。文章从最小的"看图说话"Demo切入------一张本地图片、一句提问、30行代码拿到描述;随后逐层补齐生产用法:图片传入的两种方式(公网URL直传与Base64编码内嵌,体积限制与各自的适用场景)、多图同传(一次请求分析多张图片,对比类任务的实现姿势)、指定分析维度(用Prompt引导模型按"主体/场景/文字/异常"的框架输出,避免自由发挥)、JSON结构化输出(配合response_format让视觉结果直接进入程序流水线,字段定义+校验的完整代码)、以及三个生产级细节(图片预处理降成本、错误重试、结果缓存)。每个能力点附可直接运行的Python代码,配以Mermaid流程图展示从图片到结构化数据的完整链路。读完你将能把"图片分析"像调用普通函数一样嵌入任何Python项目,适合刚接触多模态API的开发者阅读参考。

🎬 个人主页: 源码骑士
❄ 专栏传送门: 《Android开发基础》《python基础课程》
⭐️热衷从源码视角拆解技术底层原理,将复杂架构讲得通俗易懂
🎬 源码骑士的简介:
5年Android Framework系统开发经验,曾主导多项系统级性能优化专项
技术栈覆盖Android系统全链路(Binder/Handler/AMS/WMS/启动流程)及Java后端全家桶(Spring + MyBatis + Redis + Oracle)
累计产出原创技术文章100+篇,文章以流程图为特色,被读者评价为"看一篇胜过啃一周源码"
导入语
上一篇建立了多模态的能力认知,这篇直接动手。目标很简单:让你的Python程序"看见"图片------扔给它一张照片,返回一段描述、一个分类、或者一份结构化的分析报告。
这个能力一旦入库,很多原本需要训练专用模型的需求瞬间平民化:商品图自动打标、用户上传内容初审、工单截图自动分类------以前要攒数据、训模型、搭服务,现在只是一次API调用。
我们从30行的Hello World开始,逐步补齐URL/Base64两种传图方式、多图对比、结构化输出这些生产必备技能。全程基于OpenAI兼容接口------同一套代码,换base_url就能切到Qwen-VL等国产模型(第40篇的统一调用思路在这里继续生效)。
1 ~> Hello World:30行看图说话
python
import base64
from openai import OpenAI
client = OpenAI() # 读环境变量 OPENAI_API_KEY
def encode_image(path: str) -> str:
"""本地图片 → Base64字符串"""
with open(path, "rb") as f:
return base64.b64encode(f.read()).decode()
def describe_image(path: str, question: str = "请详细描述这张图片") -> str:
b64 = encode_image(path)
resp = client.chat.completions.create(
model="gpt-4o-mini", # 带视觉能力的模型
messages=[{
"role": "user",
"content": [
{"type": "text", "text": question},
{"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
],
}],
)
return resp.choices[0].message.content
print(describe_image("product.jpg"))
注意请求体的结构:content不再是字符串,而是一个数组------文本块和图片块按顺序拼在一起。这个"多块拼装"的结构就是多模态请求的全部秘密。
1.1 调用链路
#mermaid-svg-mnYNKdwndPHdr7mp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mnYNKdwndPHdr7mp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mnYNKdwndPHdr7mp .error-icon{fill:#552222;}#mermaid-svg-mnYNKdwndPHdr7mp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mnYNKdwndPHdr7mp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mnYNKdwndPHdr7mp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mnYNKdwndPHdr7mp .marker.cross{stroke:#333333;}#mermaid-svg-mnYNKdwndPHdr7mp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mnYNKdwndPHdr7mp p{margin:0;}#mermaid-svg-mnYNKdwndPHdr7mp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mnYNKdwndPHdr7mp .cluster-label text{fill:#333;}#mermaid-svg-mnYNKdwndPHdr7mp .cluster-label span{color:#333;}#mermaid-svg-mnYNKdwndPHdr7mp .cluster-label span p{background-color:transparent;}#mermaid-svg-mnYNKdwndPHdr7mp .label text,#mermaid-svg-mnYNKdwndPHdr7mp span{fill:#333;color:#333;}#mermaid-svg-mnYNKdwndPHdr7mp .node rect,#mermaid-svg-mnYNKdwndPHdr7mp .node circle,#mermaid-svg-mnYNKdwndPHdr7mp .node ellipse,#mermaid-svg-mnYNKdwndPHdr7mp .node polygon,#mermaid-svg-mnYNKdwndPHdr7mp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mnYNKdwndPHdr7mp .rough-node .label text,#mermaid-svg-mnYNKdwndPHdr7mp .node .label text,#mermaid-svg-mnYNKdwndPHdr7mp .image-shape .label,#mermaid-svg-mnYNKdwndPHdr7mp .icon-shape .label{text-anchor:middle;}#mermaid-svg-mnYNKdwndPHdr7mp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mnYNKdwndPHdr7mp .rough-node .label,#mermaid-svg-mnYNKdwndPHdr7mp .node .label,#mermaid-svg-mnYNKdwndPHdr7mp .image-shape .label,#mermaid-svg-mnYNKdwndPHdr7mp .icon-shape .label{text-align:center;}#mermaid-svg-mnYNKdwndPHdr7mp .node.clickable{cursor:pointer;}#mermaid-svg-mnYNKdwndPHdr7mp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mnYNKdwndPHdr7mp .arrowheadPath{fill:#333333;}#mermaid-svg-mnYNKdwndPHdr7mp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mnYNKdwndPHdr7mp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mnYNKdwndPHdr7mp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mnYNKdwndPHdr7mp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mnYNKdwndPHdr7mp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mnYNKdwndPHdr7mp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mnYNKdwndPHdr7mp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mnYNKdwndPHdr7mp .cluster text{fill:#333;}#mermaid-svg-mnYNKdwndPHdr7mp .cluster span{color:#333;}#mermaid-svg-mnYNKdwndPHdr7mp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mnYNKdwndPHdr7mp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mnYNKdwndPHdr7mp rect.text{fill:none;stroke-width:0;}#mermaid-svg-mnYNKdwndPHdr7mp .icon-shape,#mermaid-svg-mnYNKdwndPHdr7mp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mnYNKdwndPHdr7mp .icon-shape p,#mermaid-svg-mnYNKdwndPHdr7mp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mnYNKdwndPHdr7mp .icon-shape .label rect,#mermaid-svg-mnYNKdwndPHdr7mp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mnYNKdwndPHdr7mp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mnYNKdwndPHdr7mp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mnYNKdwndPHdr7mp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 自由描述
JSON Mode
本地图片
Base64编码
或公网URL
组装content数组
文本块+图片块
多模态模型
GPT-4o/Qwen-VL
输出形式
文本回答
结构化字段
进程序流水线
2 ~> 传图两式:URL vs Base64
| 方式 | 写法 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| 公网URL | {"url": "https://..."} |
请求体小 | 图片须公网可访问 | 图床/OSS上的图 |
| Base64内嵌 | data:image/jpeg;base64,... |
图片不出门也能传 | 请求体膨胀约33% | 本地/内网图片 |
Base64的两个实用细节:
bash
细节一:体积限制
单图建议压到 2MB 以内再编码
原图10MB直接编码 → 请求体13MB+,又慢又可能超限
细节二:先缩放再编码,省钱大招
视觉模型按图片token计费,分辨率越高越贵
2048px的长边足够大多数分析场景
→ PIL缩一下再传,费用立降50%+
python
from PIL import Image
import io
def encode_image_smart(path: str, max_side: int = 2048) -> str:
"""缩放后再编码:成本直降,分析质量几乎无损"""
img = Image.open(path)
img.thumbnail((max_side, max_side))
buf = io.BytesIO()
img.convert("RGB").save(buf, format="JPEG", quality=85)
return base64.b64encode(buf.getvalue()).decode()
这一条是本文最立竿见影的成本优化:视觉调用的费用和图片分辨率直接挂钩,而2048px对"看图说话、分类、字段提取"这些任务绰绰有余。原图直传是新手最常见的浪费。
3 ~> 多图同传:对比类任务的打开方式
content数组里可以放多个图片块------对比、找不同、序列理解类任务靠它:
python
def compare_images(paths: list[str], question: str):
content = [{"type": "text", "text": question}]
for p in paths:
content.append({
"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{encode_image_smart(p)}"},
})
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": content}],
)
return resp.choices[0].message.content
# 用法示例
compare_images(
["design_v1.png", "design_v2.png"],
"对比两版设计稿,列出所有视觉差异,按区域逐条说明",
)
典型场景:设计稿版本对比、设备前后照片巡检、多帧截图还原操作序列。注意图片数量与费用成正比------多图任务记得先用上一节的缩放控制成本。
4 ~> 结构化输出:让视觉结果进流水线
自由文本对人是友好的,对程序是灾难。生产场景必须JSON化(第46篇JSON Mode的组合技):
python
import json
PRODUCT_SCHEMA = {
"type": "object",
"properties": {
"category": {"type": "string",
"description": "商品类目:服装/数码/食品/家居/其他"},
"brand_visible": {"type": "boolean", "description": "是否可见品牌logo"},
"text_in_image": {"type": "array", "items": {"type": "string"},
"description": "图中出现的文字"},
"quality_issues": {"type": "array", "items": {"type": "string"},
"description": "画质问题:模糊/过曝/水印/截断"},
},
"required": ["category", "brand_visible", "text_in_image", "quality_issues"],
}
def analyze_product(path: str) -> dict:
b64 = encode_image_smart(path)
resp = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": [
{"type": "text",
"text": "分析这张商品图,按schema输出结构化结果"},
{"type": "image_url",
"image_url": {"url": f"data:image/jpeg;base64,{b64}"}},
],
}],
response_format={
"type": "json_schema",
"json_schema": {"name": "product_analysis", "schema": PRODUCT_SCHEMA},
},
)
return json.loads(resp.choices[0].message.content)
4.1 Schema 设计的两个经验
bash
经验一:枚举值写死在description里
"服装/数码/食品/家居/其他" ------ 给了封闭选项,
模型就不会发明"美妆个护"这种你系统里没有的类目
经验二:每个字段都是一次"显式提问"
没有quality_issues字段,模型看到模糊图也不会主动提
→ Schema即提问清单:你想要什么信息,就定义什么字段
4.2 别忘了校验
第115篇强调过"输出当初稿用"------结构化之后加一层业务校验,流水线才算闭环:
python
VALID_CATEGORIES = {"服装", "数码", "食品", "家居", "其他"}
def analyze_product_safe(path: str) -> dict:
result = analyze_product(path)
if result["category"] not in VALID_CATEGORIES:
result["category"] = "其他" # 兜底,防脏数据入库
result["_need_review"] = True # 打标转人工
return result
5 ~> 生产三件套:重试、缓存、批量
python
from tenacity import retry, stop_after_attempt, wait_exponential
@retry(stop=stop_after_attempt(3), wait=wait_exponential(min=2, max=10))
def analyze_with_retry(path):
return analyze_product_safe(path)
# 结果缓存:同一张图不分析两次(第101篇的缓存思想)
import hashlib
def analyze_cached(path):
key = hashlib.md5(open(path, "rb").read()).hexdigest()
if cached := redis_get(f"vision:{key}"):
return json.loads(cached)
result = analyze_with_retry(path)
redis_set(f"vision:{key}", json.dumps(result), ex=86400)
return result
批量任务(比如给一万张商品图打标)请走第44篇的asyncio并发方案------视觉API一样有RPM限制,指数退避重试是必修课。
思考 && 总结
- 多模态请求的秘密是content数组: 文本块+图片块按序拼装,换base_url即可切换到国产视觉模型。
- 先缩放再Base64: 长边2048px对分析任务绰绰有余,费用立降50%+------原图直传是新手最大的浪费。
- 多图同传解锁对比任务: 版本对比、巡检找茬、序列理解;图片数量与费用成正比,缩放先行。
- Schema即提问清单: 想要什么信息就定义什么字段;枚举值写死在description里防模型自由发挥;出口处加业务校验。
- 生产三件套别省略: 重试抗抖动、缓存防重复、批量走异步------视觉API的RPM限制和文本API一样严格。
会分析单张图片了,下一篇上一个更值钱的场景:文档智能解析------让AI看懂你的PDF合同、发票、报告,把关键字段抠出来直接入库。财务和法务部门的自动化,从这篇开始。
结尾
各位小伙伴,本文的内容到这里就全部结束了,源码骑士在这里再次感谢您的阅读!
源码骑士 --- Android Framework & 全栈开发
👀 关注:跟博主一起从源码视角深耕底层原理,见证每一次成长
❤️ 点赞:让优质内容被更多人看见,让知识传递更有力量
⭐ 收藏:把核心知识点存好,在需要时随时查、随时用
💬 评论:分享你的经验或疑问,评论区一起交流避坑
🔄 一键四连:不要忘记给博主"一键四连"哦!
🗡️ 寄语:技术之路难免有困惑,但同行的人会让前进更有方向
结语:一个content数组、一个JSON Schema------图片从"程序看不懂的二进制"变成"流水线里的结构化数据",靠的就是这两个不起眼的结构。你的第一个视觉应用,今晚就能跑起来。不要忘记给博主"一键四连"哦!