Box 与图像:让 Jev 在图里找东西
前五课的 state 装的都是文本。但真实的判断需求常常长在图里:门铃照片上有没有人、扫描表格上的签名在哪个位置、喂鸟相机这一帧里鸟站在哪。把这类图交给 GPT,它吐一段描述——你要的却是一个能直接裁剪、能直接进 if 的坐标。
这节课讲 Jev Box:给它一张图和一句话,它返回围住目标的最小矩形的四条边。全程 10 分钟。
先把丑话说在前面:Images 和 Box 是实验特性。官方原话叫 dreamer-ready——还没达到他们敢担保的生产标准,请求格式和精度都可能随版本变化。拿来搭原型、验证想法很好;上生产要自己掂量。
学完你能做什么
- 用 Python SDK 的
Image把一张图放进 state,发出第一个带 Danger Zone 请求头的 Box 请求 - 读懂 Box 答案:
top/bottom/left/right四条边各是图片宽高的几等分,并换算回像素坐标裁出目标 - 用 Box 答案自带的
confidence决定直接用框、扩边再裁,还是换更好的图再问 - 用一道 Noul 守门,解决「Box 没有『找不到』这个答案」的问题
- 说出图像请求的限制:格式、尺寸、大小,以及超限会拿到什么错误码
你现在的困境
- 让 GPT 找图里的东西,它回一段「画面中偏左处有一个马克杯」的散文——方向有了,坐标没有,裁剪代码写不出来
- 用专门的检测模型又得标注数据、训练、调参,就想找一个杯子的位置,犯不上
- 多模态模型每次返回的坐标格式都不一样,还得写解析器兜底各种「看不清」「未找到」的花式回复
什么时候用这一招
当你需要:
- 目标在图里的位置:扫描件上的签名、门铃照片里的人、货架照片里的某个商品
- 位置能直接进代码:裁剪出目标区域送去 OCR、画框标注、按
top边排序 - 判断和定位一起问:人手里拿的是包裹还是袋子、目标在不在图里
而且不想:
- 训练或部署一个专用视觉模型
- 解析模型自由发挥的自然语言坐标描述
- 在原型阶段就为视觉能力搭一整套基础设施
🎒 开始前的准备
- 完成了 State 状态:知道 state 是什么、问题怎么用反引号路径指向它的字段
- 跑通过第一个请求:
.venv里装好了typesafe-sdk,TYPESAFE_API_KEY已在环境变量里 - 准备一张要找东西的图(官方示例找的是厨房照片里的马克杯;没有的话,任何 PNG/JPEG 都行——先看第 1 步的尺寸限制)
- 知道 Box 需要 Jev 1.14(
jev-latest已指向它,你大概率不用改);想确认就看模型与价格
核心思路
Box 回答「在哪」。 官方定位:Box 是第四种题型(Choice 挑选项、Score 打等级、Noul 断是非),也是第一个纯粹为「输出即数据」设计的位置题型。给它一张图和一句「图里的咖啡杯」,它返回围住咖啡杯的最小矩形。
答案就是四条边,而且是比例。
| 字段 | 含义 | 范围 |
|---|---|---|
left / right | 距图片左侧边缘的距离,占宽度的比例 | 0–1,left < right |
top / bottom | 距图片顶部边缘的距离,占高度的比例 | 0–1,top < bottom |
原点在左上角:top: 0 是图片上缘,bottom: 1 是下缘。全部是 0–1 的比例,意味着同一个答案在任何图片尺寸下都成立——手机拍的 4000×3000 和缩略图 400×300,框住的相对位置一样。要画框或裁剪时,把各边乘以图片的像素宽高就行(第 3 步)。
图像怎么进请求:引用 + assets。 每张图在请求顶层的 assets 里登记一次,拿到一个引用字符串;把这个引用写进 state、instructions 或 criteria 里任何字符串能出现的位置,模型就在那里收到图。直连 HTTP 时用 multipart 文件或 base64;Python SDK 会替你生成引用并自动上传,Image(路径) 直接放进 state 就完事(第 2 步)。
一个开关:Danger Zone 请求头。 图像和 Box 默认关闭。每个带图或带 Box 的请求都要加:
TypeSafe-Danger-Zone-Images: trueSDK 里就是传 extra_headers(第 2 步)。忘了加,请求直接 400。
Box 能和别的题混着问。 和前几课一样:所有题对同一份 state 并行评估,加题几乎不增加响应时间。
一个关键性格:Box 永远返回一个框。 没有「找不到」这个答案——图里没有咖啡杯,它照样给你四条边。守门的方法是同请求加一道 Noul「图里有没有咖啡杯」,Noul 说没有就不理会那个框(第 5 步)。
跟我做
第 1 步:准备一张符合限制的图片并安装 Pillow
为什么:图像请求的限制和文本不同——尺寸、格式、大小都卡得死,先按单子备图,第 2 步的请求才不会 413 / 400。
官方限制(images.md):
| 项 | 限制 |
|---|---|
| 格式 | PNG 或 JPEG,静态图(动态 PNG 会被拒) |
| 每边像素 | 280–1568,任何一边低于 280 会被拒 |
| 总像素 | 78,400–2,458,624(即 280×280 到 1568×1568 的面积) |
| 单文件 | ≤ 16 MiB |
| 请求体 | ≤ 32 MiB(含所有图、JSON、multipart 框架) |
准备图片的三件事(官方「Preparing images」守则):
- 缩放:最长的边压到 1568 像素以内。一部 1200 万像素的手机照片通常变成 1568×1176
- 转正:API 会剥掉所有元数据(EXIF 等)且不应用 EXIF 方向——看图软件里是竖的,到模型那里可能是横的。旋转像素本身,别依赖 EXIF
- 换格式:GIF、WebP、HEIC 先转成 PNG 或 JPEG;标准 8-bit baseline JPEG 最稳
一张图要就绪,用 Pillow 三行搞定(转正 + 缩放 + 转标准 JPEG):
from PIL import Image, ImageOps
with Image.open("photo.jpg") as original:
img = ImageOps.exif_transpose(original) # rotate the pixels upright
img.thumbnail((1568, 1568)) # longest side at most 1568, same aspect ratio
img.convert("RGB").save("photo-ready.jpg", "JPEG", quality=90)没装 Pillow 的话装一下(后面 Image.from_pil 和裁剪也都要用):
.venv/bin/pip install pillow你应该看到:photo-ready.jpg 生成成功;用图片查看器打开,方向正确、清晰度没变糊。
- 我的图是 PNG/JPEG、每边 280–1568 像素、方向已经转正
第 2 步:用 Python SDK 的 Image 发出第一个 Jev Box 请求
为什么:直连 HTTP 要自己拼 multipart、管引用、转 base64;Python SDK(v0.7.5 起支持 Image 和 Box)把这些全包了——Image 对象放进内容里,引用自动生成、文件自动上传。
Image 支持四种装法:
| 装法 | 写法 | 场景 |
|---|---|---|
| 文件路径 | Image(Path("kitchen.jpg")) | 最常用 |
| 原始字节 | Image(data) | 从网络流、数据库拿到的字节 |
| base64 文本 | Image.from_base64(data_base64) | 接收别人的上传 |
| PIL 对象 | Image.from_pil(pil_image) | 裁剪/缩放后直接发,省一次落盘;需要 typesafe-sdk[pil] |
新建 box_kitchen.py。就用你的 photo-ready.jpg,找图里的一个目标(官方示例是咖啡杯,换成你的图里实际有的东西):
from pathlib import Path
from typesafe_sdk import Box, Image, TypeSafeClient
with TypeSafeClient() as client:
response = client.system_one(
state={"photo": Image(Path("photo-ready.jpg"))},
questions={
"target": Box(instructions="the coffee mug in `photo`"),
},
extra_headers={"TypeSafe-Danger-Zone-Images": "true"},
)
print(response.answers["target"].box)
print(response.answers["target"].confidence)跑:
.venv/bin/python box_kitchen.py你应该看到(数值取决于你的图和目标):
top=0.25 bottom=0.82 left=0.44 right=0.84
0.92三个细节:
Box 题只有两个字段:
type(固定"box")和instructions(找什么、在哪张图)。没有criteria——选项、等级、是非定义在这里都用不上instructions不必是问句,一句话就够:"the coffee mug in`photo`"——反引号路径指明在哪张图里找(官方原话:It doesn't have to be a question)Image(Path(...))放在 state 里,SDK 自动生成一个类 UUID 的引用——多张图、嵌套结构都不会串我跑出了四个 0–1 之间的数和一个 confidence,没有 400 报错
⚠️ 忘了 Danger Zone 头会直接 400
只要请求里有图或有 Box 问题,TypeSafe-Danger-Zone-Images: true 一个都不能少。报错不客气:400,原因写得很明白。直连 HTTP 的人群,API 速查 有 curl 的 multipart 完整示例。
💡 Box 需要 Jev 1.14
Jev 1.13 和 1.14 都接受图像,但 Box 问题是 1.14 起才有。jev-latest 已指向 jev-1.14.0,正常跟着走就行;锁了 1.13 的项目想用 Box,得先升模型。
第 3 步:读懂 Box 答案的四条边并换算回像素坐标
为什么:0–1 的比例不能直接画框也不能直接裁剪——得乘回你这张图的像素尺寸。官方「Reading a Box」的代码就是这么做的。
先看原始返回(curl 直连人群会直接拿到这个 JSON;SDK 拿到的是同一个结构):
{
"model": "jev-1.14.0",
"answers": {
"mug": {
"type": "box",
"box": {
"top": 0.25,
"bottom": 0.82,
"left": 0.44,
"right": 0.84
},
"confidence": 0.92
}
},
"usage": {"input_tokens": 290, "output_tokens": 21}
}读法对照(就当你的图是 1568×1176):
| 字段 | 值 | 换算 | 像素位置 |
|---|---|---|---|
left | 0.44 | 0.44 × 1568 | 距左 690px |
right | 0.84 | 0.84 × 1568 | 距左 1317px |
top | 0.25 | 0.25 × 1176 | 距顶 294px |
bottom | 0.82 | 0.82 × 1176 | 距顶 964px |
官方裁剪代码(Pillow 的 crop 收像素坐标,顺序是 left, top, right, bottom):
from PIL import Image
image = Image.open("photo-ready.jpg")
width, height = image.size
mug = response["answers"]["mug"]
edges = mug["box"]
box = (
round(edges["left"] * width),
round(edges["top"] * height),
round(edges["right"] * width),
round(edges["bottom"] * height),
)
image.crop(box).save("mug.jpg")SDK 人群照抄即可,只是把 mug = response["answers"]["mug"] 换成 mug = response.answers["mug"]、edges = mug["box"] 换成 edges = mug.box——答案字段是同一个结构。
你应该看到:mug.jpg 打开后正好是你问的那个目标——咖啡杯被围得干干净净,上下左右基本没有多余背景。
- 我的裁剪结果围住了目标,四条边换算没搞反(左/右乘宽度,上/下乘高度)
第 4 步:用 confidence 决定裁剪还是复核
为什么:Box 答案自带 confidence(0–1,越高越确定)。和上节课的思路一样:高置信直接用,低置信多照应——一个框裁歪了,后面整条流水线都在处理错数据。
官方说法:没有推荐阈值,在你自己的图上调试。低于你的阈值时,官方给了两条路:
| 做法 | 怎么做 | 什么时候用 |
|---|---|---|
| 换更好的图再问 | 发更大的版本,或裁出框附近的区域放大后重问同一个 Box | 原图分辨率低、目标太小 |
| 先扩边再裁剪 | 把四条边向外挪一点再 crop,并保持在图片范围内 | 裁剪绝不能切掉目标任何部分时 |
扩边的官方代码(PAD 是每边向外扩的比例,照抄后按你的图调):
PAD = 0.05 # Fraction of the image to add on each side. Tune for your images.
MIN_CONFIDENCE = 0.6 # Example threshold. Tune it on your own images.
if mug["confidence"] < MIN_CONFIDENCE:
edges = {
"left": max(0.0, edges["left"] - PAD),
"top": max(0.0, edges["top"] - PAD),
"right": min(1.0, edges["right"] + PAD),
"bottom": min(1.0, edges["bottom"] + PAD),
}随后用这组新的 edges 按第 3 步的方式裁剪——目标连同一圈余量一起进 mug.jpg,宁可多带点背景,不可切掉半个杯把。
你应该看到:confidence 高的答案原样用;低于阈值的答案要么框变大了一圈,要么走了「放大重问」的路。
- 我给自己的 Box 答案定了 confidence 阈值,低于它有明确的处理路径(扩边或重问)
第 5 步:用 Noul 守门、混题批量问
为什么:第 1 个性格问题——Box 永远返回一个框,没有「找不到」。目标不在图里,它照样给四条边。守门方案官方已经写好:同一请求加一道 Noul。
官方的疫苗记录示例就是这么配的(Noul 问「在不在」,Box 问「在哪」):
questions = {
"mug_visible": Noul(instructions="Is there a coffee mug in `photo`?"),
"mug": Box(instructions="the coffee mug in `photo`"),
}用法一句话:Noul 说没有,就不理会 mug 的框。两题并行评估、一个请求、答案各挂各的 ID。
其他三个高频配合技巧,都来自官方「Writing Box questions」守则:
| 场景 | 做法 |
|---|---|
| 图里可能不止一个目标 | 用文字限定到唯一一个:"the mug nearest the sink in `photo` " |
| 文字说不清有几个 | 旁边加一道 Choice,选项 none / one / several;答案是 several 就把图切块、每块单独再问 |
| 大图里找小目标 | 先用 Choice 在图的多个 tile 里挑出目标所在的 tile,再对那个 tile 问 Box |
| 拿一张图当靶子 | 目标直接写进 instructions:"the person in `photo` who matches this badge photo: 9a21c7d3-badge" |
最后一条是图像引用的另一面:criteria 和 instructions 里也能放图。官方文档的寻狗示例用的就是这个机制:捡到的狗的照片放进 state,寻狗启事的照片直接写进问题里,问「是不是同一条狗」。
你应该看到:一个请求里 Noul 和 Box(还能再加 Choice / Score)并存,各答各的;mug_visible 低时你的代码直接跳过 mug 的框。
- 我的 Box 请求里有一道守门题(Noul 或 Choice),「找不到」有明确的代码出口
检查点 ✅
- 我知道 Images / Box 是实验特性(dreamer-ready):格式可能变、精度可能变,官方不建议直接上生产
- 我能说清四条边:
left/right是宽度的比例、top/bottom是高度的比例,原点在左上角 - 我跑通了带
TypeSafe-Danger-Zone-Images: true的请求,且能按像素换算裁出目标 - 我的 Box 答案有 confidence 阈值,低于阈值会扩边或换图重问
- 我的「找不到」场景有一道 Noul / Choice 守门,不会把空框当真
踩坑提醒
❌ 图没问题,请求 400 / 413
400: Images are not turned on
413: The request is over a size limit三件事自查:
- 带任何图或 Box 问题的请求都加了
TypeSafe-Danger-Zone-Images: true头吗?(400) - 图片尺寸出界了吗?每边 280–1568、总像素 78,400–2,458,624、单文件 ≤ 16 MiB——超限直接拒,不会自动缩放(
413或400) - 一批图合计超 32 MiB 了吗?API 处理时会重编码,处理后的图可能比你发的文件还大——发一堆小图也可能撞线(
413)
官方还提示:一个请求有多个问题时报错只报其中一个,对着限制表逐项排查。
❌ 拿着竖拍照片当竖图用
看图软件里竖得好好的,到模型眼里是横的——API 剥掉全部元数据(含 EXIF 方向)且不应用它。你问「图底部的杯子」,模型看的是一张横图里的杯子。
正确做法:发之前用 ImageOps.exif_transpose 把像素本身转正(第 1 步的 Pillow 三行)。别只改元数据。
❌ 裁出的框和目标对不上
先查换算有没有搞反:上下乘高度、左右乘宽度;再查 Pillow crop 的参数顺序是 (left, top, right, bottom)。两处任一搞反,框就飞到图外或歪到别处。
还错?看 confidence——低置信的框本来就可能偏。低于阈值先扩边(第 4 步的 PAD),或裁出区域放大后把同一个 Box 问题再问一次。
❌ 一道 Box 问「所有咖啡杯的位置」
Box 每题只回一个框,这是设计。想要多个目标的多个位置:每个目标单独一题(同一请求里发);同种目标分不清时就加 Choice(none / one / several),答案是 several 就切块分头再问。别把「找出所有 X」塞进一道 instructions。
❌ 目标根本不在图里,框照样有
Box 没有「找不到」。图里没杯子,答案照样是四条边——你的裁剪代码会开开心心裁出一块背景。
正确做法就是第 5 步:旁边加一道 Noul("Is there a coffee mug in `photo` ?"),Noul 的概率低就不理会那个框。官方的疫苗记录、发票核对、喂鸟相机三个 cookbook 全是这个套路。
进阶技巧(可选)
三种引用装法按场景挑。 自动引用之外,SDK 还有两种更细的控制:
assets参数——自己起引用名,用不容易撞的定界符(官方例子是<product>):state="Inspect this returned product: <product>"配assets={"<product>": Image(photo)}client.attach_assets——把图挂到 client 上、str(asset)嵌进正文:state=f"The customer says {photo} arrived scratched."。必须用with client.attach_assets(photo):上下文管理器,少了withSDK 就无法把文中的引用对回图片数据(官方警告原话)
PIL 对象直发,省一次落盘。 裁剪、缩放之后不用先保存再读回——Image.from_pil(pil_image) 直接发(JPEG 用 quality 95 编码;带透明或调色板的图传 format="png")。装法:.venv/bin/pip install "typesafe-sdk[pil]"。官方 OCR cookbook 用的就是这条路:整页找字段、裁出区域放大、再对裁剪图细问。
按 top 边排序读扫描件。 疫苗记录 cookbook 的招:每行一个 Box,box["box"]["top"] 小的排前面——纯代码就能把乱序的行读成有顺序的记录。完整案例在图像实战。
图像没有单独的价格。 每张图按输入 token 计费,和文本同价;usage.input_tokens 里含所有图的 token。图也占上下文长度(64k per-request / 32k per-question)——大图塞多了会 max_tokens_exceeded,细节见模型与价格。
SDK 的答案集合。 Box 也可以从 response.boxes["target"] 拿——和 response.answers["target"] 是同一批答案的 Box 专用入口。
本课小结
✅ Jev Box 的用法:Image 放进 state、Box(instructions=...) 一句话说清找什么,Danger Zone 头一开就能问。
✅ 读答案:四条边全是 0–1 的比例(left/right 乘宽度、top/bottom 乘高度),任何图片尺寸下同一答案都成立。
✅ confidence:Box 自带,没有官方推荐阈值——低于你自己定的线就扩边再裁,或换更好的图重问。
✅ 守门:Box 永远返回一个框;Noul 问「在不在」,答案低就不理会框。
✅ 实验特性:dreamer-ready,格式与精度可能随版本变化,原型阶段用最合适。
关键配置字段回顾:
| 字段 | 推荐写法 | 说明 |
|---|---|---|
type | "box" | 固定值 |
instructions | 一句话:找什么、在哪张图 | 不必是问句;没有 criteria |
| 请求头 | extra_headers={"TypeSafe-Danger-Zone-Images": "true"} | 每个带图/带 Box 的请求都要 |
Image(...) | 路径 / bytes / from_base64 / from_pil | from_pil 需 typesafe-sdk[pil] |
| 图片尺寸 | 每边 280–1568 像素,最长边 ≤ 1568 | 先转正(EXIF 不会被应用),超限直接拒 |
| 答案访问 | response.answers["id"].box / response.boxes["id"] | .box 是四条边,.confidence 0–1 |
想看 Box 在真实项目里的完整用法:图像实战:读扫描件、核对 OCR 结果——疫苗记录逐行定位、发票 OCR 逐字段核对、喂鸟相机物种日志,三个 cookbook 全流程。
下一课预告
单题的用法到这里就齐了:五种提问方式(Choice / Score / Noul / confidence / Box)你都过了一遍。接下来把它们拼成系统。下一课:高效工作流:四种架构模式与 coding agent 配合。
你会学到:
- 一次请求问一批问题的并行模式:官方实测 12.2 倍降本、10 倍提速
- 置信度门控路由:答案告诉你「是什么」,confidence 告诉你「要不要做」
- 复杂判断拆成原子 Score 再代码加权合并的组合打分
- 让 Claude Code 装上 TypeSafe Agent Skill,直接替你写调用 Jev 的代码