scratch lenet(1): 读写 pgm 图像文件

scratch lenet(1): 读写 pgm 图像文件

文章目录

  • scratch lenet(1): 读写 pgm 图像文件
    • 1. 目的
    • 2. pgm 格式介绍
      • 2.1 概要
      • 2.2 meta 信息
      • 2.3 像素内容
    • 3. 创建 .pgm 文件
    • 4. 使用C语言读取 .pgm 灰度图文件
      • 4.1 实现
      • 4.2 解释
    • 5. 使用C语言保存 .pgm 灰度图文件

1. 目的

最近在 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 格式。

2. pgm 格式介绍

2.1 概要

pgm 里的 g 表示 gray, 是灰度图, 数据范围通常是 0 到 255。

pgm 文件,以二进制格式进行存储, 不过它的内容其实是 文本 + 二进制混合的:meta 信息是文本, 图像像素内容是二进制。

2.2 meta 信息

第一行是 P5 两个字符。
第二行是空格分隔的两个整数, 分别表示图像宽度,高度。
第三行是一个整数, 表示像素最大取值, 通常是255。

这些信息都是文本格式写入, 也就是用 fprintf 写入, fscanf 读取。也可以用记事本查看 .pgm 文件。

2.3 像素内容

用二进制形式存储, row-major。
也就是说, 用 fread 读取, 用 fwrite 写入。

3. 创建 .pgm 文件

我的开发环境是 Ubuntu 22.04, KDE 桌面, 也可以叫做 “KUbuntu”. 不过我认为 KUbuntu 的叫法很奇怪,Ubuntu 并不限制你用什么桌面, 安装了 KDE 之后也可以安装 Cinamon, XFCE 等桌面, 如果安装的不是 Ubuntu 而是 OpenSude, Manjaro 等 Linux 发行版, 也可以安装 KDE。

我使用 KDE 里的 KolourPaint 这个绘图软件,制作一张手写数字“3”的图像:

scratch lenet(1): 读写 pgm 图像文件_第1张图片

保存图像, 选择 .pgm 格式:
scratch lenet(1): 读写 pgm 图像文件_第2张图片

4. 使用C语言读取 .pgm 灰度图文件

4.1 实现

简单起见, 我们直接读取刚刚用 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, 是图像大小。

4.2 解释

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); 是读取像素内容, 是按二进制格式读取。

5. 使用C语言保存 .pgm 灰度图文件

和读取过程是配套的,代码如下

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);
}

你可能感兴趣的:(C/C++,C,pgm,编解码,深度学习)