代码笔记程序员

项目:基于ffmpeg视频质量安卓测试平台.(计算PSNR与SS

2018-07-09  本文已影响1人  _VITA

============================================================================================

首先记录一下YUV420。我们需要对Camera采集的图像进行编码,就需要对YUV格式文件进行处理。大致上可以这么看,mp4==>解码=>YUV==>用MediaCoder编码==>H264==>用MediaMuxer打包成MP4格式==>mp4。

YUV是一种亮度信号Y和色度信号U、V是分离的色彩空间,它主要用于优化彩色视频信号的传输,使其向后相容老式黑白电视。与RGB视频信号传输相比,它最大的优点在于只需占用极少的频宽(RGB要求三个独立的视频信号同时传输)。其中“Y”表示明亮度(Luminance或Luma),也就是灰阶值;而“U”和“V”表示的则是色度(Chrominance或Chroma),作用是描述影像色彩及饱和度,用于指定像素的颜色。

YUV主流有三种格式格式。

YUV444,YUV422,YUV420.为什么分了三种呢? 因为人眼对亮度感受明显,对色度感受不明显。所以,这三种格式完全保留了亮度信息,而对色度信息有一定的取舍。
如何取舍?

image.png
黑点表示采样该像素点的Y分量,以空心圆圈表示采用该像素点的UV分量。
YUV420 的两种帧格式

CameraPrevieCallback实时采集的视频帧格式为YV12或者NV21,它们都属于YUV420采样格式。YUV格式的存放方式永远是先排列完Y分量,再排序U或V分量,不同的采样只是Y或V分量的排列格式和顺序不同。。NV21、NV12的区别在于Y值排序完全相同,U和V交错排序,不同在于UV顺序:

image.png

所以可以分析出对于1920×1080的资源每帧的大小为:1920×1080×2/3。
转换方式在网上很多,基本原理就是旋转==》数值的交换。
==================================================================================

YUV文件非常非常大,测试中10m的mp4文件,解码之后YUV420文件达500m。Camera采集的YUV图像通常为YUV420,而在现实网络中,这么高的上行宽带一般是很难达到的,因此,我们就必须在传输之前对采集的视频数据进行压缩编码。所以我们需要H264了。
H.264简介:
H.264是MPEG-4的第十部分,是由VCEG和MPEG联合提出的高度压缩数字视频编码器标准,目前在多媒体开发应用中非常广泛。H.264具有低码率、高压缩、高质量的图像、容错能力强、网络适应性强等特点,它最大的优势拥有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的两倍以上。
总之,就是压缩压缩,如何压缩呢?

三种帧(I,B,P)的协作。

在H.264协议里定义了三种帧,完整编码的帧叫I帧(关键帧),参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。H.264编码采用的核心算法是帧内压缩帧间压缩

通俗的来说,H.264编码的就是对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小。B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。

三种帧如何协作呢?

h264的压缩方法:

1.分组:把几帧图像分为一组(GOP,也就是一个序列),为防止运动变化,帧数不宜取多。
2.定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;
3.预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;
4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输。

通俗的来说,H.264编码的就是对于一段变化不大图像画面,我们可以先编码出一个完整的图像帧A,随后的B帧就不编码全部图像,只写入与A帧的差别,这样B帧的大小就只有完整帧的1/10或更小。B帧之后的C帧如果变化不大,我们可以继续以参考B的方式编码C帧,这样循环下去。

在ffmpeg里,I帧间隔可以自己设置,用来测试不同I帧间隔对视频质量有何影响。

==================================================================================

PSNR& SSIM

把原始参考视频与失真视频在每一个对应帧中的每一个对应像素之问进行比较。准确的讲,这种方法得到的并不是真正的视频质量,而是失真视频相对于原始视频的相似程度或保真程度。

SSIM = Structural SIMilarity(结构相似性),这是一种用来评测图像质量的一种方法。由于人类视觉很容易从图像中抽取出结构信息,因此计算两幅图像结构信息的相似性就可以用来作为一种检测图像质量的好坏.其取值范围为[0,1],值越大越好;
结构相似性理论认为,图像是高度结构化的,即像素间有很强的相关性,特别是空域中最接近的像素,这种相关性蕴含着视觉场景中物体结构的重要信息;作为结构相似性理论的实现,结构相似度指数从图像组成的角度将结构信息定义为独立于亮度、对比度的,反映场景中物体结构的属性,并将失真建模为亮度、对比度和结构三个不同因素的组合。用均值作为亮度的估计,标准差作为对比度的估计,协方差作为结构相似程度的估计,计算数学模型如下:亮度表示L;对比度表示C;结构相似性表示S;通常取C1=(K1L)^2,C2=(K2L)^2, C3=C2/2, K1=0.01, K2=0.03, L=255.

image.png

最后的SSIM指数为:


image.png

当我们设定C3=C2/2时,我们可以将公式改写成更加简单的形式:


image.png
SSIM相当于将数据进行归一化后,分别计算图像块照明度(图像块的均值),对比度(图像块的方差)和归一化后的像素向量这三者相似度,并将三者相乘。

PSNR是最普遍和使用最为广泛的一种图像客观评价指标,然而它是基于对应像素点间的误差,即基于误差敏感的图像质量评价。由于并未考虑到人眼的视觉特性(人眼对空间频率较低的对比差异敏感度较高,人眼对亮度对比差异的敏感度较色度高,人眼对一个区域的感知结果会受到其周围邻近区域的影响等),因而经常出现评价结果与人的主观感觉不一致的情况。

PSNR定义与计算

PSNR本质上与MSE相同,是MSE的对数表示。

对于大多数常见的8bit/彩色视频图像,
PSNR完全由MSE确定。PSNR较MSE更常用,因为人们想把图像质量与某个范围的PSNR相联系。

根据实际经验,对于亮度像素分量:
PSNR高于40dB说明图像质量极好(即非常接近原始图像),
在30—40dB通常表示图像质量是好的(即失真可以察觉但可以接受),
在20—30dB说明图像质量差;
最后,PSNR低于20dB图像不可接受

获取每帧PSNR与SSIM的代码:

这里用到了滑动窗口的算法:4×4的窗口2格2格的滑动 ;
isRD是用来画制RD曲线图,当没有必要画就去掉相应代码即可。

public class Psnr {
    private int width;
    private int height;
    private int YuvFormat;
    private int F;
    private String yuvSource;
    private String dstSource;
    private String filename;
    private boolean isSsim;
    private boolean isRD = true;
    private Handler handler;
    private float ave_psnr;
    private float ave_ssim;


    public Psnr(int width, int height, int yuvFormat, String y, String des, String filename, Handler handler) {
        this.width = width;
        this.height = height;
        this.YuvFormat = yuvFormat;
        this.yuvSource = y;
        this.dstSource = des;
        this.filename = filename;
        this.handler = handler;
        switch (YuvFormat) {
            case 400:
                F = this.width * this.height;
                break;
            case 422:
                F = this.width * this.height * 2;
                break;
            case 444:
                F = this.width * this.height * 3;
                break;
            default:
            case 420:
                F = this.width * this.height * 3 / 2;
                break;
        }
    }

    public float getSsim(byte[] data1, byte[] data2, int framecount) {
        if (!isSsim) {
            return -1;
        }
        try {

            Message message = new Message();
            if (framecount == 0) {
                message.what = -2;
                message.arg1 = 1;
                handler.sendMessage(message);
            }

            float tem_ssim;
            tem_ssim = x264_pixel_ssim_wxh(data1, width, data2, width, width, height);

            Message message1 = new Message();
            message1.what = 1;
            message1.arg1 = framecount;
            handler.sendMessage(message1);

            Log.i("SSIM", "tem_ssim" + tem_ssim + "_" + framecount);

            return tem_ssim;

        } catch (Exception e) {
            e.printStackTrace();
        }
        return -1;

    }
    /*
     * 功能:计算SSIM
     * s1: 一帧受损数据
     * s2: 一帧原始数据
     * i_width: 图像宽
     * i_height: 图像高
     */

    private float x264_pixel_ssim_wxh(byte[] s1, int stride1, byte[] s2, int stride2, int width, int height) {
        int x, y, z;
        float ssim = 0;
        //按照4x4的块对像素进行处理的。使用sum1保存上一行块的“信息”,sum0保存当前一行块的“信息”
        ArrayList<int[]> sum0 = new ArrayList<>(width);
        /*
         * sum0是一个数组指针,其中存储了一个4元素数组的地址
         * 换句话说,sum0中每一个元素对应一个4x4块的信息(该信息包含4个元素)。
         *
         * 4个元素中:
         * [0]原始像素之和
         * [1]受损像素之和
         * [2]原始像素平方之和+受损像素平方之和
         * [3]原始像素*受损像素的值的和
         *
         */
        ArrayList<int[]> sum1 = new ArrayList<>(width);
        width >>= 2;
        //除以4
        height >>= 2;
        z = 0;

        for (y = 1; y < height; y++) {
            //下面这个循环,只有在第一次执行的时候执行2次,处理第1行和第2行的块
            //后面的都只会执行一次
            for (; z <= y; z++) {
                //执行完XCHG()之后,sum1[]存储上1行块的值(在上面),而sum0[]等待ssim_4x4x2_core()计算当前行的值(在下面)
                ArrayList<int[]> temp = new ArrayList<>(width);

                //System.arraycopy(sum0,0,temp,width-1,width);
                if (sum0.size() > 0) {
                    temp.addAll(sum0);
                    sum0 = new ArrayList<>(sum1);
                    sum1 = new ArrayList<>(temp);
                }
                //获取4x4块的信息(4个值存于长度为4的一维数组)(这里并没有代入公式计算SSIM结果)
                //结果存储在sum0中。从左到右每个4x4的块依次存储在sum0.get[0],sum0.get[1],sum0.get[2]...
                //每次x前进2个块
                /*
                 * ssim_4x4x2_core():计算2个4x4块,两个4×4有一半重叠部分
                 * +----+----+
                 * |    |    |
                 * +----+----+
                 */
                for (x = 0; x < width; x += 2) {
                    sum0.add(ssim_4x4x2_core(4 * (x + z * stride1), 4 * (x + z * stride2), s1, stride1, s2, stride2));
                    sum0.add(ssim_4x4x2_core(4 * (x + z * stride1) + 4, 4 * (x + z * stride2) + 4, s1, stride1, s2, stride2));
                    if (sum0.isEmpty() || sum0.get(x) == null || sum0.get(x + 1) == null || sum0.get(x).length != 4 || sum0.get(x).length != 4) {
                        return -1;
                    }
                }
            }
            for (x = 0; x < width; x += 4) {
                //sum1是储存上一行的信息,sum0是储存本行的信息,ssim_end4是进行2(line)×4×4×2 2行每行2个4×4的块的单元进行处理
                ssim += ssim_end4(x, sum0, sum1, Math.min(4, width - x - 1));
            }
            sum1.clear();
        }
        return ssim / ((width - 1) * (height - 1));
    }

    private int[] ssim_4x4x2_core(int shift1, int shift2, byte[] source1, int stride1, byte[] source2, int stride2) {

        int x, y, z;
        //“信息”包含4个元素:
        //
        //s1:原始像素之和;
        //
        //s2:受损像素之和;
        //
        //ss:原始像素平方之和+受损像素平方之和;
        //
        //s12:原始像素*受损像素的值的和。
        //每次计算两个4×4的方格的信息

        int[] sum = new int[4];

        int s1 = 0;
        int s2 = 0;
        int ss = 0;
        int s12 = 0;
        for (y = 0; y < 4; y++) {
            for (x = 0; x < 4; x++) {
                int a = source1[x + y * stride1 + shift1] & 0xFF;
                int b = source2[x + y * stride2 + shift2] & 0xFF;
                s1 += a;
                s2 += b;
                ss += a * a;
                ss += b * b;
                s12 += a * b;
            }
        }
        sum[0] = s1;
        sum[1] = s2;
        sum[2] = ss;
        sum[3] = s12;

        return sum;
    }

    private double ssim_end4(int shift, ArrayList<int[]> sum0, ArrayList<int[]> sum1, int width) {
        double ssim = 0.0;
        for (int i = 0; i < width; i++) {

            ssim += ssim_end1(sum0.get(shift + i)[0] + sum0.get(shift + i + 1)[0] + sum1.get(shift + i)[0] + sum1.get(shift + i + 1)[0],
                    sum0.get(shift + i)[1] + sum0.get(shift + i + 1)[1] + sum1.get(shift + i)[1] + sum1.get(shift + i + 1)[1],
                    sum0.get(shift + i)[2] + sum0.get(shift + i + 1)[2] + sum1.get(shift + i)[2] + sum1.get(shift + i + 1)[2],
                    sum0.get(shift + i)[3] + sum0.get(shift + i + 1)[3] + sum1.get(shift + i)[3] + sum1.get(shift + i + 1)[3]);
        }
        return ssim;
    }

    private double ssim_end1(int s1, int s2, int ss, int s12) {
        int ssim_c1 = (int) (.01 * .01 * 255 * 255 * 64 + .5);
        int ssim_c2 = (int) (.03 * .03 * 255 * 255 * 64 * 63 + .5);
        int vars = ss * 64 - s1 * s1 - s2 * s2;
        int covar = s12 * 64 - s1 * s2;
        return (float) (2 * s1 * s2 + ssim_c1) * (float) (2 * covar + ssim_c2) / ((float) (s1 * s1 + s2 * s2 + ssim_c1) * (float) (vars + ssim_c2));
    }


    public float getPsnr(byte[] frame1, byte[] frame2, int framecount) {
        if (width == 0 || height == 0 || yuvSource.isEmpty() || dstSource.isEmpty()) {//YuvFormat????
            return -1;
        }

        Message message = new Message();
        if (framecount == 0) {
            message.what = -2;
            message.arg1 = 2;
            handler.sendMessage(message);
        }

        float mse = 0;
        float diff;
        float tem_psnr;
        int num = width * height;

        for (int n = 0; n < num; n++) {
            diff = ((frame1[n] & 0XFF) - (frame2[n] & 0xFF));
            mse += diff * diff;
        }

        mse = mse / (float) num;
        tem_psnr = (float) (10 * StrictMath.log10((255.0 * 255.0) / mse));

        Message message1 = new Message();
        message1.what = 2;
        message1.arg1 = framecount;
        handler.sendMessage(message1);
        return tem_psnr;

    }

    public int start() {
        return get2ImgArrayByFrame(new File(yuvSource), new File(dstSource));
    }

    private int get2ImgArrayByFrame(File imgSrcYuv, File imgDstFile) {
        if (width == 0 || height == 0 || yuvSource.isEmpty() || dstSource.isEmpty()) {//YuvFormat????
            return 0;
        }
        byte[] pictureArray1;
        byte[] pictureArray2;

        pictureArray1 = new byte[F];
        pictureArray2 = new byte[F];

        int framCount = 0;
        try {
            FileInputStream inp1 = new FileInputStream(imgSrcYuv);
            FileInputStream inp2= new FileInputStream(imgDstFile);

            if (isSsim) {
                File ssim = new File(filename + "_ssim.txt"); // 相对路径,如果没有则要建立一个新的output。txt文件
                ssim.createNewFile(); // 创建新文件
                BufferedWriter out2 = new BufferedWriter(new FileWriter(ssim));

                for (framCount = 0; ; ) {
                    if (inp1.read(pictureArray1) == -1 || inp2.read(pictureArray2) == -1) {
                        if (isRD) {
                            out2.write("AVE_SSIM is " + ave_ssim / framCount + "\n");
                        }
                        out2.flush();
                        out2.close();
                        inp1.close();
                        inp2.close();


                        Message message = new Message();
                        message.what = 0;
                        message.arg1 = 1;
                        handler.sendMessage(message);

                        break;
                    } else {
                        double tem_ssim = getSsim(pictureArray1, pictureArray2, framCount);
                        ave_ssim += tem_ssim;
                        out2.write(tem_ssim + "\n");
                        framCount++;
                    }
                }
            } else {
                File psnr = new File(filename + "_psnr.txt"); // 相对路径,如果没有则要建立一个新的output。txt文件
                psnr.createNewFile(); // 创建新文件
                BufferedWriter out1 = new BufferedWriter(new FileWriter(psnr));

                for (framCount = 0; ; ) {
                    if (inp1.read(pictureArray1) == -1 || inp2.read(pictureArray2) == -1) {
                        if (isRD) {
                            out1.write("AVE_PSNR is " + ave_psnr / framCount + "\n");
                        }
                        out1.flush();
                        out1.close();
                        inp1.close();
                        inp2.close();

                        if (isRD) {
                            isSsim = true;
                            start();
                        } else {
                            Message message = new Message();
                            message.what = 0;
                            message.arg1 = 2;
                            handler.sendMessage(message);
                        }

                        break;
                    } else {
                        float tem_psnr = getPsnr(pictureArray1, pictureArray2, framCount);
                        ave_psnr += tem_psnr;
                        out1.write(tem_psnr + "\n");
                        framCount++;
                    }

                }

            }

        } catch (Exception e) {
            Log.i("getImgArray", "Exception" + e);
            return 0;
            //JOptionPane.showMessageDialog(null,"Loading Image Data, Click to continue....","PSNR Calculation",JOptionPane.INYuvFormatION_MESSAGE);
        }
        return framCount;
    }

    public boolean getIssSim() {
        return isSsim;
    }

    public void setIssSim(boolean bo) {
        isSsim = bo;
    }

    public void setRD(boolean bo) {
        isRD = bo;
    }


}

上一篇 下一篇

猜你喜欢

热点阅读