YUV实战代码解析,通道分离,灰度图,变暗处理


在YUV格式通透解析一文中,详细描述了YUV格式的概念、优点、采样格式和存储格式,在文末,还放置了一些对YUV图像进行分量处理的效果图,本文就是那些效果图的实战源码解析,理论与实践相结合,助你更进一步理解YUV格式。

YUV格式通透解析


实战准备


开发环境

  • 编程语言 - C++
  • IDE - CLion
  • 编译器 - MinGW

开发环境这里简单解释一下,我之前一直在做后端开发和Android开发,分别用IDEA和Android studio,这两个IDE风格和快捷键几乎一样,用得很舒服。但C++只在大学一年级写过一点代码,到后来就没有怎么搞过了,所以在网上简单查了一下,把微软的VS2019弄了下来。体验了VS不到一天,就很难熬下去了,难道是我习惯了JetBrains的产品了吗?VS太难用了,体积大,配置库链接麻烦,快捷键竟然还需组合处理,比如注释,要先按Ctrl+K,再按Ctrl+C,连续按下这个组合后,才能对代码行注释,取消注释也是类似的操作,虽然可以自己设置快捷键,但是VS默认好多操作的快捷键都是组个按键的,用得很糟心;再比如,我定义个函数之后,想给这个函数做一个方法级注释的,类似下面这样:

/**
 * 分离YUV420P图像数据
 *
 * @param url yuv图像文件路径
 * @param w   指定yuv图像宽
 * @param h   指定yuv图像高
 * @return 处理成功返回0,否则返回-1
 */
int yuv420p_split(const char* url, int w, int h);

但是发现VS要配置好这些方法级注释模板,贼麻烦,算了,不想吐槽了。(但我不能否定VS不强,它也很强,我不喜欢而已)

然后去网上继续查了一下,惊喜发现JetBrains竟然有一个专门用于开发C++的,CLion,果断下载下来,虽然不是免费,但是搞个激活码还简单么?结果体验了一番CLion,爽!还是IDEA那个味道。

如果你也想使用CLion,文末会分享百度云盘资源,自己官网下载也可以,只要你的网够快。


资源

对YUV图像进行分量处理,不能直接操作封装格式的图片文件,如jpg、png、webp等,需要将图片转为.yuv后缀的文件格式才行,即把压缩数据转为原始数据。

这个操作有两种方式:

  • 找个在线转换网站
  • 使用ffmpeg处理

这里就直接建议第二种方式了在线转换网站很慢,还有一些不太友好的因素在里面。使用ffmpeg很方便,直接下载Windows版的ffmpeg,然后敲上几句命令就可以了。

下载Windows版的ffmpeg解压后是这个样子:

ffmpeg解压结果


然后附上两条你可能需要用到的命令:

jpg转yuv420p

ffmpeg -i input.jpg -pix_fmt yuv420p output.yuv

jpg转yuv444p

ffmpeg -i input.jpg -pix_fmt yuv444p output.yuv

ffmpeg可能你也会下载得比较慢,文末会提供云盘资源。

当然,你也可以跳过这一步,因为项目源码里头,提供了我使用过的YUV文件了,你可以直接用我提供的yuv文件跑代码。


核心代码解析


先放一张原图,龙猫:

longmao

YUV420P单独分离出Y、U、V分量图像

代码如下:

int yuv420p_split(const char *url, int w, int h) {
    FILE *fp;
    if (fopen_s(&fp, url, "rb+") != 0) {
        printf("The file open faild!");
        return -1;
    }

    FILE *fp1, *fp2, *fp3;
    errno_t err1, err2, err3;

    err1 = fopen_s(&fp1, "../out/longmao_420_y.y", "wb+");
    err2 = fopen_s(&fp2, "../out/longmao_420_u.y", "wb+");
    err3 = fopen_s(&fp3, "../out/longmao_420_v.y", "wb+");
    
    if (err1 != 0 || err2 != 0 || err3 != 0) {
        printf("err1: %d, err2: %d, err3: %d", err1, err1, err3);
        return -1;
    }

    unsigned char *pic = (unsigned char *) malloc(w * h * 3 / 2);

    fread(pic, 1, w * h * 3 / 2, fp);
    // Y
    fwrite(pic, 1, w * h, fp1);
    // U
    fwrite(pic + w * h, 1, w * h / 4, fp2);
    // V
    fwrite(pic + w * h * 5 / 4, 1, w * h / 4, fp3);
    
    free(pic);
    fclose(fp);
    fclose(fp1);
    fclose(fp2);
    fclose(fp3);

    return 0;
}

解析:

首先需要弄清楚,yuv文件是纯粹的像素数据,不记录有宽高信息,所以我们处理时需要人为指定宽高。

然后说一下这一行代码:

unsigned char *pic = (unsigned char *) malloc(w * h * 3 / 2);

主要解释一下malloc时,为什么是w * h * 3 / 2,还记得上一篇文章YUV420P的解析吧,YUV420P是YUV420采样,存储格式是planar,即先全部存储完Y分量,再存U分量,再存V分量,而在YUV420采样中,Y:U和Y:V都是4:1,即有w*h个Y,w * h / 4个U,w * h / 4个V,最终该YUV占用的内存大小就是w * h * 3 / 2个字节,而C语言中,char类型就是占用一字节。

