智绘鸿蒙 · 如 7 而至端侧 AI 能力实战手记

工地翻拍的图纸看不清,端侧超分 + OCR 把它变成能查的台账

智绘鸿蒙·如 7 而至

前言

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

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

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

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

为什么是这两个能力

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

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

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

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

接口与约束

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

// 图像超分:分析器实例模式
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() 失败和返回空文本。

开发步骤

第一步:定义台账条目结构

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

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

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

第二步:解码时限住长边

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

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。

第三步:超分

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,可以直接用,不需要落盘再读:

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;
}

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

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

第五步:串行批处理

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

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

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

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

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

上一篇(《买家发来的糊图,怎么直接上详情页》)里我给过平板上的耗时: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