HarmonyOS APP开发:图像超分辨率与画质增强

举报
Jack20 发表于 2026/06/21 13:57:57 2026/06/21
【摘要】 HarmonyOS APP开发:图像超分辨率与画质增强核心要点:本文深入讲解HarmonyOS端侧图像超分辨率技术,涵盖ESRGAN/Real-ESRGAN/RCAN等模型原理,重点实现端侧超分推理引擎、多级画质增强流水线、实时视频超分方案,以及超分模型的轻量化与加速策略。 一、背景与动机你翻出一张十年前的老照片,想打印出来挂墙上,可放大一看全是马赛克——因为那时候手机摄像头才500万像素...

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/>开创性工作,3CNN]
    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 超分模型的输入输出

超分模型的输入输出关系:

放大倍数 输入尺寸 输出尺寸 适用场景
128×128 256×256 视频通话增强
360×640 720×1280 视频流超分
64×64 256×256 缩略图放大
270×480 1080×1920 老照片修复

关键约束:输出尺寸 = 输入尺寸 × 放大倍数,所以输入越大,输出越大,推理越慢。端侧推荐使用2×放大,输入控制在360p以内。

2.4 分块超分策略

对于大尺寸图像,直接推理会超出NPU内存限制。解决方案是分块超分(Patch-based SR)

  1. 将大图切分为多个小块(如128×128)
  2. 每个小块独立超分
  3. 将超分后的小块拼合回大图
  4. 处理块间重叠区域,避免拼接缝隙

三、代码实战

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 分块拼接缝隙

:分块超分后,块与块之间出现明显的拼接缝隙。

原因:卷积操作在图像边缘会有边界效应,导致边缘像素的值不准确。如果直接拼接,相邻块的边缘不一致就会产生缝隙。

  1. 增加重叠区域:每个块向四周扩展tilePadding个像素(推荐8-16像素),拼接时只取中心区域
  2. 渐变融合:在重叠区域使用线性渐变权重混合两个块的结果
  3. 减小分块尺寸:块越小,边界效应影响的区域占比越大,所以分块不宜太小

4.2 超分结果色彩偏移

:超分后的图像颜色和原图不一致,偏暖或偏冷。

原因:模型训练时的归一化方式和推理时不一致,或者模型本身存在色彩偏移(GAN模型常见)。

  1. 确保预处理和训练时一致([-1,1] vs [0,1] vs ImageNet归一化)
  2. 超分后做色彩校正:将超分结果的亮度和色度通道对齐到原图
  3. 使用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,加上中间张量,轻松超过应用内存限制。

  1. 限制最大输入尺寸(如1080p),超过则先降采样
  2. 使用分块策略,每次只处理一小块
  3. 及时释放中间PixelMap
  4. 使用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-lightFSRCNN,在速度和画质之间取得平衡。


五、HarmonyOS 6适配

5.1 新增特性

特性 说明
系统超分API @kit.ImageEffectKit内置超分能力
视频流超分 相机预览流实时超分
NPU专用超分算子 PixelShuffle等算子NPU加速
智能分块 框架自动管理分块策略
质量自适应 根据设备算力自动选择模型

5.2 迁移指南

  1. 系统超分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);
  1. 视频流超分
// HarmonyOS 6 相机预览流超分
const cameraSR = imageEffect.createCameraSuperResolution({
  scaleFactor: 2,
  targetFps: 30
});
cameraSR.on('frame', (enhancedFrame: PixelMap) => {
  // 每帧超分结果
  this.updatePreview(enhancedFrame);
});
cameraSR.start(cameraPreviewStream);
  1. 质量自适应
// 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 分块
│   └── 后处理:反归一化 + NCHWHWC
├── 分块策略
│   ├── 大图切分为小块独立超分
│   ├── 重叠区域避免拼接缝隙
│   ├── 渐变融合消除边界
│   └── 分块不宜太小(128+)
├── 画质增强流水线
│   ├── 降噪:高斯模糊 + 混合
│   ├── 锐化:Unsharp Mask
│   ├── 色彩增强:HSL空间调S通道
│   └── 对比度增强:线性变换
├── 性能优化
│   ├── 轻量化模型选择
│   ├── YCbCr空间只超分Y通道
│   ├── 分块控制内存
│   └── NPU加速PixelShuffle(HarmonyOS 6)
└── 踩坑要点
    ├── 分块拼接缝隙
    ├── 超分色彩偏移
    ├── 大图内存溢出
    └── 模型速度与画质权衡

一句话总结:端侧超分辨率的本质是"在有限算力下尽可能还原细节"——选择合适的轻量化模型是基础,分块策略是处理大图的关键,YCbCr空间超分是避免色彩偏移的技巧,而画质增强流水线则是锦上添花的最后一公里。记住一个原则:超分不是万能的,它只能"猜"出合理的细节,不能凭空创造不存在的真实信息。选择正确的放大倍数(2×比4×更靠谱)和合适的模型,比一味追求高倍率更重要。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。