news 2026/10/11 9:09:43

【共创稿事节】鸿蒙图像超分 · 壁纸适配工作台:三种屏幕比例、居中裁切、端侧 4× 超分、所见即所得

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【共创稿事节】鸿蒙图像超分 · 壁纸适配工作台:三种屏幕比例、居中裁切、端侧 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。这就引出三个新命题:

  1. 比例不对就得裁:一张 4:3 照片要当手机壁纸,必须裁成 9:20,否则两边黑边或拉伸变形;
  2. 超分和裁切要算好顺序:先超分再裁,还是先裁到工作尺寸再超分?这直接决定 NPU 输入大小和最终清晰度;
  3. 结果要导出成精确的像素尺寸:不是「放大 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 访问器privategetpreset():WallpaperPreset{returnWALLPAPER_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.2fileIo.accessSync对不存在路径是抛异常,不是返回 false

保存功能一开始永远失败。原写法想「目录不存在就创建」:

// ❌ 错误写法:accessSync 对不存在路径抛异常,永远走不到 mkdirSyncif(!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

九、踩坑清单速查表

现象原因修法
一进页面就 jscrashCannot read property width of undefinedArkTS struct 用了get访问器,编译过但运行时返回undefined改用普通方法currentPreset()
首次保存永远失败fileIo.accessSync对不存在路径抛异常,!accessSync()走不到创建分支try/catch 访问,失败再mkdirSync(dir, true)
内置素材偶发解码错位getRawFileContent返回的Uint8Array带非零byteOffset,直接传.bufferbuffer.slice(byteOffset, byteOffset+byteLength)按视图切片
大图增益计算卡好几秒JS 逐像素遍历几百万像素原生applyScale先缩到 192px 网格再算梯度能量
切换规格后预览构图不变只改了 label,没重新裁切保留源图source,切换时prepareInput重新裁切
分割线两边画面错位两张图objectFit/尺寸/高度不一致都用Cover+ 同宽高,上层clip(true)裁split%
超分结果尺寸不是目标像素直接用 NPU 输出,没按预设裁切工作尺寸 = 目标/4,输出再coverTo到精确目标
PreBuild 报00303218user_grant 权限缺reason/usedScene补两字段
PreBuild 报00303038reason写了中文纯文本改$string:资源引用
打包报spawn java ENOENTshell 缺 JDK把 DevEco 自带jbr\bin加进PATH

十、总结

至此图像超分系列走完第六步:从「技术演示」走向「场景闭环」。

这一篇的核心不是又接了一个新 API,而是把端侧超分放进一个真实的产品场景里重新审视——壁纸有明确的尺寸和比例,超分不能再是「放大 4 倍看效果」,而要「精确产出 1440×3200 这块能直接设成壁纸的图」。为此我们反推出「工作尺寸 = 目标尺寸 / 4」的输入策略,用 Cover 适配解决任意比例的居中裁切,用保留源图 + 重新裁切让规格切换真正生效,再把前作里潜伏的三个运行时坑(get访问器崩溃、accessSync语义、Uint8Array偏移)一次性填平。

至此,从单图重建、实时放大镜、批量工作台、相册直选,到今天的壁纸适配,端侧超分这条线已经覆盖了「能力验证 → 交互增强 → 工程批量化 → 真实输入 → 场景落地」的完整路径。下一步可以继续往「设置成系统壁纸」这个最后一步走(需要setWallpaper相关能力),或者把裁切框做成可手动拖拽调整构图的交互——那就留给下一篇了。

工程里RealImageSuperResolutionService.ets保留了 Core Vision Kit 的官方接入参考,真机上把useReal置真即可切换到 NPU 推理;模拟器/不支持设备自动回退演示素材,保证任何环境都能完整体验。壁纸比例与尺寸的工作台已经搭好,剩下的就是把每一张图,都适配到它的屏幕上。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 9:04:55

产品设计端引入营销的小思考

摘要传统产品研发范式遵循「市场调研→需求文档→产品设计→工程实现→量产→营销推广」。其隐含假设是&#xff1a;产品供给先行&#xff0c;营销负责包装与说服。在内容电商生态下&#xff0c;这一假设正在失效。失效的根源不在于营销技巧不足&#xff0c;而在于产品的核心卖…

作者头像 李华
网站建设 2026/10/11 9:04:21

Java设计模式实战指南:从源码到框架,把背八股变成用得上

聊到Java设计模式&#xff0c;很多人的第一反应是23种模式的名字和定义&#xff0c;接着就是那句经典的感叹&#xff1a;背倒是背过&#xff0c;项目里真用不上。我这些年面过不少人&#xff0c;也被面过不少次&#xff0c;最深的感受是&#xff1a;设计模式面试题从来不是考你…

作者头像 李华
网站建设 2026/10/11 9:04:12

登记测试与验收测试报告:区别、风险与实操安排

1. 两个报告到底差在哪&#xff1a;先从名字背后的“出身”说起登记测试报告和验收测试报告&#xff0c;名字里都有“测试”两个字&#xff0c;但这两个东西从头到尾就不是一回事。我见过太多项目方&#xff0c;拿着登记测试报告去应付项目验收&#xff0c;结果被甲方打回来重新…

作者头像 李华
网站建设 2026/10/11 9:03:22

agent-skills 实战:从技能定义到编排,构建可落地的 AI 智能体执行体系

1. 从“会聊”到“会做”&#xff1a;agent-skills 到底在解决什么问题这两年跟不少做 AI 应用的朋友聊&#xff0c;大家有个共同的感受&#xff1a;模型本身越来越聪明&#xff0c;但真让它去干一件具体的事&#xff0c;往往还是“嘴上功夫”。你问它“帮我整理一下这周的会议…

作者头像 李华
网站建设 2026/10/11 8:57:25

PostgreSQL + pgvector + RRF 混合检索替代向量数据库的落地实践

这事得从一笔账单说起。去年年底&#xff0c;项目里的向量数据库服务快到期了&#xff0c;我看了眼续费单&#xff0c;再对照我们过去三个月的实际调用量&#xff0c;心底那杆秤就开始晃了。随后我花了一个周末&#xff0c;把基于 PostgreSQL 的方案搭了出来&#xff1a;pgvect…

作者头像 李华