音视频入门基础:像素格式专题(2)——不通过第三方库将RGB24格式视频转换为BMP格式图片

news2024/11/17 0:00:57

=================================================================

音视频入门基础:像素格式专题系列文章:

音视频入门基础:像素格式专题(1)——RGB简介

音视频入门基础:像素格式专题(2)——不通过第三方库将RGB24格式视频转换为BMP格式图片

=================================================================

一、引言

    在上一节《音视频入门基础:像素格式专题(1)——RGB简介》中,讲述了RGB格式,以及生成RGB24格式视频的方法。本文讲述跟RGB相关的一种图像文件格式:BMP格式,这种格式内部实际上存储的就是RGB数据。本文对RGB像素数据进行封装处理,不用任何第三方开源库仅通过C++代码实现将RGB24格式的视频转换为一张张BMP图片。效果如下:

原视频(RGB24格式的视频,存放RGB24格式的像素数据,总共有387帧):

转换出来的BMP图片(总共转换出387张图片,与原视频的总帧数一致):

二、BMP格式简介

BMP取自位图Bitmap的缩写,也称为DIB(与设备无关的位图),是一种独立于显示器的位图数字图像文件格式。常见于微软视窗和OS/2操作系统,Windows GDI API内部使用的DIB数据结构与 BMP 文件格式几乎相同。

BMP文件通常是不压缩的,所以它们通常比同一幅图像的压缩图像文件格式要大很多。例如,一张800×600分辨率的24位的BMP格式图片几乎占据1.4MB空间。因此它们通常不适合在因特网或者其他低速或者有容量限制的介质上进行传输。

由于BMP格式通常不压缩,图片体积大,因此生活中我们观看/存贮图片一般使用PNG这种无损压缩格式,或者JPEG这种有损压缩格式。但某些情况下BMP格式比需要压缩的位图格式更有优势:比如速度需求大于储存空间需求的场合,或者系统的算力比储存空间更重要的场合。PNG和JPEG保存和读取时要经过压缩和解压,但BMP没有经过压缩读写速度会更快,并且BMP的空间消耗更稳定。所以需要用空间换时间的场合可以考虑使用BMP。

不包含ColorTable(调色板)的情况下整张BMP图片由Header(位图文件头,总共14字节) + InfoHeader(位图信息头,总共40字节) + Raster Data(RGB像素数据,大小等于位图宽度*位图高度*每个像素所占字节数)组成,如下图所示。其中位图文件头 + 位图信息头 等于整个BMP头,总共占54字节(14+40=54字节)。

所以BMP图片不包含调色板的情况下,比如24位,32位位图,则整张图片的近似字节数可以用下面的公式计算:

BMP文件大小

其中54为整个BMP头的大小,width为位图宽度,height为位图高度(以像素为单位),n为每个像素所占位数,n除以8等于每个像素所占字节数。

因此假如一张BMP图片的分辨率为1280*720,每个像素存贮的位数为24位,则不包含调色板的情况下,该BMP图片的大小约等于 54  + (1280*720*24/8) = 2764854字节。

三、C++代码实现将RGB24格式的视频转换为一张张BMP图片

将裸RGB24文件转换为BMP图片,简单的来讲只要将每个RGB24视频帧封装上BMP header就可以了。首先根据《音视频入门基础:像素格式专题(1)——RGB简介》中的FFmpeg命令,生成像素格式为rgb24的文件:“视频素材_天空中的云_1280x720_rgb24.rgb”。该视频的分辨率为1280x720

ffmpeg -i 视频素材_天空中的云.mp4 -pix_fmt rgb24 视频素材_天空中的云_1280x720_rgb24.rgb

新建Visual Studio(我用的是vs2019) 的C++控制台程序,在main.cpp中输入如下代码:

#include <iostream>
#include <fstream>
#include <vector>
#include <string>

using namespace std;


