pressure_gauge
真值 reading = 2.3 bar(range [0, 4])· 三模型同图同 prompt
ox-alpha:命中真值 2.3 · 误差 0.0
GLM-5V:提取 0.0 · 误差记 1.0
Kimi K3:命中真值 2.30 · 误差 0.0
data/C18/images/pressure_gauge.pngtruth:reading = 2.3 · unit = bar · range = [0, 4](三个模型的输入完全相同)
| ox-alpha | GLM-5V | Kimi K3 | |
|---|---|---|---|
| reading_extracted | 2.3 (与真值一致) | 0.0 (⚠ 脚本提取记录为 0.0;回复原文写的是 2.25——两者不一致,照录不补算) | 2.3 (与真值一致) |
| truth | 2.3 (unit bar · 三份 JSON 相同) | ||
| 读数相对误差 ↓ | 0.0 | 1.0 | 0.0 |
| parser可运行_近似 | 1.0 | 0.0 | 0.0 |
| usage | p 453 / c 5770 (total 6223 tok · cost 0 · finish_reason "stop") | p 402 / c 2456 (reasoning 1612 · cost 0.0103064) | p 449 / c 7481 (reasoning 6622 · cost 0.1305963) |
RESPONSE stealth/ox-alpha · 模型回复原文刻度系统 → 读数 2.3 bar → 解析程序 → JSON;读数与真值一致
## 1. 刻度系统
| 项目 | 内容 |
|---|---|
| 量程 | 0 – 4 bar |
| 最小刻度(分度值) | 0.5 bar(仅有主刻度线,无小刻度) |
| 单位 | bar(表盘下方标注) |
| 倍率 / 多圈 | 无倍率;单圈表,扫角约 270°(0 在左下、4 在右下、2 在正上方) |
| 副尺 | 无 |
## 2. 读数
- 指针位于 **2 与 2.5 之间**,无小刻度,需按角度内插估读。
- 以像素估算:针尖约 (256, 106),表盘中心约 (210, 236),指针偏离竖直方向约 19°–20°;每 0.5 bar 对应 270°/8 = 33.75°,故超出 "2" 约 0.57 个格 → **读数 ≈ 2.29 ≈ 2.3 bar**。
- 分度值 0.5 bar,按 1/10 分度估读(0.05 bar),结果记为 **2.30 bar**。
## 3. 刻度解析程序
本表为**线性、单圈**刻度,程序直接适用;同时通过标定表 `cal` 支持非线性刻度,通过 `turns/rev` 支持多圈表(多圈时同一角度对应多个示数,必须外部提供当前圈序,否则存在歧义)。
```python
import math
def read_gauge(tip, center, p0, p1, v0=0.0, v1=4.0, turns=1, rev=0, cal=None):
"""tip/center/p0/p1: (x,y)像素坐标; p0/p1=量程起/止刻度中点
v0/v1: 起/止示数; turns: 总圈数; rev: 当前圈序(多圈表必填,0起)
cal: 非线性刻度标定表[(相对起点顺时针角度deg, 示数),...]; 线性传None"""
ang = lambda p: math.degrees(math.atan2(p[1]-center[1], p[0]-center[0]))
a0 = ang(p0)
sweep = lambda p: (ang(p) - a0) % 360.0 # 相对起点的顺时针扫角
s_tip, s_end = sweep(tip), sweep(p1)
if cal is None: # —— 线性刻度 ——
if turns > 1 and not 0 <= rev < turns:
raise ValueError("多圈表单角度有歧义, 必须提供 rev")
per = (v1 - v0) / turns
return round(v0 + rev * per + (s_tip / s_end) * per, 3)
pts = sorted((sweep(a), v) for a, v in cal) # —— 非线性: 角度-示数插值 ——
s = min(max(s_tip, pts[0][0]), pts[-1][0])
for (sa, va), (sb, vb) in zip(pts, pts[1:]):
if sa <= s <= sb:
return round(va + (vb - va) * (s - sa) / (sb - sa), 3)
return pts[-1][1]
# 本表调用(像素为估计值):
# read_gauge(tip=(256,106), center=(210,236), p0=(108,339), p1=(313,339))
# -> 2.29 ≈ 2.3 bar
```
## 输出 JSON
```json
{
"scale": {
"range": [0, 4],
"unit": "bar",
"smallest_division": 0.5,
"minor_ticks": false,
"multiplier": 1,
"multi_turn": false,
"sweep_angle_deg": 270,
"vernier": false
},
"reading": 2.3,
"parser": "import math\ndef read_gauge(tip, center, p0, p1, v0=0.0, v1=4.0, turns=1, rev=0, cal=None):\n ang = lambda p: math.degrees(math.atan2(p[1]-center[1], p[0]-center[0]))\n a0 = ang(p0)\n sweep = lambda p: (ang(p) - a0) % 360.0\n s_tip, s_end = sweep(tip), sweep(p1)\n if cal is None:\n if turns > 1 and not 0 <= rev < turns:\n raise ValueError('multi-turn gauge needs rev')\n per = (v1 - v0) / turns\n return round(v0 + rev * per + (s_tip / s_end) * per, 3)\n pts = sorted((sweep(a), v) for a, v in cal)\n s = min(max(s_tip, pts[0][0]), pts[-1][0])\n for (sa, va), (sb, vb) in zip(pts, pts[1:]):\n if sa <= s <= sb:\n return round(va + (vb - va) * (s - sa) / (sb - sa), 3)\n return pts[-1][1]\n# 本表: read_gauge(tip=(256,106), center=(210,236), p0=(108,339), p1=(313,339)) -> 2.29"
}
```
RESPONSE z-ai/glm-5v-turbo · 模型回复原文纯 JSON 输出:scale → reading(2.25 bar)→ parser(Python)
```json
{
"scale": {
"range": "0 ~ 4 bar",
"min_division": "0.25 bar",
"unit": "bar",
"multiplier_or_multi_turn": "无(单圈线性刻度)",
"vernier_scale": "无"
},
"reading": {
"value": 2.25,
"explanation": "指针位于刻度 2 与 2.5 之间。该表盘每 0.5 bar 有一个大格,中间有 1 个小格,故最小分度为 0.25 bar。指针尖端恰好指向或非常接近 2 与 2.5 之间的中间刻度线(即 2.25 刻度处)。按物理仪表读数规则,估读到最小分度的下一位,读数约为 2.25 bar(合理范围在 2.20 ~ 2.30 bar 之间均视为正确)。"
},
"parser": "def parse_gauge(pointer_pos, center_pos, zero_angle, full_scale_angle, min_val=0, max_val=4, is_nonlinear=False, calibration_curve=None):
"""
解析表盘读数的通用程序
:param pointer_pos: 指针端点的像素坐标 (x, y)
:param center_pos: 表盘中心的像素坐标 (x, y)
:param zero_angle: 零刻度对应的角度(弧度)
:param full_scale_angle: 满量程对应的角度(弧度)
:param min_val: 量程最小值
:param max_val: 量程最大值
:param is_nonlinear: 是否为非线性刻度(如对数、方根等)
:param calibration_curve: 非线性刻度的校正函数(角度->数值),若为 None 则按线性处理
:return: 仪表读数
"""
import math
# 1. 计算指针相对中心的角度
dx = pointer_pos[0] - center_pos[0]
dy = pointer_pos[1] - center_pos[1]
# atan2 返回 -pi 到 pi,需根据表盘实际方向调整基准
theta = math.atan2(dy, dx)
# 2. 归一化角度到 [0, 1] 区间
# 处理角度跨越 -pi/pi 的情况(如表盘跨越12点钟方向)
angle_range = full_scale_angle - zero_angle
if angle_range == 0:
angle_range = 2 * math.pi # 完整一圈
normalized = (theta - zero_angle) / angle_range
# 处理多圈情况(若存在倍率或多圈指示):
# 对于多圈表,通常需要结合机械计数器或历史帧数据判断圈数。
# 单幅静态图像无法确定绝对圈数,此处仅计算当前圈内的相对位置。
# 如果已知总圈数 N,可加上 N * (max_val - min_val)
normalized = normalized % 1.0
# 3. 映射为数值
if is_nonlinear and calibration_curve:
value = calibration_curve(normalized)
else:
# 默认线性处理
value = min_val + normalized * (max_val - min_val)
return value
# 针对本表的说明:
# 本表为标准线性刻度压力表,不涉及非线性或多圈复杂情况。
# 可直接通过指针角度与 0-4bar 范围的比例关系计算出精确读数。"
}
```
RESPONSE moonshotai/kimi-k3 · 模型回复原文单行 JSON:scale → reading 2.30 → parser(Python);读数与真值一致
{"scale":{"range":[0,4],"min_division":0.5,"unit":"bar","multiplier":null,"multi_turn":false,"sub_scale":false,"note":"No range multiplier (dial reads directly in bar), single-turn less-than-360 deg movement (0 to 4 bar spans about 270 deg of arc), and no vernier/sub-scale. The pointer tip falls between the 2 and 2.5 marks; because the scale is uniform I take the value by linear interpolation in dial angle: theta_tip about 69.6 deg (math angle, y up) versus 90 deg at '2' and 56.25 deg at '2.5', fraction=(90-69.6)/(90-56.25)=0.604, value=2+0.604*0.5=2.30 bar. With a 0.5 bar minor division this is normally quoted to 0.1 bar, i.e. 2.3 bar."},"reading":2.30,"parser":"import math\nfrom bisect import bisect_right\n\ndef read_gauge(center, tip, start_pt, end_pt, v_min, v_max, clockwise=True, calib=None, turns=0, turn_value=None, sweep_deg=None):\n '''center/tip/start_pt/end_pt are (x,y) image pixels (y down). start_pt/end_pt are the dial points for v_min/v_max. clockwise=True means value increases clockwise (true for this gauge). calib (optional) is a monotonic list of (phi,value) for a NON-LINEAR scale, phi=fractional arc from start in [0,1]; if given it overrides the linear map. turns/turn_value handle MULTI-TURN dials: turns=completed full revolutions, turn_value=value added per revolution. sweep_deg=total increasing-direction sweep start->end; default computed from the two end points.'''\n TAU=2*math.pi\n def ang(p):\n return math.atan2(-(p[1]-center[1]), p[0]-center[0]) % TAU\n def fwd(a,b):\n d=(b-a)%TAU\n return d if clockwise else (TAU-d)%TAU\n a0,a1,at=ang(start_pt),ang(end_pt),ang(tip)\n sweep=math.radians(sweep_deg) if sweep_deg is not None else fwd(a0,a1)\n if sweep<=0:\n raise ValueError('zero sweep')\n phi=min(max(fwd(a0,at)/sweep,0.0),1.0)\n if calib:\n xs=[p for p,_ in calib]; ys=[v for _,v in calib]\n i=min(max(bisect_right(xs,phi)-1,0),len(xs)-2)\n x0,x1=xs[i],xs[i+1]; y0,y1=ys[i],ys[i+1]\n base=y0 if x1==x0 else y0+(y1-y0)*(phi-x0)/(x1-x0)\n else:\n base=v_min+phi*(v_max-v_min)\n if turn_value is not None:\n return turns*turn_value+(base-v_min)\n return base\n\n# This gauge: read_gauge(center=(210,237), tip=(256,113), start_pt=<pixel of the 0 mark>, end_pt=<pixel of the 4 mark>, v_min=0.0, v_max=4.0, clockwise=True) -> about 2.30. Non-linear (calib) and multi-turn (turns/turn_value) branches are NOT used here: the 0-4 bar range is a single uniform arc of ~270 deg (under one revolution, the pointer never passes the origin again), so turns=0, turn_value=None and the linear map suffices."}