背景介绍

在开发过程中,遇到这样一个问题,热敏打印机打印图片,需要将图片进行缩放、二值化处理后,将图片像素数据发送给打印机进行打印,最初,是基于canvas绘制图片,对图片进行缩放处理,这种方式虽然也能达到目的,但是我觉得并不完美,我觉得一定有办法,能将原始图像的像素矩阵,进行各种运算后得到想要的效果,比如缩放、裁剪、灰度化等,于是有了对图像进阶了解的探索之路。

图像的表现形式

在计算机中,位图是用像素来展示图片的,也就是将图片分割为很小很小的单元格,每个单元格存储一个色值,这些单元格按照特定顺序排列在一起,也就形成了一张图片,这也就是为什么,像素越高(指定尺寸内的单元格越多),图像看起来就约细腻。

像素

计算机中,颜色由RGB三种颜色混合而成,那么一个像素点的色值就会包含三个数据,即红色R,绿色G,蓝色B,然而通常使用的,还会包含透明度(Alpha通道,也就是RGBA),因此一个像素就变成了四个数值来表示,每个值用一个字节(8位)存储,那么每个值的范围是0-255,所以RGB图像能表现的颜色值共有256*256*256=16777216种。

图像缩放原理

明白了上述的图像表现原理,就容易知道,对图片的各种变化,其实就是对图片像素数据的运算。

图片缩小

图片缩小原理是,按照比例,将像素使用特定算法进行合并,得到一个新的像素矩阵,即实现了图片的缩小,如下图所示(一个格子表示一个像素),左边表示一张8*8大小的图片,需要缩小到4*4大小,就需要将右图中每个单元格的数据使用算法进行合并,得到一个像素值。

image-20230111122706882

图片放大

明白了图片缩小原理,图片放大就容易理解了,既然缩小是减少数据,那放大就是增加数据,通过算法,产生新的像素数据进行排列,就得到了放大后图片的像素矩阵。

对于图片缩放的像素运算算法,这里不做讲解,有兴趣可参考博文【图像缩放】双立方(三次)卷积插值。了解图片缩放的原理后,图片的裁剪、旋转等操作也都类似,都可以基于图片像素矩阵进行运算后得到新图片的像素矩阵。

彩色图片转灰度图

灰度图定义

任何颜色都由红、绿、蓝三原色组成,而灰度图只有一个通道,他有256个灰度等级,255代表全白,0表示全黑。

RGB图转灰度图

我们知道RGB颜色由三个字节存储,而灰度图又只有一个字节存储色值,如果要将RGB图像转换为灰度图,我们可以通过算法对RGB进行计算,得到灰阶值,然后将RGB都改为灰阶值,所有像素通过如此计算,图片就变成了灰度图。

计算灰阶值通常有如下算法:

1.浮点法:Gray=R*0.3+G*0.59+B*0.11

2.整数法:Gray=(R*30+G*59+B*11) / 100

3.移位法:Gray=(R*77+G*151+B*28)>>8

4.平均值法:Gray=(R+G+B)/ 3

5.仅取绿色:Gray=G

上面的系数是怎么来的呢,因为人眼对红绿蓝三种颜色的感知程度不同,因此对三种颜色以不同的权重进行计算,得到的灰阶值会更符合人眼观看。但是每种算法都有它的优缺点。

代码示例

下面我们使用HTML语法演示彩色图片转灰阶图;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片处理</title>
<style>
#group {
display: flex;
width: 700px;
justify-content: space-between;
}

.item {
width: 300px;
height: 300px;
border: 1px solid #000;
}

#canvas {
border: 1px dashed #000;
}
</style>
</head>
<body>
<input type="file" name="image" id="imgFile" onchange="loadImg()">
<input type="button" value="灰阶图" onclick="toGray()">
<div id="group">
<div class="scream item">
<img alt="loading.." width="300" id="scream">
</div>
<canvas id="canvas" width="300" height="300" class="item"></canvas>
</div>

<script>

var c_w = 300, c_h = 300

//加载图片
function loadImg () {
var img = getEleById('scream')
var file = getEleById('imgFile').files[0]
if (!/image\/\w+/.test(file.type)) {
alert('文件必须为图片!')
return false
}

var reader = new FileReader()
reader.addEventListener('load', function () {
img.src = this.result
})
if (file) {
reader.readAsDataURL(file)
loadCanvas()
}
console.log(file)
}

//加载canvas
function loadCanvas () {
var canvas = getEleById('canvas')
var ctx = canvas.getContext('2d')
var img = getEleById('scream')
img.onload = function () {
ctx.clearRect(0, 0, c_w, c_h)
ctx.drawImage(img, 0, 0, img.width, img.height)
}
}

function getEleById (id) {
return document.getElementById(id)
}

