OpenCV 图像调色优化实录:基于图像金字塔的 RAW / HEIC 文件加载与调色实践

一、引言:为什么调色"看似简单,实则复杂"

在大多数图像编辑软件中,调色功能往往以滑块形式呈现,用户只需拖动滑杆,就能快速调整曝光、色温、饱和度、对比度等参数。这似乎只是一些简单的数值微调,背后逻辑也无非是对像素值做加减乘除。然而,真正开发一个高质量的调色模块,却远非表面那样轻松。

在实际应用中,调色模块需要同时满足以下几个严苛的目标:

  • 输出图像质量高,不引入瑕疵(如色带、噪声放大);
  • 调色行为可预测,用户操作后应有"直觉的一致性"反馈;
  • 性能高效,适用于大图、RAW 图、批量处理等使用场景。

这些目标往往互相制约。例如,为了避免颜色断层(banding),可能需要引入 LUT 插值或 gamma 空间调整,而这些操作又会增加计算开销,或引发不同色域下的数值偏差。

我在做图像编辑器 Monica (github.com/fengzhizi71...) 时,早期的调色模块仅实现了 HSV 调整和简单的对比度控制,虽然逻辑清晰,但在高分辨率图像和连续处理操作中,逐渐暴露出性能瓶颈,且难以精确控制局部区域。仅靠 naive 的像素遍历方式,难以支撑专业用户对质量与效率的双重要求。

二、初始方案回顾:C++ 从 forEach 到并行 + LUT

在 Monica 的调色模块开发初期,使用了最直接的方式实现图像调整逻辑 ------ OpenCV 的 Mat::forEach 方法。每一个像素的调色操作(如色温调整、饱和度增强、高光阴影处理等)都以函数形式在 forEach 中完成。这种写法直观易懂,代码结构清晰,尤其适合快速验证算法的正确性。然而,在实际使用中,forEach 带来的性能瓶颈逐渐暴露出来:在面对超分辨率的大图时,即便只是简单的色温微调,依然会带来明显的延迟感。尤其是在桌面端同步预览调色效果时,用户对交互性能的要求远高于移动端,传统逐像素计算方式已无法满足流畅性的基本要求。

为此,我开始对调色流程进行重构:

  • 预计算 LUT 表
  • 使用cv::LUT替代 forEach 操作
  • 引入cv::parallel_for_加速非线性模块

具体的方案可以看我之前的文章(OpenCV 图像调色优化实录:从 forEach 到并行 + LUT 提速之路)。

三、图像金字塔:高性能调色的关键结构

在调色模块开发中,性能与精度始终是一对难以调和的矛盾。特别是当开始支持大尺寸 RAW 与 HEIC 文件时,这个问题更加突出。一个典型的 RAW 文件往往高达 20MB 以上,解码后的分辨率轻松达到 6000×4000 或更高,直接在原始分辨率上做任何图像处理,性能瓶颈随即显现。

3.1 为什么需要图像金字塔?

在实际用户交互中,调色是一种"即时反馈"的过程。用户拖动滑块,希望看到颜色立刻发生变化。在这个过程中,真正要求"精度"的操作只有最终保存输出,而非每一次预览调整。为此,我们引入了图像金字塔(Image Pyramid)机制,将原始大图处理与预览小图展示逻辑有效分离,达到了如下几方面优化目标:

  • 加速预览处理速度 用户拖动滑块调整参数时,只对金字塔中第一级(通常是原图尺寸的 1/2)进行调色与渲染。图像尺寸缩小至原来的 1/2,计算量减少近 4 倍,极大提升了预览响应速度。

  • 保证最终图像精度 当用户点击"保存"时,我们再将调色参数应用到原始图像(图像金字塔中的底层),完成真正意义上的精细调色处理与输出,兼顾交互体验与结果质量。

  • 统一图像处理入口 金字塔结构本质上是对一张图像在不同分辨率下的封装。无论是预览、最终保存,还是导出缩略图,都可以基于 PyramidImage 对象统一处理逻辑,降低模块耦合度。

3.2 图像金字塔结构与实现方式

图像金字塔由若干级别的图像组成,每一级都是上一级的 1/2 尺寸(通过高斯降采样等方式生成)。在 Monica 的实现中,我设计了如下 PyramidImage 类:

cpp 复制代码
#pragma once

#include <opencv2/opencv.hpp>
#include <vector>
#include <memory>
#include <mutex>
#include <future>
#include <atomic>

