空间约束求解
视觉到结构化约束这一步的损耗
输入与证据锚点
冰箱内部 / 行李箱 / 书架实拍一张
双轴设计
Prompt
PROMPT主 PROMPT根据这张照片:
根据这张照片: 1. 输出 constraints.json:可用空间的估计尺寸、画面中每个物品的估计尺寸与当前位置、 以及硬约束(易碎、需冷藏、不可倒置、必须可随手取用)。 只写照片里真实出现的物品。你不确定是什么的物品,标 unknown 并给出它占的空间。 2. 输出一个求解器脚本,给出重排方案,并渲染一张排布示意图。 3. 说明你的方案在哪些约束上做了妥协。
Rubric 评分表
| 维度 | 满分 | 由谁打 | 判据 |
|---|---|---|---|
| 约束提取准确率 | 35 | judge | 与人工标注的物品清单和硬约束对比,取 F1。 |
| 幻觉物品 | 25 | 脚本 | 每个照片中不存在的物品 -8 分。unknown 标注不算幻觉,算诚实。 |
| 求解器可运行 | 25 | 脚本 | 脚本运行成功、方案不违反它自己写的硬约束(用脚本回检)。 |
| 妥协说明 | 15 | judge | 是否指出真实存在的取舍。回答"没有妥协"而实际有冲突记 0。 |
脚本= deterministic,脚本自动计算,不进 judge;judge= 主观维度,必须先看 deterministic 结果再打分。本案例总分归一化到 0-100。本案例恰好脚本与 judge 各占一半:幻觉物品+求解器可运行 共 50 分走脚本,约束提取准确率+妥协说明 共 50 分走 judge。
跑批命令
python run_batch.py --models anthropic/claude-opus-4.5 openai/gpt-5.2 google/gemini-3-pro moonshotai/kimi-k2.5 --only C05python score.py --judge anthropic/claude-opus-4.5 --out report.csv数据准备:按 data/C05/manifest.jsonl 的格式放置图片与真值。score 阶段的 judge 只处理本案例 rubric 中非 deterministic 的维度。
实测结果 · Live Run
REAL RUN · 3 MODELS · n=1| 运行用量(usage 原值) | ● ox-alpha | ● glm-5v-turbo | ● kimi-k3 |
|---|---|---|---|
| prompt tokens | 465 | 369 | 470 |
| completion tokens | 21,404 | 6,462 | 6,795 |
| 其中 reasoning tokens | 0 | 2,343 | 6,613 |
| total tokens | 21,869 | 6,831 | 7,265 |
| cost(USD · usage.cost 原值) | 0 | 0.0262908 | 0.09816825 |
| finish_reason | stop | stop | stop |
| flags / score_error | [] / null | [] / null | [] / null |
读数要点:三个脚本维度里两家完全同分——「约束JSON可解析」(1.0 / 1.0 / 1.0)与「幻觉物品数」(0 / 0 / 0);唯一分差在「求解器可运行」:ox-alpha 与 Kimi K3 的求解器脚本在打分沙箱真实跑通且回检不违反自写硬约束(1.0),GLM-5V 记 0.0(它的 constraints.json 成功解析、也无幻觉物品,json 未记录脚本未跑通的具体原因,此处如实只报分数)。judge 两维(F1、妥协说明)对三家都未执行,仍无总分。表头圆点颜色即上方图例配色。
- 模型
stealth/ox-alpha· 经 OpenRouter- 运行时间
- 2026-08-22(当日完成调用与打分;
results/C05.json最终重算写入于 22:06) - mode / scorer
single/exec_plus_judge- 样本规模
- 1 条样本(item_id:
main)· 1 轮问答 - 输入图片
images/fridge.png× 1(480×420 px)- Tokens
- prompt 465 · completion 21,404(reasoning 0)· total 21,869
- finish_reason
stop(正常结束,无截断/无报错)
四个物品全部识别成功、无 unknown 项,提取出的像素尺寸与人工真值 4/4 一致;幻觉物品数 0;求解器脚本在本机沙箱真实运行成功,方案回检未违反它自己写下的硬约束。脚本可判定的两个 Rubric 维度均拿满(合计 50/50);judge 两维(约束提取准确率 F1、妥协说明)本轮未执行打分,故不产生总分。值得注意的是模型主动做了像素↔厘米标定(图中标注「宽 400px≈40cm」→ 10px=1cm),并在妥协说明里指出了「温度 ↔ 可达性」「防滚 vs 高频取用」等真实取舍。三模型横向看:「约束JSON可解析」(1.0 / 1.0 / 1.0)与「幻觉物品数」(0 / 0 / 0)完全同分,唯一分差在「求解器可运行」——GLM-5V 的脚本未能通过打分沙箱运行检查记 0,ox-alpha 与 Kimi K3 均真实跑通拿满;GLM-5V 也是三者中唯一像素尺寸读数出现偏差的一家(分数对比见上方面板,读数对照见下方真值表)。
data/C05/images/fridge.png(480×420 px,合成示意图)。画面自带标定线索「宽 400px≈40cm」。本页直接以相对路径引用原始数据文件。| 物品 | 真值 w×h (px) | 模型提取 bbox_px | 一致 |
|---|---|---|---|
| 牛奶盒 | 120 × 160 | 120 × 160 @ (60, 60) | ✓ |
| 保鲜盒 | 150 × 100 | 150 × 100 @ (200, 80) | ✓ |
| 鸡蛋 | 140 × 80 | 140 × 80 @ (60, 240) | ✓ |
| 饮料瓶 | 50 × 180 | 50 × 180 @ (370, 50) | ✓ |
容器真值:冰箱;硬约束真值:鸡蛋易碎 · 牛奶需冷藏 · 饮料瓶不可倒置。「模型提取」列由页面脚本从模型输出的 constraints.json 原文中解析后填入(w×h 与原点坐标),比对结果为程序计算,非人工誊抄。
| 物品 | 真值 (px) | ox-alpha 读数 | glm-5v-turbo 读数 | kimi-k3 读数 |
|---|---|---|---|---|
| 牛奶盒 | 120 × 160 | 120×160 ✓ | 120×160 ✓ | 12×16 cm ✓ |
| 保鲜盒 | 150 × 100 | 150×100 ✓ | 140×100 ✗ 宽−10 | 15×10 cm ✓ |
| 鸡蛋 | 140 × 80 | 140×80 ✓ | 120×80 ✗ 宽−20 | 14×8 cm ✓ |
| 饮料瓶 | 50 × 180 | 50×180 ✓ | 45×185 ✗ −5/+5 | 5×18 cm ✓ |
三模型尺寸读数对照(页面依据各自回复原文整理的读数比对,非 judge 打分——judge 的 F1 本轮对所有模型都未计算):ox-alpha 直接给出 bbox_px 且四件全中;Kimi K3 给的是 size_cm(其自述标定 10px=1cm,换算后四件全中);GLM-5V 自报的 position_px 有三件宽度偏小 5–20px。注意「幻觉物品数」考察的是编造照片中不存在的物品而非读数误差,故三家该项均为 0。
PROMPT实际发送的 Prompt根据这张照片:
根据这张照片: 1. 输出 constraints.json:可用空间的估计尺寸、画面中每个物品的估计尺寸与当前位置、 以及硬约束(易碎、需冷藏、不可倒置、必须可随手取用)。 只写照片里真实出现的物品。你不确定是什么的物品,标 unknown 并给出它占的空间。 2. 输出一个求解器脚本,给出重排方案,并渲染一张排布示意图。 3. 说明你的方案在哪些约束上做了妥协。
本次样本 vars = {}——没有发生模板替换,实际发送文本与第 03 节题目模板逐字一致。三份 results json(ox-alpha / glm-5v-turbo / kimi-k3)记录的实际发送 Prompt 逐字相同,vars 均为空对象——没有发生模板替换,实际发送文本与第 03 节题目模板一致,即三个模型收到的是同一道题。
RESPONSEstealth/ox-alpha 回复原文# 读图前提 …(完整原文约 8,859 字符 / 21,404 completion tokens,点击展开)
# 读图前提
- 标定:图中标注「宽 400px ≈ 40cm」→ **10px = 1cm**。
- 坐标系:以内腔**左上角为原点**,x 向右(宽),y 向下(假设下方是箱门/前缘)。
- 内腔边界估读为 px (40, 28) 起,400×370 → **40cm 宽 × 37cm 深**(深度无标定,误差 ±2cm)。
- 四个物品均有清晰文字标注(牛奶盒/保鲜盒/饮料瓶/鸡蛋),**全部识别成功,无 unknown 项**;`unknown_items` 留空数组。
---
## 1. constraints.json
```json
{
"_comment": "坐标系:内腔左上角为原点,x向右(宽),y向下(深,假设朝门)。比例尺 10px=1cm。",
"calibration": { "px_per_cm": 10, "source": "图中标注:宽400px≈40cm" },
"interior": {
"bbox_px": { "x": 40, "y": 28, "w": 400, "h": 370 },
"size_cm": { "w": 40.0, "d": 37.0 },
"note": "仅宽度有标定;深度按同比例量图,误差±2cm"
},
"items": [
{
"id": "milk",
"label": "牛奶盒",
"identified": true,
"bbox_px": { "x": 60, "y": 60, "w": 120, "h": 160 },
"size_cm": { "w": 12.0, "d": 16.0 },
"current_pos_cm": { "x": 2.0, "y": 3.2 },
"hard_constraints": ["需冷藏", "不可倒置"],
"soft_preferences": ["靠后壁(温度更稳)"],
"note": "纸盒饮品,须直立,防渗漏"
},
{
"id": "fresh_box",
"label": "保鲜盒",
"identified": true,
"bbox_px": { "x": 200, "y": 80, "w": 150, "h": 100 },
"size_cm": { "w": 15.0, "d": 10.0 },
"current_pos_cm": { "x": 16.0, "y": 5.2 },
"hard_constraints": ["需冷藏", "必须可随手取用"],
"soft_preferences": [],
"note": "假定存放高频取用食材"
},
{
"id": "bottle",
"label": "饮料瓶",
"identified": true,
"bbox_px": { "x": 370, "y": 50, "w": 50, "h": 180 },
"size_cm": { "w": 5.0, "d": 18.0 },
"current_pos_cm": { "x": 33.0, "y": 2.2 },
"hard_constraints": ["需冷藏", "不可倒置"],
"soft_preferences": ["贴侧壁直立防滚动"],
"note": "圆柱瓶,横放会滚,必须直立或固定"
},
{
"id": "eggs",
"label": "鸡蛋",
"identified": true,
"bbox_px": { "x": 60, "y": 240, "w": 140, "h": 80 },
"size_cm": { "w": 14.0, "d": 8.0 },
"current_pos_cm": { "x": 2.0, "y": 21.2 },
"hard_constraints": ["易碎", "需冷藏"],
"soft_preferences": ["远离前缘/门口", "贴角落防滑撞"],
"note": "按整盒蛋托处理"
}
],
"unknown_items": [],
"global_rules": [
"任意两物品矩形不得重叠(俯视图无法表达堆叠,一律单层)",
"任何物品不得置于鸡蛋所占区域的上方/邻位滑动路径上(以贴角近似)"
]
}
```
---
## 2. 求解器脚本 `fridge_rearrange.py`
思路:位置量化到 1cm 网格 → 硬约束作为**候选剪枝** → 「最受限物品优先」回溯搜索无重叠解 → 软偏好进**评分函数**(整体紧凑、鸡蛋贴角、保鲜盒居中靠前)→ 输出 `plan.json` + 渲染 `layout.png`。
```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""冰箱重排求解器:读 constraints.json → 回溯搜索 → plan.json + layout.png"""
import json
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
STEP, TOL = 1.0, 0.5 # 搜索网格 / “贴墙”容差 (cm)
def frange(lo, hi, st):
n = int(round((hi - lo) / st))
return [round(lo + i * st, 3) for i in range(n + 1)]
def feasible(it, x, y, W, D):
"""硬约束 + 安全性规则的几何编码"""
w, d = it["size_cm"]["w"], it["size_cm"]["d"]
hc = it["hard_constraints"]
if it["id"] == "bottle" and "不可倒置" in hc:
if W - (x + w) > TOL: # 必须贴右侧壁,防滚动
return False
if it["id"] == "eggs": # 易碎:贴后壁 + 远离开门扰动区
if y > TOL or (D - (y + d)) < 10:
return False
if it["id"] == "milk": # 靠后壁,温度最稳
if y > TOL:
return False
if "必须可随手取用" in hc: # 距前缘 ≤ 6cm
if (D - (y + d)) > 6:
return False
return True
def gen_candidates(items, W, D):
cand = {}
for it in items:
w, d = it["size_cm"]["w"], it["size_cm"]["d"]
cand[it["id"]] = [(x, y) for x in frange(0, W - w, STEP)
for y in frange(0, D - d, STEP)
if feasible(it, x, y, W, D)]
return cand
def overlap(a, b):
return not (a[0]+a[2] <= b[0]+1e-9 or b[0]+b[2] <= a[0]+1e-9 or
a[1]+a[3] <= b[1]+1e-9 or b[1]+b[3] <= a[1]+1e-9)
def score(assign, W, D):
rs = assign.values()
x0 = min(r[0] for r in rs); x1 = max(r[0]+r[2] for r in rs)
y0 = min(r[1] for r in rs); y1 = max(r[1]+r[3] for r in rs)
s = -(x1 - x0) * (y1 - y0) # 包围盒最小化→都靠墙,中部留操作空间
if "eggs" in assign: # 鸡蛋越贴左右角越好
e = assign["eggs"]; s -= 2.0 * min(e[0], W - e[0] - e[2])
if "fresh_box" in assign: # 保鲜盒尽量居中靠前
f = assign["fresh_box"]; s -= 0.5 * abs(f[0] + f[2]/2 - W/2)
return s
def solve(items, W, D):
cand = gen_candidates(items, W, D)
order = sorted(items, key=lambda it: len(cand[it["id"]])) # 最受限优先
best, best_s = None, float("-inf")
def dfs(i, assign):
nonlocal best, best_s
if i == len(order):
s = score(assign, W, D)
if s > best_s:
best_s, best = s, dict(assign)
return
it = order[i]; w, d = it["size_cm"]["w"], it["size_cm"]["d"]
for (x, y) in cand[it["id"]]:
r = (x, y, w, d)
if any(overlap(r, p) for p in assign.values()):
continue
assign[it["id"]] = r
dfs(i + 1, assign)
del assign[it["id"]]
dfs(0, {})
return best, best_s
COLORS = {"milk": "#cfd8ef", "fresh_box": "#a8d8a8",
"bottle": "#8fbce6", "eggs": "#f5c98a"}
def render(items, assign, W, D, path="layout.png"):
fig, ax = plt.subplots(figsize=(8, 7.5))
ax.add_patch(Rectangle((0, 0), W, D, fill=False, lw=3, ec="k"))
for it in items:
x, y, w, d = assign[it["id"]]
ax.add_patch(Rectangle((x, y), w, d, fc=COLORS[it["id"]], ec="k"))
ax.text(x+w/2, y+d/2, f'{it["label"]}\n{w:g}×{d:g}cm',
ha="center", va="center", fontsize=9)
ox, oy = it["current_pos_cm"]["x"], it["current_pos_cm"]["y"]
if abs(ox-x) > .01 or abs(oy-y) > .01: # 红箭头 = 移动路径
ax.annotate("", xy=(x+w/2, y+d/2), xytext=(ox+w/2, oy+d/2),
arrowprops=dict(arrowstyle="->", color="red", lw=1.2))
ax.set_xlim(-2, W+2); ax.set_ylim(D+2, -2) # y 向下 = 朝门
ax.set_aspect("equal")
ax.set_title("冰箱单层重排方案(俯视:上=后壁,下=门)")
ax.set_xlabel("x / cm"); ax.set_ylabel("y / cm")
fig.savefig(path, dpi=150, bbox_inches="tight")
print(f"[ok] 示意图 → {path}")
def main():
data = json.load(open("constraints.json", encoding="utf-8"))
W, D = data["interior"]["size_cm"]["w"], data["interior"]["size_cm"]["d"]
items = data["items"]
assign, s = solve(items, W, D)
if assign is None:
print("[fail] 无可行解:请核对尺寸/约束"); return
plan = {"score": round(s, 1), "placements": []}
for it in items:
x, y, w, d = assign[it["id"]]
ox, oy = it["current_pos_cm"]["x"], it["current_pos_cm"]["y"]
plan["placements"].append({
"id": it["id"], "label": it["label"],
"old_pos_cm": [ox, oy], "new_pos_cm": [x, y],
"moved_cm": round(((x-ox)**2 + (y-oy)**2) ** .5, 1),
"hard_constraints_met": it["hard_constraints"]})
json.dump(plan, open("plan.json", "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
print(json.dumps(plan, ensure_ascii=False, indent=2))
render(items, assign, W, D)
if __name__ == "__main__":
main()
```
运行:`pip install matplotlib && python fridge_rearrange.py`(秒级出结果)。
### 本机推演的输出方案
| 物品 | 原位置 (cm) | 新位置 (cm) | 移动 | 满足的硬约束 |
|---|---|---|---|---|
| 牛奶盒 12×16 | (2.0, 3.2) | **(14, 0)** | ≈12.4 | 需冷藏✓ 不可倒置✓(直立、贴后壁) |
| 鸡蛋 14×8 | (2.0, 21.2) | **(26, 0)** | ≈32.0 | 易碎✓(后壁右上角、离前缘28cm) 需冷藏✓ |
| 饮料瓶 5×18 | (33.0, 2.2) | **(35, 8)** | ≈6.1 | 需冷藏✓ 不可倒置✓(紧贴右壁防滚) |
| 保鲜盒 15×10 | (16.0, 5.2) | **(14, 21)** | ≈15.9 | 需冷藏✓ 随手取用✓(距前缘6cm、居中) |
示意图(`layout.png` 由脚本精确渲染,此为示意):
```
y(cm) 0 ┌─────────────────────────────────────────┐ 后壁
│ ┌───────┬─────────────┐│
│ │牛奶盒 │ 鸡蛋(易碎) ││
8 │ │12×16 ├─────────────┤│
│ │ │ 饮料瓶 5×18 ││
│ │ │ (贴右壁) ││
16 │ └───────┤ ││
│ ┌────────┐ │ ││
21 │ │ 保鲜盒 │ │ ││
│ │ 15×10 │ │ ││
│ │(随手取) │ │ ││
31 │ └────────┘ └─────────────┘│
37 └─────────────────────────────────────────┘ 前缘/门
0 14 26 35 40 x(cm)
```
---
## 3. 方案的妥协说明
硬约束(易碎/需冷藏/不可倒置/随手取用)**全部满足**,妥协都在软层面和模型层面:
1. **二维单层模型**:真实冰箱有多层搁架和门架。鸡蛋的理想位置其实是门内侧蛋格或保留原包装;这里只能在单一平面里选最稳的角落。“不能压到鸡蛋”也只能用“不重叠”近似,无法表达垂直方向的堆叠禁令。
2. **温度 ↔ 可达性的取舍**:保鲜盒为满足“随手取用”被放到靠前区域——那也是温度最不稳、最暖的区域,牺牲了后壁低温区的保鲜效果(估计差 1–2℃)。
3. **“随手取用”只判给了保鲜盒**:如果饮料瓶也算高频取用,会和“贴壁防滚”直接冲突;我选择了防滚优先,因为瓶子倾倒漏液的后果比多伸手 10cm 更重。
4. **防滚/防撞靠几何而非物理固定**:瓶子只是“贴紧右壁”,没有隔板;鸡蛋与牛奶盒在后壁相邻,若有物品滑动仍可能磕碰。建议实际操作时加一根防滚杆,或让鸡蛋留在原蛋托中(方案已按整托 14×8cm 处理)。
5. **测量误差**:像素估读 ±5px(±0.5cm),深度方向无标定、误差可达 ±2cm。若实物放不进求解出的格子,改 `constraints.json` 里的尺寸重跑即可。
6. **方向假设**:假设图片下方是箱门。若实际门在别侧,把 `feasible()` 里“前缘/后壁”的条件翻转再跑一次即可。RESPONSEz-ai/glm-5v-turbo 回复原文**1. constraints.json** …(完整原文 11,626 字符 / 6,462 completion tokens,其中 reasoning_tokens 2,343,点击展开)
**1. constraints.json**
```json
{
"container": {
"id": "fridge_shelf",
"name": "冰箱内部(单层)",
"width_cm": 40.0,
"height_cm": 36.0,
"depth_cm": 35.0,
"coordinate_system": "image_pixels_top_left_origin",
"scale": "1px = 0.1cm (1mm)"
},
"items": [
{
"id": "milk_carton",
"name_zh": "牛奶盒",
"category": "dairy_liquid",
"dimensions_cm": {"w": 12.0, "h": 16.0, "d": 8.0},
"position_px": {"x": 42, "y": 62, "w": 120, "h": 160},
"hard_constraints": {
"refrigerated": true,
"fragile": false,
"no_tilt": true,
"upright_required": true,
"accessibility_priority": "high"
}
},
{
"id": "fresh_box",
"name_zh": "保鲜盒",
"category": "food_container",
"dimensions_cm": {"w": 14.0, "h": 10.0, "d": 10.0},
"position_px": {"x": 195, "y": 82, "w": 140, "h": 100},
"hard_constraints": {
"refrigerated": true,
"fragile": false,
"no_tilt": false,
"rotatable": true,
"accessibility_priority": "low"
}
},
{
"id": "drink_bottle",
"name_zh": "饮料瓶",
"category": "beverage_glass",
"dimensions_cm": {"w": 4.5, "h": 18.5, "d": 4.5},
"position_px": {"x": 320, "y": 52, "w": 45, "h": 185},
"hard_constraints": {
"refrigerated": true,
"fragile": true,
"no_tilt": true,
"upright_required": true,
"accessibility_priority": "high",
"edge_clearance": "preferred"
}
},
{
"id": "eggs",
"name_zh": "鸡蛋",
"category": "fragile_protein",
"dimensions_cm": {"w": 12.0, "h": 8.0, "d": 6.0},
"position_px": {"x": 58, "y": 242, "w": 120, "h": 80},
"hard_constraints": {
"refrigerated": true,
"fragile": true,
"no_tilt": false,
"shock_sensitive": true,
"accessibility_priority": "medium",
"stable_base_required": true
}
}
],
"metadata": {
"estimation_method": "visual_pixel_count_with_scale_reference",
"uncertainty": "±5% on linear dimensions",
"assumptions": ["bottle_is_glass", "eggs_in_carton", "single_layer_view"]
}
}
```
---
**2. 求解器脚本 (Python + Matplotlib)**
该脚本使用**带权贪心算法**(Weighted Greedy Placement)结合**碰撞检测**,优先满足硬约束(易碎品稳定性、液体直立),其次优化取用便利性。
```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from dataclasses import dataclass
from typing import List, Tuple, Optional
import numpy as np
# ==================== 数据定义 ====================
@dataclass
class Item:
id: str
name: str
w: float # cm
h: float # cm
fragile: bool
no_tilt: bool # 必须保持当前朝向(不可旋转)
accessibility: int # 1-5, 5最高优先级
color: str
def __post_init__(self):
self.x: Optional[float] = None
self.y: Optional[float] = None
self.placed = False
# 初始化物品(基于constraints.json的估计)
items = [
Item("milk", "牛奶盒", 12.0, 16.0, False, True, 5, "#FFFFFF"),
Item("fresh", "保鲜盒", 14.0, 10.0, False, False, 2, "#C1E1C1"),
Item("bottle", "饮料瓶", 4.5, 18.5, True, True, 4, "#AEC6CF"),
Item("eggs", "鸡蛋", 12.0, 8.0, True, False, 3, "#FFE4B5")
]
CONTAINER_W = 40.0 # cm
CONTAINER_H = 36.0 # cm
PADDING = 0.5 # cm, 物品间距
# ==================== 求解器核心 ====================
class FridgeSolver:
def __init__(self, width: float, height: float):
self.W = width
self.H = height
self.grid_resolution = 0.5 # 放置精度0.5cm
def check_collision(self, item: Item, x: float, y: float, placed_items: List[Item]) -> bool:
"""检查是否越界或与其他物品重叠"""
if x < 0 or y < 0 or x + item.w > self.W or y + item.h > self.H:
return True
for other in placed_items:
if other.placed:
if not (x + item.w + PADDING <= other.x or
x >= other.x + other.w + PADDING or
y + item.h + PADDING <= other.y or
y >= other.y + other.h + PADDING):
return True
return False
def score_position(self, item: Item, x: float, y: float) -> float:
"""
评分函数:越高越好
- 易碎品:偏好低重心(y值小,即靠近'地面'/前部)和角落
- 高取用优先级:偏好靠近开口处(假设y轴正向为前/易取方向)
- 稳定性:对于no_tilt物品,避免悬空边缘
"""
score = 0.0
# 1. 易碎品稳定性奖励(左下角最稳)
if item.fragile:
corner_dist = np.sqrt(x**2 + y**2)
score += max(0, 20 - corner_dist)
# 2. 取用便利性(假设y越大越靠近冰箱门/易取)
access_bonus = item.accessibility * (y / self.H) * 2
score += access_bonus
# 3. 边缘惩罚(对易碎且高的物品,如瓶子)
if item.fragile and item.h > 15:
edge_margin = min(x, y, self.W - (x+item.w), self.H - (y+item.h))
if edge_margin < 2:
score -= 10 # 太靠近边缘扣分
return score
def solve(self, items: List[Item]) -> List[Item]:
"""
排序策略:
1. 先放体积大且约束多的(鸡蛋-易碎+需稳定,牛奶-高+常用)
2. 再放灵活的(保鲜盒可旋转)
3. 最后放细长的(瓶子)
"""
# 排序:易碎优先 > 高取用优先 > 大体积 > 可旋转最后
sorted_items = sorted(items, key=lambda i: (
not i.fragile, # 易碎的先放(True=1排后面,所以not让它排前面)
-i.accessibility, # 高优先级的先放
-(i.w * i.h), # 大的先放
i.rotatable if hasattr(i, 'rotatable') else False # 不可旋转的先放
))
placed = []
for item in sorted_items:
best_score = -float('inf')
best_pos = (None, None)
# 在网格上搜索最佳位置
for x in np.arange(0, self.W - item.w + 0.1, self.grid_resolution):
for y in np.arange(0, self.H - item.h + 0.1, self.grid_resolution):
if not self.check_collision(item, x, y, placed):
s = self.score_position(item, x, y)
if s > best_score:
best_score = s
best_pos = (x, y)
if best_pos[0] is not None:
item.x, item.y = best_pos
item.placed = True
placed.append(item)
else:
print(f"警告:无法放置 {item.name}")
return placed
# ==================== 执行求解与可视化 ====================
def visualize_layout(original_items: List[Item], optimized_items: List[Item], save_path: str = "fridge_layout.png"):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 7))
def draw_scene(ax, title, item_list, show_coords=True):
ax.set_xlim(-1, CONTAINER_W + 1)
ax.set_ylim(-1, CONTAINER_H + 1)
ax.set_aspect('equal')
ax.set_title(title, fontsize=14, fontweight='bold')
# 画冰箱内框
rect = patches.Rectangle((0, 0), CONTAINER_W, CONTAINER_H,
linewidth=3, edgecolor='#333333', facecolor='#E8F4F8')
ax.add_patch(rect)
# 画物品
for item in item_list:
if item.placed or hasattr(item, 'x') and item.x is not None:
x, y, w, h = item.x, item.y, item.w, item.h
# 边框样式:易碎品用红色虚线边框提示
linestyle = '--' if item.fragile else '-'
linewidth = 2 if item.fragile else 1
patch = patches.Rectangle((x, y), w, h,
linewidth=linewidth,
edgecolor='red' if item.fragile else 'black',
facecolor=item.color,
linestyle=linestyle,
alpha=0.8)
ax.add_patch(patch)
# 标注文字
cx, cy = x + w/2, y + h/2
ax.text(cx, cy, item.name, ha='center', va='center',
fontsize=9, fontweight='bold',
color='black' if item.color in ['#FFFFFF', '#FFE4B5', '#C1E1C1'] else 'white')
if show_coords:
ax.text(x, y+h+0.5, f"({x:.1f},{y:.1f})", fontsize=7, color='gray')
# 标注特殊约束
if item.no_tilt:
ax.text(x+w/2, y-0.8, "↑直立", ha='center', fontsize=7, color='red')
if item.fragile:
ax.plot(x+1, y+1, 'x', color='red', markersize=6)
ax.set_xlabel('宽度 (cm)')
ax.set_ylabel('深度/高度 (cm)')
ax.grid(True, alpha=0.3, linestyle=':')
# 准备原始数据(转换为cm坐标,假设原图左上角为0,0,但物理上我们转为左下角为0,0以便理解)
# 注意:原图像坐标y向下为正,这里我们统一为数学坐标(y向上为正,左下角原点)
# 为简化对比,两个图都使用相同的坐标系(左下角原点)
orig_copy = []
for it in original_items:
# 将原图像素位置近似转换为cm(基于目测)
if it.id == "milk":
it.x, it.y = 4.0, 16.0 # 左上区域
elif it.id == "fresh":
it.x, it.y = 19.0, 18.0 # 中上
elif it.id == "bottle":
it.x, it.y = 33.0, 15.0 # 右侧
elif it.id == "eggs":
it.x, it.y = 5.0, 4.0 # 左下
it.placed = True
orig_copy.append(it)
draw_scene(ax1, "原始布局 (Original Layout)", orig_copy)
# 求解优化
solver = FridgeSolver(CONTAINER_W, CONTAINER_H)
optimized = solver.solve(items)
draw_scene(ax2, "优化布局 (Optimized Layout)", optimized)
plt.tight_layout()
plt.savefig(save_path, dpi=150, bbox_inches='tight')
plt.show()
# 输出坐标表
print("
=== 重排方案坐标表 (单位: cm, 原点: 左下角) ===")
print(f"{'物品':<8} {'X':<6} {'Y':<6} {'宽':<6} {'高':<6} {'备注'}")
print("-" * 50)
for it in optimized:
note = []
if it.fragile: note.append("易碎")
if it.no_tilt: note.append("直立")
if it.accessibility >= 4: note.append("常用")
print(f"{it.name:<8} {it.x:<6.1f} {it.y:<6.1f} {it.w:<6.1f} {it.h:<6.1f} {', '.join(note)}")
if __name__ == "__main__":
# 给保鲜盒添加旋转标记(虽然本例中未使用旋转,但标记其灵活性)
items[1].rotatable = True
visualize_layout(items.copy(), items)
```
**运行结果示意(文本描述):**
| 物品 | 原始位置 (x,y) | 优化位置 (x,y) | 变化说明 |
|------|---------------|---------------|---------|
| **鸡蛋** | (5.0, 4.0) | **(0.0, 0.0)** | 移至左下角,利用角落提供最大稳定性,远离开门震动区 |
| **牛奶盒** | (4.0, 16.0) | **(0.0, 9.0)** | 移至左侧中部,保持直立且易于伸手拿取(左手习惯) |
| **饮料瓶** | (33.0, 15.0) | **(35.0, 0.0)** | 移至右下角,降低重心防倾倒,且避开中央通道 |
| **保鲜盒** | (19.0, 18.0) | **(13.5, 0.0)** | 填充剩余空间,因无需频繁取用置于后排 |
*注:坐标原点为左下角,Y轴正向为"靠近冰箱门/易取方向"。*
---
**3. 方案妥协说明 (Trade-off Analysis)**
本方案在以下约束上做出了**有意妥协**:
### A. **空间利用率 vs. 取用便利性 (Space vs. Accessibility)**
- **妥协点**:将**保鲜盒**推至后排(Y值较小区域),而将**牛奶**和**饮料瓶**(高优先级物品)置于前排。
- **原因**:保鲜盒通常存放剩菜或备菜,取用频率低于饮品。牺牲其便利性以换取高频物品的"零障碍取用"。
- **风险**:若需取用保鲜盒,必须先移动或探过前排物品。
### B. **易碎品隔离 vs. 重心分布 (Fragility vs. Weight Distribution)**
- **妥协点**:**饮料瓶(玻璃/易碎)**被放置在**右下角边缘**而非绝对中心。
- **原因**:若将瓶子置于中心,会阻断左右两侧的物品存取路径形成"孤岛";置于边缘虽增加碰撞风险,但通过将其紧贴右侧壁(X=35cm)并降低Y坐标(贴近"地面")来补偿稳定性。
- **未满足的完美约束**:理想情况下玻璃瓶应有2cm以上的四周缓冲区,但在40cm宽度的紧凑空间内无法完全满足。
### C. **鸡蛋的方向性约束 (Egg Orientation)**
- **妥协点**:鸡蛋盒被旋转了90°(假设原盒为横向放置,现改为纵向或保持原状但位置改变)。
- **原因**:标准鸡蛋盒要求**大头朝上**以保持蛋黄居中。本方案仅保证其**平放且四角支撑**,但未强制规定哪边是"大头"(因图像无法判断鸡蛋盒方向)。**实际操作时需人工确认方向**。
### D. **"不可倒置"的严格几何解释**
- **妥协点**:**牛奶盒**和**饮料瓶**均保持了**竖直方向(H维度垂直于地面)**,但允许其在X-Y平面内平移。
- **潜在风险**:如果冰箱在搬运过程中发生倾斜(非水平),当前布局中**饮料瓶**因较高(18.5cm)且位于边缘,力臂较长,倾倒风险略高于中心位置。这是为了换取空间效率的必要让步。
### E. **尺寸估计误差容错**
- **隐含妥协**:所有尺寸基于像素估算(±5%误差)。若实际**保鲜盒**比估计的14cm更宽(如15.5cm),则优化后的紧密排列会出现**0.5cm的重叠**。脚本中预留了`PADDING=0.5cm`的安全边距来吸收此误差,但这降低了理论最大空间利用率约3%。
### 总结建议
此方案最适合**日常静态存储**场景。若冰箱经常处于**移动/车载环境**,建议进一步妥协**取用便利性**,将**饮料瓶**和**鸡蛋**都用硅胶绑带固定或改用吸盘底座,并将所有物品质心向几何中心收敛。RESPONSEmoonshotai/kimi-k3 回复原文## 1) constraints.json …(完整原文 5,714 字符 / 6,795 completion tokens,其中 reasoning_tokens 6,613,点击展开)
## 1) constraints.json
比例按图中标题 `400px ≈ 40cm` → `10 px = 1 cm`。坐标原点=可用空间左上角,x 向右,y 向下(y 大=靠冰箱门/前方,更“随手可取”)。尺寸/位置为像素读数估计,误差约 ±1–2 cm。
```json
{
"scale": { "px_per_cm": 10, "note": "取自标题:冰箱内部宽 400px≈40cm" },
"available_space": {
"width_cm": 40,
"height_cm": 36,
"estimation_error_cm": 2,
"coordinate_frame": "origin=usable-space top-left; x→right, y→toward door/front"
},
"items": [
{
"id": "milk", "label": "牛奶盒", "unknown": false,
"size_cm": { "w": 12, "h": 16 },
"current_pos_cm": { "x": 1.5, "y": 2.5 },
"constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
"basis": "乳制品需冷藏;带盖/利乐包不建议倒置(no_invert 按保守处理)"
},
{
"id": "crisper", "label": "保鲜盒", "unknown": false,
"size_cm": { "w": 15, "h": 10 },
"current_pos_cm": { "x": 15.5, "y": 4.5 },
"constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": false, "must_be_accessible": false },
"basis": "默认内有食物需冷藏;密封盒可横放,取用频率按最低处理"
},
{
"id": "bottle", "label": "饮料瓶", "unknown": false,
"size_cm": { "w": 5, "h": 18 },
"current_pos_cm": { "x": 32.5, "y": 1.5 },
"constraints": { "fragile": false, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
"basis": "瓶口向上防漏/碳酸;日常常取"
},
{
"id": "eggs", "label": "鸡蛋", "unknown": false,
"size_cm": { "w": 14, "h": 8 },
"current_pos_cm": { "x": 1.5, "y": 20.5 },
"constraints": { "fragile": true, "needs_refrigeration": true, "no_invert": true, "must_be_accessible": true },
"basis": "易碎;保持平放不倒置;日常常取"
}
],
"hard_constraint_definitions": {
"needs_refrigeration": "重排后仍必须位于冰箱内部空间内(本问题为 2D 层内重排,自动满足)",
"no_invert": "求解器禁止 90° 旋转/翻转,保持图示朝向",
"fragile": "要求与其它物品间留出安全间隙 gap,且不被压在其它物品下方(2D 中体现为间隙约束)",
"must_be_accessible": "尽量贴近 y=height(前缘/门口)放置,且不被其它物品遮挡;无法全部满足时按优先级取舍"
},
"solver_settings": { "gap_cm": 1.0, "allow_rotation": false }
}
```
## 2) 求解器脚本(纯 Python + matplotlib),与重排方案
```python
# solver.py
import json, math
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
C = json.load(open("constraints.json"))
W = C["available_space"]["width_cm"]
H = C["available_space"]["height_cm"]
GAP = C["solver_settings"]["gap_cm"]
items = C["items"]
placed = []
def overlap(x, y, w, h):
for p in placed:
if not (x + w + GAP <= p["x"] or p["x"] + p["w"] + GAP <= x or
y + h + GAP <= p["y"] or p["y"] + p["h"] + GAP <= y):
return True
return False
def try_place(it, x, y):
w, h = it["size_cm"]["w"], it["size_cm"]["h"]
if x < 0 or y < 0 or x + w > W or y + h > H: return False
if overlap(x, y, w, h): return False
placed.append({"id": it["id"], "label": it["label"], "x": x, "y": y, "w": w, "h": h})
return True
# 阶段1:必须随手可取的物品 → 底排(贴近前缘 y=H),按高度降序从左排
front = sorted([i for i in items if i["constraints"]["must_be_accessible"]],
key=lambda i: -i["size_cm"]["h"])
cx = 0.0
for it in front:
w, h = it["size_cm"]["w"], it["size_cm"]["h"]
assert try_place(it, cx, H - h), f"front row overflow: {it['id']}"
cx += w + GAP
# 阶段2:非随手物品 → 从后/顶部 (y 小) 扫描可放位置
back = [i for i in items if not i["constraints"]["must_be_accessible"]]
step = 0.5
for it in back:
w, h = it["size_cm"]["w"], it["size_cm"]["h"]
done = False
y = 0.0
while y + h <= H and not done:
x = 0.0
while x + w <= W and not done:
done = try_place(it, round(x,2), round(y,2))
x += step
y += step
assert done, f"cannot place {it['id']}"
# 渲染
fig, ax = plt.subplots(figsize=(6, 6))
ax.add_patch(Rectangle((0, 0), W, H, fill=False, lw=2))
colors = {"milk":"#e8def5","crisper":"#cbe7c9","bottle":"#bcd6ee","eggs":"#fbeeC7"}
for p in placed:
ax.add_patch(Rectangle((p["x"], p["y"]), p["w"], p["h"],
facecolor=colors.get(p["id"], "#ccc"),
edgecolor="#444", lw=1.2))
ax.text(p["x"]+0.6, p["y"]+1.6, p["label"], fontsize=10)
ax.text(W/2, H+1.2, "前缘 / 冰箱门(随手可取区)", ha="center", fontsize=9)
ax.set_xlim(-2, W+2); ax.set_ylim(H+3, -3); ax.set_aspect("equal")
ax.set_title(f"重排方案 {W}x{H}cm gap={GAP}cm")
plt.savefig("layout.png", dpi=160, bbox_inches="tight")
print(json.dumps({"placed": placed, "canvas": {"W": W, "H": H}}, ensure_ascii=False, indent=2))
```
跑出的方案(gap=1cm,坐标为左上角 cm)与一个 ASCII 预览:
```text
y=0(后/里) ┌──────────────┬────────────────────────────┐
│ 保鲜盒 15x10 │ │ ← 非随手,放最里
y=10 ├────┬─────────┼──────────┬─────────────────┤
│饮 │ 牛奶盒 │ 鸡蛋 │ (空隙) │ ← 前缘随手排
│料 │ 12x16 │ 14x8 │ │
│瓶 │ │ │ │
y=36(前)───┴5x18┴──────────┴──────────┴──────────────────┘
x=0 x=6 x=19 x=33
placed:
crisper x=0.0 y=0.0 (最里,妥协可达性)
bottle x=0.0 y=18.0 (前缘左)
milk x=6.0 y=20.0 (前缘)
eggs x=19.0 y=28.0 (前缘)
```
## 3) 在哪些约束上做了妥协
1. **“必须可随手取用”无法对所有物品同时成立。** 在 2D 层内若把 4 件全部放同一前缘排,总宽 = 12+15+5+14 = 46 cm(加间隙 >48)超过 40 cm,必然不可行。因此把**取用频率判定最低的“保鲜盒”(must_be_accessible=false)**牺牲到最里/后排;其余三件常用物品(饮料、牛奶、鸡蛋)全部贴前缘,且互相不遮挡。这是主要妥协。
2. **尺寸是像素估计(±1–2 cm)。** 用 `gap=1 cm` 安全间隙吸收读数误差;代价是牺牲少量空间利用率,换取“易碎鸡蛋”等不被挤压、以及估计误差的鲁棒性。
3. **“不可倒置 / 不旋转”被强制定向。** 求解器禁用 90° 旋转(饮料瓶、牛奶盒、鸡蛋保持原朝向),牺牲了一部分可能的紧密装箱可能性,以满足 no_invert,并避免标签/瓶口朝下。
4. **“需冷藏”实际上不约束本重排。** 因为所有物品本来就在同一冰箱层内,2D 重排不会离开冷藏环境;脚本把它作为语义记录而非几何约束。
5. **可达性判定带主观假设。** “保鲜盒”被标为随手需求最低;如果你实际把它当每日常用,只需把它 `must_be_accessible` 改成 true,但前排就会放不下——脚本会按同样规则转而把鸡蛋/牛奶被挤到里排,需要在回答层重新声明取舍。以下为 stealth/ox-alpha 一家的 score 字段逐字段明细(GLM-5V 与 Kimi K3 的对应数值已并入上方「三模型分数对比」;两份 json 的 score 字段结构相同,同样只有这三个脚本维度,judge 维度同样缺席,flags 均为 []、score_error 均为 null):
| 打分项(score 字段原文 · stealth/ox-alpha) | 结果 | 对应 Rubric 维度 | 说明 |
|---|---|---|---|
| 约束JSON可解析 | 1.0 ✓ | 求解链路前置检查 | 模型输出的 constraints.json 在打分脚本中成功解析。(此项正是本轮重算所修复的检查点:修复前多行代码塞进字符串字段会导致严格解析失败、被误记为未作答。) |
| 幻觉物品数 | 0 | 脚本 幻觉物品(25 分) | 照片中不存在而模型写出的物品数为 0 → 按 rubric 规则「每个幻觉物品 −8 分」,本维不扣分,得 25/25。unknown 标注不算幻觉(本例 unknown_items 为空数组)。 |
| 求解器可运行 | 1.0 ✓ | 脚本 求解器可运行(25 分) | 脚本在本机沙箱真实运行成功、方案回检不违反自写硬约束,得 25/25。口径说明(环境限制):沙箱对模型生成代码用启发式多签名尝试调用,属近似判定,非严格基准口径。 |
| flags | [] | — | 无异常标记。 |
| score_error | null | — | 打分过程本身无异常(score_error: null)。 |
| 约束提取准确率 | 未评分 | judge (35 分) | 本轮真实跑批未包含 judge 打分环节,F1 未计算,如实留空。 |
| 妥协说明 | 未评分 | judge (15 分) | 同上,本轮未执行 judge 打分。(模型原文第 3 节确实给出了 6 条妥协分析,供人工查阅。) |
分数数值逐字取自 results/C05.json 的 score 字段:{"约束JSON可解析": 1.0, "幻觉物品数": 0, "求解器可运行": 1.0, "flags": []}。本轮可由脚本判定的部分合计 50/50;judge 部分 50 分未执行,因此本次运行没有归一化总分,请勿将 50/50 解读为百分制满分。
- 模型生成代码的函数签名不受约束,沙箱用启发式多签名尝试调用,非严格基准
- 样本规模为演示级缩小版(1),非原始题库规模
- 三个模型的调用方式相同(同一套
run_batch.py风格的 OpenRouter chat/completions 调用),但 stealth/ox-alpha 是强制深度推理的 stealth 模型,glm-5v-turbo 与 kimi-k3 的推理行为未特别配置——输出风格与推理深度可能天然不同,这是解读对比结果时要注意的局限,不是哪一方“作弊”。 - 本轮三个模型均只有脚本维度分(各 3 项),judge 两维对所有模型都未执行,因此不存在可比的总分。