图像显示原理(一)

平时工作中我们会遇到各种各样的图片,常规的图片分为两种格式,即位图和矢量图。位图就是根据像素进行展示,一个像素点中包含了颜色,明暗度和透明度等信息。我们常见的位图格式有 BMP, PNG,JPG,GIF 等。

一 BMP图片

最原始的图像格式,完全将像素元素转换成数据,以下面的图片为例。由于 BMP 格式完全存储了每一个像素的所有数据,通常它占用较多的内存,我们可以通过 图片格式转换器 将一副普通图片转换成 BMP 格式图片。

一幅图片常见的原始数据包括尺寸,分辨率(dpi,lpi,ppi,PPD 等),大小,色彩空间(RGB,CMY,LAB,YUV,HSI 等等)。

在 BMP 格式文件中,前 14 个字节主要是描述文件信息结构体

字段地址段(字节)描述
file_type0 ~ 2文件标识,表明文件格式
file_size2 ~ 6文件大小
reserved16 ~ 8保留字 1
reserved28 ~ 10保留字 2
offset_bits10~14偏移量

然后 40 个字节主要是图像显示信息

字段地址段(字节)描述
bitmap_info_size14 ~ 18位图信息的大小
bitmap_width18 ~ 22位图宽度
bitmap_height22 ~ 26位图高度
planes26 ~ 28位图的位面数
image_depth28~30位图的图像深度,表示位图数据中,几个二进制位表示一个像素点
compression30~34位图压缩方式
image_size34~38位图的数据大小
x_pels_permeter38 ~ 42指定位图目标设备的水平打印分辨率
y_pels_permeter42 ~ 46指定位图目标设备的垂直打印分辨率
color_used46 ~ 50位图实际使用调色板的颜色数量
color_important50 ~ 54重要的颜色数量

1.1 图像数据解析

我们可以通过 node 调用 fs 模块获取文件原始数据:

const originData = fs.readFileSync('../source/koubei.bmp');

默认 fs 通过 8 进制的形式进行内容输出,为了方便我们查看,将数据转换为 16 进制显示。

const data = origin.toString('hex');

截取部分图像数据

424d46881200000000003600000028000000d6010000860200000100200000000000000000002516000025160000000000000000000086d03aff86d03aff86d13aff86d13aff85d03aff85d03aff86d13aff86d13aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff85d03aff84d03aff84d03aff85d03aff85d03aff84d03aff84d03aff85d03aff85d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff84d039ff84d039ff84d03aff84d03aff84d03aff84d03aff84d039ff84d039ff84d03aff84d03aff84d039ff84d039ff84d039ff84d039ff84d039ff84d039ff84d03aff84d03aff84d03aff84d03aff84d03aff84d03aff82d036ff82d036ff80cf31ff80cf31ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80cf31ff80cf31ff81d032ff81d032ff80cf31ff80cf31ff80cf31ff80cf31ff80d032ff80d032ff80cf31ff80cf31ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff81d032ff81d032ff80d032ff80d032ff81d032ff81d032ff80d032ff80d032ff80d032ff80d032ff80d032ff80d

注意在 BMP 文件中数据存储方式为小端存储模式,我们习惯于按大端存储来读取数据,例如上面这幅图描述文件大小的字符串为"46881200",实际大小为 0X00128846。这里实现一个简易字符串转换函数方便后面使用

// 将小端存储的字符串转换成大端存储
const transformBit = (str) => {
	let string = '';
	for (var i = 0; i < str.length; i++) {
		if (i % 2 == 0) {
			string = string + (str[str.length - 2 - i] + str[str.length - 1 - i]);
		}
	}
	return string;
};

通过 transformBit 函数计算文件大小。由于 MAC 硬盘中采用 1000 进位,而 Windows 采用 1024 进位,所以这个文件在 MAC 上显示 1.2M,而 Windows 上则只有 1.158M。

图像原始数据 54 位以后所有数据都是 BMP 图像的显示数据了,以上面的图片为例,我们获取原始数据的 compression 为 0,表示无压缩。 image_depth 数据得到 0X002000 , 换算下来是 8 位表示一个像素点。在这个像素点中,通常是按照颜色分量 R,G,B 和一位保留字来存储的。

1.2 图像显示

在继续获取到其他头部信息后,我们使用 canvas 来实现一个简易的 BMP 图片浏览器。

        // canvas 描点
        let draw = (x, y, color) => {
          ctx.beginPath();
          ctx.fillStyle = '#' + color;
          ctx.rect(x, y, x + 1, y + 1);
          ctx.fill();
          ctx.closePath();
        };

        // 图像转换成二维数组
        let imgArr = [[]];
        for (let i = 0; i < imgData.length; i+=8) {
            if (x >= width) {
              x = 0;
              y++;
              imgArr[y] = [];
            }
            // 拼装数据
            let color = imgData.substring(i, i + 6);
            color = transformBit(color);
            imgArr[y][x] = color;
            x++;
        }
        // 正常显示图片
        for (let j = 0; j < y; j++) {
          for (let i = 0; i < x; i++) {
            let color = imgArr[j][i];
            draw(i, j, color);
          }
        }

1.3 图像处理

灰度处理
灰度计算通常有平均值法和加权平均值法,加权平均值法是重新分配三种颜色的权重,比例为 R(30%) G(59%) B(11%),对于人体视觉最为友好。这里示例直接用平均值法实现。

  // 转换成黑白色
  const getAverage = (color) => {
    let R = parseInt(color.slice(0, 2), 16);
    let G = parseInt(color.slice(2, 4), 16);
    let B = parseInt(color.slice(4, 6), 16);
    let average = parseInt((R + G + B) / 3);
    average = average > 15 ? average.toString(16) : '0' + average.toString(16);
    return average + average + average;
  };
  ...
  color = getAverage(color);

