图像超分的体验页很容易做成“两张图片,左边放原图,右边放清晰图”。但它真正容易暴露的问题并不是按钮样式,而是拖动分界线时山脊、建筑边缘会突然跳一下:明明同一场景,滑到中间就像换了拍摄角度。我将这类视觉误差单独做成了一个HarmonyOS ArkUI业务示例,叫作 CompareLens。它不执行超分推理,重点只解决原图和结果图怎样在同一裁切坐标系内对照。示例数据均为预设,并不等于设备测速或模型输出。

01

一、为什么拖的是滑杆,错的是图像坐标

最初我以为两层Image各自设置相同宽高就够了。后来把“裁切区域变窄”和“图片组件变窄”放在一起对照,发现它们根本不是同一回事。假设预览窗口固定为320×240vp,原始素材是1280×960,增强素材是2560×1920,二者都是4:3。用ImageFit.Cover铺进同一窗口时,两张图会经历等比例缩放;只要窗口始终是320×240,它们的场景边界理论上能保持重合。

如果把增强图组件本身的宽度改成115vp,再让它独立使用ImageFit.Cover,Cover就可能按照新的115×240容器重新裁掉两侧。用户看到的不是“增强图露出了36%”,而是另一套画面裁切结果。山脊线不连续,问题并不出在Slider值,而出在两幅图不再共享相同的基准画框。这一点比滑块颜色、过渡动画重要得多。

项目将业务资源命名为sr_before与sr_after,必须使用同一张场景的两版图片。预览卡片宽320vp、高240vp;IMG-610的示例滑杆位置为36%,表示左侧增强图露出36%,右侧仍显示原图。为故意暴露边界问题,我还准备了宽高比不一致的IMG-611和高度为零的IMG-612两组元数据,页面只放行有效的一组,状态记为READY 1/3。这些编号会在手机画面和开发界面里原样出现。

二、先验证素材,而不是先调UI参数

很多超分结果虽然像素更大,却不一定保持原来的内容比例。用户可能在上传时手动裁图,模型调用链也可能按固定尺寸做中心裁剪。仅看“放大了两倍”很容易误判。CompareLens先对两版素材读取到的宽高进行独立校验:宽高都必须为正整数,宽高比差异小于千分之一。这里的阈值属于示例业务策略,不是ArkUI或图像超分SDK的官方门槛。

下面的函数不读取设备相册,它接收上游已经解析好的宽高元数据。这样设计是为了把“素材是否可比较”和“素材怎么读进来”隔离,接口权限、PixelMap生命周期和模型推理都不混入本次计算。业务层收到失败结果时保持当前预览,不让损坏元数据直接把组件尺寸变成零。

interface PhotoSize { width: number; height: number }

function validatePair(before: PhotoSize, after: PhotoSize): string {
const valid = (s: PhotoSize): boolean =>
Number.isInteger(s.width) && Number.isInteger(s.height) &&
s.width > 0 && s.height > 0;
if (!valid(before) || !valid(after)) return 'INVALID_SIZE';
const a = before.width / before.height;
const b = after.width / after.height;
if (Math.abs(a - b) > 0.001) return 'RATIO_MISMATCH';
return 'PASS';
}

const checks: string[] = [
validatePair({ width: 1280, height: 960 }, { width: 2560, height: 1920 }),
validatePair({ width: 1280, height: 960 }, { width: 1920, height: 1080 }),
validatePair({ width: 1280, height: 960 }, { width: 0, height: 0 })
];
// 预期:PASS、RATIO_MISMATCH、INVALID_SIZE

这段校验不会判断清晰度提升是否真实,也无法发现两张照片内容不同但长宽比相同。因此我把返回值叫作“几何预检”,而不是“超分质量评分”。生产环境还应验证拍摄来源标识、旋转方向、色彩空间和解码是否成功;尤其对来自网络的图片,不能只相信JSON里声称的尺寸。

三、把滑杆的位置归一到固定画框

Slider的value是一个处于最小值与最大值之间的数。参考华为官方ArkUI Slider组件资料,onChange会给出当前数值以及交互阶段。这里设置0~100、步进1,使用@State存储百分比。用户每次拖动只改变“覆盖层显示区域”的宽度,不改两张图片各自的逻辑画框。比例计算必须做有限数校验与夹取,避免上游持久化值变成NaN、负数或者溢出100。

我把核心几何计算写成一个小函数,而不是把乘除法分散在UI链式调用中。这样下次从320vp切换到宽屏的480vp,只需改实际容器宽度;裁切和分界线的位置由同一个数派生,便不会出现分界线走了、内容却留在原地的第二种错位。

interface RevealGeometry { percent: number; widthVp: number }

function getRevealGeometry(value: number, frameVp: number): RevealGeometry {
const safe = Number.isFinite(value) ? Math.max(0, Math.min(100, value)) : 50;
const width = Number.isFinite(frameVp) && frameVp > 0 ? frameVp : 320;
return { percent: safe, widthVp: width * safe / 100 };
}

