• NV21图片格式深入解析与代码实战-RGB转NV21与画框


    1.NV21格式图片解析

    NV21图像格式属于 YUV颜色空间中的YUV420SP格式
    每四个Y分量共用一组U分量和V分量,Y连续排序,U与V交叉排序

    在这里插入图片描述

    重点总结

    • uv交错模式
    • 4Y共用一组uv(2个)
    • 大小:UV= Y 的一半

    排列方式如下

    Y Y   Y Y   Y Y   Y Y
    Y Y   Y Y   Y Y   Y Y

    Y Y   Y Y   Y Y   Y Y
    Y Y   Y Y   Y Y   Y Y

    V U  V U  V U  V U

    V U  V U  V U  V U

    2.RGB图片转NV21—逐像素

    基本公式

    • yuv --> rgb

      R = (298*Y + 411 * V - 57344)>>8
      G = (298*Y - 101* U - 211* V+ 34739)>>8
      B = (298*Y + 519* U- 71117)>>8
      
      • 1
      • 2
      • 3
    • rgb --> yuv

      Y= (  66*R + 129*G  +  25*B)>>8 + 16 
      U= (-38*R  -    74*G  + 112*B)>>8 +128
      V= (112*R -    94*G  -   18*B)>>8   + 128
      
      • 1
      • 2
      • 3

    c++代码

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
     
    void RGB2NV21()
    {
    	const char *filename = "yuv.yuv";
    	cv::Mat Img = cv::imread("RGB.jpg");
    	FILE  *fp = fopen(filename,"wb");
    	
    	if (Img.empty())
    	{
    		std::cout << "empty!check your image";
    		return;
    	}
    	int cols = Img.cols;
    	int rows = Img.rows;
     
    	int Yindex = 0;
    	int UVindex = rows * cols;
     
    	unsigned char* yuvbuff = new unsigned char[1.5 * rows * cols];
     
    	cv::Mat NV21(rows+rows/2, cols, CV_8UC1);
    	cv::Mat OpencvYUV;
    	cv::Mat OpencvImg;
    	cv::cvtColor(Img, OpencvYUV, CV_BGR2YUV_YV12);
    	
    	int UVRow{ 0 };
    	for (int i=0;i<rows;i++)
    	{
    		for (int j=0;j<cols;j++)
    		{
    			uchar* YPointer = NV21.ptr<uchar>(i);
     
    			int B = Img.at<cv::Vec3b>(i, j)[0];
    			int G = Img.at<cv::Vec3b>(i, j)[1];
    			int R = Img.at<cv::Vec3b>(i, j)[2];
     
    			//计算Y的值
    			int Y = (77 * R + 150 * G + 29 * B) >> 8;
    			YPointer[j] = Y;
    			yuvbuff[Yindex++] = (Y < 0) ? 0 : ((Y > 255) ? 255 : Y);
    			uchar* UVPointer = NV21.ptr<uchar>(rows+i/2);
    			//计算U、V的值,进行2x2的采样
    			if (i%2==0&&(j)%2==0)
    			{
    				int U = ((-44 * R - 87 * G + 131 * B) >> 8) + 128;
    				int V = ((131 * R - 110 * G - 21 * B) >> 8) + 128;
    				UVPointer[j] = V;
    				UVPointer[j+1] = U;
    				yuvbuff[UVindex++] = (V < 0) ? 0 : ((V > 255) ? 255 : V);
    				yuvbuff[UVindex++] = (U < 0) ? 0 : ((U > 255) ? 255 : U);
    			}
    		}
    	}
    	for (int i=0;i< 1.5 * rows * cols;i++)
    	{
    		fwrite(&yuvbuff[i], 1, 1, fp);
    	}
    	fclose(fp);
    	std::cout << "write to file ok!" << std::endl;
    	std::cout << "srcImg: " << "rows:" << Img.rows << "cols:" << Img.cols << std::endl;
    	std::cout << "NV21: " << "rows:" << NV21.rows << "cols:" << NV21.cols << std::endl;
    	std::cout << "opencv_YUV: " << "rows:" << OpencvYUV.rows << "cols:" << OpencvYUV.cols << std::endl;
     
    	cv::imshow("src", Img);//原图
    	cv::imshow("YUV", NV21);//转换后的图片
    	cv::imshow("opencv_YUV", OpencvYUV); //opencv转换后的图片
    	cv::imwrite("NV21.jpg", NV21);
    	cv::waitKey(30000);
    }
     
    int main()
    {
    	RGB2NV21();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    3.NV21图像逐个像素画框

    结果展示

    在这里插入图片描述

    整体流程

    将原始图像保存到一维数组

    2个水平画线

    2个竖直画线

    主要难点就是找2个uv起始坐标的迭代公式

    水平画线

    y :外循环更新下一行,内循环改变一行值

    uv:外循环更新下一行,内循环改变一行值

    因为交错模式,uv内循环更新要隔着2个

    因为4个y对应一组uv,uv外循环次数比y外循环次数少一半

    Y分量起始位置更新公式

    //计算Y分量在nv21Data数组中的起始位置:Y的数据+绘制横坐标位置
    yStartIndex = (y+i_r) * imageWidth + x;
    
    • 1
    • 2
    yStartIndex:Y分量在nv21Data数组中的起始位置
    i_r:行更新值
    imageWidth:图像宽
    x:绘制起始点的横坐标位置
    
    • 1
    • 2
    • 3
    • 4

    UV分量起始位置更新公式

    // 计算UV分量在nv21Data数组中的起始位置:Y的数据+(UV数据行/2*数据宽度)+绘制横坐标(列)位置                   
    uvStartIndex = imageWidth * imageHeight + ((y+i_r)/ 2 * imageWidth)  + x ;  
    
    • 1
    • 2
    uvStartIndex:uv分量在nv21Data数组中的起始位置
    i_r:行更新值
    imageWidth:图像宽
    imageHeight:图像高
    x:绘制起始点的横坐标位置
    
    • 1
    • 2
    • 3
    • 4
    • 5

    竖直画线

    y :外循环更新下一列,内循环改变一列值-间隔mageWidth渲染

    uv:外循环更新下一列,内循环改变一列值

    因为交错模式,uv外循环更新+2

    因为4个y对应一组uv,uv内循环次数比y内循环次数少一半

    Y分量起始位置更新公式

    // 计算Y分量在nv21Data数组中的起始位置:Y的数据+绘制横坐标位置 
    yStartIndex = (y) * imageWidth + x+i_c;
    
    • 1
    • 2
    yStartIndex:Y分量在nv21Data数组中的起始位置
    i_c:列更新值
    imageWidth:图像宽
    x:绘制起始点的横坐标位置
    
    • 1
    • 2
    • 3
    • 4

    UV分量起始位置更新公式

    // 计算UV分量在nv21Data数组中的起始位置:Y的数据+(UV数据行/2*数据宽度)+绘制列位置
    uvStartIndex = imageWidth * imageHeight + (y/2 * imageWidth)  + x+(i_c);
    
    • 1
    • 2
    uvStartIndex:uv分量在nv21Data数组中的起始位置
    i_c:列更新值
    imageWidth:图像宽
    imageHeight:图像高
    x:绘制起始点的横坐标位置
    
    • 1
    • 2
    • 3
    • 4
    • 5

    完整代码

    #include 
    #include 
    #include 
    
    
    
    /**
     * 水平画线
     *      y :外循环更新下一行,内循环改变一行值
     *      uv:外循环更新下一行,内循环改变一行值
     *      因为交错模式,uv内循环更新要隔着2个
     *      因为4个y对应一组uv,uv外循环次数比y外循环次数少一半
     *
     * 参数:
     * unsigned char *nv21Data,int imageWidth, int imageHeight:图像的数据和宽高
     * int x:线条起始点的横坐标
     * int y:线条起始点的纵坐标
     * int line_len:线条的长度
    */
    void draw_line_Horizontal(unsigned char *nv21Data,int imageWidth, int imageHeight,int x,int y,int line_len){
        //参数判断
        if(nv21Data==NULL || x>imageWidth || y>imageHeight||line_len>imageWidth){
            return;
        }
    
        int line_width = 3;//设置画线的宽度为3
        int y_width = line_width;//改变Y的宽度
        // 4个y共用一组vu,渲染的时候要/2+1
        int uv_width = y_width/2 + 1; //设置改变UV的宽度为 Y/2+1
    
        int yStartIndex, uvStartIndex;//定义起始位置
    
        // 设置Y分量
        for(int i_r=0;i_r<y_width;i_r++){//i_r表示行更新值
            // 计算Y分量在nv21Data数组中的起始位置:Y的数据+绘制横坐标位置
            yStartIndex = (y+i_r) * imageWidth + x;
            // 开始set
            for (int i = 0; i < line_len; i++) {
                // 设置Y分量为蓝色
                nv21Data[yStartIndex + i] = 60;
            }
    
        }
    
        // 设置UV分量
        for(int i_r=0;i_r<uv_width;i_r++){//更新行位置
            // 计算UV分量在nv21Data数组中的起始位置:Y的数据+(UV数据行/2*数据宽度)+绘制横坐标(列)位置
            uvStartIndex = imageWidth * imageHeight + ((y+i_r)/ 2 * imageWidth)  + x ;
            // 开始set
            for (int i = 0; i < line_len; i+=2) {
                // 设置UV分量为蓝色:因为UV交错分布,故+2
                nv21Data[uvStartIndex + i] = 100;
                nv21Data[uvStartIndex + i + 1] = 212;
            }
        }
    
    }
    
    /**
     * 竖直画线
     *      间隔一个imageWidth渲染
     *      y :外循环更新下一列,内循环改变一列值
     *      uv:外循环更新下一列,内循环改变一列值
     *      因为交错模式,uv外循环更新+2
     *      因为4个y对应一组uv,uv内循环次数比y内循环次数少一半
     *
     * 参数:
     * unsigned char *nv21Data,int imageWidth, int imageHeight:图像的数据和宽高
     * int x:线条起始点的横坐标
     * int y:线条起始点的纵坐标
     * int line_len:线条的长度
    */
    void draw_line_Vertical(unsigned char *nv21Data,int imageWidth, int imageHeight,int x,int y,int line_len){
        //参数判断
        if(nv21Data==NULL || x>imageWidth || y>imageHeight||line_len>imageWidth){
            return;
        }
        int line_width = 3;//设置画线的宽度为3
        int y_width = line_width;//改变Y的宽度
        // 4个y共用一组vu,渲染的时候要/2+1
        int uv_width = y_width/2+1; //设置改变UV的宽度为 Y
    
        int yStartIndex, uvStartIndex;//定义起始位置
    
        // 设置Y分量
        for(int i_c=0;i_c<y_width;i_c++){
            // 计算Y分量在nv21Data数组中的起始位置:Y的数据+绘制横坐标位置
            yStartIndex = (y+i_c) * imageWidth + x;
            // 开始set
            for (int i = 0; i < line_len; i++) {
                // 设置Y分量为蓝色
                int index_y = yStartIndex + imageWidth*i;
                nv21Data[index_y] = 65;
            }
    
        }
    
        // 设置UV分量
        for(int i_c=0;i_c<uv_width;i_c+=2){//外循环更新的时候因为交错模式=每次更新列位置+2
            // 计算UV分量在nv21Data数组中的起始位置:Y的数据+(UV数据行/2*数据宽度)+绘制列位置
            uvStartIndex = imageWidth * imageHeight + (y/2 * imageWidth)  + x+(i_c);
            // 开始set:
            //    因为竖直方向 uv是y的一半,所以line_len/2
            //    又因为竖直方向内循环没有交错模式影响,不用跳过2
            for (int i = 0; i < line_len/2; i++) {
                int index_u = uvStartIndex + imageWidth*i;//下一行index:间隔imageWidth*i
                int index_v = uvStartIndex + imageWidth*i+1;
                nv21Data[index_u] = 100;
                nv21Data[index_v] = 212;
            }
        }
    }
    
    
    void drawRectOnNv21Image(const char *pImagePath, int imageWidth, int imageHeight, int left, int top, int right, int bottom) {
        /*
            参数检测
        */
    
        if(pImagePath==NULL || imageHeight==0 || imageWidth==0){
            printf("Error: Failed parameter\n");
            return;
        }
    
        /*
            程序运行主体
        */
        FILE *file = fopen(pImagePath, "rb");
        if (file == NULL) {
            printf("Error: Failed fopen pImagePath\n");
            return;
        }
    
        // 计算NV21图片的总大小
        int imageSize = imageWidth * imageHeight * 3 / 2;
        fseek(file, 0, SEEK_END);
        int file_size = ftell(file);
        fseek(file, 0, SEEK_SET);
    
        // 判断文件是否为NV21格式:file_size!=imageSize
        if(file_size!=imageSize){
            printf("Error: Failed imageSize!=1.5 * imageWidth * imageHeight \n");
            fclose(file);
            return;
        }
        // 申请空间存储图片yuv数据
        unsigned char *nv21Data = (unsigned char *)malloc(imageSize);
        if (nv21Data == NULL) {
            printf("Error: Failed malloc\n");
            fclose(file);
            return;
        }
    
        // 读取NV21图片数据
        fread(nv21Data, sizeof(unsigned char), imageSize, file);
        fclose(file);
    
        // 画矩形框
        int rectWidth = right - left;
        int rectHeight = bottom - top;
    
        // 上边框
        draw_line_Horizontal(nv21Data,imageWidth,imageHeight,left,top,rectWidth);
    
        // 下边框
        draw_line_Horizontal(nv21Data,imageWidth,imageHeight,left,bottom,rectWidth);
    
        // 左边框
        draw_line_Vertical(nv21Data,imageWidth,imageHeight,left,top,rectHeight);
    
        // 右边框
        draw_line_Vertical(nv21Data,imageWidth,imageHeight,right,top,rectHeight);
    
        // 保存结果为新的NV21图片
        char output_image_path[] = "output_nv21_image.nv21";
        FILE *outputFile = fopen(output_image_path, "wb");
        if (outputFile == NULL) {
            printf("Error: Failed fopen output_image_path\n");
            free(nv21Data);
            return;
        }
        fwrite(nv21Data, sizeof(unsigned char), imageSize, outputFile);
        fclose(outputFile);
    
        free(nv21Data);
        printf("Program ok!\n");
        printf("Output image save path:%s\n",output_image_path);
    }
    
    int main() {
        const char *pImagePath = "input_nv21_iamge.nv21";    // NV21图片文件路径
        int imageWidth = 640;                   // 图片宽度
        int imageHeight = 480;                  // 图片高度
        int left = 200;                         // 矩形框左上角x坐标
        int top = 170;                          // 矩形框左上角y坐标
        int right = 430;                        // 矩形框右下角x坐标
        int bottom = 380;                       // 矩形框右下角y坐标
    
        drawRectOnNv21Image(pImagePath, imageWidth, imageHeight, left, top, right, bottom);
        return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189
    • 190
    • 191
    • 192
    • 193
    • 194
    • 195
    • 196
    • 197
    • 198
    • 199
    • 200
    • 201
    • 202
    • 203
  • 相关阅读:
    一文总结MySQL的指令是如何工作的
    vue项目中使用本地下载的iconfont文件中的图标,全部显示的都是方框
    一、osg编译
    【C语言】二级指针的深度理解,峰值的寻找(每日小细节004)
    【漏洞复现】某厂商明御安全网关sslvpn命令执行漏洞
    青龙面板 pip3 not found等神奇的错误修复
    None 和 NaN分不清? pandas 难点彻底搞懂
    经典网络解(三) 生成模型VAE | 自编码器、变分自编码器|有监督,无监督
    初识linux之简单了解TCP协议与UDP协议
    3.24 OrCAD中移动元器件时怎么让连线不跟着元器件一起动? OrCAD中如何添加文本标注、图形标注?
  • 原文地址:https://blog.csdn.net/qq_43537701/article/details/133833914