class PyramidImage {
public:
    // 从解码后的图像构造(可用原图或预览图)
    explicit PyramidImage(const cv::Mat& image, int levels = 4);

    void waitForPyramid() const;

    bool isPyramidReady() const;

    // 更新原图(如解码完成后替换预览)
    void updateImage(const cv::Mat& newImage);

    // 获取原图
    cv::Mat getOriginal() const;

    // 获取指定层级(0 表示原图,levels-1 为最小图)
    cv::Mat getLevel(int level) const;

    // 获取预览图(默认返回第一层,非最后一层)
    cv::Mat getPreview() const;

    // 获取 pyramid 层级总数
    int getLevelCount() const;

private:
    void buildPyramidAsync();
    int computeValidLevels(const cv::Mat& image, int maxLevel) const;
    cv::Mat downsample(const cv::Mat& input);

    cv::Mat originalImage;
    std::vector<cv::Mat> pyramid;
    int numLevels;

    mutable std::mutex pyramidMutex;
    mutable std::shared_future<void> pyramidReadyFuture;
    mutable std::shared_ptr<std::promise<void>> pyramidReadyPromise;

    std::atomic<bool> isBuilding{false};
};
cpp 复制代码
#include "../../include/pyramid/PyramidImage.h"
#include <thread>
#include <algorithm>
#include <iostream>  // 可用于调试日志

PyramidImage::PyramidImage(const cv::Mat& image, int levels)
        : originalImage(image.clone()), numLevels(std::max(1, levels)) {
    buildPyramidAsync();
}

void PyramidImage::updateImage(const cv::Mat& newImage) {
    {
        std::lock_guard<std::mutex> lock(pyramidMutex);
        originalImage = newImage.clone();
    }
    buildPyramidAsync();
}

void PyramidImage::waitForPyramid() const {
    if (pyramidReadyFuture.valid()) {
        pyramidReadyFuture.wait();
    }
}

bool PyramidImage::isPyramidReady() const {
    return pyramidReadyFuture.valid() &&
           pyramidReadyFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready;
}

cv::Mat PyramidImage::getOriginal() const {
    std::lock_guard<std::mutex> lock(pyramidMutex);
    return originalImage.clone();
}

cv::Mat PyramidImage::getLevel(int level) const {
    waitForPyramid();
    std::lock_guard<std::mutex> lock(pyramidMutex);
    if (level < 0 || level >= static_cast<int>(pyramid.size()))
        return cv::Mat();
    return pyramid[level].clone();
}

cv::Mat PyramidImage::getPreview() const {
    waitForPyramid();
    std::lock_guard<std::mutex> lock(pyramidMutex);
    if (pyramid.empty()) return cv::Mat();
    return pyramid[std::min(1, static_cast<int>(pyramid.size() - 1))].clone();
}

int PyramidImage::getLevelCount() const {
    waitForPyramid();
    std::lock_guard<std::mutex> lock(pyramidMutex);
    return static_cast<int>(pyramid.size());
}

int PyramidImage::computeValidLevels(const cv::Mat& image, int maxLevel) const {
    int w = image.cols;
    int h = image.rows;
    int levels = 1;
    while (w > 64 && h > 64 && levels < maxLevel) {
        w /= 2;
        h /= 2;
        levels++;
    }
    return levels;
}

void PyramidImage::buildPyramidAsync() {
    std::lock_guard<std::mutex> lock(pyramidMutex);

    if (isBuilding.exchange(true)) return;

    pyramidReadyPromise = std::make_shared<std::promise<void>>();
    pyramidReadyFuture = pyramidReadyPromise->get_future().share();

    cv::Mat base = originalImage.clone();
    const int levels = computeValidLevels(base, numLevels);

    std::thread([this, base, levels]() {
        std::vector<cv::Mat> levelsVec(levels);

        try {
            if (base.empty()) {
                std::cerr << "[Pyramid] base image empty\n";
                pyramidReadyPromise->set_value();
                isBuilding = false;
                return;
            }

            levelsVec[0] = base;

            for (int i = 1; i < levels; ++i) {
                cv::Mat down;
                cv::pyrDown(levelsVec[i - 1], down);

                if (down.empty() || down.cols < 4 || down.rows < 4) {
                    break;
                }

                levelsVec[i] = down;
            }

            {
                std::lock_guard<std::mutex> lock(pyramidMutex);
                pyramid = std::move(levelsVec);
            }
        } catch (const std::exception& e) {
            std::cerr << "[Pyramid] Exception: " << e.what() << std::endl;
        } catch (...) {
            std::cerr << "[Pyramid] Unknown exception\n";
        }

        try {
            pyramidReadyPromise->set_value();
        } catch (...) {
            // 防止重复 set_value 抛异常
        }

        isBuilding = false;
    }).detach();
}

