YUV420,YUV420P,YUV420SP,YV12,YU12,NV12,NV21

preface

There are many YUV formats. It's really hard to learn at the beginning. The articles on online search are not very clear.

In fact, different YUV formats only have different sampling methods and storage methods. For these two points, different sampling methods are to save memory, and different storage methods are not useful for the time being.

RGB to YUV444

Let's assume that there is a picture with a width of 4 pixels and a height of 2 pixels, then the picture has a total of 8 pixels. I use a grid to represent a pixel, and 8 pixels are 8 grids, as shown in the following figure:

As shown in the figure above, there are 8 grids representing 8 pixels, and 0 ~ 7 represent the position of pixels. In fact, a pixel is very small. A pixel on the computer is almost invisible to the naked eye, so I use a large grid to represent a pixel for easy understanding. The pixels in the computer are composed of three primary colors of RGB. Here I label RGB into the pixels as follows:

Each R is stored with 1 byte, G is stored with 1 byte, and B is also stored with 1 byte. Therefore, RGB in one pixel needs 3 bytes to be stored, while 8 pixels in the above figure, that is, 8 pairs of RGB values, need 24 bytes to be stored.

RGB can be converted into YUV through mathematical formula. As for the principle of conversion, we don't need to understand it. Just know that it can be converted into each other through formula. Convert the RGB in the above figure into YUV format, as follows:

Y needs 1 byte to store, U needs 1 byte to store, and V also needs 1 byte to store. Therefore, YUV pictures with 8 pixels also need 24 bytes to store, just like RGB. It feels like YUV has no advantages. Don't worry. You'll know what's good later.