class CBmpOperation              //封装了对BMP图片进行操作的类
{
/*位图文件头,该结构体12字节,加上“BM”后总共14字节。这部分数据块位于文件开头,用于进行文件的识别。典型的应用程序会
首先普通读取这部分数据以确保的确是位图文件并且没有损坏。所有的整数值都以小端序存放(即最低有效位前置)。*/
struct STBmpHead                 
{
	uint32_t m_nFileSize;        //整个BMP文件的大小,单位为字节
	uint32_t m_nReserved;        //保留;实际值因创建程序而异
	uint32_t m_nDataOffset;      //位图数据(像素数组)的地址偏移,即位图数据的存贮起始地址
};

struct STInfoHead                 //位图信息头,总共40字节
{
	uint32_t  m_nSize;            //DIB header大小(该头结构的大小,40字节)
	uint32_t  m_nWidth;           //位图宽度,单位为像素
	uint32_t  m_nHeight;          //位图高度,单位为像素
	uint16_t  m_nPlanes;          //色彩平面数;必须为1
	uint16_t  m_nBitCount;        //每个像素所占位数,即图像的色深。典型值为1、4、8、16、24和32
	uint32_t  m_nCompression;     //所使用的压缩方法。0表示不压缩
	uint32_t  m_nImageSize;       //图像大小。指原始位图数据的大小。与文件大小不是同一个概念
	uint32_t  m_nXpixelsPerM;     //图像的横向分辨率,单位为像素每米
	uint32_t  m_nYpixelsPerM;     //图像的纵向分辨率,单位为像素每米
	uint32_t  m_nColorsUsed;      //调色板的颜色数,为0时表示颜色数为默认的2色深个
	uint32_t  m_nColorsImportant; //重要颜色数,为0时表示所有颜色都是重要的;通常不使用本项
};

public:
/**
 * 将裸rgb24文件转为BMP图片
 * @param rgb24path    裸rgb24文件的路径
 * @param width        rgb24文件的宽度(单位为像素)
 * @param height       rgb24文件的高度(单位为像素)
 * @param url_out      rgb文件的视频总帧数
 * @param strBmpDir    生成的BMP图片的存贮目录
 * @return             成功返回0,失败返回负值
 */
	int simplest_rgb24_to_bmp(const string &strRgb24path, int width, int height, int totalNum, const string &strBmpDir) {

		ifstream ifsRgb24;
		ifsRgb24.open(strRgb24path, ios::binary | ios::in);
		if (!ifsRgb24.is_open())
		{
			cout << "Error: Cannot open input RGB24 file: " << strRgb24path << endl;
			ifsRgb24.close();
			return -1;
		}

		for (int num = 0; num < totalNum; num++)
		{
			int i = 0, j = 0;
			string strBmppath = strBmpDir + "/output_" + std::to_string(num) + ".bmp";
			uint8_t arrSignature[2] = {'B', 'M'};     //用于标识BMP和DIB文件,一般为0x42 0x4D,即ASCII的BM
			STBmpHead stBMPHeader = { 0 };
			STInfoHead  stBMPInfoHeader = { 0 };
			int nHeaderSize = sizeof(arrSignature) + sizeof(STBmpHead) + sizeof(STInfoHead);  //总共54字节

			ofstream ofsBmp;
			ofsBmp.open(strBmppath, ios::binary | ios::out);
			if (!ofsBmp.is_open())
			{
				cout << "Error: Cannot open output BMP file: " << strBmppath << endl;
				ofsBmp.close();
			}
			
			vector<char> vecBuf(width * height * 3);
			ifsRgb24.read(&vecBuf[0], vecBuf.size());

			stBMPHeader.m_nFileSize = 3 * width * height + nHeaderSize;
			stBMPHeader.m_nReserved = 0;
			stBMPHeader.m_nDataOffset = nHeaderSize;

			stBMPInfoHeader.m_nSize = sizeof(STInfoHead);
			stBMPInfoHeader.m_nWidth = width;
//BMP storage pixel data in opposite direction of Y-axis (from bottom to top).
			stBMPInfoHeader.m_nHeight = -height;
			stBMPInfoHeader.m_nPlanes = 1;
			stBMPInfoHeader.m_nBitCount = 24;
			stBMPInfoHeader.m_nImageSize = 3 * width * height;
			stBMPInfoHeader.m_nXpixelsPerM = 0;
			stBMPInfoHeader.m_nYpixelsPerM = 0;
			stBMPInfoHeader.m_nColorsUsed = 0;
			stBMPInfoHeader.m_nColorsImportant = 0;

			ofsBmp.write((const char*)arrSignature, sizeof(arrSignature));
			ofsBmp.write((const char*)&stBMPHeader, sizeof(stBMPHeader));
			ofsBmp.write((const char*)&stBMPInfoHeader, sizeof(stBMPInfoHeader));

//BMP save R1|G1|B1,R2|G2|B2 as B1|G1|R1,B2|G2|R2
//It saves pixel data in Little Endian
//So we change 'R' and 'B'		
			for (j = 0; j < height; j++) {
				for (i = 0; i < width; i++) {
					char temp = vecBuf[(j * width + i) * 3 + 2];
					vecBuf[(j * width + i) * 3 + 2] = vecBuf[(j * width + i) * 3 + 0];
					vecBuf[(j * width + i) * 3 + 0] = temp;
				}
			}
			ofsBmp.write(&vecBuf[0], vecBuf.size());
			ofsBmp.close();
			cout << "Finish generate " << strBmppath << endl;
		}
		ifsRgb24.close();

		return 0;
	}
};