cv::Mat PyramidImage::downsample(const cv::Mat& input) {
    cv::Mat output;
    cv::pyrDown(input, output);
    return output;
}
  • 构造函数中会异步构建金字塔各级别,避免阻塞 UI 线程。
  • getPreview() 返回缩放图用于快速预览和交互操作。
  • updateImage() 会在调色完成后更新整个金字塔,保持各级一致性。

图像金字塔不仅是性能优化工具,更成为连接解码层、调色层与展示层之间的重要桥梁。

3.3 对 RAW / HEIC 的特别优化

对于 RAW 文件,在解码阶段提供 half_size 模式(LibRaw),快速获取一个缩略版本作为金字塔第一层;对于 HEIC 文件,通过 libheif 解码出的全尺寸图像再生成金字塔。统一封装后,不同格式的图像处理流程得以打通,减少了平台与格式之间的差异带来的额外复杂度。

四、格式解码与图像处理模块的解耦设计

在 Monica 的图像处理架构中,图像格式的解码逻辑与调色等图像后处理逻辑被明确地解耦。这种设计不仅提高了模块可维护性和复用性,也让不同图像格式(如 RAW、HEIC)可以统一进入调色管线,并且天然支持跨平台的 JNI 接入。

4.1 解耦的基本策略:解码只负责"还原像素",处理专注"改进像素"

Monica 遵循"职责分离"的基本设计原则:

  • 格式解码模块:负责从文件中读取图像(RAW / HEIC 等),并将其转换为 cv::Mat 图像矩阵,作为中间表示。
  • 图像处理模块:以 cv::Mat 为输入,执行如 LUT 应用、色相、色温、对比度等调色操作,最终输出处理后图像。
  • 二者通过 PyramidImage 作为桥梁:提供图像缓存、预览图、原图访问与更新等功能。

这种设计的一个明显优势是:调色逻辑可以复用于任意来源的图像,而不仅仅局限于某种格式的解码结果。

4.2 模块调用流程(JNI 端)

在 JNI 端,解码与调色流程被划分为三个主要阶段:

  • 解码并构建图像金字塔(PyramidImage)
  • 根据用户参数执行调色(ColorCorrection)
  • 保存结果

PyramidImage 作为中间缓存与调色入口,在整个调用链中扮演关键角色。

scss 复制代码
Java/Kotlin 层
   │
   ├─(首次加载 RAW 文件)─> decodeRawToBufferForPreView(path)
   │                       ├─ decodeRawInternal() → 使用 half_size 模式解码预览 RAW 图像
   │                       ├─ 构建 PyramidImage(金字塔结构)
   │                       ├─ doColorCorrection() → 使用 PyramidImage 对象对预览图进行调色
   │                       └─ decodeRawAndColorCorrection(path, nativePtr, settings, cppPtr) → 保存时重新加载原图并执行调色
   │ 
   ├─(首次加载 HEIC 文件)─> decodeHeif(path)
   │                       ├─ 构建 PyramidImage(金字塔结构)
   │                       ├─ doColorCorrection() → 使用 PyramidImage 对象对图像进行调色
   │                       └─ 保存时,调用 colorCorrectionWithPyramidImage(nativePtr, settings, cppPtr)
   │
   ├─(非首次 RAW / HEIC 图像)─> doColorCorrection()
   │                            ├─ 保存时,调用 colorCorrectionWithPyramidImage(nativePtr, settings, cppPtr)
   │                            ├─ 使用已构建的 PyramidImage
   │                            ├─ 从 PyramidImage 获取原图(非预览图)
   │                            └─ 执行调色,生成预览图并返回
   │
   └─返回预览图(ARGB 格式) → 传回 Java/Kotlin 层渲染

最终保存时返回给 Kotlin 层 DecodedPreviewImage 对象用于渲染/预览。

kotlin 复制代码
data class DecodedPreviewImage(
    val nativePtr: Long,  // 对应 MonicaImageProcess 中 PyramidImage 对象的指针地址
    val width: Int,
    val height: Int,
    val previewImage: IntArray
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as DecodedPreviewImage

        if (nativePtr != other.nativePtr) return false
        if (width != other.width) return false
        if (height != other.height) return false
        if (!previewImage.contentEquals(other.previewImage)) return false

        return true
    }

    override fun hashCode(): Int {
        var result = nativePtr.hashCode()
        result = 31 * result + width
        result = 31 * result + height
        result = 31 * result + previewImage.contentHashCode()
        return result
    }
}