Acquisition mode of YV12 (YUV444 101(YV12)

The YUV data after RGB conversion is called YUV444. The YUV sampling in this format is complete without losing any accuracy. The YUV image data we get from the camera can never be in YUV444 format, but in other YUV formats, which have lost accuracy, such as YV12. In Android system, the data collected by the camera can be set to YV12 format (or NV21 format). YV12 is a YUV format that has lost accuracy. Y will not be lost during sampling, When collecting U, take one every other and one row every other. When collecting V, take one every other and one row every other. Assuming that a picture is 6 pixels (6 rows), the acquisition method of YV12 format is as follows:

The first line: collect all Y, collect U, collect one every other, and do not collect V
The second line: Y is collected, V is collected, one is collected every other, and U is not collected
The third line: collect all Y, collect U, collect one every other, and do not collect V
The fourth line: Y is collected, V is collected, one is collected every other, and U is not collected
The fifth line: Y is collected, U is collected, one is collected every other, and V is not collected
The sixth line: Y is collected, V is collected, one is collected every other, and U is not collected

It should be easy to understand that u and V are lost, so the required storage space will become less. YV12 saves half the storage space compared with YUV444. This is why the data from the camera is in YUV format rather than RGB format. Because the required storage space is small, and although some U and V are lost, the image quality almost does not decline with the naked eye.

According to the sampling method of YV12 format, we sample the previous pictures of YUV444 into YV12 format, as follows:

The first line: Y is collected, U is collected, one is collected every other, and V is not collected, as follows:

The second line: Y is collected, V is collected, one is collected every other, and U is not collected, as follows:

Taken together, the two lines are as follows:

It can also be seen from the above figure that the original YVU444 has 24 Y, U and V related data, which needs 24 bytes to save. After being collected into YV12, there are only 12 Y, U and V data, which only needs 12 bytes to save, which is half of the original storage. This is why the data collected by the camera is generally in YUV format.

Storage mode of YV12

The storage method of YV12 is to save Y, U and V respectively. First save Y, then save V, and then save U, as follows:

YV12 stores Y, U and V separately in this way. The professional term is divided into three planes. It seems that it needs to be stored in three arrays. In fact, the YUV data collected by the camera and transmitted to us is a one-dimensional array, not a two-dimensional array, as follows:

Therefore, we should not think that YV12 is divided into three planes and is stored in three arrays. YV12 from the camera is a one-dimensional array. When saving a file, we can directly save this one-dimensional array. Of course, in the code, in order to facilitate the operation of the YV12 data, you can convert the one-dimensional array into three arrays holding Y, U and V respectively.

At this point, we'll learn about another YUV format: YU12 (also known as I420), which is very similar to YV12, except that YU12 stores U first and then V, as follows:

YV12 restore (YV12 101(YUV444)

The data sampled through YV12 is missing some U and V. how to restore it? As follows:

As can be seen from the above figure, the restore method of YV12 is: every four adjacent Y in every two rows share a set of UVs. When YUV is converted back to RGB, it must be different from the previous original RGB, but in practical effect, we can hardly see the difference with our naked eyes (you can see the difference only by looking at one color, but you can't see the difference if you look at an overall picture).

According to the principle of restoring YV12 back to YUV444, we can know that the width and height of the image should be even, so when we practice, when setting the width and height of the image, we should not make an odd width and height to avoid abnormalities! We are looking at the resolution settings of some mobile phones or cameras, which are even, and there is no odd number.

Simulate YUV444 ➜ YV12 ➜ YUV444 through code

1. Illustration

As shown in the figure above, we will simulate the data collection and storage process from YUV444 to YV12 through code, and then restore it back to YUV444. For the convenience of operating YUV data later, we use three arrays instead of one array to store YV12 data.

2. Simulate YUV444 data


YUV444 data as shown in the figure above is simulated by code, as follows:

fun main() {
    val yuv444Bytes = arrayOf(
        arrayOf("Y0", "U0", "V0", "Y1", "U1", "V1", "Y2", "U2", "V2", "Y3", "U3", "V3"),
        arrayOf("Y4", "U4", "V4", "Y5", "U5", "V5", "Y6", "U6", "V6", "Y7", "U7", "V7")
    )
    printYUV444(yuv444Bytes)
}

fun printYUV444(yuv444Bytes: Array<Array<String>>) {
    println("Output below YUV444 data")
    for (oneLine in yuv444Bytes) {
        for (columnIndex in oneLine.indices step 3) {
            val y = oneLine[columnIndex + 0]
            val u = oneLine[columnIndex + 1]
            val v = oneLine[columnIndex + 2]
            print("$y $u $v | ")
        }
        println()
    }
}

The operation results are as follows:

Output below YUV444 data
Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 | 
Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 | 

Here, we use a two-dimensional string array to represent the YUV444 data of a picture with a width of 4 and a height of 2. The string is used to simulate YUV data to facilitate you to view the results. Next, we need to collect YV12 data from YUV444.

3. YV12 data is collected from YUV444


As shown in the figure above, we want to collect YV12 data from YUV444 data. The code is as follows:

fun main() {
    val yuv444Bytes = arrayOf(
        arrayOf("Y0", "U0", "V0", "Y1", "U1", "V1", "Y2", "U2", "V2", "Y3", "U3", "V3"),
        arrayOf("Y4", "U4", "V4", "Y5", "U5", "V5", "Y6", "U6", "V6", "Y7", "U7", "V7")
    )
    printYUV444(yuv444Bytes)

    val (yBytes, uBytes, vBytes) = yuv444ToYv12(yuv444Bytes)
    printYV12(yBytes, uBytes, vBytes)
}

fun printYUV444(yuv444Bytes: Array<Array<String>>) {
    println("Output below YUV444 data")
    for (oneLine in yuv444Bytes) {
        for (columnIndex in oneLine.indices step 3) {
            val y = oneLine[columnIndex + 0]
            val u = oneLine[columnIndex + 1]
            val v = oneLine[columnIndex + 2]
            print("$y $u $v | ")
        }
        println()
    }
}

fun printYV12(yBytes: Array<String>, uBytes: Array<String>, vBytes: Array<String>) {
    println("Output below YV12 data")
    yBytes.forEach { print("$it ") }
    println()
    vBytes.forEach { print("$it ") }
    println()
    uBytes.forEach { print("$it ") }
    println()
}

private fun yuv444ToYv12(yuv444Bytes: Array<Array<String>>): Triple<Array<String>, Array<String>, Array<String>> {
    val width = yuv444Bytes[0].size
    val height = yuv444Bytes.size
    val ySize = width * height
    val vSize = ySize / 4
    val yBytes = Array(ySize) { "" }
    val uBytes = Array(vSize) { "" }
    val vBytes = Array(vSize) { "" }
    var yIndex = 0
    var uIndex = 0
    var vIndex = 0
    var saveU = true
    var saveV = true
    for (rowIndex in 0 until height) {
        val oneLine = yuv444Bytes[rowIndex]
        for (columnIndex in oneLine.indices step 3) {
            val y = oneLine[columnIndex + 0]
            val u = oneLine[columnIndex + 1]
            val v = oneLine[columnIndex + 2]
            yBytes[yIndex++] = y
            if (rowIndex % 2 == 0) {
                // Take U for even rows and take one every other
                if (saveU) {
                    uBytes[uIndex++] = u
                }
                saveU = !saveU
            } else {
                // V is taken for singular lines, one after another
                if (saveV) {
                    vBytes[vIndex++] = v
                }
                saveV = !saveV
            }
        }
    }
    return Triple(yBytes, uBytes, vBytes)
}

The operation effect is as follows:

Output below YUV444 data
Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 | 
Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 | 
Output below YV12 data
Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7                 
V4 V6     
U0 U2 

4. Restore YV12 to YUV444


The implementation code is as follows:

fun main() {
    val yuv444Bytes = arrayOf(
        arrayOf("Y0", "U0", "V0", "Y1", "U1", "V1", "Y2", "U2", "V2", "Y3", "U3", "V3"),
        arrayOf("Y4", "U4", "V4", "Y5", "U5", "V5", "Y6", "U6", "V6", "Y7", "U7", "V7")
    )
    printYUV444(yuv444Bytes)

    val (yBytes, uBytes, vBytes) = yuv444ToYv12(yuv444Bytes)
    printYV12(yBytes, uBytes, vBytes)

    val width = yuv444Bytes[0].size
    val height = yuv444Bytes.size
    val yuv444 = yv12ToYuv444(width, height, yBytes, uBytes, vBytes)
    printYUV444(yuv444)
}

fun yv12ToYuv444(width: Int, height: Int, yBytes: Array<String>, uBytes: Array<String>, vBytes: Array<String>): Array<Array<String>> {
    var yIndex = 0
    val yuv444Bytes = Array(height) { Array(width) { " " } }
    val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
    var twoLineIndex = -1  // Count per two lines
    for (rowIndex in 0 until height) {
        val oneLineBytes = yuv444Bytes[rowIndex]
        var u = ""
        var v = ""

        // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
        if (rowIndex % 2 == 0) {
            twoLineIndex++
        }

        // Calculate the starting position of uvIndex when taking UV for each row
        var uvIndex = twoLineIndex * oneLineUvSize

        for (columnIndex in oneLineBytes.indices step 3) {
            if (yIndex % 2 == 0) {
                // UV s are taken only once for every two Y's in a row
                u = uBytes[uvIndex]
                v = vBytes[uvIndex]
                uvIndex++
            }

            val y = yBytes[yIndex++]
            oneLineBytes[columnIndex + 0] = y
            oneLineBytes[columnIndex + 1] = u
            oneLineBytes[columnIndex + 2] = v
        }
    }

    return yuv444Bytes
}

The operation results are as follows:

Output below YUV444 data
Y0 U0 V0 | Y1 U1 V1 | Y2 U2 V2 | Y3 U3 V3 | 
Y4 U4 V4 | Y5 U5 V5 | Y6 U6 V6 | Y7 U7 V7 | 
Output below YV12 data
Y0 Y1 Y2 Y3 Y4 Y5 Y6 Y7                 
V4 V6     
U0 U2     
Output below YUV444 data
Y0 U0 V4 | Y1 U0 V4 | Y2 U2 V6 | Y3 U2 V6 | 
Y4 U0 V4 | Y5 U0 V4 | Y6 U2 V6 | Y7 U2 V6 | 

The results can be compared with the previous screenshot:

It can be seen that four Y share a set of UVs. In this way, the YUV value of each pixel is not the original YUV value, so the color effect must be biased. However, from the perspective of the whole picture, the naked eye can hardly see the difference. As mentioned earlier, we need to be clear about this.

YUV444 to YV12 are relatively simple, but the logic of restoring YV12 to YUV444 is quite complex, so here are some details:

As shown in the above figure, in a row, every two Y share a set of UVs, so you can only read UVs once for every two y. let's observe the index of each element of the array storing y, as follows:

That is, the UV is read every two y. it is found from the above figure that it is actually read when the index of Y is even, that is, when it is 0, 2, 4 and 6. Therefore, the implementation code is as follows:

if (yIndex % 2 == 0) {
    u = uBytes[uvIndex]
    v = vBytes[uvIndex]
    uvIndex++
}

It is easier to read UVs every two Y. what is more difficult is how to get UVs in the second, third and fourth rows? This is really a difficult problem. It belongs to a logical problem. To find its law:

  1. The first row takes as like as two peas of UV and second lines, which are read from 0, so let their uvIndex remain the same when reading the first line and the second row. (UV)
  2. The UV in the third row is the same as that in the fourth row. Different from the first and second rows, the starting position of uvIndex does not start from 0.

Therefore, the difficulty is how to find out the starting position of uvIndex. In order to find out the law, we need more data. Assuming that the width is 8 pixels and the height is 6 pixels, there will be 6 x 8 = 48 pixels and 48 Y. we know that every 4 Y corresponds to a U and V, then 48 / 4 = 12, that is, there will be 12 u and 12 v. the drawing analysis is as follows:

As shown in the figure above, it is the formula for analyzing and calculating the starting position when reading UV in each line. The corresponding implementation code is as follows:

. . . 
fun yv12ToYuv444(. . . ): Array<Array<String>> {
    . . . 
    val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
    var twoLineIndex = -1  // Count per two lines
    for (rowIndex in 0 until height) {
        . . . 
        // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
        if (rowIndex % 2 == 0) {
            twoLineIndex++
        }

        // Calculate the starting position of uvIndex for each row when reading UV s
        var uvIndex = twoLineIndex * oneLineUvSize
        . . . 
    }

    return yuv444Bytes
}

5. Complete code

fun main() {
    val yuv444Bytes = arrayOf(
        arrayOf("Y0", "U0", "V0", "Y1", "U1", "V1", "Y2", "U2", "V2", "Y3", "U3", "V3"),
        arrayOf("Y4", "U4", "V4", "Y5", "U5", "V5", "Y6", "U6", "V6", "Y7", "U7", "V7")
    )
    printYUV444(yuv444Bytes)

    val (yBytes, uBytes, vBytes) = yuv444ToYv12(yuv444Bytes)
    printYV12(yBytes, uBytes, vBytes)

    val width = yuv444Bytes[0].size
    val height = yuv444Bytes.size
    val yuv444 = yv12ToYuv444(width, height, yBytes, uBytes, vBytes)
    printYUV444(yuv444)
}

fun yv12ToYuv444(width: Int, height: Int, yBytes: Array<String>, uBytes: Array<String>, vBytes: Array<String>): Array<Array<String>> {
    var yIndex = 0
    val yuv444Bytes = Array(height) { Array(width) { " " } }
    val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
    var twoLineIndex = -1  // Count per two lines
    for (rowIndex in 0 until height) {
        val oneLineBytes = yuv444Bytes[rowIndex]
        var u = ""
        var v = ""

        // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
        if (rowIndex % 2 == 0) {
            twoLineIndex++
        }

        // Calculate the starting position of uvIndex when taking UV for each row
        var uvIndex = twoLineIndex * oneLineUvSize

        for (columnIndex in oneLineBytes.indices step 3) {
            if (yIndex % 2 == 0) {
                // UV s are taken only once for every two Y's in a row
                u = uBytes[uvIndex]
                v = vBytes[uvIndex]
                uvIndex++
            }

            val y = yBytes[yIndex++]
            oneLineBytes[columnIndex + 0] = y
            oneLineBytes[columnIndex + 1] = u
            oneLineBytes[columnIndex + 2] = v
        }
    }

    return yuv444Bytes
}

fun printYUV444(yuv444Bytes: Array<Array<String>>) {
    println("Output below YUV444 data")
    for (oneLine in yuv444Bytes) {
        for (columnIndex in oneLine.indices step 3) {
            val y = oneLine[columnIndex + 0]
            val u = oneLine[columnIndex + 1]
            val v = oneLine[columnIndex + 2]
            print("$y $u $v | ")
        }
        println()
    }
}

fun printYV12(yBytes: Array<String>, uBytes: Array<String>, vBytes: Array<String>) {
    println("Output below YV12 data")
    yBytes.forEach { print("$it ") }
    println()
    vBytes.forEach { print("$it ") }
    println()
    uBytes.forEach { print("$it ") }
    println()
}

fun yuv444ToYv12(yuv444Bytes: Array<Array<String>>): Triple<Array<String>, Array<String>, Array<String>> {
    val width = yuv444Bytes[0].size
    val height = yuv444Bytes.size
    val ySize = width * height
    val vSize = ySize / 4
    val yBytes = Array(ySize) { "" }
    val uBytes = Array(vSize) { "" }
    val vBytes = Array(vSize) { "" }
    var yIndex = 0
    var uIndex = 0
    var vIndex = 0
    var saveU = true
    var saveV = true
    for (rowIndex in 0 until height) {
        val oneLine = yuv444Bytes[rowIndex]
        for (columnIndex in oneLine.indices step 3) {
            val y = oneLine[columnIndex + 0]
            val u = oneLine[columnIndex + 1]
            val v = oneLine[columnIndex + 2]
            yBytes[yIndex++] = y
            if (rowIndex % 2 == 0) {
                // Take U for even rows and take one every other
                if (saveU) {
                    uBytes[uIndex++] = u
                }
                saveU = !saveU
            } else {
                // V is taken for singular lines, one after another
                if (saveV) {
                    vBytes[vIndex++] = v
                }
                saveV = !saveV
            }
        }
    }
    return Triple(yBytes, uBytes, vBytes)
}

6. Complete code (replace String with byte)

Next, we modify the code of yuv444 to YV12 and YV12 to yuv444 to use byte and package it into YuvUtil to facilitate reuse:

object YuvUtil {

    fun yv12ToYuv444(width: Int, height: Int, yBytes: Array<Byte>, uBytes: Array<Byte>, vBytes: Array<Byte>): Array<ByteArray> {
        var yIndex = 0
        val yuv444Bytes = Array(height) { ByteArray(width * 3) }
        val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
        var twoLineIndex = -1  // Count per two lines
        for (rowIndex in 0 until height) {
            val oneLineBytes = yuv444Bytes[rowIndex]
            var u: Byte = 0
            var v: Byte = 0

            // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
            if (rowIndex % 2 == 0) {
                twoLineIndex++
            }

            // Calculate the starting position of uvIndex when taking UV for each row
            var uvIndex = twoLineIndex * oneLineUvSize

            for (columnIndex in oneLineBytes.indices step 3) {
                if (yIndex % 2 == 0) {
                    // UV s are taken only once for every two Y's in a row
                    u = uBytes[uvIndex]
                    v = vBytes[uvIndex]
                    uvIndex++
                }

                val y = yBytes[yIndex++]
                oneLineBytes[columnIndex + 0] = y
                oneLineBytes[columnIndex + 1] = u
                oneLineBytes[columnIndex + 2] = v
            }
        }

        return yuv444Bytes
    }

    private fun yuv444ToYv12(yuv444Bytes: Array<ByteArray>): Triple<ByteArray, ByteArray, ByteArray> {
        val width = yuv444Bytes[0].size
        val height = yuv444Bytes.size
        val ySize = width * height
        val vSize = ySize / 4
        val yBytes = ByteArray(ySize)
        val uBytes = ByteArray(vSize)
        val vBytes = ByteArray(vSize)
        var yIndex = 0
        var uIndex = 0
        var vIndex = 0
        var saveU = true
        var saveV = true
        for (rowIndex in 0 until height) {
            val oneLine = yuv444Bytes[rowIndex]
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                yBytes[yIndex++] = y
                if (rowIndex % 2 == 0) {
                    // Take U for even rows and take one every other
                    if (saveU) {
                        uBytes[uIndex++] = u
                    }
                    saveU = !saveU
                } else {
                    // V is taken for singular lines, one after another
                    if (saveV) {
                        vBytes[vIndex++] = v
                    }
                    saveV = !saveV
                }
            }
        }
        return Triple(yBytes, uBytes, vBytes)
    }

    fun printYUV444(yuv444Bytes: Array<ByteArray>) {
        println("Output below YUV444 data")
        for (oneLine in yuv444Bytes) {
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                print("$y $u $v | ")
            }
            println()
        }
    }

    fun printYV12(yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray) {
        // Print in hexadecimal
        println("Output below YV12 data")
        println("Output below Y data")
        yBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below V data")
        vBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below U data")
        uBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println()
    }

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun toHexString(int: Int): String = Integer.toHexString(int)

}

YV12 pictures and bmp pictures interact with each other

For knowledge about bmp pictures, please refer to this document: https://blog.csdn.net/android_cai_niao/article/details/120528734

The conversion between YV12 and bmp is simply the conversion between YUV and rgb. Find the conversion formula. There are many conversion formulas on the Internet. I don't know which of these conversion formulas is reliable, because there are too many knowledge points in them. Different color spaces have different conversion formulas. I convert a YUV picture from the mobile camera into a bmp picture and check it under the computer. The color is almost the same. I think it is the correct formula, I'm too lazy to think about what color space it is.

RGB to YUV formula

  • Y = 0.299 * R + 0.587 * G + 0.114 * B
  • U = -0.169 * R - 0.331 * G + 0.499 * B + 128
  • V = 0.499 * R - 0.418 * G - 0.0813 * B + 128

It should be noted that the range of R, G and B is 0 ~ 255, and exactly one byte can represent that when we read RGB from memory, it is also byte type data. However, when participating in the conversion formula, we should note that byte in java is signed, and a byte has 8 bits. If all are 1, it is - 1 in byte, and if it is 255 in int, So we need to convert byte to a positive int value, otherwise the calculation formula will not work. It should also be noted that the byte.toInt() function is still - 1 after a byte of - 1 is converted to int, so it should be noted that we need to take the lowest 8 bits of int, then change the high bits to 0, and then become a positive number. In addition, the range of Y, U and V values calculated by the conversion formula is also 0 ~ 255, which needs to be processed beyond the range.

The corresponding Kotlin implementation code is as follows:

object YuvUtil {   

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun verify(int: Int) = if (int < 0) 0 else if (int > 255) 255 else int
    fun toHexString(int: Int): String = Integer.toHexString(int)

    fun rgbToYuv(R: Byte, G: Byte, B: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of R, G and B values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return rgbToYuv(byteToInt(R), byteToInt(G), byteToInt(B))
    }

    fun rgbToYuv(R: Int, G: Int, B: Int): Triple<Byte, Byte, Byte> {
        var Y = (0.299 * R + 0.587 * G + 0.114 * B).toInt()
        var U = (-0.169 * R - 0.331 * G + 0.499 * B + 128).toInt()
        var V = (0.499 * R - 0.418 * G - 0.0813 * B + 128).toInt()
        Y = verify(Y)
        U = verify(U)
        V = verify(V)
        println("rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)} -> yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)}")
        return Triple(Y.toByte(), U.toByte(), V.toByte())
    }
}

YUV to RGB formula

  • R = Y + 1.4075 * (V - 128)
  • G = Y - 0.3455 * (U - 128) - 0.7169 * (V - 128)
  • B = Y + 1.7790 * (U - 128)

Here, we also need to pay attention to the processing of byte data before participating in formula calculation and the out of range processing of calculation results.

The corresponding Kotlin implementation code is as follows:

object YuvUtil {    

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun verify(int: Int) = if (int < 0) 0 else if (int > 255) 255 else int
    fun toHexString(int: Int): String = Integer.toHexString(int)    

    fun yuvToRgb(Y: Byte, U: Byte, V: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of Y, U and V values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return yuvToRgb(byteToInt(Y), byteToInt(U), byteToInt(V))
    }

    fun yuvToRgb(Y: Int, U: Int, V: Int): Triple<Byte, Byte, Byte> {
        var R = (Y + 1.4075 * (V - 128)).toInt()
        var G = (Y - 0.3455 * (U - 128) - 0.7169 * (V - 128)).toInt()
        var B = (Y + 1.779 * (U - 128)).toInt()
        R = verify(R)
        G = verify(G)
        B = verify(B)
        println("yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)} -> rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)}")
        return Triple(R.toByte(), G.toByte(), B.toByte())
    }

}

RGB and YUV mutual rotation deviation

RGB and YUV cannot be perfectly converted to each other, that is, after RGB is converted to YUV, it may deviate from the original RGB when it is converted back to GRB. Examples are as follows:

fun main() {
    val (Y, U, V) = YuvUtil.rgbToYuv(0xff, 0, 0)
    val (R, G, B) = YuvUtil.yuvToRgb(Y, U, V)
}

The operation results are as follows:

rgb: ff 0 0 -> yuv: 4c 54 ff
yuv: 4c 54 ff -> rgb: fe 0 0

It can be seen from the results that the initial RGB is 0xff0000, converted to YUV is 0x4c54ff, and then converted back to RGB is 0xfe0000. The original RGB is different, but it is very close, that is, it is all red. The initial red and the converted red are almost invisible to the naked eye

bmp picture to YUV picture

object YuvUtil {

    fun bmpFileToYV12FileDemo() {
        val grbBytes = BmpUtil.createRgbBytes(4, 2)
        println("Output below RGB Pixel data:")
        BmpUtil.printColorBytes(grbBytes)
        val yuv444Bytes = rgbBytesToYuv444Bytes(grbBytes)
        println("Output below YUV Pixel data:")
        BmpUtil.printColorBytes(yuv444Bytes)
        val (yBytes, uBytes, vBytes) = yuv444BytesToYv12Bytes(yuv444Bytes)
        printYV12(yBytes, uBytes, vBytes)
        val yv12File = File("C:\\Users\\Even\\Pictures\\demo.yuv")
        writeYv12BytesToFile(yv12File, yBytes, vBytes, uBytes)
    }

    fun bmpFileToYV12FileDemo2() {
        val bmpFile = File("C:\\Users\\Even\\Pictures\\Haiqin smoke.bmp")
        val yv12File = File("C:\\Users\\Even\\Pictures\\Haiqin smoke.yuv")
        val rgbBytes = BmpUtil.readBmpFilePixelBytes(bmpFile)
        val yuv444Bytes = rgbBytesToYuv444Bytes(rgbBytes)
        val (yBytes, uBytes, vBytes) = yuv444BytesToYv12Bytes(yuv444Bytes)
        writeYv12BytesToFile(yv12File, yBytes, vBytes, uBytes)
    }

    private fun writeYv12BytesToFile(yv12File: File, yBytes: ByteArray, vBytes: ByteArray, uBytes: ByteArray) {
        FileOutputStream(yv12File).use { fos ->
            BufferedOutputStream(fos).use { bos ->
                bos.write(yBytes)
                bos.write(vBytes)
                bos.write(uBytes)
            }
        }
    }

    fun rgbBytesToYuv444Bytes(rgbBytes: Array<ByteArray>): Array<ByteArray> {
        val yuv444Bytes = Array(rgbBytes.size) { ByteArray(rgbBytes[0].size) }
        for (rowIndex in rgbBytes.indices) {
            val oneLineBytes = rgbBytes[rowIndex]
            val oneLineYuv444Bytes = yuv444Bytes[rowIndex]
            for (columnIndex in oneLineBytes.indices step 3) {
                val red   = oneLineBytes[columnIndex + 0]
                val green = oneLineBytes[columnIndex + 1]
                val blue  = oneLineBytes[columnIndex + 2]
                val (Y, U, V) = rgbToYuv(red, green, blue)
                oneLineYuv444Bytes[columnIndex + 0] = Y
                oneLineYuv444Bytes[columnIndex + 1] = U
                oneLineYuv444Bytes[columnIndex + 2] = V
            }
        }
        return yuv444Bytes
    }

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun verify(int: Int) = if (int < 0) 0 else if (int > 255) 255 else int
    fun toHexString(int: Int): String = Integer.toHexString(int)

    fun rgbToYuv(R: Byte, G: Byte, B: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of R, G and B values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return rgbToYuv(byteToInt(R), byteToInt(G), byteToInt(B))
    }

    fun rgbToYuv(R: Int, G: Int, B: Int): Triple<Byte, Byte, Byte> {
        var Y = (0.299 * R + 0.587 * G + 0.114 * B).toInt()
        var U = (-0.169 * R - 0.331 * G + 0.499 * B + 128).toInt()
        var V = (0.499 * R - 0.418 * G - 0.0813 * B + 128).toInt()
        Y = verify(Y)
        U = verify(U)
        V = verify(V)
        //println("rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)} -> yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)}")
        return Triple(Y.toByte(), U.toByte(), V.toByte())
    }

    fun yuvToRgb(Y: Byte, U: Byte, V: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of Y, U and V values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return yuvToRgb(byteToInt(Y), byteToInt(U), byteToInt(V))
    }

    fun yuvToRgb(Y: Int, U: Int, V: Int): Triple<Byte, Byte, Byte> {
        var R = (Y + 1.4075 * (V - 128)).toInt()
        var G = (Y - 0.3455 * (U - 128) - 0.7169 * (V - 128)).toInt()
        var B = (Y + 1.779 * (U - 128)).toInt()
        R = verify(R)
        G = verify(G)
        B = verify(B)
        //println("yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)} -> rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)}")
        return Triple(R.toByte(), G.toByte(), B.toByte())
    }



    fun yv12BytesToYuv444Bytes(width: Int, height: Int, yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray): Array<ByteArray> {
        var yIndex = 0
        val yuv444Bytes = Array(height) { ByteArray(width * 3) }
        val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
        var twoLineIndex = -1  // Count per two lines
        for (rowIndex in 0 until height) {
            val oneLineBytes = yuv444Bytes[rowIndex]
            var u: Byte = 0
            var v: Byte = 0

            // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
            if (rowIndex % 2 == 0) {
                twoLineIndex++
            }

            // Calculate the starting position of uvIndex when taking UV for each row
            var uvIndex = twoLineIndex * oneLineUvSize

            for (columnIndex in oneLineBytes.indices step 3) {
                if (yIndex % 2 == 0) {
                    // UV s are taken only once for every two Y's in a row
                    u = uBytes[uvIndex]
                    v = vBytes[uvIndex]
                    uvIndex++
                }

                val y = yBytes[yIndex++]
                oneLineBytes[columnIndex + 0] = y
                oneLineBytes[columnIndex + 1] = u
                oneLineBytes[columnIndex + 2] = v
            }
        }

        return yuv444Bytes
    }

    private fun yuv444BytesToYv12Bytes(yuv444Bytes: Array<ByteArray>): Triple<ByteArray, ByteArray, ByteArray> {
        val width = yuv444Bytes[0].size / 3  // Each pixel takes up 3 bytes, so divide by 3
        val height = yuv444Bytes.size
        val ySize = width * height
        val vSize = ySize / 4
        val yBytes = ByteArray(ySize)
        val uBytes = ByteArray(vSize)
        val vBytes = ByteArray(vSize)
        var yIndex = 0
        var uIndex = 0
        var vIndex = 0
        var saveU = true
        var saveV = true
        for (rowIndex in 0 until height) {
            val oneLine = yuv444Bytes[rowIndex]
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                yBytes[yIndex++] = y
                if (rowIndex % 2 == 0) {
                    // Take U for even rows and take one every other
                    if (saveU) {
                        uBytes[uIndex++] = u
                    }
                    saveU = !saveU
                } else {
                    // V is taken for singular lines, one after another
                    if (saveV) {
                        vBytes[vIndex++] = v
                    }
                    saveV = !saveV
                }
            }
        }
        return Triple(yBytes, uBytes, vBytes)
    }

    fun printYUV444(yuv444Bytes: Array<ByteArray>) {
        println("Output below YUV444 data")
        for (oneLine in yuv444Bytes) {
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                print("$y $u $v | ")
            }
            println()
        }
    }

    fun printYV12(yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray) {
        // Print in hexadecimal
        println("Output below YV12 data")
        println("Output below Y data")
        yBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below V data")
        vBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below U data")
        uBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println()
    }

}
import java.io.*

