【共创稿事节】鸿蒙图像超分 · 文档增强工作台:把内置演示图换成纸面文档、深色暗房 UI、滑动对比一键落盘
【共创稿事节】鸿蒙图像超分 · 文档增强工作台:把内置演示图换成纸面文档、深色暗房 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 质量 | 用途 |
|---|---|---|---|
| 轻度 720p | 509×720 | 85 | 看小幅提升 |
| 中度 480p | 339×480 | 55 | 对比最明显(推荐) |
| 重度 360p | 254×360 | 28 | 加噪点,极限测试 |
三档并排看,退化程度递进,正文从「能读」到「发虚」再到「几乎看不清」:

中度 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)做点缀,像暗房里那盏安全灯。整体结构从上往下是四段:
- 顶部深色 AppBar:应用名「清晰卷」+ 英文小字「DOCUMENT ENHANCER」,右侧一颗胶囊,用绿/金圆点实时区分「端侧 REAL / 演示 DEMO」;
- 中部主对比区:深色容器(
#14171E)承载图像,空态是虚线描边占位,分隔线和手柄用琥珀金; - 数据带 + 预设:输入/输出/增益三个数据用竖线分隔成一条横带;文档类型(讲义书页/收据标签/白板照片)做成深色分段选项卡;
- 底部固定操作栏:琥珀金实心主按钮(开始超分/保存),下方三个次要按钮(相册/演示/重置)平铺。
分隔线和对比手柄的绘制,是这套 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」的状态胶囊,正是为了把这种「诚实」摆在明面上:真实模式只展示分析器返回的结果,推理失败就明确提示,绝不拿演示图冒充真机效果。

七、收尾:这一篇到底做了什么
回看这一篇,没有引入新的超分能力,做的都是「对齐」的功夫:
- 素材对齐主题:把内置演示图从城市夜景换成纸面文档,让「文档增强」这个卖点第一次有了匹配的样品;
- 素材与代码解耦:厘清了 rawfile 演示素材「文件名 + 宽高比 + 解码尺寸 + 4 倍关系」的耦合点,替换只改 6 行常量;
- 视觉对齐气质:UI 从蓝色卡片模板重构为深色暗房风,与第五篇彻底区分,且不伤业务逻辑;
- 测试对齐真实:程序化合成 + 三档退化,既能批量造图,又能横向对比不同输入质量下的超分表现,诚实标注真实/演示模式。
很多时候,一个 Demo 给别人的「专业感」,恰恰藏在这些细节里:演示素材是不是贴合主题、界面是不是有辨识度、测试数据是不是站得住脚。这一篇把这三件事一次性补齐了。
配套脚本:
test_images/gen_test_images.py(合成文档母版 + 三档降质)、test_images/make_demo_assets.py(生成 rawfile 演示素材)。想换文档内容、调分辨率或噪点强度,改参数重跑即可。
本文工程与脚本均已开源在对应目录,运行截图来自 Mate 90 Pro 模拟器(HarmonyOS 7.0 / API 26)实机。
更多推荐





所有评论(0)