【BMP】RGB字节数据流转BMP图片格式

我心飞翔 分类:javascript

前段时间有Flutter相机实时预览的需求,使用的web API,传输的是Motion JPEG数据格式,前些天又需要拓展另一品牌的相机,这个相机传输的是RGB数据格式。

因为Motion JPEG的数据是一帧帧完整的JPEG图,所以我们只需要将数据包装好,放入Image.memory()中就好,而RGB数据是每一个像素点的三原色数据,不能直接显示,所以需要将其转换为合格的图片编码格式。

根据Image.memory()官方文档显示

which can be encoded in any of the following supported image formats: JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP

以下支持被编码的图片格式有:JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP

所以我们可以将其转换为BMP格式进行展示。别问为什么用BMP格式,我只会这个

别废话,先看代码

BMP图片的格式结构可以分为2大部分,图片头数据和图片体数据

1、新建一个class来规定图片头数据

class BMPHeader {
  int w;
  int h;

  late Uint8List _bmp;
  late int _headerSize;

  BMPHeader(this._width, this._height) : assert(_width & 3 == 0) {
    _headerSize = 54;
    int fileLength = _headerSize + w * h * 3; // 文件长度
    _bmp = new Uint8List(fileLength);
    ByteData bd = _bmp.buffer.asByteData();
    bd.setUint8(0, 0x42);
    bd.setUint8(1, 0x4d);
    bd.setUint32(2, fileLength, Endian.little); // 文件长度
    bd.setUint32(10, _headerSize, Endian.little); // 图片数据开始
    bd.setUint32(14, 40, Endian.little); // 信息头长度
    bd.setUint32(18, w, Endian.little); // 图片的宽度
    bd.setUint32(22, -h, Endian.little); // 图片的高度
    bd.setUint32(26, 1, Endian.little); // 目标设备说明位面数
    bd.setUint32(28, 24, Endian.little); // 颜色编码格式
    bd.setUint32(34, _width * _height, Endian.little); // bitmap size
  }

  Uint8List appendBitmap(Uint8List bitmap) {
    int size = _width * _height * 3;
    assert(bitmap.length == size);
    _bmp.setRange(_headerSize, _headerSize + size, bitmap);
    return _bmp;
  }
}
 

2、将RGB数据导入进去,也就是图片体数据,此处规定图片的显示内容

获取的数据中不仅有需要的数据,还要有宽和高,将宽和高传入BMPHeader(w, h),就可以将图片头确定,而BMP格式需要的是GBR数据,所以我们需要再转换一次在传入进去。

int w = rgbData.width;
int h = rgbData.heigh;
Uint8List rgbF = rgbData.frameData;
List<int> gbrf = [];
for(int i = 0; i<rgbF.length/3; i++) {
    gbrf.add(f[i*3+2]);
    gbrf.add(f[i*3+1]);
    gbrf.add(f[i*3]);
}

BMPHeader header = BMPHeader(w, h);
Uint8List bmp = header.appendBitmap(Uint8List.fromList(gbrf));
 

好的,我们已经知道了如何解决这个问题,文章到此结束。

OIP.jpeg

桥豆麻袋,知其然,知其所以然,问题解决了,我们还要知道问题怎么解决的。

BMP格式详解

在网上已经有了非常多的BMP格式结构的说明,《BMP图像数据格式详解》、《不同BMP位图与调色板分析》已经完整的介绍了BMP是如何编码的,而我们只需要搞懂其中一部分就好。

信息头每次setUint传入3个值,分别代表:位置字节序

前2个用来规定每个位置的值,第三个值有兴趣的可以自己去了解下。

1、文件头的长度

通过上面的《详解》可以得知,其实BMP文件分4部分:

  • 文件头:文件的格式、大小等信息;
  • 信息头:图像数据的尺寸、位平面数、压缩方式、颜色索引等信息;
  • 调色板:可选,如使用索引来表示图像,调色板就是索引与其对应的颜色的映射表
  • 位图数据:图像数据