object BmpUtil {

    /** An example of creating a Bitmap: read the pixels of a bmp file, and then write these pixels to a new bmp file */
    fun createBitmapDemo2() {
        val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\Haiqin smoke.bmp"))
        //printPixelBytes(bmpFilePixelBytes)
        createBmpFile(bmpFilePixelBytes, File("C:\\Users\\Even\\Pictures\\demo.bmp"))
    }

    /** Example of creating a Bitmap: create a bmp file with the top half red and the bottom half green */
    fun createBitmapDemo() {
        val width = 300 // Note: the width and height should be set as a multiple of 4 to avoid the need for filling
        val height = 200
        val pixelBytes = createRgbBytes(width, height)
        //printPixelBytes(pixelBytes)
        val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
        createBmpFile(pixelBytes, bmpFile)
    }

    fun readBmpFilePixelBytes(bmpFile: File): Array<ByteArray> {
        // Get all bytes of bmp file
        val bmpFileBytes = bmpFile.readBytes()

        // Get the byte array of the width and height of the image from the bmp file
        val widthBigEndianBytes = ByteArray(4)
        val heightBigEndianBytes = ByteArray(4)
        System.arraycopy(bmpFileBytes, 0x12, widthBigEndianBytes, 0, 4)
        System.arraycopy(bmpFileBytes, 0x16, heightBigEndianBytes, 0, 4)

        // Convert the large byte array to Int
        val width = bigEndianBytesToInt(widthBigEndianBytes)
        val height = bigEndianBytesToInt(heightBigEndianBytes)
        println("Read bmp image width = $width, height = $height")
        val pixelBytes = Array(height) { ByteArray(width * 3) }
        var rowIndex = height - 1 // Because the bmp image is saved from the last line, we move it to the correct position when reading
        var columnIndex = 0
        var oneLineBytes = pixelBytes[rowIndex]
        val oneLineBytesSize = oneLineBytes.size
        // Pixel values are saved from the position of 0x36, and each pixel is 3 bytes
        for (i in 0x36 until bmpFileBytes.size step 3) {
            if (columnIndex == oneLineBytesSize) {
                // There is a full line, which needs to be saved in a new line. Here -- rowIndex is because the original image is saved from the last row to the front row
                oneLineBytes = pixelBytes[--rowIndex]
                columnIndex = 0
            }

            // Note: the colors of bmp files are saved in the order of blue, green and red
            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
    }

    /** Convert the byte array of BigEnding to int */
    private fun bigEndianBytesToInt(bigEndianBytes: ByteArray): Int {
        val littleEndianBytes = bigEndianBytes.reversedArray()
        val bais = ByteArrayInputStream(littleEndianBytes)
        val dis = DataInputStream(bais)
        return dis.readInt()
    }

    /** Create a pixel matrix. Note: the width should be set to a multiple of 4 */
    fun createRgbBytes(width: Int, height: Int) : Array<ByteArray> {
        val redColor   = 0xFF0000
        val greenColor = 0x00FF00
        val redBytes   = getColorBytes(redColor)
        val greenBytes = getColorBytes(greenColor)
        val rgbBytes = Array(height) { ByteArray(width * 3) }
        for (rowIndex in 0 until height) {
            val colorBytes = if (rowIndex < height / 2) redBytes else greenBytes
            val oneLineBytes = rgbBytes[rowIndex]
            for (columnIndex in oneLineBytes.indices step 3) {
                val red   = colorBytes[0x00]
                val green = colorBytes[0x01]
                val blue  = colorBytes[0x02]
                oneLineBytes[columnIndex + 0] = red
                oneLineBytes[columnIndex + 1] = green
                oneLineBytes[columnIndex + 2] = blue
            }
        }
        return rgbBytes
    }

    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
    }