int main()
{
	CBmpOperation bmpOperation;
	bmpOperation.simplest_rgb24_to_bmp("视频素材_天空中的云_1280x720_rgb24.rgb", 1280, 720, 387, "Pic");
}

将视频素材_天空中的云_1280x720_rgb24.rgb放到vs的工程目录下,然后工程目录下新建Pic目录。

编译,运行程序,在Pic目录下即会生成转换出来的BMP格式图片

通过“属性”可以看到生成的每一张BMP图片的大小为2.63 MB (2,764,854 字节) = 54  + (1280*720*24/8)字节,说明上面计算BMP图片大小的公式是正确的。

下面讲解代码实现。

四、代码解析

首先定义位图文件头的结构体STBmpHead,该结构体占12字节(3 * 4 = 12字节)

/*位图文件头,该结构体12字节,加上“BM”后总共14字节。这部分数据块位于文件开头,用于进行文件的识别。典型的应用程序会
首先普通读取这部分数据以确保的确是位图文件并且没有损坏。所有的整数值都以小端序存放(即最低有效位前置)。*/
struct STBmpHead                 
{
	uint32_t m_nFileSize;        //整个BMP文件的大小,单位为字节
	uint32_t m_nReserved;        //保留;实际值因创建程序而异
	uint32_t m_nDataOffset;      //位图数据(像素数组)的地址偏移,即位图数据的存贮起始地址
};

注意由于C/C++里面结构体对齐的问题,不能定义成以下这种形式:

struct STBmpHead                 //位图文件头,总共14字节
{
	uint8_t m_arrSignature[2];   //用于标识BMP和DIB文件,一般为0x42 0x4D,即ASCII的BM
	uint32_t imageSize;          //整个BMP图片的大小,单位为字节
	uint32_t blank;              //保留数据
	uint32_t startPosition;      //图片像素的存贮位置,即图片像素是存贮在第几个字节的。
};

因为如果结构体里面有uint8_t m_arrSignature[2],由于C/C++里面结构体对齐的问题,整个STBmpHead不是占14字节,而是占16字节。

然后定义位图信息头的结构体STInfoHead,该结构体占40字节

struct STInfoHead                 //位图信息头,总共40字节
{
	uint32_t  m_nSize;            //DIB header大小(该头结构的大小,40字节)
	uint32_t  m_nWidth;           //位图宽度,单位为像素
	uint32_t  m_nHeight;          //位图高度,单位为像素
	uint16_t  m_nPlanes;          //色彩平面数;必须为1
	uint16_t  m_nBitCount;        //每个像素所占位数,即图像的色深。典型值为1、4、8、16、24和32
	uint32_t  m_nCompression;     //所使用的压缩方法。0表示不压缩
	uint32_t  m_nImageSize;       //图像大小。指原始位图数据的大小。与文件大小不是同一个概念
	uint32_t  m_nXpixelsPerM;     //图像的横向分辨率,单位为像素每米
	uint32_t  m_nYpixelsPerM;     //图像的纵向分辨率,单位为像素每米
	uint32_t  m_nColorsUsed;      //调色板的颜色数,为0时表示颜色数为默认的2色深个
	uint32_t  m_nColorsImportant; //重要颜色数,为0时表示所有颜色都是重要的;通常不使用本项
};

