digit_eye开发记录(3): C语言读取MNIST数据集

在前两篇,我们解读了 MNIST 数据集的 IDX 文件格式,并分别用 C++ 和 Python 做了 读取 MNIST 数据集的实现。 基于 C++ 的代码稍长,基于 Python 的代码则明显更短,然而它们的共同特点是:依赖了外部库:

  • 基于 C++ 的实现: 依赖了 OpenCV
  • 基于 Python 的实现: 依赖了 Numpy

基于 C++ 的实现,有哪些问题

为了配置 OpenCV,无论是手动下载 OpenCV 预编译包 + 自行写 CMake 配置; 还是安装 vcpkg 后,从 vcpkg 安装 OpenCV + 自行写 CMake 配置,都略微麻烦:

  • vcpkg install opencv 会在本地源码编译 opencv,耗时几十分钟

即便配置完毕,还会看到关于 cmake minimum version 的提示:

读取 MNIST 数据集这个任务的规模很小,不用 vcpkg、不用 OpenCV,完全可以做到的。更进一步,还可以拿掉 C++ 的 std::vectorstd::stringstd::fstream. 那么为啥不用 C 语言实现?完全可以。

基于 Python 的实现,有哪些问题

Pure Python 的性能堪忧,调用 Numpy 库性能确实不错,但 Numpy 是 C/C++ 实现,这性能其实和 Python 本身无关。

如果为了让代码短小,那么基于 numpy 的实现也仍显啰嗦:tensorflow/pytorch/keras/sklearn 等开源库,早就提供了 mnist 的读取的实现,安静的做一个调用者,也挺快乐的,不是吗?

基于 C 语言的实现 - 可视化怎么做?

1. 基于 ImageWatch 的自定义图像格式可视化

基于 C++ 的实现, 用了 OpenCV 是为了图像可视化,是为了验证图像和标签是否配对。抛开 OpenCV,在 Windows 下可以使用 Visual Studio 中的 ImageWatch 插件,自行扩展一下,可以得到可视化。

先看一下效果:左侧是meta信息,表明是 DE_GrayImage 类型的数据结构,大小是28x28,元素是 UINT8 类型,通道是1个;右图则是 ImageWatch 可视化的结果

ImageWatch 还提供了常见图像操作,如阈值化,@thread(image, 128) 后可视化为:

又或者,旋转90度:@rot90(image):

其他更多操作,可以在 ImageWatch文档 找到:

我们回到如何显示上述的 DE_GrayImage 类型的问题上:首先在C代码中定义:

c 复制代码
typedef struct DE_GrayImage
{
    unsigned int width;
    unsigned int height;
    unsigned char* data;
} DE_GrayImage;

然后创建文件 C:\Users\zz\Documents\Visual Studio 2022\Visualizers\DE_GrayImage.natvis, 内容如下:

xml 复制代码
<?xml version="1.0" encoding="utf-8"?> 
<AutoVisualizer xmlns="http://schemas.microsoft.com/vstudio/debugger/natvis/2010"> 
  <UIVisualizer ServiceId="{A452AFEA-3DF6-46BB-9177-C0B08F318025}" Id="1"  
                MenuName="Add to Image Watch"/> 
  <Type Name="DE_GrayImage"> 
    <UIVisualizer ServiceId="{A452AFEA-3DF6-46BB-9177-C0B08F318025}" Id="1" /> 
  </Type> 
  <Type Name="DE_GrayImage"> 
    <Expand> 
      <Synthetic Name="[type]"> 
        <DisplayString>UINT8</DisplayString> 
      </Synthetic>
      <Item Name="[channels]">1</Item> 
      <Item Name="[width]">width</Item> 
      <Item Name="[height]">height</Item> 
      <Item Name="[data]">data</Item> 
      <Item Name="[stride]">width</Item> 
    </Expand> 
  </Type>   
</AutoVisualizer>

简单解释下:

  • [type], [channels], [width], [height], [data], [stride] 是 ImageWatch 插件规定我们在编写 .natvis 文件来可视化图像时,需要填写的字段
  • <Item Name="[channels]">1</Item> 是为 channels 硬编码一个数值
  • <Synthetic Name="[type]" 则是指定数据类型

保存 .natvis 文件后,重新执行 Visual Studio 里的调试会话,就可以查看 DE_GrayImage 类型的图像的可视化了。嗯, ImageWatch 挺强大的。

不过, ImageWatch 也有不足

第一个不足:当 ImageWatch 查看的表达式本身非法时,并没有什么提示。

例如 dataset->images[0], 在 print_sample 函数内,ImageWatch 能正常显示图像内容,因为此时 dataset->images[0] 是合法的表达式

而当调用堆栈回到 main 函数, dataset->images[0] 不再是合法表达式, ImageWatch 直接显示为 invalid:

而仔细检查了代码后,发现此时 dataset 类型是 DataSet 而非 DataSet* 后,改为使用 dataset. Images[0] ,就能正常显示:

第二个不足: @mem(address, type, channels, width, height, stride) 并不能把一块内存当作图像显示

2. 化繁为简,在控制台显示图像

c 复制代码
void print_sample(const DataSet* dataset, int index)
{
    DE_GrayImage* image = &dataset->images[index];

    printf("label: %d\n", (int)dataset->labels[index]);
    for (int i=0; i<28; i++)
    {
        for (int j=0; j<28; j++)
        {
            for (int k=0; k<3;k++)
                printf("%c", image->data[i * 28 + j] > 128 ? '#' : ' ');
        }
        printf("\n");
    }
}


完整代码

对于 MNIST 数据的读取,由于我们已经很熟悉它的格式,这里直接给出 C 风格的文件读取写法.