    /** Print color values. You can print rgb color values or yuv444 color values */
    fun printColorBytes(pixelBytes: Array<ByteArray>) {
        for (rowIndex in pixelBytes.indices) {
            val oneLine = pixelBytes[rowIndex]
            for (columnIndex in oneLine.indices step 3) {
                // Get 3 color channels of 1 pixel: R, G, B or Y, U, V
                val colorChannel1 =   oneLine[columnIndex + 0]
                val colorChannel2 =  oneLine[columnIndex + 1]
                val colorChannel3 = oneLine[columnIndex + 2]

                // Convert byte to int, and then output it in hexadecimal
                val colorChannelInt1 = toHexString(byteToInt(colorChannel1))
                val colorChannelInt2 = toHexString(byteToInt(colorChannel2))
                val colorChannelInt3 = toHexString(byteToInt(colorChannel3))

                // Print in hexadecimal
                print("$colorChannelInt1 $colorChannelInt2 $colorChannelInt3| ")
            }
            println()
        }
    }

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun toHexString(int: Int): String = Integer.toHexString(int)

    /** According to the given two-dimensional pixel data, it is saved to the specified bmp file according to the bmp file specification */
    fun createBmpFile(pixelBytes: Array<ByteArray>, saveFile: File) {
        // Because each pixel in a row occupies 3 bytes, divide by 3 to get the width of the image
        val pixelWidth = pixelBytes[0].size / 3
        val pixelHeight = pixelBytes.size
        // Each pixel takes up 3 byte s, so multiply by 3
        val pixelBytesCount = pixelWidth * pixelHeight * 3
        // The total file size is: pixel data size + header file size
        val fileBytesCount = pixelBytesCount + 54
        // Create a byte array to save all byte data of bmp file
        val bmpFileBytes = ByteArray(fileBytesCount)
        // Add bmp file header to bmp filebytes
        addBmpFileHeader(pixelWidth, pixelHeight, bmpFileBytes)
        // Add pixel data to BMP filebytes
        addPixelBytes(pixelBytes, bmpFileBytes)
        // Write all bytes to the file
        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

        // file size
        var bytes = getBigEndianBytes(fileBytesCount)
        bmpFileBytes[0x02] = bytes[0]
        bmpFileBytes[0x03] = bytes[1]
        bmpFileBytes[0x04] = bytes[2]
        bmpFileBytes[0x05] = bytes[3]

        // Retain data
        bmpFileBytes[0x06] = 0x00
        bmpFileBytes[0x07] = 0x00
        bmpFileBytes[0x08] = 0x00
        bmpFileBytes[0x09] = 0x00

        // Pixel storage location
        bmpFileBytes[0x0a] = 0x36
        bmpFileBytes[0x0b] = 0x00
        bmpFileBytes[0x0c] = 0x00
        bmpFileBytes[0x0d] = 0x00

        // bmp header file size
        bmpFileBytes[0x0e] = 0x28
        bmpFileBytes[0x0f] = 0x00
        bmpFileBytes[0x10] = 0x00
        bmpFileBytes[0x11] = 0x00

        // Image width
        bytes = getBigEndianBytes(width)
        bmpFileBytes[0x12] = bytes[0]
        bmpFileBytes[0x13] = bytes[1]
        bmpFileBytes[0x14] = bytes[2]
        bmpFileBytes[0x15] = bytes[3]

        // Image height
        bytes = getBigEndianBytes(height)
        bmpFileBytes[0x16] = bytes[0]
        bmpFileBytes[0x17] = bytes[1]
        bmpFileBytes[0x18] = bytes[2]
        bmpFileBytes[0x19] = bytes[3]

        // Number of color planes
        bmpFileBytes[0x1a] = 0x01
        bmpFileBytes[0x1b] = 0x00

        // Pixel bits
        bmpFileBytes[0x1c] = 0x18
        bmpFileBytes[0x1d] = 0x00

        // Compression mode
        bmpFileBytes[0x1e] = 0x00
        bmpFileBytes[0x1f] = 0x00
        bmpFileBytes[0x20] = 0x00
        bmpFileBytes[0x21] = 0x00

        // Pixel data size
        bytes = getBigEndianBytes(pixelBytesCount)
        bmpFileBytes[0x22] = bytes[0]
        bmpFileBytes[0x23] = bytes[1]
        bmpFileBytes[0x24] = bytes[2]
        bmpFileBytes[0x25] = bytes[3]

        // Lateral resolution
        bmpFileBytes[0x26] = 0x00
        bmpFileBytes[0x27] = 0x00
        bmpFileBytes[0x28] = 0x00
        bmpFileBytes[0x29] = 0x00

        // Longitudinal resolution
        bmpFileBytes[0x2a] = 0x00
        bmpFileBytes[0x2b] = 0x00
        bmpFileBytes[0x2c] = 0x00
        bmpFileBytes[0x2d] = 0x00

        // Palette colors
        bmpFileBytes[0x2e] = 0x00
        bmpFileBytes[0x2f] = 0x00
        bmpFileBytes[0x30] = 0x00
        bmpFileBytes[0x31] = 0x00

        // Number of important colors
        bmpFileBytes[0x32] = 0x00
        bmpFileBytes[0x33] = 0x00
        bmpFileBytes[0x34] = 0x00
        bmpFileBytes[0x35] = 0x00
    }

