Java实现视频初步压缩和解压的代码示例

 更新时间:2023年10月12日 08:53:16   作者:Ha_Ha_Wu  
从摄像头读取每一帧的图片,用一些简单的方法将多张图片信息压缩到一份文件中(自定义的视频文件),自定义解码器读取视频文件,并将每帧图片展示成视频,本文主要介绍了Java实现视频初步压缩和解压,需要的朋友可以参考下

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

常见的视频压缩算法(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<BufferedImage> 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<BufferedImage> 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<HuffmanNode>{
    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<Byte,String> encodingTable;
    public static String huffmanEncoding(byte[] originalBytes){
        Map<Byte,Integer> frequencyMap = new HashMap<>();
        for (byte b: originalBytes
             ) {
            frequencyMap.put(b, frequencyMap.getOrDefault(b,0)+1);
        }
        PriorityQueue<HuffmanNode> minHeap = new PriorityQueue<>();
        for (Map.Entry<Byte, Integer> 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<Byte,String> 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) << 24) | ((two & 0xff) << 16) | ((three & 0xff) << 8) | (four & 0xff);
        byte one2 = originalBytes[4];
        byte two2 = originalBytes[5];
        byte three2 = originalBytes[6];
        byte four2 = originalBytes[7];
        int U = ((one2 & 0xff) << 24) | ((two2 & 0xff) << 16) | ((three2 & 0xff) << 8) | (four2 & 0xff);
        byte one3 = originalBytes[8];
        byte two3 = originalBytes[9];
        byte three3 = originalBytes[10];
        byte four3 = originalBytes[11];
        int V = ((one3 & 0xff) << 24) | ((two3 & 0xff) << 16) | ((three3 & 0xff) << 8) | (four3 & 0xff);
        byte one4 = originalBytes[12];
        byte two4 = originalBytes[13];
        byte three4 = originalBytes[14];
        byte four4 = originalBytes[15];
        int height = ((one4 & 0xff) << 24) | ((two4 & 0xff) << 16) | ((three4 & 0xff) << 8) | (four4 & 0xff);
        byte one5 = originalBytes[16];
        byte two5 = originalBytes[17];
        byte three5 = originalBytes[18];
        byte four5 = originalBytes[19];
        int width = ((one5 & 0xff) << 24) | ((two5 & 0xff) << 16) | ((three5 & 0xff) << 8) | (four5 & 0xff);
        System.out.println("Y: " + Y);
		//将数据读取出来
        byte[] Ys = Arrays.copyOfRange(originalBytes, 20, Y + 20);
        byte[] Us = Arrays.copyOfRange(originalBytes, Y + 20, Y + U + 20);
        byte[] Vs = Arrays.copyOfRange(originalBytes, Y + U + 20, Y + U + V + 20);
        BufferedImage bfi = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        int hongW = width / 2;
        int hongH = height / 2;
		//用YUV数据恢复成RGB,填充到图片的每一个像素
        for (int i = 0; i < height - 1; i++) {
            for (int j = 0; j < width - 1; j++) {
                int H = i / 2;
                int W = j / 2;
                byte y = Ys[(i / 2 * 2) * width + j / 2 * 4 + (i % 2) * 2 + j % 2];
                byte u = Us[H * hongW * 2 + j / 2 * 2 + i % 2];
                byte v = Vs[H * hongW + W];
                int r = (int) (y + 128 + 1.14075 * (v));
                int g = (int) (y + 128 - 0.3455 * (u) - 0.7169 * (v));
                int b = (int) (y + 128 + 1.779 * (u));
                r = Math.min(255, Math.max(0, r));
                g = Math.min(255, Math.max(0, g));
                b = Math.min(255, Math.max(0, b));
                int color = (r) << 16 | (g) << 8 | b;
                if (i < 1 && j < 20) {
                bfi.setRGB(j, i, color);
            }
        }
        return bfi;
    }
  • 简单的播放器(基于Swing)
FileInputStream fileInputStream = new FileInputStream("C:\\Users\\吴松林\\IdeaProjects\\meitu2\\压缩中的压缩.dat");
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); //此输出流中写入所有信息,最后转出为byte[],类似桶子
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = fileInputStream.read(buffer))!=-1){
            outputStream.write(buffer,0,bytesRead);
        }
        byte[] data = outputStream.toByteArray();
        InflaterInputStream iutputStream1 = utils.inflaterCompressedBytes(data); //解压
        BufferedInputStream bis = new BufferedInputStream(iutputStream1);
        List<BufferedImage> bufferedImages = new ArrayList<>();
        byte[] eachImage = new byte[(int) (20+640*480*1.75)];
        int testIndex = 0;
        int index;
        System.out.println("length: "+eachImage.length);
        try {
            while ((index = bis.read(eachImage)) != -1) {
                System.out.println("本次读取长度:" + index);
                testIndex++;
                System.out.println("test: " + testIndex);
                BufferedImage bfi = utils.getBfi(eachImage);
                bufferedImages.add(bfi);
            }
        }catch (Exception e){
            System.out.println("跳过异常,省略最后一张图片");
            e.printStackTrace();
        }
        bis.close();
        iutputStream1.close();
        outputStream.close();
        fileInputStream.close();
        JFrame jFrame = new JFrame();
        myPanel panel = new myPanel();
        jFrame.add(panel);
        jFrame.setSize(new Dimension(640,480));
        jFrame.setVisible(true);
        panel.list = bufferedImages;
        while (true){
            panel.repaint();
        }
    }
}
class myPanel extends JPanel{
    int index = 0;
    List<BufferedImage> list;
    @Override
    public void paint(Graphics g) {
        g.drawImage(list.get(index), 0, 0, null);
        if (index < list.size() - 2) {
            index++;
        }
        try {
            Thread.sleep(34);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}

注意:

  • zip包在使用时我遇到报:Unexpected end of ZLIB input stream,没找到很合适的解决办法,但发现这个异常是在读取到最后一张图片时才触发,于是我选择舍弃最后一张图
  • 这个播放器只用Swing简单写了一个用于测试能否读取文件,很明显我的播放器只能播放我的视频,因为其解码方式和编码方式息息相关,而各种常见的编码方式里的算法又太过复杂。所以这个程序就相当于写着玩而已,和其他视频/播放器难有半点干系。

以上就是Java实现视频初步压缩和解压的代码示例的详细内容,更多关于Java视频压缩和解压的资料请关注脚本之家其它相关文章!

相关文章

  • SpringBoot使用Shiro实现动态加载权限详解流程

    SpringBoot使用Shiro实现动态加载权限详解流程

    本文小编将基于 SpringBoot 集成 Shiro 实现动态uri权限,由前端vue在页面配置uri,Java后端动态刷新权限,不用重启项目,以及在页面分配给用户 角色 、 按钮 、uri 权限后,后端动态分配权限,用户无需在页面重新登录才能获取最新权限,一切权限动态加载,灵活配置
    2022-07-07
  • 使用stream的Collectors.toMap()方法常见的问题及解决

    使用stream的Collectors.toMap()方法常见的问题及解决

    这篇文章主要介绍了使用stream的Collectors.toMap()方法常见的问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-03-03
  • SpringCloud Bus如何实现配置刷新

    SpringCloud Bus如何实现配置刷新

    这篇文章主要介绍了SpringCloud Bus如何实现配置刷新,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-09-09
  • 实现quartz定时器及quartz定时器原理介绍

    实现quartz定时器及quartz定时器原理介绍

    Quartz是一个大名鼎鼎的Java版开源定时调度器,功能强悍,使用方便,下面我们看看如何使用它
    2013-12-12
  • mybatis中注解映射SQL示例代码

    mybatis中注解映射SQL示例代码

    这篇文章主要给大家介绍了关于mybatis中注解映射SQL的相关资料,文中给出了详细的示例代码供大家参考学习,对大家的学习或者共组具有一定的参考学习价值,需要的朋友们下面跟着小编来一起学习学习吧。
    2017-08-08
  • SpringCloud Sleuth实现分布式请求链路跟踪流程详解

    SpringCloud Sleuth实现分布式请求链路跟踪流程详解

    这篇文章主要介绍了SpringCloud Sleuth实现分布式请求链路跟踪流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2022-11-11
  • Java 实现微信和支付宝支付功能

    Java 实现微信和支付宝支付功能

    这篇文章主要介绍了Java 实现微信和支付宝支付功能,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-02-02
  • java:无法访问org.springframework.boot.SpringApplication的解决方法

    java:无法访问org.springframework.boot.SpringApplication的解决方法

    这篇文章主要给大家介绍了关于java:无法访问org.springframework.boot.SpringApplication的解决方法,文中通过实例代码将解决的办法介绍的非常详细,需要的朋友可以参考下
    2023-01-01
  • logback-spring.xml的内容格式详解

    logback-spring.xml的内容格式详解

    这篇文章主要介绍了logback-spring.xml的内容格式详解,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的的朋友参考下吧
    2023-11-11
  • JAVA中的FileWriter流解析

    JAVA中的FileWriter流解析

    这篇文章主要介绍了JAVA中的FileWriter流解析,FileWriter类提供了多种写入字符的方法,包括写入单个字符、写入字符数组和写入字符串等,它还提供了一些其他的方法,如刷新缓冲区、关闭文件等,需要的朋友可以参考下
    2023-10-10

最新评论