当前位置: 首页 > article >正文

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代码中定义:

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 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. 化繁为简,在控制台显示图像

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 风格的文件读取写法.

#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

  • https://learn.microsoft.com/en-us/previous-versions/visualstudio/visual-studio-2015/debugger/image-watch/image-watch-reference?view=vs-2015#pixel-formats

http://www.kler.cn/a/419155.html

相关文章:

  • Proxy详解
  • 机器学习算法(六)---逻辑回归
  • 三十二:网络爬虫的工作原理与应对方式
  • 网络安全相关证书资料
  • 【Oracle11g SQL详解】ORDER BY 子句的排序规则与应用
  • Permute for Mac 媒体文件格式转换软件 安装教程【音视频图像文件转换,简单操作,轻松转换,提高效率】
  • EtherCAT转DeviceNe台达MH2与欧姆龙CJ1W-DRM21通讯案例
  • grpc与rpcx的区别
  • Qt 面试题学习13_2024-12-1
  • 第n小的质数
  • 【韩顺平老师Java反射笔记】
  • SpringBoot 助力新冠密接者跟踪:大数据整合与深度挖掘的力量
  • 极致性能:19个Vue 项目的优化手段
  • C++关于二叉树的具体实现
  • (4)CHATGPT-3和GPT-4是生成式AI的一部分吗?
  • 【二分查找】力扣 2529. 正整数和负整数的最大计数
  • HTML CSS JS基础考试题与答案
  • springboot kafka在kafka server AUTH变动后consumer自动销毁
  • linux系统信号简介
  • Scala—列表(可变ListBuffer、不可变List)用法详解
  • FAT文件系统
  • 【ETCD】etcd简单入门之基础操作基于etcdctl进行操作
  • arkTS:持久化储存UI状态的基本用法(PersistentStorage)
  • 基于Java Springboot宠物医院微信小程序
  • UI设计-色彩、层级、字体、边距(二)
  • 民锋视角:数据分析如何助力金融决策