    /** Adds the specified pixel data to the bmp file array */
    fun addPixelBytes(pixelBytes: Array<ByteArray>, bmpFileBytes: ByteArray) {
        val height = pixelBytes.size
        var index = 0x36

        // When setting pixel data, note: it should be stored from the last row of pixels
        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]

                // The three primary colors of each pixel are stored in reverse order
                bmpFileBytes[index++] = blue
                bmpFileBytes[index++] = green
                bmpFileBytes[index++] = red
            }
        }
    }

    /** Convert int to byte array, the default is the array of small end mode, and return the array converted to large end mode */
    fun getBigEndianBytes(number: Int): ByteArray {
        val baos = ByteArrayOutputStream()
        val dos = DataOutputStream(baos)
        dos.writeInt(number)
        val littleEndianBytes = baos.toByteArray()
        val bigEndianBytes = littleEndianBytes.reversedArray()
        return bigEndianBytes
    }

}
fun main() {
    YuvUtil.bmpFileToYV12FileDemo()
    //YuvUtil.bmpFileToYV12FileDemo2()
}

Here we write two demos: bmpFileToYV12FileDemo(), bmpFileToYV12FileDemo2(). The first Demo is rgbBytes data created by code. It is only red and green, and the size is 4 x 2. This is convenient for us to check whether the data is correct. If we can't get the correct result, it will be convenient to troubleshoot the problem. The operation results are as follows:

