# 工地翻拍的图纸看不清，端侧超分 + OCR 把它变成能查的台账 # 智绘鸿蒙·如 7 而至#

## 前言

做过工程管理的都见过这个场景：现场拍一张墙上的图纸，回来想核对某个尺寸，结果照片糊得连房间分隔都看不分明。

工地相机、手机翻拍、隔着几米远、光线还不匀——拍回来的图纸十张有八张是"看得出是张图，看不出画了什么"。

台账系统要的其实不是"好看"，而是**能看清 + 能检索**：这张图对应哪个轴线区域、上面的编号是多少、尺寸能不能读出来。

这篇讲怎么用鸿蒙 7 的两个端侧能力把这条链路搭起来：**图像超分**（`imageSuperResolution`）负责"看清"，**通用文字识别**（`textRecognition`）负责"能查"。所有数据都是我在 MatePad Pro 真机上跑出来的，包括一个和上一篇不一致的耗时结论。

## 为什么是这两个能力

现场归档的痛点拆开是三个：

| 痛点 | 靠什么解决 |
| --- | --- |
| 图纸线条细、拍糊了分不清房间边界 | 图像超分，把高频细节还原 |
| 图上的编号、尺寸标注要人工抄进台账 | 通用文字识别，直接出文本 |
| 照片在工地上没网络 | 两个能力都是端侧推理，不联网、不要权限 |

这里有个顺序问题值得单独说：**必须先超分，再 OCR。**

我实测过反过来的情况——300×225 的原图上那些 5~8 像素高的数字，OCR 会给出成片的错字；先超分成 1200×900，同样的数字就稳定了。原因是 OCR 模型对字符的笔画完整性有要求，低分辨率下笔画直接粘连在一起，模型无从判断。

## 接口与约束

两个能力都在 `@kit.CoreVisionKit` 下，但生命周期模型不一样，这是最容易写乱的地方：

```typescript
// 图像超分：分析器实例模式
const analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
const res = await analyzer.process({ inputData: { pixelMap: pm } } as visionBase.Request);
await analyzer.destroy();

// 通用文字识别：全局服务模式
await textRecognition.init();
const txt = await textRecognition.recognizeText({ pixelMap: pm });
await textRecognition.release();
```

| 项目 | 图像超分 | 通用文字识别 |
| --- | --- | --- |
| 起始版本 | 26.0.0 | 4.0.0(10)，`init/release` 自 5.0.0(12) |
| 系统能力 | `SystemCapability.AI.Vision.VisionBase` | 同 |
| 支持形态 | phone / tablet / 2in1 | phone / tablet（2in1 自 5.0.1(13)） |
| 需要权限 | 无 | 无 |
| 放大倍率 | 固定 4 倍 | — |
| 输入上限 | **输出单边 ≤ 8192px，即输入长边 ≤ 2048** | 无明确上限，但越大越慢 |

> - **超分（Super-Resolution）**：从低分辨率图重建高分辨率细节，不改变画面内容。
> - **端侧推理**：模型跑在设备本地，图纸和照片不出机器——工地场景里这往往是硬性要求。
> - **台账**：工程现场按部位/轴线归档的影像记录。

超分抛 `401 The parameter check failed.` 就是踩了输入上限；OCR 侧要防的是 `init()` 失败和返回空文本。

## 开发步骤

### 第一步：定义台账条目结构

先想清楚一条台账记录要存什么，后面的处理都是往里填字段：

```typescript
export interface LedgerItem {
  point: string;        // 轴线/部位，如 "3轴-A轴"
  rawPath: string;      // 原图沙箱路径
  sharpPath: string;    // 超分后路径
  ocrText: string;      // 识别原文
  code: string;         // 从原文抽出的图纸编号
  costMs: number;       // 总耗时
}
```

`costMs` 一定要留。工地设备型号杂，没有耗时埋点你根本不知道慢在哪台机器上。

### 第二步：解码时限住长边

这是超分能不能跑起来的前提。先读元数据，把约束下推到解码阶段，避免解出全尺寸位图再缩放：

```typescript
static readonly MAX_INPUT_EDGE: number = 2048;

static async decodeCapped(source: image.ImageSource): Promise<image.PixelMap> {
  const info: image.ImageInfo = await source.getImageInfo();
  const long: number = Math.max(info.size.width, info.size.height);
  if (long <= SrEngine.MAX_INPUT_EDGE) {
    return source.createPixelMap();
  }
  const k: number = SrEngine.MAX_INPUT_EDGE / long;
  return source.createPixelMap({
    desiredSize: {
      width: Math.round(info.size.width * k),
      height: Math.round(info.size.height * k)
    }
  } as image.DecodingOptions);
}
```

现场直接翻拍的原图常常是 4000×3000，不限这一步就是 401。

### 第三步：超分

```typescript
async superResolve(src: image.PixelMap): Promise<SrResult> {
  if (!(await this.ready()) || !this.analyzer) {
    throw new SrError(-1, '超分引擎未就绪');
  }
  const imageData: visionBase.ImageData = { pixelMap: src };
  const request: visionBase.Request = { inputData: imageData };
  const start: number = Date.now();
  const response = await this.analyzer.process(request);
  const cost: number = Date.now() - start;
  // 采集输入输出尺寸与 cost
}
```

