【共创稿事节】鸿蒙图像超分 · 文档增强工作台:把内置演示图换成纸面文档、深色暗房 UI、滑动对比一键落盘

前面几篇把端侧超分的「能力链路」跑通了:单图重建、批量落盘、相册直选、壁纸适配。这一篇不堆新能力,而是回头解决一个很容易被忽略、却最影响「演示观感」的问题——内置演示素材与应用主题不匹配。

「清晰卷」这款 App 主打的是文档/纸面内容超分:讲义小字、收据标签、白板照片,让模糊的印刷体重新可读。可它的内置演示图,却是一张城市夜景——霓虹、月亮、万家灯火。用户一点「载入演示图」,看到的和「文档增强」八竿子打不着。这就像卖扫描仪的柜台,摆着一张风景明信片当样品。

这一篇就做一件事,但把它做透:用一张合成的纸面文档,替换掉那张「文不对题」的风景演示图,顺带把整套 UI 从「蓝色卡片模板」重构成「深色暗房风」,让演示素材、应用名、视觉风格三者第一次真正对齐。整个过程涉及:素材的程序化合成、rawfile 资源的加载链路、低清/高清图对的尺寸约定、以及一套和第五篇截然不同的深色界面。

工程地址:LI_harmonyOS/Image-Super-Resolution/paper-clarity-studio
运行环境:HarmonyOS 7.0(API 26)+ ArkTS 严格模式,已在 Mate 90 Pro 模拟器(7.0.0/26.0.0)安装运行验证。
包名:com.example.paperclaritystudio,为读取相册照片申请了 ohos.permission.READ_IMAGEVIDEO(user_grant,含 reason/usedScene)。


在这里插入图片描述

一、先看最终效果

整条链路只有四步:选文档类型 → 载入演示图(或相册选图)→ 端侧超分 → 滑动对比并保存。以下截图均为模拟器真机运行。

空态首屏,深色系,预览区是虚线描边的「尚未载入文档」占位,右上角胶囊实时标注当前是「端侧 REAL」还是「演示 DEMO」:

空态首屏

点「载入演示图」,预览区立刻铺出那张纸面文档——标题《关于提高大学生心理健康水平的几点思考》、三节正文、一张发生率调查表,全部清晰可辨。这就是本篇的核心:演示素材第一次和「文档增强」主题对上了:

点「开始图像超分」,进入对比态:左侧「原图」(低清发虚)、右侧「演示」(超分后清晰),中间一条琥珀金分割线 + 圆手柄。下方滑块同步控制,弹出的 toast 提示「演示对比已生成」:

滑动对比 居中

把分割线向左拖,右侧高清区扩大,可以清楚看到表格里的数字(12.5、15.8、18.2……)从发虚到锐利的差别。底部主按钮文案也随状态切换为「保存高清图到沙箱」:

滑动对比 查看增强区

拖到最左,几乎整屏都是超分后的清晰文档,正文笔画和表格线的边缘重建一目了然:

分割线拖到最左

点「保存高清图到沙箱」,数据带下方亮起绿色回执「已保存到应用沙箱 · 324 KB」,toast 提示「增强图片已保存」:

保存成功


二、问题出在哪:一张「文不对题」的演示图

「清晰卷」的功能定义非常聚焦——文档超分。但它的内置演示图,是从一个风景 Demo 里直接搬过来的城市夜景:

  • 主题错位:用户想验证的是「小字能不能变清晰」,结果给的是「夜景能不能放大」,两者评测维度完全不同。文档超分看的是笔画边缘、表格细线、数字锐度;风景超分看的是色块、光斑、远景层次。用风景图验证文档能力,结论没有意义。
  • 观感割裂:应用名叫「清晰卷 · DOCUMENT ENHANCER」,预设里写着「讲义书页 / 收据标签 / 白板照片」,结果演示图是夜景——第一眼就让人怀疑「这到底是不是做文档的」。

更要命的是,这张夜景图是写死在代码常量里的:

// 改之前:演示素材指向风景图
const DEMO_LOW: string = 'demo/low_city.png';
const DEMO_HD: string  = 'demo/hd_city.png';
const DEMO_LOW_W: number = 160;
const DEMO_LOW_H: number = 120;   // 4:3 横版
const DEMO_HD_W:  number = 640;
const DEMO_HD_H:  number = 480;