Output below RGB Pixel data:
ff 0 0| ff 0 0| ff 0 0| ff 0 0| 
0 ff 0| 0 ff 0| 0 ff 0| 0 ff 0| 
Output below YUV Pixel data:
4c 54 ff| 4c 54 ff| 4c 54 ff| 4c 54 ff| 
95 2b 15| 95 2b 15| 95 2b 15| 95 2b 15| 
Output below YV12 data
 Output below Y data
4c 4c 4c 4c 95 95 95 95 
Output below V data
15 15 
Output below U data
54 54

Because the amount of data is small, you can see that the data is correct. You can even open the generated demo.yuv in hexadecimal to view the data, as follows:

Because the amount of data is small, it is easy to check whether the data is wrong. Now that our data is correct, we can run the function bmpFileToYV12FileDemo2(), which reads a bmp image with a width and height of 640 x 480. The following is the bmp image, which is compared with the generated yuv image:

On the left is the bmp image opened with the picture viewing software provided by Windows 11, and on the right is the yuv image opened with YUV Player. You can see that the color of bmp converted to yuv is biased and can be seen. I don't know if the formula I chose is wrong.

YUV Player download address: https://github.com/latelee/YUVPlayer/tree/master/bin , this has not been updated for a long time, but it is easy to use. There is another one with more updates, but this setting feels more complex. I don't know how to adjust the parameters: https://github.com/IENT/YUView , download address: https://github.com/IENT/YUView/releases

YUV picture to bmp picture

import java.io.BufferedOutputStream
import java.io.File
import java.io.FileOutputStream

object YuvUtil {

    fun yv12FileToBmpFile() {
        val yv12File = File("C:\\Users\\Even\\Pictures\\Haiqin smoke.yuv")
        val bmpFile = File("C:\\Users\\Even\\Pictures\\Haiqin smoke(yuv turn bmp).bmp")
        val (yBytes, uBytes, vBytes) = readYuvFilePlanarBytes(yv12File, 640, 480)
        val yuv444Bytes = yv12BytesToYuv444Bytes(640, 480, yBytes, uBytes, vBytes)
        val rgbBytes = yuv444BytesToRgbBytes(yuv444Bytes)
        BmpUtil.createBmpFile(rgbBytes, bmpFile)
    }

    fun bmpFileToYV12FileDemo() {
        val grbBytes = BmpUtil.createRgbBytes(4, 2)
        println("Output below RGB Pixel data:")
        BmpUtil.printColorBytes(grbBytes)
        val yuv444Bytes = rgbBytesToYuv444Bytes(grbBytes)
        println("Output below YUV Pixel data:")
        BmpUtil.printColorBytes(yuv444Bytes)
        val (yBytes, uBytes, vBytes) = yuv444BytesToYv12Bytes(yuv444Bytes)
        printYV12(yBytes, uBytes, vBytes)
        val yv12File = File("C:\\Users\\Even\\Pictures\\demo.yuv")
        writeYv12BytesToFile(yv12File, yBytes, vBytes, uBytes)
    }

    fun bmpFileToYV12FileDemo2() {
        val bmpFile = File("C:\\Users\\Even\\Pictures\\Haiqin smoke.bmp")
        val yv12File = File("C:\\Users\\Even\\Pictures\\Haiqin smoke.yuv")
        val rgbBytes = BmpUtil.readBmpFilePixelBytes(bmpFile)
        val yuv444Bytes = rgbBytesToYuv444Bytes(rgbBytes)
        val (yBytes, uBytes, vBytes) = yuv444BytesToYv12Bytes(yuv444Bytes)
        writeYv12BytesToFile(yv12File, yBytes, vBytes, uBytes)
    }

    /** Read the three planes of YUV file and save them into three arrays, and save the three planes Y, U and V respectively */
    fun readYuvFilePlanarBytes(yuvFile: File, width: Int, height: Int): Triple<ByteArray, ByteArray, ByteArray> {
        return readYuvFilePlanarBytes(yuvFile.readBytes(), width, height)
    }

    fun readYuvFilePlanarBytes(yuvBytes: ByteArray, width: Int, height: Int): Triple<ByteArray, ByteArray, ByteArray> {
        val ySize = width * height
        val vSize = ySize / 4
        val yBytes = ByteArray(ySize)
        val uBytes = ByteArray(vSize)
        val vBytes = ByteArray(vSize)
        var i = 0
        yuvBytes.forEachIndexed { index, byte ->
            val bytes = when {
                index < ySize -> yBytes
                index < ySize + vSize -> vBytes
                else -> uBytes
            }
            if (index == ySize || index == ySize + vSize) {
                i = 0
            }
            bytes[i++] = byte
        }
        return Triple(yBytes, uBytes, vBytes)
    }

    private fun writeYv12BytesToFile(yv12File: File, yBytes: ByteArray, vBytes: ByteArray, uBytes: ByteArray) {
        FileOutputStream(yv12File).use { fos ->
            BufferedOutputStream(fos).use { bos ->
                bos.write(yBytes)
                bos.write(vBytes)
                bos.write(uBytes)
            }
        }
    }

    fun rgbBytesToYuv444Bytes(rgbBytes: Array<ByteArray>): Array<ByteArray> {
        val yuv444Bytes = Array(rgbBytes.size) { ByteArray(rgbBytes[0].size) }
        for (rowIndex in rgbBytes.indices) {
            val oneLineBytes = rgbBytes[rowIndex]
            val oneLineYuv444Bytes = yuv444Bytes[rowIndex]
            for (columnIndex in oneLineBytes.indices step 3) {
                val red   = oneLineBytes[columnIndex + 0]
                val green = oneLineBytes[columnIndex + 1]
                val blue  = oneLineBytes[columnIndex + 2]
                val (Y, U, V) = rgbToYuv(red, green, blue)
                oneLineYuv444Bytes[columnIndex + 0] = Y
                oneLineYuv444Bytes[columnIndex + 1] = U
                oneLineYuv444Bytes[columnIndex + 2] = V
            }
        }
        return yuv444Bytes
    }

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun verify(int: Int) = if (int < 0) 0 else if (int > 255) 255 else int
    fun toHexString(int: Int): String = Integer.toHexString(int)

    fun rgbToYuv(R: Byte, G: Byte, B: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of R, G and B values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return rgbToYuv(byteToInt(R), byteToInt(G), byteToInt(B))
    }