然后,分离出Y、U、V三个单独分量的文件操作也很简单,控制好偏移量,单独把每一个分量的像素数据输出到一个文件就行,所以有这样的代码:

// Y,偏移量为0,即pic首地址,数据大小是 w * h
fwrite(pic, 1, w * h, fp1);
// U,偏移量是Y分量之后开始,即首地址+Y分量大小,数据大小是 w * h /4
fwrite(pic + w * h, 1, w * h / 4, fp2);
// V,偏移量是U分量之后开始,即首地址+Y分量大小+U分量大小,数据大小是w * h /4
fwrite(pic + w * h * 5 / 4, 1, w * h / 4, fp3);

然后就得到单独三个分量的数据文件了,如下:

yuv420三分量结果

然后我们要怎么验证结果呢?预览yuv文件需要有专门的预览工具才行,这里采用了雷神开源的yuvplayer,使用起来很方便,不过有一点要注意,预览yuv图时,要指定好跟预览文件的格式和分辨率,格式和分辨率要对应上才行,并且,对于上述代码的输出结果是.y后缀的,需要将格式指定为Y才行,如图:

yuvplayer_tip

还有一点就是,对于yuv420的,假如y分量图的分辨率是450x450,那么u和v分量图的分辨率就要减半,为:225x225,为何应该不用多说了,单独分量文件,uv肯定是Y的一半的。

关于yuvplayer的使用解释就这么过,还有不明白的,可以联系我,另外资源文末给。

上述代码的效果图如下:

Y分量图

yuv420p_y_res

U分量图

yuv420p_u_res

V分量图

yuv420p_v_res

YUV420P转灰度图

代码如下:

int yuv420_gray(const char *url, int w, int h) {
    FILE *fp;
    if (fopen_s(&fp, url, "rb+") != 0) {
        printf("The file open faild!");

        return -1;
    }

    FILE *fp1;
    if (fopen_s(&fp1, "../out/longmao_420p_gray.yuv", "wb+") != 0) {
        printf("output file not exist!");
        return -1;
    }

    unsigned char *pic = (unsigned char *) malloc(w * h * 3 / 2);
    
    fread(pic, 1, w * h * 3 / 2, fp);
    memset(pic + w * h, 128, w * h / 2);
    fwrite(pic, 1, w * h * 3 / 2, fp1);

    free(pic);
    fclose(fp);
    fclose(fp1);
    return 0;
}

解析:

转灰度图主要看这句代码:

memset(pic + w * h, 128, w * h / 2);

先简单说一下该函数的作用:

void *memset(void *str, int c, size_t n)

将指针str指向的地址开始的位置后n个字符,赋值后c。其中:

  • str -- 指向要填充的内存块。
  • c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
  • n -- 要被设置为该值的字符数。

我们将彩色图变为灰度图,其实只要将UV色度分量的值置为0值就可以了,但是置为0值并不是直接赋值为0。因为U、V色度分量在存储时,经过了偏移处理,偏移处理前,UV的取值范围是-128~127,这时候的无色UV的值就是0。但是经过偏移后,原本的值范围从-128~127变为了0~255,那么这时,将UV分量的值赋值为128,就相当于偏移前的无色值了,再结合memset函数,就有了这一行代码:

memset(pic + w * h, 128, w * h / 2);

经过灰度处理后的效果图如下:

yuv420_gray_res

YUV420P亮度减半

代码如下:

int yuv420_half_y(const char *url, int w, int h) {
    FILE *fp;
    if (fopen_s(&fp, url, "rb+") != 0) {
        printf("The file open faild!");

        return -1;
    }

    FILE *fp1;
    if (fopen_s(&fp1, "../out/longmao_420p_half.yuv", "wb+") != 0) {
        printf("output file not exist!");
        return -1;
    }

    unsigned char *pic = (unsigned char *) malloc(w * h * 3 / 2);
    fread(pic, 1, w * h * 3 / 2, fp);

    for (int j = 0; j < w * h; j++) {
        // 亮度减半,直接对Y分量除以2就行
        unsigned char temp = pic[j] / 2;
        pic[j] = temp;
    }

    fwrite(pic, 1, w * h * 3 / 2, fp1);
    
    free(pic);
    fclose(fp);
    fclose(fp1);

    return 0;
}

解析:

亮度减半处理就很好理解了,Y分量就是代表亮度,直接对所有Y分量的值除以2就行,处理后的效果图如下:

yuv420_half_y_res

本文就解析一下YUV420P格式的图像处理的代码,有兴趣的话,你可以自己思考,如何处理YUV444P的图像数据,原理是一样的,只是开辟的内存空间和YUV三个分量的大小有点不一样而已,只要理解了上一篇关于YUV格式的理论部分内容,写出这些代码就so easy 了。这里就不展示过多代码了,直接附上源码,有兴趣的可以pull下来跑一下。

GitHub源码地址:

https://github.com/hxxian/YUVImageTest.git

其他资源获取方式,也从GitHub上获取吧~


THE END

你可能感兴趣的:(YUV实战代码解析,通道分离,灰度图,变暗处理)