打开Windows系统自带的画图软件,如下:
点击“文件 > 另存为”,在弹出的界面中有4种bmp格式可以选择,如下:
不知道bmp格式是不是只有上面的4种,还是有更多,我也懒得了解,这里我们只了解“24位位图”的bmp格式,因为这个格式比较简单,所以,在保存图片的时候要选择24位位图的bmp格式,不要选错了。
重新调整大小,如下:
Windows 11中调整大小在菜单中:文件 > 图像属性,如下图:
如上图,设置图片的宽为2,高为3,这样的话我们的bmp图片就只有6个像素,比较少,容易分析。按住Ctrl键然后滚动鼠标滚轮,这样可以放大图片,因为6个像素的图片非常非常的小,所以我们把它放大到最大,如下:
因为这个图片有6个像素,所以我们可以画6个颜色上去,使用铅笔工具,并且把粗细调到最细,如下:
在软件的右上位置点击“编辑颜色”,然后在出现的界面中,在红、绿、蓝的位置可以输入颜色值,如下:
设置好自己需要的颜色,然后用铅笔给图片画上6个颜色,如下:
最后,把图片保存为24位的bmp格式。下面我把我设置的6个颜色标注如下:
这6个颜色分别是:黑、白、红、绿、黄、紫。
我把图片保存为“示例.bmp”,查看该文件信息,如下:
如上图,有3个信息我们可以了解一下,分别是图片的分辨率、位深度、大小。
然后我们查看该文件的二进制,不知道如何查看文件二进制的,可以查看我的这篇文章:https://www.cdsy.xyz/computer/programme/suggest/230207/cd40315.html
"示例.bmp"的二进制内容如下:
我们再来理解一下这个界面的相关内容,如下:
如上图,分为3个区域,左边显示的是相应的行的第一个byte元素的索引位置,因为一个文件其实就是一个byte数组,如上图所示,它把一个文件的所有byte数据进行了分行显示,所以最左边的紫色数字显示了相应行第一个byte的索引位置,且是以16进制来表示的,比如第二行的00000018,对应的十进制为24,也就是说第二行的第一个byte是在数组索引24的位置。第三行第一个byte在数组中的索引位置为30(对应十进制为48)。中间区域为具体的文件数据,以16进制显示,每两位为一个字节,比如42是文件第1个字节的数据,4d是文件第2个字节的数据,依次类推。右侧区域应该是显示了每个字节对应的符号,比如42对应的十进制为66,66对应的ASCII符号为B,我们把光标定位在42上,则42高亮显示,右侧的符号B也高亮显示,说明42这个十六进制数据对应的符号为B,如下:
4d对应的十进制为77,77对应的ASCII符号为M,如下:
BM就表示这是一个bmp文件,有些数据没有对应符号的就显示为点。
bmp文件由以下3部分组成:
我们把bmp文件头和位图信息头简称文件头吧,这样文件头就占54字节,剩下的就是图片像素数据了。
回顾我们之前创建的 2 x 3大小的图片,共6个像素,保存了6个颜色,如下:
此文件的二进制数据如下:
如上图,有4行数据,每一行解释如下:
如上图,每行数据我都进行了含义说明,并把每个数据的数组索引标了出来,以16进制方式标了出来。红色为图像像素数据,其它为文件头数据。
关于大端、小端的知识点,可以查看此篇文章,如上图的文件大小为:4e00 0000,它是小端模式的,则4e是最低位的,高位都是0,所以直接把16进制的4e换为十进制即可,4e的十进制为78,说明这个文件的大小为78字节,这跟我们看文件属性上的大小是对应的,如下:
假设另一个文件大小如下:
如上图,表示大小的数据为:1675 2800,因为是小端模式,所以,换成高位到低位的顺序为:00 28 75 16,因为它是16进制的,换成十进制,我们使用系统自带的计算机进行计算,如下:
如上图,可以看到,16进制的287516对应的十进制为2651414,与文件信息上的文件大小也是对应的,如下:
bmp头文件中的信息很多,我们只需要了解其中几个即可,比如:
我个人认为对于初学者不需要了解那么多参数,如想了解所有参数可以查看别人的文章:https://www.cdsy.xyz/computer/fileABC/230208/cd40325.html、https://www.cdsy.xyz/computer/fileABC/230208/cd40326.html
前面我们了解了bmp文件的组成原理,那我们就可以通过代码的方式创建一个bmp图片,这里我们创建一个简单的图片,如下:
图像宽4,高4,上一半显示红色,一下半显示绿色。4 x 4的图片是非常小的,所以上面的截图是经过放大之后的,使用这么小的宽和高,是为了在写代码时可以打印出所有像素数据,方便观察是否正确,如果图片宽高很大的话,像素数据会非常多,不方便分析。
在开始写代码之前,需要先知道一些数据:
代码创建bmp文件非常简单,根据前面了解的bmp文件头,我们把宽、高、像素大小、文件大小这4个数据设置到文件头中,其它文件头信息就保持和之前分析的保持一样即可。最后就是把红、绿颜色的像素写到文件头后面即可。
import java.io.*
fun main() {
val width = 4
val height = 4
val pixelBytesCount = width * height * 3
val fileBytesCount = pixelBytesCount + 54
val red = 0xff0000
val green = 0x00ff00
// 424d
val bmpFileBytes = ByteArray(fileBytesCount)
bmpFileBytes[0x00] = 0x42
bmpFileBytes[0x01] = 0x4d
// 文件大小
var bytes = getLittleEndianBytes(fileBytesCount)
bmpFileBytes[0x02] = bytes[0]
bmpFileBytes[0x03] = bytes[1]
bmpFileBytes[0x04] = bytes[2]
bmpFileBytes[0x05] = bytes[3]
// 保留数据
bmpFileBytes[0x06] = 0x00
bmpFileBytes[0x07] = 0x00
bmpFileBytes[0x08] = 0x00
bmpFileBytes[0x09] = 0x00
// 像素存储位置
bmpFileBytes[0x0a] = 0x36
bmpFileBytes[0x0b] = 0x00
bmpFileBytes[0x0c] = 0x00
bmpFileBytes[0x0d] = 0x00
// bmp头文件大小
bmpFileBytes[0x0e] = 0x28
bmpFileBytes[0x0f] = 0x00
bmpFileBytes[0x10] = 0x00
bmpFileBytes[0x11] = 0x00
// 图像宽度
bytes = getLittleEndianBytes(width)
bmpFileBytes[0x12] = bytes[0]
bmpFileBytes[0x13] = bytes[1]
bmpFileBytes[0x14] = bytes[2]
bmpFileBytes[0x15] = bytes[3]
// 图像高度
bytes = getLittleEndianBytes(height)
bmpFileBytes[0x16] = bytes[0]
bmpFileBytes[0x17] = bytes[1]
bmpFileBytes[0x18] = bytes[2]
bmpFileBytes[0x19] = bytes[3]
// 色彩平面数
bmpFileBytes[0x1a] = 0x01
bmpFileBytes[0x1b] = 0x00
// 像素位数
bmpFileBytes[0x1c] = 0x18
bmpFileBytes[0x1d] = 0x00
// 压缩方式
bmpFileBytes[0x1e] = 0x00
bmpFileBytes[0x1f] = 0x00
bmpFileBytes[0x20] = 0x00
bmpFileBytes[0x21] = 0x00
// 像素数据大小
bytes = getLittleEndianBytes(pixelBytesCount)
bmpFileBytes[0x22] = bytes[0]
bmpFileBytes[0x23] = bytes[1]
bmpFileBytes[0x24] = bytes[2]
bmpFileBytes[0x25] = bytes[3]
// 横向分辨率
bmpFileBytes[0x26] = 0x00
bmpFileBytes[0x27] = 0x00
bmpFileBytes[0x28] = 0x00
bmpFileBytes[0x29] = 0x00
// 纵向分辨率
bmpFileBytes[0x2a] = 0x00
bmpFileBytes[0x2b] = 0x00
bmpFileBytes[0x2c] = 0x00
bmpFileBytes[0x2d] = 0x00
// 调色板颜色数
bmpFileBytes[0x2e] = 0x00
bmpFileBytes[0x2f] = 0x00
bmpFileBytes[0x30] = 0x00
bmpFileBytes[0x31] = 0x00
// 重要颜色数
bmpFileBytes[0x32] = 0x00
bmpFileBytes[0x33] = 0x00
bmpFileBytes[0x34] = 0x00
bmpFileBytes[0x35] = 0x00
// 图像像素
val redBytes = getLittleEndianBytes(red)
val greenBytes = getLittleEndianBytes(green)
var index = 0x36
for (rowIndex in 0 until height) {
// 注意:因为存储时是从图像最后一行开始存的,最后一行是绿色,
// 所以先存后面那些行的绿色,再存前面那些行的红色
bytes = if (rowIndex < height / 2) greenBytes else redBytes
for (columnIndex in 0 until width) {
bmpFileBytes[index++] = bytes[0]
bmpFileBytes[index++] = bytes[1]
bmpFileBytes[index++] = bytes[2]
// 注意:bytes中是有4个元素的,因为int转为了byte数组,我们知道一个int是占4个字节的
// 因为使用了小端,所以bytes[3]是int的最高位数据,我们是没有使用到的,
// 因为#FF0000这种颜色值只需要3个字节
}
}
// 把所有的字节写到文件
val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
bmpFile.writeBytes(bmpFileBytes)
}
/** 把int转换为byte数组,默认是大端方式的数组,返回转换为小端方式的数组 */
fun getLittleEndianBytes(number: Int): ByteArray {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeInt(number)
val bigEndianBytes = baos.toByteArray()
val littleEndianBytes = bigEndianBytes.reversedArray()
return littleEndianBytes
}
运行这段代码即可得到正确的bmp文件,当然,如果你在写代码的时候某些地方写错了,得不到正确的bmp图片,你可以分两步分析:
var pixelCount = 0
for (i in 0x36 until bmpFileBytes.size step 3) {
val blue = bmpFileBytes[i + 0]
val green = bmpFileBytes[i + 1]
val red = bmpFileBytes[i + 2]
print("$blue, $green, $red |")
if (++pixelCount == width) {
// 够一行了,输出一个换行
pixelCount = 0
println()
}
}
运行效果如下:
0, -1, 0 |0, -1, 0 |0, -1, 0 |0, -1, 0 |
0, -1, 0 |0, -1, 0 |0, -1, 0 |0, -1, 0 |
0, 0, -1 |0, 0, -1 |0, 0, -1 |0, 0, -1 |
0, 0, -1 |0, 0, -1 |0, 0, -1 |0, 0, -1 |
可以看到,共4行,前面两行是绿色,后两行是红色,因为存储时是先从图像的最后一行开始存储的,而且每个像素是按蓝、绿、红的顺序输出的。-1其实就是255,因为byte的表示范围是 -128 ~ 127,-1其实就是8个比特位都是1,在byte中就是-1,如果在int中就是255,所以我们还可以转为int类型来显示,修改相关代码如下:
val blue = bmpFileBytes[i + 0].toInt() shl 24 ushr 24
val green = bmpFileBytes[i + 1].toInt() shl 24 ushr 24
val red = bmpFileBytes[i + 2].toInt() shl 24 ushr 24
运行结果如下:
0, 255, 0 |0, 255, 0 |0, 255, 0 |0, 255, 0 |
0, 255, 0 |0, 255, 0 |0, 255, 0 |0, 255, 0 |
0, 0, 255 |0, 0, 255 |0, 0, 255 |0, 0, 255 |
0, 0, 255 |0, 0, 255 |0, 0, 255 |0, 0, 255 |
我们用16进制软件查看bmp时,出来的数据都是16进制的,所以,有时候我们也喜欢用16进制的方式来查看结果,修改相关代码如下:
val blue = Integer.toHexString(bmpFileBytes[i + 0].toInt() shl 24 ushr 24)
val green = Integer.toHexString(bmpFileBytes[i + 1].toInt() shl 24 ushr 24)
val red = Integer.toHexString(bmpFileBytes[i + 2].toInt() shl 24 ushr 24)
运行结果如下:
0, ff, 0 |0, ff, 0 |0, ff, 0 |0, ff, 0 |
0, ff, 0 |0, ff, 0 |0, ff, 0 |0, ff, 0 |
0, 0, ff |0, 0, ff |0, 0, ff |0, 0, ff |
0, 0, ff |0, 0, ff |0, 0, ff |0, 0, ff |
当你写的小Demo没问题了,此时你就可以把宽高改大一些了,因为此时你应该是不用再分析数据是否对错了,只需要看生成的图片效果是否正常即可。比如设置为300 x 200,运行效果如下:
我们前面的代码是按着逻辑一点点拼装的数据,另外,我们也可以假设有一个正常的像素矩阵了,需要把它写到bmp文件中,这次我们把相关的实现代码封装到一个叫BmpUtil的类中,以方便进行复用,示例如下:
1、我们先模拟出一个像素矩阵,如下:
object BmpUtil {
/** 创建Bitmap的示例:创建一个上一半为红色,下一半为绿色的bmp文件 */
fun createBitmapDemo() {
val width = 4 // 注意:宽高要设置为4的倍数,以避免需要进行补位的操作
val height = 4
val pixelBytes = createPixelBytes(width, height)
printPixelBytes(pixelBytes)
}
/** 创建像素矩阵,注意:宽要设置为4的倍数 */
fun createPixelBytes(width: Int, height: Int) : Array<ByteArray> {
val redColor = 0xFF0000
val greenColor = 0x00FF00
val redBytes = getColorBytes(redColor)
val greenBytes = getColorBytes(greenColor)
val pixelBytes = Array(height) { ByteArray(width * 3) }
for (rowIndex in 0 until height) {
val colorBytes = if (rowIndex < height / 2) redBytes else greenBytes
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = colorBytes[0]
val green = colorBytes[1]
val blue = colorBytes[2]
oneLineBytes[columnIndex + 0] = red
oneLineBytes[columnIndex + 1] = green
oneLineBytes[columnIndex + 2] = blue
}
}
return pixelBytes
}
fun getColorBytes(color: Int): ByteArray {
val red = (color and 0xFF0000 ushr 16).toByte()
val green = (color and 0x00FF00 ushr 8).toByte()
val blue = (color and 0x0000FF).toByte()
val colorBytes = byteArrayOf(red, green, blue)
return colorBytes
}
/** 打印像素值,可打印rgbBytes,也可以打印yuv444Bytes */
fun printPixelBytes(pixelBytes: Array<ByteArray>) {
for (rowIndex in pixelBytes.indices) {
val oneLine = pixelBytes[rowIndex]
for (columnIndex in oneLine.indices step 3) {
// 获取1个像素的3个颜色通道:R、G、B 或 Y、U、V
val colorChannel1 = oneLine[columnIndex + 0]
val colorChannel2 = oneLine[columnIndex + 1]
val colorChannel3 = oneLine[columnIndex + 2]
// 把byte转为int,再以16进制进行输出
val colorChannelInt1 = toHexString(byteToInt(colorChannel1))
val colorChannelInt2 = toHexString(byteToInt(colorChannel2))
val colorChannelInt3 = toHexString(byteToInt(colorChannel3))
// 以16进制进行打印
print("$colorChannelInt1 $colorChannelInt2 $colorChannelInt3| ")
}
println()
}
}
fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
fun toHexString(int: Int): String = Integer.toHexString(int)
}
fun main() {
BmpUtil.createBitmapDemo()
}
运行结果如下:
ff 0 0| ff 0 0| ff 0 0| ff 0 0|
ff 0 0| ff 0 0| ff 0 0| ff 0 0|
0 ff 0| 0 ff 0| 0 ff 0| 0 ff 0|
0 ff 0| 0 ff 0| 0 ff 0| 0 ff 0|
可以看到,这是一个4 x 4的图像,前两行是红色,后两行是绿色。需要注意的是,这里在获取一个颜色值的byte数组时,使用了位操作,这也是比较方便的。如果按照之前的使用输出流来获取一个int的4个字节时需要注意,因为一个颜色值只占3个字节,所以要取低位的3个字节,但是如果不小心从高位开始取就会出错,这是很容易出现的错误。
接下来就是要把这些图像数据写到bmp文件中,如下:
object BmpUtil {
/** 创建Bitmap的示例:创建一个上一半为红色,下一半为绿色的bmp文件 */
fun createBitmapDemo() {
val width = 4 // 注意:宽高要设置为4的倍数,以避免需要进行补位的操作
val height = 4
val pixelBytes = createPixelBytes(width, height)
//printPixelBytes(pixelBytes)
val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
createBmpFile(pixelBytes, bmpFile)
}
/** 根据给定的像素二维数据,按照bmp文件规范保存到指定的bmp文件中 */
fun createBmpFile(pixelBytes: Array<ByteArray>, saveFile: File) {
// 因为一行像素中每个像素是占3个字节的,除以3就得到图像的宽度了
val pixelWidth = pixelBytes[0].size / 3
val pixelHeight = pixelBytes.size
// 每个像素占3个byte,所以要乘以3
val pixelBytesCount = pixelWidth * pixelHeight * 3
// 文件总大小为:像素数据大小 + 头文件大小
val fileBytesCount = pixelBytesCount + 54
// 创建一个byte数组,用于保存bmp文件的所有byte数据
val bmpFileBytes = ByteArray(fileBytesCount)
// 往bmpFileBytes中添加bmp文件头
addBmpFileHeader(pixelWidth, pixelHeight, bmpFileBytes)
// 往bmpFileBytes中添加像素数据
addPixelBytes(pixelBytes, bmpFileBytes)
// 把所有的字节写到文件
saveFile.writeBytes(bmpFileBytes)
}
fun addBmpFileHeader(width: Int, height: Int, bmpFileBytes: ByteArray) {
val pixelBytesCount = width * height * 3
val fileBytesCount = pixelBytesCount + 54
// 424d
bmpFileBytes[0x00] = 0x42
bmpFileBytes[0x01] = 0x4d
// 文件大小
var bytes = getLittleEndianBytes(fileBytesCount)
bmpFileBytes[0x02] = bytes[0]
bmpFileBytes[0x03] = bytes[1]
bmpFileBytes[0x04] = bytes[2]
bmpFileBytes[0x05] = bytes[3]
// 保留数据
bmpFileBytes[0x06] = 0x00
bmpFileBytes[0x07] = 0x00
bmpFileBytes[0x08] = 0x00
bmpFileBytes[0x09] = 0x00
// 像素存储位置
bmpFileBytes[0x0a] = 0x36
bmpFileBytes[0x0b] = 0x00
bmpFileBytes[0x0c] = 0x00
bmpFileBytes[0x0d] = 0x00
// bmp头文件大小
bmpFileBytes[0x0e] = 0x28
bmpFileBytes[0x0f] = 0x00
bmpFileBytes[0x10] = 0x00
bmpFileBytes[0x11] = 0x00
// 图像宽度
bytes = getLittleEndianBytes(width)
bmpFileBytes[0x12] = bytes[0]
bmpFileBytes[0x13] = bytes[1]
bmpFileBytes[0x14] = bytes[2]
bmpFileBytes[0x15] = bytes[3]
// 图像高度
bytes = getLittleEndianBytes(height)
bmpFileBytes[0x16] = bytes[0]
bmpFileBytes[0x17] = bytes[1]
bmpFileBytes[0x18] = bytes[2]
bmpFileBytes[0x19] = bytes[3]
// 色彩平面数
bmpFileBytes[0x1a] = 0x01
bmpFileBytes[0x1b] = 0x00
// 像素位数
bmpFileBytes[0x1c] = 0x18
bmpFileBytes[0x1d] = 0x00
// 压缩方式
bmpFileBytes[0x1e] = 0x00
bmpFileBytes[0x1f] = 0x00
bmpFileBytes[0x20] = 0x00
bmpFileBytes[0x21] = 0x00
// 像素数据大小
bytes = getLittleEndianBytes(pixelBytesCount)
bmpFileBytes[0x22] = bytes[0]
bmpFileBytes[0x23] = bytes[1]
bmpFileBytes[0x24] = bytes[2]
bmpFileBytes[0x25] = bytes[3]
// 横向分辨率
bmpFileBytes[0x26] = 0x00
bmpFileBytes[0x27] = 0x00
bmpFileBytes[0x28] = 0x00
bmpFileBytes[0x29] = 0x00
// 纵向分辨率
bmpFileBytes[0x2a] = 0x00
bmpFileBytes[0x2b] = 0x00
bmpFileBytes[0x2c] = 0x00
bmpFileBytes[0x2d] = 0x00
// 调色板颜色数
bmpFileBytes[0x2e] = 0x00
bmpFileBytes[0x2f] = 0x00
bmpFileBytes[0x30] = 0x00
bmpFileBytes[0x31] = 0x00
// 重要颜色数
bmpFileBytes[0x32] = 0x00
bmpFileBytes[0x33] = 0x00
bmpFileBytes[0x34] = 0x00
bmpFileBytes[0x35] = 0x00
}
/** 把指定的像素数据添加到bmp文件数组中 */
fun addPixelBytes(pixelBytes: Array<ByteArray>, bmpFileBytes: ByteArray) {
val height = pixelBytes.size
var index = 0x36
// 设置像素数据,注意:要从像素的最后一行开始进行存储
for (rowIndex in height - 1 downTo 0) {
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = oneLineBytes[columnIndex + 0]
val green = oneLineBytes[columnIndex + 1]
val blue = oneLineBytes[columnIndex + 2]
// 每个像素的三原色按倒序存储
bmpFileBytes[index++] = blue
bmpFileBytes[index++] = green
bmpFileBytes[index++] = red
}
}
}
/** 把int转换为byte数组,默认是大端方式的数组,返回转换为小端方式的数组 */
fun getLittleEndianBytes(number: Int): ByteArray {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeInt(number)
val bigEndianBytes = baos.toByteArray()
val littleEndianBytes = bigEndianBytes.reversedArray()
return littleEndianBytes
}
}
为了阅读代码的方便,BmpUtil中之前已经实现的代码就没有添加到上述代码中了,完整代码如下:
Test.kt 文件代码如下:
fun main() {
BmpUtil.createBitmapDemo()
}
BmpUtil.kt 文件代码如下:
import java.io.ByteArrayOutputStream
import java.io.DataOutputStream
import java.io.File
object BmpUtil {
/** 创建Bitmap的示例:创建一个上一半为红色,下一半为绿色的bmp文件 */
fun createBitmapDemo() {
val width = 300 // 注意:宽高要设置为4的倍数,以避免需要进行补位的操作
val height = 200
val pixelBytes = createPixelBytes(width, height)
//printPixelBytes(pixelBytes)
val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
createBmpFile(pixelBytes, bmpFile)
}
/** 创建像素矩阵,注意:宽要设置为4的倍数 */
fun createPixelBytes(width: Int, height: Int) : Array<ByteArray> {
val redColor = 0xFF0000
val greenColor = 0x00FF00
val redBytes = getColorBytes(redColor)
val greenBytes = getColorBytes(greenColor)
val pixelBytes = Array(height) { ByteArray(width * 3) }
for (rowIndex in 0 until height) {
val colorBytes = if (rowIndex < height / 2) redBytes else greenBytes
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = colorBytes[0]
val green = colorBytes[1]
val blue = colorBytes[2]
oneLineBytes[columnIndex + 0] = red
oneLineBytes[columnIndex + 1] = green
oneLineBytes[columnIndex + 2] = blue
}
}
return pixelBytes
}
fun getColorBytes(color: Int): ByteArray {
val red = (color and 0xFF0000 ushr 16).toByte()
val green = (color and 0x00FF00 ushr 8).toByte()
val blue = (color and 0x0000FF).toByte()
val colorBytes = byteArrayOf(red, green, blue)
return colorBytes
}
/** 打印像素值,可打印rgbBytes,也可以打印yuv444Bytes */
fun printPixelBytes(pixelBytes: Array<ByteArray>) {
for (rowIndex in pixelBytes.indices) {
val oneLine = pixelBytes[rowIndex]
for (columnIndex in oneLine.indices step 3) {
// 获取1个像素的3个颜色通道:R、G、B 或 Y、U、V
val colorChannel1 = oneLine[columnIndex + 0]
val colorChannel2 = oneLine[columnIndex + 1]
val colorChannel3 = oneLine[columnIndex + 2]
// 把byte转为int,再以16进制进行输出
val colorChannelInt1 = toHexString(byteToInt(colorChannel1))
val colorChannelInt2 = toHexString(byteToInt(colorChannel2))
val colorChannelInt3 = toHexString(byteToInt(colorChannel3))
// 以16进制进行打印
print("$colorChannelInt1 $colorChannelInt2 $colorChannelInt3| ")
}
println()
}
}
fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
fun toHexString(int: Int): String = Integer.toHexString(int)
/** 根据给定的像素二维数据,按照bmp文件规范保存到指定的bmp文件中 */
fun createBmpFile(pixelBytes: Array<ByteArray>, saveFile: File) {
// 因为一行像素中每个像素是占3个字节的,除以3就得到图像的宽度了
val pixelWidth = pixelBytes[0].size / 3
val pixelHeight = pixelBytes.size
// 每个像素占3个byte,所以要乘以3
val pixelBytesCount = pixelWidth * pixelHeight * 3
// 文件总大小为:像素数据大小 + 头文件大小
val fileBytesCount = pixelBytesCount + 54
// 创建一个byte数组,用于保存bmp文件的所有byte数据
val bmpFileBytes = ByteArray(fileBytesCount)
// 往bmpFileBytes中添加bmp文件头
addBmpFileHeader(pixelWidth, pixelHeight, bmpFileBytes)
// 往bmpFileBytes中添加像素数据
addPixelBytes(pixelBytes, bmpFileBytes)
// 把所有的字节写到文件
saveFile.writeBytes(bmpFileBytes)
}
fun addBmpFileHeader(width: Int, height: Int, bmpFileBytes: ByteArray) {
val pixelBytesCount = width * height * 3
val fileBytesCount = pixelBytesCount + 54
// 424d
bmpFileBytes[0x00] = 0x42
bmpFileBytes[0x01] = 0x4d
// 文件大小
var bytes = getLittleEndianBytes(fileBytesCount)
bmpFileBytes[0x02] = bytes[0]
bmpFileBytes[0x03] = bytes[1]
bmpFileBytes[0x04] = bytes[2]
bmpFileBytes[0x05] = bytes[3]
// 保留数据
bmpFileBytes[0x06] = 0x00
bmpFileBytes[0x07] = 0x00
bmpFileBytes[0x08] = 0x00
bmpFileBytes[0x09] = 0x00
// 像素存储位置
bmpFileBytes[0x0a] = 0x36
bmpFileBytes[0x0b] = 0x00
bmpFileBytes[0x0c] = 0x00
bmpFileBytes[0x0d] = 0x00
// bmp头文件大小
bmpFileBytes[0x0e] = 0x28
bmpFileBytes[0x0f] = 0x00
bmpFileBytes[0x10] = 0x00
bmpFileBytes[0x11] = 0x00
// 图像宽度
bytes = getLittleEndianBytes(width)
bmpFileBytes[0x12] = bytes[0]
bmpFileBytes[0x13] = bytes[1]
bmpFileBytes[0x14] = bytes[2]
bmpFileBytes[0x15] = bytes[3]
// 图像高度
bytes = getLittleEndianBytes(height)
bmpFileBytes[0x16] = bytes[0]
bmpFileBytes[0x17] = bytes[1]
bmpFileBytes[0x18] = bytes[2]
bmpFileBytes[0x19] = bytes[3]
// 色彩平面数
bmpFileBytes[0x1a] = 0x01
bmpFileBytes[0x1b] = 0x00
// 像素位数
bmpFileBytes[0x1c] = 0x18
bmpFileBytes[0x1d] = 0x00
// 压缩方式
bmpFileBytes[0x1e] = 0x00
bmpFileBytes[0x1f] = 0x00
bmpFileBytes[0x20] = 0x00
bmpFileBytes[0x21] = 0x00
// 像素数据大小
bytes = getLittleEndianBytes(pixelBytesCount)
bmpFileBytes[0x22] = bytes[0]
bmpFileBytes[0x23] = bytes[1]
bmpFileBytes[0x24] = bytes[2]
bmpFileBytes[0x25] = bytes[3]
// 横向分辨率
bmpFileBytes[0x26] = 0x00
bmpFileBytes[0x27] = 0x00
bmpFileBytes[0x28] = 0x00
bmpFileBytes[0x29] = 0x00
// 纵向分辨率
bmpFileBytes[0x2a] = 0x00
bmpFileBytes[0x2b] = 0x00
bmpFileBytes[0x2c] = 0x00
bmpFileBytes[0x2d] = 0x00
// 调色板颜色数
bmpFileBytes[0x2e] = 0x00
bmpFileBytes[0x2f] = 0x00
bmpFileBytes[0x30] = 0x00
bmpFileBytes[0x31] = 0x00
// 重要颜色数
bmpFileBytes[0x32] = 0x00
bmpFileBytes[0x33] = 0x00
bmpFileBytes[0x34] = 0x00
bmpFileBytes[0x35] = 0x00
}
/** 把指定的像素数据添加到bmp文件数组中 */
fun addPixelBytes(pixelBytes: Array<ByteArray>, bmpFileBytes: ByteArray) {
val height = pixelBytes.size
var index = 0x36
// 设置像素数据,注意:要从像素的最后一行开始进行存储
for (rowIndex in height - 1 downTo 0) {
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = oneLineBytes[columnIndex + 0]
val green = oneLineBytes[columnIndex + 1]
val blue = oneLineBytes[columnIndex + 2]
// 每个像素的三原色按倒序存储
bmpFileBytes[index++] = blue
bmpFileBytes[index++] = green
bmpFileBytes[index++] = red
}
}
}
/** 把int转换为byte数组,默认是大端方式的数组,返回转换为小端方式的数组 */
fun getLittleEndianBytes(number: Int): ByteArray {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeInt(number)
val bigEndianBytes = baos.toByteArray()
val littleEndianBytes = bigEndianBytes.reversedArray()
return littleEndianBytes
}
}
读出来又保存回去,这似乎是在做无用功啊!其实也是有些用处的,可以加深对bmp文件格式的理解,我们前面的例子比较简单,只有红色和绿色,如何构建一幅复杂颜色的图片呢?纯代码创造那是要搞死人的,所以我们可以用截图软件截个画面出来并保存为bmp,然后读取里面的像素数据,然后再保存为bmp文件,示例代码如下:
为了方便观察数据,我们使用较小的宽高,而且为了预防图片的像素数据没有补位的情况,我们宽要设置为4的倍数,这里我们使用系统自带画图创建一个4 x 2的图像,如下:
把这个图像保存为24位的bmp文件。上图中8个颜色的颜色值如下:
接下来就是读取这个图片的像素值,为了方便操作,我们使用二维数组来保存数据,还有,为了方便阅读相关代码,之前已经实现的代码也不会出现在下面的代码中。示例如下:
Test.kt 文件代码如下:
fun main() {
//BmpUtil.createBitmapDemo()
BmpUtil.createBitmapDemo2()
}
BmpUtil.kt 文件代码如下:
运行结果如下:
object BmpUtil {
/** 创建Bitmap的示例:通过读取一个bmp文件的像素,再把这些像素写入一个新的bmp文件 */
fun createBitmapDemo2() {
val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\8个颜色.bmp"))
printPixelBytes(bmpFilePixelBytes)
}
fun readBmpFilePixelBytes(bmpFile: File): Array<ByteArray> {
// 得到bmp文件的所有字节
val bmpFileBytes = bmpFile.readBytes()
// 从bmp文件中获取图像的宽和高的字节数组
val widthLittleEndianBytes = ByteArray(4)
val heightLittleEndianBytes = ByteArray(4)
System.arraycopy(bmpFileBytes, 0x12, widthLittleEndianBytes, 0, 4)
System.arraycopy(bmpFileBytes, 0x16, heightLittleEndianBytes, 0, 4)
// 把小端的字节数组转换为Int
val width = bigEndianBytesToInt(widthLittleEndianBytes)
val height = bigEndianBytesToInt(heightLittleEndianBytes)
println("读取到bmp图像width = $width, height = $height")
val pixelBytes = Array(height) { ByteArray(width * 3) }
var rowIndex = height - 1 // 因为bmp图片是从最后一行开始保存的,读取的时候我们把它往到正确的位置
var columnIndex = 0
var oneLineBytes = pixelBytes[rowIndex]
val oneLineBytesSize = oneLineBytes.size
// 像素值都是从0x36的位置开始保存的,而且每个像素点3个字节
for (i in 0x36 until bmpFileBytes.size step 3) {
if (columnIndex == oneLineBytesSize) {
// 存满一行了,需要换行保存了。这里--rowIndex是因为原图片是从最后一行向前面行的顺序保存的
oneLineBytes = pixelBytes[--rowIndex]
columnIndex = 0
}
// 注意:bmp文件的颜色是按蓝、绿、红的顺序保存的
val blue = bmpFileBytes[i + 0]
val green = bmpFileBytes[i + 1]
val red = bmpFileBytes[i + 2]
oneLineBytes[columnIndex++] = red
oneLineBytes[columnIndex++] = green
oneLineBytes[columnIndex++] = blue
}
return pixelBytes
}
/** 把小端的字节数组转换为int */
private fun bigEndianBytesToInt(littleEndianBytes: ByteArray): Int {
val bigEndianBytes= littleEndianBytes.reversedArray()
val bais = ByteArrayInputStream(bigEndianBytes)
val dis = DataInputStream(bais)
return dis.readInt()
}
}
运行结果如下:
读取到bmp图像width = 4, height = 2
0 0 0| ff ff ff| ff 0 0| 0 ff 0|
0 0 ff| ff ff 0| ff 0 ff| 0 ff ff|
和原图对比一下数据是否正确:
bmp文件保存的像素数据是从最后一行开始保存的,而且颜色是按蓝、绿、红的顺序存储的,我们取出来的时候把它们都调正了,这是为了方便我们查看结果,从上面打印的结果看,它和图片的每个像素的位置是一至的,而且每个值都是正确的。
Ok,一张bmp的文件的所有像素数据我们都读取到代码中了,接下来就是把它写回到bmp文件中,如下:
object BmpUtil {
/** 创建Bitmap的示例:通过读取一个bmp文件的像素,再把这些像素写入一个新的bmp文件 */
fun createBitmapDemo2() {
val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\8个颜色.bmp"))
//printPixelBytes(bmpFilePixelBytes)
createBmpFile(bmpFilePixelBytes, File("C:\\Users\\Even\\Pictures\\demo.bmp"))
}
}
运行如上代码,将会生成一张demo.bmp的图片,用系统自带的画图软件打开这张图片,并且放大,然后观看效果,如下:
可以看到,正确还原了!如果发现没有正确还原的话,可以打印一下bmpFileBytes中的数据进行分析,假设怀疑是像素的数据有问题,则可以只打印像素数据的部分,如下:
// 打印bmpFileBytes中保存的像素数据
val bmpFileBytes = File("C:\\Users\\Even\\Pictures\\demo.bmp").readBytes()
var count = 0
val width = 4
for (index in 0x36 until bmpFileBytes.size step 3) {
val blue = toHexString(byteToInt(bmpFileBytes[index + 0]))
val green = toHexString(byteToInt(bmpFileBytes[index + 1]))
val red = toHexString(byteToInt(bmpFileBytes[index + 2]))
print("$blue $green $red | ")
count += 3
if (count == width * 3) {
// 读够一行了,输出一个换行
println()
}
}
运行结果如下:
ff 0 0 | 0 ff ff | ff 0 ff | ff ff 0 |
0 0 0 | ff ff ff | 0 0 ff | 0 ff 0 |
分析这个数据是否正确,需要注意的是,它的行是倒着的,每个像素的三原色也是倒着存的,当然了,在打印的时候,你可以调整一下三原色的输出顺序,以方便观察。如果发现像素数据没问题,那就是添加bmp文件头出问题了,就可以打印一下文件头的数据,看哪里出了问题。
还有更简单的检查办法,我们可以以16进制的方式打开自己生成的文件和原始的文件,对比数据查看,因为数据很少,也很容易找出问题所在。
这个Demo我们就完成了,接下来就可以试一些复杂的图片了,比如,使用截图软件截图一张640 * 480的图片,然后保存为24位的bmp文件,如下:
我们只需要变文件路径即可,如下:
fun createBitmapDemo2() {
val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\海琴烟.bmp"))
createBmpFile(bmpFilePixelBytes, File("C:\\Users\\Even\\Pictures\\demo.bmp"))
}
完整代码如下:
import java.io.*
object BmpUtil {
/** 创建Bitmap的示例:通过读取一个bmp文件的像素,再把这些像素写入一个新的bmp文件 */
fun createBitmapDemo2() {
val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\海琴烟.bmp"))
//printPixelBytes(bmpFilePixelBytes)
createBmpFile(bmpFilePixelBytes, File("C:\\Users\\Even\\Pictures\\demo.bmp"))
}
/** 创建Bitmap的示例:创建一个上一半为红色,下一半为绿色的bmp文件 */
fun createBitmapDemo() {
val width = 300 // 注意:宽高要设置为4的倍数,以避免需要进行补位的操作
val height = 200
val pixelBytes = createPixelBytes(width, height)
//printPixelBytes(pixelBytes)
val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
createBmpFile(pixelBytes, bmpFile)
}
fun readBmpFilePixelBytes(bmpFile: File): Array<ByteArray> {
// 得到bmp文件的所有字节
val bmpFileBytes = bmpFile.readBytes()
// 从bmp文件中获取图像的宽和高的字节数组
val widthLittleEndianBytes = ByteArray(4)
val heightLittleEndianBytes = ByteArray(4)
System.arraycopy(bmpFileBytes, 0x12, widthLittleEndianBytes, 0, 4)
System.arraycopy(bmpFileBytes, 0x16, heightLittleEndianBytes, 0, 4)
// 把小端的字节数组转换为Int
val width = littleEndianBytesToInt(widthLittleEndianBytes)
val height = littleEndianBytesToInt(heightLittleEndianBytes)
println("读取到bmp图像width = $width, height = $height")
val pixelBytes = Array(height) { ByteArray(width * 3) }
var rowIndex = height - 1 // 因为bmp图片是从最后一行开始保存的,读取的时候我们把它往到正确的位置
var columnIndex = 0
var oneLineBytes = pixelBytes[rowIndex]
val oneLineBytesSize = oneLineBytes.size
// 像素值都是从0x36的位置开始保存的,而且每个像素点3个字节
for (i in 0x36 until bmpFileBytes.size step 3) {
if (columnIndex == oneLineBytesSize) {
// 存满一行了,需要换行保存了。这里--rowIndex是因为原图片是从最后一行向前面行的顺序保存的
oneLineBytes = pixelBytes[--rowIndex]
columnIndex = 0
}
// 注意:bmp文件的颜色是按蓝、绿、红的顺序保存的
val blue = bmpFileBytes[i + 0]
val green = bmpFileBytes[i + 1]
val red = bmpFileBytes[i + 2]
oneLineBytes[columnIndex++] = red
oneLineBytes[columnIndex++] = green
oneLineBytes[columnIndex++] = blue
}
return pixelBytes
}
/** 把BigEnding的字节数组转换为int */
private fun littleEndianBytesToInt(littleEndianBytes: ByteArray): Int {
val bigEndianBytes = littleEndianBytes.reversedArray()
val bais = ByteArrayInputStream(bigEndianBytes)
val dis = DataInputStream(bais)
return dis.readInt()
}
/** 创建像素矩阵,注意:宽要设置为4的倍数 */
fun createPixelBytes(width: Int, height: Int) : Array<ByteArray> {
val redColor = 0xFF0000
val greenColor = 0x00FF00
val redBytes = getColorBytes(redColor)
val greenBytes = getColorBytes(greenColor)
val pixelBytes = Array(height) { ByteArray(width * 3) }
for (rowIndex in 0 until height) {
val colorBytes = if (rowIndex < height / 2) redBytes else greenBytes
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = colorBytes[0]
val green = colorBytes[1]
val blue = colorBytes[2]
oneLineBytes[columnIndex + 0] = red
oneLineBytes[columnIndex + 1] = green
oneLineBytes[columnIndex + 2] = blue
}
}
return pixelBytes
}
fun getColorBytes(color: Int): ByteArray {
val red = (color and 0xFF0000 ushr 16).toByte()
val green = (color and 0x00FF00 ushr 8).toByte()
val blue = (color and 0x0000FF).toByte()
val colorBytes = byteArrayOf(red, green, blue)
return colorBytes
}
/** 打印像素值,可打印rgbBytes,也可以打印yuv444Bytes */
fun printPixelBytes(pixelBytes: Array<ByteArray>) {
for (rowIndex in pixelBytes.indices) {
val oneLine = pixelBytes[rowIndex]
for (columnIndex in oneLine.indices step 3) {
// 获取1个像素的3个颜色通道:R、G、B 或 Y、U、V
val colorChannel1 = oneLine[columnIndex + 0]
val colorChannel2 = oneLine[columnIndex + 1]
val colorChannel3 = oneLine[columnIndex + 2]
// 把byte转为int,再以16进制进行输出
val colorChannelInt1 = toHexString(byteToInt(colorChannel1))
val colorChannelInt2 = toHexString(byteToInt(colorChannel2))
val colorChannelInt3 = toHexString(byteToInt(colorChannel3))
// 以16进制进行打印
print("$colorChannelInt1 $colorChannelInt2 $colorChannelInt3| ")
}
println()
}
}
fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
fun toHexString(int: Int): String = Integer.toHexString(int)
/** 根据给定的像素二维数据,按照bmp文件规范保存到指定的bmp文件中 */
fun createBmpFile(pixelBytes: Array<ByteArray>, saveFile: File) {
// 因为一行像素中每个像素是占3个字节的,除以3就得到图像的宽度了
val pixelWidth = pixelBytes[0].size / 3
val pixelHeight = pixelBytes.size
// 每个像素占3个byte,所以要乘以3
val pixelBytesCount = pixelWidth * pixelHeight * 3
// 文件总大小为:像素数据大小 + 头文件大小
val fileBytesCount = pixelBytesCount + 54
// 创建一个byte数组,用于保存bmp文件的所有byte数据
val bmpFileBytes = ByteArray(fileBytesCount)
// 往bmpFileBytes中添加bmp文件头
addBmpFileHeader(pixelWidth, pixelHeight, bmpFileBytes)
// 往bmpFileBytes中添加像素数据
addPixelBytes(pixelBytes, bmpFileBytes)
// 把所有的字节写到文件
saveFile.writeBytes(bmpFileBytes)
}
fun addBmpFileHeader(width: Int, height: Int, bmpFileBytes: ByteArray) {
val pixelBytesCount = width * height * 3
val fileBytesCount = pixelBytesCount + 54
// 424d
bmpFileBytes[0x00] = 0x42
bmpFileBytes[0x01] = 0x4d
// 文件大小
var bytes = getLittleEndianBytes(fileBytesCount)
bmpFileBytes[0x02] = bytes[0]
bmpFileBytes[0x03] = bytes[1]
bmpFileBytes[0x04] = bytes[2]
bmpFileBytes[0x05] = bytes[3]
// 保留数据
bmpFileBytes[0x06] = 0x00
bmpFileBytes[0x07] = 0x00
bmpFileBytes[0x08] = 0x00
bmpFileBytes[0x09] = 0x00
// 像素存储位置
bmpFileBytes[0x0a] = 0x36
bmpFileBytes[0x0b] = 0x00
bmpFileBytes[0x0c] = 0x00
bmpFileBytes[0x0d] = 0x00
// bmp头文件大小
bmpFileBytes[0x0e] = 0x28
bmpFileBytes[0x0f] = 0x00
bmpFileBytes[0x10] = 0x00
bmpFileBytes[0x11] = 0x00
// 图像宽度
bytes = getLittleEndianBytes(width)
bmpFileBytes[0x12] = bytes[0]
bmpFileBytes[0x13] = bytes[1]
bmpFileBytes[0x14] = bytes[2]
bmpFileBytes[0x15] = bytes[3]
// 图像高度
bytes = getLittleEndianBytes(height)
bmpFileBytes[0x16] = bytes[0]
bmpFileBytes[0x17] = bytes[1]
bmpFileBytes[0x18] = bytes[2]
bmpFileBytes[0x19] = bytes[3]
// 色彩平面数
bmpFileBytes[0x1a] = 0x01
bmpFileBytes[0x1b] = 0x00
// 像素位数
bmpFileBytes[0x1c] = 0x18
bmpFileBytes[0x1d] = 0x00
// 压缩方式
bmpFileBytes[0x1e] = 0x00
bmpFileBytes[0x1f] = 0x00
bmpFileBytes[0x20] = 0x00
bmpFileBytes[0x21] = 0x00
// 像素数据大小
bytes = getLittleEndianBytes(pixelBytesCount)
bmpFileBytes[0x22] = bytes[0]
bmpFileBytes[0x23] = bytes[1]
bmpFileBytes[0x24] = bytes[2]
bmpFileBytes[0x25] = bytes[3]
// 横向分辨率
bmpFileBytes[0x26] = 0x00
bmpFileBytes[0x27] = 0x00
bmpFileBytes[0x28] = 0x00
bmpFileBytes[0x29] = 0x00
// 纵向分辨率
bmpFileBytes[0x2a] = 0x00
bmpFileBytes[0x2b] = 0x00
bmpFileBytes[0x2c] = 0x00
bmpFileBytes[0x2d] = 0x00
// 调色板颜色数
bmpFileBytes[0x2e] = 0x00
bmpFileBytes[0x2f] = 0x00
bmpFileBytes[0x30] = 0x00
bmpFileBytes[0x31] = 0x00
// 重要颜色数
bmpFileBytes[0x32] = 0x00
bmpFileBytes[0x33] = 0x00
bmpFileBytes[0x34] = 0x00
bmpFileBytes[0x35] = 0x00
}
/** 把指定的像素数据添加到bmp文件数组中 */
fun addPixelBytes(pixelBytes: Array<ByteArray>, bmpFileBytes: ByteArray) {
val height = pixelBytes.size
var index = 0x36
// 设置像素数据,注意:要从像素的最后一行开始进行存储
for (rowIndex in height - 1 downTo 0) {
val oneLineBytes = pixelBytes[rowIndex]
for (columnIndex in oneLineBytes.indices step 3) {
val red = oneLineBytes[columnIndex + 0]
val green = oneLineBytes[columnIndex + 1]
val blue = oneLineBytes[columnIndex + 2]
// 每个像素的三原色按倒序存储
bmpFileBytes[index++] = blue
bmpFileBytes[index++] = green
bmpFileBytes[index++] = red
}
}
}
/** 把int转换为byte数组,默认是大端方式的数组,返回转换为小端方式的数组 */
fun getLittleEndianBytes(number: Int): ByteArray {
val baos = ByteArrayOutputStream()
val dos = DataOutputStream(baos)
dos.writeInt(number)
val bigEndianBytes = baos.toByteArray()
val littleEndianBytes = bigEndianBytes.reversedArray()
return littleEndianBytes
}
}
这需要用到YUV相关的知识,可参考该文章:https://www.cdsy.xyz/computer/fileABC/230207/cd40322.html,在该文章末尾有YUV与bmp图片互相转换的例子。