    fun rgbToYuv(R: Int, G: Int, B: Int): Triple<Byte, Byte, Byte> {
        var Y = (0.299 * R + 0.587 * G + 0.114 * B).toInt()
        var U = (-0.169 * R - 0.331 * G + 0.499 * B + 128).toInt()
        var V = (0.499 * R - 0.418 * G - 0.0813 * B + 128).toInt()
        Y = verify(Y)
        U = verify(U)
        V = verify(V)
        //println("rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)} -> yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)}")
        return Triple(Y.toByte(), U.toByte(), V.toByte())
    }

    fun yuvToRgb(Y: Byte, U: Byte, V: Byte): Triple<Byte, Byte, Byte> {
        // Note: the range of Y, U and V values is 0 ~ 255. There is no negative number, which needs to be converted to a positive int.
        // A negative number is still a negative number after being converted with byte.toInt(), so we convert it through the bit operator. The - 1 of Byte should be converted to the Int value of 255
        return yuvToRgb(byteToInt(Y), byteToInt(U), byteToInt(V))
    }

    fun yuvToRgb(Y: Int, U: Int, V: Int): Triple<Byte, Byte, Byte> {
        var R = (Y + 1.4075 * (V - 128)).toInt()
        var G = (Y - 0.3455 * (U - 128) - 0.7169 * (V - 128)).toInt()
        var B = (Y + 1.779 * (U - 128)).toInt()
        R = verify(R)
        G = verify(G)
        B = verify(B)
        //println("yuv: ${toHexString(Y)} ${toHexString(U)} ${toHexString(V)} -> rgb: ${toHexString(R)} ${toHexString(G)} ${toHexString(B)}")
        return Triple(R.toByte(), G.toByte(), B.toByte())
    }



    fun yv12BytesToYuv444Bytes(width: Int, height: Int, yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray): Array<ByteArray> {
        var yIndex = 0
        val yuv444Bytes = Array(height) { ByteArray(width * 3) }
        val oneLineUvSize = width / 2 // In YV 12, the number of U or V in a row
        var twoLineIndex = -1  // Count per two lines
        for (rowIndex in 0 until height) {
            val oneLineBytes = yuv444Bytes[rowIndex]
            var u: Byte = 0
            var v: Byte = 0

            // Because the starting position is the same when reading UV s in every two lines, we can only add twoLineIndex in even lines of these two lines
            if (rowIndex % 2 == 0) {
                twoLineIndex++
            }

            // Calculate the starting position of uvIndex when taking UV for each row
            var uvIndex = twoLineIndex * oneLineUvSize

            for (columnIndex in oneLineBytes.indices step 3) {
                if (yIndex % 2 == 0) {
                    // UV s are taken only once for every two Y's in a row
                    u = uBytes[uvIndex]
                    v = vBytes[uvIndex]
                    uvIndex++
                }

                val y = yBytes[yIndex++]
                oneLineBytes[columnIndex + 0] = y
                oneLineBytes[columnIndex + 1] = u
                oneLineBytes[columnIndex + 2] = v
            }
        }

        return yuv444Bytes
    }

    private fun yuv444BytesToYv12Bytes(yuv444Bytes: Array<ByteArray>): Triple<ByteArray, ByteArray, ByteArray> {
        val width = yuv444Bytes[0].size / 3  // Each pixel takes up 3 bytes, so divide by 3
        val height = yuv444Bytes.size
        val ySize = width * height
        val vSize = ySize / 4
        val yBytes = ByteArray(ySize)
        val uBytes = ByteArray(vSize)
        val vBytes = ByteArray(vSize)
        var yIndex = 0
        var uIndex = 0
        var vIndex = 0
        var saveU = true
        var saveV = true
        for (rowIndex in 0 until height) {
            val oneLine = yuv444Bytes[rowIndex]
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                yBytes[yIndex++] = y
                if (rowIndex % 2 == 0) {
                    // Take U for even rows and take one every other
                    if (saveU) {
                        uBytes[uIndex++] = u
                    }
                    saveU = !saveU
                } else {
                    // V is taken for singular lines, one after another
                    if (saveV) {
                        vBytes[vIndex++] = v
                    }
                    saveV = !saveV
                }
            }
        }
        return Triple(yBytes, uBytes, vBytes)
    }

    fun yuv444BytesToRgbBytes(yuv444Bytes: Array<ByteArray>): Array<ByteArray> {
        val rgbBytes = Array(yuv444Bytes.size) { ByteArray(yuv444Bytes[0].size) }
        for (rowIndex in yuv444Bytes.indices) {
            val oneLineYuv444Bytes = yuv444Bytes[rowIndex]
            val oneLineRgbBytes = rgbBytes[rowIndex]
            for (columnIndex in oneLineYuv444Bytes.indices step 3) {
                val Y   = oneLineYuv444Bytes[columnIndex + 0]
                val U = oneLineYuv444Bytes[columnIndex + 1]
                val V  = oneLineYuv444Bytes[columnIndex + 2]
                val (R, G, B) = yuvToRgb(Y, U, V)
                oneLineRgbBytes[columnIndex + 0] = R
                oneLineRgbBytes[columnIndex + 1] = G
                oneLineRgbBytes[columnIndex + 2] = B
            }
        }
        return rgbBytes
    }

    fun printYUV444(yuv444Bytes: Array<ByteArray>) {
        println("Output below YUV444 data")
        for (oneLine in yuv444Bytes) {
            for (columnIndex in oneLine.indices step 3) {
                val y = oneLine[columnIndex + 0]
                val u = oneLine[columnIndex + 1]
                val v = oneLine[columnIndex + 2]
                print("$y $u $v | ")
            }
            println()
        }
    }

    fun printYV12(yBytes: ByteArray, uBytes: ByteArray, vBytes: ByteArray) {
        // Print in hexadecimal
        println("Output below YV12 data")
        println("Output below Y data")
        yBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below V data")
        vBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println("\n Output below U data")
        uBytes.forEach { print("${toHexString(byteToInt(it))} ") }
        println()
    }

}
import java.io.*

object BmpUtil {

    /** An example of creating a Bitmap: read the pixels of a bmp file, and then write these pixels to a new bmp file */
    fun createBitmapDemo2() {
        val bmpFilePixelBytes = readBmpFilePixelBytes(File("C:\\Users\\Even\\Pictures\\Haiqin smoke.bmp"))
        //printPixelBytes(bmpFilePixelBytes)
        createBmpFile(bmpFilePixelBytes, File("C:\\Users\\Even\\Pictures\\demo.bmp"))
    }

    /** Example of creating a Bitmap: create a bmp file with the top half red and the bottom half green */
    fun createBitmapDemo() {
        val width = 300 // Note: the width and height should be set as a multiple of 4 to avoid the need for filling
        val height = 200
        val pixelBytes = createRgbBytes(width, height)
        //printPixelBytes(pixelBytes)
        val bmpFile = File("C:\\Users\\Even\\Pictures\\demo.bmp")
        createBmpFile(pixelBytes, bmpFile)
    }

    fun readBmpFilePixelBytes(bmpFile: File): Array<ByteArray> {
        // Get all bytes of bmp file
        val bmpFileBytes = bmpFile.readBytes()

        // Get the byte array of the width and height of the image from the bmp file
        val widthBigEndianBytes = ByteArray(4)
        val heightBigEndianBytes = ByteArray(4)
        System.arraycopy(bmpFileBytes, 0x12, widthBigEndianBytes, 0, 4)
        System.arraycopy(bmpFileBytes, 0x16, heightBigEndianBytes, 0, 4)

        // Convert the large byte array to Int
        val width = bigEndianBytesToInt(widthBigEndianBytes)
        val height = bigEndianBytesToInt(heightBigEndianBytes)
        println("Read bmp image width = $width, height = $height")
        val pixelBytes = Array(height) { ByteArray(width * 3) }
        var rowIndex = height - 1 // Because the bmp image is saved from the last line, we move it to the correct position when reading
        var columnIndex = 0
        var oneLineBytes = pixelBytes[rowIndex]
        val oneLineBytesSize = oneLineBytes.size
        // Pixel values are saved from the position of 0x36, and each pixel is 3 bytes
        for (i in 0x36 until bmpFileBytes.size step 3) {
            if (columnIndex == oneLineBytesSize) {
                // There is a full line, which needs to be saved in a new line. Here -- rowIndex is because the original image is saved from the last row to the front row
                oneLineBytes = pixelBytes[--rowIndex]
                columnIndex = 0
            }

            // Note: the colors of bmp files are saved in the order of blue, green and red
            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
    }

    /** Convert the byte array of BigEnding to int */
    private fun bigEndianBytesToInt(bigEndianBytes: ByteArray): Int {
        val littleEndianBytes = bigEndianBytes.reversedArray()
        val bais = ByteArrayInputStream(littleEndianBytes)
        val dis = DataInputStream(bais)
        return dis.readInt()
    }

    /** Create a pixel matrix. Note: the width should be set to a multiple of 4 */
    fun createRgbBytes(width: Int, height: Int) : Array<ByteArray> {
        val redColor   = 0xFF0000
        val greenColor = 0x00FF00
        val redBytes   = getColorBytes(redColor)
        val greenBytes = getColorBytes(greenColor)
        val rgbBytes = Array(height) { ByteArray(width * 3) }
        for (rowIndex in 0 until height) {
            val colorBytes = if (rowIndex < height / 2) redBytes else greenBytes
            val oneLineBytes = rgbBytes[rowIndex]
            for (columnIndex in oneLineBytes.indices step 3) {
                val red   = colorBytes[0x00]
                val green = colorBytes[0x01]
                val blue  = colorBytes[0x02]
                oneLineBytes[columnIndex + 0] = red
                oneLineBytes[columnIndex + 1] = green
                oneLineBytes[columnIndex + 2] = blue
            }
        }
        return rgbBytes
    }

    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
    }

