H264中I帧编码分析-以jcodec为例

 计算机工程中知识,只要不是从源代码中获取的,都是二手知识。我原来打算,看看能否在视频编解码方向找到一些研究点的。当然,现在也没有找到,准备放弃,遂有此篇,作为记录。之前看了很多编码器相关的博客,大多数讲解甚是模糊。也看了一些书,其中[1]写的还是不错的。JM,X264又太复杂。
 之前在github上发现一个比较mini的H264编码器[2],没有看懂,毕竟一万多行呢,但是他的参考列表里有jcodec[3],晚上看了一通,竟然有点懂了,因为它足够简单。帧内预测只实现了DC模式,也有一个简单的码率控制模块。简单分析下I帧的编码过程。
H264Encoder.java

    private void encodeSlice(SeqParameterSet sps, PictureParameterSet pps, Picture pic, ByteBuffer dup, boolean idr,
            int frameNum, SliceType sliceType, int qp) {
        for (int mbY = 0, mbAddr = 0; mbY < sps.picHeightInMapUnitsMinus1 + 1; mbY++) {
            for (int mbX = 0; mbX < sps.picWidthInMbsMinus1 + 1; mbX++, mbAddr++) {  
                 do {
                    candidate = sliceData.fork();
                    totalQpDelta += qpDelta;
                    encodeMacroblock(mbType, pic, mbX, mbY, candidate, qp, totalQpDelta);
                    //码率控制模块
                    qpDelta = rc.accept(candidate.position() - sliceData.position());
                    if (qpDelta != 0)
                        restoreMacroblock(mbType);
                } while (qpDelta != 0);
                sliceData = candidate;
                qp += totalQpDelta;

                collectPredictors(outMB.getPixels(), mbX);
                addToReference(mbX, mbY);           
            }
            }      
}

 encodeMacroblock编码一个(16X16)的宏块。collectPredictors(outMB.getPixels(), mbX),宏块被编码后,会被重新解码到outMB,用于预测。 addToReference(mbX, mbY),把outMB添加到参考帧中,在编码P帧时会用到。
H264Encoder.java

    private void encodeMacroblock(MBType mbType, Picture pic, int mbX, int mbY, BitWriter candidate, int qp, int qpDelta) {
        if (mbType == MBType.I_16x16) {
            mbEncoderI16x16.save();
            mbEncoderI16x16.encodeMacroblock(pic, mbX, mbY, candidate, outMB, mbX > 0 ? topEncoded[mbX - 1] : null,
                    mbY > 0 ? topEncoded[mbX] : null, qp + qpDelta, qpDelta);
        } else if (mbType == MBType.P_16x16) {
            mbEncoderP16x16.save();
            mbEncoderP16x16.encodeMacroblock(pic, mbX, mbY, candidate, outMB, mbX > 0 ? topEncoded[mbX - 1] : null,
                    mbY > 0 ? topEncoded[mbX] : null, qp + qpDelta, qpDelta);
        } else
            throw new RuntimeException("Macroblock of type " + mbType + " is not supported.");
    }

 只简单分析第一个分叉。
MBEncoderI16x16.java

    public void encodeMacroblock(Picture pic, int mbX, int mbY, BitWriter out, EncodedMB outMB,
            EncodedMB leftOutMB, EncodedMB topOutMB, int qp, int qpDelta) {
        CAVLCWriter.writeUE(out, 0); // Chroma prediction mode -- DC
        CAVLCWriter.writeSE(out, qpDelta); // MB QP delta

        outMB.setType(MBType.I_16x16);
        outMB.setQp(qp);
		//编码Y
        luma(pic, mbX, mbY, out, qp, outMB.getPixels(), cavlc[0]);
        //编码U,V
        chroma(pic, mbX, mbY, out, qp, outMB.getPixels());

        new MBDeblocker().deblockMBI(outMB, leftOutMB, topOutMB);
    }

