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.rhkb.cn/news/482504.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C#窗体小程序计算器

使其能完成2个数的加、减、乘、除基本运算。界面如下图&#xff0c;单击相应的运算符按钮&#xff0c;则完成相应的运算&#xff0c;并将结果显示出来&#xff0c;同时不允许在结果栏中输入内容 代码如下&#xff1a; private void button1_Click(object sender, EventArgs e)…

Linux命令进阶·如何切换root以及回退、sudo命令、用户/用户组管理,以及解决创建用户不显示问题和Ubuntu不显示用户名只显示“$“符号问题

目录 1. root用户&#xff08;超级管理员&#xff09; 1.1 用于账户切换的系统命令——su 1.2 退回上一个用户命令——exit 1.3 普通命令临时授权root身份执行——sudo 1.3.1 为普通用户配置sudo认证 2. 用户/用户组管理 2.1 用户组管理 2.2 用户管理 2.2.1 …

【JavaEE】JavaEE、web 开发、框架(Spring) 、Maven

文章目录 一、JavaEE 发展历程二、什么是 web 开发1、什么是 web 开发&#xff1f;2、web 网站的工作流程 三、框架1、什么是框架&#xff1f;2、为什么要学框架&#xff1f;3、框架的优点&#xff08;Spring Boot VS Servlet&#xff09; 四、Maven 一、JavaEE 发展历程 Java…

虚拟机玩游戏,轻松实现多开不同IP

嘿&#xff0c;亲爱的游戏小伙伴们&#xff01;今天要和大家分享一个超级实用的技巧&#xff0c;让你在游戏中轻松多开不同IP&#xff0c;享受开挂的乐趣&#xff01; 第一步&#xff1a;准备虚拟机 首先&#xff0c;你需要下载一个虚拟机软件&#xff0c;比如VMware或者Virt…

MySQL常用语句整理

《SQL必知必会》(第3版)SQL是目前使用最为广泛的数据库语言之一。本书没有涉及理论&#xff0c;而是从实践出发&#xff0c;由浅入深地讲解了广大读者所必需的SQL知识&#xff0c;适用于各种主流数据库。实例丰富&#xff0c;便于查阅。本书涉及不同平台上数据的排序、过滤和分…

【MATLAB】基于RSSI的蓝牙定位与例程,设置4个基站、二维定位

目录 ​编辑 商品描述 主要功能 技术细节 适用场景 下载链接 商品描述 这款基于接收信号强度指示&#xff08;RSSI&#xff09;原理的蓝牙定位程序&#xff0c;专为需要高效、可靠定位解决方案的开发者和研究人员设计。它能够在二维平面内&#xff0c;通过4个锚点实现对未…

DreamCamera2相机预览变形的处理

最近遇到一个问题&#xff0c;相机更换了摄像头后&#xff0c;发现人像角度顺时针旋转了90度&#xff0c;待人像角度正常后&#xff0c;发现 预览时图像有挤压变形&#xff0c;最终解决。在此记录 一人像角度的修改 先放示意图 设备预览人像角度如图1所示&#xff0c;顺时针旋…

鸿蒙修饰符

文章目录 一、引言1.1 什么是修饰符1.2 修饰符在鸿蒙开发中的重要性1.3 修饰符的作用机制 二、UI装饰类修饰符2.1 Styles修饰符2.1.1 基本概念和使用场景2.1.2 使用示例2.1.3 最佳实践 2.2 Extend修饰符2.2.1 基本概念2.2.2 使用示例2.2.3 Extend vs Styles 对比2.2.4 使用建议…

phpmyadmin导出wordpress数据教程

网站搬家或网站修改&#xff0c;需要导出数据或备份数据&#xff0c;一般主机控制面板最常用phpmyadmin。下面这个是一个有详细图文操作步骤的phpmyadmin导出wordpress数据教程。 1、先登陆到你主机的管理面板&#xff0c;找到数据库&#xff0c;再找到phpmyadmin&#xff0c;…

阅读笔记——SVD本质+计算+应用

摘要&#xff1a;本文讨论了正交相似对角化的方法&#xff0c;几何含义&#xff1b;方阵&#xff0c;非方阵的奇异值分解的计算&#xff0c;矫正方法以及与正交相似对角化存在区别&#xff1b;最后讨论了奇异值分解的应用。 1.实对称矩阵A的变换是一种线性变换&#xff0c;对应…

