OpenGlass 多模型图像描述评测:以 img_48 为例对比 Llava 与 Moondream 的视觉输出
OpenGlass 多模型图像描述评测:以 img_48 为例对比 Llava 与 Moondream 的视觉输出
导读
本篇文章以 OpenGlass 仓库中 prompts/series_1/img_48.md 这份评测文档为骨架,完整还原了同一张室内场景照片(img_48.jpeg)被四个本地视觉语言模型(Vision Language Model, VLM)分别描述的输出,并结合 prompts/generate.ts、sources/agent/imageDescription.ts、sources/modules/ollama.ts 等源码,讲清楚这套"多模型对照描述"评测流水线的运行机制与底层实现。读完本文,你将理解 OpenGlass 智能眼镜项目如何对拍摄帧进行多模型交叉评测,掌握 Ollama 多模态推理的调用方式(含 Base64 编码、指数退避重试、缩进裁剪等细节),并能照葫芦画瓢复现属于自己的图像描述评测。
上图就是 img_48.md 所描述的原始样本:一间白墙红砖天花的大开间,顶部贯穿两根大管道,中部安装有空调通风机组,左侧窗户引入自然光。同一张图,交给不同的本地视觉模型,得到的描述在细节、侧重与措辞上差异明显——这正是本文要展开讨论的核心现象。
一、这份评测文档从哪来:generate.ts 批处理流水线
img_48.md 并非人工手写,而是由 prompts/generate.ts 自动生成的评测产物。该脚本的逻辑非常直接:
- 扫描
prompts/下的所有子目录(如series_1),找出全部.jpeg图片; - 对每张图片依次调用 4 个"描述模型",把输出按
####标题####的分隔格式拼进同一个.md文件; - 用
cli-progress进度条实时显示批量处理进度; - 将最终结果写回与图片同名的
.md文件(例如img_48.jpeg→img_48.md)。
// prompts/generate.ts 关键逻辑(节选)
for (let s of files) {
if (s.endsWith('.jpeg')) {
let image = fs.readFileSync(path.join(__dirname, f, s));
imageTests.push({ path: path.join(__dirname, f, s).replace('.jpeg', '.md'), image, outputs: '' });
}
}
await runTest('Description', async (img) => await imageDescription(img));
await runTest('Description (llava-llama3)', async (img) => await imageDescription(img, 'llava-llama3'));
await runTest('Description (llava:34b-v1.6)', async (img) => await imageDescription(img, 'llava:34b-v1.6'));
await runTest('Description (moondream:1.8b-v2-fp16)', async (img) => await imageDescription(img, 'moondream:1.8b-v2-fp16'));
从中可以确认两个关键事实:第一,img_48.md 中的每个 ####Description (模型名)#### 段就是某个模型对同一张图片的原始输出;第二,默认模型(不带后缀的 Description)与 moondream:1.8b-v2-fp16 段是两个独立调用——imageDescription 函数在未传模型参数时默认使用 moondream:1.8b-v2-fp16,而 generate.ts 又显式跑了 'Description (moondream:1.8b-v2-fp16)' 一项,因此 img_48.md 头部第一段与末尾段在模型上是同一族的,只是独立采样、输出各自不同。
有趣的是,prompts/generate.ts 中还预留了被注释掉的模糊度检测环节(imageBlurry),说明评测流水线原本打算同时输出"图像是否模糊/损坏"的判定,当前版本处于注释状态。相关实现位于 sources/agent/imageBlurry.ts,它使用 moondream:1.8b-v2-moondream2-text-model-f16 模型并要求模型只能回答 YES 或 NO。
二、img_48.md 原文:四个模型的完整描述输出
以下是 img_48.md 的完整内容(原文照录,未做删减),它是理解多模型差异的第一手素材。
2.1 默认 Description(基础调用,模型为 moondream:1.8b-v2-fp16)
The image shows a large, open space with white walls and a brick ceiling. The room features two large pipes running across the ceiling, one on each side of the room. There is also an air conditioning unit installed in the middle of the room, providing ventilation for the space. A window can be seen on the left wall, allowing natural light to enter the area.
这段输出精炼、条理清晰:先概括"大而开放的空间 + 白墙 + 砖顶",再逐项列出关键元素——屋顶两侧各一根大管道、房间中部的空调机组、左侧墙上的窗户与自然光。整体是"总—分"结构,要素齐全,几乎没有多余的文学性修饰。
2.2 Description (llava-llama3)
The image captures a serene indoor setting. Dominating the scene is a high ceiling, from which hang three white beams of varying lengths. These beams are arranged in a triangular formation, creating an interesting geometric pattern against the backdrop of the ceiling.
On the right wall of the room, there's a sign that stands out with its vibrant blue and white colors. The text on the sign reads "sa_101", but without additional context, it's hard to determine its significance.
The left side of the image is bathed in natural light streaming in from two windows located at the top of the wall. The light illuminates the room, casting soft shadows and highlighting the textures of the bricks that make up the walls.
Adding a touch of contrast to the scene is a black pole positioned on the left side of the image, standing tall against the white ceiling. Its presence adds a sense of depth and dimension to the overall composition of the image.
llava-llama3 的视角明显更"审美化":强调"静谧的室内氛围"、横梁的"三角几何构图"、砖墙纹理、光影与黑色立柱带来的"纵深感"。更重要的是,它完成了 OCR 级转写——识别出墙上蓝白标牌上的文字 "sa_101",并诚实标注"缺乏上下文无法判断其含义"(这正是系统提示词中 "Transcribe any text you see." 要求的体现)。这是四个模型里唯一识别出文字的。
2.3 Description (llava:34b-v1.6)
The image shows a section of an interior space with architectural features visible. There is a structural column on the left side, which appears to be made of metal or steel. Above it, there's an exposed ceiling grid with what might be insulation panels. The ceiling has beams that are possibly part of the building's support structure.
Natural light seems to be coming from above, likely through skylights, illuminating a portion of the wall and floor, suggesting that the photo was taken during daytime. It might have been captured from inside a vehicle or another structure that obstructs part of the view.
The overall style of the photograph seems casual, possibly taken without a high-quality camera given the softness and lack of sharp focus. It might have been captured from inside a vehicle or another structure that obstructs part of the view.
llava:34b-v1.6 的输出带明显的"推理演绎"色彩:把左侧立柱推断为"金属/钢制结构柱"、把顶部网格推断为"隔热板"、把光线来源推断为"天窗"、甚至推测照片拍摄自车内/遮挡结构、以及相机质量不佳导致画面偏软。它在事实描述之外叠加了大量 "seems / likely / possibly / might be" 的推测性语言,与前两个模型的确定性陈述形成鲜明对照。
2.4 Description (moondream:1.8b-v2-fp16)
In the image, a large white pipe runs vertically down from one end of an industrial building to another. The pipe is suspended at various heights, creating an interesting architectural display throughout the space. Nearby, there are multiple windows in different sizes and orientations, likely providing natural light for the workspace. Additionally, two black fans can be seen positioned above the ceiling, which might serve ventilation purposes or create a unique visual effect as they appear to be floating in mid-air.
moondream 的表述同样带推测色彩("likely providing natural light"、"might serve ventilation purposes"),但它观察到的对象与前几个模型略有出入——把管道描述为"垂直贯穿工业建筑两端"、"高低错落悬挂",并注意到"两个黑色风扇"悬浮在屋顶。这些细节与默认 Description 中的"空调机组"可能是同一物体的不同理解,恰好体现了小型视觉模型的识别不确定性。
三、差异背后的设计:提示词、模型与调用链
四个模型之所以输出风格迥异,根源在于 sources/agent/imageDescription.ts 中统一的提示词约定与各模型自身能力:
export async function imageDescription(src: Uint8Array, model: KnownModel = 'moondream:1.8b-v2-fp16'): Promise<string> {
return ollamaInference({
model: model,
messages: [{
role: 'system',
content: 'You are a very advanced model and your task is to describe the image as precisely as possible. Transcribe any text you see.'
}, {
role: 'user',
content: 'Describe the scene',
images: [src],
}]
});
}
注意三点设计细节:
- 系统提示词明确要求"尽可能精确地描述"并"转写所见文字"。
img_48.md中 llava-llama3 转写出的 "sa_101" 正是这条指令的直接产物。 - 模型名通过
KnownModel类型约束。该类型在 sources/modules/ollama.ts 中定义,白名单包括llama3、llama3-gradient、llama3:8b-instruct-fp16、llava-llama3、llava:34b-v1.6、moondream:1.8b-v2-fp16、moondream:1.8b-v2-moondream2-text-model-f16。这意味着评测只能在本地已拉取的模型间切换。 - 图片以原始字节(Uint8Array)传入,由底层负责编码,调用方无需关心格式。
底层调用:ollamaInference 的完整请求管线
ollama.ts 中的 ollamaInference 承担了从图片字节到 Ollama 响应的完整转换:
let converted: { role: string, content: string, images?: string[] }[] = [];
for (let message of args.messages) {
converted.push({
role: message.role,
content: trimIdent(message.content),
images: message.images ? message.images.map((image) => toBase64(image)) : undefined,
});
}
let resp = await axios.post(keys.ollama, {
stream: false,
model: args.model,
messages: converted,
});
return trimIdent(((response.message?.content ?? '') as string));
要点拆解:
- Base64 转换:sources/utils/base64.ts 中的
toBase64先把字节数组逐字节转为字符,再经btoa编码。多模态消息中的images字段正是以这种 Base64 字符串传给 Ollama 的(/api/chat接口的stream: false模式)。 - 指数退避重试:整个请求被 sources/utils/time.ts 的
backoff包裹。默认minDelay = 250ms、maxDelay = 1000ms、maxFailureCount = 50,失败会随机化等待时间并重试,最多 50 次,保障大批量评测(series_1共有 57 张图 × 4 个模型)在模型服务不稳定时仍能跑完。 - 缩进裁剪(trimIdent):请求与响应两侧都经过 sources/utils/trimIdent.ts 处理——去掉首尾空行,并计算所有非空行的最小前导空格数统一切除,从而让多行提示词与模型输出保持整洁。
img_48.md中各段描述段落整齐、无缩进噪声,正是该函数的效果。
四、从评测到实战:描述在 Agent 里的真实用途
img_48.md 这类评测文档不只是"模型比赛",它反映的是 OpenGlass 智能眼镜最核心的感知链路:眼镜拍摄 → 本地 VLM 描述 → LLM 结合描述回答问题。这条链路在 sources/agent/Agent.ts 中落地:
async addPhoto(photos: Uint8Array[]) {
await this.#lock.inLock(async () => {
for (let p of photos) {
let description = await imageDescription(p); // 默认 moondream 模型
this.#photos.push({ photo: p, description });
}
});
}
async answer(question: string) {
await this.#lock.inLock(async () => {
let combined = '';
let i = 0;
for (let p of this.#photos) {
combined += '\n\nImage #' + i + '\n\n' + p.description;
i++;
}
let answer = await llamaFind(question, combined); // Groq 上的 llama3
this.#state.answer = answer;
});
}
可以看到一个清晰的两段式架构:
- 感知段(本地):
addPhoto把每帧图片交给imageDescription生成文字描述,存入内存队列。默认模型是轻量的moondream:1.8b-v2-fp16——这正是img_48.md中评测的模型之一,评测数据直接对应真实运行时会走的模型。 - 推理段(云端/远程):
answer把多帧描述拼接成Image #0 / Image #1 / ...的文本上下文,交给llamaFind(imageDescription.ts 中的 Groq llama3 调用)回答问题,并严格约束 LLM"只依据给定描述作答、不得泛化"。
所以 img_48.md 的实战意义在于:它定量展示了"眼镜实时看到的画面会被本地模型压缩成什么样的文字"——描述的准确性直接决定了后续问答的质量。评测文档中暴露出的问题(如 moondream 把空调认作风扇、llava:34b 大量使用推测语气)恰恰是部署真实设备时需要在模型选择上权衡的:想要 OCR 能力选 llava-llama3,想要轻量快速选 moondream,想要结构化叙述则要结合实际帧率与资源预算综合决策。
五、复现方法:在本地跑起这套多模型评测
OpenGlass 的视觉评测全部基于本地 Ollama 服务。若要复现 img_48.md 的生成,需要:
- 准备模型:在 Ollama 中拉取参与评测的模型(对应
KnownModel白名单),至少包括moondream:1.8b-v2-fp16、llava-llama3、llava:34b-v1.6。注意llava:34b-v1.6体量较大,需足够的显存/内存。 - 配置服务地址:
ollamaInference通过keys.ollama指定 Ollama 服务端点(见 sources/keys.ts,仓库示例中注释掉了远程 hosthttps://ai-1.korshakov.com,本地部署可指向http://localhost:11434)。 - 运行评测脚本:在仓库根目录执行 TypeScript 脚本
prompts/generate.ts(需先安装依赖,参见 package.json)。脚本会遍历prompts/下全部*.jpeg,把每个模型的描述写入同名.md文件。
需要说明的边界:这些描述输出是评测当时的模型快照结果,模型版本、采样参数(temperature 等)不同都会导致输出变化;且图片描述存在主观性——同一张图(如 img_48)在不同模型眼中可能是"横梁三角构图"也可能是"工业管道与风扇",这并非模型"错误",而是视觉理解的多解性。在实际使用 OpenGlass 时,应将描述质量、推理速度与设备资源(眼镜端与本地服务端)一起纳入模型选型考量。
结语
从 img_48.md 一份小小的多模型描述文档出发,我们完整还原了 OpenGlass 的视觉评测机制:generate.ts 批量调度、imageDescription 统一提示词、ollamaInference 负责 Base64 编码与退避重试、trimIdent 负责文本整洁、Agent 负责把描述接入问答闭环。这份文档既是模型能力的"体检报告",也是理解整条"拍摄→描述→问答"链路的窗口——当你给 OpenGlass 戴上眼镜时,它眼中的世界,就长在 prompts/series_1/img_48.md 这样的描述里。
