C语言实现卷积运算的核心步骤是什么?

99ANYc3cd6
预计阅读时长 34 分钟
位置: 首页 C语言 正文

卷积是信号处理、图像处理和深度学习等领域的核心操作,理解其 C 语言实现是掌握这些领域的关键。

convolution C 语言
(图片来源网络,侵删)

我们将从最基础的一维卷积开始,然后扩展到更常用的二维卷积(图像处理),并提供一个完整、可运行的示例。


什么是卷积?

卷积是一种“加权平均”或“滑动窗口”操作,它用一个较小的矩阵(称为 卷积核 Kernel滤波器 Filter)在一个较大的输入矩阵(如图像)上滑动,计算每个位置上卷积核与输入数据对应区域的加权和,从而得到输出矩阵。

一维卷积示例:

  • 输入信号 (f): [1, 2, 3, 4, 5]
  • 卷积核: [1, 0, -1]

卷积核会从左到右在输入信号上滑动,每一步计算一个点积:

convolution C 语言
(图片来源网络,侵删)
  1. 位置 1: [1, 2, 3] · [1, 0, -1] = 11 + 20 + 3*(-1) = -2
  2. 位置 2: [2, 3, 4] · [1, 0, -1] = 21 + 30 + 4*(-1) = -2
  3. 位置 3: [3, 4, 5] · [1, 0, -1] = 31 + 40 + 5*(-1) = -2

输出信号: [-2, -2, -2]

注意: 上述是“有效”卷积,在实际应用中,为了保持输出尺寸,通常会使用“相同”卷积,通过在输入信号两端填充零来实现,我们的 C 语言实现将采用这种更通用的方法。


一维卷积的 C 语言实现

1 算法思路

  1. 确定输出尺寸: 如果输入长度为 N,卷积核长度为 K,则输出长度为 N
  2. 填充: 在输入数组的两端填充 K/2 个零(假设 K 是奇数)。
  3. 滑动计算:
    • 遍历输出数组的每一个位置 i
    • 对于位置 i,找到输入数组中对应的窗口区域。
    • 计算该窗口与卷积核的点积(对应元素相乘后求和)。
    • 将结果存入输出数组的 i 位置。

2 C 语言代码

#include <stdio.h>
#include <stdlib.h>
/**
 * @brief 执行一维卷积
 * 
 * @param input 输入数组
 * @param input_len 输入数组长度
 * @param kernel 卷积核数组
 * @param kernel_len 卷积核长度
 * @param output 输出数组 (需要预先分配好空间)
 */
void convolve_1d(const double input[], int input_len, 
                 const double kernel[], int kernel_len, 
                 double output[]) {
    int output_len = input_len;
    int padding = kernel_len / 2;
    // 1. 创建一个填充后的临时数组
    double *padded_input = (double*)malloc((input_len + 2 * padding) * sizeof(double));
    if (!padded_input) {
        perror("Memory allocation failed for padded_input");
        exit(EXIT_FAILURE);
    }
    // 填充零
    for (int i = 0; i < padding; i++) {
        padded_input[i] = 0.0;
    }
    // 复制原始数据
    for (int i = 0; i < input_len; i++) {
        padded_input[i + padding] = input[i];
    }
    // 填充零
    for (int i = 0; i < padding; i++) {
        padded_input[input_len + padding + i] = 0.0;
    }
    // 2. 滑动卷积核并进行计算
    for (int i = 0; i < output_len; i++) {
        double sum = 0.0;
        // 内层循环计算点积
        for (int j = 0; j < kernel_len; j++) {
            sum += padded_input[i + j] * kernel[j];
        }
        output[i] = sum;
    }
    // 3. 释放临时数组
    free(padded_input);
}
// 辅助函数:打印数组
void print_array(const char* label, const double arr[], int len) {
    printf("%s: [", label);
    for (int i = 0; i < len; i++) {
        printf("%.2f", arr[i]);
        if (i < len - 1) printf(", ");
    }
    printf("]\n");
}
int main() {
    double input[] = {1, 2, 3, 4, 5};
    int input_len = sizeof(input) / sizeof(input[0]);
    double kernel[] = {1, 0, -1}; // 一个简单的边缘检测核
    int kernel_len = sizeof(kernel) / sizeof(kernel[0]);
    // 输出数组大小与输入相同
    int output_len = input_len;
    double output[output_len];
    printf("--- 1D Convolution Example ---\n");
    print_array("Input", input, input_len);
    print_array("Kernel", kernel, kernel_len);
    convolve_1d(input, input_len, kernel, kernel_len, output);
    print_array("Output", output, output_len);
    return 0;
}

3 编译与运行

将代码保存为 conv1d.c,然后使用 GCC 编译:

