# Paper Recognizer WASM
尿检试纸图像识别模块,OpenCV C++ 编译为 WebAssembly,适用于浏览器和微信小程序。
## 目录结构
```
├── src/
│ ├── PaperRecognizer.cpp/h # 核心识别算法
│ ├── ColorReader.cpp/h # 色块颜色读取
│ ├── PaperConfig.cpp/h # 试纸规格配置
│ ├── emsc_bridge.cpp # Emscripten 桥接(对外 API)
│ ├── Types.h # 类型定义
│ └── GlobalParams.h # 全局常量
├── web/
│ ├── paper_recognizer.js # JS 胶水代码(编译产物)
│ └── paper_recognizer.wasm # WASM 模块(编译产物)
├── build.sh # 编译脚本
├── test.js # Node.js 测试
└── README.md # 本文件
```
## 环境要求
- **Emscripten SDK**(`emsdk`),安装于 `/home/ubuntu/emsdk`
- **OpenCV** 已编译为 WASM 静态库,位于 `/home/ubuntu/opencv/build_mini_os/`
如果环境路径不同,请修改 `build.sh` 中的 `EMSDK_DIR` 和 `OPENCV_DIR`。
## 编译方法
修改 `src/` 下的代码后,执行:
```bash
cd /home/ubuntu/nanocode/web-opencv/miniappA
./build.sh
```
产物输出到 `web/` 目录:
- `web/paper_recognizer.js` — JS 胶水代码
- `web/paper_recognizer.wasm` — WASM 二进制
### 编译说明
编译使用 `-Os` 优化级别,静态链接 OpenCV core + imgproc + imgcodecs,输出单文件 WASM。
关键编译选项:
- `ALLOW_MEMORY_GROWTH=1` — 允许 WASM 内存动态增长
- `MODULARIZE=1` — 模块化导出(异步加载)
- `FILESYSTEM=0` — 不包含文件系统(减小体积)
- `DISABLE_EXCEPTION_CATCHING=1` — 禁用 C++ 异常捕获
WASM 体积:1014KB(gzip 后约 348KB)。
## 使用方法
### 加载模块
```javascript
const Module = require('./web/paper_recognizer.js');
const m = await Module();
```
浏览器中使用:
```html
```
### API 说明
所有 API 通过 Module 上的函数调用,函数名带前导下划线。
#### `_recognize(data, width, height)`
执行试纸识别。
| 参数 | 类型 | 说明 |
|------|------|------|
| `data` | `number` (指针) | RGBA 像素数据在 WASM 堆中的指针 |
| `width` | `number` | 图像宽度 |
| `height` | `number` | 图像高度 |
返回 `0` 成功,`-1` 失败。
**调用前需要先将图像数据拷贝到 WASM 堆:**
```javascript
const pixels = new Uint8Array(imageData); // RGBA 格式
const ptr = m._malloc(pixels.length);
m.HEAPU8.set(pixels, ptr);
const ret = m._recognize(ptr, width, height);
```
#### `_getResultJson()`
返回识别结果的 JSON 字符串指针,使用 `m.UTF8ToString(ptr)` 读取。
返回的 JSON 结构:
```json
{
"success": true,
"errorCode": 0,
"errorMessage": "",
"paperType": "14_S",
"locatorColor": 0,
"itemCount": 14,
"smallLocatorOnLeft": true,
"blurLevel": 0,
"blurScore": 0.0,
"colorBlockCenters": [{ "x": 100.0, "y": 200.0 }, ...],
"colors": [
{
"id": 0,
"r": 200.0, "g": 150.0, "b_val": 100.0,
"h": 30.0, "s": 0.5, "v": 0.8,
"l": 60.0, "a": 10.0, "b": 20.0
}
]
}
```
#### `_getAnnotatedWidth()` / `_getAnnotatedHeight()`
获取标注图像的宽度和高度(像素)。
#### `_getAnnotatedPixels()`
返回标注图像 BGRA 像素数据的指针。每像素 4 字节(B, G, R, A),总大小 = `width * height * 4`。
```javascript
const w = m._getAnnotatedWidth();
const h = m._getAnnotatedHeight();
const ptr = m._getAnnotatedPixels();
const size = w * h * 4;
const pixels = new Uint8Array(m.HEAPU8.buffer, ptr, size);
```
#### `_freeResult()`
释放内部缓存(结果 JSON、标注图像)。每次 `_recognize()` 调用后需调用一次。
### 完整调用示例
```javascript
async function processImage(imageData, width, height) {
const m = await PaperRecognizerModule();
// 拷贝图像到 WASM 堆
const dataSize = width * height * 4;
const ptr = m._malloc(dataSize);
m.HEAPU8.set(new Uint8Array(imageData), ptr);
// 识别
const ret = m._recognize(ptr, width, height);
// 读取结果
const jsonPtr = m._getResultJson();
const result = JSON.parse(m.UTF8ToString(jsonPtr));
// 读取标注图
const aw = m._getAnnotatedWidth();
const ah = m._getAnnotatedHeight();
const ap = m._getAnnotatedPixels();
const annotatedData = new Uint8Array(m.HEAPU8.buffer, ap, aw * ah * 4);
// 清理
m._freeResult();
m._free(ptr);
return { result, annotatedData, width: aw, height: ah };
}
```
### 重要说明
1. **内存管理**:`_malloc` 分配的内存必须用 `_free` 释放;`_freeResult` 必须每次 `_recognize` 后调用。
2. **输入格式**:接收 RGBA 字节数组(每像素 4 字节),R/G/B/A 各 8 位。
3. **标注图格式**:返回 BGRA 字节数组(B/G/R/A 顺序),在 JavaScript 侧绘制到 Canvas 前可能需要转换。
4. **错误处理**:`_recognize` 返回 -1 时可通过 `_getResultJson()` 查看 `errorCode` 和 `errorMessage`。
5. **模块是单例**:多次调用 `PaperRecognizerModule()` 返回同一个实例。
## 测试
```bash
node test.js
```
测试会加载模块、用纯灰色图像调用识别,并验证所有导出函数正常。