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

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

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

音视频入门基础:像素格式专题(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图片文件原始数据分析》

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

抖音、快手、百度极速版挂机项目分享、看广告收益最多的软件!快手极速版挂机脚本,刷金币脚本、挂机项目、免费分享!

“看小说就能赚钱”、“刷视频就能赚钱”...... 号称动动手指就能赚钱的APP越来越多&#xff0c;比如各种极速版的APP、电子书APP、新闻资讯APP、搜索APP等等。 很多人也都乐此不疲&#xff0c;一天到晚就是“刷刷刷”。 最近一位“阿姨”晒各种“赚钱的APP”收入截图在业内传…

【LeetCode:23. 合并 K 个升序链表 + 链表 + 归并 + 递归】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

正点原子FreeRTOS学习笔记——列表与列表项

目录 一、什么是列表和列表项 1、概念 2、FreeRTOS代码 &#xff08;1&#xff09;列表 &#xff08;2&#xff09;列表项 &#xff08;3&#xff09;迷你列表项 二、列表与列表项初始化 1、列表初始化 2、列表项初始化 三、列表插入与删除列表项 1、原理解释 2、…

Django使用

一、根目录下安装 pip install django 二、创建djiango项目 django-admin startproject 项目名称 三、创建app python manage.py startapp app名称 四、启动 python manage.py runserver 五、编写URL与视图关系&#xff0c;相对路径 1、manage.py&#xff08;见资源绑定…

(十)Python基础练习题一(50道选择题)#Python

本文整理了Python基础知识相关的练习题&#xff0c;共50道&#xff0c;适用于刚入门初级Python想巩固基础的同学。来源&#xff1a;如荷学数据科学题库&#xff08;技术专项-Python一&#xff09;。 1&#xff09; 2&#xff09; 3&#xff09; 4&#xff09; 5&#xff09; 6…

Vue的学习 —— <vue组件>

目录 前言 正文 一、选项式API与组合式API 二、生命周期函数 1、onBeforeMount() 2、onMounted() 3、onBeforeUpdate() 4、onUpdated() 5、onBeforeUnmount() 6、onUnmounted() 三、组件之间的样式冲突 四、父组件向子组件传递数据 1、定义props 2、静态绑定props…

java内容快速回顾+SSM+SpringBoot简要概述

文章目录 java基础知识基本知识列表面对对象堆与栈的关系值修改与引用修改异常&#xff1a;错误异常 SSMspringMVCServletSpringMVC&#xff1a;基于 Servlet的 Spring Web 框架&#xff0c; spring控制反转 IoC(Inversion of Control)面向切面 Aop MybatisJDBCMybatis SpringB…

【启明智显技术分享】工业级HMI芯片--Model系列(Model3C/Model3/Model4)烧录操作指南

前言 「Model系列」芯片是启明智显针对工业、行业以及车载产品市场推出的系列HMI芯片&#xff0c;主要应用于工业自动化、智能终端HMI、车载仪表盘、串口屏、智能中控、智能家居、充电桩显示屏、储能显示屏、工业触摸屏等领域。此系列具有高性能、低成本的特点&#xff0c;支持…

失误删除也能救回,推荐前10款手机数据恢复软件!

在手机的日常使用中&#xff0c;不可避免的会误删数据&#xff0c;这些数据可能包括照片、视频、联系人、短信等重要信息。不过好在市面上有很多优秀的手机数据恢复软件&#xff0c;可以帮助我们从各种情况下恢复丢失的数据。 本文将为您推荐十大手机数据恢复软件&#xff0c;…

【全开源】JAVA同城圈子达人交友系统源码支持微信小程序+微信公众号+H5+APP

同城达人 精准匹配&#xff1a;系统通过用户填写的个人信息和兴趣爱好&#xff0c;运用智能算法进行精准匹配&#xff0c;推荐合适的同城朋友。多种互动方式&#xff1a;提供在线聊天、语音通话、视频交流等多种互动方式&#xff0c;让用户能够随时随地与朋友保持联系。本地生…

外贸营销脚本,自动化营销工具的制作!

在当今全球化的商业环境下&#xff0c;外贸行业面临着日益激烈的竞争&#xff0c;为了提高营销效率、降低成本并增加销售额&#xff0c;许多外贸企业开始寻求自动化营销的解决方案。 本文将深入探讨外贸自动化营销脚本与工具的制作方法&#xff0c;并分享五段实用的源代码&…

2023年数维杯国际大学生数学建模挑战赛C题人工智能生成文本的智能识别与检测解题全过程论文及程序

2023年数维杯国际大学生数学建模挑战赛 C题 人工智能生成文本的智能识别与检测 原题再现&#xff1a; 近年来&#xff0c;随着信息技术的飞速发展&#xff0c;人工智能的各种应用应运而生。典型应用包括机器人导航、语音识别、图像识别、自然语言处理和智能推荐等。其中&…

【class8】人工智能初步(图像识别-----卷积神经网络)

上节回顾 上节课&#xff0c;我们简单了解了图像识别和深度学习的相关知识。 快速回顾一下吧&#xff5e; A图像识别是以图像的主要特征为基础的。B. 图像分辨率决定图像的质量。 C&#xff0e; 像素是图像中的最小单位D. 在图像识别的原理上&#xff0c;计算机和人类在本质…

富唯智能复合机器人:CNC铝块上下料安全新标准

在CNC铝块加工过程中&#xff0c;上下料环节的安全问题一直是企业关注的焦点。富唯智能复合机器人的应用&#xff0c;为这一环节树立了新的安全标准。 传统的上下料方式往往依赖于人工操作&#xff0c;存在着较大的安全隐患。而富唯智能复合机器人采用先进的视觉识别技术和精准…

机器学习中常用的几种距离——欧式、余弦等

目录 一、欧式距离&#xff08;L2距离&#xff09;二、曼哈顿距离&#xff08;L1距离&#xff09;三、汉明距离四、余弦相似度 一、欧式距离&#xff08;L2距离&#xff09; &#xff08;1&#xff09;二维空间的距离公式&#xff08;三维空间的在这个基础上类推&#xff09;&…

【机器学习:IT行业的现在与未来】技术应用与趋势探索

&#x1f525; 个人主页&#xff1a;空白诗 文章目录 &#x1f381; 引言&#x1f300; 当前机器学习技术的深度剖析与实践展示&#x1f3af; 算法与架构的革新轨迹&#x1f4c8; 数据科学与基础设施的融合进化&#x1f512; 安全、隐私与伦理&#xff1a;技术的道德指南针&…

电子商务电商数据采集接口||电子商务市场数据采集方法,你学到了吗?

小刘从某职业院校电子商务专业毕业后&#xff0c;-直在某品牌电商部负责运营工作&#xff0c;近期&#xff0c;同班同学小王邀请小刘加入创业大军&#xff0c;共同开设网店&#xff0c;销售家乡的螃蟹、鲜虾、扇贝等生鲜水产。 运营经验丰富的小刘决定&#xff0c;在创业开始前…

需要无广告、结构化信息的搜索引擎?秘塔AI搜索,你的新选择

工欲善其事&#xff0c;必先利其器。 随着AI技术与各个行业或细分场景的深度融合&#xff0c;日常工作可使用的AI工具呈现出井喷式发展的趋势&#xff0c;AI工具的类别也从最初的AI文本生成、AI绘画工具&#xff0c;逐渐扩展到AI思维导图工具、AI流程图工具、AI生成PPT工具、AI…

数图智能营运管理系统助力企业数字化转型升级

数图智能营运管理系统不仅仅是一个业绩查看工具&#xff0c;它还具备了主动预警机制以及专家级的品类分析逻辑。系统能够协助企业持续优化库存管理&#xff0c;提升品类结构合理性&#xff0c;显著提高运营效率&#xff0c;减少对员工专业技能的依赖&#xff0c;并缩短处理时间…

【Image captioning】In Defense of Grid Features for Visual Question Answering实现流程

In Defense of Grid Features for Visual Question Answering实现流程 网格特征预训练代码 这是该论文的特征预训练代码发布: @InProceedings{jiang2020defense,title={In Defense of Grid Features for Visual Question Answering},author={Jiang, Huaizu and Misra, Ishan…