最近在 github 上关注了 LLM 的流行库 llama.cpp 和 whisper.cpp 的作者 Georgi Gerganov, 简称 gg 哥。 通过 gg哥关注到了 deepdream 算法作者开源的 deepdream_c 代码。完全用 C89 写成的 deepdream, 编译只需要1秒钟, Windows 和 Linux 都能运行。完全不依赖 PyTorch 和 OpenCV 等框架, 连 C++ 都没使用,非常克制, 可移植性非常高。这个风格和 gg 哥的风格有点像的: 喜欢单一的 .vimrc
文件, 写 ggml 主要放在一个16000行的文件中。打算按照这种风格, 用 C99 标准,不借助外部库, 实现 lenet。
实现过程中难免遇到不熟悉的内容, 因为这是一种“全都自己造”的风格; 没关系, 我会尝试逐一弄懂, 以博客形式分享出来。
这是这系列的第一篇, 分享的是 pgm 图像的读写。用 pgm 格式的原因是, 项目规模非常小的时候, BMP 的编解码都显得过于复杂, 目前只需要灰度图的前提下, pgm 足够使用, 而 Linux KDE 下的默认图像查看器, 完全可以查看 pgm 格式。
pgm 里的 g 表示 gray, 是灰度图, 数据范围通常是 0 到 255。
pgm 文件,以二进制格式进行存储, 不过它的内容其实是 文本 + 二进制混合的:meta 信息是文本, 图像像素内容是二进制。
第一行是 P5 两个字符。
第二行是空格分隔的两个整数, 分别表示图像宽度,高度。
第三行是一个整数, 表示像素最大取值, 通常是255。
这些信息都是文本格式写入, 也就是用 fprintf 写入, fscanf 读取。也可以用记事本查看 .pgm 文件。
用二进制形式存储, row-major。
也就是说, 用 fread 读取, 用 fwrite 写入。
我的开发环境是 Ubuntu 22.04, KDE 桌面, 也可以叫做 “KUbuntu”. 不过我认为 KUbuntu 的叫法很奇怪,Ubuntu 并不限制你用什么桌面, 安装了 KDE 之后也可以安装 Cinamon, XFCE 等桌面, 如果安装的不是 Ubuntu 而是 OpenSude, Manjaro 等 Linux 发行版, 也可以安装 KDE。
我使用 KDE 里的 KolourPaint 这个绘图软件,制作一张手写数字“3”的图像:
简单起见, 我们直接读取刚刚用 KolourPaint 生成的 3.pgm 文件。
uchar g_image[784];
void read_pgm_image()
{
FILE* fin = fopen("3.pgm", "rb");
char magic[3];
int width, height;
int nscan = fscanf(fin, "%2s\n%d %d\n255\n", magic, &width, &height);
if (nscan == 3 && magic[0] == 'P' && magic[1] == '5')
{
fread(g_image, 784, 1, fin);
}
fclose(fin);
}
如果打算读取其他 .pgm 文件, 可以自行重构, 其中 784 等于 28 * 28
, 是图像大小。
FILE* fin = fopen("3.pgm", "rb");
是以二进制格式打开文件 3.pgm.
读取第一行的 P5
两个字符时, 使用了
char magic[3];
而不是
char magic[2];
原因是避免内存越界, 具体分析见 Cracking C++(13): 读取不超过n个字符。
int nscan = fscanf(fin, "%2s\n%d %d\n255\n", magic, &width, &height);
是读取 meta 信息,是以文本方式读取。
fread(g_image, 784, 1, fin);
是读取像素内容, 是按二进制格式读取。
和读取过程是配套的,代码如下
void write_pgm_image(uchar* image, int width, int height, const char* filename)
{
FILE* fout = fopen(filename, "wb");
fprintf(fout, "P5\n%d %d\n255\n", width, height);
fwrite(image, width * height, 1, fout);
fclose(fout);
}