【共创稿事节】鸿蒙图像超分 · 壁纸适配工作台:三种屏幕比例、居中裁切、端侧 4× 超分、所见即所得
【共创稿事节】鸿蒙图像超分 · 壁纸适配工作台:三种屏幕比例、居中裁切、端侧 4× 超分、所见即所得
前五篇把端侧超分这个「单点能力」打磨得很顺了——单图重建、实时放大镜、批量落盘、相册直选,每一步都验证了 HarmonyOS 端侧 NPU 推理的可用性。但都是技术演示视角:选一张图、跑一遍模型、看对比。真实用户的诉求其实更朴素——「我想把这张照片做成手机壁纸」。这一篇换个视角,从「做成壁纸」这个具体场景出发,做一个把壁纸比例、居中裁切、端侧超分、导出尺寸串成一条链的工作台。同时借这个机会,把前作里几个一直被忽略的坑——壁纸预设形同虚设、保存功能永远失败、结构体 get 访问器运行时崩——一次性修掉。

工程地址:
LI_harmonyOS/Image-Super-Resolution/harmony-sr-studio
运行环境:HarmonyOS 7.0(API 26)+ ArkTS 严格模式,已在 Mate 90 Pro 模拟器(7.0.0/26.0.0)安装运行验证。
包名:com.example.harmonysrstudio,为读取相册照片申请了ohos.permission.READ_IMAGEVIDEO(user_grant,含 reason/usedScene)。
一、效果展示
工作台的完整链路只有四步:选壁纸规格 → 导入照片(相册或内置演示)→ 端侧超分 → 导出目标尺寸壁纸。以下截图均为模拟器真机运行,演示模式与真机模式走同一套 UI。
选好规格(这里是「平板桌面」2560×1800)并载入内置狗狗演示素材后,预览区按 32:18 的横屏比例铺开,构图与最终壁纸一致——这就是「所见即所得」的核心:预览什么样,导出的壁纸就什么样:
往下滚动可以看到输入/工作/输出三个尺寸卡、处理方式按钮区。演示模式下明确标注「演示素材」「DEMO MODE」,不会伪装成真实推理:
超分完成后进入对比态:左侧「原始」、右侧「演示素材」,中间一条可拖动的白色分割线。默认分割居中,左右同源同构图、只分辨率不同:
把分割线一路拖到最左,整个预览区就只剩右侧的高清结果,方便单独检查细节:
切换到「手机壁纸」1440×3200 后,预览区立刻按 9:20 的窄竖条重新裁切——同一张狗狗图,构图从横屏变成竖屏,壁纸比例的差异一目了然:
二、这个 App 解决什么问题
前五篇的超分输入都是「正方形或 4:3 的素材」,对比的是「放大前 vs 放大后」。但壁纸是有明确目标尺寸和宽高比的:手机 9:20、平板 32:18、方形封面 1:1。这就引出三个新命题:
- 比例不对就得裁:一张 4:3 照片要当手机壁纸,必须裁成 9:20,否则两边黑边或拉伸变形;
- 超分和裁切要算好顺序:先超分再裁,还是先裁到工作尺寸再超分?这直接决定 NPU 输入大小和最终清晰度;
- 结果要导出成精确的像素尺寸:不是「放大 4 倍就行」,而是「必须正好是 1440×3200」。
这个工作台给出的答案是:按预设宽高比居中裁切源图 → 缩放到目标尺寸的 1/4 作为超分工作输入 → 端侧 4× 超分正好得到目标像素 → 导出。核心思路是「工作尺寸 = 目标尺寸 / 4」,让 Core Vision Kit 的固定 4 倍放大刚好等于目标分辨率。
三、核心算法:为什么工作尺寸是目标尺寸的 1/4
这是本篇在算法上唯一需要想清楚的地方,也是整条链路的「灵魂」。
Core Vision Kit 的图像超分是固定 4 倍放大——输入 W×H,输出就是 4W×4H,不可调。而壁纸要求精确的目标像素(如 1440×3200)。两者要严丝合缝,唯一的办法就是反推输入尺寸:
工作尺寸 = 目标尺寸 / 4
手机壁纸 1440×3200 → 工作输入 360×800
平板桌面 2560×1800 → 工作输入 640×450
方形封面 2048×2048 → 工作输入 512×512
这样喂给 NPU 一张 360×800 的图,输出正好 1440×3200,一次推理直接得到目标分辨率,无需任何二次缩放。这套「反推法」有两个硬性约束要处理:
public async prepareInput(preset: WallpaperPreset): Promise<LoadedImage | null> {
if (!this.source) {
return null;
}
const info: image.ImageInfo = await this.source.getImageInfo();
const region: image.Region = PhotoLabService.centerCrop(info.size.width, info.size.height,
preset.width / preset.height);
const working: image.Size = this.workingSize(preset, region.size);
const prepared: image.PixelMap | null = await this.coverTo(this.source, working.width, working.height);
if (!prepared) {
return null;
}
return { lowPm: prepared, lowW: working.width, lowH: working.height, srcW: this.srcW, srcH: this.srcH };
}
第一个约束是 NPU 输入上限 2048。工作尺寸里最大的是方形封面的 512×512,远小于 2048,安全;但如果素材本身分辨率不够(比如内置狗狗演示图只有 640×408),就退化为「裁切区域的 1/4」而不是目标的 1/4:
private workingSize(preset: WallpaperPreset, crop: image.Size): image.Size {
let workW: number = Math.max(1, Math.round(preset.width / SR_SCALE));
let workH: number = Math.max(1, Math.round(preset.height / SR_SCALE));
if (workW > crop.width || workH > crop.height) {
// 素材分辨率不足以支撑目标工作尺寸,改为素材裁切区域的 1/4
workW = Math.max(1, Math.round(crop.width / SR_SCALE));
workH = Math.max(1, Math.round(crop.height / SR_SCALE));
}
const cap: number = Math.min(1, SR_MAX_EDGE / Math.max(workW, workH));
return {
width: Math.max(MIN_WORK_EDGE, Math.round(workW * cap)),
height: Math.max(MIN_WORK_EDGE, Math.round(workH * cap))
};
}
第二个约束是先裁后缩。centerCrop 先把源图按目标宽高比居中裁出一块(构图不变),coverTo 再把这块缩放到工作尺寸。这样最终壁纸的构图、裁切位置在预览阶段就完全确定。
四、工程结构
harmony-sr-studio/
├── entry/src/main/
│ ├── ets/
│ │ ├── pages/Index.ets # 三态页面、规格切换、滑动对比、导出
│ │ └── common/
│ │ ├── PhotoLabModels.ets # LoadedImage / ProcessResult / 壁纸预设
│ │ ├── PhotoLabService.ets # 单例服务:源管理/裁切/NPU超分/落盘
│ │ ├── PixelOps.ets # 像素读取 / 双线性基线 / 梯度能量
│ │ └── RealImageSuperResolutionService.ets # Core Vision Kit 官方接入参考
│ └── resources/rawfile/demo/ # 狗狗演示素材(同源低清/高清)
└── AppScope/
壁纸预设用一个常量数组统一定义,UI 和服务端都从这一个地方取,避免「UI 显示一套、实际处理另一套」:
export interface WallpaperPreset {
name: string;
width: number;
height: number;
}
/** 内置壁纸规格 */
export const WALLPAPER_PRESETS: WallpaperPreset[] = [
{ name: '手机壁纸', width: 1440, height: 3200 },
{ name: '平板桌面', width: 2560, height: 1800 },
{ name: '方形封面', width: 2048, height: 2048 }
];
服务沿用系列前作的单例 + 双模式设计,但多了一个关键角色——输入源 source。它持有解码后的源图 PixelMap,所有「按预设裁切」都基于这个源,这样切换规格时能重新裁切,而不是拿着旧裁切结果继续用。