`create()` 要 catch `1018700001 Service exception`，工地设备上系统版本参差，这个异常不算罕见。

### 第四步：把超分结果喂给 OCR

注意 `process()` 返回的是新的 `PixelMap`，可以直接用，不需要落盘再读：

```typescript
async readNumbers(sharp: image.PixelMap): Promise<string> {
  await this.ocr.init();
  const info: textRecognition.VisionInfo = { pixelMap: sharp };
  const res: textRecognition.TextRecognitionResult =
    await textRecognition.recognizeText(info);
  return res.value;
}
```

拿到全文后，图纸编号用正则抽。这里有个实际经验：**编号往往出现在标题栏，而标题栏在右下角**，所以按行匹配比在整段文本里捞数字可靠：

```typescript
static pickDrawingCode(raw: string): string {
  const m: RegExpMatchArray | null = raw.match(/(?:图号|图纸编号|编号|比例)[:：]?\s*([A-Za-z0-9\-\/]{3,20})/);
  return m ? m[1] : '未识别';
}
```

### 第五步：串行批处理

一次归档几十张是常态。两个能力都是重资源，**必须串行**，而且要能中断：

```typescript
for (let i = 0; i < jobs.length; i++) {
  if (this.cancelled) {
    break;
  }
  const pm: image.PixelMap = await SrEngine.decodeCapped(sources[i]);
  const sharp: SrResult = await this.sr.superResolve(pm);
  const text: string = await this.ocr.readNumbers(sharp.output);
  items.push(this.build(sharp, text));
  this.progress = `第 ${i + 1}/${jobs.length} 张`;
}
```

进度按张数报，别报百分比——现场用户只关心还剩几张。

## 真机数据

测试机：HUAWEI MatePad Pro（MRDI-W00），HarmonyOS 7.0.0.107，API 26。样片是一张 300×225 的图纸现场翻拍图。

```
super resolved 300x225 -> 1200x900 in 916ms
```

首页四张待处理样片，第四张就是工地翻拍图纸：

![](assets/01-loaded.png)

超分前后对比（蓝线可拖动）——左侧线条发虚、房间分隔几乎看不出，右侧细线恢复、右下角标题栏表格重新可读：

![](assets/02-result.png)

## 一个和上一篇不一致的结论

上一篇（《买家发来的糊图，怎么直接上详情页》）里我给过平板上的耗时：250×333（83k 像素）稳定在 772~845 ms。

这次 300×225 只有 **67.5k 像素，反而用了 916 ms**。

像素更少、耗时更长，说明**在小尺寸区间，平板上的耗时被固定开销主导，和输入像素数已经不线性了**。而手机上那组数据（240×298 → 148ms、400×496 → 300ms…）在 45 万像素以上才呈现 `≈1.18 μs/px` 的线性关系。

所以之前那个线性模型，适用边界要说清楚：

| 条件 | 模型是否成立 |
| --- | --- |
| 手机、输入 ≥ 45 万像素 | 成立，约 1.18 μs/px |
| 手机、小尺寸输入 | 有约 50ms 固定开销，仍可估 |
| **平板、小尺寸输入** | **不成立**，固定开销 700ms 以上且波动 |

工程上的处理办法：**不要按公式估时，按机型分桶实测**。我在埋点里记 `deviceInfo.productModel` + 输入像素数 + 实测耗时，跑几天就有真实分布。批量场景的超时阈值取该机型 p95，而不是取平均值。

## 台账场景另外三件事

**图纸上的污损超分不会处理。** 样片那张纸有折痕和水渍，超分之后折痕被完整地、更清晰地保留了下来。它只补细节，不做清理。要出"干净电子版"，得再加一步透视矫正和去污，那是另一个话题。

**OCR 不做语义纠错。** 图纸上小字被识别成错字时，它会照原样输出。所以台账里凡是自动抽取的编号，都要留人工复核入口，不能直接当主键用。

**没网络反而是优势。** 这两个能力全程端侧，工地上信号差甚至无信号都能跑，回办公室再同步结果。这也是选端侧方案而不是云端 API 的实际理由。

## 总结

什么场景值得这么搭：工程、监理、巡检、资产盘点这类"现场翻拍图纸或铭牌、回来要能查能核对"的活。超分管看清，OCR 管能查，全程端侧适配无网环境。

不适合的是把它当"扫描件生成工具"——折痕、水渍、透视变形它都不管，那些得单独处理。

> 💡 **一句话**：先超分再 OCR，顺序不能反；耗时别套公式，按机型实测取 p95。

## 参考文献

1. Core Vision Kit 开发指南 · 图像超分：https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-image-super-resolution
2. Core Vision Kit API 参考 · imageSuperResolution（图像超分）：https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-image-super-resolution-api
3. Core Vision Kit 开发指南 · 通用文字识别：https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/core-vision-text-recognition
4. Core Vision Kit API 参考 · textRecognition（文字识别）：https://developer.huawei.com/consumer/cn/doc/harmonyos-references/core-vision-text-recognition-api