function toGray () {
var img = getEleById('scream')
var canvas = getEleById('canvas')
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, c_w, c_h)
ctx.drawImage(img, 0, 0, img.width, img.height)

var orignImgData = ctx.getImageData(0, 0, img.width, img.height)
var pixelArr = orignImgData.data
var len = pixelArr.length

for (var i = 0; i < len; i += 4) {
var R = pixelArr[i]
var G = pixelArr[i + 1]
var B = pixelArr[i + 2]

// 根据不同算法计算图片灰度值,这里使用基于人眼感知的权重算法计算
var grayValue = R * 0.3 + G * 0.59 + B * 0.11
pixelArr[i] = grayValue
pixelArr[i + 1] = grayValue
pixelArr[i + 2] = grayValue
}
ctx.putImageData(orignImgData, 0, 0)
}
</script>
</body>
</html>

image-20230111130150866

通过上图可以看到,基于人眼感知权重分配计算的灰度图,其他算法可自行尝试对比。

RGB彩图二值化

什么是二值化

上面我们了解了灰度图,一个像素由一个字节表示,可表示256个色值,二值化图片,就是把像素改为1位表示,即像素点只有黑(1)与白(0)两种颜色,因此彩图二值化的流程应该是彩图 -> 灰度图 -> 二值化。二值化图像有它实用的场景,比如扫描文档时,背景会有浅灰色的杂色,我们可以使用二值化处理图片,让图片变成只有黑白两色,又比如,热敏打印机只能打印黑白两色,我们想要在热敏打印机中打印图片,就只能将图片二值化处理后才能打印。

二值化实现

彩色图片二值化,首先需要将图片转为灰度图,将原来一个像素由3个字节存储变为一个像素由一个字节存储(灰度图0为全黑色,255为全白色),然后根据阈值判断这个灰度图的像素是黑色还是白色,比如设置阈值是150,一个灰度图的像素值是132,150 > 132,因此这个像素点二值化后为0,即这个点是黑色。

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>图片二值化</title>
<style>
#group {
display: flex;
width: 700px;
justify-content: space-between;
}

.item {
width: 300px;
height: 300px;
border: 1px solid #000;
}

#canvas {
border: 1px dashed #000;
}
</style>
</head>
<body>
<input type="file" name="image" id="imgFile" onchange="loadImg()">
<!-- 请输入0-255的阈值-->
<input type="text" name="Threshold " value="150" id="Threshold">
<input type="button" value="二值化" onclick="binary()">
<div id="group">
<div class="scream item">
<img alt="loading.." width="300" id="scream">
</div>
<canvas id="canvas" width="300" height="300" class="item"></canvas>
</div>

<script>

var c_w = 300, c_h = 300

//加载图片
function loadImg () {
var img = getEleById('scream')
var file = getEleById('imgFile').files[0]
if (!/image\/\w+/.test(file.type)) {
alert('文件必须为图片!')
return false
}

var reader = new FileReader()
reader.addEventListener('load', function () {
img.src = this.result
})
if (file) {
reader.readAsDataURL(file)
loadCanvas()
}
console.log(file)
}

//加载canvas
function loadCanvas () {
var canvas = getEleById('canvas')
var ctx = canvas.getContext('2d')
var img = getEleById('scream')
img.onload = function () {
ctx.clearRect(0, 0, c_w, c_h)
ctx.drawImage(img, 0, 0, img.width, img.height)
}
}

function getEleById (id) {
return document.getElementById(id)
}

//图片二值化
function binary () {
var Threshold = getEleById('Threshold').value
var img = getEleById('scream')
var canvas = getEleById('canvas')
var ctx = canvas.getContext('2d')
ctx.clearRect(0, 0, c_w, c_h)
ctx.drawImage(img, 0, 0, img.width, img.height)

var orignImgData = ctx.getImageData(0, 0, img.width, img.height)
var pixelArr = orignImgData.data
var len = pixelArr.length

for (var i = 0; i < len; i += 4) {
var R = pixelArr[i]
var G = pixelArr[i + 1]
var B = pixelArr[i + 2]

// 根据不同算法计算图片灰度值,这里使用基于人眼感知的权重算法计算
var grayValue = R * 0.3 + G * 0.59 + B * 0.11
// 这里是为了显示二值化后的图片,实际二值化后的图片像素数组为只有0和1的值
var binaryValue = grayValue > Threshold ? 255 : 0
pixelArr[i] = binaryValue
pixelArr[i + 1] = binaryValue
pixelArr[i + 2] = binaryValue
}
ctx.putImageData(orignImgData, 0, 0)
}
</script>
</body>
</html>

image-20230111132433815

image-20230111132501481

image-20230111132547362

可以看到,阈值的选定对二值化后的图片有着较大影响,因此,为了得到更好的二值化图片,通常会使用算法优化,动态改变阈值。