cpp 复制代码
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>

long get_filesize(FILE* fp)
{
    fseek(fp, 0, SEEK_END);
    long filesize = ftell(fp);
    fseek(fp, 0, SEEK_SET);
    return filesize;
}

typedef enum Endian {
    ENDIAN_LSB = 0,
    ENDIAN_MSB = 1
} Endian;

int read_int_from_4_bytes(unsigned char* buf, Endian endian)
{
    int x = 0;
    int c[2][4] = {
        { (1 << 0),  (1 << 8), (1 << 16), (1 << 24) },
        { (1 << 24), (1 << 16), (1 << 8), (1 << 0) }
    };
    for (int i=0; i<4; i++)
        x += buf[i] * c[endian][i];
    return x;
}

typedef struct DE_GrayImage
{
    unsigned int width;
    unsigned int height;
    unsigned char* data;
} DE_GrayImage;

typedef struct DataSet
{
    DE_GrayImage* images;
    uint8_t* labels;
    uint8_t* image_buf;
    uint8_t* label_buf;
    int num_images;
    int num_labels;
} DataSet;

void destroy_dataset(DataSet* dataset)
{
    if (dataset)
    {
        free(dataset->image_buf);
        dataset->image_buf = NULL;

        free(dataset->label_buf);
        dataset->labels = NULL;
        
        free(dataset->images);
        dataset->images = NULL;
    }
}

void load_labels(DataSet* dataset, const char* filename)
{
    FILE* fin = fopen(filename, "rb");
    long filesize = get_filesize(fin);
    unsigned char* buf = (unsigned char*)malloc(filesize + 1);
    if (buf == NULL)
        exit(1);
    buf[filesize] = '\0';
    dataset->label_buf = buf;
    fread((void*)buf, filesize, 1, fin);
    fclose(fin);
    dataset->num_labels = read_int_from_4_bytes(buf + 4, ENDIAN_MSB);
    dataset->labels = buf + 8;
}

void load_images(DataSet* dataset, const char* filename)
{
    FILE* fin = fopen(filename, "rb");
    long filesize = get_filesize(fin);

    unsigned char* buf = (unsigned char*)malloc(filesize + 1);
    if (buf == NULL)
        exit(1);
    dataset->image_buf = buf;
    buf[filesize] = '\0';
    fread((void*)buf, filesize, 1, fin);
    fclose(fin);

    uint8_t magic[4] = { buf[0], buf[1], buf[2], buf[3] };

    int num_images = read_int_from_4_bytes(buf + 4, ENDIAN_MSB);
    int rows = read_int_from_4_bytes(buf + 8, ENDIAN_MSB);
    int cols = read_int_from_4_bytes(buf + 12, ENDIAN_MSB);
   
    DE_GrayImage* images = (DE_GrayImage*)malloc(sizeof(DE_GrayImage) * num_images);
    if (images == NULL) 
        exit(1);
    dataset->images = images;
    for (int i=0; i<num_images; i++)
    {
        images[i].height = rows;
        images[i].width = cols;
        images[i].data = buf + 16 + i * rows * cols;
    }
}

void print_sample(const DataSet* dataset, int index)
{
    DE_GrayImage* image = &dataset->images[index];

    printf("label: %d\n", (int)dataset->labels[index]);
    for (int i=0; i<28; i++)
    {
        for (int j=0; j<28; j++)
        {
            for (int k=0; k<3;k++)
                printf("%c", image->data[i * 28 + j] > 128 ? '#' : ' ');
        }
        printf("\n");
    }
}

int main()
{
    DataSet dataset;
    load_images(&dataset, "C:/work/digit_eye/data/train-images.idx3-ubyte");
    load_labels(&dataset, "C:/work/digit_eye/data/train-labels.idx1-ubyte");
    
    print_sample(&dataset, 0);
    print_sample(&dataset, 233);
    print_sample(&dataset, 666);

    printf("wait\n");
    destroy_dataset(&dataset);

    return 0;
}

总结

这一篇尝试了以最少依赖的方式,实现 MNIST 数据集的读取,假定了读者已经熟悉 MNIST 数据集格式。 使用 C 语言而非 C++,在图像可视化方面去掉了对于 OpenCV 的依赖,探索了使用 ImageWatch 插件、 在控制台输出这两种方式;在文件读取方面使用 C标准库的 fopen, fread, ftell 等 API 替代了 C++ 的 std::fstream

References

相关推荐
R6bandito_41 分钟前
Qt几何数据类型:QLine类型详解(基础向)
c语言·开发语言·c++·经验分享·qt
禊月初三1 小时前
C++面试突破---C/C++基础
c语言·c++·面试
就爱学编程1 小时前
重生之我在异世界学编程之C语言:选择结构与循环结构篇
c语言·数据结构·算法
橘子真甜~1 小时前
Linux操作系统3-文件与IO操作1(从C语言IO操作到系统调用)
linux·运维·服务器·c语言·io·文件操作·文件fd
Lbs_gemini06038 小时前
C++研发笔记14——C语言程序设计初阶学习笔记12
c语言·开发语言·c++·笔记·学习
没有名字的鬼14 小时前
C_字符串的一些函数
c语言·开发语言·算法
Heris9914 小时前
零基础快速掌握——c语言基础【二维数组】
c语言·开发语言·数据结构·算法
tjsoft15 小时前
delphi 12 idhttpsever(S)+idhttp(C) 实现简单的JSON API服务
c语言·开发语言·json
武昌库里写JAVA15 小时前
SpringBoot
c语言·开发语言·数据结构·算法·二维数组
Ning_.16 小时前
C语言链表分区问题
c语言·网络·链表