ImageKnifePro 把所有图形变换收归到 C++ Native 层,分出 GPU 和 CPU 两条管线。GPU 管线通过 OpenGL ES 离屏渲染执行片段着色器,CPU 管线在像素缓冲区上直接操作或调用系统 API。两条管线统一挂在 TransformationBase 下面,由 MultiTransformation 做链式编排,对上层完全透明。

一、三层继承——Transformation、TransformationBase、TransformationGPU

最顶层的 Transformation 是纯接口,定义了两个纯虚函数:Transform(task) 执行变换,GetTransformInfo() 返回变换描述字符串。后者在缓存系统中用于生成 memory key,不同参数的同类变换需要返回不同的字符串(比如 "Default Blur:10""Default Blur:25"),以保证缓存不会混淆。

TransformationBase 承接 Transformation,把 Transform 实现为 final 方法。它的内部流程分三个阶段。BeforeProcess(task) 在整次变换开始前执行一次,GPU 变换在这里初始化 GL 环境,返回 false 则中止变换。然后根据帧数选择处理路径:单帧走 Process(pixelmapIn, pixelmapOut),多帧走 TransformAnimation(task, count) 逐帧循环调用 ProcessAfterProcess() 变换结束后执行一次,通过 EndCallback 的析构函数自动触发——保证即使 Process 中途 return 也能执行清理逻辑。

Process 是纯虚函数,子类在这里做实际的像素处理。输出参数 pixelmapOut 如果写回 nullptr,表示变换在原图上原地进行;如果创建了新的 PixelMap,TransformationBase 会用新的 ImageData 替换 task->product.imageData

TransformationGPUTransformationBase 和具体 GPU 变换之间插入了一层。它持有一个 shared_ptr<GPUTransform>,把三个钩子的实现模板化了:

bool BeforeProcess(std::shared_ptr<ImageKnifeTask> task) final {
    if (filter_->InitGL()) return true;
    task->EchoError("GL Init Failed");
    return false;
}

std::string Process(OH_PixelmapNative* pixelmapIn, OH_PixelmapNative **pixelmapOut) final {
    PixemapInfo info = ParsePixelmapInfoReadPixels(pixelmapIn);
    if (info.innerBuffer == nullptr) return "Read Pixels Failed";
    filter_->Rendering(info.innerBuffer, info.width, info.height);
    CreatePixelmapWithBuffer(info.innerBuffer, info, pixelmapOut);
    free(info.innerBuffer);
    return std::string();
}

void AfterProcess() final {
    filter_->DeleteGL();
}

具体的 GPU 变换子类(如 TransformationBlurTransformationBrighten)只需在构造函数中创建对应的 GPUFilter 并调用 SetGpuFilter,不再操心 GL 生命周期和像素读写。

classDiagram
    class Transformation {
        <<abstract>>
        +Transform(task) bool
        +GetTransformInfo() string
    }
    class TransformationBase {
        +Transform(task) bool [final]
        #BeforeProcess(task) bool
        #Process(in, out) string
        #AfterProcess()
        -TransformAnimation(task, count) bool
    }
    class TransformationGPU {
        -filter_ : GPUTransform
        +SetGpuFilter(filter)
        #BeforeProcess() [final]
        #Process() [final]
        #AfterProcess() [final]
    }
    class TransformationBlur
    class TransformationBrighten
    class TransformationGaussianBlur
    class TransformationCropCircle

    Transformation <|-- TransformationBase
    TransformationBase <|-- TransformationGPU
    TransformationBase <|-- TransformationGaussianBlur
    TransformationBase <|-- TransformationCropCircle
    TransformationGPU <|-- TransformationBlur
    TransformationGPU <|-- TransformationBrighten

二、GPUTransform——OpenGL ES 离屏渲染管线

GPUTransform 基类把离屏渲染拆成初始化、数据绑定、渲染执行、资源释放四个阶段。

InitGL() 依次执行三步。TextureInit() 创建两个纹理——imageTextureId_ 接收原始图像像素,fboTextureId_ 作为 FBO 的颜色附着点,两个纹理都设置了 GL_REPEAT 环绕模式和 GL_LINEAR 过滤模式。BufferInit() 创建三个 VBO 和一个 VAO:第一个 VBO 存顶点坐标(四个顶点覆盖 NDC 空间的 [-1,1] 范围),第二个 VBO 存纹理坐标,第三个 VBO 存索引数据(两个三角形组成一个四边形)。VAO 把顶点属性绑定到 location 0(vec3 位置)和 location 1(vec2 纹理坐标)。BufferInit 最后还创建了 FBO。CreateProgram() 编译链接着色器——顶点着色器由基类的 GetVertexShaderString() 提供(一个标准的直通着色器),片段着色器由子类的 GetFragmentShaderString() 返回。

