借用的GitHub源代码地址:
https://github.com/Dianasfan-xiaoyang/C-
原up主视频链接:
https://www.bilibili.com/video/BV1iP4y1Y7CA/?spm_id_from=333.999.0.0&vd_source=bf0e7006c28cadb7082af8daac683005#/
初始化操作
一些结构体(与后文有一部分重合)
struct parameter //网络参数结构体
{
double kernel1[3][3];
double kernel11[3][3];
double kernel2[3][3];
double kernel22[3][3];
double kernel3[3][3];
double kernel33[3][3];
double firsthiddenlayer[1152][180];
double secondhiddenlayer[180][45];
double outhiddenlayer[45][10];
};
struct result //保存网络每一步输出结果的结构体,为反向传播计算梯度提供数据
{
double picturedata[30][30];
double firstcon[28][28];
double firstcon1[28][28];
double secondcon[26][26];
double secondcon1[26][26];
double thirdcon[24][24];
double thirdcon1[24][24];
double beforepool[1][1152];
double firstmlp[1][180];
double firstrelu[1][180];
double secondmlp[1][45];
double secondrelu[1][45];
double outmlp[1][10]; //全连接输出
double result[10]; //softmax输出
};
struct input //保存全部训练集的结构体,训练集为30*30像素图片
{
double a[10][SAMPLE_NUM][30][30];
};
struct sample //保存每一个图片样本的结构体
{
double a[30][30];
int number; //样本标签,0~9之间的数字
}
Sample[SAMPLE_NUM * 10];
initialization(初始化网络参数)
void initialization(struct parameter * a) //用随机数初始化网络参数
{
srand(time(NULL));
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel1[j][k] = (rand() / (RAND_MAX + 1.0));
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel2[j][k] = (rand() / (RAND_MAX + 1.0)) / 5;
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel3[j][k] = (rand() / (RAND_MAX + 1.0)) / 3;
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel11[j][k] = (rand() / (RAND_MAX + 1.0));
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel22[j][k] = (rand() / (RAND_MAX + 1.0)) / 5;
for (int j = 0; j < 3; j++)
for (int k = 0; k < 3; k++) a -> kernel33[j][k] = (rand() / (RAND_MAX + 1.0)) / 3;
for (int i = 0; i < 1152; i++)
for (int j = 0; j < 180; j++) a -> firsthiddenlayer[i][j] = (rand() / (RAND_MAX + 1.0)) / 1000;
for (int i = 0; i < 180; i++)
for (int j = 0; j < 45; j++) a -> secondhiddenlayer[i][j] = (rand() / (RAND_MAX + 1.0)) / 100;
for (int i = 0; i < 45; i++)
for (int j = 0; j < 10; j++) a -> outhiddenlayer[i][j] = (rand() / (RAND_MAX + 1.0)) / 10;
}
文件操作
首先是读取数据集操作
读取文件夹
sprintf(route_name1, "%d%s", m, "\\");
代码行:
sprintf(route_name1, "%d%s", m, "\\\\");
route_name1
是未被初始化的char[5]
(可能是任意值,除非使用={0}
)
是用来格式化字符串并将结果存储在字符数组 route_name1
中的。具体来说:
sprintf
是一个格式化输出函数,类似于printf
,但它将格式化后的字符串输出到一个字符串(字符数组)而不是标准输出。- 第一个参数
route_name1
是指向字符数组的指针,这个数组将接收格式化后的字符串。 - 第二个参数
"%d%s"
是一个格式字符串,它指定了后续参数如何被格式化:%d
表示一个整数,这里对应变量m
的值。%s
表示一个字符串,这里对应参数"\\\\"
(即一个字面反斜杠字符串)。
m
是一个整数变量,它的值将被插入到格式字符串中的%d
位置。"\\"
是一个字符串字面量,表示两个反斜杠。在 C 语言中,反斜杠\
是转义字符的开始,因此要表示一个字面的反斜杠,需要使用两个反斜杠\\
。
这行代码的目的是构造一个路径段,其中包含数字 m
和一个反斜杠。例如,如果 m
是 5,那么执行 sprintf
后 route_name1
将包含字符串 "5\\"
。
需要注意的是:
- 确保
route_name1
数组足够大以存储格式化后的字符串,包括终止字符 ‘\0’。 - 如果
route_name1
数组太小,可能会导致缓冲区溢出,这是一个严重的安全问题。 - 在 Windows 系统中,文件路径通常使用反斜杠
\\
作为分隔符,而在 Unix/Linux 系统中,通常使用正斜杠/
。因此,这个代码片段是特定于 Windows 路径格式的。
strcat(route_name2, route_name1);
在C语言中,strcat
函数用于将一个字符串连接到另一个字符串的末尾。在您提供的代码片段中:
strcat(route_name2, route_name1);
这行代码的作用是将 route_name1
数组中的字符串追加到 route_name2
数组的末尾。以下是详细说明:
route_name2
初始化:route_name2
是一个字符数组,它被初始化为"Training_set\\"
。这意味着它有足够的空间来存储 “Training_set” 后面的反斜杠和额外的字符串。route_name1
格式化:route_name1
是通过sprintf
函数格式化的,假设格式化成功,它将包含一个整数(由变量m
的值决定)和一个反斜杠。例如,如果m
是 5,route_name1
将包含"5\\"
。- 连接操作:
strcat
函数将route_name1
的内容追加到route_name2
的末尾。在连接之前,route_name2
是"Training_set\\"
,连接后,如果m
是 5,route_name2
将变为"Training_set\\5\\"
。 - 注意事项:
- 确保
route_name2
有足够的空间来容纳route_name1
的内容和字符串终止符\\0
。 strcat
函数会覆盖route_name2
中的字符串终止符\\0
,并将route_name1
的内容复制到route_name2
的末尾,然后在新的位置放置一个字符串终止符。- 如果
route_name1
没有以\\0
结尾,strcat
可能会导致缓冲区溢出,这是一个严重的安全问题。
- 确保
- 未初始化问题:如果
route_name1
没有被正确初始化,它的值是未定义的,这可能导致strcat
函数行为不可预测。因此,在使用strcat
之前,确保route_name1
已经被格式化或初始化为一个有效的字符串。 - 路径构建:在这个上下文中,
strcat
用于构建文件路径。构建路径时,通常需要考虑不同操作系统中的路径分隔符差异,以及确保路径的正确性和安全性。
总之,strcat(route_name2, route_name1);
这行代码将 route_name1
中的字符串追加到 route_name2
中,构建了一个完整的文件路径。使用时需要谨慎,确保不会发生缓冲区溢出等问题。
FILE * fp;
在 C 语言中,FILE
是一个结构体,通常在 <stdio.h>
头文件中定义,它代表文件的内部结构,用于文件输入输出操作。FILE * fp;
这行代码声明了一个指向 FILE
结构的指针 fp
。
这个指针通常用于指向一个文件流,文件流可以是输入流(如从文件读取数据)或输出流(如向文件写入数据)。以下是一些常见的操作:
-
打开文件:使用
fopen
函数可以打开一个文件,并返回一个FILE *
类型的指针,指向新创建的文件流。该结构体包含了文件的状态信息,如文件位置指示器、文件模式(读、写、追加等)、缓冲区信息等。 -
例如:
FILE *fp = fopen("example.txt", "r"); // 以读取模式打开文件
如果文件成功打开,
fp
将不是一个NULL
指针;如果打开失败,fp
将被赋值为NULL
。 -
读取文件:使用
fread
或fgets
等函数可以从文件中读取数据。 -
写入文件:使用
fwrite
或fputs
等函数可以向文件写入数据。 -
文件定位:使用
fseek
函数可以在文件中定位到特定的位置。 -
关闭文件:使用
fclose
函数关闭文件,释放与文件流相关联的资源。例如:fclose(fp); // 关闭文件
-
错误检查:
ferror
函数可以用来检查文件流是否有错误发生。 -
文件结束检测:
feof
函数可以检测文件流是否到达文件末尾。
FILE * fp;
只是一个指针声明,要使用 fp
进行文件操作,你需要先通过 fopen
函数打开一个文件,并确保正确处理文件打开失败的情况。使用完文件后,应该使用 fclose
函数关闭文件以释放系统资源。
-
FILE结构体
#ifndef _FILE_DEFINED struct _iobuf { #ifdef _UCRT void *_Placeholder; #else char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; #endif }; typedef struct _iobuf FILE; #define _FILE_DEFINED #endif
这段代码使用了预处理器指令来定义
FILE
结构体,并确保这个结构体只被定义一次。下面是每条指令的详细解释:#ifndef _FILE_DEFINED
这个指令检查_FILE_DEFINED
宏是否未定义。如果已定义,下面的代码将不会被包含进编译过程中。struct _iobuf { ... };
如果_FILE_DEFINED
未定义,这里定义了一个名为_iobuf
的结构体。这个结构体是FILE
结构的内部表示,通常在实现中是不可见的。#ifdef _UCRT ... #else ... #endif
这是一系列条件编译指令:#ifdef _UCRT
检查_UCRT
宏是否已定义。如果已定义,则使用 UCRT(Universal C Runtime)特定的实现。void *_Placeholder;
在 UCRT 条件下,结构体包含一个占位符指针_Placeholder
。#else
如果_UCRT
未定义,将使用传统的文件缓冲结构体实现。#endif
结束条件编译块。
typedef struct _iobuf FILE;
这行代码为_iobuf
结构体定义了一个新类型名FILE
。这样,你可以在代码中使用FILE*
来声明指向文件流的指针。#define _FILE_DEFINED
定义了_FILE_DEFINED
宏,以确保FILE
结构体在后续的代码中不会被再次定义。#endif
这个指令结束了最开始的#ifndef _FILE_DEFINED
条件编译块。
这种条件编译的方式允许开发者在不同的编译环境下控制代码的包含。例如,如果你在编译时定义了
_UCRT
,编译器将会使用与 Universal C Runtime 兼容的代码。如果没有定义_UCRT
,则会使用标准的 MSVCRT(Microsoft Visual C Runtime)代码。此外,通过使用
_FILE_DEFINED
宏,可以确保FILE
结构体在整个程序中只被定义一次,避免在包含多个头文件时重复定义结构体,这在大型项目中是很常见的。这是一种常见的做法,用于保护类型定义不被重复。
fp = fopen(route_name2, "rb")
在 C 语言中,fopen
函数用于打开文件,并返回一个指向 FILE
结构体的指针,该结构体包含了文件流的信息。下面是对 fp = fopen(route_name2, "rb");
这行代码的中文详细解释:
fp
: 这是一个指向FILE
结构体的指针变量。FILE
是 C 语言标准库中的一个结构体,用于表示文件流。fp
用于在程序中引用打开的文件,并进行后续的读写操作。fopen
: 这是 C 语言标准库中的一个函数,用于打开文件。它的原型是FILE* fopen(const char* filename, const char* mode);
,其中:const char* filename
: 指定要打开的文件的名称。在这个例子中,它是route_name2
,一个包含文件路径的字符串变量。const char* mode
: 指定文件打开的模式。在这个例子中,模式是"rb"
。
route_name2
: 这是一个字符数组或者字符串,包含了要打开文件的路径。在执行fopen
调用之前,这个变量应该已经被赋值为包含文件路径的字符串。"rb"
: 这是一个指定文件打开模式的字符串,其中:"r"
: 表示以只读方式打开文件。如果文件不存在,或无法以读取模式打开,fopen
将返回NULL
。"b"
: 表示以二进制模式打开文件。这个模式在不同平台上的意义可能不同。在 Windows 上,它告诉系统文件是二进制文件,而在 UNIX/Linux 系统上,它通常不是必需的,因为这些系统默认以二进制方式读取文件。
执行 fp = fopen(route_name2, "rb");
这行代码后,会发生以下情况之一:
- 如果文件成功打开,
fopen
将返回指向新创建的FILE
结构体的指针,fp
将包含这个指针,并且你可以使用fp
进行后续的文件操作,如读取文件内容。 - 如果文件打开失败(可能是因为文件不存在、路径错误、没有读取权限等),
fopen
将返回NULL
。在这种情况下,fp
将被赋值为NULL
。
在实际编程中,通常需要检查 fp
是否为 NULL
来确定文件是否成功打开,并据此进行错误处理:
FILE *fp;
// ... 假设 route_name2 已经被正确赋值 ...
fp = fopen(route_name2, "rb");
if (fp == NULL) {
// 文件打开失败的处理代码,例如打印错误信息
printf("无法打开文件:%s\\n", route_name2);
} else {
// 文件成功打开,可以进行文件读取操作
// ... 进行文件操作 ...
// 操作完成后,不要忘记关闭文件
fclose(fp);
}
正确关闭文件是一个好习惯,它释放了系统资源并确保了文件内容被正确保存。这可以通过调用 fclose(fp);
来完成。
fseek(fp, 62, SEEK_SET);
fseek
函数是 C 语言标准库中用于文件定位的函数。这个函数允许你移动文件流的当前位置指针到文件中的特定位置。下面是对 fseek(fp, 62, SEEK_SET);
这行代码的详细解释:
fp
: 这是一个FILE *
类型的指针,指向之前用fopen
打开的文件流。fseek
: 函数名称,表示“文件定位”。62
: 这是要移动到的目标位置(以字节为单位)相对于文件开头的偏移量。在这个例子中,62
表示从文件开头向后移动 62 个字节。SEEK_SET
: 这是fseek
函数中的一个宏,指定了偏移量62
的参考点是文件的开头。SEEK_SET
是 POSIX 标准定义的宏,用于指示fseek
从文件的开始处设置文件位置指示器。
执行 fseek(fp, 62, SEEK_SET);
后,文件的当前读写位置将被设置为从文件开头开始的第 62 个字节。这通常用于跳过文件头部的一些内容,例如在 BMP 图像文件中,通常包含一个 54 字节的位图文件头,之后才是图像像素数据的开始。因此,如果你要读取 BMP 图像的像素数据,可能需要执行类似的 fseek
调用来跳过文件头。此处数据集中是单色位bmp图像,因此在54字节之后还要加上8个字节的调色板信息。
需要注意的是,fseek
只影响文件的读写位置,不改变文件内容。如果需要读取或写入文件,还需要使用 fread
、fwrite
等函数进行操作。
另外,fseek
函数返回一个整数,如果成功将文件位置指示器设置到正确的位置,则返回 0;如果发生错误,如文件不可寻址,则返回非零值。因此,在实际编程中,调用 fseek
后检查其返回值是一种良好的编程实践。
int result = fseek(fp, 62, SEEK_SET);
if (result != 0) {
// 处理错误,例如打印错误信息或返回
printf("Error seeking file position.\\n");
// fclose(fp); // 如果需要的话,关闭文件
// return; // 或者其他适当的错误处理
}
-
bmp
一.简介
BMP(Bitmap-File)图形文件是Windows采用的图形文件格式,在Windows环境下运行的所有图象处理软件都支持BMP图象文件格式。Windows系统内部各图像绘制操作都是以BMP为基础的。Windows 3.0以前的BMP图文件格式与显示设备有关,因此把这种BMP图象文件格式称为设备相关位图DDB(device-dependent bitmap)文件格式。Windows 3.0以后的BMP图象文件与显示设备无关,因此把这种BMP图象文件格式称为设备无关位图DIB(device-independent bitmap)格式(注:Windows 3.0以后,在系统中仍然存在DDB位图,象BitBlt()这种函数就是基于DDB位图的,只不过如果你想将图像以BMP格式保存到磁盘文件中时,微软极力推荐你以DIB格式保存),目的是为了让Windows能够在任何类型的显示设备上显示所存储的图象。BMP位图文件默认的文件扩展名是BMP或者bmp(有时它也会以.DIB或.RLE作扩展名)。
二.BMP格式结构
BMP文件的数据按照从文件头开始的先后顺序分为四个部分:
◆ 位图文件头(bmp file header): 提供文件的格式、大小等信息
◆ **位图信息头(bitmap information):**提供图像数据的尺寸、位平面数、压缩方式、颜色索引等信息
◆ **调色板(color palette):**可选,如使用索引来表示图像,调色板就是索引与其对应的颜色的映射表
◆ **位图数据(bitmap data):**图像数据区
BMP图片文件数据表如下:
数据段名称 大小(byte) 开始地址 结束地址 位图文件头(bitmap-file header) 14 0000h 000Dh 位图信息头(bitmap-information header) 40 000Eh 0035h 调色板(color table) 由biBitCount决定 0036h 未知 图片点阵数据(bitmap data) 由图片大小和颜色定 未知 未知 -
三.BMP文件头
BMP文件头结构体定义如下:
typedef struct tagBITMAPFILEHEADER
{
UINT16 bfType; //2Bytes,必须为"BM",即0x424D 才是Windows位图文件
DWORD bfSize; //4Bytes,整个BMP文件的大小
UINT16 bfReserved1; //2Bytes,保留,为0
UINT16 bfReserved2; //2Bytes,保留,为0
DWORD bfOffBits; //4Bytes,文件起始位置到图像像素数据的字节偏移量
} BITMAPFILEHEADER;
BMP文件头数据表如下:
变量名 地址偏移 大小 作用说明 bfType 0000h 2Bytes 文件标识符,必须为"BM",即0x424D 才是Windows位图文件 ‘BM’:Windows 3.1x, 95, NT,… ‘BA’:OS/2 Bitmap Array ‘CI’:OS/2 Color Icon ‘CP’:OS/2 Color Pointer ‘IC’:OS/2 Icon ‘PT’:OS/2 Pointer 因为OS/2系统并没有被普及开,所以在编程时,你只需判断第一个标识“BM”就行 bfSize 0002h 4Bytes 整个BMP文件的大小(以位B为单位) bfReserved1 0006h 2Bytes 保留,必须设置为0 bfReserved2 0008h 2Bytes 保留,必须设置为0 bfOffBits 000Ah 4Bytes 说明从文件头0000h开始到图像像素数据的字节偏移量(以字节Bytes为单位),以为位图的调色板长度根据位图格式不同而变化,可以用这个偏移量快速从文件中读取图像数据 -
四.BMP信息头
BMP信息头结构体定义如下:
typedef struct _tagBMP_INFOHEADER
{
DWORD biSize; //4Bytes,INFOHEADER结构体大小,存在其他版本I NFOHEADER,用作区分
LONG biWidth; //4Bytes,图像宽度(以像素为单位)
LONG biHeight; //4Bytes,图像高度,+:图像存储顺序为Bottom2Top,-:Top2Bottom
WORD biPlanes; //2Bytes,图像数据平面,BMP存储RGB数据,因此总为1
WORD biBitCount; //2Bytes,图像像素位数
DWORD biCompression; //4Bytes,0:不压缩,1:RLE8,2:RLE4
DWORD biSizeImage; //4Bytes,4字节对齐的图像数据大小
LONG biXPelsPerMeter; //4 Bytes,用象素/米表示的水平分辨率
LONG biYPelsPerMeter; //4 Bytes,用象素/米表示的垂直分辨率
DWORD biClrUsed; //4 Bytes,实际使用的调色板索引数,0:使用所有的调色板索引
DWORD biClrImportant; //4 Bytes,重要的调色板索引数,0:所有的调色板索引都重要
}BMP_INFOHEADER;
BMP信息头数据表如下:
变量名 地址偏移 大小 作用说明 biSize 000Eh 4Bytes BNP信息头即BMP_INFOHEADER结构体所需要的字节数(以字节为单位) biWidth 0012h 4Bytes 说明图像的宽度(以像素为单位) biHeight 0016h 4Bytes 说明图像的高度(以像素为单位)。这个值还有一个用处,指明图像是正向的位图还是倒向的位图,该值是正数说明图像是倒向的即图像存储是由下到上;该值是负数说明图像是倒向的即图像存储是由上到下。大多数BMP位图是倒向的位图,所以此值是正值。 biPlanes 001Ah 2Bytes 为目标设备说明位面数,其值总设置为1 biBitCount 001Ch 2Bytes 说明一个像素点占几位(以比特位/像素位单位),其值可为1,4,8,16,24或32 biCompression 001Eh 4Bytes 说明图像数据的压缩类型,取值范围为: 0 BI_RGB 不压缩(最常用) 1 BI_RLE8 8比特游程编码(BLE),只用于8位位图 2 BI_RLE4 4比特游程编码(BLE),只用于4位位图 3 BI_BITFIELDS比特域(BLE),只用于16/32位位图 4 biSizeImage 0022h 4Bytes 说明图像的大小,以字节为单位。当用BI_RGB格式时,总设置为0 biXPelsPerMeter 0026h 4Bytes 说明水平分辨率,用像素/米表示,有符号整数 biYPelsPerMeter 002Ah 4Bytes 说明垂直分辨率,用像素/米表示,有符号整数 biClrUsed 002Eh 4Bytes 说明位图实际使用的调色板索引数,0:使用所有的调色板索引 biClrImportant 0032h 4Bytes 说明对图像显示有重要影响的颜色索引的数目,如果是0,表示都重要。 -
五.BMP调色板
BMP调色板结构体定义如下:
typedef struct _tagRGBQUAD
{
BYTE rgbBlue; //指定蓝色强度
BYTE rgbGreen; //指定绿色强度
BYTE rgbRed; //指定红色强度
BYTE rgbReserved; //保留,设置为0
} RGBQUAD;
1,4,8位图像才会使用调色板数据,16,24,32位图像不需要调色板数据,即调色板最多只需要256项(索引0 - 255)。
颜色表的大小根据所使用的颜色模式而定:2色图像为8字节;16色图像位64字节;256色图像为1024字节。其中,每4字节表示一种颜色,并以B(蓝色)、G(绿色)、R(红色)、alpha(32位位图的透明度值,一般不需要)。即首先4字节表示颜色号1的颜色,接下来表示颜色号2的颜色,依此类推。
颜色表中RGBQUAD结构数据的个数有biBitCount来确定,当biBitCount=1,4,8时,分别有2,16,256个表项。
当biBitCount=1时,为2色图像,BMP位图中有2个数据结构**RGBQUAD,**一个调色板占用4字节数据,所以2色图像的调色板长度为2*4为8字节。
当biBitCount=4时,为16色图像,BMP位图中有16个数据结构**RGBQUAD,**一个调色板占用4字节数据,所以16像的调色板长度为16*4为64字节。
当biBitCount=8时,为256色图像,BMP位图中有256个数据结构**RGBQUAD,**一个调色板占用4字节数据,所以256色图像的调色板长度为256*4为1024字节。
当biBitCount=16,24或32时,没有颜色表。
-
五.BMP图像数据区
位图数据记录了位图的每一个像素值,记录顺序是在扫描行内是从左到右,扫描行之间是从下到上。位图的一个像素值所占的字节数:
当biBitCount=1时,8个像素占1个字节;
当biBitCount=4时,2个像素占1个字节;
当biBitCount=8时,1个像素占1个字节;
当biBitCount=24时,1个像素占3个字节;
Windows规定一个扫描行所占的字节数必须是4的倍数(即以long为单位),不足的以0填充,
一个扫描行所占的字节数计算方法:
DataSizePerLine= (biWidth* biBitCount+31)/8;
// 一个扫描行所占的字节数
DataSizePerLine= DataSizePerLine/4*4; // 字节数必须是4的倍数
位图数据的大小(不压缩情况下):
DataSize= DataSizePerLine* biHeight;
颜色表接下来位为位图文件的图像数据区,在此部分记录着每点像素对应的颜色号,其记录方式也随颜色模式而定,既2色图像每点占1位(8位为1字节);16色图像每点占4位(半字节);256色图像每点占8位(1字节);真彩色图像每点占24位(3字节)。所以,整个数据区的大小也会随之变化。究其规律而言,可的出如下计算公式:图像数据信息大小=(图像宽度图像高度记录像素的位数)/8。
-
六.调色板
1.我们知道,自然界中的所有颜色都可以由红、绿、蓝(R,G,B)三基色组合而成。而计算机对于像素的处理,用一个字节,将每种基色分为了256种等级,那么红、绿、蓝的不同组合共有256256256=16777216种,如此之多的组合,对于人眼的辨别能力来说,已经相当足够了,这就是我们平时所说的"真彩色".
对每个像素进行了(R,G,B)量化的图像就是位图,其在计算机中对应文件的扩展名一般为.bmp.
然而,对于"真彩色"来说,它的颜色种类实际上已经超出了人眼的识别范围,在很多时候,我们并没有必要将颜色划分得如此之细.而"真彩色"图片存储空间过大,也是这种文件的一种弊端.我们可以来计算一下一个800*600的"真彩色"图片所需要的存储空间的大小:
8006003 = 1440000(字节)= 1.37M(字节)
这是不是和你们平时看到的WINDOWS壁纸一样大小(当然,这里除去了位图文件中文件头以及其他信息头的大小,这些将在后面讲到).如果每个图形文件都花费掉如此大的空间,是很不划算的,于是就出现了"调色板",它的功能在于缓解位图文件存储空间过大的问题.位图文件也就出现了单色,16色,256色,16位,24位真彩色几种格式.
对于一个16色位图文件,它每个象素,只需要4bit,因为调色板提供了这16种等级对应的(R,G,B)值,我们只需要4bit来存储象素在调色板中的索引值,这样,一个800*600的16色位图文件所需要的存储空间为:
8006004/8 = 240000(字节) = 0.22 M(字节)
而调色板所带来的额外开销仅仅为16*4=64字节.存储空间大为减少了!
对于单色,16色,256色3种位图文件,都是以调色板方式进行存储;而对16位及24位真彩色以调色板进行存储是不划算的,它们直接按照R,G,B分量进行存储。
2.位图中的调色板模块
调色板(色数*4字节)
16)28-…:调色板规范.对于调色板中的每个元素,用下述方法来描述RGB的值:
1字节用于蓝色分量
1字节用于绿色分量
1字节用于红色分量
1字节为保留字
调色板针对的是需要调色板的位图,即单色,16色和256色等.对于不以调色板方式存储的位图,则没有此信息.它实际上就是一个数组,用来储存上面所提到的RGB元素,其中元素的个数由14)里提到的颜色索引数来定,当该值为0的时候,则根据色数来定,其大小等于2的n次幂,n是9)中提到的每个像素的位数(实际上,数组大小通常也就是2,16,256…).
-
fread(e, sizeof(char), 120, fp);
fread
函数是 C 语言标准库中用于从文件中读取数据的函数。这个函数每次可以读取多个数据项到内存中的数组。下面是对 fread(e, sizeof(char), 120, fp);
这行代码的详细解释:
fread
: 这是函数的名称,即从文件中读取数据的函数。e
: 这是数据读取的目标缓冲区的指针。在这个例子中,e
应该是一个足够大的字符数组的指针,能够容纳至少120个字符。sizeof(char)
: 这是每个字符元素的大小(以字节为单位)。在 C 语言中,char
类型的大小通常是 1 字节,所以sizeof(char)
指定了每个元素的大小为 1 字节。120
: 这是要读取的元素数量。在这个例子中,fread
函数将尝试从文件中读取 120 个字符。fp
: 这是指向FILE
结构的指针,该结构代表了要读取的文件流。fp
必须指向一个有效的文件流,且该文件以读模式打开。
综合来看,fread(e, sizeof(char), 120, fp);
这行代码的作用是从文件流 fp
中读取 120 个字符,并将它们存储到指针 e
指向的内存位置。如果读取成功,fread
会返回实际读取的元素数量。如果返回值小于 120,这可能意味着已经到达文件末尾,或者读取过程中发生了错误。
需要注意的是,fread
函数不会检查读取操作是否成功。为了确保数据被正确读取,你应该检查 fread
的返回值,并根据需要处理可能出现的错误。例如:
size_t items_read = fread(e, sizeof(char), 120, fp);
if (items_read < 120) {
// 处理读取不足的情况,可能是文件结束或发生错误
}
此外,fread
会根据文件流的当前位置开始读取数据,并且读取操作后,文件流的当前位置指针会自动向前移动指定数量的字节。如果需要在文件中随机访问,可以使用 fseek
函数来移动文件流的位置指针。
将上面代码读出的每一个char型数据拆开成01数据存放到数组l中
int y = 0;
for (int r = 0; r < 120; r++) {
for (int u = 1; u < 9; u++) {
l[y] = (int)(((e[r])) >> (8 - u) & 0x01); //把每一个char型数据拆开成01数据存放到数组l中
y++;
if (y > 960) break;
};
};
这段代码是一个嵌套循环,用于处理从文件中读取的单色位图(BMP)数据。具体来说,这段代码尝试将每个字符(1字节)表示的像素数据转换为二进制形式,并存储在整型数组 l
中。下面是对代码的逐行解释:
for (int r = 0; r < 120; r++) { ... }
这是一个外层循环,变量r
从0遍历到119,代表读取的字符数组e
中的每个元素。假设e
是一个包含120个字符的数组,每个字符代表BMP文件中的一个字节。for (int u = 1; u < 9; u++) { ... }
这是一个内层循环,变量u
从1遍历到8。这个循环的目的是遍历一个字节中的每个位(从最低位到最高位)。l[y] = (int)(((e[r])) >> (8 - u) & 0x01);
这行代码执行以下操作:-
e[r]
: 取出e
数组中的第r
个字节。 -
>> (8 - u)
: 将该字节右移8 - u
位。因为u
从1到8,所以这会从最低位开始逐位移出。 -
& 0x01
: 与操作0x01
(二进制的00000001
),这将确保结果仅包含当前位的状态(0或1)。-
与操作
位运算中的"与"(AND)操作是一种按位进行的逻辑运算。在C语言中,
0x01
是一个十六进制数,它等同于二进制的00000001
。当我们对一个数与0x01
进行"与"运算时,实际上是在检查该数的二进制表示中最右边的位(最低位)是0还是1。下面是
0x01
的二进制表示:0x01 = 00000001 (二进制)
当我们将一个整数与
0x01
进行"与"运算时,只有该整数的最低位(最右边的位)会影响结果:- 如果整数的最低位是1,那么结果就是1。
- 如果整数的最低位是0,那么结果就是0。
例如:
5 & 0x01
的二进制运算是0000000101 & 00000000001
,结果是00000000001
,所以结果是1。6 & 0x01
的二进制运算是0000000110 & 00000000001
,结果是00000000000
,所以结果是0。
在位运算中,"与"运算通常用于屏蔽(masking)或提取特定的位。在上面的代码片段中:
l[y] = (int)(((e[r])) >> (8 - u) & 0x01);
这行代码首先将字节
e[r]
右移(8 - u)
位,然后与0x01
进行"与"运算。这样做的目的是从右移后的字节中提取最低位(即原来的(8 - u)
位),并将其存储到数组l
的相应位置。这是一种常见的方法,用于将一个字符中的位分解成单独的二进制值。
-
-
(int)...
: 将结果强制转换为int
类型,以便存储在l
数组中。
-
y++
这行代码将索引y
增加1,以便在下一次内层循环时,将结果存储到l
数组的下一个位置。if (y > 960) break;
这行代码检查y
是否大于960。如果是,内层循环将提前终止。这个条件用于防止数组越界,假设l
数组的大小至少为960个整数。};
这是内层循环的结束括号。}
这是外层循环的结束括号。
整体来看,这段代码的目的是将120个字节的像素数据(每个字节8位)转换成960个二进制表示的整数值,每个值要么是0(背景色),要么是1(前景色)。这通常用于图像处理或机器学习中的特征提取,其中单色图像的每个像素需要被转换为二进制形式以便于处理。
定义保存每张图像的结构体
struct sample //保存每一个图片样本的结构体
{
double a[30][30];
int number; //样本标签,0~9之间的数字
}
Sample[SAMPLE_NUM * 10];
这段代码定义了一个C语言结构体sample
,用于保存图像样本数据,以及一个该结构体类型的数组Sample
。下面是详细解释:
- 结构体定义
struct sample
:- 这是一个自定义的结构体,用于存储单个图像样本的所有相关信息。
- 二维数组
double a[30][30]
:- 结构体中的
a
是一个30行30列的二维数组,用于存储图像的像素值。这里使用double
类型可能是因为需要存储的图像数据是浮点数形式,例如归一化后的像素强度值。
- 结构体中的
- 标签字段
int number
:number
是一个整数字段,用于存储图像样本的标签。在手写数字识别等分类任务中,标签通常表示图像所代表的类别,例如数字0到9。
- 结构体数组
Sample[SAMPLE_NUM * 10]
:Sample
是一个结构体数组,其大小为SAMPLE_NUM * 10
。这个数组用于存储多个图像样本。SAMPLE_NUM
是一个宏,定义了每个数字类别的样本数量。10
表示数字类别的总数(0到9),因此整个数组可以存储针对10个类别的样本。
这个结构体和数组的组合允许你创建一个图像样本数据库,其中每个样本都包含其像素数据和对应的标签。这对于机器学习中的监督学习任务非常有用,因为你可以将这个数组用作训练数据集,其中每个元素都是一个带有标签的样本图像。
在实际应用中,你需要确保SAMPLE_NUM
已经被定义,并且其值大于0,以避免潜在的数组越界问题。此外,在使用这个结构体数组之前,你可能需要动态地分配内存或者确保它已经被正确初始化。
将数据储存到结构体中
for (int u = 0; u < 30; u++) {
y = 0;
for (int j = 0; j < 32; j++) {
if ((j != 30) && (j != 31)) {
Sample[m * SAMPLE_NUM + i].a[u][y] = l[g];
y++;
}; //去掉windows自动补0的数据,把真正的数据存放的样本结构体中
g++;
}
}
在处理位图(BMP)文件时,特别是当它们以字节为单位存储数据时,每个字节通常包含8个像素的位信息。在单色位图中,每个像素要么是黑色(0),要么是白色(1)。如果位图的宽度不是8的倍数,那么每一行的最后一个字节可能不会完全填满像素数据,剩余的位通常被设置为0(即填充位),以确保数据的对齐。
在这段代码中:
for (int j = 0; j < 32; j++) {
if ((j != 30) && (j != 31)) {
Sample[m * SAMPLE_NUM + i].a[u][y] = l[g];
y++;
} //去掉windows自动补0的数据,把真正的数据存放的样本结构体中
g++;
}
循环遍历了每个字节中的所有位,但跳过了索引为30和31的位。这里有几个可能的原因:
- 对齐填充:如果图像宽度是32像素,实际的像素数据可能只使用了每个字节的前6位,而最后两位是未使用的填充位。在这种情况下,跳过索引为30和31的位可以避免将这些填充位错误地解释为像素数据。
- 文件格式:BMP文件格式可能会在每行的末尾添加额外的字节来进行对齐,以确保数据按边界对齐。这些额外的字节不包含图像数据,因此需要被忽略。
- 数据解释:如果图像数据以字节为单位存储,并且每行的像素数量不是8的倍数,那么每行的最后一个字节的最后几个位可能不会被用作像素数据。跳过这些位可以确保只处理有效的像素信息。
- 特定实现:这可能是针对特定实现或特定版本的BMP文件格式的要求。有些格式可能有特定的填充规则,需要在处理时予以考虑。
在这段代码中,通过跳过索引30和31的位,程序确保只将有效的像素数据(即每行的前28位)复制到样本结构体中。这是一种常见的处理方法,用于确保图像数据的正确解释和使用。
判断图像是以白色为1还是以黑色为1——然后反转像素
int q = Sample[m * SAMPLE_NUM + i].a[0][0];
if (q == 1)
/*由于182字节大小的30*30单色bmp位图采用白色为1,黑色为0的存储方式,而184字节大小的图片则恰好相反,所以这里要检测文件格式,
一般图片右下角不会有字迹,所以检测那里是0还是1即可得知格式,然后调整样本格式为1代表黑色,0白色的格式(图片大小为184字节),这样保证训练结果可靠性*/
{
int n = 0;
int z = 0;
for (int b = 0; b < 30; b++) {
n = 0;
for (;;) {
if (n >= 30) break;
if (Sample[m * SAMPLE_NUM + i].a[z][n] == 0) Sample[m * SAMPLE_NUM + i].a[z][n] = 1;
else if (Sample[m * SAMPLE_NUM + i].a[z][n] == 1) Sample[m * SAMPLE_NUM + i].a[z][n] = 0;
n++;
}
z++;
}
}
这段代码的目的是检测并调整单色位图图像的存储格式,以确保图像的黑色和白色像素的表示是一致的。代码中提到了两种大小的位图文件:182字节和184字节,它们分别采用不同的存储方式。
- 检测图像格式:
int q = Sample[m * SAMPLE_NUM + i].a[0][0];
这行代码读取了图像样本数组Sample
中第m * SAMPLE_NUM + i
个样本的左上角像素的值(第一个字节的第一个位)。if (q == 1)
:
如果左上角像素的值为1,根据注释,这意味着图像采用的存储方式是白色为1,黑色为0。
- 调整图像格式:
- 如果检测到的格式与期望的格式不一致,则需要对图像的每个像素进行翻转,以确保白色为0,黑色为1。
- 接下来的嵌套循环将遍历图像样本中的每个像素,并执行翻转操作。
- 嵌套循环:
- 外层循环
for (int b = 0; b < 30; b++)
:遍历图像的30行。 - 内层循环
for (;;) { ... }
:使用无限循环来遍历每行的30个像素,直到遇到像素索引n
大于或等于30时跳出循环。
- 外层循环
- 像素翻转:
if (Sample[m * SAMPLE_NUM + i].a[z][n] == 0) Sample[m * SAMPLE_NUM + i].a[z][n] = 1;
如果像素值为0(黑色),将其设置为1。else if (Sample[m * SAMPLE_NUM + i].a[z][n] == 1) Sample[m * SAMPLE_NUM + i].a[z][n] = 0;
如果像素值为1(白色),将其设置为0。
- 索引更新:
n++
:内层循环中递增像素索引。z++
:外层循环中递增行索引。
通过这段代码,图像样本中的每个像素值都会被检查并根据需要进行翻转,以确保所有图像样本的格式统一。这样做可以保证后续处理(如机器学习模型的训练)的可靠性,因为模型通常期望输入数据具有一致的格式和表示方式。
需要注意的是,代码中的注释提到了“184字节大小的图片则恰好相反”,这意味着存在两种不同的存储方式,代码通过检测一个像素值来确定整个图像的存储方式,并进行相应的调整。
打标签,清除内存
Sample[m * SAMPLE_NUM + i].number = m; //给样本打标签
free(e);
e = NULL;
free(l);
l = NULL;
这段代码执行了两个主要操作:给样本打标签和释放之前分配的动态内存。
-
给样本打标签:
Sample[m * SAMPLE_NUM + i].number = m;
这行代码为当前处理的样本设置了一个标签。标签存储在
Sample
数组中相应元素的number
字段里。这里,标签被设置为变量m
的值。根据之前的上下文,m
代表当前处理的数字类别(0到9)。这样,每个样本都会根据其代表的数字被赋予一个相应的标签。 -
释放动态内存:
free(e); e = NULL;
这行代码使用
free
函数释放之前用malloc
分配给指针e
的内存。malloc
通常用于分配数组e
用来存储读取的图像数据。释放内存是一个良好的编程习惯,可以避免内存泄漏。释放后,e
被设置为NULL
,表示它不再指向任何内存地址。free(l); l = NULL;
类似地,这些代码行释放了为指针
l
分配的内存,l
通常用于存储图像数据的中间表示,例如转换后的二进制像素值。释放后,l
也被设置为NULL
。
在内存管理中,将指针设置为 NULL
是一个重要的步骤,因为这可以防止悬空指针(dangling pointer)问题,悬空指针是指一个指针指向已经被释放的内存。在 free
之后将指针设置为 NULL
是一个安全的做法,可以避免无意中通过该指针访问已释放的内存。
总的来说,这段代码确保了样本数据被正确标记,并且不再需要的动态分配内存被妥善释放,有助于防止内存泄漏和提高程序的健壮性。
在所有任务正常完成后,函数会返回0.判断返回值是否为0 之后即可开启接下来的操作
读取文件夹数据失败则暂停程序,等待用户按下任意键后推出程序(return 0)
system("pause");
return 0;
下面是 system("pause")
的一些详细说明:
-
system
函数: 这个函数是 C 语言标准库中的一个函数,它属于<cstdlib>
头文件中。system
函数会调用操作系统的命令行解释器来执行传入的字符串命令。 -
“pause” 命令: 这是 Windows 命令行(cmd.exe)中的一个内置命令。当执行
system("pause")
时,程序会显示消息 “Press any key to continue…” 并等待用户按下任意键。 -
用途:
system("pause")
通常用于调试目的,以便在程序的某个特定点暂停执行,检查程序状态或等待某个事件的发生。在批处理脚本或简单的控制台应用程序中,这个命令也可以用来确保用户有足够的时间来查看屏幕上的信息。 -
跨平台兼容性: 需要注意的是,
"pause"
命令是特定于 Windows 系统的。在 UNIX 或 Linux 系统上,没有"pause"
命令。在跨平台的代码中,如果要实现类似的功能,可能需要使用其他方法,如循环等待用户输入。 -
示例:
#include <stdlib.h> int main() { // 程序的其他部分 system("pause"); // 暂停程序,等待用户按键 return 0; }
使用 system("pause")
可以方便地在控制台程序中进行调试和信息展示,但应当谨慎使用,因为它会使程序的执行暂停,直到用户采取行动。在某些情况下,可能需要更精细的控制,比如只暂停程序的特定部分,或者响应特定的用户输入。