在 Monica 应用中,针对 RAW、HEIC 这类格式,针对「首次加载」和「后续调色」做了区分优化。

4.2.1 RAW 文件的首次加载(低分辨率预览)

由于 RAW 图像文件可能非常大,首次加载时我们采取:

  • 先通过 decodeRawInternal() 获取预览模式尺寸图像
  • 构建 PyramidImage(图像金字塔)以支持后续预览缩放、调色
  • 执行调色并返回预览图像
  • 后续保存时,再加载原始尺寸图像用于更高质量导出

这一过程调用的是:

cpp 复制代码
libraw_processed_image_t* decodeRawInternal(const char *path, jboolean isPreview) {

    LibRaw rawProcessor;

    // 根据 isPreview 设置解码参数
    if (isPreview == JNI_TRUE) {
        rawProcessor.imgdata.params.half_size = 1;             // 快速预览模式(低分辨率)
        rawProcessor.imgdata.params.output_color = 0;          // 禁用色彩空间转换
        rawProcessor.imgdata.params.use_camera_matrix = 0;     // 禁用相机色彩矩阵转换
    } else {
        rawProcessor.imgdata.params.half_size = 0;             // 全尺寸解码
    }

    rawProcessor.imgdata.params.output_bps = 8;                // 输出 8-bit 图像(节省内存)
    rawProcessor.imgdata.params.use_camera_wb = 1;             // 使用相机白平衡
    rawProcessor.imgdata.params.no_auto_bright = 1;            // 禁用自动亮度增强

    if (rawProcessor.open_file(path) != LIBRAW_SUCCESS) {
        std::cerr << "LibRaw failed to open file: " << path << std::endl;
        return nullptr;
    }

    if (rawProcessor.unpack() != LIBRAW_SUCCESS) {
        std::cerr << "LibRaw failed to unpack file: " << path << std::endl;
        rawProcessor.recycle();
        return nullptr;
    }

    if (rawProcessor.dcraw_process() != LIBRAW_SUCCESS) {
        std::cerr << "LibRaw failed to process file: " << path << std::endl;
        rawProcessor.recycle();
        return nullptr;
    }

    libraw_processed_image_t *img = rawProcessor.dcraw_make_mem_image();
    if (!img || img->type != LIBRAW_IMAGE_BITMAP) {
        std::cerr << "LibRaw returned invalid image" << std::endl;
        rawProcessor.recycle();
        return nullptr;
    }

    rawProcessor.recycle();

    return img;
}

jobject decodeRawToBufferInternal(JNIEnv *env, jstring filePath, jboolean isPreview) {

    const char *path = env->GetStringUTFChars(filePath, nullptr);

    libraw_processed_image_t *img = decodeRawInternal(path, isPreview);

    if (img == nullptr) {
        env->ReleaseStringUTFChars(filePath, path);
        return nullptr;
    }

    // 构造 cv::Mat
    int width = img->width;
    int height = img->height;

    cv::Mat mat(height, width, (img->colors == 3) ? CV_8UC3 : CV_8UC1, img->data);
    cv::Mat bgrMat;
    cv::cvtColor(mat, bgrMat, cv::COLOR_RGB2BGR); // RAW 是 RGB 顺序

    // 构造 PyramidImage(内部是异步构建金字塔)
    auto* pyramid = new PyramidImage(bgrMat);

    // 获取预览图像并转 ARGB int array
    cv::Mat preview = pyramid->getPreview();
    jintArray previewArray = matToIntArray(env, preview);

    jclass cls = env->FindClass("cn/netdiscovery/monica/domain/DecodedPreviewImage");
    jmethodID constructor = env->GetMethodID(cls, "<init>", "(JII[I)V");
    jobject result = env->NewObject(cls, constructor, reinterpret_cast<jlong>(pyramid), preview.cols, preview.rows, previewArray);

    LibRaw::dcraw_clear_mem(img);
    env->ReleaseStringUTFChars(filePath, path);

    return result;
}

4.2.2 HEIC 文件首次加载

  • 解码为完整图像(HEIC 无 half-size 模式)
  • 同样构建 PyramidImage
  • 调色时,使用 PyramidImage 对象并调用ColorCorrection::doColorCorrection()
  • 保存时直接复用原图,调用 colorCorrectionWithPyramidImage()。

