C语言基础项目:编写简易图像格式转换器预处理DeOldify输入
C语言基础项目编写简易图像格式转换器预处理DeOldify输入想给老照片上色但发现AI模型DeOldify只认特定的数据格式网上找的工具要么太复杂要么不好用别急今天咱们就用最基础的C语言自己动手写一个图片格式转换器。这个项目不仅能帮你搞定DeOldify的输入预处理更重要的是它能让你彻底搞懂图片在计算机里到底是怎么“长”的以及内存里那些字节是怎么“跳舞”的。对于C语言初学者来说光看语法和概念有点枯燥通过这个实实在在的项目你会把文件操作、内存管理、结构体这些知识点全都串起来。学完它你不仅收获一个能用的工具更能获得“哦原来程序是这么操控数据的”那种豁然开朗的感觉。我们的目标是写一个命令行小工具它能把常见的BMP、PNG、JPEG图片转换成DeOldify模型需要的统一格式——比如一个简单的RGB像素值数组。听起来是不是很有挑战性别担心我们会一步步拆解从最简单的BMP格式开始。1. 项目目标与环境准备在动手写代码之前我们先得把目标搞清楚并把“战场”准备好。1.1 我们要做什么简单说这个工具就是一个“翻译官”。它要完成以下几件事读取图片能认识并打开BMP、PNG、JPEG这三种格式的图片文件。理解内容不管图片原来是什么格式都要能正确解析出它的宽度、高度以及每一个像素的红R、绿G、蓝B颜色值。统一输出把所有解析出来的像素信息以一种简单、统一的格式比如纯文本的RGB列表或者二进制的像素数据块保存下来方便DeOldify这样的模型直接读取使用。为什么从BMP开始因为BMP位图格式的结构最简单、最直观没有压缩文件内容几乎就是像素数据的直接排列。我们先攻克它建立信心和理解再去挑战有压缩的PNG和JPEG。1.2 搭建你的C语言工作台你只需要两样东西一个C语言编译器推荐使用GCC。如果你用Windows可以安装 MinGW-w64用macOS打开终端安装Xcode Command Line Toolsxcode-select --install用Linux通常系统自带GCC。一个代码编辑器任何你顺手的都行比如VS Code、CLion、甚至记事本。VS Code搭配C/C插件体验会很好。为了处理PNG和JPEG我们需要两个强大的帮手库libpng专门读写PNG格式的库。libjpeg专门读写JPEG格式的库。在Linux/macOS上安装它们通常很简单# Ubuntu/Debian sudo apt-get install libpng-dev libjpeg-dev # macOS (使用Homebrew) brew install libpng libjpeg在Windows上安装可以搜索 “libpng for Windows precompiled” 和 “libjpeg for Windows precompiled”下载编译好的库文件.dll, .lib和头文件.h然后配置到你的编译器或IDE中。这个过程稍微麻烦点但网上有很多详细的教程。安装好后我们可以用一段简单的代码测试一下编译环境是否就绪。创建一个test_env.c文件#include stdio.h #include png.h #include jpeglib.h int main() { printf(环境测试通过\n); printf(libpng 版本: %s\n, PNG_LIBPNG_VER_STRING); // libjpeg没有简单的版本宏这里只是确保头文件存在 printf(libjpeg 头文件加载成功。\n); return 0; }用GCC编译试试记得链接库gcc test_env.c -o test_env -lpng -ljpeg ./test_env如果成功运行并打印出版本信息恭喜你环境搞定2. 理解图像文件与BMP格式解析万事开头难但BMP格式是这个项目里最友好的开头。让我们像拆解一个乐高模型一样看看BMP文件是怎么拼起来的。2.1 图像文件是什么你可以把一张数字图片想象成一个巨大的、有规律的格子画布矩阵。每个小格子就是一个“像素”Pixel。每个像素的颜色通常由红Red、绿Green、蓝Blue三个分量混合而成这就是RGB色彩模型。每个分量用一个数字表示亮度通常是0-255。所以一张图片在计算机内存中最本质的样子就是一个按行排列的RGB数值列表。不同图片格式的区别就在于它们用什么“包装盒”文件头来描述这个画布有多大、色彩怎么排列以及是否对这个列表进行了“压缩”编码。2.2 BMP文件结构解剖BMP文件的结构非常规整主要分三到四个部分文件头Bitmap File Header14字节。告诉你“这是一个BMP文件”以及整个文件有多大、像素数据从哪里开始。信息头Bitmap Information Header40字节最常见。这是核心包含了图片的宽度、高度、颜色位数如24位真彩色、压缩方式等关键信息。调色板Color Table可选。对于颜色位数小于等于8的图片如黑白、256色这里存储了颜色索引对应的实际RGB值。我们主要处理24位真彩色无调色板。像素数据Pixel Data图片的“血肉”。从这里开始按行从下往上存储每个像素的BGR值注意顺序是BGR不是RGB。2.3 动手解析BMP头信息理论说再多不如一行代码。我们来定义对应的C语言结构体并编写读取函数。#include stdio.h #include stdint.h // 使用明确字节宽度的类型如uint16_t // BMP文件头 (14字节) #pragma pack(push, 1) // 确保编译器不对结构体进行内存对齐严格按1字节对齐 typedef struct { uint16_t signature; // 文件标识BM (0x4D42) uint32_t file_size; // 整个文件大小 uint16_t reserved1; // 保留必须为0 uint16_t reserved2; // 保留必须为0 uint32_t data_offset; // 像素数据开始的偏移量 } BMPFileHeader; #pragma pack(pop) // BMP信息头 (40字节BITMAPINFOHEADER) #pragma pack(push, 1) typedef struct { uint32_t header_size; // 本结构大小40 int32_t width; // 图像宽度像素有符号整数 int32_t height; // 图像高度像素。正数表示像素数据从下往上存储 uint16_t planes; // 颜色平面数总是1 uint16_t bits_per_pixel; // 每像素位数24表示真彩色 uint32_t compression; // 压缩类型0表示不压缩 uint32_t image_size; // 像素数据部分的大小可为0 int32_t x_pixels_per_meter; // 水平分辨率 int32_t y_pixels_per_meter; // 垂直分辨率 uint32_t colors_used; // 实际使用的颜色索引数0表示使用全部 uint32_t colors_important; // 重要颜色索引数0表示都重要 } BMPInfoHeader; #pragma pack(pop) // 读取BMP文件头信息的函数 int read_bmp_header(const char* filename, BMPFileHeader* file_header, BMPInfoHeader* info_header) { FILE* file fopen(filename, rb); // 以二进制只读模式打开 if (!file) { perror(无法打开文件); return -1; } // 读取文件头 if (fread(file_header, sizeof(BMPFileHeader), 1, file) ! 1) { perror(读取文件头失败); fclose(file); return -1; } // 检查文件签名是否为BM if (file_header-signature ! 0x4D42) { fprintf(stderr, 错误不是有效的BMP文件。\n); fclose(file); return -1; } // 读取信息头 if (fread(info_header, sizeof(BMPInfoHeader), 1, file) ! 1) { perror(读取信息头失败); fclose(file); return -1; } // 简单验证我们只处理24位无压缩的BMP if (info_header-bits_per_pixel ! 24 || info_header-compression ! 0) { fprintf(stderr, 警告仅支持24位无压缩BMP。此文件可能不被正确处理。\n); // 不直接返回错误但后续读取像素时可能出错 } printf(BMP文件解析成功\n); printf( 尺寸%d x %d 像素\n, info_header-width, info_header-height); printf( 颜色深度%d 位/像素\n, info_header-bits_per_pixel); printf( 像素数据起始偏移%d 字节\n, file_header-data_offset); fclose(file); return 0; }这段代码做了几件关键事定义了精确映射文件格式的结构体打开了文件读取并验证了头信息。#pragma pack指令非常重要它保证了结构体在内存中的布局和文件中的字节序列完全一致。3. 核心实现读取像素与格式转换拿到“地图”文件头后接下来就是去挖掘“宝藏”像素数据了。3.1 读取BMP像素数据BMP的像素数据存储有两个特点需要注意行顺序通常是从图片的最后一行开始存储向上到第一行。行对齐每一行像素数据的字节数必须是4的倍数。如果不是会用0填充到4的倍数。这叫做“行填充”Row Padding。我们需要计算这个填充值并正确地跳过它。#include stdlib.h // 代表一个像素 typedef struct { unsigned char b; // 蓝色 unsigned char g; // 绿色 unsigned char r; // 红色 } Pixel; // 读取BMP像素数据 Pixel* read_bmp_pixels(const char* filename, const BMPFileHeader* file_header, const BMPInfoHeader* info_header, int* out_width, int* out_height) { FILE* file fopen(filename, rb); if (!file) return NULL; // 跳转到像素数据开始位置 fseek(file, file_header-data_offset, SEEK_SET); int width info_header-width; int height abs(info_header-height); // 高度取绝对值处理可能为负的情况从上到下存储 int channels 3; // RGB // 计算每行实际的字节数含填充 int row_stride (width * channels 3) ~3; // 位运算技巧向上取整到4的倍数 int padding row_stride - (width * channels); // 为所有像素分配内存 (height * width * Pixel) Pixel* image_data (Pixel*)malloc(height * width * sizeof(Pixel)); if (!image_data) { perror(内存分配失败); fclose(file); return NULL; } // 分配一个缓冲区用于读取一整行含填充 unsigned char* row_buffer (unsigned char*)malloc(row_stride); if (!row_buffer) { perror(行缓冲区分配失败); free(image_data); fclose(file); return NULL; } // 读取像素数据从最后一行开始读 for (int y height - 1; y 0; y--) { if (fread(row_buffer, 1, row_stride, file) ! row_stride) { perror(读取像素行失败); free(row_buffer); free(image_data); fclose(file); return NULL; } // 将缓冲区中的数据复制到图像数据中跳过填充字节 for (int x 0; x width; x) { int buffer_index x * channels; int pixel_index y * width x; // 计算在image_data中的索引 image_data[pixel_index].b row_buffer[buffer_index]; image_data[pixel_index].g row_buffer[buffer_index 1]; image_data[pixel_index].r row_buffer[buffer_index 2]; // 注意BMP文件存储顺序是BGR我们按BGR读到结构体但结构体成员顺序是b, g, r所以赋值是对的。 } } free(row_buffer); fclose(file); *out_width width; *out_height height; return image_data; // 调用者需要负责释放这片内存 }3.2 输出为统一格式RGB数组现在像素数据已经在内存里了是一个Pixel结构体数组。为了给DeOldify用我们需要把它转换成一种简单的格式。这里我们选择两种常见的输出方式纯文本格式每个像素的RGB值用空格或逗号隔开一行一个像素或一行一行地写。好处是肉眼可读方便调试。简单二进制格式直接把这个Pixel数组或者按RGB顺序排列的字节流写入文件。好处是文件小读写快。我们先实现一个文本格式的输出因为它最直观// 将像素数据保存为简单的文本格式 (每行: R G B) int save_pixels_as_text(const char* output_filename, Pixel* pixels, int width, int height) { FILE* file fopen(output_filename, w); if (!file) return -1; fprintf(file, %d %d\n, width, height); // 第一行写入宽高 for (int i 0; i width * height; i) { fprintf(file, %d %d %d\n, pixels[i].r, pixels[i].g, pixels[i].b); } fclose(file); printf(像素数据已保存为文本文件%s\n, output_filename); return 0; } // 将像素数据保存为原始RGB二进制格式 int save_pixels_as_binary(const char* output_filename, Pixel* pixels, int width, int height) { FILE* file fopen(output_filename, wb); if (!file) return -1; // 可选先写入宽高作为文件头 fwrite(width, sizeof(int), 1, file); fwrite(height, sizeof(int), 1, file); // 写入所有像素的RGB数据注意顺序是R, G, B for (int i 0; i width * height; i) { fwrite((pixels[i].r), sizeof(unsigned char), 1, file); fwrite((pixels[i].g), sizeof(unsigned char), 1, file); fwrite((pixels[i].b), sizeof(unsigned char), 1, file); } fclose(file); printf(像素数据已保存为二进制文件%s\n, output_filename); return 0; }4. 功能扩展支持PNG与JPEG处理完BMP我们已经掌握了图像解析的核心流程读头信息、读像素、转换格式。对于PNG和JPEG虽然它们内部编码复杂压缩、块结构等但幸运的是我们有libpng和libjpeg这样的库来帮我们处理脏活累活。我们的任务从“自己解析”变成了“正确使用库”。4.1 使用libpng读取PNGPNG支持透明度、无损压缩结构比BMP复杂。使用libpng的基本步骤是固定的。#include png.h Pixel* read_png(const char* filename, int* out_width, int* out_height) { FILE* fp fopen(filename, rb); if (!fp) return NULL; // 1. 检查PNG签名 unsigned char header[8]; fread(header, 1, 8, fp); if (png_sig_cmp(header, 0, 8)) { fprintf(stderr, 错误不是有效的PNG文件。\n); fclose(fp); return NULL; } // 2. 创建PNG读写结构体 png_structp png_ptr png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); if (!png_ptr) { fclose(fp); return NULL; } png_infop info_ptr png_create_info_struct(png_ptr); if (!info_ptr) { png_destroy_read_struct(png_ptr, NULL, NULL); fclose(fp); return NULL; } // 3. 设置错误处理 if (setjmp(png_jmpbuf(png_ptr))) { png_destroy_read_struct(png_ptr, info_ptr, NULL); fclose(fp); return NULL; } png_init_io(png_ptr, fp); png_set_sig_bytes(png_ptr, 8); // 告诉libpng我们已经读了8字节签名 // 4. 读取图像信息 png_read_info(png_ptr, info_ptr); int width png_get_image_width(png_ptr, info_ptr); int height png_get_image_height(png_ptr, info_ptr); png_byte color_type png_get_color_type(png_ptr, info_ptr); png_byte bit_depth png_get_bit_depth(png_ptr, info_ptr); // 5. 转换确保我们得到8位深度的RGB格式 if (bit_depth 16) png_set_strip_16(png_ptr); if (color_type PNG_COLOR_TYPE_PALETTE) png_set_palette_to_rgb(png_ptr); if (color_type PNG_COLOR_TYPE_GRAY bit_depth 8) png_set_expand_gray_1_2_4_to_8(png_ptr); if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS)) png_set_tRNS_to_alpha(png_ptr); if (color_type PNG_COLOR_TYPE_RGB || color_type PNG_COLOR_TYPE_GRAY || color_type PNG_COLOR_TYPE_PALETTE) png_set_filler(png_ptr, 0xFF, PNG_FILLER_AFTER); // 添加不透明的Alpha通道 if (color_type PNG_COLOR_TYPE_GRAY || color_type PNG_COLOR_TYPE_GRAY_ALPHA) png_set_gray_to_rgb(png_ptr); // 灰度转RGB png_read_update_info(png_ptr, info_ptr); // 6. 分配内存并读取图像数据 png_bytep* row_pointers (png_bytep*)malloc(sizeof(png_bytep) * height); int new_rowbytes png_get_rowbytes(png_ptr, info_ptr); for (int y 0; y height; y) { row_pointers[y] (png_byte*)malloc(new_rowbytes); } png_read_image(png_ptr, row_pointers); // 7. 将libpng的数据转换到我们的Pixel结构 Pixel* image_data (Pixel*)malloc(width * height * sizeof(Pixel)); for (int y 0; y height; y) { png_bytep row row_pointers[y]; for (int x 0; x width; x) { png_bytep px (row[x * 4]); // 现在是RGBA 4通道 int idx y * width x; image_data[idx].r px[0]; image_data[idx].g px[1]; image_data[idx].b px[2]; // 忽略Alpha通道px[3] } free(row_pointers[y]); // 释放每一行的内存 } free(row_pointers); // 8. 清理并返回 png_destroy_read_struct(png_ptr, info_ptr, NULL); fclose(fp); *out_width width; *out_height height; return image_data; }4.2 使用libjpeg读取JPEGJPEG是有损压缩格式解码过程由libjpeg库管理。#include jpeglib.h #include setjmp.h // 用于错误处理 // 自定义错误管理器结构 struct my_error_mgr { struct jpeg_error_mgr pub; jmp_buf setjmp_buffer; }; typedef struct my_error_mgr* my_error_ptr; // 错误处理函数 void my_error_exit(j_common_ptr cinfo) { my_error_ptr myerr (my_error_ptr)cinfo-err; (*cinfo-err-output_message)(cinfo); longjmp(myerr-setjmp_buffer, 1); } Pixel* read_jpeg(const char* filename, int* out_width, int* out_height) { FILE* infile fopen(filename, rb); if (!infile) return NULL; // 1. 初始化JPEG解压缩对象 struct jpeg_decompress_struct cinfo; struct my_error_mgr jerr; cinfo.err jpeg_std_error(jerr.pub); jerr.pub.error_exit my_error_exit; if (setjmp(jerr.setjmp_buffer)) { // 如果libjpeg发生错误会跳转到这里 jpeg_destroy_decompress(cinfo); fclose(infile); return NULL; } jpeg_create_decompress(cinfo); jpeg_stdio_src(cinfo, infile); jpeg_read_header(cinfo, TRUE); // 2. 设置输出为RGB格式 cinfo.out_color_space JCS_RGB; jpeg_start_decompress(cinfo); int width cinfo.output_width; int height cinfo.output_height; int channels cinfo.output_components; // 应该是3 (RGB) // 3. 分配内存存储图像数据 Pixel* image_data (Pixel*)malloc(width * height * sizeof(Pixel)); if (!image_data) { jpeg_destroy_decompress(cinfo); fclose(infile); return NULL; } // 4. 逐行读取JPEG数据 unsigned char* row_buffer (unsigned char*)malloc(width * channels); int row_stride width * channels; int current_row 0; while (cinfo.output_scanline cinfo.output_height) { jpeg_read_scanlines(cinfo, row_buffer, 1); for (int x 0; x width; x) { int idx current_row * width x; image_data[idx].r row_buffer[x * channels]; image_data[idx].g row_buffer[x * channels 1]; image_data[idx].b row_buffer[x * channels 2]; } current_row; } // 5. 清理并返回 free(row_buffer); jpeg_finish_decompress(cinfo); jpeg_destroy_decompress(cinfo); fclose(infile); *out_width width; *out_height height; return image_data; }5. 整合与使用打造你的命令行工具现在我们有了处理三种格式的“武器”。最后一步就是打造一个统一的、方便的命令行工具。5.1 创建主函数与参数解析我们将创建一个main函数根据用户输入的文件名后缀自动调用对应的读取函数然后统一进行格式转换和输出。#include string.h // 根据文件扩展名判断格式 typedef enum { FORMAT_UNKNOWN, FORMAT_BMP, FORMAT_PNG, FORMAT_JPEG } ImageFormat; ImageFormat get_image_format(const char* filename) { const char* dot strrchr(filename, .); if (!dot) return FORMAT_UNKNOWN; if (strcasecmp(dot, .bmp) 0) return FORMAT_BMP; if (strcasecmp(dot, .png) 0) return FORMAT_PNG; if (strcasecmp(dot, .jpg) 0 || strcasecmp(dot, .jpeg) 0 || strcasecmp(dot, .jpe) 0) return FORMAT_JPEG; return FORMAT_UNKNOWN; } int main(int argc, char* argv[]) { if (argc 3) { printf(用法: %s 输入图片 输出文件前缀\n, argv[0]); printf(示例: %s old_photo.bmp output\n, argv[0]); printf( 将生成 output.txt 和 output.bin\n); return 1; } const char* input_file argv[1]; const char* output_prefix argv[2]; ImageFormat format get_image_format(input_file); Pixel* pixels NULL; int width 0, height 0; // 根据格式调用不同的读取函数 switch (format) { case FORMAT_BMP: { BMPFileHeader file_header; BMPInfoHeader info_header; if (read_bmp_header(input_file, file_header, info_header) 0) { pixels read_bmp_pixels(input_file, file_header, info_header, width, height); } break; } case FORMAT_PNG: pixels read_png(input_file, width, height); break; case FORMAT_JPEG: pixels read_jpeg(input_file, width, height); break; default: fprintf(stderr, 错误不支持的文件格式或未知格式。\n); return 1; } if (!pixels) { fprintf(stderr, 错误无法读取图像数据。\n); return 1; } printf(成功读取图像%s尺寸%d x %d\n, input_file, width, height); // 构建输出文件名 char output_txt[256]; char output_bin[256]; snprintf(output_txt, sizeof(output_txt), %s.txt, output_prefix); snprintf(output_bin, sizeof(output_bin), %s.bin, output_prefix); // 保存为两种格式 if (save_pixels_as_text(output_txt, pixels, width, height) ! 0) { fprintf(stderr, 警告保存文本文件失败。\n); } if (save_pixels_as_binary(output_bin, pixels, width, height) ! 0) { fprintf(stderr, 警告保存二进制文件失败。\n); } // 释放内存 free(pixels); printf(转换完成\n); return 0; }5.2 编译与运行将以上所有代码片段整合到一个.c文件比如image_converter.c中然后使用GCC编译。记得链接我们需要的库。gcc image_converter.c -o image_converter -lpng -ljpeg编译成功后你就可以在命令行中使用它了# 转换一张BMP图片 ./image_converter sample.bmp output_sample # 转换一张PNG图片 ./image_converter drawing.png output_drawing # 转换一张JPEG图片 ./image_converter photo.jpg output_photo运行后你会得到output_sample.txt文本格式和output_sample.bin二进制格式两个文件。文本文件可以用文本编辑器打开查看二进制文件则可以被其他程序如Python脚本读取并传递给DeOldify模型。6. 总结与展望走完这一趟你应该对“图像文件”这个概念不再感到神秘了。它们本质上就是按特定规则组织起来的二进制数据。我们通过C语言直接操作这些字节完成了从文件到内存中RGB数组的转换。这个过程强化了你对结构体、指针、内存管理和文件I/O的理解。这个简易转换器已经可以工作但它还有很多可以完善的地方这也是你后续可以继续探索的方向错误处理目前的错误处理还比较基础可以更健壮给用户更明确的错误提示。更多格式可以尝试支持TIFF、WebP等格式原理相通只是需要找到或使用对应的库。功能增强添加缩放、裁剪、色彩空间转换如RGB转灰度等预处理功能。直接接口不输出到文件而是直接在内存中准备好数据通过管道或进程间通信直接传递给Python端的DeOldify脚本效率更高。最重要的是你亲手搭建了一个连接“原始数据文件”和“AI模型输入”的桥梁。理解了这个过程以后再面对任何需要数据预处理的任务你都能抓住本质知道从哪里下手了。编程的乐趣往往就藏在这些将想法一步步变成现实的过程里。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2413110.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!