五、核心实现
5.1 三态状态机:空态 / 预览 / 结果
整个页面用一个 stage 字段驱动,三态互斥,build() 里按状态渲染,没有命令式跳转:
type ScreenStage = 'empty' | 'preview' | 'result';
- empty:空态引导,提示「导入一张照片,开启细节重建」;
- preview:已载入素材、按当前规格裁切好,显示「待增强」,等用户点「开始图像超分」;
- result:超分完成,进入滑动对比,主按钮变为「导出壁纸」。
预览区高度不是写死的,而是随壁纸比例动态计算:宽度固定,高度 = 宽度 × 目标高宽比,再夹在 [200, 360] 之间。这样手机壁纸是窄竖条、平板桌面是横屏、方形封面是正方形,视觉上直接体现比例差异:
private previewHeight(): number {
const preset: WallpaperPreset = this.currentPreset();
const width: number = this.previewWidth > 0 ? this.previewWidth : PREVIEW_FALLBACK_WIDTH;
const raw: number = width * preset.height / preset.width;
return Math.max(PREVIEW_MIN_HEIGHT, Math.min(PREVIEW_MAX_HEIGHT, Math.round(raw)));
}
5.2 Cover 适配:居中裁切 + 原生缩放
「把任意比例源图变成目标比例壁纸」用的是标准的 Cover 适配:先居中裁切到目标宽高比,再等比缩放到目标尺寸。两步都走系统原生 PixelMap 操作,不做任何 JS 逐像素处理:
private async coverTo(src: image.PixelMap, targetW: number, targetH: number): Promise<image.PixelMap | null> {
let clone: image.PixelMap | null = null;
try {
const info: image.ImageInfo = await src.getImageInfo();
const sw: number = info.size.width;
const sh: number = info.size.height;
if (sw <= 0 || sh <= 0 || targetW <= 0 || targetH <= 0) {
return null;
}
const region: image.Region = PhotoLabService.centerCrop(sw, sh, targetW / targetH);
clone = await src.clone();
if (region.size.width !== sw || region.size.height !== sh) {
await clone.applyCrop(region);
}
const fx: number = targetW / region.size.width;
const fy: number = targetH / region.size.height;
if (Math.abs(fx - 1) > 0.001 || Math.abs(fy - 1) > 0.001) {
await clone.applyScale(fx, fy, image.AntiAliasingLevel.LOW);
}
return clone;
}
关键细节:先 clone 再原地改。applyCrop/applyScale 都是原地操作,直接改会污染源图,所以先 clone() 出一份再裁。裁切区域的计算是纯数学——比较源宽高比和目标宽高比,谁宽裁谁:
private static centerCrop(srcW: number, srcH: number, targetAspect: number): image.Region {
const srcAspect: number = srcW / srcH;
let cropW: number = srcW;
let cropH: number = srcH;
if (srcAspect > targetAspect) {
cropW = Math.max(1, Math.min(srcW, Math.floor(srcH * targetAspect)));
} else if (srcAspect < targetAspect) {
cropH = Math.max(1, Math.min(srcH, Math.floor(srcW / targetAspect)));
}
const x: number = Math.max(0, Math.floor((srcW - cropW) / 2));
const y: number = Math.max(0, Math.floor((srcH - cropH) / 2));
return { size: { width: cropW, height: cropH }, x: x, y: y };
}
5.3 规格切换:丢弃旧结果、重新裁切
这是把「壁纸预设」从摆设变成功能的关键。切换规格时不是简单改个 label,而是释放旧的低清图和结果,用新比例对源图重新裁切:
private async selectPreset(index: number): Promise<void> {
if (this.busy || index === this.presetIndex) {
return;
}
this.presetIndex = index;
this.savedLabel = '';
if (this.stage === 'empty') {
return;
}
// 已有素材:丢弃旧结果,按新比例重新裁切输入
this.busy = true;
try {
const label: string = this.service.isDemoSource() ? '内置演示素材' : '相册照片';
await this.prepareInput(label);
} finally {
this.busy = false;
}
}
prepareInput 内部先释放旧的 lowPm 和 result,再按新预设裁切出新的工作图。这里有一个很容易犯的内存错误:必须先 release 旧 PixelMap 再赋新值,否则切换几次规格就泄漏好几张图:
private async prepareInput(label: string): Promise<boolean> {
const loaded: LoadedImage | null = await this.service.prepareInput(this.currentPreset());
if (!loaded || !loaded.lowPm) {
this.releaseImages();
this.stage = 'empty';
this.imageSource = '请选择一张照片开始';
this.toast('按所选规格裁切失败,请重试');
return false;
}
if (this.result) {
if (this.result.hdPm) {
this.result.hdPm.release();
}
this.result = null;
}
if (this.lowPm) {
this.lowPm.release();
}
this.lowPm = loaded.lowPm;
this.sourceWidth = loaded.srcW;
this.sourceHeight = loaded.srcH;
this.imageSource = `${label} · ${loaded.srcW} × ${loaded.srcH}`;
this.stage = 'preview';
this.split = 50;
return true;
}
5.4 处理主流程:NPU 超分 + 演示回退 + 增益量化
process 一条链路产出 UI 需要的全部东西:真机模式走 NPU,失败或演示模式回退到「同源高清参考」(用同一源图裁出目标尺寸,保证对比图构图一致),最后统一裁切到精确目标像素并算清晰度增益:
public async process(lowPm: image.PixelMap, lowW: number, lowH: number, useReal: boolean,
preset: WallpaperPreset): Promise<ProcessResult | null> {
let raw: image.PixelMap | null = null;
try {
if (useReal && this.analyzer) {
raw = await this.superResolve(lowPm);
} else if (this.source) {
// 演示模式:用同一输入源裁切出高清参考,保证前后对比同源同构图
raw = await this.coverTo(this.source, preset.width, preset.height);
}
if (!raw) {
return null;
}
const hd: image.PixelMap | null = await this.coverTo(raw, preset.width, preset.height);
raw.release();
if (!hd) {
return null;
}
const gain: number = await this.estimateGain(lowPm, lowW, lowH, hd);
return { hdPm: hd, hdW: preset.width, hdH: preset.height, gain: gain };
}
真机 NPU 调用本身与系列前作完全一致——因为工作尺寸最大才 640×450,远小于 2048 上限,一次 process() 整图出结果:
private async superResolve(lowPm: image.PixelMap): Promise<image.PixelMap | null> {
if (!this.analyzer) {
return null;
}
try {
const request: visionBase.Request = { inputData: { pixelMap: lowPm } };
const response: imageSuperResolution.ISPResponse = await this.analyzer.process(request);
return response.pixelMap;
} catch (error) {
const err = error as Error;
hilog.error(DOMAIN, TAG, 'super resolution failed: %{public}s', err.message);
return null;
}
}
5.5 增益评估:先降采样到小网格,避免 JS 逐像素卡顿
清晰度增益用系列前作同口径的梯度能量(邻域亮度差绝对值和,越高越清晰),但要比较「超分结果」和「低清双线性放大」,两者必须同尺寸。问题是结果可能是 2560×1800 这种大图,在 JS 里逐像素遍历几百万像素会卡好几秒。
解法是先把两边都用原生 applyScale 缩到 192px 小网格,JS 只遍历约 3.7 万像素,毫秒级完成:
private async estimateGain(lowPm: image.PixelMap, lowW: number, lowH: number,
hdPm: image.PixelMap): Promise<number> {
const hdInfo: image.ImageInfo = await hdPm.getImageInfo();
const hdW: number = hdInfo.size.width;
const hdH: number = hdInfo.size.height;
const longEdge: number = Math.max(hdW, hdH);
if (longEdge <= 0) {
return 0;
}
const ratio: number = Math.min(1, METRIC_EDGE / longEdge);
const gridW: number = Math.max(2, Math.round(hdW * ratio));
const gridH: number = Math.max(2, Math.round(hdH * ratio));
const lowBuffer: Uint8Array | null = await readRgba(lowPm);
if (!lowBuffer) {
return 0;
}
const baseline: Uint8Array | null = bilinearUpscaleTo(lowBuffer, lowW, lowH, gridW, gridH);
if (!baseline) {
return 0;
}
const baselineEnergy: number = gradientEnergy(baseline, gridW, gridH);
const highEnergy: number = await this.downscaledEnergy(hdPm, gridW, gridH);
if (baselineEnergy <= 0) {
return 0;
}
return Math.max(0, Math.min(99, Math.round((highEnergy / baselineEnergy - 1) * 100)));
}
5.6 滑动对比:Stack 叠两层 + clip 裁剪
对比控件没用任何像素混合,思路和前作一致但做了简化:Stack 底层放结果图满铺,上层放一个宽度为 split%、溢出裁剪的子 Stack 包住原始图,再压一条白色分割线和两枚标签。拖动手势只改一个 0~100 的 @State split:
Stack({ alignContent: Alignment.Center }) {
if (this.result?.hdPm && this.stage === 'result') {
Image(this.result.hdPm)
.width('100%')
.height(height)
.objectFit(ImageFit.Cover)
.borderRadius(16)
Stack({ alignContent: Alignment.Start }) {
Image(this.lowPm)
.width(this.previewWidth > 0 ? this.previewWidth : PREVIEW_FALLBACK_WIDTH)
.height(height)
.objectFit(ImageFit.Cover)
}
.width(`${this.split}%`)
.height(height)
.clip(true)
Column()
.width(2)
.height(height)
.backgroundColor('#FFFFFF')
.position({ x: this.dividerX() - 1, y: 0 })
两个几何细节:两张图都必须 objectFit(ImageFit.Cover) 且同尺寸同高度,否则分割线两边错位;上层子 Stack 用 .clip(true) 把超出 split% 宽度的部分裁掉,分割线位置由 containerWidth() * split / 100 算出,横竖屏、折叠屏都不会算错。
5.7 结果导出:按预设裁切后写 PNG 沙箱
导出沿用系列的零权限方案,但多了一步——先按预设再裁切缩放一次,确保落盘的像素尺寸就是目标尺寸:
public async exportWallpaper(hdPm: image.PixelMap, preset: WallpaperPreset, name: string): Promise<PackResult | null> {
const fitted: image.PixelMap | null = await this.coverTo(hdPm, preset.width, preset.height);
if (!fitted) {
return null;
}
try {
return await this.writePng(fitted, name);
} finally {
fitted.release();
}
}
文件名用 {宽}x{高}_{时间戳} 保证多次保存不覆盖,写盘用 packToData → openSync → writeSync → fsyncSync → closeSync,最后回显沙箱路径和文件大小。

六、踩坑实录:三个隐蔽的「能编译但跑挂/失效」
这一篇把工程从「能编译」打磨到「真机稳定」,修掉了三个非常典型的 HarmonyOS 坑。它们有个共同点:编译全绿、静态检查不报错,但一跑就出问题。
6.1 结构体 get 访问器:编译通过、运行时 undefined
最致命的一个。为了取当前壁纸规格,我最开始写了一个访问器:
// ❌ 错误写法:ArkTS @Component struct 不支持 get/set 访问器
private get preset(): WallpaperPreset {
return WALLPAPER_PRESETS[this.presetIndex];
}
编译完全通过,没有任何报错。但真机一进首帧渲染就 jscrash:
TypeError: Cannot read property width of undefined
at initialRender (Index.ets:437:37)
原因是 ArkTS 的 @Component struct 不支持 get/set 访问器——它在编译产物里没有被正确绑定,this.preset 运行时就是 undefined,再去取 .width 直接崩。改成普通方法立刻解决:
/** 取当前壁纸规格;ArkTS 结构体不支持 get 访问器,必须用普通方法 */
private currentPreset(): WallpaperPreset {
const presets: WallpaperPreset[] = WALLPAPER_PRESETS;
const index: number = (presets.length > 0 && this.presetIndex >= 0 && this.presetIndex < presets.length)
? this.presetIndex : 0;
return presets[index];
}
教训:ArkTS 结构体里访问派生数据,一律用方法,不要用 get。这类「编译能过、运行时崩」的问题,只能真机/模拟器跑出来。
6.2 fileIo.accessSync 对不存在路径是抛异常,不是返回 false
保存功能一开始永远失败。原写法想「目录不存在就创建」:
// ❌ 错误写法:accessSync 对不存在路径抛异常,永远走不到 mkdirSync
if (!fileIo.accessSync(directory)) {
fileIo.mkdirSync(directory, true);
}
但 accessSync 的语义是「存在返回 true,不存在直接抛异常」,所以 !accessSync(...) 这个判断在目录不存在时根本执行不到——异常先抛了,被外层 catch 吞掉,首图保存必定失败。正确做法是 try/catch 里尝试访问,失败再创建:
private ensureDirectory(dir: string): boolean {
try {
if (fileIo.accessSync(dir)) {
return true;
}
} catch (error) {
// 目录不存在时 accessSync 会抛异常,落到下方创建逻辑
}
try {
fileIo.mkdirSync(dir, true);
return true;
} catch (error) {
const err = error as Error;
hilog.error(DOMAIN, TAG, 'create directory failed: %{public}s', err.message);
return false;
}
}
6.3 rawfile 解码:Uint8Array.buffer 可能带非零偏移
读内置演示素材时,一开始直接 image.createImageSource(content.buffer)。大部分时候能解码,但偶发解码错位——getRawFileContent 返回的 Uint8Array 可能带有非零 byteOffset,直接传底层 buffer 会把整个共享缓冲区(而不是这个视图)喂给解码器。必须按视图切片:
const content: Uint8Array = await this.context.resourceManager.getRawFileContent(name);
// Uint8Array 可能带有非零 byteOffset,直接传 buffer 会导致解码错位,必须按视图切片
const data: ArrayBuffer = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
source = image.createImageSource(data);
return await source.createPixelMap({ desiredPixelFormat: image.PixelMapFormat.RGBA_8888 });
这三个坑在踩坑清单速查表里都有对应条目,是这一篇最有价值的「实战经验」。
七、权限配置:user_grant 权限的三要素
从相册读照片需要 ohos.permission.READ_IMAGEVIDEO,这是个 user_grant 权限,module.json5 里必须同时声明 reason 和 usedScene,缺一不可,而且 reason 必须是 $string: 资源引用,不能写中文文本:
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:permission_read_imagevideo_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
对应的字符串资源放在 resources/base/element/string.json:
{
"name": "permission_read_imagevideo_reason",
"value": "用于读取您选择的图片或视频,进行端侧超分辨率处理。"
}
少了 reason/usedScene 会报 00303218 Configuration Error;reason 写了纯文本会报 00303038 Schema validate failed。这两个错都是在 PreBuild 阶段卡死,先把配置写对才能进入真正的 ArkTS 编译。
八、真机 / 模拟器运行指南
- 演示模式(默认):点「载入演示图」用内置狗狗素材,不调 NPU,任何设备都能跑通「选规格 → 预览 → 超分 → 对比 → 导出」全流程,结果明确标注「演示素材」;
- 真机模式:点「从相册选择」在系统相册选任意图,会真实走一遍端侧 NPU 4× 超分。若设备不支持 Core Vision Kit,会自动回退演示素材并 toast 提示「真实超分需要支持的设备」,不会伪装成成功;
- 结果保存在沙箱
filesDir/wallpaper_sr_out/{宽}x{高}_{时间戳}.png,取出方式:hdc shell "ls /data/app/el2/100/base/com.example.harmonysrstudio/haps/entry/files/wallpaper_sr_out" hdc file recv /data/app/el2/100/base/com.example.harmonysrstudio/haps/entry/files/wallpaper_sr_out ./out
命令行构建需要把 DevEco 自带的 JBR 加进 PATH(否则打包阶段报 spawn java ENOENT):
$env:DEVECO_SDK_HOME="C:\Program Files\Huawei\DevEco Studio\sdk"
$env:JAVA_HOME="C:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="C:\Program Files\Huawei\DevEco Studio\jbr\bin;$env:PATH"
node "C:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.js" `
--mode module -p module=entry@default -p product=default -p requiredDeviceType=phone `
assembleHap --analyze=normal --parallel --no-daemon
九、踩坑清单速查表
| 现象 | 原因 | 修法 |
|---|---|---|
一进页面就 jscrash Cannot read property width of undefined | ArkTS struct 用了 get 访问器,编译过但运行时返回 undefined | 改用普通方法 currentPreset() |
| 首次保存永远失败 | fileIo.accessSync 对不存在路径抛异常,!accessSync() 走不到创建分支 | try/catch 访问,失败再 mkdirSync(dir, true) |
| 内置素材偶发解码错位 | getRawFileContent 返回的 Uint8Array 带非零 byteOffset,直接传 .buffer | buffer.slice(byteOffset, byteOffset+byteLength) 按视图切片 |
| 大图增益计算卡好几秒 | JS 逐像素遍历几百万像素 | 原生 applyScale 先缩到 192px 网格再算梯度能量 |
| 切换规格后预览构图不变 | 只改了 label,没重新裁切 | 保留源图 source,切换时 prepareInput 重新裁切 |
| 分割线两边画面错位 | 两张图 objectFit/尺寸/高度不一致 | 都用 Cover + 同宽高,上层 clip(true) 裁 split% |
| 超分结果尺寸不是目标像素 | 直接用 NPU 输出,没按预设裁切 | 工作尺寸 = 目标/4,输出再 coverTo 到精确目标 |
PreBuild 报 00303218 | user_grant 权限缺 reason/usedScene | 补两字段 |
PreBuild 报 00303038 | reason 写了中文纯文本 | 改 $string: 资源引用 |
打包报 spawn java ENOENT | shell 缺 JDK | 把 DevEco 自带 jbr\bin 加进 PATH |

十、总结
至此图像超分系列走完第六步:从「技术演示」走向「场景闭环」。
这一篇的核心不是又接了一个新 API,而是把端侧超分放进一个真实的产品场景里重新审视——壁纸有明确的尺寸和比例,超分不能再是「放大 4 倍看效果」,而要「精确产出 1440×3200 这块能直接设成壁纸的图」。为此我们反推出「工作尺寸 = 目标尺寸 / 4」的输入策略,用 Cover 适配解决任意比例的居中裁切,用保留源图 + 重新裁切让规格切换真正生效,再把前作里潜伏的三个运行时坑(get 访问器崩溃、accessSync 语义、Uint8Array 偏移)一次性填平。
至此,从单图重建、实时放大镜、批量工作台、相册直选,到今天的壁纸适配,端侧超分这条线已经覆盖了「能力验证 → 交互增强 → 工程批量化 → 真实输入 → 场景落地」的完整路径。下一步可以继续往「设置成系统壁纸」这个最后一步走(需要 setWallpaper 相关能力),或者把裁切框做成可手动拖拽调整构图的交互——那就留给下一篇了。
工程里 RealImageSuperResolutionService.ets 保留了 Core Vision Kit 的官方接入参考,真机上把 useReal 置真即可切换到 NPU 推理;模拟器/不支持设备自动回退演示素材,保证任何环境都能完整体验。壁纸比例与尺寸的工作台已经搭好,剩下的就是把每一张图,都适配到它的屏幕上。
更多推荐

所有评论(0)