    /** Print color values. You can print rgb color values or yuv444 color values */
    fun printColorBytes(pixelBytes: Array<ByteArray>) {
        for (rowIndex in pixelBytes.indices) {
            val oneLine = pixelBytes[rowIndex]
            for (columnIndex in oneLine.indices step 3) {
                // Get 3 color channels of 1 pixel: R, G, B or Y, U, V
                val colorChannel1 =   oneLine[columnIndex + 0]
                val colorChannel2 =  oneLine[columnIndex + 1]
                val colorChannel3 = oneLine[columnIndex + 2]

                // Convert byte to int, and then output it in hexadecimal
                val colorChannelInt1 = toHexString(byteToInt(colorChannel1))
                val colorChannelInt2 = toHexString(byteToInt(colorChannel2))
                val colorChannelInt3 = toHexString(byteToInt(colorChannel3))

                // Print in hexadecimal
                print("$colorChannelInt1 $colorChannelInt2 $colorChannelInt3| ")
            }
            println()
        }
    }

    fun byteToInt(byte: Byte): Int = byte.toInt() shl 24 ushr 24
    fun toHexString(int: Int): String = Integer.toHexString(int)

    /** According to the given two-dimensional pixel data, it is saved to the specified bmp file according to the bmp file specification */
    fun createBmpFile(rgbBytes: Array<ByteArray>, saveFile: File) {
        // Because each pixel in a row occupies 3 bytes, divide by 3 to get the width of the image
        val pixelWidth = rgbBytes[0].size / 3
        val pixelHeight = rgbBytes.size
        // Each pixel takes up 3 byte s, so multiply by 3
        val pixelBytesCount = pixelWidth * pixelHeight * 3
        // The total file size is: pixel data size + header file size
        val fileBytesCount = pixelBytesCount + 54
        // Create a byte array to save all byte data of bmp file
        val bmpFileBytes = ByteArray(fileBytesCount)
        // Add bmp file header to bmp filebytes
        addBmpFileHeader(pixelWidth, pixelHeight, bmpFileBytes)
        // Add pixel data to BMP filebytes
        addPixelBytes(rgbBytes, bmpFileBytes)
        // Write all bytes to the file
        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

        // file size
        var bytes = getBigEndianBytes(fileBytesCount)
        bmpFileBytes[0x02] = bytes[0]
        bmpFileBytes[0x03] = bytes[1]
        bmpFileBytes[0x04] = bytes[2]
        bmpFileBytes[0x05] = bytes[3]

        // Retain data
        bmpFileBytes[0x06] = 0x00
        bmpFileBytes[0x07] = 0x00
        bmpFileBytes[0x08] = 0x00
        bmpFileBytes[0x09] = 0x00

        // Pixel storage location
        bmpFileBytes[0x0a] = 0x36
        bmpFileBytes[0x0b] = 0x00
        bmpFileBytes[0x0c] = 0x00
        bmpFileBytes[0x0d] = 0x00

        // bmp header file size
        bmpFileBytes[0x0e] = 0x28
        bmpFileBytes[0x0f] = 0x00
        bmpFileBytes[0x10] = 0x00
        bmpFileBytes[0x11] = 0x00

        // Image width
        bytes = getBigEndianBytes(width)
        bmpFileBytes[0x12] = bytes[0]
        bmpFileBytes[0x13] = bytes[1]
        bmpFileBytes[0x14] = bytes[2]
        bmpFileBytes[0x15] = bytes[3]

        // Image height
        bytes = getBigEndianBytes(height)
        bmpFileBytes[0x16] = bytes[0]
        bmpFileBytes[0x17] = bytes[1]
        bmpFileBytes[0x18] = bytes[2]
        bmpFileBytes[0x19] = bytes[3]

        // Number of color planes
        bmpFileBytes[0x1a] = 0x01
        bmpFileBytes[0x1b] = 0x00

        // Pixel bits
        bmpFileBytes[0x1c] = 0x18
        bmpFileBytes[0x1d] = 0x00

        // Compression mode
        bmpFileBytes[0x1e] = 0x00
        bmpFileBytes[0x1f] = 0x00
        bmpFileBytes[0x20] = 0x00
        bmpFileBytes[0x21] = 0x00

        // Pixel data size
        bytes = getBigEndianBytes(pixelBytesCount)
        bmpFileBytes[0x22] = bytes[0]
        bmpFileBytes[0x23] = bytes[1]
        bmpFileBytes[0x24] = bytes[2]
        bmpFileBytes[0x25] = bytes[3]

        // Lateral resolution
        bmpFileBytes[0x26] = 0x00
        bmpFileBytes[0x27] = 0x00
        bmpFileBytes[0x28] = 0x00
        bmpFileBytes[0x29] = 0x00

        // Longitudinal resolution
        bmpFileBytes[0x2a] = 0x00
        bmpFileBytes[0x2b] = 0x00
        bmpFileBytes[0x2c] = 0x00
        bmpFileBytes[0x2d] = 0x00

        // Palette colors
        bmpFileBytes[0x2e] = 0x00
        bmpFileBytes[0x2f] = 0x00
        bmpFileBytes[0x30] = 0x00
        bmpFileBytes[0x31] = 0x00

        // Number of important colors
        bmpFileBytes[0x32] = 0x00
        bmpFileBytes[0x33] = 0x00
        bmpFileBytes[0x34] = 0x00
        bmpFileBytes[0x35] = 0x00
    }

    /** Adds the specified pixel data to the bmp file array */
    fun addPixelBytes(pixelBytes: Array<ByteArray>, bmpFileBytes: ByteArray) {
        val height = pixelBytes.size
        var index = 0x36

        // When setting pixel data, note: it should be stored from the last row of pixels
        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]

                // The three primary colors of each pixel are stored in reverse order
                bmpFileBytes[index++] = blue
                bmpFileBytes[index++] = green
                bmpFileBytes[index++] = red
            }
        }
    }

    /** Convert int to byte array, the default is the array of small end mode, and return the array converted to large end mode */
    fun getBigEndianBytes(number: Int): ByteArray {
        val baos = ByteArrayOutputStream()
        val dos = DataOutputStream(baos)
        dos.writeInt(number)
        val littleEndianBytes = baos.toByteArray()
        val bigEndianBytes = littleEndianBytes.reversedArray()
        return bigEndianBytes
    }

}
fun main() {
//    YuvUtil.bmpFileToYV12FileDemo()
//    YuvUtil.bmpFileToYV12FileDemo2()
    YuvUtil.yv12FileToBmpFile()
}

The operation effect is as follows:

On the far left is the bmp original file saved after the screenshot of the screenshot software. In the middle is the yuv image converted from bmp, and on the right is the bmp image converted from yuv. I don't know if there is a difference. I feel that the difference is relatively small. If there is no comparison of the original image, I generally can't feel the difference!

Tags: Android

Posted on Sun, 24 Oct 2021 19:54:55 -0400 by hcspider