这一过程的解码,主要调用:

cpp 复制代码
jobject decodeHeifInternal(JNIEnv *env, jstring filePath) {
    const char *cpath = env->GetStringUTFChars(filePath, nullptr);

    heif_context* ctx = heif_context_alloc();
    heif_error err = heif_context_read_from_file(ctx, cpath, nullptr);
    if (err.code != heif_error_Ok) {
        std::cerr << "Failed to read HEIF: " << err.message << std::endl;
        heif_context_free(ctx);
        env->ReleaseStringUTFChars(filePath, cpath);
        return nullptr;
    }

    heif_image_handle* handle = nullptr;
    err = heif_context_get_primary_image_handle(ctx, &handle);
    if (err.code != heif_error_Ok) {
        std::cerr << "Failed to get primary image handle" << std::endl;
        heif_context_free(ctx);
        env->ReleaseStringUTFChars(filePath, cpath);
        return nullptr;
    }

    heif_image* img = nullptr;
    err = heif_decode_image(handle, &img, heif_colorspace_RGB, heif_chroma_interleaved_RGBA, nullptr);
    if (err.code != heif_error_Ok) {
        std::cerr << "Failed to decode image" << std::endl;
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        env->ReleaseStringUTFChars(filePath, cpath);
        return nullptr;
    }

    int width = heif_image_get_width(img, heif_channel_interleaved);
    int height = heif_image_get_height(img, heif_channel_interleaved);

    int stride = 0;
    const uint8_t* data = heif_image_get_plane_readonly(img, heif_channel_interleaved, &stride);
    if (!data) {
        std::cerr << "Failed to get pixel data" << std::endl;
        heif_image_release(img);
        heif_image_handle_release(handle);
        heif_context_free(ctx);
        env->ReleaseStringUTFChars(filePath, cpath);
        return nullptr;
    }

    // 构建 cv::Mat(RGBA → BGR)
    cv::Mat rgba(height, width, CV_8UC4, (void*)data, stride);
    cv::Mat bgr;
    cv::cvtColor(rgba, bgr, cv::COLOR_RGBA2BGR);

    // 构建 PyramidImage(内部异步金字塔)
    auto* pyramid = new PyramidImage(bgr);

    // 生成预览图并转换为 jintArray
    cv::Mat preview = pyramid->getPreview();
    jintArray previewArray = matToIntArray(env, preview);

    // 构建 DecodedPreviewImage Java 对象
    jclass cls = env->FindClass("cn/netdiscovery/monica/domain/DecodedPreviewImage");
    jmethodID constructor = env->GetMethodID(cls, "<init>", "(JII[I)V");
    jobject result = env->NewObject(cls, constructor, reinterpret_cast<jlong>(pyramid), preview.cols, preview.rows, previewArray);

    // 清理
    heif_image_release(img);
    heif_image_handle_release(handle);
    heif_context_free(ctx);
    env->ReleaseStringUTFChars(filePath, cpath);

    return result;
}

4.2.3 RAW / HEIC 非首次加载:

当图像已加载过一次且已经完成过一次调色流程时:

  • 直接从 PyramidImage 中获取原图(不再解码)
  • 执行调色,并更新图像金字塔
  • 返回的仍然是预览图(一般为 1/2 尺寸)

这一过程调用的是:

cpp 复制代码
jobject colorCorrectionWithPyramidImageInternal(JNIEnv* env, jlong nativePtr, jobject jobj, jlong cppObjectPtr) {
    return safeJniCall<jobject>(env, [&]() -> jobject {
        if (nativePtr == 0 || cppObjectPtr == 0 || jobj == nullptr) {
            return nullptr;
        }

        cacheColorCorrectionFields(env);  // 保证只初始化一次字段ID等

        ColorCorrection* colorCorrection = reinterpret_cast<ColorCorrection*>(cppObjectPtr);
        ColorCorrectionSettings settings = extractColorCorrectionSettings(env, jobj);

        PyramidImage* pyramidImage = reinterpret_cast<PyramidImage*>(nativePtr);
        Mat image = pyramidImage->getOriginal();

        if (image.empty()) {
            std::cerr << "[colorCorrectionWithPyramidImage] original image is empty" << std::endl;
            return nullptr;
        }

        // 调色
        colorCorrection->origin = image.clone();
        cv::Mat dst;
        colorCorrection->doColorCorrection(settings, dst);

        // 更新图像金字塔
        pyramidImage->updateImage(dst);

        // 生成预览图并转换为 jintArray
        cv::Mat preview = pyramidImage->getPreview();
        jintArray previewArray = matToIntArray(env, preview);

        // 构建 DecodedPreviewImage Java 对象
        jclass cls = env->FindClass("cn/netdiscovery/monica/domain/DecodedPreviewImage");
        jmethodID constructor = env->GetMethodID(cls, "<init>", "(JII[I)V");
        jobject result = env->NewObject(cls, constructor, nativePtr, preview.cols, preview.rows, previewArray);

        return result;
    }, nullptr);
}