浮雕效果处理
浮雕效果实现有多种算法,实现效果也不一样。这里使用斜角相邻点差值计算的方式:

  for (let j = 1; j < y - 1; j++) {
    for (let i = 1; i < x - 1; i++) {
      const leftTop = imgArr[j - 1][i - 1];
      const rightBottom = imgArr[j + 1][i + 1];

      const R =
        parseInt(leftTop.slice(0, 2), 16) - parseInt(rightBottom.slice(0, 2), 16) + 128;
      const G =
        parseInt(leftTop.slice(2, 4), 16) - parseInt(rightBottom.slice(2, 4), 16) + 128;
      const B =
        parseInt(leftTop.slice(4, 6), 16) - parseInt(rightBottom.slice(4, 6), 16) + 128;
      let average = parseInt((R + G + B) / 3);
      if (average < 0) average = 0;
      if (average > 255) average = 255;
      average = average > 15 ? average.toString(16) : '0' + average.toString(16);
      let color = average + average + average;
      draw(i, j, color);
    }
  }

要实现旋转,底片,缩放,通道修改等原理都比较简单,这里不再过多罗列。

二 base64 图片

当我们开发中遇到比较小的图片时,一般 10K 以下,可以将图片直接转换成 base64 格式数据放入 src 解析。
在 Mac 下,我们可以通过 openssl 工具来进行 base64 的编解码

openssl base64 -d -in <infile> -out <outfile> //解码
openssl base64 -in <infile> -out <outfile> //编码

2.1 图像数据解析

被 base64 编码的图片数据大致这样

data:image/bmp;base64,Qk1GiBIAAAAAADYAAAAoAAAA1gEAAIYCAAABACAAAAAAAAAAAAAlFgAAJRYAAAAAAAAAAAAAhtA6/4bQOv+G0Tr/htE6/4XQOv+F0Dr/htE6/4bROv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+F0Dr/hdA6/4XQOv+E0Dr/hNA6/4XQOv+F0Dr/hNA6/4TQOv+F0Dr/hdA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dn/hNA5/4TQOv+E0Dr/hNA6/4TQOv+E0Dn/hNA5/4TQOv+E0Dr/hNA5/4TQOf+E0Dn/hNA5/4TQOf+E0Dn/hNA6/4TQOv+E0Dr/hNA6/4TQOv+E0Dr/gtA2/4LQNv+AzzH/gM8x/4DQMv+A0DL/gNAy/4DQMv+B0DL/gdAy/4DQMv+A0DL/gdAy/4HQMv+B0DL/gdAy/4DQMv+A0DL/gdAy/4HQMv+A0DL/gNAy/4DQMv+A0DL/gdAy/

前面 data 指明数据格式,base64 表示数据编码格式。其余部分为图片原始数据,由于 http 传输内容必须为字符,所以数据传输时采用 ASCII 中"A-Z、a-z、0-9、+、/" 这 64 个可打印字符进行编解码。

索引对应字符索引对应字符索引对应字符索引对应字符
0A17R34i51z
1B18S35j520
2C19T36k531
3D20U37l542
4E21V38m553
5F22W39n564
6G23X40o575
7H24Y41p586
8I25Z42q597
9J26a43r608
10K27b44s619
11L28c45t62+
12M29d46u63/
13N30e47v
14O31f48w
15P32g49x
16Q33h50y

由于 64 = 2^6 ,最多需要 6 位二进制表示一个字符,在 ASCII 码中 8 位表示一个字符。为了节省字符空间,通常用 4 位 base64 编码来表示 3 位 ASCII 字符。 在内容结尾处可能存在凑不够 3 位 ASCII 字符的情况,base64 编码时使用 = 进行占位。

2.2 图像编解码

base64 编码:

const encode = (str) => {
	var t = "", result = "";
	var n, r, i, s, o, u, a;
	var f = 0;
	while (f < str.length) {
		n = '0X' + str.slice(f, f += 2);
		r = '0X' + str.slice(f, f += 2);
		i = '0X' + str.slice(f, f += 2);
		s = n >> 2;
		o = (n & 3) << 4 | r >> 4;
		u = (r & 15) << 2 | i >> 6;
		a = i & 63;

		result += _keyStr[s]
		result += _keyStr[o]
		result += _keyStr[u]
		result += _keyStr[a]
	}
	return result
}

base64 解码代码:

const dict = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const transform = (number) => {
	let str = number.toString(16);
	return str.length < 2 ? '0' + str : str;
}
const decode = (str) => {
	var t = "", n, r, i, s, o, u, a, f = 0;
	let result = ""
	while (f < str.length) {
		s = dict.indexOf(str.charAt(f++));
		o = dict.indexOf(str.charAt(f++));
		u = dict.indexOf(str.charAt(f++));
		a = dict.indexOf(str.charAt(f++));
		n = s << 2 | o >> 4;
		r = (o & 15) << 4 | u >> 2;
		i = (u & 3) << 6 | a;
		result += transform(n)
		result += transform(r)
		result += transform(i)
	}
	return result
}

base64的图片被解码之后为图片原始数据,包含了图片头部信息和数据部分,我们可以给商品图片添加水印,在客户端本地进行图片合成等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值