Web 集成
学习如何将 Tiny-DL-Inference 集成到 Web 应用程序中,在浏览器中进行实时推理。
概述
本示例演示如何构建完整的 Web 应用程序,实现:
- 通过 Fetch API 加载神经网络模型
- 使用 Canvas 处理来自文件输入的图像
- 在 WebGPU 上运行推理
- 显示带计时信息的预测结果
源代码包括一个 HTML 文件和一个 TypeScript 模块(app.ts)。
HTML 设置
创建包含文件输入和结果展示区域的基本 HTML 页面:
html
<!DOCTYPE html>
<html>
<head>
<title>Web 推理演示</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#status {
margin: 20px 0;
padding: 10px;
font-family: monospace;
background: #f5f5f5;
border-radius: 4px;
}
#result {
margin: 20px 0;
padding: 15px;
background: #f0f0f0;
border-radius: 4px;
}
#result h3 {
margin-top: 0;
}
.prediction {
margin: 5px 0;
}
</style>
</head>
<body>
<h1>Tiny-DL-Inference Web 演示</h1>
<input type="file" id="imageInput" accept="image/*">
<div id="status">就绪</div>
<div id="result"></div>
<script type="module" src="app.js"></script>
</body>
</html>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
TypeScript 模块
创建包含推理逻辑的 app.ts:
步骤 1:初始化应用程序
typescript
import { InferenceEngine } from 'tiny-dl-inference';
class WebInferenceDemo {
private engine: InferenceEngine;
private statusEl: HTMLElement;
private resultEl: HTMLElement;
constructor() {
this.engine = new InferenceEngine();
this.statusEl = document.getElementById('status')!;
this.resultEl = document.getElementById('result')!;
}
async initialize() {
this.setStatus('正在初始化 WebGPU...');
try {
await this.engine.initialize();
this.setStatus('正在加载模型...');
const model = await this.loadModel();
await this.engine.loadModel(model);
this.setStatus('已就绪,可进行推理');
this.setupEventListeners();
} catch (error: unknown) {
const message = error instanceof Error ? error.message : '未知错误';
this.setStatus(`错误:${message}`);
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
步骤 2:加载模型
typescript
private async loadModel() {
const response = await fetch('model.json');
return await response.json();
}1
2
3
4
2
3
4
模型文件应遵循自定义模型加载中描述的格式。
步骤 3:处理图像输入
typescript
private setupEventListeners() {
const input = document.getElementById('imageInput') as HTMLInputElement;
input.addEventListener('change', (e) => this.handleImageSelect(e));
}
private async handleImageSelect(event: Event) {
const file = (event.target as HTMLInputElement).files?.[0];
if (!file) return;
this.setStatus('正在处理图像...');
try {
const imageData = await this.loadImage(file);
const input = this.engine.tensorFromArray(imageData, [1, 3, 224, 224]);
const start = performance.now();
const output = await this.engine.infer(input);
const end = performance.now();
const predictions = await output.download();
this.displayResults(predictions, end - start);
this.setStatus('推理完成');
} catch (error: unknown) {
const message = error instanceof Error ? error.message : '未知错误';
this.setStatus(`错误:${message}`);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
步骤 4:加载和处理图像
typescript
private async loadImage(file: File): Promise<Float32Array> {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = 224;
canvas.height = 224;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0, 224, 224);
const imageData = ctx.getImageData(0, 0, 224, 224);
// 将 RGBA 转换为 RGB 并归一化到 [0, 1]
const floatData = new Float32Array(3 * 224 * 224);
for (let i = 0; i < 224 * 224; i++) {
floatData[i] = imageData.data[i * 4] / 255.0; // R
floatData[i + 224 * 224] = imageData.data[i * 4 + 1] / 255.0; // G
floatData[i + 2 * 224 * 224] = imageData.data[i * 4 + 2] / 255.0; // B
}
URL.revokeObjectURL(img.src);
resolve(floatData);
};
img.onerror = () => {
URL.revokeObjectURL(img.src);
reject(new Error('图像加载失败'));
};
img.src = URL.createObjectURL(file);
});
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
步骤 5:显示结果
typescript
private displayResults(predictions: Float32Array, timeMs: number) {
// 获取前 3 个预测
const top3 = Array.from(predictions)
.map((prob, idx) => ({ class: idx, prob }))
.sort((a, b) => b.prob - a.prob)
.slice(0, 3);
this.resultEl.innerHTML = `
<h3>结果 (${timeMs.toFixed(2)}ms)</h3>
${top3.map(item => `
<div class="prediction">
类别 ${item.class}: ${(item.prob * 100).toFixed(2)}%
</div>
`).join('')}
`;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
步骤 6:工具方法
typescript
private setStatus(message: string) {
this.statusEl.textContent = message;
}
destroy() {
this.engine.destroy();
}
}
// 初始化应用程序
const demo = new WebInferenceDemo();
demo.initialize();1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
核心概念
WebGPU 初始化
在任何张量操作之前必须初始化 WebGPU:
typescript
await this.engine.initialize();1
这会检查 WebGPU 支持并创建 GPU 设备。
图像预处理
图像必须经过以下处理:
- 调整大小到模型期望的输入尺寸(如 224x224)
- 转换从 RGBA 到模型的通道格式(如 NCHW 布局的 RGB)
- 归一化到模型期望的范围(通常是 [0, 1] 或 [-1, 1])
NCHW 数据布局
示例将图像转换为 NCHW 布局(通道优先):
typescript
// NCHW:所有 R 值,然后所有 G 值,最后所有 B 值
floatData[i] = imageData.data[i * 4] / 255.0; // R 通道
floatData[i + 224 * 224] = imageData.data[i * 4 + 1] / 255.0; // G 通道
floatData[i + 2 * 224 * 224] = imageData.data[i * 4 + 2] / 255.0; // B 通道1
2
3
4
2
3
4
对于 NHWC 布局(通道最后),数据会交错排列:R0, G0, B0, R1, G1, B1, ...
错误处理
始终将推理调用包裹在 try/catch 块中:
typescript
try {
const output = await this.engine.infer(input);
// 处理结果
} catch (error) {
// 优雅地处理错误
}1
2
3
4
5
6
2
3
4
5
6
性能技巧
- 复用引擎 - 初始化一次,用于多次推理
- 尽可能批处理 - 在单个张量中处理多张图像
- 及时清理 - 完成后销毁张量和引擎
- 预热 GPU - 在计前运行一次虚拟推理以避免冷启动开销
浏览器兼容性
| 浏览器 | 最低版本 | 备注 |
|---|---|---|
| Chrome | 113+ | 推荐 |
| Edge | 113+ | 推荐 |
| Safari | 18+ | macOS Sonoma+ |
检查 WebGPU 支持:
typescript
if (!navigator.gpu) {
console.error('此浏览器不支持 WebGPU');
// 显示备用消息或重定向
}1
2
3
4
2
3
4