函数simplest_rgb24_to_bmp中,通过

ifsRgb24.read(&vecBuf[0], vecBuf.size());

 从rgb24视频文件中读取一帧图片的rgb数据,读取的数据量为位图宽度 * 位图高度 * 3字节(RGB24每个像素占3字节),存贮到vecBuf中。这里使用vector<char>作为输入缓冲区,具有不用手动调用delete函数释放内存,避免内存泄漏的优点,具体可以参考:《使用vector<char>作为输入缓冲区》

封装位图文件头:

uint8_t arrSignature[2] = {'B', 'M'};     //用于标识BMP和DIB文件,一般为0x42 0x4D,即ASCII的BM
........
........
........
stBMPHeader.m_nFileSize = 3 * width * height + nHeaderSize;
stBMPHeader.m_nReserved = 0;
stBMPHeader.m_nDataOffset = nHeaderSize;

封装位图信息头:

stBMPInfoHeader.m_nSize = sizeof(STInfoHead);
stBMPInfoHeader.m_nWidth = width;
//BMP storage pixel data in opposite direction of Y-axis (from bottom to top).
stBMPInfoHeader.m_nHeight = -height;
stBMPInfoHeader.m_nPlanes = 1;
stBMPInfoHeader.m_nBitCount = 24;
stBMPInfoHeader.m_nImageSize = 3 * width * height;
stBMPInfoHeader.m_nXpixelsPerM = 0;
stBMPInfoHeader.m_nYpixelsPerM = 0;
stBMPInfoHeader.m_nColorsUsed = 0;
stBMPInfoHeader.m_nColorsImportant = 0;

将位图文件图和位图信息头写入进BMP图片中

ofsBmp.write((const char*)arrSignature, sizeof(arrSignature));
ofsBmp.write((const char*)&stBMPHeader, sizeof(stBMPHeader));
ofsBmp.write((const char*)&stBMPInfoHeader, sizeof(stBMPInfoHeader));

BMP采用的是小端(Little Endian)存储方式,像素的排布为BGR,而不是RGB,所以需要将“R”和“B”顺序作一个调换再进行存储。

//BMP save R1|G1|B1,R2|G2|B2 as B1|G1|R1,B2|G2|R2
//It saves pixel data in Little Endian
//So we change 'R' and 'B'		
for (j = 0; j < height; j++) {
    for (i = 0; i < width; i++) {
	    char temp = vecBuf[(j * width + i) * 3 + 2];
		vecBuf[(j * width + i) * 3 + 2] = vecBuf[(j * width + i) * 3 + 0];
		vecBuf[(j * width + i) * 3 + 0] = temp;
    }
}

以上是将RGB24视频中的一帧视频画面转为一张BMP图片的逻辑。由于视频中有多帧视频画面,所以通过for循环:

for (int num = 0; num < totalNum; num++)

将视频中的所有视频帧转为BMP图片。

五、案例:通过分析BMP  header判断BMP图片显示不出来的原因

BMP图片正常的情况下,在Windows系统中我们是可以预览其缩略图的,如下所示:

但某些情况下,比如BMP图片被破坏时,我们会发现其无法被正常预览:

用WPS等工具打开被损坏的BMP图片,也会发现无法正常显示:

这往往是因为BMP图片的 header出现了问题,导致无法读取。这个时候我们可以根据《win10 以 十六进制 形式(方式) 查看文件 内容》中的方法,使用Format-Hex工具,以16进制方式,查看该BMP图片的header:

可以看到它的头0~1字节为42、4d,也就是B、M字符,这个是正确的。但是第2到5字节为 0x30380000(小端模式),BMP图片的第2到5字节为整个BMP图片的大小,0x30380000换算成10进制为808976384字节,也即是771M byte。

在Windows操作系统中,通过“属性”看到该BMP图片大小为2.63 MB (2,764,856 字节),大小跟上面的808976384字节对不上,所以我们可以判断是该BMP图片header中的第2到5字节出错了。