也就是说,换素材不只是「丢两张图进 rawfile」那么简单——图的宽高比、解码尺寸、低清/高清的倍数关系,全都和代码常量强耦合。文档是竖版(约 1:1.41 的 A4 比例),风景是横版(4:3),直接替换文件不改尺寸,预览就会拉伸变形。这正是本篇要系统性处理的地方。


三、第一步:程序化合成一张「以假乱真」的纸面文档

与其去网上找一张带版权风险的文档照,不如用代码直接画一张。好处是:内容可控、分辨率可控、还能顺便批量生成各种降质版本做横向测试。我用 Python + PIL 写了个生成器,分两步。

3.1 先画一张高清「母版」

母版按 A4 竖版比例(1240×1754)合成,把真实纸面文档的几个关键特征都模拟出来:

  • 内容结构:居中大标题 + 三节正文(引言/现状/对策)+ 一张带边框的数据表格;
  • 纸面质感:暖灰底色(234,231,224)+ 随机噪点,避免「死白」的数字感;
  • 不均匀光照:左下角压暗、右上角提亮,模拟手机拍摄时手部遮挡的阴影——这是真实文档照最典型的退化;
  • 轻微失焦:整体 GaussianBlur(0.6),模拟手持拍摄的轻微跑焦。
def build_master():
    img = Image.new("RGB", (W, H), (234, 231, 224))   # 暖灰纸面
    d = ImageDraw.Draw(img)
    # … 随机噪点打底 …
    # 标题 / 三节正文 / 表格,逐段绘制
    # 不均匀光照:用竖向渐变 L 通道 + 高斯模糊,左下压暗
    light = Image.new("L", (W, H), 0)
    ld = ImageDraw.Draw(light)
    for i in range(0, H, 6):
        ratio = i / H
        ld.rectangle([0, i, W, i + 6], fill=int(30 - 60 * ratio))
    light = light.filter(ImageFilter.GaussianBlur(120))
    img = Image.composite(img, Image.blend(img, dark, 0.45), light)
    return img.filter(ImageFilter.GaussianBlur(0.6))   # 轻微失焦

合成出来的母版,结构和观感都贴近真实讲义:

文档母版

3.2 再从母版「退化」出低清版

超分要对比,就得有一对同源、同构图、仅分辨率/质量不同的图。低清版直接从母版「退化」而来,一个 degrade() 函数搞定所有档位:

def degrade(img, out, target_h, quality, noise, blur):
    r = target_h / img.height
    m = img.resize((int(img.width * r), target_h), Image.LANCZOS)  # 降采样
    m = m.filter(ImageFilter.GaussianBlur(blur))                   # 模糊
    # … 叠加噪点 …
    m = ImageEnhance.Contrast(m).enhance(0.92)                     # 降对比
    m.save(out, "JPEG", quality=quality)                           # 压缩

我一口气生了三档,方便横向对比不同输入质量下的超分表现:

档位尺寸JPEG 质量用途
轻度 720p509×72085看小幅提升
中度 480p339×48055对比最明显(推荐)
重度 360p254×36028加噪点,极限测试

三档并排看,退化程度递进,正文从「能读」到「发虚」再到「几乎看不清」:

三档测试图

中度 480p 这一档,正文笔画已经开始粘连、表格数字发虚,但版面结构完整——是验证超分重建能力的最佳输入。


四、第二步:生成内置演示素材,接通加载链路

有了图,接下来把它变成 App 的内置演示素材。这里有个关键约定要讲清楚。

4.1 低清/高清的「4 倍」约定

Core Vision Kit 的端侧超分是固定 4 倍放大:输入 W×H,输出 4W×4H。所以内置演示素材必须是一对严格 4 倍关系的图:

  • 低清输入 doc_paper.png:160×226(演示模式下的「原图」)
  • 高清结果 hd_doc_paper.png:640×904(恰好 4×,演示模式下的「超分输出」)

640×904 ÷ 160×226 = 4×4,严丝合缝。这样演示模式的「假超分」(直接读高清图)和真机模式的「真超分」(NPU 4× 推理)在尺寸上完全一致,UI 不用为两种模式做区分。