const geometry = getRevealGeometry(36, 320);
// 预期:percent=36,widthVp=115.2

在320vp窗口下,115.2vp就是增强区域应显示的宽度。0%时只能看到原图,100%时只能看到增强图,36%时两层的分界线落在相同的115.2vp处。这里使用vp作为布局单位,不用原图1280像素直接除以屏幕像素密度;否则在不同设备的缩放和像素密度下,触控位置会出现系统性的偏差。

四、ArkUI里保留两张完整图,再裁掉覆盖区域

页面结构采用Stack承载两张完整尺寸的Image,底层是原图,上层是增强图。两者的width、height和objectFit完全相同;上层Image只通过clipShape改变可见形状。Stack按照顺序叠放子组件的行为来自官方ArkUI文档。资源app.media.sr_before与app.media.sr_after需要由示例工程实际提供,本文代码只是页面关键部分,并未宣称脱离资源可以直接编译。

@Entry
@Component
struct CompareLensPage {
@State private ratio: number = 36;
private readonly frameWidth: number = 320;
private readonly frameHeight: number = 240;

build() {
Column({ space: 16 }) {
Text('CompareLens · IMG-610').fontSize(20).fontWeight(FontWeight.Bold)
Stack({ alignContent: Alignment.TopStart }) {
Image($r('app.media.sr_before'))
.width(this.frameWidth).height(this.frameHeight)
.objectFit(ImageFit.Cover)
Image($r('app.media.sr_after'))
.width(this.frameWidth).height(this.frameHeight)
.objectFit(ImageFit.Cover)
.clipShape(new Rect({
width: getRevealGeometry(this.ratio, this.frameWidth).widthVp,
height: this.frameHeight
}))
}.width(this.frameWidth).height(this.frameHeight)
Slider({ value: this.ratio, min: 0, max: 100, step: 1 })
.onChange((value: number, mode: SliderChangeMode) => {
this.ratio = getRevealGeometry(value, this.frameWidth).percent;
})
Text(`增强侧 ${this.ratio}% · READY 1/3`)
}.width('100%').alignItems(HorizontalAlign.Center)
}
}

02

这段代码的重点不是让Slider覆盖图像,而是让Slider仅控制一个可检验的数字。项目里要把画框的实际尺寸变成统一状态源,并把分界线也叠加在同一个Stack之内;上面省略了装饰分界线,避免视觉效果代码淹没裁切原理。注意不同API版本中裁剪形状的定位及组件测量细节,应以所用SDK实际预览为准,

五、失败路径和生命周期比拖动动画更值得检查

我给这页安排了四种手工验证路径。先拖到0和100,看裁切区间是否出现负宽度、越界或者多余白条;再快速来回拖动,看@State更新是不是只改变覆盖可见区,不重新生成超分任务;然后切换宽屏与窄屏,确保容器宽和裁切宽使用同一份测量值;最后换进一张比例不一致的结果图,看是否阻止进入比较页而不是用拉伸掩盖问题。

有两种容易被忽略的“看起来能用”。第一种是两版照片的EXIF旋转标记不同:尺寸虽然都写4:3,实际显示内容却一横一竖。第二种是增强后局部生成了新细节:滑杆边缘看上去有断层,可能是算法引入的真实内容差异,而不是UI裁切故障。工程上不应该把所有对不上都归咎于ImageFit.Cover,需要用同一来源、同一裁切盒和同一预处理版本的样本复查。

整个示例把图片资源放在应用内,因此不涉及相册读写授权。以后若使用PixelMap或从后台下载图片,应在页面不可见、切换资源时明确解除引用或按所属API约定释放,并用性能工具核对内存走势。@State里只保存滑杆比例和状态,尽量不塞大尺寸像素缓存。频繁滑动时也不该调用真实超分模型,推理应由独立流程完成,界面只消费已经验证的结果。

六、结果展示与裁切校验

第三张配图采用IMG-610样例,标题“超分对照”,显示原图1280×960、增强图2560×1920、增强侧36%、状态READY 1/3。山脊与湖岸在分界线两侧应当保持同一空间位置。若要向读者展示真实效果,下一步必须加入真实同源图片和SDK环境,并分别留存构建版本、设备型号及处理耗时。

03

这里的结论其实很朴素:别用改变图片组件宽度的方法去制造裁切效果。稳定的超分对照页要固定两层图像的坐标系,让比例只支配遮罩与指示线;进入这一步之前,先筛掉尺寸和长宽比明显不可信的素材。这样完成的是可信的“观看工具”,不是对超分算法质量的背书。它与前文涉及的批任务乱序隔离、缩略图固定卡槽都属于图像业务,却分别处理不同层次的工程问题。

七、官方文档及适用边界

本篇仅采用ArkUI已公开的Slider、Stack、Image、ImageFit.Cover和通用裁剪思想。官方资料:

真正接入图像超分SDK、真实PixelMap、图片权限与跨屏自适应时,还需参考目标API版本逐项验证。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