MBEncoderI16x16.java

    private void luma(Picture pic, int mbX, int mbY, BitWriter out, int qp, Picture outMB, CAVLC cavlc) {
        int x = mbX << 4;
        int y = mbY << 4;
        int[][] ac = new int[16][16];
        byte[][] pred = new byte[16][16];
		//帧内预测,只实现了DC模式
        lumaDCPred(x, y, pred);
        //求残差,并进行Hadamard变换。
        transform(pic, 0, ac, pred, x, y);
        int[] dc = extractDC(ac);
        writeDC(cavlc, mbX, mbY, out, qp, mbX << 2, mbY << 2, dc, I_16x16, I_16x16);
        writeAC(cavlc, mbX, mbY, out, mbX << 2, mbY << 2, ac, qp, I_16x16, I_16x16, DUMMY);

        restorePlane(dc, ac, qp);

        for (int blk = 0; blk < ac.length; blk++) {
            MBEncoderHelper.putBlk(outMB.getPlaneData(0), ac[blk], pred[blk], 4, BLK_X[blk], BLK_Y[blk], 4, 4);
        }
    }
        private void transform(Picture pic, int comp, int[][] ac, byte[][] pred, int x, int y) {
        for (int i = 0; i < ac.length; i++) {
            int[] coeff = ac[i];
            MBEncoderHelper.takeSubtract(pic.getPlaneData(comp), pic.getPlaneWidth(comp), pic.getPlaneHeight(comp), x
                    + BLK_X[i], y + BLK_Y[i], coeff, pred[i], 4, 4);
            CoeffTransformer.fdct4x4(coeff);
        }
    }

 宏块大小为16X16,被分成了16个4X4的子块,对子块进行Hadamard变换,但是总变换后的系数个数,仍等于宏块内像素的个数。一个4X4的块,变换为产生16个系数,因此 ac = new int[16][16]。writeAC还要进行量化操作。restorePlane进行反量化,逆变化,最会把恢复的数据放进outMB中。因为在编码下一块的时候,就需要使用此块的左列像素,或者上列像素。这句话说的有点饶,就是lumaDCPred函数需要用到,即16X16的DC预测模式。
在这里插入图片描述

    private void lumaDCPred(int x, int y, byte[][] pred) {
        int dc;
        if (x == 0 && y == 0)
            dc = 0;
        else if (y == 0)
            dc = (ArrayUtil.sumByte(leftRow[0]) + 8) >> 4;
        else if (x == 0)
            dc = (ArrayUtil.sumByte3(topLine[0], x, 16) + 8) >> 4;
        else
            dc = (ArrayUtil.sumByte(leftRow[0]) + ArrayUtil.sumByte3(topLine[0], x, 16) + 16) >> 5;

        for (int i = 0; i < pred.length; i++)
            for (int j = 0; j < pred[i].length; j++)
                pred[i][j] += dc;
    }

 这里面的三种处理情况,书[1]的p71有说明,有一处不同,书中说当P(x,-1)与P(-1,y)都不可用的时候,预测值是128,代码是0。那就不知道会产生什么后果了。
 outMB是怎样和leftRow和topLine在什么地方产生联系的?

    private void collectPredictors(Picture outMB, int mbX) {
        arraycopy(outMB.getPlaneData(0), 240, topLine[0], mbX << 4, 16);
        arraycopy(outMB.getPlaneData(1), 56, topLine[1], mbX << 3, 8);
        arraycopy(outMB.getPlaneData(2), 56, topLine[2], mbX << 3, 8);

        copyCol(outMB.getPlaneData(0), 15, 16, leftRow[0]);
        copyCol(outMB.getPlaneData(1), 7, 8, leftRow[1]);
        copyCol(outMB.getPlaneData(2), 7, 8, leftRow[2]);
    }

[1] 深入理解视频编解码技术-基于H.264标准及参考模型
[2] minih264 https://github.com/lieff/minih264
[3]jcodec https://github.com/jcodec/jcodec
[4] Intra Luma Prediction https://www.cnblogs.com/TaigaCon/p/4190806.html
[5] Hadamard变换 https://www.cnblogs.com/xkfz007/archive/2012/07/31/2616791.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章