如果该BMP图片的header是正确的,那用16进制查看到的第2到第5个字节应该是0x002A3036,换算成10进制为2764854字节:

六、参考文章

《BMP-维基百科》

《Structure of BMP file》

《BMP图像文件完全解析》

《BMP图片文件原始数据分析》

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1665460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32-07-STM32_外部中断

文章目录 STM32 中断系统1. 中断2. NVIC3. EXTI4. AFIO5. 中断配置步骤6. 外部中断代码 STM32 中断系统 1. 中断 目的&#xff1a;中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。中断过程&#xff1a;当CPU正在处理某事件的时候外界发生了紧急事件请求 &#…

ESP-01S刷固件ESP8266_NonOS_AT_Bin_V1.7.5_1 笔记240510

ESP-01S刷固件ESP8266_NonOS_AT_Bin_V1.7.5_1 笔记240510 固件下载地址 ESP-AT固件页面: https://www.espressif.com.cn/zh-hans/products/sdks/esp-at/resource 直接下载ESP8266 NonOS AT Bin V1.7.5.zip: https://www.espressif.com.cn/sites/default/files/ap/ESP8266_No…

32.768kHz晶振的时间精度问题及其解决方法

32.768kHz晶振因其稳定性高、功耗低&#xff0c;被广泛应用于实时时钟(RTC)、计时电路及低功耗电子产品中。然而&#xff0c;在某些情况下&#xff0c;这些晶振可能出现时间偏差&#xff0c;影响设备正常工作。以下是可能导致32.768kHz晶振时间误差的原因及相应的解决策略。 温…

Snort规则编写

1&#xff09;TCP NULL端口扫描的特征&#xff1a; 扫描者发送一个TCP SYN包到目标主机的某个端口&#xff0c;但不设置任何TCP标志位&#xff08;即flag为NULL&#xff09;。 如果端口关闭&#xff0c;目标主机会回应一个RST&#xff08;复位&#xff09;和ACK&#xff08;确认…

数据结构之图——探索图论的奥秘

前言 在这篇文章中&#xff0c;我们一起来看看我们生活中都会用到&#xff0c;但却不那么熟悉的数据结构——图&#xff08;英语&#xff1a;graph&#xff09;。我们看下百科定义&#xff1a; 在计算机科学中&#xff0c;图&#xff08;英语&#xff1a;graph&#xff09;是一…

py黑帽子学习笔记_网络编程工具

tcp客户端 socket.AF_INET表示使用标准IPV4地址和主机名 SOCK_STREAM表示这是一个TCP客户端 udp客户端 udp无需连接&#xff0c;因此不需要client.connect这种代码 socket.SOCK_DGRAM是udp的 tcp服务端 server.listen(5)表示设置最大连接数为5 发现kill server后端口仍占用…

大模型微调之 在亚马逊AWS上实战LlaMA案例(九)

大模型微调之 在亚马逊AWS上实战LlaMA案例&#xff08;九&#xff09; 代码阅读 src/llama_recipes/inference/prompt_format_utils.py 这段代码是一个Python模块&#xff0c;它定义了几个类和模板&#xff0c;用于生成安全评估的提示文本。以下是对每一行代码的注释和提示词…

Datax数据采集

一、Datax介绍 官网&#xff1a; DataX/introduction.md at master alibaba/DataX GitHub DataX 是阿里云 DataWorks数据集成 的开源版本&#xff0c;在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。 DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、…

MySQL innodb_buffer_pool_size 相关常用语句

对于MySQL速度慢的问题&#xff0c;除了优化 SQL 以外&#xff0c;应该必须优先想到的即使 MySQL 数据库的 innodb_buffer_pool_size 配置问题。 一般来说&#xff0c;innodb_buffer_pool_size 的默认大小都是很小的&#xff0c;尤其是 win 下其默认大小更是只有离谱的 8M。Li…

2024 年最新本地、云服务器安装部署 miniconda 环境详细教程(更新中)

Anaconda 概述 Anaconda 是专门为了方便使用 Python 进行数据科学研究而建立的一组软件包&#xff0c;涵盖了数据科学领域常见的 Python 库&#xff0c;并且自带了专门用来解决软件环境依赖问题的 conda 包管理系统。主要是提供了包管理与环境管理的功能&#xff0c;可以很方便…

