C语言基础编写简易程序批量预处理人脸训练数据1. 引言如果你正在接触AI图像相关的项目比如想自己动手微调一个图像编辑模型那么你很快就会遇到一个绕不开的环节数据预处理。网上很多教程都会教你用Python的PIL或者OpenCV库这当然很方便。但不知道你有没有想过当你要处理成千上万张图片时速度会不会成为瓶颈今天我们就换个思路用C语言来写一个简单的图片批量处理工具。你可能会问现在Python这么流行为什么还要用C原因很简单快。对于像人脸数据预处理这种需要反复进行文件读写、像素操作的重复性任务C语言在性能上的优势是实实在在的。它能帮你把原本可能需要几十分钟的预处理工作压缩到几分钟甚至更短。这篇文章我们就从零开始手把手教你用C语言的标准库写一个能批量读取图片、统一裁剪尺寸、并打包成自定义二进制格式的小程序。整个过程不需要复杂的第三方库只用最基础的C语言知识。学完之后你不仅能掌握一种高效的数据准备方法还能更深入地理解计算机是如何处理图像数据的。准备好了吗我们开始吧。2. 环境准备与项目目标在开始写代码之前我们先把“战场”布置好。你不需要安装什么特别的IDE一个能写C代码的文本编辑器比如VSCode、Sublime Text甚至记事本和一个C语言编译器就足够了。2.1 你需要什么C语言编译器推荐使用GCC(GNU Compiler Collection)。在Linux或macOS上它通常已经预装了。在Windows上你可以安装 MinGW-w64 或者使用WSLWindows Subsystem for Linux。安装后在终端或命令提示符里输入gcc --version能看到版本信息就说明成功了。一个图片处理库轻量级为了简化图像解码和编码的复杂操作我们选择一个轻量的库。这里我推荐stb_image和stb_image_write。它们都是单头文件库非常容易集成。你只需要去它们的GitHub页面搜索“stb”把stb_image.h和stb_image_write.h两个文件下载下来放到你的项目文件夹里就行。一些测试图片准备一些.jpg或.png格式的人脸图片放在一个单独的文件夹里比如叫做raw_faces。2.2 我们要做什么我们的目标是编写一个程序它能自动完成以下三件事批量读取自动扫描指定文件夹里的所有图片文件。 *.标准化处理把每张图片都裁剪或缩放到统一的大小例如 256x256 像素这是很多AI模型输入的要求。打包输出将处理好的图片数据通常是RGB像素值以一种紧凑的、自定义的二进制格式保存到一个文件里。这种格式读取起来非常快非常适合后续的模型训练。整个过程我们会用C语言一步步实现你会看到如何用fopen,fread操作文件如何用指针遍历和修改内存中的像素数据以及如何组织数据使其高效。让我们先从最简单的步骤——读取单张图片开始。3. 第一步用C语言读取一张图片万事开头难但第一步我们让它简单点。我们先不搞“批量”也不搞“裁剪”就看看怎么用C语言和stb_image库把一张图片从硬盘加载到内存里。首先在你的项目目录下创建一个新文件比如叫preprocess.c。然后把下载好的stb_image.h也放进来。3.1 引入必要的头文件在preprocess.c的开头我们写上这些代码#include stdio.h #include stdlib.h #include string.h #define STB_IMAGE_IMPLEMENTATION #include stb_image.hstdio.h,stdlib.h,string.h是C标准库用于输入输出、内存管理和字符串操作。那个#define STB_IMAGE_IMPLEMENTATION非常重要而且只能在一个源文件里定义一次。它的作用是告诉编译器“请在这里实现stb_image库的所有函数”。接着我们再包含stb_image.h头文件。3.2 编写图片加载函数接下来我们写一个函数来加载图片unsigned char* load_image(const char* filepath, int* width, int* height, int* channels) { // 使用stb_image加载图片 // 参数文件路径 宽度指针 高度指针 通道数指针 期望通道数0表示原样 unsigned char* image_data stbi_load(filepath, width, height, channels, 0); if (image_data NULL) { fprintf(stderr, 错误无法加载图片 %s。原因%s\n, filepath, stbi_failure_reason()); return NULL; } printf(成功加载图片%s 尺寸%dx%d 通道数%d\n, filepath, *width, *height, *channels); return image_data; }这个函数load_image干了啥调用stbi_load。你只需要告诉它图片在哪filepath它就会把图片的宽度、高度、通道数通常是3代表RGB填到我们传入的width,height,channels指针里然后把图片的像素数据一个长长的数组返回给我们。检查是否成功。如果失败了比如文件不存在或格式不支持stbi_load会返回NULL我们用stbi_failure_reason()打印错误信息。如果成功就打印一下图片信息并把数据返回。3.3 在主函数里测试一下光有函数不行我们得试试它能不能跑通。int main() { const char* test_image ./raw_faces/face1.jpg; // 换成你的图片路径 int width, height, channels; unsigned char* img load_image(test_image, width, height, channels); if (img ! NULL) { printf(图片数据指针地址%p\n, (void*)img); // 注意使用完图片数据后必须释放内存 stbi_image_free(img); printf(图片数据已释放。\n); } return 0; }怎么编译和运行打开终端进入你的项目目录。输入编译命令gcc preprocess.c -o preprocess -lm-o preprocess指定生成的可执行文件叫preprocess-lm是链接数学库某些情况下stb库需要。运行它./preprocess如果一切顺利你应该能看到终端打印出类似“成功加载图片...尺寸1024x768通道数3”的信息。重点提醒stbi_load分配的内存必须用stbi_image_free来释放否则会造成内存泄漏。这是使用这个库的一个关键点。好了现在我们已经能把一张图片“搬”进电脑的内存里了。图片数据在内存里就是一个按顺序排列的数组每3个数字代表一个像素的R、G、B值。理解了这一点我们就可以对它“动手术”了——比如裁剪。4. 第二步实现中心裁剪与缩放拿到图片数据后我们通常需要把它变成统一的尺寸。这里我们采用一种简单实用的方法中心裁剪。假设我们需要所有图片都是256x256而原图是1024x768我们就从图片正中间挖出一块256x256的区域。4.1 理解图片数据在内存中的布局在继续之前我们必须清楚img指针指向的数据长什么样。对于一个宽为W高为H通道为CC3的图片数据是按行存储的一维数组[R00, G00, B00, R01, G01, B01, ..., R(W-1)0, G(W-1)0, B(W-1)0, R01, G01, B01, ...]其中R00代表第0行第0列像素的红色值。访问第row行第col列像素的起始位置索引是(row * width col) * channels。4.2 编写中心裁剪函数我们来写一个函数实现中心裁剪到目标尺寸比如TARGET_SIZExTARGET_SIZE。// 定义我们的目标尺寸 #define TARGET_SIZE 256 unsigned char* center_crop(const unsigned char* src_img, int src_w, int src_h, int channels) { // 为目标图片分配内存。大小 高 * 宽 * 通道数 unsigned char* cropped_img (unsigned char*)malloc(TARGET_SIZE * TARGET_SIZE * channels * sizeof(unsigned char)); if (cropped_img NULL) { fprintf(stderr, 错误内存分配失败\n); return NULL; } // 计算裁剪的起始坐标左上角 int start_x (src_w - TARGET_SIZE) / 2; int start_y (src_h - TARGET_SIZE) / 2; // 确保起始坐标非负处理原图比目标图小的情况这里先简单处理实际可能需要缩放 if (start_x 0) start_x 0; if (start_y 0) start_y 0; // 注意如果原图尺寸小于目标尺寸这个简单的裁剪会越界。更健壮的做法是先缩放这里为简化先这样。 // 假设我们的输入图片都足够大。 // 逐行复制像素数据 for (int y 0; y TARGET_SIZE; y) { for (int x 0; x TARGET_SIZE; x) { // 计算源图像和目标图像中的像素索引 int src_idx ((start_y y) * src_w (start_x x)) * channels; int dst_idx (y * TARGET_SIZE x) * channels; // 复制R, G, B三个通道的值 for (int c 0; c channels; c) { cropped_img[dst_idx c] src_img[src_idx c]; } } } printf(中心裁剪完成从(%d,%d)开始裁剪尺寸%dx%d\n, start_x, start_y, TARGET_SIZE, TARGET_SIZE); return cropped_img; }这个函数做了以下几件事malloc申请了一块刚好能存放TARGET_SIZE * TARGET_SIZE * 3个unsigned char的内存用于存放裁剪后的图片。计算从原图的哪个位置开始裁剪确保是正中心。使用两层循环遍历目标图片的每一个像素位置(x, y)。对于每个目标像素找到它在原图中对应的位置然后把那个位置的R、G、B三个值复制过来。4.3 测试裁剪功能并保存结果裁剪完了我们得看看效果。修改我们的main函数加入裁剪和保存的步骤。这里我们需要用到另一个单文件库stb_image_write.h。// 在文件顶部引入stb_image_write #define STB_IMAGE_WRITE_IMPLEMENTATION #include stb_image_write.h int main() { const char* test_image ./raw_faces/face1.jpg; int width, height, channels; unsigned char* img load_image(test_image, width, height, channels); if (img NULL) return 1; // 进行中心裁剪 unsigned char* cropped_img center_crop(img, width, height, channels); if (cropped_img NULL) { stbi_image_free(img); return 1; } // 保存裁剪后的图片以便检查 const char* output_image ./cropped_face.jpg; // stbi_write_jpg: 参数依次是文件名 宽 高 通道数 数据指针 质量(1-100) int success stbi_write_jpg(output_image, TARGET_SIZE, TARGET_SIZE, channels, cropped_img, 90); if (success) { printf(裁剪后的图片已保存为%s\n, output_image); } else { fprintf(stderr, 错误保存图片失败\n); } // 释放内存 stbi_image_free(img); free(cropped_img); // 注意这里用free因为是我们自己malloc的 return 0; }重新编译并运行程序。如果成功你会在项目目录下看到一个名为cropped_face.jpg的新图片它应该是一张256x256的正方形人脸特写。打开看看裁剪的位置是不是在正中间恭喜你已经完成了单张图片的加载、裁剪和保存。这是最核心的图像操作。接下来我们要让它“批量”起来并打包成更高效的格式。5. 第三步遍历文件夹与批量处理现在我们要处理一个文件夹里的所有图片而不是单张。C标准库本身没有直接遍历文件夹的函数但我们可以用dirent.h在POSIX系统如Linux、macOS上或windows.h在Windows上来实现。为了代码的通用性和简洁性我们这里介绍一种更简单的方法在程序运行前手动或通过脚本生成一个文件列表。5.1 创建文件列表我们在存放原始图片的raw_faces文件夹同级目录下创建一个文本文件filelist.txt里面每一行写一张图片的路径。raw_faces/face1.jpg raw_faces/face2.jpg raw_faces/face3.jpg ...你可以用命令行快速生成这个列表在raw_faces的上级目录ls raw_faces/*.jpg filelist.txt或者在Windows PowerShell里Get-ChildItem raw_faces\*.jpg | Resolve-Path -Relative filelist.txt5.2 编写批量处理函数我们的程序将读取这个列表文件然后逐行处理。#include dirent.h // 用于目录遍历备用方案 void process_batch(const char* list_filepath) { FILE* list_file fopen(list_filepath, r); if (!list_file) { perror(无法打开文件列表); return; } char image_path[256]; int processed_count 0; printf(开始批量处理...\n); // 逐行读取文件列表 while (fgets(image_path, sizeof(image_path), list_file)) { // 去掉行尾的换行符 image_path[strcspn(image_path, \n)] 0; if (strlen(image_path) 0) continue; // 跳过空行 int width, height, channels; unsigned char* original_img load_image(image_path, width, height, channels); if (original_img NULL) { continue; // 加载失败跳过这张图 } // 检查图片是否足够大可以进行裁剪 if (width TARGET_SIZE || height TARGET_SIZE) { fprintf(stderr, 警告图片 %s 尺寸(%dx%d)小于目标尺寸跳过。\n, image_path, width, height); stbi_image_free(original_img); continue; } // 进行中心裁剪 unsigned char* cropped_img center_crop(original_img, width, height, channels); if (cropped_img NULL) { stbi_image_free(original_img); continue; } // TODO: 这里暂时先保存为独立文件下一步我们会改成写入二进制包 char output_path[256]; snprintf(output_path, sizeof(output_path), ./processed/face_%04d.jpg, processed_count); stbi_write_jpg(output_path, TARGET_SIZE, TARGET_SIZE, channels, cropped_img, 90); // 释放内存 stbi_image_free(original_img); free(cropped_img); processed_count; printf(已处理%s - %s\n, image_path, output_path); } fclose(list_file); printf(批量处理完成共处理了 %d 张图片。\n, processed_count); }这个函数逻辑很清晰打开文件列表。循环读取每一行即一个图片路径。对每张图执行我们之前测试过的流程加载 - 检查尺寸 - 裁剪 - 暂时保存为单独的JPG文件。记得在循环内及时释放每一张图片的内存。最后统计并打印处理了多少张图。记得在main函数里调用它int main() { // 单张测试的代码可以先注释掉 // ... process_batch(./filelist.txt); return 0; }运行前确保创建好processed文件夹来存放输出图片。现在你的程序已经可以批量处理图片了但我们的最终目标不是生成一堆JPG而是打包成一个文件。6. 第四步打包为自定义二进制数据集将处理后的图片保存为单独的JPG文件在后续读取时会产生大量小文件的I/O开销影响训练速度。更好的做法是打包成一个二进制文件里面连续存储所有图片的像素数据。6.1 设计二进制文件格式我们可以设计一个简单的格式文件头存储一些元信息比如图片数量、图片宽、高、通道数。这样读取的时候就知道该怎么解析后面的数据。数据区连续存储每一张处理后的图片的像素数据R,G,B,R,G,B...。假设我们处理了N张图每张图是TARGET_SIZE * TARGET_SIZE * 3字节。那么文件结构如下[N (int), TARGET_SIZE (int), TARGET_SIZE (int), 3 (int)] [图片1数据] [图片2数据] ... [图片N数据]6.2 修改批量处理函数写入二进制文件我们不再保存为单独的JPG而是将所有数据写入一个.bin文件。void process_and_pack(const char* list_filepath, const char* output_bin_path) { FILE* list_file fopen(list_filepath, r); FILE* bin_file fopen(output_bin_path, wb); // 以二进制写入模式打开 if (!list_file || !bin_file) { perror(打开文件失败); if (list_file) fclose(list_file); if (bin_file) fclose(bin_file); return; } char image_path[256]; int processed_count 0; int total_count 0; // 第一遍先数一下有多少张有效的图片以便写入文件头或者我们可以先写入一个占位符最后再修改 // 为了简单我们采用动态数组或先收集路径的方法。这里我们用动态数组先收集路径。 // 但为了教学清晰我们换一种更直接的方法先写入一个“假的”数量到头处理完后再修正。 // 我们先写入文件头数量先写0 int num_images 0; int img_width TARGET_SIZE; int img_height TARGET_SIZE; int img_channels 3; fwrite(num_images, sizeof(int), 1, bin_file); fwrite(img_width, sizeof(int), 1, bin_file); fwrite(img_height, sizeof(int), 1, bin_file); fwrite(img_channels, sizeof(int), 1, bin_file); printf(开始处理并打包...\n); // 第二遍处理每张图片并将像素数据追加到二进制文件 while (fgets(image_path, sizeof(image_path), list_file)) { image_path[strcspn(image_path, \n)] 0; if (strlen(image_path) 0) continue; int width, height, channels; unsigned char* original_img load_image(image_path, width, height, channels); if (original_img NULL) continue; if (width TARGET_SIZE || height TARGET_SIZE) { fprintf(stderr, 警告图片 %s 尺寸过小跳过。\n, image_path); stbi_image_free(original_img); continue; } unsigned char* cropped_img center_crop(original_img, width, height, channels); if (cropped_img NULL) { stbi_image_free(original_img); continue; } // 将裁剪后的图片数据写入二进制文件 size_t data_size TARGET_SIZE * TARGET_SIZE * channels; size_t written fwrite(cropped_img, sizeof(unsigned char), data_size, bin_file); if (written data_size) { processed_count; printf(已打包%s\n, image_path); } else { fprintf(stderr, 错误写入图片数据失败\n); } stbi_image_free(original_img); free(cropped_img); } // 所有图片处理完毕回到文件开头更新正确的图片数量 fseek(bin_file, 0, SEEK_SET); fwrite(processed_count, sizeof(int), 1, bin_file); fclose(list_file); fclose(bin_file); printf(打包完成共处理 %d 张图片已保存到 %s\n, processed_count, output_bin_path); }6.3 编写读取二进制文件的验证程序打包好了我们得验证一下数据是否正确。写一个简单的程序来读取这个.bin文件并把第一张图片还原出来看看。// 新建一个文件 verify.c #include stdio.h #include stdlib.h #define STB_IMAGE_WRITE_IMPLEMENTATION #include stb_image_write.h int main() { const char* bin_file ./face_dataset.bin; FILE* fp fopen(bin_file, rb); if (!fp) { perror(无法打开二进制文件); return 1; } int num_images, width, height, channels; fread(num_images, sizeof(int), 1, fp); fread(width, sizeof(int), 1, fp); fread(height, sizeof(int), 1, fp); fread(channels, sizeof(int), 1, fp); printf(数据集信息图片数%d, 宽%d, 高%d, 通道数%d\n, num_images, width, height, channels); if (num_images 0) { // 读取第一张图片的数据 size_t img_size width * height * channels; unsigned char* img_data (unsigned char*)malloc(img_size); fread(img_data, sizeof(unsigned char), img_size, fp); // 保存为JPG以便查看 stbi_write_jpg(./verify_first_image.jpg, width, height, channels, img_data, 90); printf(已提取第一张图片并保存为 verify_first_image.jpg\n); free(img_data); } fclose(fp); return 0; }编译并运行这个验证程序gcc verify.c -o verify -lm ./verify如果一切正常你会看到打印出的数据集信息并且文件夹里会多出一张verify_first_image.jpg它应该和你之前用单张测试程序裁剪出的第一张图一模一样。7. 总结走完这一趟我们从零开始用纯C语言完成了一个虽然基础但非常实用的人脸数据预处理流水线。回顾一下我们做了什么我们先是学会了怎么用stb_image这个轻量级的库把图片文件加载到内存里看到了图片数据其实就是一堆代表颜色的数字。然后我们写了一个中心裁剪的函数学会了如何用指针和循环在内存里精准地“切割”图片把它变成我们需要的正方形尺寸。为了让程序能处理一大堆图片我们采用了读取文件列表的方式来实现批量操作避开了直接操作文件夹的复杂性。最后也是性能提升的关键一步我们没有把处理好的图片存成一个个零散的文件而是把所有图片的像素数据连同基本的描述信息有多少张、多大紧密地打包进了一个二进制文件。这种格式对于后续的AI训练程序来说读取速度会快得多。整个过程我们没有依赖任何庞大的框架就是最基础的C语言和两个单文件库。你可能会觉得这比用Python几行代码调用库要麻烦。确实在开发效率上Python胜出。但当你真正需要处理海量数据对速度有极致要求时今天写的这个C语言小工具的优势就体现出来了。它运行起来更快更节省资源让你对数据处理的每一个字节都有完全的控制力。当然这个程序还有很多可以完善的地方比如增加更智能的缩放而不仅仅是裁剪小图、支持更多的图片格式、或者加入一些简单的数据增强如随机翻转。但最重要的是你已经掌握了用C语言处理图像数据的核心思路内存操作、文件I/O和二进制打包。你可以基于这个骨架去构建更复杂、更强大的数据处理工具。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。