OpenGlass 视觉模型批量评测:基于 moondream / llava 的多模态图片描述生成与输出对比
OpenGlass 视觉模型批量评测:基于 moondream / llava 的多模态图片描述生成与输出对比
OpenGlass 是一个把任意普通眼镜改造成 AI 智能眼镜的开源项目,其视觉能力的关键一环,是用本地多模态模型对眼镜拍摄的画面实时生成文字描述。本文以仓库中 prompts/series_1/img_45.md 这一实测样例为骨架,完整还原"一张图片被 4 个不同视觉模型描述"的生成流程,并结合 prompts/generate.ts、sources/agent/imageDescription.ts、sources/modules/ollama.ts 等源码,讲清批处理脚本如何工作、每个模型输出差异在哪、以及如何在你本地复现这套评测。读完本文,你将掌握 OpenGlass 的图片描述生成管线、Ollama 推理的调用细节,并能自己跑通多模型对比实验。
一、样例文档是什么:一份四模型输出对比报告
prompts/series_1/img_45.md 是 OpenGlass 批量视觉评测脚本对第 45 张测试图(img_45.jpeg,600×800 室内场景)生成的描述记录。文档本身不含任何人工撰写内容,而是由 4 段被 ####标题#### 分隔的输出拼接而成,每段对应一个模型的独立输出:
| 文档小节标题 | 使用的视觉模型 | 输出要点 |
|---|---|---|
####Description#### |
默认模型 moondream:1.8b-v2-fp16 |
拱门下的多人交谈场景,画面左中部有一台疑似笔记本的设备 |
####Description (llava-llama3)#### |
llava-llama3 |
室内白墙灰顶、大拱门、自然光,两人站在拱门旁,一人坐在带显示器的桌前 |
####Description (llava:34b-v1.6)#### |
llava:34b-v1.6 |
工业感室内空间,高天花板、管道与天窗,一人在信息自助机前,另有人在长椅上 |
####Description (moondream:1.8b-v2-fp16)#### |
moondream:1.8b-v2-fp16(第二次运行) |
白色墙面与拱廊的开放式建筑内,有人在打电话,多人分散站立 |
这份文档的价值在于:它本身就是一份可复现、可对比的视觉模型评测数据。同一张图,不同模型在"是否识别出人物动作""是否注意到桌面设备""如何理解建筑空间"上给出了不同的答案,直接反映了小体积视觉模型(moondream 约 1.8B 参数)与大参数视觉模型(llava:34b-v1.6)之间的描述粒度差异。原图 img_45.jpeg 展示了画面中真实存在的拱券、采光窗、多人站立及左侧带标识的白色设备,可作为人工核对模型描述准确性的基准。
二、这些描述是怎么批量生成的:generate.ts 全流程拆解
prompts/series_1 目录下的 57 组 img_N.jpeg / img_N.md 文件并非手工整理,而是由 prompts/generate.ts 一次性批量生成的。理解这个脚本,就理解了整个评测数据集的来源。
2.1 自动发现图片,建立任务队列
脚本启动后遍历 __dirname(即 prompts 目录)下的所有子目录,把每个以 .jpeg 结尾的图片文件收集起来,并将输出路径映射为同名的 .md 文件:
let allFiles = fs.readdirSync(__dirname);
for (let f of allFiles) {
if (fs.statSync(path.join(__dirname, f)).isDirectory()) {
let files = fs.readdirSync(path.join(__dirname, f));
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: ''
});
}
}
}
}
要点:任务队列 imageTests 中每个元素同时持有图片 Buffer和目标 .md 输出路径;outputs 字段用于在内存中累积所有模型的输出文本,最后一次性写盘。
2.2 多模型顺序跑批,输出拼接到同一文档
核心的 runTest(title, test) 函数负责按序对每张图调用一次指定的模型,并把结果以 ####标题#### 前缀追加到 outputs:
async function runTest(title: string, test: (img: Uint8Array) => Promise<string>) {
let bar = new progress.SingleBar({}, progress.Presets.shades_classic);
bar.start(imageTests.length, 0);
for (let i = 0; i < imageTests.length; i++) {
let o = await test(imageTests[i].image);
imageTests[i].outputs += '####' + title + '####\n';
imageTests[i].outputs += o + '\n';
bar.increment();
}
bar.stop();
}
可以看到,img_45.md 中那 4 个以 #### 开头的小节标题,正是这里拼接进去的。脚本随后依次注册了 4 轮评测:
await runTest('Description', (img) => imageDescription(img)); // 默认 moondream:1.8b-v2-fp16
await runTest('Description (llava-llama3)', (img) => imageDescription(img, 'llava-llama3'));
await runTest('Description (llava:34b-v1.6)', (img) => imageDescription(img, 'llava:34b-v1.6'));
await runTest('Description (moondream:1.8b-v2-fp16)', (img) => imageDescription(img, 'moondream:1.8b-v2-fp16'));
这正是 img_45.md 中 4 个小节标题与顺序的直接来源。此外脚本中还保留了一段被注释掉的模糊检测评测(####Blurry####,调用 imageBlurry),说明该批数据集的原始设计包含"描述 + 质量检测"两类任务,模糊检测在正式生成时被禁用。
2.3 批量完成后的落盘
全部模型跑完后,脚本遍历队列将累积的 outputs 写入对应 .md 文件:
for (let i of imageTests) {
fs.writeFileSync(i.path, i.outputs);
}
因此 img_45.md 是纯脚本产物,没有任何人工编辑痕迹,这也保证了评测数据的客观性与可复现性:只要环境与模型一致,重跑脚本即可得到完全相同的输出。
三、单张图片如何被描述:imageDescription 与 Ollama 调用链
批量脚本最终会调用 sources/agent/imageDescription.ts 中定义的 imageDescription。该函数是"图片 → 文字描述"的入口,签名与默认模型如下:
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_45.md中 llava-llama3 提到"computer monitor and keyboard(显示器与键盘)"、moondream 提到"talking on his cell phone(正在打电话)"这类细节,正是受该提示约束的结果。 - 图片以
images: [src]数组形式随用户消息一起发送,即走的是 Ollama 的多模态 Chat API,而非纯文本补全接口。 model参数类型KnownModel在 sources/modules/ollama.ts 中定义为联合类型,当前仓库支持 7 个模型: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。其中llava-llama3与llava:34b-v1.6是描述评测的常用模型,moondream系列两个变体分别用于描述与模糊检测。
3.1 ollamaInference 的底层实现
实际发请求的逻辑在 ollamaInference(sources/modules/ollama.ts):
export async function ollamaInference(args: {
model: KnownModel,
messages: { role: 'system' | 'user' | 'assistant', content: string, images?: Uint8Array[] }[],
}) {
const response = await backoff<any>(async () => {
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 resp.data;
});
return trimIdent(((response.message?.content ?? '') as string));
}
其关键机制包括:
- 图片二进制转 Base64:
message.images中的Uint8Array通过 sources/utils/base64.ts 的toBase64逐张转为字符串后随请求发送,这是 Ollama 多模态请求的标准做法。 - 文本清理:
trimIdent(sources/utils/trimIdent.ts)会去掉提示词中的缩进空白,避免多余空格进入模型输入;返回的模型输出同样经过trimIdent裁剪,保证写入 .md 的描述干净规整。 - 请求地址来自 keys.ts:
axios.post(keys.ollama, ...)中的 URL 由 sources/keys.ts 提供,即环境变量EXPO_PUBLIC_OLLAMA_API_URL。按 README.md 的说明,Ollama 需自托管,URL 形如http://localhost:11434/api/chat,且要在终端先执行ollama pull moondream:1.8b-v2-fp16拉取模型。 - 重试与流式关闭:整个请求被
backoff(sources/utils/time.ts)包裹,失败时按退避策略重试;请求体显式设置stream: false,等待完整 JSON 响应,再从中读取response.message?.content作为最终文本。
四、输出差异背后的原因:四个模型各自的定位
对照 img_45.md 的四段输出,可以归纳出 OpenGlass 项目中这几个视觉模型的真实差异(这些差异与各模型参数规模、训练数据的定位一致,也解释了为何脚本要同时评测多个模型):
- moondream:1.8b-v2-fp16:轻量、可运行在低端硬件上的默认描述模型。输出偏"事实清单"风格——先给人数,再给位置与可疑物体("five individuals""an object that resembles a laptop"),语言直白、确定性表达多("This could indicate"),适合眼镜端低延迟推理场景。
- llava-llama3:基于 Llama 3 的 LLaVA 变体,描述更注重场景关系与氛围("casual yet focused atmosphere"),能捕捉"坐在桌前、面前有显示器与键盘"这样的空间布局。
- llava:34b-v1.6:参数规模最大的模型,输出最具"推理感",把空间解读为"waiting area or a transit hub(候车区或换乘枢纽)",并推断人物"seeking instructions or about to interact(在寻求指引或即将操作)"。它给出了最丰富的语义解读,但部分推断(如自助机)属于模型假设而非文档确认的事实。
- moondream:1.8b-v2-fp16(文档第四段):与第一段同一模型,但输出侧重人物动作细节("talking on his cell phone"),这提醒我们:同一模型在不同运行下输出会有波动,做评测时应多次采样或结合人工核对,这正是
img_45.md中同模型出现两段的原因之一。
4.1 若需要"看图问答"而非"描述":llamaFind / openAIFind
imageDescription 只负责生成描述。当需要基于描述回答具体问题时,仓库在 sources/agent/imageDescription.ts 中还提供了 llamaFind(走 Groq 的 llama3-70b-8192,见 sources/modules/groq-llama3.ts)与 openAIFind(走 OpenAI gpt-4o,见 sources/modules/openai.ts)。两者的系统提示词要求模型"只依据图片描述作答、不泛化、不提及图片本身、简洁具体"——即先由视觉模型把图像转成文本,再由大语言模型做事实问答的两段式管线。这与"图片描述 + 文本推理"的架构定位一致,可作为理解整套视觉链路的延伸阅读。
五、如何在你本地复现这套评测
以下步骤基于仓库当前状态与 README.md 的说明,可完整重现 img_45.md 的生成过程:
- 准备本地 Ollama 环境:自托管 Ollama REST API(
http://localhost:11434/api/chat),并按评测所需拉取模型:ollama pull moondream:1.8b-v2-fp16 ollama pull llava-llama3 ollama pull llava:34b-v1.6 - 配置 API 地址:在 sources/keys.ts 中通过环境变量注入,例如:
export EXPO_PUBLIC_OLLAMA_API_URL=http://localhost:11434/api/chat - 安装依赖:在仓库根目录执行
npm install(或yarn install)。 - 准备评测图片:将待测图片放入
prompts下的任一子目录(如prompts/series_1/),命名以.jpeg结尾,脚本会自动发现。 - 运行生成脚本:
脚本会依次执行 4 轮评测,每轮用进度条显示进度,完成后为每张图片生成同名npx tsx prompts/generate.ts.md文件,格式与img_45.md完全一致。
注意:脚本依赖本地已拉取全部模型,若某个模型缺失,对应轮次会失败;llava:34b-v1.6 体积较大,首次加载耗时明显,属正常现象。
六、总结
prompts/series_1/img_45.md 表面上是 57 份描述文件中的普通一份,实则是 OpenGlass 视觉评测管线的完整缩影:generate.ts 负责批量发现图片并驱动多轮模型评测,imageDescription 统一封装"精确描述 + 转录文字"的提示词,ollamaInference 完成 Base64 编码、退避重试与结果清洗,最终把 moondream 与 llava 系列模型的差异化输出沉淀为可对比、可复现的 Markdown 数据。对希望复现或扩展该评测、或者想为智能眼镜挑选更合适视觉模型的开发者而言,这份样例文档连同其背后的源码,就是一份可以直接上手的最小工作集。
(本文基于仓库 prompts/series_1/img_45.md 及其生成脚本 prompts/generate.ts、推理封装 sources/agent/imageDescription.ts 与 sources/modules/ollama.ts 撰写;文中所有模型行为描述均来自该样例文档输出,未对模型性能作任何外部宣称。)
