Java實現(xiàn)視頻初步壓縮和解壓的代碼示例
第一步:按照某些算法幀內(nèi)壓縮
常見的視頻壓縮算法(H264,H265,MP4)過程很復(fù)雜,實現(xiàn)的壓縮比率也很恐怖(H265可以做到0.5%的壓縮率,也就是就算每幀圖片加起來有2個GB,合并起來的視頻也就10MB),其中壓縮算法流程大致如下,我的程序沒有細(xì)究算法,簡單實現(xiàn)了25%的壓縮率。
幀內(nèi)壓縮:
- 幀分割: 將原本RGB格式的圖像用YUV表示,用YUV是將原本的像素信息轉(zhuǎn)化成亮度和色度信息,由于人眼對色度的變化并不敏感,所以YUV可以在多個像素點之上采用同一數(shù)據(jù)以實現(xiàn)數(shù)據(jù)壓縮。具體的做法是:將原本圖片分成22 / 44 / 88 / 1616的宏塊,每個宏塊(4*4為例)內(nèi)按照YUV格式數(shù)據(jù)采集——記錄每個像素格的亮度Y,記錄每橫向兩個像素格的色度U,記錄每個宏塊左上角像素各的色度V。算法將Y,U,V分別存儲,再在接收端分別取出某個宏塊對應(yīng)的數(shù)據(jù),恢復(fù)成YUV,再恢復(fù)成RGB。
- 幀內(nèi)預(yù)測: 鄰近的宏塊之間可以進(jìn)行預(yù)測,算法思想是由一個宏塊,通過某種預(yù)測模式,得到一個預(yù)測的模塊,將實際值和預(yù)測值之間的殘差進(jìn)行保存。
- 離散余弦變換(DCT) 對每個塊的殘差執(zhí)行DCT變換,算法思想是:圖像數(shù)據(jù)分為細(xì)節(jié)、紋理和快速變化這類的高頻信息,和像整體趨勢、平均值和慢速變化這類低頻信息;DCT主要保留包含了數(shù)據(jù)整體特征的低頻信息。
- 量化: 由于DCT的結(jié)果中浮點數(shù)較多,量化將其截斷為整數(shù)以減少數(shù)據(jù)量
- 熵編碼: 熵編碼用于編碼多種類型的信息,像文本、圖像、音頻等信息根據(jù)數(shù)據(jù)的概率分布(如字符、像素、采樣值)映射為可變長度的編碼。經(jīng)典哈夫曼樹就是一種實現(xiàn)。在此就是將像素值/YUV值根據(jù)其概率分布設(shè)置不同編碼。
幀間壓縮:
- 幀間預(yù)測: 由于很多幀之間存在冗余,算法首先選擇一個參考幀,然后計算參考幀和當(dāng)前幀之間的運動矢量,由此去除冗余信息
- 運動補(bǔ)償...
- 殘差計算...
- ...
我的代碼:
- 主要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("錄制結(jié)束");
webcam.close();
//調(diào)用壓縮方法,將結(jié)果寫入文件中
byte[] bytes = outerCompressionUtils.photosToCompressedBytes(bufferedImages);
File file = new File("壓縮中的壓縮.dat");
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytes);
fos.close();
System.out.println("持久化結(jié)束");
}壓縮:
- 工具方法:將rgb轉(zhuǎn)化成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的宏塊,將其中對應(yīng)YUV分別寫到Y(jié)UV的數(shù)組中
需要注意的是我犯的一個錯誤:沒有注意到Y(jié)和U的遍歷過程,導(dǎo)致在解碼的時候圖片異常
*/
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都是一起傳的,需要在包開頭標(biāo)明每個數(shù)組長度
//Y區(qū)的長度
byte[] bytes1 = intToByte(length1);
for (byte b : bytes1) {
targetBytes[targetIndex] = b;
targetIndex++;
}
//U區(qū)長度
byte[] bytes2 = intToByte(length2);
for (byte b : bytes2) {
targetBytes[targetIndex] = b;
targetIndex++;
}
//V區(qū)長度
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++;
}
//傳遞真實數(shù)據(jù)
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 {
//數(shù)據(jù)流中未必要有各種輔助信息,比如各類字段長度,在外規(guī)定好算了
//這里每一幀的長度就是: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;
}- 嘗試用哈夫曼編碼優(yōu)化
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("原始數(shù)組長度"+originalBytes.length);
System.out.println("哈夫曼后數(shù)組長度"+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);
}
}但其實這里用哈夫曼并不會優(yōu)化數(shù)據(jù)量,原因如下: 我傳輸?shù)臄?shù)據(jù)是-128到127的byte類型,這些byte來自圖片的亮度和色度,調(diào)試中發(fā)現(xiàn)這255個數(shù)字出現(xiàn)的頻率差不多,全部都在14萬到20萬之間,兩個最小值加起來任然比最大值大,這就意味著這顆哈夫曼樹會比較滿,類似完全二叉樹,于是就無法區(qū)分出現(xiàn)頻率最高的某個字符。
另外,原本255個數(shù)將8位byte全都占滿,假如有一個頻率很高的元素,我們把較短的0101賦給它,那勢必會導(dǎo)致原本以0101開頭的元素用8位以上的長度進(jìn)行表示,而程序中各元素出現(xiàn)頻率相近,這就會導(dǎo)致如果有元素用短于8位的編碼,其他長于8位編碼的元素會導(dǎo)致數(shù)據(jù)更加龐大。
我在用huffman編碼后,數(shù)據(jù)量一點都沒有變,只是由長度為40647865的byte數(shù)組變成長度為325182920的字符串,其實就是×8 。懷疑是代碼哪里錯了...
常見的壓縮算法是將DCT變換后的結(jié)果進(jìn)行哈夫曼編碼,DCT變換后低頻信息和高頻信息自然區(qū)分開,確實更適合這個熵編碼方法