LOW_W, LOW_H = 160, 226    # 低清输入
HD_W, HD_H   = 640, 904    # 超分结果 = 4x
low = master.resize((LOW_W, LOW_H), Image.LANCZOS).filter(ImageFilter.GaussianBlur(0.7))
low.save(OUT + "/doc_paper.png", "PNG")
hd = master.resize((HD_W, HD_H), Image.LANCZOS)
hd.save(OUT + "/hd_doc_paper.png", "PNG")

两张图落到 entry/src/main/resources/rawfile/demo/ 目录。低清输入特意再叠一层 0.7 的高斯模糊,让「待超分」的发虚感更明显:

低清演示输入

4.2 改常量,完成切换

素材就位后,切换只需要改 PhotoLabService.ets 顶部的六个常量——把风景图的文件名和尺寸,换成文档图的:

// 改之后:演示素材指向文档图,竖版 A4 比例
const DEMO_LOW: string = 'demo/doc_paper.png';
const DEMO_HD: string  = 'demo/hd_doc_paper.png';
const DEMO_LOW_W: number = 160;
const DEMO_LOW_H: number = 226;   // 竖版
const DEMO_HD_W:  number = 640;
const DEMO_HD_H:  number = 904;

这就是「文件名 + 宽高比 + 解码尺寸」三者强耦合的具体体现:少改一个,要么图片找不到(解码失败返回 null),要么尺寸错(预览拉伸)。改对之后,下游两个方法自动跟着切换,无需再动:

载入演示图——读低清版作为「原图」:

public async loadDemo(): Promise<LoadedImage | null> {
  const pixelMap = await this.decodeRawfile(DEMO_LOW, DEMO_LOW_W, DEMO_LOW_H);
  if (!pixelMap) { return null; }
  const info = await pixelMap.getImageInfo();
  return { lowPm: pixelMap, lowW: info.size.width, lowH: info.size.height,
           srcW: info.size.width, srcH: info.size.height };
}

演示模式「超分」——直接读高清版冒充推理结果:

} else if (!useReal) {
  highPm = await this.decodeRawfile(DEMO_HD, DEMO_HD_W, DEMO_HD_H);
}

而 decodeRawfile 这条底层解码链路是素材无关的,从 rawfile 读字节流、按目标尺寸解码成 RGBA_8888 的 PixelMap,对风景图和文档图一视同仁——所以这次替换完全没有触碰解码逻辑:

private async decodeRawfile(name: string, width: number, height: number) {
  const content: Uint8Array = await this.context.resourceManager.getRawFileContent(name);
  const source = image.createImageSource(content.buffer);
  const pixelMap = await source.createPixelMap({
    desiredSize: { width: width, height: height },
    desiredPixelFormat: image.PixelMapFormat.RGBA_8888
  });
  await source.release();
  return pixelMap;
}

整个替换过程,业务代码只改了 6 行常量,符合「素材与逻辑解耦」的设计——这也是前几篇打下的底子。


五、第三步:把 UI 从「蓝色卡片」重构成「深色暗房」

换素材的同时,我顺手把整套界面也重写了。原因很直接:之前的 UI 和第五篇是同一套模板——顶部渐变横幅、白色圆角卡片、蓝色主按钮,布局和配色几乎一样。既然是新的应用、新的主题,视觉上就该彻底区分开。