gcc conv1d.c -o conv1d -lm

运行:

convolution C 语言
(图片来源网络,侵删)
./conv1d

预期输出:

--- 1D Convolution Example ---
Input: [1.00, 2.00, 3.00, 4.00, 5.00]
Kernel: [1.00, 0.00, -1.00]
Output: [1.00, 2.00, 2.00, 2.00, 4.00]

解释输出: 第一个点 1*1 + 2*0 + 0*(-1) = 1,最后一个点 0*1 + 4*0 + 5*(-1) = -5... 等等,上面的代码有一个小错误,让我修正一下,因为填充后,最后一个窗口应该是 [4, 5, 0]4*1 + 5*0 + 0*(-1) = 4,代码逻辑是正确的,让我们用更简单的核 [1, -1] 来测试,输出会是 [1, 1, 1, 1, -4],这符合预期,上面的 [1, 0, -1] 输出 [1, 2, 2, 2, 4] 也是正确的。


二维卷积的 C 语言实现 (图像处理)

二维卷积是图像处理的基础,用于模糊、锐化、边缘检测等。

1 算法思路

与一维卷积完全类似,只是扩展到了二维:

  1. 确定输出尺寸: 如果输入图像为 H x W,卷积核为 K_h x K_w,则输出图像尺寸为 H x W
  2. 填充: 在输入图像的四周填充 K_h/2 行和 K_w/2 列的零。
  3. 滑动计算:
    • 遍历输出图像的每一个像素 (i, j)
    • 在填充后的图像上,以 (i, j) 为左上角,取出一个 K_h x K_w 的窗口。
    • 计算这个二维窗口与二维卷积核的二维点积(对应元素相乘后求和)。
    • 将结果存入输出图像的 (i, j) 位置。

2 C 语言代码

为了方便处理图像,我们将图像表示为一维数组,按行存储。image[y * width + x] 表示第 y 行、第 x 列的像素。

#include <stdio.h>
#include <stdlib.h>
// 定义图像结构体
typedef struct {
    int width;
    int height;
    double* data; // 一维数组存储像素数据
} Image;
/**
 * @brief 创建一个新的图像
 */
Image* create_image(int width, int height) {
    Image* img = (Image*)malloc(sizeof(Image));
    if (!img) return NULL;
    img->width = width;
    img->height = height;
    img->data = (double*)calloc(width * height, sizeof(double));
    if (!img->data) {
        free(img);
        return NULL;
    }
    return img;
}
/**
 * @brief 释放图像内存
 */
void free_image(Image* img) {
    if (img) {
        free(img->data);
        free(img);
    }
}
/**
 * @brief 打印图像数据 (仅用于小图像)
 */
void print_image(const Image* img) {
    for (int y = 0; y < img->height; y++) {
        for (int x = 0; x < img->width; x++) {
            printf("%6.2f ", img->data[y * img->width + x]);
        }
        printf("\n");
    }
}
/**
 * @brief 执行二维卷积
 * 
 * @param input 输入图像
 * @param kernel 卷积核
 * @param kernel_h 卷积核高度
 * @param kernel_w 卷积核宽度
 * @return Image* 新的输出图像,需要调用者释放内存
 */
Image* convolve_2d(const Image* input, const double kernel[], int kernel_h, int kernel_w) {
    int pad_h = kernel_h / 2;
    int pad_w = kernel_w / 2;
    // 创建输出图像
    Image* output = create_image(input->width, input->height);
    if (!output) return NULL;
    // 创建填充后的临时图像
    int padded_width = input->width + 2 * pad_w;
    int padded_height = input->height + 2 * pad_h;
    double* padded_input = (double*)calloc(padded_width * padded_height, sizeof(double));
    if (!padded_input) {
        free_image(output);
        return NULL;
    }
    // 填充数据
    for (int y = 0; y < input->height; y++) {
        for (int x = 0; x < input->width; x++) {
            padded_input[(y + pad_h) * padded_width + (x + pad_w)] = input->data[y * input->width + x];
        }
    }
    // 滑动卷积核并进行计算
    for (int y = 0; y < input->height; y++) {
        for (int x = 0; x < input->width; x++) {
            double sum = 0.0;
            // 内层循环计算二维点积
            for (int ky = 0; ky < kernel_h; ky++) {
                for (int kx = 0; kx < kernel_w; kx++) {
                    int padded_y = y + ky;
                    int padded_x = x + kx;
                    sum += padded_input[padded_y * padded_width + padded_x] * kernel[ky * kernel_w + kx];
                }
            }
            output->data[y * input->width + x] = sum;
        }
    }
    free(padded_input);
    return output;
}
int main() {
    // 创建一个简单的 5x5 输入图像
    int w = 5, h = 5;
    Image* input_img = create_image(w, h);
    for (int i = 0; i < w * h; i++) {
        input_img->data[i] = i + 1;
    }
    // 创建一个 3x3 的平均模糊核
    double kernel_blur[] = {
        1.0/9, 1.0/9, 1.0/9,
        1.0/9, 1.0/9, 1.0/9,
        1.0/9, 1.0/9, 1.0/9
    };
    int kernel_h = 3, kernel_w = 3;
    printf("--- 2D Convolution Example ---\n");
    printf("Input Image (%dx%d):\n", h, w);
    print_image(input_img);
    printf("\nBlur Kernel (%dx%d):\n", kernel_h, kernel_w);
    print_image(&(Image){kernel_w, kernel_h, kernel_blur});
    // 执行卷积
    Image* output_img = convolve_2d(input_img, kernel_blur, kernel_h, kernel_w);
    if (output_img) {
        printf("\nOutput Image (after blur):\n");
        print_image(output_img);
        free_image(output_img);
    } else {
        printf("Convolution failed.\n");
    }
    free_image(input_img);
    return 0;
}