本次优化带来的好处:

目标 实现方式
首次快速预览 通过 RAW half-size 模式和 HEIC 解码预览图提升速度
避免重复解码 PyramidImage 缓存图像金字塔结构
精度保障 保存时始终以原图为输入执行调色
架构清晰、可扩展 多格式统一接入 PyramidImage 管理调色输入

五、性能瓶颈与未来优化方向

在当前版本的调色模块中,通过 LUT(查找表)加速、缓存预处理参数以及并行化处理(如 OpenCV 的 parallel_for_)等手段,显著提升了图像处理的速度。然而,在实际应用中,仍存在若干性能瓶颈,主要集中在以下几个方面:

  • 初始化和 LUT 更新耗时 每当用户调整参数时,LUT 需要重新计算。尽管 LUT 本身计算代价不高,但在连续快速调节多个参数时,频繁触发更新可能造成处理排队与帧率下降。

  • 图像内存访问瓶颈 虽然使用了 cv::parallel_for_ 实现多线程并行,但图像内存仍然是共享资源,某些操作存在线程间访问冲突或缓存失效的风险,影响整体并发效率。

  • 调色精度与 SIMD 对齐限制 当前为了保证色彩一致性,未启用 SIMD 指令优化(如 SSE/NEON),导致像素级调色仍依赖逐通道处理,CPU 指令执行效率未能最大化利用。

  • 多次处理中的冗余步骤 若用户连续调整多个参数(如饱和度、对比度、色温),系统会在每次调整后完整执行一次 LUT 应用流程,而非批处理或延迟执行,带来重复计算。

针对上述瓶颈,未来该模块的优化方向:

  • 引入懒更新机制
  • 支持 SIMD 加速路径
  • 增量式调色图优化
  • 使用 OpenCL / Metal,将调色流程向 GPU 演进

六、总结

在图像调色这个看似简单却隐藏诸多挑战的领域,本文从图像编辑器 Monica 项目的实际需求出发,围绕 RAW 与 HEIC 文件的加载与调色优化,进行了结构化的系统重构与性能提升。

从最初基于 forEach 的像素迭代方案,到引入 LUT 与并行处理进行调色提速;再到进一步使用图像金字塔结构,实现了在加载预览图时即可完成用户感知的调色操作,同时在保存阶段再进行高精度图像处理 ------ 有效平衡了性能、资源占用与用户体验。

图像金字塔的引入,不仅优化了调色流程的执行路径,也为后续模块化设计、格式兼容与预览性能打下了基础。而解码层与图像处理层的解耦,也让整体架构更具可维护性与扩展性。

最后,本次优化的代码,可以在这里找到: github.com/fengzhizi71...

另外,图像编辑器的地址:github.com/fengzhizi71...

相关推荐
朝朝又沐沐1 小时前
算法竞赛阶段二-数据结构(36)数据结构双向链表模拟实现
开发语言·数据结构·c++·算法·链表
薰衣草23332 小时前
一天两道力扣(6)
算法·leetcode
剪一朵云爱着2 小时前
力扣946. 验证栈序列
算法·
遇见尚硅谷3 小时前
C语言:*p++与p++有何区别
c语言·开发语言·笔记·学习·算法
天天开心(∩_∩)3 小时前
代码随想录算法训练营第三十二天
算法
YouQian7723 小时前
(AC)缓存系统
算法·缓存
艾莉丝努力练剑3 小时前
【数据结构与算法】数据结构初阶:详解排序(二)——交换排序中的快速排序
c语言·开发语言·数据结构·学习·算法·链表·排序算法
科大饭桶3 小时前
数据结构自学Day13 -- 快速排序--“前后指针法”
数据结构·算法·leetcode·排序算法·c
李永奉3 小时前
C语言-流程控制语句:for循环语句、while和do…while循环语句;
c语言·开发语言·c++·算法
程序员-King.3 小时前
day69—动态规划—爬楼梯(LeetCode-70)
算法·动态规划