Java实现视频初步压缩和解压

2023年 9月 27日 40.7k 0

本文主要做了什么

从摄像头读取每一帧的图片,用一些简单的方法将多张图片信息压缩到一份文件中(自定义的视频文件),自定义解码器读取视频文件,并将每帧图片展示成视频

第一步:按照某些算法帧内压缩

常见的视频压缩算法(H264,H265,MP4)过程很复杂,实现的压缩比率也很恐怖(H265可以做到0.5%的压缩率,也就是就算每帧图片加起来有2个GB,合并起来的视频也就10MB),其中压缩算法流程大致如下,我的程序没有细究算法,简单实现了25%的压缩率。

帧内压缩:
  • 帧分割:
    将原本RGB格式的图像用YUV表示,用YUV是将原本的像素信息转化成亮度和色度信息,由于人眼对色度的变化并不敏感,所以YUV可以在多个像素点之上采用同一数据以实现数据压缩。具体的做法是:将原本图片分成22 / 44 / 88 / 1616的宏块,每个宏块(4*4为例)内按照YUV格式数据采集——记录每个像素格的亮度Y,记录每横向两个像素格的色度U,记录每个宏块左上角像素各的色度V。算法将Y,U,V分别存储,再在接收端分别取出某个宏块对应的数据,恢复成YUV,再恢复成RGB。
  • 帧内预测:
    邻近的宏块之间可以进行预测,算法思想是由一个宏块,通过某种预测模式,得到一个预测的模块,将实际值和预测值之间的残差进行保存。
  • 离散余弦变换(DCT)
    对每个块的残差执行DCT变换,算法思想是:图像数据分为细节、纹理和快速变化这类的高频信息,和像整体趋势、平均值和慢速变化这类低频信息;DCT主要保留包含了数据整体特征的低频信息。
  • 量化:
    由于DCT的结果中浮点数较多,量化将其截断为整数以减少数据量
  • 熵编码:
    熵编码用于编码多种类型的信息,像文本、图像、音频等信息根据数据的概率分布(如字符、像素、采样值)映射为可变长度的编码。经典哈夫曼树就是一种实现。在此就是将像素值/YUV值根据其概率分布设置不同编码。
  • 帧间压缩:
  • 帧间预测:
    由于很多帧之间存在冗余,算法首先选择一个参考帧,然后计算参考帧和当前帧之间的运动矢量,由此去除冗余信息
  • 运动补偿...
  • 残差计算...
  • ...
  • 我的代码:

  • 主要Controller:
    @GetMapping("/compressedVideos")
    public void getCompressedBytes() throws IOException {
    	//录制5秒的视频,存在List中
        webcam.open();
        long startTime = System.currentTimeMillis();
        List bufferedImages = new ArrayList();
        while (System.currentTimeMillis() - startTime < 5000) {
            BufferedImage image = webcam.getImage();
            bufferedImages.add(image);
        }
        System.out.println("录制结束");
        webcam.close();
        //调用压缩方法,将结果写入文件中
        byte[] bytes = outerCompressionUtils.photosToCompressedBytes(bufferedImages);
        File file = new File("压缩中的压缩.dat");
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(bytes);
        fos.close();
        System.out.println("持久化结束");
    }
    
  • 压缩:
  • 工具方法:将rgb转化成YUV
  • public static int[] rgb2YUV(int rgb) {
        int[] rgb1 = photoOps.RGBToInts(rgb);
        int red = rgb1[0];
        int green = rgb1[1];
        int blue = rgb1[2];
    
        int Y = (int) (0.299 * red + 0.587 * green + 0.114 * blue -128); //-128 到 127
        int U = (int) (-0.1684 * red - 0.3316 * green + 0.5 * blue);//-128 到 127
        int V = (int) (0.5 * red - 0.4187 * green - 0.083 * blue); //-128 到 127
    
        return new int[]{Y, U, V};
    }
    
  • 工具方法:一张图片化成YUV
  • public static byte[] compressToOneChannel(BufferedImage bufferedImage) {
    
        byte[] Ys = new byte[bufferedImage.getWidth() * bufferedImage.getHeight()];
        byte[] Us = new byte[bufferedImage.getHeight() * (bufferedImage.getWidth() / 2)];
        byte[] Vs = new byte[(bufferedImage.getWidth() / 2) * (bufferedImage.getHeight() / 2)];
    
        int targetYs = 0;
        int targetUs = 0;
        int targetVs = 0;
    
    	/*
    	这里就是遍历2*2的宏块,将其中对应YUV分别写到YUV的数组中
    	需要注意的是我犯的一个错误:没有注意到Y和U的遍历过程,导致在解码的时候图片异常
    	*/
        for (int i = 0; i < bufferedImage.getHeight(); i += 2) {
            for (int j = 0; j < bufferedImage.getWidth(); j += 2) {
                for (int k = 0; k < 2; k++) {
                    for (int l = 0; l < 2; l++) {
                        int[] ints = rgb2YUV(bufferedImage.getRGB(j + l, i + k));
                        int Y = ints[0];
                        Ys[targetYs] = (byte) (Y);
                        targetYs++;
                    }
                    int[] ints = rgb2YUV(bufferedImage.getRGB(j, i + k));
                    int U = ints[1];
                    Us[targetUs] = (byte) (U);
                    targetUs++;
                }
                int[] ints = rgb2YUV(bufferedImage.getRGB(j, i));
                int V = ints[2];
                Vs[targetVs] = (byte) (V);
                targetVs++;
            }
    
        }
        int length1 = Ys.length; //大小估计 : 图片3000*2000 = 6000000 不会超int范围
        int length2 = Us.length;
        int length3 = Vs.length;
    
    
        byte[] targetBytes = new byte[4 * 5 + length1 + length2 + length3];
        int targetIndex = 0;
    	
    	//这里是将byte[]开头填充一些用于解码的信息,因为Ys,Us,Vs都是一起传的,需要在包开头标明每个数组长度
    	//Y区的长度
        byte[] bytes1 = intToByte(length1);
        for (byte b : bytes1) {
            targetBytes[targetIndex] = b;
            targetIndex++;
        }
        //U区长度
        byte[] bytes2 = intToByte(length2);
        for (byte b : bytes2) {
            targetBytes[targetIndex] = b;
            targetIndex++;
        }
        //V区长度
        byte[] bytes3 = intToByte(length3);
        for (byte b : bytes3) {
            targetBytes[targetIndex] = b;
            targetIndex++;
        }
        //图片的高
        byte[] bytes4 = intToByte(bufferedImage.getHeight());
        for (byte b : bytes4) {
            targetBytes[targetIndex] = b;
            targetIndex++;
        }
        //图片的宽
        byte[] bytes5 = intToByte(bufferedImage.getWidth());
        for (byte b : bytes5) {
            targetBytes[targetIndex] = b;
            targetIndex++;
        }
    
    	//传递真实数据
        for (byte y : Ys) {
            targetBytes[targetIndex] = y;
            targetIndex++;
        }
    
        for (byte u : Us) {
            targetBytes[targetIndex] = u;
    
            targetIndex++;
        }
    
        for (byte v : Vs) {
            targetBytes[targetIndex] = v;
            targetIndex++;
        }
        return targetBytes;
    
    }
    
  • 工具方法:多张图片化成YUV并压缩
    public static byte[] photosToCompressedBytes(List bufferedImages) throws IOException {
    
        //数据流中未必要有各种辅助信息,比如各类字段长度,在外规定好算了
        //这里每一帧的长度就是:20 + 640 * 480 * 1.75
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        //java提供的压缩工具,此输出流将输出的东西压缩输出
        //传入的Deflater对象用于控制压缩算法
        DeflaterOutputStream dos = new DeflaterOutputStream(baos,new Deflater());
    
    	//帧信息添加到压缩流
        for (BufferedImage bufferedImage: bufferedImages
             ) {
            byte[] bytes = innerCompressionUtils.compressToOneChannel(bufferedImage);
            System.out.println("一帧的长度为:"+bytes.length);
            dos.write(bytes);
        }
        byte[] compressedData = baos.toByteArray();
        return compressedData;
    }
    
  • 尝试用哈夫曼编码优化
  • class HuffmanNode implements Comparable{
    
        byte value;
        int frequency;
    
        HuffmanNode left;
        HuffmanNode right;
    
        public HuffmanNode(byte value,int frequency){
            this.value = value;
            this.frequency = frequency;
        }
    
        @Override
        public int compareTo(@NotNull HuffmanNode o) {
            return this.frequency - o.frequency;
        }
    }
    
    public class Huffman {
    
        public static Map encodingTable;
    
        public static String huffmanEncoding(byte[] originalBytes){
            Map frequencyMap = new HashMap();
    
            for (byte b: originalBytes
                 ) {
                frequencyMap.put(b, frequencyMap.getOrDefault(b,0)+1);
            }
            PriorityQueue minHeap = new PriorityQueue();
    
            for (Map.Entry entry : frequencyMap.entrySet()
                    ) {
                minHeap.add(new HuffmanNode(entry.getKey(),entry.getValue()));
            }
            while (minHeap.size()>1){
                HuffmanNode left = minHeap.poll();
                HuffmanNode right = minHeap.poll();
    
                HuffmanNode mergeNode = new HuffmanNode((byte)0, left.frequency + right.frequency);
                mergeNode.left = left;
                mergeNode.right = right;
    
                minHeap.add(mergeNode);
            }
            encodingTable = new HashMap();
            HuffmanNode root = minHeap.poll();
    
            buildEncodingTable(root,"",encodingTable);
    
            StringBuilder encodingData = new StringBuilder();
            for (Byte b: originalBytes
                 ) {
                encodingData.append(encodingTable.get(b));
            }
            System.out.println("原始数组长度"+originalBytes.length);
            System.out.println("哈夫曼后数组长度"+encodingData.length());
            return encodingData.toString();
    
        }
    
    public static void buildEncodingTable(HuffmanNode node,String currentCode,Map encodingMap) {
    
            if (node == null) {
                return;
            }
            if (node.left == null && node.right == null) {
                encodingMap.put(node.value, currentCode);
            } else {
                buildEncodingTable(node.left, currentCode + "0", encodingMap);
                buildEncodingTable(node.right, currentCode + "1", encodingMap);
            }
        }
    

    但其实这里用哈夫曼并不会优化数据量,原因如下:
    我传输的数据是-128到127的byte类型,这些byte来自图片的亮度和色度,调试中发现这255个数字出现的频率差不多,全部都在14万到20万之间,两个最小值加起来任然比最大值大,这就意味着这颗哈夫曼树会比较满,类似完全二叉树,于是就无法区分出现频率最高的某个字符。

    另外,原本255个数将8位byte全都占满,假如有一个频率很高的元素,我们把较短的0101赋给它,那势必会导致原本以0101开头的元素用8位以上的长度进行表示,而程序中各元素出现频率相近,这就会导致如果有元素用短于8位的编码,其他长于8位编码的元素会导致数据更加庞大。

    我在用huffman编码后,数据量一点都没有变,只是由长度为40647865的byte数组变成长度为325182920的字符串,其实就是×8 。怀疑是代码哪里错了...

    常见的压缩算法是将DCT变换后的结果进行哈夫曼编码,DCT变换后低频信息和高频信息自然区分开,确实更适合这个熵编码方法

    在这里插入图片描述
    在这里插入图片描述

  • 解压:
  • 先将java zip包的压缩过程解压
  • public static InflaterInputStream inflaterCompressedBytes(byte[] bytes) throws IOException {
            //解压数据
            ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
            
            InflaterInputStream lis = new InflaterInputStream(bais, new Inflater());
            
            return lis;
        }
    
  • 依据压缩时自定义的格式进行对byte数组解析
  • public static BufferedImage getBfi(byte[] originalBytes) {
    //分别先把开头表示各个区长度以及图片宽高的参数取出来
    byte one = originalBytes[0];
    byte two = originalBytes[1];
    byte three = originalBytes[2];
    byte four = originalBytes[3];

    int Y = ((one & 0xff)

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论