16.08-图片OCR
要点
- OCR 在 AI 项目里的典型位置:扫描文档、截图、照片中的文字提取,作为 LLM 处理的前置步骤
- Workers 环境的限制——Tesseract.js 的 Node.js 依赖和 sharp 的原生模块都跑不了,只能走 API 调用
- 三条路径:Workers AI 视觉模型、多模态 LLM(GPT-4o / Claude)、外部 OCR API(Google Vision / Textract)
- 图片预处理和质量优化对 OCR 精度影响大——分辨率、对比度、格式选择都需要处理
1. AI 项目里 OCR 的位置
AI 项目里,图片输入通常有两条路径:
路径一:图片 → OCR → 文本 → LLM 处理
路径二:图片 → 多模态 LLM → 直接输出结果路径一适用于图片中主要是文字的场景:文档扫描件、应用截图、纸质表单。OCR 先把文字提取出来,再交给 LLM 做语义处理。好处是成本低、速度快,而且能保留文字的空间位置信息(表格结构、行列布局)。
路径二适用于需要理解图片内容的场景:流程图、架构图、带标注的 UI 截图。多模态 LLM 不只提取文字,还能理解视觉关系。代价是成本高、延迟大。
两条路径可以组合:OCR 提取文字,多模态 LLM 理解图片上下文,OCR 结果作为补充 token 喂给模型,提高文字部分的准确度。
2. Workers 环境的限制
先说清楚约束。Workers 运行在 V8 Isolate 上,不是完整的 Node.js 环境。这带来几个直接影响:
- Tesseract.js 不能用 — 虽然 Tesseract.js 有 WASM 构建版本,但它的默认依赖链需要 Node.js 的
fs、path等模块。在 Workers 里直接import { createWorker } from 'tesseract.js'会报错 - sharp 不能用 — sharp 依赖原生 C++ 绑定(libvips),Workers 不支持原生模块
- 内存有限 — Workers 的单次请求内存上限是 128 MB,处理高分辨率图片时需要注意
在 Workers 里做 OCR,能走的方向:
- 用 Workers AI 提供的视觉模型(纯 API 调用,不需要本地依赖)
- 调用外部 LLM API(GPT-4o、Claude),图片以 base64 或 URL 形式传过去
- 调用外部 OCR API(Google Vision、AWS Textract)
- 图片预处理放到客户端做,Workers 只负责接收和转发
3. 方案 A:Workers AI 视觉模型
Cloudflare Workers AI 提供了视觉模型,可以直接在 Worker 里跑 OCR,不需要调用外部 API。
// src/routes/ocr.ts
import { Hono } from 'hono'
import type { AppEnv } from '../types'
export const ocrApp = new Hono<AppEnv>()
ocrApp.post('/ocr/workers-ai', async (c) => {
const body = await c.req.parseBody()
const file = body['image']
if (!(file instanceof File)) {
return c.json({ error: '请上传图片文件' }, 400)
}
const buffer = await file.arrayBuffer()
const imageArray = [...new Uint8Array(buffer)]
// Workers AI 视觉模型识别
const ai = c.env.AI
const response = await ai.run(
'@cf/unum/uform-gen2-qwen-500m',
{
image: imageArray,
prompt: '请提取图片中的所有文字内容,保持原有格式输出。',
}
)
// response 包含模型生成的文本
const text = (response as any).description ?? ''
return c.json({
text,
source: 'workers-ai',
charCount: text.length,
})
})Workers AI 的好处:数据不出 Cloudflare 网络,延迟低,不需要额外的 API key。
需要注意的是,Workers AI 的模型列表会更新,具体可用的 OCR 模型以 Cloudflare 文档 为准。调用时 image 参数是字节数组,prompt 字段用来指定识别要求。
4. 方案 B:多模态 LLM 直接识别
把图片发给 GPT-4o 或 Claude,让模型同时处理视觉和文字。精度最高,能理解复杂排版、手写体、中英文混合。
// src/lib/llm-ocr.ts
// 把图片 buffer 转成 base64 字符串(分段拼接,避免大文件时 call stack 溢出)
function bufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
for (let i = 0; i < bytes.length; i += 8192) {
binary += String.fromCharCode(...bytes.subarray(i, i + 8192))
}
return btoa(binary)
}
export async function ocrWithGPT4o(
imageBuffer: ArrayBuffer,
apiKey: string,
): Promise<string> {
const base64 = bufferToBase64(imageBuffer)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: '提取图片中的所有文字内容。保持原有格式和布局。表格用 Markdown 表格格式输出。',
},
{
role: 'user',
content: [
{
type: 'image_url',
image_url: { url: `data:image/png;base64,${base64}` },
},
],
},
],
max_tokens: 4096,
}),
})
const data = await response.json()
return data.choices?.[0]?.message?.content ?? ''
}Claude 的调用方式类似,区别在端点和消息格式:用 https://api.anthropic.com/v1/messages,消息里 content 包含一个 type: 'image' 的 block,图片数据放在 source.data 里(base64),配合一个 type: 'text' 的 block 写 prompt。
多模态 LLM 做 OCR 的权衡:
- 精度最高:复杂排版、手写体、中英文混合都能处理好
- 成本最高:一张 1000×1000 的图约 1000 input tokens,按 GPT-4o 的价格算大约 $0.0025 / 张
- 延迟最大:每次调用涉及完整的 LLM 推理,通常 2-5 秒
- 适合需要语义理解的场景:不只是提取文字,还要理解文字含义和上下文
5. 方案 C:外部 OCR API
如果只需要提取文字(不需要语义理解),传统 OCR API 更合适——速度快、成本低。
Google Cloud Vision
// src/lib/google-vision-ocr.ts
export async function ocrWithGoogleVision(
imageBuffer: ArrayBuffer,
apiKey: string,
): Promise<{ text: string; confidence: number }> {
const base64 = bufferToBase64(imageBuffer)
const response = await fetch(
`https://vision.googleapis.com/v1/images:annotate?key=${apiKey}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requests: [
{
image: { content: base64 },
features: [
{ type: 'TEXT_DETECTION' },
{ type: 'DOCUMENT_TEXT_DETECTION' }, // 更精确的文档 OCR
],
imageContext: {
languageHints: ['zh', 'en'], // 指定语言,提高识别精度
},
},
],
}),
},
)
const data = await response.json()
const annotation = data.responses?.[0]?.fullTextAnnotation
return {
text: annotation?.text ?? '',
confidence: annotation?.pages?.[0]?.confidence ?? 0,
}
}AWS Textract
Textract 的 REST API 端点是 Textract.DetectDocumentText,传入 base64 编码的图片,返回按行组织的文本块。
不过 Textract 需要 AWS Signature V4 签名。生产环境建议直接用 @aws-sdk/client-textract:
import { TextractClient, DetectDocumentTextCommand } from '@aws-sdk/client-textract'
const client = new TextractClient({ region: 'us-east-1' })
const command = new DetectDocumentTextCommand({
Document: { Bytes: new Uint8Array(imageBuffer) },
})
const result = await client.send(command)
const text = result.Blocks?.filter(b => b.BlockType === 'LINE').map(b => b.Text).join('\n') ?? ''SDK 会处理签名、重试和分页,省去自己实现 Signature V4 的麻烦。
6. 图片预处理
图片质量对 OCR 精度影响很大。Workers 里跑不了 sharp,预处理有几个替代方向。
base64 编码和格式转换
// src/lib/image-utils.ts
// 安全的 base64 编码——避免大文件时 call stack 溢出
export function bufferToBase64(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
let binary = ''
const chunkSize = 8192
for (let i = 0; i < bytes.length; i += chunkSize) {
binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize))
}
return btoa(binary)
}
// 检查图片格式并返回 MIME type
export function detectImageType(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
// PNG: 89 50 4E 47
if (bytes[0] === 0x89 && bytes[1] === 0x50) return 'image/png'
// JPEG: FF D8 FF
if (bytes[0] === 0xFF && bytes[1] === 0xD8) return 'image/jpeg'
// WebP: 52 49 46 46 ... 57 45 42 50
if (bytes[0] === 0x52 && bytes[4] === 0x57) return 'image/webp'
return 'application/octet-stream'
}客户端预处理
Workers 里跑不了 sharp,图片预处理放到客户端做。用 Canvas API 处理后再上传:
// 客户端代码:上传前压缩 + 灰度化
function preprocessImage(file: File): Promise<Blob> {
return new Promise((resolve) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement('canvas')
// 限制最大宽度 2000px
const maxWidth = 2000
const scale = Math.min(1, maxWidth / img.width)
canvas.width = img.width * scale
canvas.height = img.height * scale
const ctx = canvas.getContext('2d')!
ctx.drawImage(img, 0, 0, canvas.width, canvas.height)
// 灰度化 + 二值化:提升文字对比度
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
for (let i = 0; i < imageData.data.length; i += 4) {
const gray = imageData.data[i] * 0.299 + imageData.data[i + 1] * 0.587 + imageData.data[i + 2] * 0.114
const v = gray > 128 ? 255 : 0
imageData.data[i] = imageData.data[i + 1] = imageData.data[i + 2] = v
}
ctx.putImageData(imageData, 0, 0)
// 转 JPEG,quality 0.85 是精度和体积的平衡点
canvas.toBlob((blob) => resolve(blob!), 'image/jpeg', 0.85)
}
img.src = URL.createObjectURL(file)
})
}预处理后图片体积更小,OCR API 响应更快,精度也更高。
7. OCR 质量优化
影响 OCR 精度的几个因素:
- 分辨率 — 打印体英文 150 DPI 就够用,中文建议 300 DPI 以上。图片宽度低于 1000px 时,中文识别精度会下降
- 对比度 — 文字和背景对比度高,识别准确率高。扫描件背景发灰、发黄的,做一下灰度化 + 二值化
- 倾斜角度 — 拍照时倾斜超过 5° 就会影响识别率。传统 OCR 引擎通常内置倾斜校正,LLM 方案的容错性更好
- 语言指定 — 告诉 OCR 引擎识别哪种语言,能避免误识别。Google Vision 的
imageContext.languageHints支持指定['zh', 'en']这样的语言组合 - 格式选择 — 线条文字、截图用 PNG(无损);照片用 JPEG(体积小);带透明通道的用 WebP。发给 OCR API 之前,确认对方支持的格式
8. 完整示例:上传图片 → OCR → 返回文本
把前面的内容串起来,一个完整的 OCR 路由。支持三种模式,通过 mode 参数切换:
// src/routes/ocr.ts
import { Hono } from 'hono'
import type { AppEnv } from '../types'
import { bufferToBase64, detectImageType } from '../lib/image-utils'
export const ocrApp = new Hono<AppEnv>()
const MAX_IMAGE_SIZE = 10 * 1024 * 1024
ocrApp.post('/ocr', async (c) => {
const body = await c.req.parseBody()
const file = body['image']
if (!(file instanceof File)) {
return c.json({ error: '请上传图片文件' }, 400)
}
if (file.size > MAX_IMAGE_SIZE) {
return c.json({ error: '图片大小不能超过 10MB' }, 400)
}
const buffer = await file.arrayBuffer()
const mimeType = detectImageType(buffer)
if (!mimeType.startsWith('image/')) {
return c.json({ error: '不支持的图片格式' }, 400)
}
const mode = (body['mode'] as string) || 'llm'
let text = ''
let source = ''
if (mode === 'workers-ai') {
// 方案 A:Workers AI 视觉模型
const ai = c.env.AI
const result = await ai.run('@cf/unum/uform-gen2-qwen-500m', {
image: [...new Uint8Array(buffer)],
prompt: '请提取图片中的所有文字内容,保持原有格式输出。',
})
text = (result as any).description ?? ''
source = 'workers-ai'
} else if (mode === 'gpt4o') {
// 方案 B:GPT-4o 多模态
const base64 = bufferToBase64(buffer)
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${c.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: 'gpt-4o',
messages: [
{ role: 'system', content: '提取图片中的所有文字,保持原有格式。' },
{ role: 'user', content: [{ type: 'image_url', image_url: { url: `data:${mimeType};base64,${base64}` } }] },
],
max_tokens: 4096,
}),
})
const data = await response.json()
text = data.choices?.[0]?.message?.content ?? ''
source = 'gpt-4o'
} else {
// 方案 C:Google Vision
const base64 = bufferToBase64(buffer)
const response = await fetch(
`https://vision.googleapis.com/v1/images:annotate?key=${c.env.GOOGLE_VISION_KEY}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
requests: [{
image: { content: base64 },
features: [{ type: 'DOCUMENT_TEXT_DETECTION' }],
imageContext: { languageHints: ['zh', 'en'] },
}],
}),
},
)
const data = await response.json()
text = data.responses?.[0]?.fullTextAnnotation?.text ?? ''
source = 'google-vision'
}
return c.json({ text, charCount: text.length, source })
})挂载到入口文件:
// src/index.ts
import { ocrApp } from './routes/ocr'
app.route('/api', ocrApp)客户端调用:
const formData = new FormData()
formData.append('image', file)
formData.append('mode', 'gpt4o') // 可选:workers-ai / gpt4o / vision
const { text, charCount, source } = await fetch('/api/ocr', {
method: 'POST',
body: formData,
}).then(r => r.json())9. 方案选择参考
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 文档扫描件(大批量) | Google Vision / Textract | 成本低,速度快,精度高 |
| 截图文字提取 | GPT-4o / Claude | 能理解布局,处理复杂排版 |
| 表单字段提取 | Google Vision(表单专用) | 结构化提取,精度高 |
| 图片中的表格 | GPT-4o / Claude | 传统 OCR 难以理解表格结构 |
| 手写文字识别 | GPT-4o / Claude | 手写识别传统方案精度低 |
| Workers 内部闭环 | Workers AI | 数据不出 Cloudflare,延迟最低 |
| 发票 / 收据 | 专用票据 OCR | 结构化字段提取,精度最高 |
成本参考(以 1000 张图片计):Google Vision TEXT_DETECTION 约 $1.50,AWS Textract 约 $1.00-1.50,GPT-4o(按 ~1000 token / 张)约 $2.50,Workers AI 包含在配额内无额外按量费用。
总结
回顾这篇的要点:
- Workers 环境的限制决定了 OCR 实现路径——Tesseract.js 和 sharp 都用不了,走 API 调用是唯一出路
- Workers AI 提供视觉模型,能在 Worker 内完成 OCR,数据不出 Cloudflare 网络
- 多模态 LLM(GPT-4o / Claude)精度最高,适合复杂场景——手写体、表格、混合语言
- 传统 OCR API(Google Vision / Textract)成本低速度快,适合批量文档处理
- 图片预处理放在客户端做——Workers 里没有原生图片处理能力
- 分辨率、对比度、语言指定是影响 OCR 精度的三个关键因素
- 方案选择按场景定:批量文档用传统 OCR,复杂场景用多模态 LLM,内部闭环用 Workers AI