重构思路是把它做成一个「扫描暗房」:墨黑底色(#0B0D12)营造暗房氛围,琥珀金(#F5C86B)做点缀,像暗房里那盏安全灯。整体结构从上往下是四段:

  1. 顶部深色 AppBar:应用名「清晰卷」+ 英文小字「DOCUMENT ENHANCER」,右侧一颗胶囊,用绿/金圆点实时区分「端侧 REAL / 演示 DEMO」;
  2. 中部主对比区:深色容器(#14171E)承载图像,空态是虚线描边占位,分隔线和手柄用琥珀金;
  3. 数据带 + 预设:输入/输出/增益三个数据用竖线分隔成一条横带;文档类型(讲义书页/收据标签/白板照片)做成深色分段选项卡;
  4. 底部固定操作栏:琥珀金实心主按钮(开始超分/保存),下方三个次要按钮(相册/演示/重置)平铺。

分隔线和对比手柄的绘制,是这套 UI 里最有「暗房感」的细节——一条 2px 的琥珀金竖线 + 一个实心圆手柄,叠在图像上随 split 百分比移动:

Column()
  .width(2).height(PREVIEW_HEIGHT)
  .backgroundColor('#F5C86B')
  .position({ x: this.previewWidth * this.split / 100 - 1, y: 0 })
Text('⇄')
  .width(30).height(30)
  .fontColor('#14161C').backgroundColor('#F5C86B')
  .borderRadius(15)
  .position({ x: this.previewWidth * this.split / 100 - 15, y: PREVIEW_HEIGHT / 2 - 15 })

底部主按钮则根据状态动态切换文案和可用性——忙时置灰显示「处理中…」,结果态显示「保存高清图到沙箱」,平时是「开始图像超分」:

Button(this.busy ? '处理中…' : (this.stage === 'result' ? '保存高清图到沙箱' : '开始图像超分'))
  .backgroundColor(this.busy || (this.stage !== 'result' && !this.lowPm) ? '#2A303C' : '#F5C86B')
  .fontColor(this.busy || (this.stage !== 'result' && !this.lowPm) ? '#6E7482' : '#14161C')
  .enabled(!this.busy && (this.stage === 'result' || this.lowPm !== null))

值得强调的是:这次 UI 重构只动了视觉层,业务逻辑一行没改。服务调用、stage 状态机(empty/preview/result)、PixelMap 的 release() 内存管理、滑动对比的 split 计算,全部原样保留。这也再次验证了前几篇「UI 与逻辑分离」的价值——换皮肤不伤筋动骨。


六、横向测试:三档输入下的超分表现

素材和界面都就绪后,我用前面生成的三档降质图做了一轮横向验证。把它们传进模拟器相册,用「从相册选择」逐档导入,跑超分看效果:

  • 720p 轻度:原图本就接近可读,超分后主要是笔画边缘更锐、表格细线更利落,提升偏「润色」;
  • 480p 中度:差异最戏剧化——发虚粘连的正文重新根根分明,表格里的「18.2」「13.6」等小数从糊成一团到清晰可读,增益带上的百分比也最高;
  • 360p 重度+噪点:原图已接近不可读,超分能找回版面结构和大字标题,但密集小字难免有涂抹感——这也如实反映了端侧轻量模型的能力边界,不会为了演示效果而伪装成「无所不能」。

应用右上角那颗「端侧 REAL / 演示 DEMO」的状态胶囊,正是为了把这种「诚实」摆在明面上:真实模式只展示分析器返回的结果,推理失败就明确提示,绝不拿演示图冒充真机效果。


在这里插入图片描述

七、收尾:这一篇到底做了什么

回看这一篇,没有引入新的超分能力,做的都是「对齐」的功夫:

  1. 素材对齐主题:把内置演示图从城市夜景换成纸面文档,让「文档增强」这个卖点第一次有了匹配的样品;
  2. 素材与代码解耦:厘清了 rawfile 演示素材「文件名 + 宽高比 + 解码尺寸 + 4 倍关系」的耦合点,替换只改 6 行常量;
  3. 视觉对齐气质:UI 从蓝色卡片模板重构为深色暗房风,与第五篇彻底区分,且不伤业务逻辑;
  4. 测试对齐真实:程序化合成 + 三档退化,既能批量造图,又能横向对比不同输入质量下的超分表现,诚实标注真实/演示模式。

很多时候,一个 Demo 给别人的「专业感」,恰恰藏在这些细节里:演示素材是不是贴合主题、界面是不是有辨识度、测试数据是不是站得住脚。这一篇把这三件事一次性补齐了。

配套脚本:test_images/gen_test_images.py(合成文档母版 + 三档降质)、test_images/make_demo_assets.py(生成 rawfile 演示素材)。想换文档内容、调分辨率或噪点强度,改参数重跑即可。


本文工程与脚本均已开源在对应目录,运行截图来自 Mate 90 Pro 模拟器(HarmonyOS 7.0 / API 26)实机。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