Skip to content
📚 日常使用第 7 / 7 节

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 的请求都要加:

text
TypeSafe-Danger-Zone-Images: true

SDK 里就是传 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):

python
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 和裁剪也都要用):

bash
.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,找图里的一个目标(官方示例是咖啡杯,换成你的图里实际有的东西):

python
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)

跑:

bash
.venv/bin/python box_kitchen.py

你应该看到(数值取决于你的图和目标):

text
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 拿到的是同一个结构):

json
{
  "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):

字段值换算像素位置
left0.440.44 × 1568距左 690px
right0.840.84 × 1568距左 1317px
top0.250.25 × 1176距顶 294px
bottom0.820.82 × 1176距顶 964px

官方裁剪代码(Pillow 的 crop 收像素坐标,顺序是 left, top, right, bottom):

python
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 是每边向外扩的比例,照抄后按你的图调):

python
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 问「在哪」):

python
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 ​

text
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): 上下文管理器,少了 with SDK 就无法把文中的引用对回图片数据(官方警告原话)

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_pilfrom_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 的代码
搞懂AI关于我们怎么写隐私说明

独立的第三方教程,与文中提到的任何产品及其公司没有隶属或合作关系;产品名称和商标归各自所有者。价格和政策以官方页面为准。