前段时间有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));
复制代码
好的,我们已经知道了如何解决这个问题,文章到此结束。
桥豆麻袋,知其然,知其所以然,问题解决了,我们还要知道问题怎么解决的。
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;
复制代码
但图片为灰度图,效果是这样的:
调色板设置为:24
如果需要16M真彩色图的时候,将位置28
规定为24
的时候,3个字节变为一个像素,但是BMP的颜色格式为BGR
,我们直接将图片数据传入的时候,会是这样的:
所以需要我们将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]);
}
复制代码
效果就可以变成这样:
同理,如果是其他的数据格式,我们需要设置不同的调色板,根据调色板的长度,再修改一下headerSize
,其他的就没什么好说的了。
总结
这次需求,在每个环节都要卡很久,还是基础知识不够。从flutter的image.memory支持什么图片格式?到rgb如何转为图片?选用什么样的图片格式?BMP图片的编码结构是怎么样的?为什么出来的颜色不对?BMP调色板怎么设置?gbr转换时候的移位操作等等,都涉及了很多基础知识。但每一次需求也都是在补基础知识,每个需求也会越来越顺利。