Linux网络_网络协议_网络传输_网络字节序

一.协议 1.概念 协议&#xff08;Protocol&#xff09; 是一组规则和约定&#xff0c;用于定义计算机网络中不同设备之间如何进行通信和数据交换。协议规定了数据的格式、传输方式、传输顺序等详细规则&#xff0c;确保不同设备和系统能够有效地互联互通。 在网络通信中&#…

数据结构 (16)特殊矩阵的压缩存储

前言 特殊矩阵的压缩存储是数据结构中的一个重要概念&#xff0c;它旨在通过找出特殊矩阵中值相同的矩阵元素的分布规律&#xff0c;把那些呈现规律性分布的、值相同的多个矩阵元素压缩存储到一个存储空间中&#xff0c;从而节省存储空间。 一、特殊矩阵的定义 特殊矩阵是指具有…

ehr系统建设方案,人力资源功能模块主要分为哪些,hrm平台实际案例源码,springboot人力资源系统,vue,JAVA语言hr系统(源码)

eHR人力资源管理系统&#xff1a;功能强大的人力资源管理工具 随着企业规模的不断扩大和业务需求的多样化&#xff0c;传统的人力资源管理模式已无法满足现代企业的需求。eHR人力资源管理系统作为一种先进的管理工具&#xff0c;能够为企业提供高效、准确、实时的人力资源管理。…

搭建AD域服务器

搭建AD域服务器 使用深信服HCI搭建AD域服务器 1、新建虚拟机 2、填写参数 3、省略安装过程 4、进入服务器管理器 5、 6、 7、 8、 9、 10、 11、 12、 13、 14、 15、 16、 17、 18、 19、 20、 21、 22、 23、

MOH: MULTI-HEAD ATTENTION AS MIXTURE-OFHEAD ATTENTION

当前的问题 多头注意力使用多个头部可以提高模型的精度。然而&#xff0c;并不是所有的注意力头都具有同样的重要性。一些研究表明&#xff0c;许多注意力头可以被修剪而不影响准确性。 此外&#xff0c;在多头注意中&#xff0c;每个注意头并行操作&#xff0c;最终输出是所…

Spring boot之BeanDefinition介绍

在spring框架中IOC容器进行bean的创建和管理。Bean的创建是一个比较复杂的过程&#xff0c;它并不像我们创建对象一样只是直接new一下就行&#xff0c;虽然有些bean确实就是New一下。但在Spring中可以通过一些途径对bean进行增强扩展。在这个过程中&#xff0c;BeanDefinition作…

Ubuntu 服务器部署 Tomcat 并配置 SSL/TLS 证书

本文目录 准备登陆云服务器安装 Java下载 tomcat 包配置防火墙浏览器访问 Tomcat 默认页面以服务的形式运行 Tomcat创建 Tomcat 用户和组创建 systemd 服务文件启动 tomcat 服务 Tomcat webapps 文件目录部署一个静态网站tomcat 的配置文件 将域名解析到服务器Tomcat 配置 SSL/…

C++小问题

怎么分辨const修饰的是谁 是限定谁不能被改变的&#xff1f; 在C中&#xff0c;const关键字的用途和位置非常关键&#xff0c;它决定了谁不能被修改。const可以修饰变量、指针、引用等不同的对象&#xff0c;并且具体的作用取决于const的修饰位置。理解const的规则能够帮助我们…

PPT不能编辑,按钮都是灰色,怎么办?

PPT文件打开之后&#xff0c;发现无法编辑&#xff0c;再仔细查看发现工具栏中的功能按钮都是灰色的&#xff0c;无法使用&#xff0c;这是什么原因&#xff1f;该如何解决&#xff1f; 原因&#xff1a;无法编辑PPT文件&#xff0c;并且功能按钮都是灰色&#xff0c;这是因为…

相交链表和环形链表

&#xff08;一&#xff09;相交链表 相交链表 思路&#xff1a;先分别计算出A列表和B列表的长度&#xff0c;判断它们的尾节点是否相等&#xff0c;如果不相等就不相交&#xff0c;直接返回空。然后让两个列表中的长的列表先走它们的差距步&#xff0c;然后再一起走&#xff…