着色器编译失败时会输出 GL InfoLog 用于调试。链接成功后直接 glUseProgram,并在 InitGL 中把纹理采样器 s_TextureMap 绑定到 GL_TEXTURE0。最后调用子类的 UniformValue() 设置滤镜参数。

Rendering() 是逐帧调用的渲染入口。处理 GIF 时 TransformationBase 会循环调用 Process,每帧都走一次 Rendering。具体步骤:先调用 UniformValueWithImageSize() 设置与图片尺寸相关的 uniform;然后 SetImageData() 把像素数据上传到 imageTextureId_,同时将 fboTextureId_ 附着到 FBO 并分配与图片同尺寸的存储;接着 glViewport 设置渲染区域,glDrawElements 执行渲染;最后 glReadPixels 从 FBO 读回处理后的像素到原 buffer 中。

glReadPixels 是一个同步调用,GPU 必须完成渲染管线的所有工作后才能返回数据。代码注释中提到了 PBO(Pixel Buffer Object)作为潜在优化方向——PBO 可以异步发起像素传输,减少 CPU 阻塞,但代价是多占一份显存。

DeleteGL() 按 Program、Texture、VBO、VAO、FBO 的顺序释放所有 GL 资源。它不销毁 EGL 上下文,因为上下文的生命周期由外层的 ImageKnifeEGL 管理。

三、13 种 GPU 滤镜

所有 GPU 滤镜都是 GPUTransform 的子类,核心工作量在于编写 GLSL 片段着色器。着色器字符串通过虚函数 GetFragmentShaderString() 返回,增加新滤镜只需添加一个子类,已有代码无需改动。

GPUBlurFilter 实现一维高斯模糊。权重函数 getWeight(i) 用高斯公式 (1/(sqrt(2*PI*sigma^2))) * exp(-i^2/(2*sigma^2)) 计算每个采样点的权重,sigma 取 blurRadius / 3.0。沿 blurOffset 方向正反各采样 blurRadius 次,加权求和得到输出颜色。

GPUKuwaharaFilter 把以当前像素为中心的方形区域分成四个象限,分别统计均值和方差,选方差最小的象限均值作为输出颜色——这种非线性滤波能保留边缘同时平滑区域内部细节。它的实现比其他滤镜复杂得多:需要在 UniformValueWithImageSize 中传入图片宽高计算纹素步进值。

GPUSwirlFilter 以极坐标方式旋转纹理采样坐标,angleradius 参数控制旋转强度和范围。GPUSketchFilterGPUToonFilter 需要 3x3 纹理采样顶点着色器来做 Sobel 边缘检测。其余滤镜如 Brighten、GrayScale、Invert、Sepia、Pixelation、Vignette、Mirror 的着色器都比较短小,核心逻辑只有几行 GLSL 代码。

TransformationBlur 为例,整个类的代码非常精简:

class TransformationBlur : public TransformationGPU {
public:
    explicit TransformationBlur(std::shared_ptr<TransformationOption> option)
    {
        int radius = defaultRadius;
        float offsetX = defaultOffset, offsetY = defaultOffset;
        if (option->f32.size() >= 1) radius = option->f32[0];
        if (option->f32.size() >= 3) {
            offsetX = option->f32[1];
            offsetY = option->f32[2];
        }
        info_ = "Default Blur:" + std::to_string(radius);
        SetGpuFilter(std::make_shared<GPUBlurFilter>(radius, offsetX, offsetY));
    }
};

TransformationOption.f32 读取参数,构造对应的 GPUFilter,设置变换信息字符串——三步完成一个滤镜的注册。

四、CPU 变换——逐像素操作与系统 API

继承自 TransformationBase 但不经过 TransformationGPU 的变换类在 CPU 上完成像素处理。按实现方式可以分成三类。

手写逐像素操作。 TransformationCropCircle 把图片裁成最大内切圆。算法先计算圆心和半径,然后按行预计算圆弧的左右边界。对于 y 从 0 到 centerY 的每一行,通过圆方程 dx = sqrt(r^2 - dy^2) 得到该行在圆弧上的 x 坐标范围。利用上下对称性同时处理两行。每行只遍历圆外像素(将 RGBA 四通道全部置 0),圆内像素保持不变。TransformationCropCircleWithBorder 在此基础上增加了边框绘制,预计算内外两组圆弧边界,中间区域填充用户指定的 RGB 颜色。

调用 PixelMap 系统 API。 TransformationCropSquare 直接调用 OH_PixelmapNative_Crop,取 min(width, height) 为正方形边长,居中裁切。没有像素拷贝,也不创建新的 PixelMap。TransformationCrop 支持 Top、Center、Bottom 三种裁切位置,先 OH_PixelmapNative_Scale 缩放再 OH_PixelmapNative_Crop 裁切。

