HarmonyOS APP开发:图像超分辨率与画质增强
HarmonyOS APP开发:图像超分辨率与画质增强
核心要点:本文深入讲解HarmonyOS端侧图像超分辨率技术,涵盖ESRGAN/Real-ESRGAN/RCAN等模型原理,重点实现端侧超分推理引擎、多级画质增强流水线、实时视频超分方案,以及超分模型的轻量化与加速策略。
一、背景与动机
你翻出一张十年前的老照片,想打印出来挂墙上,可放大一看全是马赛克——因为那时候手机摄像头才500万像素。或者你在微信里收到一张缩略图,想看清上面的文字,但一放大就糊成一片。这些场景,都是图像超分辨率要解决的问题。
图像超分辨率(Super-Resolution,SR)就是"无中生有"——从一张低分辨率图像重建出高分辨率图像,而且重建出来的细节要看起来真实、自然。这不是简单的双线性插值放大(那只会让马赛克更大),而是AI模型根据学习到的先验知识,"猜"出缺失的细节。
端侧超分的应用场景非常丰富:老照片修复、视频通话画质增强、游戏画面超分、文档OCR预处理、卫星图像细节恢复……随着手机NPU算力的提升,端侧实时超分已经成为现实。但超分模型通常很重——ESRGAN有1600万参数,推理一张1080p图像需要数秒。怎么在端侧实现实时超分?怎么平衡画质和速度?这就是本文要回答的问题。
二、核心原理
2.1 超分辨率技术演进
超分辨率技术经历了从传统方法到深度学习的演进:
flowchart TB
A[图像超分辨率技术] --> B[传统方法]
A --> C[深度学习方法]
B --> B1[双线性/双三次插值<br/>速度快,细节模糊]
B --> B2[稀疏编码<br/>字典学习,效果有限]
B --> B3[邻域嵌入<br/>局部匹配,泛化差]
C --> C1[SRCNN<br/>开创性工作,3层CNN]
C --> C2[FSRCNN<br/>加速版,反卷积上采样]
C --> C3[ESRGAN<br/>生成对抗网络,细节丰富]
C --> C4[Real-ESRGAN<br/>真实世界退化模型]
C --> C5[RCAN<br/>通道注意力机制]
C --> C6[SwinIR<br/>Transformer架构]
classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
classDef error fill:#EF5350,stroke:#C62828,color:#fff
classDef info fill:#81C784,stroke:#388E3C,color:#000
classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000
class A primary
class B,B1,B2,B3 warning
class C,C1,C2 info
class C3,C4 purple
class C5,C6 error
2.2 ESRGAN核心架构
ESRGAN(Enhanced Super-Resolution GAN)是当前最流行的超分模型之一,它的核心组件:
- RRDB(Residual-in-Residual Dense Block):基本构建单元,包含3个Dense Block,每个Dense Block有5个卷积层,层间密集连接
- 感知损失:不逐像素比对,而是在VGG特征空间中比较,更符合人眼感知
- GAN损失:判别器引导生成器产生更真实的纹理细节
- 上采样模块:使用亚像素卷积(PixelShuffle)实现2×或4×放大
端侧部署时,我们只使用生成器部分(判别器只在训练时使用)。
2.3 超分模型的输入输出
超分模型的输入输出关系:
| 放大倍数 | 输入尺寸 | 输出尺寸 | 适用场景 |
|---|---|---|---|
| 2× | 128×128 | 256×256 | 视频通话增强 |
| 2× | 360×640 | 720×1280 | 视频流超分 |
| 4× | 64×64 | 256×256 | 缩略图放大 |
| 4× | 270×480 | 1080×1920 | 老照片修复 |
关键约束:输出尺寸 = 输入尺寸 × 放大倍数,所以输入越大,输出越大,推理越慢。端侧推荐使用2×放大,输入控制在360p以内。
2.4 分块超分策略
对于大尺寸图像,直接推理会超出NPU内存限制。解决方案是分块超分(Patch-based SR):
- 将大图切分为多个小块(如128×128)
- 每个小块独立超分
- 将超分后的小块拼合回大图
- 处理块间重叠区域,避免拼接缝隙
三、代码实战
3.1 超分辨率数据结构定义
// SRTypes.ets - 超分辨率数据结构定义
/**
* 超分辨率模型配置
*/
export interface SRConfig {
modelName: string; // 模型名称
modelPath: string; // 模型文件路径
inputWidth: number; // 输入宽度
inputHeight: number; // 输入高度
scaleFactor: number; // 放大倍数(2或4)
outputChannels: number; // 输出通道数(3=RGB)
tileSize: number; // 分块大小
tilePadding: number; // 块间重叠像素数
}
/**
* 超分结果
*/
export interface SRResult {
outputPixelMap: image.PixelMap | null; // 超分后的图像
inputSize: { width: number; height: number }; // 输入尺寸
outputSize: { width: number; height: number }; // 输出尺寸
timeMs: number; // 推理耗时
psnr?: number; // 峰值信噪比(有参考图时计算)
}
/**
* 画质增强参数
*/
export interface EnhancementParams {
denoise: boolean; // 是否降噪
denoiseStrength: number; // 降噪强度(0-1)
sharpen: boolean; // 是否锐化
sharpenStrength: number; // 锐化强度(0-1)
colorEnhance: boolean; // 是否色彩增强
colorSaturation: number; // 饱和度(0.5-2.0)
contrastEnhance: boolean; // 是否对比度增强
contrastFactor: number; // 对比度因子(0.5-2.0)
}
/**
* 默认画质增强参数
*/
export const DEFAULT_ENHANCEMENT: EnhancementParams = {
denoise: true,
denoiseStrength: 0.5,
sharpen: true,
sharpenStrength: 0.3,
colorEnhance: false,
colorSaturation: 1.0,
contrastEnhance: false,
contrastFactor: 1.0
};
// 引入image模块
import { image } from '@kit.ImageKit';
3.2 超分辨率推理引擎(含分块策略)
// SuperResolutionEngine.ets - 超分辨率推理引擎
import { mindspore } from '@kit.MindSporeLiteKit';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';
import { fs } from '@kit.CoreFileKit';
import { SRConfig, SRResult } from './SRTypes';
/**
* 图像超分辨率引擎
* 支持整图超分和分块超分两种模式
*/
export class SuperResolutionEngine {
private context: common.Context;
private config: SRConfig;
private session: mindspore.Session | null = null;
private model: mindspore.Model | null = null;
private isInitialized: boolean = false;
constructor(context: common.Context, config: SRConfig) {
this.context = context;
this.config = config;
}
/**
* 初始化超分推理引擎
*/
async initialize(): Promise<boolean> {
try {
const modelPath = await this.copyModelToSandbox();
const msContext: mindspore.Context = {};
const npuDevice: mindspore.DeviceInfo = {
deviceType: mindspore.DeviceType.kNPU,
enableFloat16: true
};
const cpuDevice: mindspore.DeviceInfo = {
deviceType: mindspore.DeviceType.kCPU,
enableFloat16: true,
cpuCores: [0, 1, 2, 3]
};
msContext.deviceInfos = [npuDevice, cpuDevice];
this.model = new mindspore.Model();
let result = this.model.loadModelFromFile(modelPath, msContext);
if (result !== mindspore.kMSStatusSuccess) {
msContext.deviceInfos = [cpuDevice];
result = this.model.loadModelFromFile(modelPath, msContext);
if (result !== mindspore.kMSStatusSuccess) {
console.error('[SREngine] 模型加载失败');
return false;
}
}
this.session = this.model.createSession(msContext);
this.isInitialized = this.session !== null;
if (this.isInitialized) {
console.info(`[SREngine] 初始化成功,${this.config.scaleFactor}×超分`);
}
return this.isInitialized;
} catch (error) {
console.error(`[SREngine] 初始化异常: ${error}`);
return false;
}
}
/**
* 拷贝模型到沙箱
*/
private async copyModelToSandbox(): Promise<string> {
const sandboxPath = `${this.context.filesDir}/${this.config.modelName}.ms`;
if (fs.accessSync(sandboxPath)) return sandboxPath;
const content = this.context.resourceMgr.getRawFileContentSync(`models/${this.config.modelName}.ms`);
const file = fs.openSync(sandboxPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
fs.writeSync(file.fd, content.buffer);
fs.closeSync(file);
return sandboxPath;
}
/**
* 执行超分辨率推理
* 自动判断是否需要分块
*/
async upscale(pixelMap: image.PixelMap): Promise<SRResult> {
if (!this.isInitialized || this.session === null) {
return this.emptyResult(pixelMap);
}
try {
const startTime = Date.now();
const imageInfo = pixelMap.getImageInfo();
const { width: imgW, height: imgH } = imageInfo.size;
const { inputWidth, inputHeight, scaleFactor } = this.config;
const outW = imgW * scaleFactor;
const outH = imgH * scaleFactor;
let outputPixelMap: image.PixelMap | null = null;
// 判断是否需要分块
if (imgW <= inputWidth && imgH <= inputHeight) {
// 小图:整图超分
outputPixelMap = await this.upscaleFull(pixelMap);
} else {
// 大图:分块超分
outputPixelMap = await this.upscaleTiled(pixelMap);
}
const timeMs = Date.now() - startTime;
console.info(`[SREngine] 超分完成: ${imgW}×${imgH} → ${outW}×${outH}, 耗时${timeMs}ms`);
return {
outputPixelMap: outputPixelMap,
inputSize: { width: imgW, height: imgH },
outputSize: { width: outW, height: outH },
timeMs: timeMs
};
} catch (error) {
console.error(`[SREngine] 超分失败: ${error}`);
return this.emptyResult(pixelMap);
}
}
/**
* 整图超分(小图使用)
*/
private async upscaleFull(pixelMap: image.PixelMap): Promise<image.PixelMap | null> {
const { inputWidth, inputHeight, scaleFactor, outputChannels } = this.config;
const imageInfo = pixelMap.getImageInfo();
// 预处理
const inputData = this.preprocessTile(pixelMap, 0, 0, imageInfo.size.width, imageInfo.size.height,
inputWidth, inputHeight);
// 推理
const inputs = this.session!.getInputs();
inputs[0].setData(inputData.buffer);
this.session!.run(inputs);
// 获取输出
const outputs = this.session!.getOutputs();
const outputData = new Float32Array(outputs[0].getData());
// 后处理:输出张量 → PixelMap
const outW = inputWidth * scaleFactor;
const outH = inputHeight * scaleFactor;
return this.postprocessTile(outputData, outW, outH, outputChannels);
}
/**
* 分块超分(大图使用)
* 将大图切分为多个小块,分别超分后拼合
*/
private async upscaleTiled(pixelMap: image.PixelMap): Promise<image.PixelMap | null> {
const { tileSize, tilePadding, scaleFactor, outputChannels, inputWidth, inputHeight } = this.config;
const imageInfo = pixelMap.getImageInfo();
const { width: imgW, height: imgH } = imageInfo.size;
const outW = imgW * scaleFactor;
const outH = imgH * scaleFactor;
// 创建输出PixelMap
const outputInfo: image.ImageInfo = {
size: { width: outW, height: outH },
pixelFormat: image.PixelFormat.RGBA_8888,
alphaType: image.AlphaType.OPAQUE
};
const outputPixelMap = image.createPixelMapSync(new ArrayBuffer(outW * outH * 4), outputInfo);
const outputPixels = new Uint8Array(outW * outH * 4);
// 计算分块数量
const effectiveTileSize = tileSize - 2 * tilePadding;
const tilesX = Math.ceil(imgW / effectiveTileSize);
const tilesY = Math.ceil(imgH / effectiveTileSize);
console.info(`[SREngine] 分块超分: ${tilesX}×${tilesY} = ${tilesX * tilesY}块`);
// 逐块处理
for (let ty = 0; ty < tilesY; ty++) {
for (let tx = 0; tx < tilesX; tx++) {
// 计算当前块在原图中的位置
const srcX = tx * effectiveTileSize;
const srcY = ty * effectiveTileSize;
const srcW = Math.min(tileSize, imgW - srcX);
const srcH = Math.min(tileSize, imgH - srcY);
// 预处理当前块
const inputData = this.preprocessTile(pixelMap, srcX, srcY, srcW, srcH,
inputWidth, inputHeight);
// 推理
const inputs = this.session!.getInputs();
inputs[0].setData(inputData.buffer);
this.session!.run(inputs);
// 获取输出
const outputs = this.session!.getOutputs();
const outputData = new Float32Array(outputs[0].getData());
// 后处理当前块
const tileOutW = srcW * scaleFactor;
const tileOutH = srcH * scaleFactor;
const tilePixelMap = this.postprocessTile(outputData, tileOutW, tileOutH, outputChannels);
if (tilePixelMap !== null) {
// 将当前块的结果写入输出图像
const tilePixels = new Uint8Array(tileOutW * tileOutH * 4);
tilePixelMap.readPixelsToBufferSync(tilePixels.buffer);
// 计算在输出图像中的位置
const dstX = srcX * scaleFactor;
const dstY = srcY * scaleFactor;
// 逐行拷贝(只拷贝有效区域,跳过padding)
const padPixels = tilePadding * scaleFactor;
for (let row = padPixels; row < tileOutH - padPixels; row++) {
const srcRowStart = (row * tileOutW + padPixels) * 4;
const dstRowStart = ((dstY + row - padPixels) * outW + dstX) * 4;
const copyWidth = Math.min((tileOutW - 2 * padPixels) * 4, (outW - dstX) * 4);
for (let i = 0; i < copyWidth; i++) {
outputPixels[dstRowStart + i] = tilePixels[srcRowStart + i];
}
}
tilePixelMap.release();
}
}
}
outputPixelMap.writeBufferToPixelsSync(outputPixels.buffer);
return outputPixelMap;
}
/**
* 预处理单个分块
* 从原图中裁剪指定区域,缩放到模型输入尺寸,归一化
*/
private preprocessTile(
pixelMap: image.PixelMap,
srcX: number, srcY: number,
srcW: number, srcH: number,
targetW: number, targetH: number
): Float32Array {
const imageInfo = pixelMap.getImageInfo();
const pixelBytes = new Uint8Array(imageInfo.size.width * imageInfo.size.height * 4);
pixelMap.readPixelsToBufferSync(pixelBytes.buffer);
const inputData = new Float32Array(3 * targetW * targetH);
for (let h = 0; h < targetH; h++) {
for (let w = 0; w < targetW; w++) {
// 映射到源图像中的位置
const origX = srcX + Math.floor(w * srcW / targetW);
const origY = srcY + Math.floor(h * srcH / targetH);
// 边界检查
const clampedX = Math.min(Math.max(origX, 0), imageInfo.size.width - 1);
const clampedY = Math.min(Math.max(origY, 0), imageInfo.size.height - 1);
const srcIdx = (clampedY * imageInfo.size.width + clampedX) * 4;
const r = pixelBytes[srcIdx] / 255.0;
const g = pixelBytes[srcIdx + 1] / 255.0;
const b = pixelBytes[srcIdx + 2] / 255.0;
// NCHW格式,归一化到[-1, 1]
const dstIdx = h * targetW + w;
inputData[0 * targetW * targetH + dstIdx] = r * 2.0 - 1.0;
inputData[1 * targetW * targetH + dstIdx] = g * 2.0 - 1.0;
inputData[2 * targetW * targetH + dstIdx] = b * 2.0 - 1.0;
}
}
return inputData;
}
/**
* 后处理:输出张量 → PixelMap
*/
private postprocessTile(
outputData: Float32Array,
outW: number, outH: number,
channels: number
): image.PixelMap | null {
try {
const imageInfo: image.ImageInfo = {
size: { width: outW, height: outH },
pixelFormat: image.PixelFormat.RGBA_8888,
alphaType: image.AlphaType.OPAQUE
};
const pixelMap = image.createPixelMapSync(new ArrayBuffer(outW * outH * 4), imageInfo);
const pixels = new Uint8Array(outW * outH * 4);
for (let h = 0; h < outH; h++) {
for (let w = 0; w < outW; w++) {
const pixelIdx = (h * outW + w) * 4;
// 从NCHW格式中提取RGB值,反归一化到[0, 255]
const rIdx = 0 * outW * outH + h * outW + w;
const gIdx = 1 * outW * outH + h * outW + w;
const bIdx = 2 * outW * outH + h * outW + w;
pixels[pixelIdx] = Math.min(255, Math.max(0, Math.round((outputData[rIdx] + 1.0) / 2.0 * 255)));
pixels[pixelIdx + 1] = Math.min(255, Math.max(0, Math.round((outputData[gIdx] + 1.0) / 2.0 * 255)));
pixels[pixelIdx + 2] = Math.min(255, Math.max(0, Math.round((outputData[bIdx] + 1.0) / 2.0 * 255)));
pixels[pixelIdx + 3] = 255;
}
}
pixelMap.writeBufferToPixelsSync(pixels.buffer);
return pixelMap;
} catch (error) {
console.error(`[SREngine] 后处理失败: ${error}`);
return null;
}
}
/**
* 返回空结果
*/
private emptyResult(pixelMap: image.PixelMap): SRResult {
const imageInfo = pixelMap.getImageInfo();
return {
outputPixelMap: null,
inputSize: { width: imageInfo.size.width, height: imageInfo.size.height },
outputSize: { width: 0, height: 0 },
timeMs: 0
};
}
release(): void {
if (this.session !== null) {
this.model?.freeSession(this.session);
this.session = null;
}
if (this.model !== null) {
this.model.freeModel();
this.model = null;
}
this.isInitialized = false;
}
}
3.3 画质增强流水线
超分之后,还可以通过传统图像处理进一步提升画质:
// ImageEnhancer.ets - 画质增强流水线
import { image } from '@kit.ImageKit';
import { EnhancementParams, DEFAULT_ENHANCEMENT } from './SRTypes';
/**
* 图像画质增强器
* 提供降噪、锐化、色彩增强、对比度增强等后处理能力
*/
export class ImageEnhancer {
/**
* 执行画质增强流水线
* 按顺序执行:降噪 → 锐化 → 色彩增强 → 对比度增强
*/
static enhance(pixelMap: image.PixelMap, params: EnhancementParams = DEFAULT_ENHANCEMENT): image.PixelMap | null {
try {
const imageInfo = pixelMap.getImageInfo();
const { width, height } = imageInfo.size;
// 读取像素数据
const pixels = new Uint8Array(width * height * 4);
pixelMap.readPixelsToBufferSync(pixels.buffer);
// 创建输出
let output = new Uint8Array(pixels);
// 第一步:降噪(简易高斯模糊降噪)
if (params.denoise) {
output = this.denoise(output, width, height, params.denoiseStrength);
}
// 第二步:锐化(Unsharp Mask)
if (params.sharpen) {
output = this.sharpen(output, width, height, params.sharpenStrength);
}
// 第三步:色彩增强
if (params.colorEnhance) {
output = this.enhanceColor(output, width, height, params.colorSaturation);
}
// 第四步:对比度增强
if (params.contrastEnhance) {
output = this.enhanceContrast(output, width, height, params.contrastFactor);
}
// 写入输出PixelMap
const outputInfo: image.ImageInfo = {
size: { width, height },
pixelFormat: image.PixelFormat.RGBA_8888,
alphaType: image.AlphaType.OPAQUE
};
const outputPixelMap = image.createPixelMapSync(new ArrayBuffer(width * height * 4), outputInfo);
outputPixelMap.writeBufferToPixelsSync(output.buffer);
return outputPixelMap;
} catch (error) {
console.error(`[ImageEnhancer] 增强失败: ${error}`);
return null;
}
}
/**
* 高斯模糊降噪
* 使用3×3高斯核进行卷积
*/
private static denoise(
pixels: Uint8Array, width: number, height: number, strength: number
): Uint8Array {
const output = new Uint8Array(pixels.length);
// 3×3高斯核权重
const kernel = [
1, 2, 1,
2, 4, 2,
1, 2, 1
];
const kernelSum = 16;
// 根据strength调整混合比例
// strength=0时不做处理,strength=1时完全使用模糊结果
const blendRatio = strength * 0.5;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const pixelIdx = (y * width + x) * 4;
for (let c = 0; c < 3; c++) {
let sum = 0;
let ki = 0;
// 3×3卷积
for (let ky = -1; ky <= 1; ky++) {
for (let kx = -1; kx <= 1; kx++) {
const ny = Math.min(Math.max(y + ky, 0), height - 1);
const nx = Math.min(Math.max(x + kx, 0), width - 1);
const nIdx = (ny * width + nx) * 4 + c;
sum += pixels[nIdx] * kernel[ki];
ki++;
}
}
const blurred = sum / kernelSum;
// 混合原图和模糊图
output[pixelIdx + c] = Math.round(pixels[pixelIdx + c] * (1 - blendRatio) + blurred * blendRatio);
}
output[pixelIdx + 3] = 255;
}
}
return output;
}
/**
* Unsharp Mask锐化
* 锐化 = 原图 + strength × (原图 - 模糊图)
*/
private static sharpen(
pixels: Uint8Array, width: number, height: number, strength: number
): Uint8Array {
const output = new Uint8Array(pixels.length);
// 先做模糊
const blurred = this.denoise(pixels, width, height, 1.0);
for (let i = 0; i < pixels.length; i += 4) {
for (let c = 0; c < 3; c++) {
// USM公式:output = original + strength * (original - blurred)
const diff = pixels[i + c] - blurred[i + c];
const sharpened = pixels[i + c] + strength * diff * 2.0;
output[i + c] = Math.min(255, Math.max(0, Math.round(sharpened)));
}
output[i + 3] = 255;
}
return output;
}
/**
* 色彩饱和度增强
* 在HSL空间中调整S通道
*/
private static enhanceColor(
pixels: Uint8Array, width: number, height: number, saturation: number
): Uint8Array {
const output = new Uint8Array(pixels.length);
for (let i = 0; i < pixels.length; i += 4) {
const r = pixels[i] / 255.0;
const g = pixels[i + 1] / 255.0;
const b = pixels[i + 2] / 255.0;
// RGB → HSL
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const l = (max + min) / 2;
let h = 0;
let s = 0;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
if (max === r) {
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
} else if (max === g) {
h = ((b - r) / d + 2) / 6;
} else {
h = ((r - g) / d + 4) / 6;
}
}
// 调整饱和度
s = Math.min(1, s * saturation);
// HSL → RGB
const [nr, ng, nb] = this.hslToRgb(h, s, l);
output[i] = Math.round(nr * 255);
output[i + 1] = Math.round(ng * 255);
output[i + 2] = Math.round(nb * 255);
output[i + 3] = 255;
}
return output;
}
/**
* HSL → RGB转换
*/
private static hslToRgb(h: number, s: number, l: number): number[] {
if (s === 0) {
return [l, l, l];
}
const hue2rgb = (p: number, q: number, t: number): number => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
return [
hue2rgb(p, q, h + 1 / 3),
hue2rgb(p, q, h),
hue2rgb(p, q, h - 1 / 3)
];
}
/**
* 对比度增强
* 使用线性变换调整对比度
*/
private static enhanceContrast(
pixels: Uint8Array, width: number, height: number, factor: number
): Uint8Array {
const output = new Uint8Array(pixels.length);
const midpoint = 128; // 对比度中点
for (let i = 0; i < pixels.length; i += 4) {
for (let c = 0; c < 3; c++) {
// 对比度公式:output = factor × (input - midpoint) + midpoint
const adjusted = factor * (pixels[i + c] - midpoint) + midpoint;
output[i + c] = Math.min(255, Math.max(0, Math.round(adjusted)));
}
output[i + 3] = 255;
}
return output;
}
}
3.4 完整的超分辨率与画质增强页面
// SuperResolutionPage.ets - 超分辨率与画质增强完整页面
import { image } from '@kit.ImageKit';
import { picker } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { SRConfig, SRResult, EnhancementParams, DEFAULT_ENHANCEMENT } from './SRTypes';
import { SuperResolutionEngine } from './SuperResolutionEngine';
import { ImageEnhancer } from './ImageEnhancer';
@Entry
@Component
struct SuperResolutionPage {
@State originalImage: PixelMap | null = null;
@State srResultImage: PixelMap | null = null;
@State enhancedImage: PixelMap | null = null;
@State isLoading: boolean = false;
@State isEngineReady: boolean = false;
@State scaleFactor: number = 2;
@State srTimeMs: number = 0;
@State inputSize: string = '';
@State outputSize: string = '';
@State showEnhanced: boolean = false;
// 画质增强参数
@State denoiseEnabled: boolean = true;
@State denoiseStrength: number = 50;
@State sharpenEnabled: boolean = true;
@State sharpenStrength: number = 30;
@State colorSaturation: number = 100;
@State contrastFactor: number = 100;
private srEngine: SuperResolutionEngine | null = null;
aboutToAppear() {
this.initEngine();
}
aboutToDisappear() {
this.srEngine?.release();
}
async initEngine() {
const context = getContext(this) as common.Context;
const config: SRConfig = {
modelName: 'esrgan_x2',
modelPath: 'esrgan_x2.ms',
inputWidth: 128,
inputHeight: 128,
scaleFactor: 2,
outputChannels: 3,
tileSize: 128,
tilePadding: 8
};
this.srEngine = new SuperResolutionEngine(context, config);
this.isEngineReady = await this.srEngine.initialize();
}
async pickAndUpscale() {
if (!this.isEngineReady || this.srEngine === null) return;
try {
this.isLoading = true;
this.srResultImage = null;
this.enhancedImage = null;
this.showEnhanced = false;
const photoSelectOptions = new picker.PhotoSelectOptions();
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
const photoViewPicker = new picker.PhotoViewPicker();
const result = await photoViewPicker.select(photoSelectOptions);
if (result.photoUris.length === 0) {
this.isLoading = false;
return;
}
// 解码图像
const imageSource = image.createImageSource(result.photoUris[0]);
const pixelMap = await imageSource.createPixelMap();
this.originalImage = pixelMap;
const imageInfo = pixelMap.getImageInfo();
this.inputSize = `${imageInfo.size.width}×${imageInfo.size.height}`;
// 执行超分
const srResult = await this.srEngine.upscale(pixelMap);
this.srResultImage = srResult.outputPixelMap;
this.srTimeMs = srResult.timeMs;
this.outputSize = `${srResult.outputSize.width}×${srResult.outputSize.height}`;
this.isLoading = false;
} catch (error) {
console.error(`[Page] 超分失败: ${error}`);
this.isLoading = false;
}
}
/**
* 应用画质增强
*/
applyEnhancement() {
if (this.srResultImage === null) return;
const params: EnhancementParams = {
denoise: this.denoiseEnabled,
denoiseStrength: this.denoiseStrength / 100,
sharpen: this.sharpenEnabled,
sharpenStrength: this.sharpenStrength / 100,
colorEnhance: this.colorSaturation !== 100,
colorSaturation: this.colorSaturation / 100,
contrastEnhance: this.contrastFactor !== 100,
contrastFactor: this.contrastFactor / 100
};
this.enhancedImage = ImageEnhancer.enhance(this.srResultImage!, params);
this.showEnhanced = true;
}
build() {
Scroll() {
Column() {
// 标题栏
Row() {
Text('图像超分辨率')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
}
.width('100%')
.height(56)
.justifyContent(FlexAlign.Center)
.backgroundColor('#1A1A2E')
// 图像对比展示
Row() {
// 原图
Column() {
Text('原图')
.fontSize(12)
.fontColor('#AAAAAA')
.margin({ bottom: 4 })
if (this.originalImage !== null) {
Image(this.originalImage)
.width(160)
.height(160)
.objectFit(ImageFit.Contain)
.borderRadius(8)
} else {
Column() {
Text('📷')
.fontSize(32)
}
.width(160)
.height(160)
.justifyContent(FlexAlign.Center)
.borderRadius(8)
.backgroundColor('#1E1E3A')
}
}
// 箭头
Column() {
Text('→')
.fontSize(24)
.fontColor('#4FC3F7')
.fontWeight(FontWeight.Bold)
}
.justifyContent(FlexAlign.Center)
.height(180)
// 超分结果
Column() {
Text(this.showEnhanced ? '增强后' : `${this.scaleFactor}×超分`)
.fontSize(12)
.fontColor('#4FC3F7')
.margin({ bottom: 4 })
if (this.showEnhanced && this.enhancedImage !== null) {
Image(this.enhancedImage)
.width(160)
.height(160)
.objectFit(ImageFit.Contain)
.borderRadius(8)
} else if (this.srResultImage !== null) {
Image(this.srResultImage)
.width(160)
.height(160)
.objectFit(ImageFit.Contain)
.borderRadius(8)
} else {
Column() {
Text('✨')
.fontSize(32)
}
.width(160)
.height(160)
.justifyContent(FlexAlign.Center)
.borderRadius(8)
.backgroundColor('#1E1E3A')
}
}
}
.width('100%')
.justifyContent(FlexAlign.Center)
.margin({ top: 16 })
// 尺寸信息
if (this.inputSize && this.outputSize) {
Row() {
Text(`${this.inputSize}`)
.fontSize(13)
.fontColor('#AAAAAA')
Text(' → ')
.fontSize(13)
.fontColor('#666666')
Text(`${this.outputSize}`)
.fontSize(13)
.fontColor('#4FC3F7')
Text(` 耗时 ${this.srTimeMs}ms`)
.fontSize(12)
.fontColor('#888888')
}
.margin({ top: 8 })
}
// 操作按钮
Row() {
Button('选择图片超分')
.width(160)
.height(44)
.fontSize(14)
.backgroundColor('#4FC3F7')
.fontColor('#1A1A2E')
.borderRadius(22)
.enabled(this.isEngineReady && !this.isLoading)
.onClick(() => this.pickAndUpscale())
Button('应用增强')
.width(120)
.height(44)
.fontSize(14)
.backgroundColor('#81C784')
.fontColor('#1A1A2E')
.borderRadius(22)
.enabled(this.srResultImage !== null)
.onClick(() => this.applyEnhancement())
.margin({ left: 12 })
}
.margin({ top: 16 })
// 画质增强参数面板
if (this.srResultImage !== null) {
Column() {
Text('画质增强参数')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FFFFFF')
.margin({ bottom: 12 })
// 降噪
Row() {
Toggle({ type: ToggleType.Checkbox, isOn: this.denoiseEnabled })
.onChange((value: boolean) => { this.denoiseEnabled = value })
.selectedColor('#4FC3F7')
Text('降噪强度')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 8 })
.layoutWeight(1)
Slider({ value: this.denoiseStrength, min: 0, max: 100, step: 5 })
.width(120)
.trackColor('#2A2A4A')
.selectedColor('#4FC3F7')
.onChange((value: number) => { this.denoiseStrength = value })
}
.width('100%')
.height(36)
.alignItems(VerticalAlign.Center)
// 锐化
Row() {
Toggle({ type: ToggleType.Checkbox, isOn: this.sharpenEnabled })
.onChange((value: boolean) => { this.sharpenEnabled = value })
.selectedColor('#4FC3F7')
Text('锐化强度')
.fontSize(13)
.fontColor('#CCCCCC')
.margin({ left: 8 })
.layoutWeight(1)
Slider({ value: this.sharpenStrength, min: 0, max: 100, step: 5 })
.width(120)
.trackColor('#2A2A4A')
.selectedColor('#4FC3F7')
.onChange((value: number) => { this.sharpenStrength = value })
}
.width('100%')
.height(36)
.alignItems(VerticalAlign.Center)
// 色彩饱和度
Row() {
Text('色彩饱和度')
.fontSize(13)
.fontColor('#CCCCCC')
.layoutWeight(1)
Slider({ value: this.colorSaturation, min: 50, max: 200, step: 10 })
.width(120)
.trackColor('#2A2A4A')
.selectedColor('#CE93D8')
.onChange((value: number) => { this.colorSaturation = value })
Text(`${this.colorSaturation}%`)
.fontSize(12)
.fontColor('#CE93D8')
.width(40)
}
.width('100%')
.height(36)
.alignItems(VerticalAlign.Center)
// 对比度
Row() {
Text('对比度')
.fontSize(13)
.fontColor('#CCCCCC')
.layoutWeight(1)
Slider({ value: this.contrastFactor, min: 50, max: 200, step: 10 })
.width(120)
.trackColor('#2A2A4A')
.selectedColor('#FFB74D')
.onChange((value: number) => { this.contrastFactor = value })
Text(`${this.contrastFactor}%`)
.fontSize(12)
.fontColor('#FFB74D')
.width(40)
}
.width('100%')
.height(36)
.alignItems(VerticalAlign.Center)
}
.width('92%')
.padding(16)
.borderRadius(12)
.backgroundColor('#16213E')
.margin({ top: 16 })
.alignItems(HorizontalAlign.Start)
}
if (this.isLoading) {
Column() {
LoadingProgress()
.width(48)
.height(48)
.color('#4FC3F7')
Text('正在超分处理...')
.fontSize(14)
.fontColor('#AAAAAA')
.margin({ top: 8 })
}
.margin({ top: 20 })
}
}
.width('100%')
}
.width('100%')
.height('100%')
.backgroundColor('#0F0F23')
}
}
四、踩坑与注意事项
4.1 分块拼接缝隙
坑:分块超分后,块与块之间出现明显的拼接缝隙。
原因:卷积操作在图像边缘会有边界效应,导致边缘像素的值不准确。如果直接拼接,相邻块的边缘不一致就会产生缝隙。
解:
- 增加重叠区域:每个块向四周扩展
tilePadding个像素(推荐8-16像素),拼接时只取中心区域 - 渐变融合:在重叠区域使用线性渐变权重混合两个块的结果
- 减小分块尺寸:块越小,边界效应影响的区域占比越大,所以分块不宜太小
4.2 超分结果色彩偏移
坑:超分后的图像颜色和原图不一致,偏暖或偏冷。
原因:模型训练时的归一化方式和推理时不一致,或者模型本身存在色彩偏移(GAN模型常见)。
解:
- 确保预处理和训练时一致([-1,1] vs [0,1] vs ImageNet归一化)
- 超分后做色彩校正:将超分结果的亮度和色度通道对齐到原图
- 使用YCbCr空间:只在Y通道做超分,CbCr通道用双线性插值上采样
// YCbCr空间超分:只超分亮度通道
function upscaleInYCbCr(srEngine: SuperResolutionEngine, pixelMap: PixelMap): PixelMap | null {
// 1. RGB → YCbCr
// 2. Y通道用模型超分
// 3. Cb/Cr通道用双线性插值
// 4. YCbCr → RGB
// 好处:避免色彩偏移,且CbCr通道不需要模型推理,速度更快
}
4.3 内存溢出
坑:超分4K图像时,应用直接崩溃。
原因:一张4K图像(3840×2160)的RGBA数据占32MB,超分2×后变成128MB,加上中间张量,轻松超过应用内存限制。
解:
- 限制最大输入尺寸(如1080p),超过则先降采样
- 使用分块策略,每次只处理一小块
- 及时释放中间PixelMap
- 使用
image.createPixelMapSync的同步版本减少异步开销
4.4 模型选择策略
坑:直接使用ESRGAN原始模型,推理一张720p图像需要30秒+。
解:端侧必须使用轻量化模型:
| 模型 | 参数量 | 720p推理速度 | 画质 | 适用场景 |
|---|---|---|---|---|
| ESRGAN | 16.7M | 30s+ | 最佳 | 离线处理 |
| Real-ESRGAN-x2plus | 16.7M | 30s+ | 优秀 | 离线处理 |
| RCAN-light | 0.3M | 200ms | 良好 | 实时处理 |
| FSRCNN | 0.02M | 50ms | 可用 | 视频超分 |
| ESPCN | 0.02M | 30ms | 可用 | 视频流 |
端侧推荐RCAN-light或FSRCNN,在速度和画质之间取得平衡。
五、HarmonyOS 6适配
5.1 新增特性
| 特性 | 说明 |
|---|---|
| 系统超分API | @kit.ImageEffectKit内置超分能力 |
| 视频流超分 | 相机预览流实时超分 |
| NPU专用超分算子 | PixelShuffle等算子NPU加速 |
| 智能分块 | 框架自动管理分块策略 |
| 质量自适应 | 根据设备算力自动选择模型 |
5.2 迁移指南
- 系统超分API(HarmonyOS 6推荐):
// HarmonyOS 6 系统级超分
import { imageEffect } from '@kit.ImageEffectKit';
const srEffect = imageEffect.createSuperResolutionEffect({
scaleFactor: 2, // 2×放大
quality: imageEffect.Quality.HIGH // 画质优先
});
const result = await srEffect.process(pixelMap);
- 视频流超分:
// HarmonyOS 6 相机预览流超分
const cameraSR = imageEffect.createCameraSuperResolution({
scaleFactor: 2,
targetFps: 30
});
cameraSR.on('frame', (enhancedFrame: PixelMap) => {
// 每帧超分结果
this.updatePreview(enhancedFrame);
});
cameraSR.start(cameraPreviewStream);
- 质量自适应:
// HarmonyOS 6 自动选择最优模型
const adaptiveSR = imageEffect.createAdaptiveSuperResolution({
targetLatency: 100 // 目标延迟100ms
});
// 框架根据设备算力自动选择合适的模型
const result = await adaptiveSR.process(pixelMap);
六、总结
本文完整讲解了HarmonyOS端侧图像超分辨率与画质增强的技术方案,核心知识点回顾:
图像超分辨率与画质增强
├── 超分模型
│ ├── ESRGAN:细节最丰富,但太慢
│ ├── Real-ESRGAN:真实世界退化,离线首选
│ ├── RCAN-light:端侧实时首选
│ └── FSRCNN/ESPCN:视频流超分
├── 核心流程
│ ├── 预处理:Resize + NCHW + 归一化
│ ├── 推理:整图 or 分块
│ └── 后处理:反归一化 + NCHW→HWC
├── 分块策略
│ ├── 大图切分为小块独立超分
│ ├── 重叠区域避免拼接缝隙
│ ├── 渐变融合消除边界
│ └── 分块不宜太小(128+)
├── 画质增强流水线
│ ├── 降噪:高斯模糊 + 混合
│ ├── 锐化:Unsharp Mask
│ ├── 色彩增强:HSL空间调S通道
│ └── 对比度增强:线性变换
├── 性能优化
│ ├── 轻量化模型选择
│ ├── YCbCr空间只超分Y通道
│ ├── 分块控制内存
│ └── NPU加速PixelShuffle(HarmonyOS 6)
└── 踩坑要点
├── 分块拼接缝隙
├── 超分色彩偏移
├── 大图内存溢出
└── 模型速度与画质权衡
一句话总结:端侧超分辨率的本质是"在有限算力下尽可能还原细节"——选择合适的轻量化模型是基础,分块策略是处理大图的关键,YCbCr空间超分是避免色彩偏移的技巧,而画质增强流水线则是锦上添花的最后一公里。记住一个原则:超分不是万能的,它只能"猜"出合理的细节,不能凭空创造不存在的真实信息。选择正确的放大倍数(2×比4×更靠谱)和合适的模型,比一味追求高倍率更重要。
- 点赞
- 收藏
- 关注作者
评论(0)