其中文件头长度为14,信息头长度为40,调色板数据是可选的,如果没有调色板,那么加起来刚好是54.

2、高度为什么用负数

在图片头中需要规定图片的宽高,这么不仅规定了宽高,而且规定了图片的存储方向。

bd.setUint32(18, w, Endian.little); // 图片的宽度
bd.setUint32(22, -h, Endian.little); // 图片的高度
 

而在BMP格式的图片渲染中:

  • 第一个点是左下角,第一行是最下行
  • 每行从左到右,一行一行向上扫描

所以我们在数据存储的时候,需要从下往上存,渲染也是反的,负负得正,我们就得到了正确的图像

3、颜色编码格式

规定颜色的编码格式,才能将位图数据正确的解析。如果是1、4、8则需要申明规定调色板,信息头的长度为54 + 调色板的长度,而16、24、32则不需要调色板,信息头的长度为54

bd.setUint32(28, 24, Endian.little);
 

先来看下每个色值的说明:

  • 1:单色图,调色板中含有两种颜色,也就是我们通常说的黑白图片
  • 4:16色图
  • 8:256色图,通常说的灰度图
  • 16:64K图,一般没有调色板,图像数据中每两个字节表示一个像素,5个或6个位表示一个RGB分量
  • 24:16M真彩色图,一般没有调色板,图像数据中每3个字节表示一个像素,每个字节表示一个RGB分量
  • 32:4G真彩色,一般没有调色板,每4个字节表示一个像素,相对24位真彩图而言,加入了一个透明度,即RGBA模式

调色板设置为:8

如果需要256色图的时候,就是灰度图,将位置28规定为8的时候,我们需要写入调色板

for (int rgb = 0; rgb < 256; rgb++) {
  int offset = _headerSize + rgb * 4;
  bd.setUint8(offset + 3, 255); // A
  bd.setUint8(offset + 2, rgb); // R
  bd.setUint8(offset + 1, rgb); // G
  bd.setUint8(offset, rgb); // B
}
 

这里一个每次循环写入了4个长度,共循环256次,所以一共长1024,我们需要文件长度变成这样

int fileLength = _headerSize + w * h * 3 + 1024;
 

但图片为灰度图,效果是这样的:
Screenshot_2021-04-19-18-12300-58-139_hz_camera_exam.jpg

调色板设置为:24

如果需要16M真彩色图的时候,将位置28规定为24的时候,3个字节变为一个像素,但是BMP的颜色格式为BGR,我们直接将图片数据传入的时候,会是这样的:
Screenshot_2021-04-19-17-40-22-598_hz_camera_exam.jpg

所以需要我们将RGB转换为GBR在传入BMP中,也非常简单,就是将数据中每3个字节一组,第一位和第三位对调。

Uint8List rgbF = rgbData.frameData; //原始rgb数据
List<int> gbrf = []; //转换后的gbr数据
for(int i = 0; i<rgbF.length/3; i++) {
    gbrf.add(f[i*3+2]);
    gbrf.add(f[i*3+1]);
    gbrf.add(f[i*3]);
}
 

效果就可以变成这样:

Screenshot_2021-04-19-18-00-58-139_hz_camera_exam.jpg

同理,如果是其他的数据格式,我们需要设置不同的调色板,根据调色板的长度,再修改一下headerSize,其他的就没什么好说的了。

总结

这次需求,在每个环节都要卡很久,还是基础知识不够。从flutter的image.memory支持什么图片格式?到rgb如何转为图片?选用什么样的图片格式?BMP图片的编码结构是怎么样的?为什么出来的颜色不对?BMP调色板怎么设置?gbr转换时候的移位操作等等,都涉及了很多基础知识。但每一次需求也都是在补基础知识,每个需求也会越来越顺利。

回复

我来回复
  • 暂无回复内容