调用 Effect 系统 API。 TransformationGaussianBlur 通过 native_effect/effect_filter.h 调用 HarmonyOS 原生的高斯模糊能力。由于 Effect API 非线程安全,内部用了一把静态 mutex 做串行化保护。与 GPU 版的 TransformationBlur 相比,TransformationGaussianBlur 做的是完整的二维高斯卷积,模糊效果更平滑。

Mask 变换。 TransformationMaskTransformationOption.imageSrc 加载遮罩图片。BeforeProcess 阶段判断来源类型——PixelMap 直接取引用,URI 或 Resource 通过 ImageKnifeDispatcher::LoadFromImageSourceSync 同步加载。Process 阶段把遮罩缩放到与目标图片同尺寸,逐像素遍历:遮罩 alpha 通道非零的像素覆盖到目标图片对应位置。

五、MultiTransformation——链式组合与 EGL 上下文管理

MultiTransformation 持有一个 vector<shared_ptr<Transformation>>,在 Transform() 中逐个执行变换。执行前检查请求是否已销毁或出现致命错误,支持中途中断。

EGL 上下文的位置是这个类的关键设计决策。Transform() 在进入循环前创建 ImageKnifeEGL 对象,是栈上局部变量:

void MultiTransformation::Transform(std::shared_ptr<ImageKnifeTask> task) {
    ImageKnifeEGL egl;
    IMAGE_KNIFE_CHECK(!egl.Init(), "EGL Init Failed");
    for (auto transformation : transformations_) {
        // ...
        bool result = transformation->Transform(task);
        // ...
    }
}

所有 GPU 变换在同一个线程、同一个 EGL 上下文中串联执行。GPU 变换的 BeforeProcess 只做纹理、Buffer、Program 的初始化,不创建 EGL 上下文;AfterProcess 只释放 GL 资源不销毁上下文。CPU 变换完全不感知 EGL 的存在,直接在 buffer 上操作。当 MultiTransformation 函数返回时,栈上的 ImageKnifeEGL 对象析构,EGL 上下文随之释放——RAII 保证了无论变换链中途中断还是正常结束都能正确清理。

这种分层让 GPU 和 CPU 变换可以混合串联。一条典型的变换链可以是:blur(GPU) -> crop_circle(CPU) -> brightness(GPU) -> crop_square(CPU)。GPU 变换通过 ParsePixelmapInfoReadPixels 读出像素交给 GPU 处理,CreatePixelmapWithBuffer 把结果写回新的 PixelMap。下一个 CPU 变换再从新的 imageData 中读出像素做处理。串联顺序由用户在配置中指定,框架不做任何限制。

sequenceDiagram
    participant MT as MultiTransformation
    participant EGL as ImageKnifeEGL (栈对象)
    participant GPU1 as TransformationBlur (GPU)
    participant CPU1 as TransformationCropCircle (CPU)
    participant GPU2 as TransformationBrighten (GPU)

    MT->>EGL: Init() 创建 EGL 上下文
    MT->>GPU1: Transform(task)
    GPU1->>GPU1: InitGL → Rendering → DeleteGL
    MT->>CPU1: Transform(task)
    CPU1->>CPU1: 逐像素裁圆 (不涉及 EGL)
    MT->>GPU2: Transform(task)
    GPU2->>GPU2: InitGL → Rendering → DeleteGL
    MT->>EGL: 函数返回 → 析构释放 EGL

MultiTransformation 还为每个变换采集了耗时信息(TransformationInfo),记录变换类型、开始时间、结束时间和是否成功,方便上层做性能监控。

六、transformation_factory——按类型创建变换

TransformationFactory::CreateTransformation 接收 shared_ptr<TransformationOption>,内部维护一个 unordered_map<TransformationType, Builder> 静态映射表。当前注册了 20 种变换类型:12 种走 GPU 管线(Blur、Brighten、GrayScale、Invert、Kuwahara、Pixelation、Sepia、Sketch、Swirl、Toon、Vignette、Mirror),6 种走 CPU 管线(GaussianBlur、CropCircle、CropCircleWithBorder、CropSquare、Crop 的三种模式、Mask),加上 UNKNOWN 返回 nullptr。

TransformationOption 包含三类参数。type 指定变换类型枚举。f32vector<float> 承载数值参数——模糊半径、亮度值、旋转角度、边框宽度、颜色 RGB 值等都通过这个数组传递,不同变换按约定的索引位置读取。imageSrc 用于 Mask 变换指定遮罩图片来源。

ImageKnifeDispatcher::TransformImage 中,单个变换和多重变换的调用路径有微妙差异:单个变换会在调用处创建 ImageKnifeEGL;多重变换则由 MultiTransformation::Transform 内部创建。两条路径都保证了 EGL 上下文在变换期间可用、变换结束后释放。

以上就是本篇内容的所有了~有什么问题欢迎在评论区提出


项目地址:ImageKnifePro

标签: none

添加新评论