土壤墒情自动监测站—墒情异常数据报警提示

TH-TS600土壤墒情自动监测站通常配备有预警提示功能&#xff0c;用于在墒情出现异常情况时及时向用户发出警告。这一功能对于农业生产至关重要&#xff0c;因为它可以帮助农民或农田管理者及时发现土壤墒情的变化&#xff0c;并采取相应的措施来确保作物健康生长。 土壤墒情自动…

Excel实用技巧持续学习

1、Excel高效设置图标格式&#xff1a; 2、饼图可以统一设置数据标签在图外面&#xff01;&#xff01; 环形图不可以&#xff0c;但是可以中间手动加上白色圆形&#xff0c;将饼图变为圆环。 可以设置标签的文本显示&#xff1a; 3、饼图和环形图最好进行排序&#xff01;显得…

睿尔曼机械臂ROS控制

下载git工程 git clone https://github.com/RealManRobot/rm_robot.git安装配置 catkin build rm_msgs source devel/setup.bash catkin build source setup.bash这里注意&#xff0c;如果采用setup.sh多半不会成功&#xff0c;必须要source setup.bash文件&#xff0c;ros才…

云渲染动画300帧需要多久呢?瑞云渲染为你揭秘

在动画制作过程中&#xff0c;渲染的速度非常关键。对于一个项目需要渲染的300帧来说&#xff0c;由于硬件的限制&#xff0c;许多公司的设备可能无法快速完成这项任务。此时&#xff0c;借助云渲染服务的强大计算能力&#xff0c;可以显著减少完成时间&#xff0c;从而提速整个…

使用Python和akshare完成个股信息统计和实时记录

标题:使用Python和akshare完成个股信息统计和实时记录 在金融投资领域,实时获取和记录个股信息对于投资者至关重要。Python 作为一种强大的编程语言,在金融数据分析和处理方面有着广泛的应用。akshare 是一个专门用于获取金融数据的 Python 库,提供了丰富的金融数据接口,可…

抽空学学go

2024年5月9日11:14:24 学习go 看课8小时转职Golang工程师(如果你想低成本学习Go语言)_哔哩哔哩_bilibili 文档[8小时转职Golang工程师 (yuque.com)]( 1.安装go 2024年5月9日11:27:16 2.安装 vscode go配置环境 vs code配置go开发环境 (zhihu.com) vscode里面配置代理&…

Rust使用HashSet对Vec类型的元素进行去重

在Rust语言中&#xff0c;对Vec类型的元素进行去重&#xff0c;一种常见的方法是使用一个HashSet来帮助我们快速检查元素是否已经存在。以下是使用HashSet对Vec进行去重的示例代码&#xff1a; use std::collections::HashSet;fn main() {let vec_numbers vec![1, 2, 2, 3, 4…

大数据比赛-环境搭建(一)

1、安装VMware Workstation 链接&#xff1a;https://pan.baidu.com/s/1IvSFzpnQFl3svWyCGRtEmg 提取码&#xff1a;ukpo 内有安装包及破解方式&#xff0c;安装教程。 2、下载Ubuntu系统 阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区 (aliyun.com) 点击下载&#xff…

The Sandbox 在利雅得的首次教育活动

2024年4月22日&#xff0c;The Sandbox 与沙特阿拉伯利雅得的 KACST&#xff08;阿卜杜勒阿齐兹国王科技城&#xff09;合作&#xff0c;举办了首次创作者研讨会。此次活动标志着沙特在推动生态系统增长和扩展方面的重要一步。 The Sandbox 的核心使命是通过无编程工具赋能下一…

在做题中学习(54):点名

LCR 173. 点名 - 力扣&#xff08;LeetCode&#xff09; 此题有不同的几种解法&#xff1a; 解法一&#xff1a;暴力枚举 O(n); 解法二&#xff1a;哈希表 把原数组丢入哈希表&#xff0c;遍历哈希表&#xff0c;看看哪个数值为0即可。 O(n)空间O(n)时间 解法三&…