- 解壓:
先將java zip包的壓縮過程解壓
public static InflaterInputStream inflaterCompressedBytes(byte[] bytes) throws IOException {
//解壓數(shù)據(jù)
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
InflaterInputStream lis = new InflaterInputStream(bais, new Inflater());
return lis;
}- 依據(jù)壓縮時自定義的格式進(jìn)行對byte數(shù)組解析
public static BufferedImage getBfi(byte[] originalBytes) {
//分別先把開頭表示各個區(qū)長度以及圖片寬高的參數(shù)取出來
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);
//將數(shù)據(jù)讀取出來
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數(shù)據(jù)恢復(fù)成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(); //此輸出流中寫入所有信息,最后轉(zhuǎn)出為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,沒找到很合適的解決辦法,但發(fā)現(xiàn)這個異常是在讀取到最后一張圖片時才觸發(fā),于是我選擇舍棄最后一張圖
- 這個播放器只用Swing簡單寫了一個用于測試能否讀取文件,很明顯我的播放器只能播放我的視頻,因為其解碼方式和編碼方式息息相關(guān),而各種常見的編碼方式里的算法又太過復(fù)雜。所以這個程序就相當(dāng)于寫著玩而已,和其他視頻/播放器難有半點干系。
以上就是Java實現(xiàn)視頻初步壓縮和解壓的代碼示例的詳細(xì)內(nèi)容,更多關(guān)于Java視頻壓縮和解壓的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
spring為java.util.Properties類型的屬性進(jìn)行賦值過程解析
這篇文章主要介紹了spring為java.util.Properties類型的屬性進(jìn)行賦值過程解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-01-01
Java線程安全的計數(shù)器簡單實現(xiàn)代碼示例
這篇文章主要介紹了Java線程安全的計數(shù)器簡單實現(xiàn)代碼示例,具有一定參考價值,需要的朋友可以了解下。2017-10-10
SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實現(xiàn)方法詳解
這篇文章主要介紹了SpringBoot導(dǎo)入導(dǎo)出數(shù)據(jù)實現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-12-12
java實現(xiàn)memcache服務(wù)器的示例代碼
本篇文章主要介紹了java實現(xiàn)memcache服務(wù)器的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-04-04
使用Jenkins一鍵打包部署SpringBoot項目的步驟詳解
任何簡單操作的背后,都有一套相當(dāng)復(fù)雜的機(jī)制,本文將以SpringBoot應(yīng)用的在Docker環(huán)境下的打包部署為例,詳細(xì)講解如何使用Jenkins一鍵打包部署SpringBoot應(yīng)用,文中通過圖文結(jié)合講解的非常詳細(xì),需要的朋友可以參考下2023-11-11

