浏览器端图像识别实战:从用户上传到实时检测的完整工作流
最近一年,在浏览器里跑 AI 模型从"科研项目"变成了"可实用的产品功能"。
原因是两个:
- TensorFlow.js、ONNX Runtime 等库成熟了
- 模型优化(量化、剪枝)让大模型可以跑在 GPU 上
这项技术的实际应用场景也很清晰:
- 在线工具:文件格式转换、图片编辑、证件识别
- 内容审核:用户上传图片的秒级检查
- 电商应用:拍照搜索、商品识别
本文就讲怎样从 0 到 1 搭建一个实用的"图像识别系统"。
1. 完整工作流设计
一个典型的浏览器端图像识别应该包括:
用户上传/拍照 → 图像预处理 → 加载模型 → 实时推理 → 结果可视化 → 可选上传结果
每个环节都有优化空间和常见坑。
2. 图像上传与预处理
方案 A:文件输入
<input type="file" id="imageInput" accept="image/*" />
<canvas id="preview"></canvas>
document.getElementById('imageInput').addEventListener('change', async (e) => {
const file = e.target.files[0]
const img = new Image()
img.onload = () => {
// 预处理图像
const canvas = document.getElementById('preview')
preprocessImage(img, canvas)
}
img.src = URL.createObjectURL(file)
})
function preprocessImage(img, canvas) {
const ctx = canvas.getContext('2d')
canvas.width = 640 // 对应模型的输入尺寸
canvas.height = 480
// 缩放调整长宽比
const scale = Math.min(canvas.width / img.width, canvas.height / img.height)
const x = (canvas.width - img.width * scale) / 2
const y = (canvas.height - img.height * scale) / 2
ctx.fillStyle = 'white'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(img, x, y, img.width * scale, img.height * scale)
}
方案 B:摄像头实时推理
async function setupCamera() {
const video = document.getElementById('video')
const stream = await navigator.mediaDevices.getUserMedia({
video: { width: 640, height: 480 }
})
video.srcObject = stream
return new Promise((resolve) => {
video.onloadedmetadata = () => resolve(video)
})
}
async function detectInRealTime(model, video) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const detect = async () => {
ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight)
const tensor = tf.browser.fromPixels(canvas)
const predictions = await model.estimateObjects(tensor)
visualizeResults(predictions, canvas, ctx)
tensor.dispose()
requestAnimationFrame(detect)
}
detect()
}
3. 模型选择与加载
轻量级模型(快速,精度稍低)
import * as cocoSsd from '@tensorflow-models/coco-ssd'
async function loadModel() {
const model = await cocoSsd.load()
return model
}
推理速度:50-100ms(GPU),适合实时应用。
精准模型(更准,更慢)
// YOLOv8 WebGL 版本(需要 ONNX runtime)
const session = await ort.InferenceSession.create('yolov8n.onnx')
推理速度:200-500ms,更适合离线处理。
选择建议:
| 场景 | 推荐模型 | 延迟 |
|---|---|---|
| 实时视频检测 | COCO-SSD | 50ms |
| 静态图检测 | YOLOv8 nano | 200ms |
| 高精度要求 | YOLOv8 small | 400ms |
4. 推理与结果处理
async function runInference(model, imageData) {
// 推理
const predictions = await model.estimateObjects(imageData)
// 过滤低置信度结果
const filtered = predictions.filter(p => p.score > 0.5)
// 返回结果
return filtered
}
// 结果结构:
// [
// {
// class: 'person',
// score: 0.96,
// bbox: [x, y, width, height]
// },
// ...
// ]
关键优化:
// 使用 tf.tidy() 自动管理内存
const results = tf.tidy(() => {
const tensor = tf.browser.fromPixels(canvas)
const normalized = tensor.div(255.0)
const predictions = model.estimateObjects(normalized)
return predictions
})
// tensor 和 normalized 自动被释放
5. 结果可视化
function visualizeResults(predictions, canvas, ctx) {
// 清空画布,重新绘制图像
ctx.clearRect(0, 0, canvas.width, canvas.height)
predictions.forEach(pred => {
const [x, y, w, h] = pred.bbox
// 绘制边界框
ctx.strokeStyle = '#00FF00'
ctx.lineWidth = 2
ctx.strokeRect(x, y, w, h)
// 绘制标签和置信度
const label = `${pred.class} ${(pred.score * 100).toFixed(1)}%`
ctx.fillStyle = '#00FF00'
ctx.font = '14px Arial'
ctx.fillText(label, x, y - 5)
})
}
6. 性能优化与监控
问题:实时检测可能很耗 CPU/GPU。
监控代码:
let frameCount = 0
let lastTime = performance.now()
const detect = async () => {
const start = performance.now()
// 检测代码
const predictions = await model.estimateObjects(tensor)
const duration = performance.now() - start
console.log(`Inference time: ${duration.toFixed(1)}ms`)
// 监控 FPS
frameCount++
const now = performance.now()
if (now - lastTime >= 1000) {
console.log(`FPS: ${frameCount}`)
frameCount = 0
lastTime = now
}
requestAnimationFrame(detect)
}
优化技巧:
- 降低推理频率(每 3 帧推理一次,中间帧用缓存结果)
- 降低输入分辨率
- 用量化模型(更小,更快)
- 启用 GPU(webgl 后端)
// 每 3 帧推理一次
let frameCounter = 0
const detect = async () => {
if (frameCounter++ % 3 === 0) {
predictions = await model.estimateObjects(tensor)
}
visualizeResults(predictions, canvas, ctx)
requestAnimationFrame(detect)
}
7. 与后端集成
识别结果可能需要反馈到后端:
// 用户确认检测结果后上传
async function uploadResults(predictions, originalImage) {
const formData = new FormData()
// 上传原始图片
formData.append('image', originalImage)
// 上传检测结果(而不是整个模型输出)
formData.append('results', JSON.stringify({
detections: predictions.map(p => ({
class: p.class,
score: p.score,
bbox: p.bbox
})),
timestamp: Date.now()
}))
const res = await fetch('/api/upload-detection', {
method: 'POST',
body: formData
})
return res.json()
}
最佳实践:
- 用户在浏览器端看到即时反馈
- 只上传最终结果,不上传原始图片(隐私)
- 后端可以验证或调整结果
8. 常见问题与解决方案
Q1:模型加载特别慢
A:预加载模型,或缓存到 IndexedDB
async function getModel() {
try {
return await tf.loadLayersModel(tf.io.indexedDB('my-model'))
} catch {
// IndexedDB 不存在,从网络加载
const model = await cocoSsd.load()
// 保存到 IndexedDB
await model.save(tf.io.indexedDB('my-model'))
return model
}
}
Q2:内存占用一直在增长
A:记得 dispose tensor
// ❌ 内存泄漏
const tensor = tf.browser.fromPixels(canvas)
const predictions = model.estimate(tensor)
// ✅ 正确
const predictions = tf.tidy(() => {
const tensor = tf.browser.fromPixels(canvas)
return model.estimate(tensor)
})
Q3:在手机上特别卡
A:降低分辨率和推理频率
// 手机上调整参数
const isMobile = /iPhone|iPad|Android/.test(navigator.userAgent)
const inferenceFrequency = isMobile ? 2 : 1 // 手机上每 2 帧推理一次
const inputSize = isMobile ? [320, 240] : [640, 480]
9. 完整示例代码
看 GitHub 的完整实现:
10. 最佳实践清单
- 选择合适的模型(速度 vs 精度的平衡)
- 预处理图像(缩放、归一化)
- 用 tf.tidy() 管理内存
- 监控推理时间和 FPS
- 在移动设备上测试
- 实现渐进式加载(用户等待时显示进度)
- 可选上传结果到后端进行验证