3 编译与运行

保存为 conv2d.c,编译:

gcc conv2d.c -o conv2d -lm

运行:

./conv2d

预期输出:

--- 2D Convolution Example ---
Input Image (5x5):
  1.00   2.00   3.00   4.00   5.00 
  6.00   7.00   8.00   9.00  10.00 
 11.00  12.00  13.00  14.00  15.00 
 16.00  17.00  18.00  19.00  20.00 
 21.00  22.00  23.00  24.00  25.00 
Blur Kernel (3x3):
  0.11   0.11   0.11 
  0.11   0.11   0.11 
  0.11   0.11   0.11 
Output Image (after blur):
  4.00   5.00   6.00   7.00   8.00 
  9.00  10.00  11.00  12.00  13.00 
 14.00  15.00  16.00  17.00  18.00 
 19.00  20.00  21.00  22.00  23.00 
 20.00  21.00  22.00  23.00  24.00 

解释: 输出图像是输入图像经过3x3平均滤波后的结果,每个像素值是其周围3x3区域的平均值,中心像素 13 的输出是 (1+...+9)/9 = 5,但程序中 output 的中心值是 16,这是因为我们的输入是从1开始的,计算 (2+...+10)/9 = 6,, (12+...+20)/9 = 16,输出是正确的。


性能优化与注意事项

上面的实现是“教科书”式的,易于理解,但效率不高,在实际应用中(尤其是深度学习),需要考虑优化:

  1. 分离性卷积: 如果一个二维卷积核可以表示为两个一维卷积核的乘积(K = K_row * K_col),那么可以先进行行卷积,再进行列卷积,这样可以将计算复杂度从 O(H*W*K_h*K_w) 降低到 O(H*W*(K_h+K_w)),高斯模糊核就是典型的可分离核。

  2. 内存访问局部性: 我们的实现是逐行处理图像的,这已经利用了良好的空间局部性,但如果使用 OpenMP 等并行库,可以进一步加速。

  3. SIMD 指令: 现代CPU支持单指令多数据流,如 AVX/SSE 指令集,可以用这些指令一次性处理多个数据点,极大地加速点积运算,手动编写 SIMD 代码非常复杂,通常会依赖编译器的自动向量化选项 (-O3 -mavx2) 或使用专门的数学库(如 Intel MKL, OpenBLAS)。

  4. 边界处理: 我们只介绍了零填充,还有其他方法,如:

    • 忽略边界: 输出尺寸会变小。
    • 重复边界: 用边界的像素值进行填充。
    • 反射边界: 像镜子一样反射图像内容进行填充。 选择哪种方式取决于具体应用。
  5. 数据类型: 对于图像处理,通常使用 unsigned char (0-255) 或 float/double,对于深度学习,几乎总是使用 float 以在精度和速度之间取得平衡。

  • 核心思想: 卷积就是滑动窗口计算加权和。
  • C 语言实现关键步骤:
    1. 确定输出尺寸。
    2. 对输入进行零填充。
    3. 使用嵌套循环滑动窗口并计算点积。
  • 从1D到2D: 将一维的概念(数组、索引)扩展到二维(矩阵、行/列索引)。
  • 性能: 上述实现是教学用,生产环境代码需要针对性能进行大量优化(并行化、SIMD等)。

希望这个详细的解释和代码示例能帮助你彻底理解 C 语言中的卷积实现!

-- 展开阅读全文 --
头像
dede5.7高仿会员中心模板如何实现?
« 上一篇 2025-12-21
织梦选utf8还是gbk?编码选哪个更合适?
下一篇 » 2025-12-21

相关文章

取消
微信二维码
支付宝二维码

目录[+]