commit c92cf914fc5d22ea2734c75c8be8d63dc4de1a23 Author: KNEMC <2014539504@qq.com> Date: Sun Jul 26 22:35:06 2026 +0800 KVFCodec diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..98e7f39 --- /dev/null +++ b/.classpath @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f59ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/4K-Test-SDR/ +/input2/ +/input3/ +/PngSuite-2017jul19.zip +/input/ diff --git a/.project b/.project new file mode 100644 index 0000000..642957b --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + KVFCodec + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..99f26c0 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..aa26f9f --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,14 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=23 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=23 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.source=23 diff --git a/bin/.gitignore b/bin/.gitignore new file mode 100644 index 0000000..cf1db2e --- /dev/null +++ b/bin/.gitignore @@ -0,0 +1 @@ +/org/ diff --git a/lib/JavaOpenCL64.jar b/lib/JavaOpenCL64.jar new file mode 100644 index 0000000..08f769f Binary files /dev/null and b/lib/JavaOpenCL64.jar differ diff --git a/lib/KNEOptimize.jar b/lib/KNEOptimize.jar new file mode 100644 index 0000000..624137d Binary files /dev/null and b/lib/KNEOptimize.jar differ diff --git a/lib/aircompressor-2.0.3-javadoc.jar b/lib/aircompressor-2.0.3-javadoc.jar new file mode 100644 index 0000000..d7d0d5f Binary files /dev/null and b/lib/aircompressor-2.0.3-javadoc.jar differ diff --git a/lib/aircompressor-2.0.3-sources.jar b/lib/aircompressor-2.0.3-sources.jar new file mode 100644 index 0000000..94a1c76 Binary files /dev/null and b/lib/aircompressor-2.0.3-sources.jar differ diff --git a/lib/aircompressor-2.0.3-test-sources.jar b/lib/aircompressor-2.0.3-test-sources.jar new file mode 100644 index 0000000..5a45b3b Binary files /dev/null and b/lib/aircompressor-2.0.3-test-sources.jar differ diff --git a/lib/aircompressor-2.0.3-tests.jar b/lib/aircompressor-2.0.3-tests.jar new file mode 100644 index 0000000..f7b4909 Binary files /dev/null and b/lib/aircompressor-2.0.3-tests.jar differ diff --git a/lib/aircompressor-2.0.3.jar b/lib/aircompressor-2.0.3.jar new file mode 100644 index 0000000..4cb57f2 Binary files /dev/null and b/lib/aircompressor-2.0.3.jar differ diff --git a/lib/commons-compress-1.28.0-javadoc.jar b/lib/commons-compress-1.28.0-javadoc.jar new file mode 100644 index 0000000..bdfe2e5 Binary files /dev/null and b/lib/commons-compress-1.28.0-javadoc.jar differ diff --git a/lib/commons-compress-1.28.0-sources.jar b/lib/commons-compress-1.28.0-sources.jar new file mode 100644 index 0000000..41c04e5 Binary files /dev/null and b/lib/commons-compress-1.28.0-sources.jar differ diff --git a/lib/commons-compress-1.28.0.jar b/lib/commons-compress-1.28.0.jar new file mode 100644 index 0000000..ff7e6aa Binary files /dev/null and b/lib/commons-compress-1.28.0.jar differ diff --git a/lib/jcodec-0.2.5-javadoc.jar b/lib/jcodec-0.2.5-javadoc.jar new file mode 100644 index 0000000..af56c26 Binary files /dev/null and b/lib/jcodec-0.2.5-javadoc.jar differ diff --git a/lib/jcodec-0.2.5-sources.jar b/lib/jcodec-0.2.5-sources.jar new file mode 100644 index 0000000..204f9dd Binary files /dev/null and b/lib/jcodec-0.2.5-sources.jar differ diff --git a/lib/jcodec-0.2.5.jar b/lib/jcodec-0.2.5.jar new file mode 100644 index 0000000..554a253 Binary files /dev/null and b/lib/jcodec-0.2.5.jar differ diff --git a/lib/jcodec-javase-0.2.5-javadoc.jar b/lib/jcodec-javase-0.2.5-javadoc.jar new file mode 100644 index 0000000..ac66a37 Binary files /dev/null and b/lib/jcodec-javase-0.2.5-javadoc.jar differ diff --git a/lib/jcodec-javase-0.2.5-sources.jar b/lib/jcodec-javase-0.2.5-sources.jar new file mode 100644 index 0000000..1a292e2 Binary files /dev/null and b/lib/jcodec-javase-0.2.5-sources.jar differ diff --git a/lib/jcodec-javase-0.2.5.jar b/lib/jcodec-javase-0.2.5.jar new file mode 100644 index 0000000..4350db6 Binary files /dev/null and b/lib/jcodec-javase-0.2.5.jar differ diff --git a/src/org/kne/codec/kif/BitPlane.java b/src/org/kne/codec/kif/BitPlane.java new file mode 100644 index 0000000..a99d442 --- /dev/null +++ b/src/org/kne/codec/kif/BitPlane.java @@ -0,0 +1,177 @@ +package org.kne.codec.kif; + +import org.kne.membandboost.MembandBoost; + +/** + * 位平面编码工具 + * 将字节数组按位平面重排:先存所有最高位,再存次高位,...,最后存最低位 + * 例如:输入 [0x01, 0x02, 0x03] + * 二进制: 00000001, 00000010, 00000011 + * 位平面拆分: bit7: 000, bit6: 000, ..., bit1: 011, bit0: 101 + * 输出: [00000000, 00000000, ..., 00000011, 00000101] + */ +public class BitPlane { + + + + /** + * 位平面编码(矩阵转置风格) + * + * 核心逻辑: + * 对于输出数组的每个字节,依次从输入数组的8个位平面各取1个bit + * 即:输出字节的 bit i = 输入数组第 i 个位平面的当前位 + * + * 等价于:把 8×len 的 bit 矩阵转置为 len×8 的 bit 矩阵 + */ + public static byte[] encodePacked(byte[] input) { + int len = input.length; + byte[] output = MembandBoost.allocateUninitializedByteArray(len); + + + // 输出数组的每个字节,由8个位平面的各1个bit组成 + for (int outIdx = 0; outIdx < len; outIdx++) { + int outByte = 0; + // 从8个位平面各取1个bit,组装成一个字节 + for (int plane = 0; plane < 8; plane++) { + // 当前位平面:bitShift + // 从输入数组的 inputPos 位置取 bit + int bitpos=(outIdx<<3)+plane; + int inputPos = bitpos%len; + int bitShift = bitpos/len; + int bit = (input[inputPos] >> (7-bitShift)) & 1; + outByte |= (bit << plane); // 放到输出字节的第 plane 位 + + } + output[outIdx] = (byte) outByte; + } + return output; + } + + /** + * 位平面编码(极速版) + * 按位平面批量处理,利用位平面内部的连续性 + */ + /* public static byte[] encodePacked(byte[] input) { + int len = input.length; + byte[] output = new byte[len]; + + for (int plane = 0; plane < 8; plane++) { + int bit = 7 - plane; + int planeStart = plane * len; + int planeByteStart = planeStart >> 3; + int planeBitOffset = planeStart & 7; + + // 处理当前位平面 + // 收集位平面数据到一个 int(最多 32 位),批量写入 + for (int i = 0; i < len; i++) { + int inputBit = (input[i] >> bit) & 1; + if (inputBit == 1) { + int bytePos = planeByteStart + ((i + planeBitOffset) >> 3); + int bitPos = 7 - ((i + planeBitOffset) & 7); + output[bytePos] |= (1 << bitPos); + } + } + } + return output; + }*/ + + + /** + * 位平面解码(矩阵转置逆操作) + * + * 核心逻辑: + * 对于输出数组的每个字节,从输入数组的8个字节中各取1个bit + * 即:输出字节的 bit i = 输入数组第 i 个字节的第 plane 位 + */ + public static byte[] decodePacked(byte[] input) { + byte[] output = MembandBoost.allocateUninitializedByteArray(input.length); + + // 输出数组的每个字节,由8个输入字节的对应bit组成 + for (int outIdx = 0; outIdx < output.length; outIdx++) { + int outByte = 0; + for (int plane = 0; plane < 8; plane++) { + // 从输入数组的当前字节取第 bitShift 位 + int bitpos2=plane*input.length+outIdx; + int inputpos=bitpos2>>3; + int inputshift=bitpos2&0b111; + int bit = (input[inputpos] >> inputshift) & 1; + outByte |= (bit << (7-plane)); + } + output[outIdx] = (byte) outByte; + } + return output; + } + + /** + * 位平面解码(极速版) + */ + /*public static byte[] decodePacked(byte[] input) { + byte[] output = new byte[length]; + + for (int plane = 0; plane < 8; plane++) { + int bit = 7 - plane; + int planeStart = plane * len; + int planeByteStart = planeStart >> 3; + int planeBitOffset = planeStart & 7; + + for (int i = 0; i < len; i++) { + int bytePos = planeByteStart + ((i + planeBitOffset) >> 3); + int bitPos = 7 - ((i + planeBitOffset) & 7); + int inputBit = (input[bytePos] >> bitPos) & 1; + if (inputBit == 1) { + output[i] |= (1 << bit); + } + } + } + return output; + }*/ + + + public static void main(String[] args) { + // 测试数据:0-255 的字节 + byte[] testData = new byte[16]; + for (int i = 0; i < testData.length; i++) { + testData[i] = (byte) i; + } + + // 打包版编码 + byte[] encoded = BitPlane.encodePacked(testData); + System.out.println("原始长度: " + testData.length); + System.out.println("编码后长度: " + encoded.length); + + // 解码 + byte[] decoded = BitPlane.decodePacked(encoded); + + + // 查看位平面结构 + System.out.println("\n前 16 个字节的测试数据:"); + for (int i = 0; i < 16; i++) { + System.out.printf("%02X ", testData[i]); + } + + + System.out.println("\n前 16 个字节的位平面编码:"); + for (int i = 0; i < 16; i++) { + System.out.printf("%02X ", encoded[i]); + } + + + + System.out.println("\n前 16 个字节的位平面解码:"); + for (int i = 0; i < 16; i++) { + System.out.printf("%02X ", decoded[i]); + } + + + // 验证 + boolean ok = true; + for (int i = 0; i < testData.length; i++) { + if (testData[i] != decoded[i]) { + ok = false; + System.out.println("❌ 第 " + i + " 个字节不一致"); + break; + } + } + System.out.println("无损还原: " + (ok ? "✅" : "❌")); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/BlockCoord.java b/src/org/kne/codec/kif/BlockCoord.java new file mode 100644 index 0000000..cb82e4a --- /dev/null +++ b/src/org/kne/codec/kif/BlockCoord.java @@ -0,0 +1,50 @@ +package org.kne.codec.kif; + +import java.util.ArrayList; +import java.util.List; + +/** + * 分块坐标信息(仅包含位置和尺寸,不包含实际像素数据) + */ +public class BlockCoord { + private final int posX; + private final int posY; + private final int width; + private final int height; + + public BlockCoord(int posX, int posY, int width, int height) { + this.posX = posX; + this.posY = posY; + this.width = width; + this.height = height; + } + + public int getPosX() { return posX; } + public int getPosY() { return posY; } + public int getWidth() { return width; } + public int getHeight() { return height; } + + @Override + public String toString() { + return String.format("BlockCoord(%d,%d,%dx%d)", posX, posY, width, height); + } + + // ==================== 分块坐标计算 ==================== + + public static List calculateBlockCoords(int width, int height, int blockWidth, int blockHeight) { + int blocksX = (width + blockWidth - 1) / blockWidth; + int blocksY = (height + blockHeight - 1) / blockHeight; + List coords = new ArrayList<>(blocksX * blocksY); + + for (int by = 0; by < blocksY; by++) { + for (int bx = 0; bx < blocksX; bx++) { + int posX = bx * blockWidth; + int posY = by * blockHeight; + int bw = Math.min(blockWidth, width - posX); + int bh = Math.min(blockHeight, height - posY); + coords.add(new BlockCoord(posX, posY, bw, bh)); + } + } + return coords; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/BufferedImageMembandBoost.java b/src/org/kne/codec/kif/BufferedImageMembandBoost.java new file mode 100644 index 0000000..b0650f2 --- /dev/null +++ b/src/org/kne/codec/kif/BufferedImageMembandBoost.java @@ -0,0 +1,70 @@ +package org.kne.codec.kif; + +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.DataBufferInt; +import java.awt.image.DirectColorModel; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import org.kne.membandboost.MembandBoost; + +public class BufferedImageMembandBoost { + private static final int[] MASK=new int[]{0x00FF0000, 0x0000FF00, 0x000000FF, 0xFF000000};// R, G, B, A 掩码 + /** + * 创建一个不归零的 TYPE_INT_ARGB BufferedImage + * 使用 MembandBoost 分配未初始化的像素数组 + */ + public static BufferedImage createUninitializedBufferedImage(int width, int height) { + int size = width * height; + + // 1. 创建未初始化的 DataBuffer(不归零!) + DataBufferInt db = new DataBufferInt( + MembandBoost.allocateUninitializedIntArray(size), + size + ); + + // 2. 创建 ARGB Raster(4 波段,对应 TYPE_INT_ARGB) + WritableRaster raster = Raster.createPackedRaster( + db, + width, height, + width, + MASK, + null + ); + + // 3. 创建匹配的 ARGB ColorModel + ColorModel colorModel = ColorModel.getRGBdefault(); + + // 4. 创建 BufferedImage + return new BufferedImage( + colorModel, + raster, + false, // isRasterPremultiplied + null + ); + } + + /** + * 从已有的 int[] 像素数据创建 BufferedImage(不拷贝) + */ + public static BufferedImage createFromPixels(int[] pixels, int width, int height) { + int size = width * height; + if (pixels.length < size) { + throw new IllegalArgumentException("pixels 数组长度不足"); + } + + DataBufferInt db = new DataBufferInt(pixels, size); + WritableRaster raster = Raster.createPackedRaster( + db, + width, height, + width, + MASK, + null + ); + + ColorModel colorModel = ColorModel.getRGBdefault(); + + return new BufferedImage(colorModel, raster, false, null); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ByteFrequencyMapper.java b/src/org/kne/codec/kif/ByteFrequencyMapper.java new file mode 100644 index 0000000..cfe8cc3 --- /dev/null +++ b/src/org/kne/codec/kif/ByteFrequencyMapper.java @@ -0,0 +1,199 @@ +package org.kne.codec.kif; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Map; +import java.util.stream.Collectors; + +import javax.annotation.processing.SupportedSourceVersion; + +/** + * 字节值频率统计与映射表生成工具 + * 用于调色板模式中的字节级概率空间映射 + */ +public class ByteFrequencyMapper { + + /** + * 统计字节数组中每个值(0~255)出现的次数 + * + * @param data 输入的字节数组 + * @return 长度为 256 的 int 数组,index 表示字节值,value 表示出现次数 + */ + public static int[] buildFrequency(byte[] data) { + int[] freq = new int[256]; + for (byte b : data) { + freq[b & 0xFF]++; + } + return freq; + } + + /** + * 根据频率数组生成映射表(按频率降序排列) + * 映射表:index -> byte value + * 即:映射表[0] = 出现次数最多的字节值 + * + * @param freq 长度为 256 的频率数组 + * @return 长度为 256 的 byte 数组,按频率降序排列 + */ + public static byte[] buildInverseMap(int[] freq) { + // 创建索引数组 0~255 + Integer[] indices = new Integer[256]; + for (int i = 0; i < 256; i++) { + indices[i] = i; + } + + // 按频率降序排序 + Arrays.sort(indices, new Comparator() { + @Override + public int compare(Integer a, Integer b) { + // 频率高的排前面 + int cmp = Integer.compare(freq[b], freq[a]); + if (cmp != 0) return cmp; + // 频率相同,按值升序 + return Integer.compare(a, b); + } + }); + + // 生成映射表 + byte[] forwardMap = new byte[256]; + for (int i = 0; i < 256; i++) { + forwardMap[i] = (byte) indices[i].intValue(); + } + return forwardMap; + } + + /** + * 生成逆映射表 + * 逆映射表:byte value -> index + * 即:inverseMap[原始字节值] = 在映射表中的位置 + * + * @param map 正向映射表(index -> byte value) + * @return 长度为 256 的 byte 数组,逆映射表 + */ + public static byte[] inverse(byte[] map) { + byte[] inverseMap = new byte[256]; + for (int i = 0; i < map.length; i++) { + int value = map[i] & 0xFF; + inverseMap[value] = (byte) i; + } + return inverseMap; + } + + /** + * 使用映射表对字节数组进行映射(编码) + * + * @param data 原始字节数组 + * @param forwardMap 映射表 + * @return 映射后的字节数组 + */ + public static byte[] applyMap(byte[] data, byte[] forwardMap) { + byte[] result = new byte[data.length]; + for (int i = 0; i < data.length; i++) { + int value = data[i] & 0xFF; + // 查找该值在映射表中的位置 + // 注意:如果 forwardMap 是压缩的(只包含部分值),需要处理未映射的情况 + // 这里假设 forwardMap 是全映射 + result[i] = forwardMap[value]; + } + return result; + } + + + + /** + * 截断映射表:只保留前 K 个高频值,其余映射到最近的高频值 + * + * @param freq 频率数组 + * @param k 保留的高频值数量(1~256) + * @return 截断后的映射表(长度仍然为 256,但只包含 K 种唯一值) + */ + public static byte[] buildTruncatedForwardMap(int[] freq, int k) { + if (k <= 0 || k > 256) { + throw new IllegalArgumentException("k 必须在 1~256 之间"); + } + + // 获取完整映射表 + byte[] fullMap = buildInverseMap(freq); + + // 取前 k 个高频值 + byte[] truncated = new byte[256]; + // 初始化为 0(默认映射到第一个高频值) + Arrays.fill(truncated, fullMap[0]); + + // 将前 k 个高频值放入映射表 + for (int i = 0; i < k; i++) { + truncated[i] = fullMap[i]; + } + + // 对于低频值,需要建立逆映射:原始字节值 -> 最近的高频值索引 + // 这里简化为:低频值全部映射到索引 0(最高频值) + // 更精细的做法可以是:找到最近的映射表值 + + return truncated; + } + + /** + * 获取映射表的统计信息 + */ + public static String getMapStats(byte[] forwardMap, int[] freq) { + int total = 0; + for (int f : freq) { + total += f; + } + + StringBuilder sb = new StringBuilder(); + sb.append("映射表统计:\n"); + sb.append(" 唯一值数: ").append(getUniqueCount(forwardMap)).append("\n"); + + long covered = 0; + int threshold = 99; + for (int i = 0; i < forwardMap.length; i++) { + int value = forwardMap[i] & 0xFF; + covered += freq[value]; + double pct = covered * 100.0 / total; + if (pct >= threshold && i > 0) { + sb.append(String.format(" 前 %d 个值覆盖 %.2f%% 的像素\n", i + 1, pct)); + break; + } + } + + double pct = covered * 100.0 / total; + sb.append(String.format(" 全部值覆盖 %.2f%% 的像素\n", pct)); + return sb.toString(); + } + + private static int getUniqueCount(byte[] map) { + boolean[] seen = new boolean[256]; + int count = 0; + for (byte b : map) { + int value = b & 0xFF; + if (!seen[value]) { + seen[value] = true; + count++; + } + } + return count; + } + public static void main(String[] args) { + byte[] serialData = new byte[] {1,1,1,1,1,0,0,0,2,3,3,3,3,3,3,3,4}; + int[]freq=ByteFrequencyMapper.buildFrequency(serialData); + byte[]inverse=ByteFrequencyMapper.buildInverseMap(freq); + byte[]foward=ByteFrequencyMapper.inverse(inverse); + byte[] serialDatafor= ByteFrequencyMapper.applyMap(serialData, foward); + System.out.println("输入:"); + ByteStatisticsCollector bsc=new ByteStatisticsCollector(); + bsc.analyze(serialData); + System.out.println(bsc); + + System.out.println("编码:"); + ByteStatisticsCollector bsc2=new ByteStatisticsCollector(); + bsc2.analyze(serialDatafor); + System.out.println(bsc2); + + byte[] serialDatainv= ByteFrequencyMapper.applyMap(serialDatafor, inverse); + System.out.println("解码:"); + ByteStatisticsCollector bsc32=new ByteStatisticsCollector(); + bsc32.analyze(serialDatainv); + System.out.println(bsc32); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ByteStatisticsCollector.java b/src/org/kne/codec/kif/ByteStatisticsCollector.java new file mode 100644 index 0000000..18f1e62 --- /dev/null +++ b/src/org/kne/codec/kif/ByteStatisticsCollector.java @@ -0,0 +1,98 @@ +package org.kne.codec.kif; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 字节统计收集器 + * 统计 byte[] 中不同字节值的出现次数与概率(0~255) + * + * 用于分析像素数据、残差数据、位平面数据的分布特性 + */ +public class ByteStatisticsCollector extends StatisticsCollector { + + private static final ByteFormatter FORMATTER = new ByteFormatter(); + + /** + * 统计整个字节数组 + */ + public void analyze(byte[] data) { + for (byte b : data) { + addRecord(b); + } + } + + /** + * 统计字节数组的指定范围 + */ + public void analyze(byte[] data, int offset, int length) { + int end = Math.min(offset + length, data.length); + for (int i = offset; i < end; i++) { + addRecord(data[i]); + } + } + + /** + * 统计后返回最常见的字节值 + */ + public byte getMostCommonByte() { + Map sorted = getSorted(); + if (sorted.isEmpty()) { + return 0; + } + return sorted.entrySet().iterator().next().getKey(); + } + + /** + * 统计后返回最常见字节值的出现次数 + */ + public long getMostCommonCount() { + Map sorted = getSorted(); + if (sorted.isEmpty()) { + return 0; + } + return sorted.entrySet().iterator().next().getValue().get(); + } + + /** + * 统计后返回最常见字节值的占比(百分比) + */ + public double getMostCommonProbability() { + byte mostCommon = getMostCommonByte(); + return getProbability(mostCommon); + } + + /** + * 获取字节值对应的可读字符串 + */ + public String byteToString(byte b) { + int v = b & 0xFF; + return String.format("0x%02X (%d)", v, v); + } + + /** + * 生成字节统计的表格报告 + */ + @Override + public String toString() { + return formatTable(FORMATTER); + } + + /** + * 生成 CSV 报告 + */ + public String toCSV() { + return super.toCSV(FORMATTER); + } + + /** + * Byte 格式化器 + */ + private static class ByteFormatter implements ItemFormatter { + @Override + public String format(Byte item) { + int v = item & 0xFF; + return String.format("0x%02X", v); + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ColorStatisticsCollector.java b/src/org/kne/codec/kif/ColorStatisticsCollector.java new file mode 100644 index 0000000..83b92a5 --- /dev/null +++ b/src/org/kne/codec/kif/ColorStatisticsCollector.java @@ -0,0 +1,201 @@ +package org.kne.codec.kif; + +import java.awt.Color; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 颜色统计收集器 + * 用于统计图像中各种颜色出现的频率 + * 继承自 StatisticsCollector + */ +public class ColorStatisticsCollector extends StatisticsCollector { + + + public void analyze(int[] pixels) { + for (int i : pixels) { + addRecord(i); + } + } + + /** + * 记录一个 RGB 整数值(0x00RRGGBB)的颜色 + */ + public void addRecord(int rgb) { + Color color = new Color(rgb); + addRecord(color); + } + + /** + * 记录一个 RGB 整数值(0x00RRGGBB)的颜色,多次 + */ + public void addRecord(int rgb, long count) { + Color color = new Color(rgb); + addRecord(color, count); + } + + /** + * 记录一个由 R、G、B 分量组成的颜色 + */ + public void addRecord(int r, int g, int b) { + Color color = new Color(r & 0xFF, g & 0xFF, b & 0xFF); + addRecord(color); + } + + /** + * 获取某个 RGB 颜色的出现次数 + */ + public long getCount(int rgb) { + return getCount(new Color(rgb)); + } + + /** + * 获取某个 RGB 颜色的出现概率(百分比) + */ + public double getProbability(int rgb) { + return getProbability(new Color(rgb)); + } + + /** + * 获取最常见的颜色(出现次数最多的颜色) + */ + public Color getMostFrequentColor() { + Map sorted = getSorted(); + if (sorted.isEmpty()) { + return null; + } + return sorted.entrySet().iterator().next().getKey(); + } + + /** + * 获取最罕见的颜色(出现次数最少的颜色) + */ + public Color getLeastFrequentColor() { + Map sorted = getSortedAscending(); + if (sorted.isEmpty()) { + return null; + } + return sorted.entrySet().iterator().next().getKey(); + } + + /** + * 获取前 N 种最常见的颜色 + */ + public Map getTopColors(int n) { + Map sorted = getSorted(); + return sorted.entrySet().stream() + .limit(n) + .collect(java.util.stream.Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (old, neu) -> old, + java.util.LinkedHashMap::new + )); + } + + /** + * 获取唯一颜色数量 + */ + public int getColorCount() { + return getUniqueCount(); + } + + /** + * 生成颜色统计报告 + */ + public String formatColorTable() { + return formatTable(new ItemFormatter() { + @Override + public String format(Color color) { + return String.format("RGB(%d,%d,%d)", + color.getRed(), + color.getGreen(), + color.getBlue() + ); + } + }); + } + /** + * 生成颜色统计报告(带颜色名称/简写) + */ + public String formatColorTable(boolean showName) { + if (!showName) { + return formatColorTable(); + } + + long t = total.get(); + if (t == 0) { + return "(无统计数据)"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("╔═══════════════════════════════════════════════════════════════════════════════════╗\n"); + sb.append("║ 颜色统计报告 ║\n"); + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + sb.append("║ 序号 │ 颜色 │ 出现次数 │ 占比 │ 累积占比 ║\n"); + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + + Map sorted = getSorted(); + long cumulative = 0; + int index = 0; + + for (Map.Entry entry : sorted.entrySet()) { + Color color = entry.getKey(); + long count = entry.getValue().get(); + double pct = count * 100.0 / t; + cumulative += count; + double cumPct = cumulative * 100.0 / t; + index++; + + String colorStr = String.format("RGB(%d,%d,%d)", + color.getRed(), + color.getGreen(), + color.getBlue() + ); + if (colorStr.length() > 27) { + colorStr = colorStr.substring(0, 24) + "..."; + } + + sb.append(String.format("║ %4d │ %-27s │ %8d │ %6.2f%% │ %6.2f%% ║\n", + index, + colorStr, + count, + pct, + cumPct + )); + } + + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + sb.append(String.format("║ 总像素数: %d │ 唯一颜色数: %d │ 覆盖率: %6.2f%% ║\n", + t, + getUniqueCount(), + 100.0 + )); + sb.append("╚═══════════════════════════════════════════════════════════════════════════════════╝"); + return sb.toString(); + } + + /** + * 生成简洁的颜色统计摘要 + */ + public String getSummary() { + long t = total.get(); + if (t == 0) { + return "无数据"; + } + + Color mostFrequent = getMostFrequentColor(); + Color leastFrequent = getLeastFrequentColor(); + + return String.format( + "总像素数: %d, 唯一颜色数: %d, 最频繁颜色: RGB(%d,%d,%d) (%.2f%%), 最罕见颜色: RGB(%d,%d,%d) (%.2f%%)", + t, + getUniqueCount(), + mostFrequent.getRed(), mostFrequent.getGreen(), mostFrequent.getBlue(), + getProbability(mostFrequent), + leastFrequent.getRed(), leastFrequent.getGreen(), leastFrequent.getBlue(), + getProbability(leastFrequent) + ); + } + +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ColorTransform.java b/src/org/kne/codec/kif/ColorTransform.java new file mode 100644 index 0000000..3defc65 --- /dev/null +++ b/src/org/kne/codec/kif/ColorTransform.java @@ -0,0 +1,125 @@ +package org.kne.codec.kif; + +import org.kne.membandboost.MembandBoost; + +public class ColorTransform { + + // ==================== RGB 版本(3通道) ==================== + + /** + * R-G 和 B-G 跨通道颜色变换(RGB) + * 编码:R' = R - G, G' = G, B' = B - G + */ + public static int[] colorTransform(int[] input, int w, int h) { + int[] output = MembandBoost.allocateUninitializedIntArray(input.length); + for (int i = 0; i < input.length; i++) { + int rgb = input[i]; + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + byte rg = (byte) (r - g); + byte bg = (byte) (b - g); + output[i] = ((rg & 0xFF) << 16) | ((g & 0xFF) << 8) | (bg & 0xFF); + } + return output; + } + + public static int[] colorInverse(int[] input, int w, int h) { + int[] output = MembandBoost.allocateUninitializedIntArray(input.length); + for (int i = 0; i < input.length; i++) { + int packed = input[i]; + int rg = (packed >> 16) & 0xFF; + int g = (packed >> 8) & 0xFF; + int bg = packed & 0xFF; + int r = g + rg; + int b = g + bg; + output[i] = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return output; + } + + public static void colorTransformInPlace(int[] input, int w, int h) { + for (int i = 0; i < input.length; i++) { + int rgb = input[i]; + int r = (rgb >> 16) & 0xFF; + int g = (rgb >> 8) & 0xFF; + int b = rgb & 0xFF; + byte rg = (byte) (r - g); + byte bg = (byte) (b - g); + input[i] = ((rg & 0xFF) << 16) | ((g & 0xFF) << 8) | (bg & 0xFF); + } + } + + public static void colorInverseInPlace(int[] input, int w, int h) { + for (int i = 0; i < input.length; i++) { + int packed = input[i]; + int rg = (packed >> 16) & 0xFF; + int g = (packed >> 8) & 0xFF; + int bg = packed & 0xFF; + int r = g + rg; + int b = g + bg; + input[i] = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + } + + // ==================== RGBA 版本(4通道,Alpha 原封不动) ==================== + + /** + * RGBA 颜色变换:R' = R - G, G' = G, B' = B - G, A' = A(原封不动) + */ + public static int[] colorTransformRGBA(int[] input, int w, int h) { + int[] output = MembandBoost.allocateUninitializedIntArray(input.length); + for (int i = 0; i < input.length; i++) { + int pixel = input[i]; + int r = (pixel >> 16) & 0xFF; + int g = (pixel >> 8) & 0xFF; + int b = pixel & 0xFF; + int a = (pixel >> 24) & 0xFF; // Alpha 原封不动 + byte rg = (byte) (r - g); + byte bg = (byte) (b - g); + output[i] = ((a & 0xFF) << 24) | ((rg & 0xFF) << 16) | ((g & 0xFF) << 8) | (bg & 0xFF); + } + return output; + } + + public static int[] colorInverseRGBA(int[] input, int w, int h) { + int[] output = MembandBoost.allocateUninitializedIntArray(input.length); + for (int i = 0; i < input.length; i++) { + int packed = input[i]; + int a = (packed >> 24) & 0xFF; + int rg = (packed >> 16) & 0xFF; + int g = (packed >> 8) & 0xFF; + int bg = packed & 0xFF; + int r = g + rg; + int b = g + bg; + output[i] = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return output; + } + + public static void colorTransformRGBAInPlace(int[] input, int w, int h) { + for (int i = 0; i < input.length; i++) { + int pixel = input[i]; + int r = (pixel >> 16) & 0xFF; + int g = (pixel >> 8) & 0xFF; + int b = pixel & 0xFF; + int a = (pixel >> 24) & 0xFF; + byte rg = (byte) (r - g); + byte bg = (byte) (b - g); + input[i] = ((a & 0xFF) << 24) | ((rg & 0xFF) << 16) | ((g & 0xFF) << 8) | (bg & 0xFF); + } + } + + public static void colorInverseRGBAInPlace(int[] input, int w, int h) { + for (int i = 0; i < input.length; i++) { + int packed = input[i]; + int a = (packed >> 24) & 0xFF; + int rg = (packed >> 16) & 0xFF; + int g = (packed >> 8) & 0xFF; + int bg = packed & 0xFF; + int r = g + rg; + int b = g + bg; + input[i] = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/CompressionExperiment.java b/src/org/kne/codec/kif/CompressionExperiment.java new file mode 100644 index 0000000..8636c4b --- /dev/null +++ b/src/org/kne/codec/kif/CompressionExperiment.java @@ -0,0 +1,455 @@ +package org.kne.codec.kif; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 图像压缩格式对比实验 + * 遍历 input 目录下的所有图片,用 ImageIO 注册的所有格式进行压缩, + * 输出压缩后大小、压缩率,并验证无损/有损。 + * 分别记录编码时间和解码时间 + */ +public class CompressionExperiment { + + // 支持的输入格式(ImageIO 能读取的) + private static final Set INPUT_FORMATS = new HashSet<>(Arrays.asList( + "png", "bmp", "kif" + )); + // 要测试的输出格式(从 ImageIO 注册的 Writer 中获取) + private static Set outputFormats=new HashSet<>(Arrays.asList( + "png", "kif" + )); + + public static void main(String[] args) throws Exception { + // 🔥 强制覆盖标准输出流为 UTF-8 + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); + + // 1. 注册 KIF 插件 + org.kne.codec.kif.KIFImageReaderSpi.register(); + org.kne.codec.kif.KIFImageWriterSpi.register(); + + // 2. 获取所有可用的输出格式 + System.out.println("=== 可用的输出格式 ==="); + outputFormats.forEach(f -> System.out.println(" " + f)); + System.out.println(); + + // 3. 准备目录 + Path inputDir = Paths.get("input2"); + Path outputDir = Paths.get("output"); + if (!Files.exists(inputDir)) { + System.err.println("错误: input 目录不存在!"); + return; + } + if (!Files.exists(outputDir)) { + Files.createDirectories(outputDir); + } + + // 4. 获取所有测试图片 + List imageFiles = getTestImages(inputDir); + if (imageFiles.isEmpty()) { + System.err.println("错误: input 目录中没有找到图片!"); + return; + } + System.out.println("找到 " + imageFiles.size() + " 张测试图片\n"); + // 5.预热 + warmup(outputDir, imageFiles,30000000000L); + // 6. 执行实验 + List results = runExperiment(outputDir, imageFiles); + + // 7. 输出汇总报告 + printSummary(results); + + System.out.println(KIFImageWriter.getModeCollector()); + } + private static void warmup(Path outputDir, List imageFiles, long warmupTimeNs) { + long startTime = System.nanoTime(); + long elapsed = 0; + int iteration = 0; + + System.out.println("========================================"); + System.out.println("🔥 预热开始 (目标: " + (warmupTimeNs / 1_000_000) + "ms)"); + System.out.println("========================================"); + + loop:while(true) { + for (Path imageFile : imageFiles) { + if(elapsed >= warmupTimeNs) { + break loop; + } + long iterStart = System.nanoTime(); + try { + BufferedImage original = ImageIO.read(imageFile.toFile()); + if (original == null) { + continue; + } + + // 对所有格式进行预热压缩和解压(丢弃结果) + for (String format : outputFormats) { + try { + // 执行完整的编码+解码流程,但不保存结果 + Path tempFile = outputDir.resolve("warmup_" + iteration + "_" + + imageFile.getFileName().toString().replaceAll("\\.[^.]*$", "") + + "." + format.toLowerCase()); + + // 编码 + boolean written = ImageIO.write(original, format, tempFile.toFile()); + if (!written) { + continue; + } + + // 解码(验证无损性) + BufferedImage decoded = ImageIO.read(tempFile.toFile()); + if (decoded != null) { + // 简单比较尺寸,不逐像素比较(节省时间) + if (decoded.getWidth() == original.getWidth() && + decoded.getHeight() == original.getHeight()) { + // 预热成功 + } + } + + // 立即删除临时文件 + Files.deleteIfExists(tempFile); + + } catch (Exception e) { + // 预热时忽略错误 + } + } + + } catch (Exception e) { + // 忽略预热错误 + } + iteration++; + long iterEnd = System.nanoTime(); + elapsed = iterEnd - startTime; + } + } + + + + + System.out.println("========================================"); + System.out.printf("🔥 预热完成: %d 张图片, 耗时 %dms\n", + iteration, elapsed / 1_000_000L); + System.out.println("========================================"); + + // 清理临时文件 + try { + Files.list(outputDir) + .filter(p -> p.getFileName().toString().startsWith("warmup_")) + .forEach(p -> { + try { Files.deleteIfExists(p); } catch (Exception e) {} + }); + } catch (Exception e) { + // 忽略清理错误 + } + } + private static List runExperiment(Path outputDir, List imageFiles) { + List results = new ArrayList<>(); + for (Path imageFile : imageFiles) { + try { + // 读取原图 + long readStart = System.nanoTime(); + BufferedImage original = ImageIO.read(imageFile.toFile()); + long readEnd = System.nanoTime(); + long readTimeMs = (readEnd - readStart) / 1_000_000; + + if (original == null) { + System.err.println(" ⚠️ 无法读取:"+imageFile.getFileName()+",跳过\n"); + continue; + } + + System.out.println("========================================"); + System.out.println("测试图片: " + imageFile.getFileName()+" ("+original.getWidth()+"x"+original.getHeight()+")"); + System.out.println("========================================"); + + + + long originalSize = Files.size(imageFile); + System.out.printf(" 原始大小: %,d bytes (%.2f KB) | 读取耗时: %dms\n", + originalSize, originalSize / 1024.0, readTimeMs); + + // 对每种输出格式进行测试 + for (String format : outputFormats) { + try { + ExperimentResult result = testFormat(original, imageFile, format, outputDir); + results.add(result); + System.out.println(result); + } catch (Exception e) { + System.err.println(" ❌ " + format + " 测试失败: " ); + e.printStackTrace(); + } + } + System.out.println(); + + } catch (Exception e) { + System.err.println(" ❌ 处理 " + imageFile.getFileName() + " 时出错: " ); + e.printStackTrace(); + } + } + return results; + } + + /** + * 获取所有可用的图像写入格式 + */ + private static Set getAvailableWriterFormats() { + Set formats = new HashSet<>(); + IIORegistry registry = IIORegistry.getDefaultInstance(); + Iterator iter = registry.getServiceProviders(ImageWriterSpi.class, true); + while (iter.hasNext()) { + ImageWriterSpi spi = iter.next(); + String[] names = spi.getFormatNames(); + if (names != null) { + for (String name : names) { + formats.add(name.toLowerCase()); + } + } + } + return formats; + } + + /** + * 获取 input 目录下的所有图片文件 + */ + private static List getTestImages(Path inputDir) throws IOException { + List images = new ArrayList<>(); + try (DirectoryStream stream = Files.newDirectoryStream(inputDir)) { + for (Path entry : stream) { + if (Files.isRegularFile(entry)) { + String fileName = entry.getFileName().toString().toLowerCase(); + for (String fmt : INPUT_FORMATS) { + if (fileName.endsWith("." + fmt)) { + images.add(entry); + break; + } + } + } + } + } + images.sort(Comparator.comparing(p -> p.getFileName().toString())); + return images; + } + + /** + * 测试一种格式的压缩效果(纯内存测试,排除硬盘 I/O) + * 分别记录编码时间和解码时间 + */ + private static ExperimentResult testFormat(BufferedImage original, Path sourceFile, + String format, Path outputDir) throws Exception { + // 生成输出文件名(仅用于显示和保存验证) + String baseName = sourceFile.getFileName().toString(); + int dotIdx = baseName.lastIndexOf('.'); + String nameWithoutExt = (dotIdx > 0) ? baseName.substring(0, dotIdx) : baseName; + String outputFileName = nameWithoutExt + "." + format.toLowerCase(); + Path outputFile = outputDir.resolve(outputFileName); + + // ===== 编码时间(纯内存) ===== + long encodeStart = System.nanoTime(); + + // 使用 ByteArrayOutputStream 替代 FileOutputStream + int estimatedSize = (int) (original.getWidth()*original.getHeight()*4 * 1.5); + ByteArrayOutputStream baos = new ByteArrayOutputStream(estimatedSize); + boolean written = ImageIO.write(original, format, baos); + + long encodeEnd = System.nanoTime(); + long encodeTimeMs = (encodeEnd - encodeStart) / 1_000_000; + + if (!written) { + throw new IOException("ImageIO.write() 返回 false,可能不支持此格式"); + } + + // 压缩后大小(从内存中获取) + byte[] encodedData = baos.toByteArray(); + long compressedSize = encodedData.length; + long originalSize = Files.size(sourceFile); + double compressionRatio = (double) compressedSize / originalSize * 100; + + // ===== 解码时间(纯内存) ===== + long decodeStart = System.nanoTime(); + + // 使用 ByteArrayInputStream 替代 FileInputStream + ByteArrayInputStream bais = new ByteArrayInputStream(encodedData); + BufferedImage decoded = ImageIO.read(bais); + + long decodeEnd = System.nanoTime(); + long decodeTimeMs = (decodeEnd - decodeStart) / 1_000_000; + + // 验证无损性 + boolean lossless = compareImages(original, decoded); + + // ===== 可选:将结果保存到磁盘(用于调试/验证) ===== + // 如果需要实际保存文件以便后续查看,取消注释下面的代码 + // try (FileOutputStream fos = new FileOutputStream(outputFile.toFile())) { + // fos.write(encodedData); + // } + + return new ExperimentResult( + sourceFile.getFileName().toString(), + format, + originalSize, + compressedSize, + compressionRatio, + encodeTimeMs, + decodeTimeMs, + lossless + ); + } + + /** + * 逐像素比较两张图片是否完全相同 + */ + private static boolean compareImages(BufferedImage img1, BufferedImage img2) { + if (img1 == null || img2 == null) return false; + if (img1.getWidth() != img2.getWidth()) return false; + if (img1.getHeight() != img2.getHeight()) return false; + + int width = img1.getWidth(); + int height = img1.getHeight(); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (img1.getRGB(x, y) != img2.getRGB(x, y)) { + return false; + } + } + } + return true; + } + + /** + * 打印汇总报告 + */ + private static void printSummary(List results) { + System.out.println("\n\n"); + System.out.println("╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗"); + System.out.println("║ 汇总报告 ║"); + System.out.println("╚═══════════════════════════════════════════════════════════════════════════════════════════════════╝"); + + // 按格式分组统计 + Map> grouped = results.stream() + .collect(Collectors.groupingBy(r -> r.format)); + + // 表头 - 压缩率统计 + System.out.println("\n┌──────────┬────────────┬──────────────┬─────────────┬────────────┬──────────┐"); + System.out.println("│ 格式 │ 测试数量 │ 平均压缩率 │ 最大压缩率 │ 最小压缩率 │ 无损率 │"); + System.out.println("├──────────┼────────────┼──────────────┼─────────────┼────────────┼──────────┤"); + + for (Map.Entry> entry : grouped.entrySet()) { + String format = entry.getKey(); + List list = entry.getValue(); + + double avgRatio = list.stream().mapToDouble(r -> r.compressionRatio).average().orElse(0); + double maxRatio = list.stream().mapToDouble(r -> r.compressionRatio).max().orElse(0); + double minRatio = list.stream().mapToDouble(r -> r.compressionRatio).min().orElse(0); + long losslessCount = list.stream().filter(r -> r.lossless).count(); + double losslessRate = (double) losslessCount / list.size() * 100; + + System.out.printf("│ %-8s│ %10d│ %12.2f%%│ %11.2f%%│ %10.2f%%│ %8.1f%%│\n", + format, list.size(), avgRatio, maxRatio, minRatio, losslessRate); + } + System.out.println("└──────────┴────────────┴──────────────┴─────────────┴────────────┴──────────┘"); + + // 表头 - 时间统计 + System.out.println("\n┌──────────┬────────────┬──────────────┬──────────────┬──────────────────┐"); + System.out.println("│ 格式 │ 测试数量 │ 平均编码时间 │ 平均解码时间 │ 编码/解码比 │"); + System.out.println("├──────────┼────────────┼──────────────┼──────────────┼──────────────────┤"); + + for (Map.Entry> entry : grouped.entrySet()) { + String format = entry.getKey(); + List list = entry.getValue(); + + double avgEncode = list.stream().mapToLong(r -> r.encodeTimeMs).average().orElse(0); + double avgDecode = list.stream().mapToLong(r -> r.decodeTimeMs).average().orElse(0); + double ratio = avgDecode > 0 ? avgEncode / avgDecode : 0; + + System.out.printf("│ %-8s│ %10d│ %12.1fms│ %12.1fms│ %8.2fx │\n", + format, list.size(), avgEncode, avgDecode, ratio); + } + System.out.println("└──────────┴────────────┴──────────────┴──────────────┴──────────────────┘"); + + // KIF 专项统计 + List kifResults = results.stream() + .filter(r -> "kif".equalsIgnoreCase(r.format)) + .collect(Collectors.toList()); + + if (!kifResults.isEmpty()) { + System.out.println("\n📊 KIF 格式详细统计:"); + System.out.println(" 测试图片数: " + kifResults.size()); + double avgKifRatio = kifResults.stream().mapToDouble(r -> r.compressionRatio).average().orElse(0); + System.out.printf(" 平均压缩率: %.2f%% (相对于原始文件)\n", avgKifRatio); + long losslessKif = kifResults.stream().filter(r -> r.lossless).count(); + System.out.println(" 无损图片数: " + losslessKif + "/" + kifResults.size()); + + double avgKifEncode = kifResults.stream().mapToLong(r -> r.encodeTimeMs).average().orElse(0); + double avgKifDecode = kifResults.stream().mapToLong(r -> r.decodeTimeMs).average().orElse(0); + System.out.printf(" 平均编码时间: %.1f ms\n", avgKifEncode); + System.out.printf(" 平均解码时间: %.1f ms\n", avgKifDecode); + + // 与 PNG 对比 + List pngResults = results.stream() + .filter(r -> "png".equalsIgnoreCase(r.format)) + .collect(Collectors.toList()); + + if (!pngResults.isEmpty() && kifResults.size() == pngResults.size()) { + double pngAvg = pngResults.stream().mapToDouble(r -> r.compressionRatio).average().orElse(0); + System.out.printf(" PNG 平均压缩率: %.2f%%\n", pngAvg); + System.out.printf(" KIF vs PNG: %.2f%% (KIF 比 PNG %s %.2f%%)\n", + avgKifRatio, + avgKifRatio < pngAvg ? "小" : "大", + Math.abs(avgKifRatio - pngAvg)); + + double pngAvgEncode = pngResults.stream().mapToLong(r -> r.encodeTimeMs).average().orElse(0); + double pngAvgDecode = pngResults.stream().mapToLong(r -> r.decodeTimeMs).average().orElse(0); + System.out.printf(" PNG 平均编码时间: %.1f ms\n", pngAvgEncode); + System.out.printf(" PNG 平均解码时间: %.1f ms\n", pngAvgDecode); + System.out.printf(" KIF 编码是 PNG 的 %.2f 倍\n", avgKifEncode / pngAvgEncode); + System.out.printf(" KIF 解码是 PNG 的 %.2f 倍\n", avgKifDecode / pngAvgDecode); + } + } + } + + /** + * 实验结果记录类 + */ + static class ExperimentResult { + String sourceFileName; + String format; + long originalSize; + long compressedSize; + double compressionRatio; + long encodeTimeMs; + long decodeTimeMs; + boolean lossless; + + ExperimentResult(String sourceFileName, String format, long originalSize, + long compressedSize, double compressionRatio, + long encodeTimeMs, long decodeTimeMs, boolean lossless) { + this.sourceFileName = sourceFileName; + this.format = format; + this.originalSize = originalSize; + this.compressedSize = compressedSize; + this.compressionRatio = compressionRatio; + this.encodeTimeMs = encodeTimeMs; + this.decodeTimeMs = decodeTimeMs; + this.lossless = lossless; + } + + @Override + public String toString() { + String status = lossless ? "✅ 无损" : "⚠️ 有损"; + return String.format(" %-6s | %,8d bytes (%.1f KB) | 压缩率: %5.1f%% | 编码: %4dms | 解码: %4dms | %s", + format, compressedSize, compressedSize / 1024.0, + compressionRatio, encodeTimeMs, decodeTimeMs, status); + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/Compressors.java b/src/org/kne/codec/kif/Compressors.java new file mode 100644 index 0000000..276ab91 --- /dev/null +++ b/src/org/kne/codec/kif/Compressors.java @@ -0,0 +1,94 @@ +package org.kne.codec.kif; + +import io.airlift.compress.Compressor; +import io.airlift.compress.Decompressor; +import io.airlift.compress.thirdparty.JdkDeflateCompressor; +import io.airlift.compress.thirdparty.JdkInflateDecompressor; +import io.airlift.compress.thirdparty.ZstdJniCompressor; +import io.airlift.compress.zstd.ZstdCompressor; +import io.airlift.compress.zstd.ZstdDecompressor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import org.kne.membandboost.MembandBoost; + +/** + * 压缩/解压缩工具 + * 支持 Deflate(ZLIB)和 Zstandard 两种算法 + */ +public class Compressors { + + // ==================== Zstandard (Zstd) ==================== + private static final ThreadLocal ZSTD_COMPRESSOR = + ThreadLocal.withInitial(ZstdCompressor::new); + + private static final ThreadLocal ZSTD_DECOMPRESSOR = + ThreadLocal.withInitial(ZstdDecompressor::new); + + // ==================== Deflate (ZLIB) ==================== + + private static final ThreadLocal DEFLATE_COMPRESSOR = + ThreadLocal.withInitial(JdkDeflateCompressor::new); + + private static final ThreadLocal DEFLATE_DECOMPRESSOR = + ThreadLocal.withInitial(JdkInflateDecompressor::new); + /** + * 压缩 + */ + public static byte[] compress(Compressor comp,byte[] input) { + int maxLen = comp.maxCompressedLength(input.length); + byte[] compressed = MembandBoost.allocateUninitializedByteArray(maxLen); + int compressedSize = comp.compress( + input, 0, input.length, + compressed, 0, + maxLen + ); + //System.out.println("in:"+input.length+" out:"+compressedSize); + byte[] copy = MembandBoost.allocateUninitializedByteArray(compressedSize); + System.arraycopy(compressed, 0, copy, 0, + Math.min(compressed.length, compressedSize)); + return copy; + } + + /** + * 解压(已知原始长度) + */ + public static byte[] decompress(Decompressor decomp,byte[] input, int expectedLength) throws IOException { + try { + + byte[] output = MembandBoost.allocateUninitializedByteArray(expectedLength); + int decompressedSize = decomp.decompress( + input, 0, input.length, + output, 0, expectedLength + ); + if (decompressedSize != expectedLength) { + throw new IOException("解压长度不匹配: 期望 " + expectedLength + ", 实际 " + decompressedSize); + } + return output; + }catch(Exception e) { + e.printStackTrace(); + System.out.println("in:"+input.length+" expected:"+expectedLength); + throw e; + } + } + + + public static byte[] compressZstd(byte[] input) { + return compress(ZSTD_COMPRESSOR.get(), input); + } + public static byte[] decompressZstd(byte[] input, int expectedLength) throws IOException { + return decompress(ZSTD_DECOMPRESSOR.get(), input, expectedLength); + } + + public static byte[] compressDeflate(byte[] input) { + return compress(DEFLATE_COMPRESSOR.get(), input); + } + public static byte[] decompressDeflate(byte[] input, int expectedLength) throws IOException { + return decompress(DEFLATE_DECOMPRESSOR.get(), input, expectedLength); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/DebugDump.java b/src/org/kne/codec/kif/DebugDump.java new file mode 100644 index 0000000..2df7c04 --- /dev/null +++ b/src/org/kne/codec/kif/DebugDump.java @@ -0,0 +1,149 @@ +package org.kne.codec.kif; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.UUID; + +/** + * 调试工具:将字节数组 dump 到磁盘文件 + * 用于保存算法中间状态,方便用十六进制编辑器分析 + */ +public class DebugDump { + + // 调试开关:设为 false 可全局禁用 dump + private static boolean ENABLED = true; + + // dump 文件根目录 + private static final String DUMP_DIR = "debug"; + + /** + * 将字节数组 dump 到文件 + * 文件名:debug/[uuid].dat + * + * @param data 要保存的字节数组 + * @return 保存的文件路径,如果禁用则返回 null + */ + public static String dump(byte[] data) { + return dump(data, null); + } + + /** + * 将字节数组 dump 到文件,附带说明标签 + * 文件名:debug/[uuid]_[tag].dat + * + * @param data 要保存的字节数组 + * @param tag 标签(如 "raw", "predicted", "compressed"),可为 null + * @return 保存的文件路径,如果禁用则返回 null + */ + public static String dump(byte[] data, String tag) { + if (!ENABLED) { + return null; + } + + try { + // 确保目录存在 + File dir = new File(DUMP_DIR); + if (!dir.exists()) { + dir.mkdirs(); + } + + // 生成文件名 + String uuid = UUID.randomUUID().toString().substring(0, 8); + String fileName = tag != null && !tag.isEmpty() + ? uuid + "_" + tag + ".dat" + : uuid + ".dat"; + File file = new File(dir, fileName); + + // 写入数据 + try (FileOutputStream fos = new FileOutputStream(file)) { + fos.write(data); + } + + // 同时输出一个 .info 文件,记录元数据 + String infoFile = fileName.replace(".dat", ".info"); + String infoContent = String.format( + "Dump Info:\n" + + " UUID: %s\n" + + " Tag: %s\n" + + " Size: %,d bytes (%.2f KB)\n" + + " Time: %s\n", + uuid, + tag != null ? tag : "(none)", + data.length, + data.length / 1024.0, + java.time.LocalDateTime.now() + ); + Files.write(Paths.get(dir.getPath(), infoFile), infoContent.getBytes()); + + System.out.println("🔍 Dump: " + file.getAbsolutePath() + " (" + data.length + " bytes)"); + return file.getAbsolutePath(); + + } catch (IOException e) { + System.err.println("❌ Dump 失败: " + e.getMessage()); + return null; + } + } + + /** + * 将 int 数组转为 byte 数组后 dump(小端序) + */ + public static String dumpIntArray(int[] data, String tag) { + byte[] bytes = new byte[data.length * 4]; + ByteBuffer bb = ByteBuffer.wrap(bytes); + bb.order(ByteOrder.LITTLE_ENDIAN); + for (int v : data) { + bb.putInt(v); + } + return dump(bytes, tag); + } + + /** + * 将 int 数组转为 byte 数组后 dump(大端序) + */ + public static String dumpIntArrayBE(int[] data, String tag) { + byte[] bytes = new byte[data.length * 4]; + ByteBuffer bb = ByteBuffer.wrap(bytes); + bb.order(ByteOrder.BIG_ENDIAN); + for (int v : data) { + bb.putInt(v); + } + return dump(bytes, tag); + } + + /** + * 将 short 数组转为 byte 数组后 dump(小端序) + */ + public static String dumpShortArray(short[] data, String tag) { + byte[] bytes = new byte[data.length * 2]; + ByteBuffer bb = ByteBuffer.wrap(bytes); + bb.order(ByteOrder.LITTLE_ENDIAN); + for (short v : data) { + bb.putShort(v); + } + return dump(bytes, tag); + } + + /** + * 启用/禁用 dump + */ + public static void setEnabled(boolean enabled) { + ENABLED = enabled; + } + + /** + * 清空 debug 目录 + */ + public static void clean() throws IOException { + File dir = new File(DUMP_DIR); + if (dir.exists()) { + for (File f : dir.listFiles()) { + f.delete(); + } + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ExceptionTool.java b/src/org/kne/codec/kif/ExceptionTool.java new file mode 100644 index 0000000..60b34b1 --- /dev/null +++ b/src/org/kne/codec/kif/ExceptionTool.java @@ -0,0 +1,30 @@ +package org.kne.codec.kif; + +import java.io.IOException; +import java.util.concurrent.ExecutionException; + +public class ExceptionTool { + public static void throwIOException(Throwable t) throws IOException { + if (t == null) { + return; + } + + // 递归展开 ExecutionException + while (t instanceof ExecutionException && t.getCause() != null) { + t = t.getCause(); + } + + if (t instanceof IOException) { + throw (IOException) t; + } + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + if (t instanceof Error) { + throw (Error) t; + } + + throw new IOException(t); + } +} + diff --git a/src/org/kne/codec/kif/ImageCopy.java b/src/org/kne/codec/kif/ImageCopy.java new file mode 100644 index 0000000..7c0dc51 --- /dev/null +++ b/src/org/kne/codec/kif/ImageCopy.java @@ -0,0 +1,50 @@ +package org.kne.codec.kif; + +public class ImageCopy { + /** + * 图像块拷贝:将源图像中的一个矩形区域拷贝到目标图像的指定位置 + * + * @param src 源像素数组 (ARGB/RGB) + * @param srcw 源图像宽度 + * @param srch 源图像高度 + * @param dst 目标像素数组 (ARGB/RGB) + * @param dstw 目标图像宽度 + * @param dsth 目标图像高度 + * @param srcposx 源区域左上角 x 坐标 + * @param srcposy 源区域左上角 y 坐标 + * @param dstposx 目标区域左上角 x 坐标 + * @param dstposy 目标区域左上角 y 坐标 + * @param copyw 拷贝宽度 + * @param copyh 拷贝高度 + * @throws IllegalArgumentException 如果参数越界或无效 + */ + public static void imageCopy(int[] src, int srcw, int srch, + int[] dst, int dstw, int dsth, + int srcposx, int srcposy, + int dstposx, int dstposy, + int copyw, int copyh) { + // 参数校验 + if (src == null || dst == null) { + throw new IllegalArgumentException("源或目标数组不能为空"); + } + if (srcposx < 0 || srcposy < 0 || dstposx < 0 || dstposy < 0) { + throw new IllegalArgumentException("起始坐标不能为负数"); + } + if (copyw <= 0 || copyh <= 0) { + throw new IllegalArgumentException("拷贝宽度和高度必须大于0"); + } + if (srcposx + copyw > srcw || srcposy + copyh > srch) { + throw new IllegalArgumentException("源区域超出边界"); + } + if (dstposx + copyw > dstw || dstposy + copyh > dsth) { + throw new IllegalArgumentException("目标区域超出边界"); + } + + // 逐行拷贝(比逐像素快,利用内存连续性) + for (int row = 0; row < copyh; row++) { + int srcIdx = (srcposy + row) * srcw + srcposx; + int dstIdx = (dstposy + row) * dstw + dstposx; + System.arraycopy(src, srcIdx, dst, dstIdx, copyw); + } + } +} diff --git a/src/org/kne/codec/kif/IntImageBlock.java b/src/org/kne/codec/kif/IntImageBlock.java new file mode 100644 index 0000000..e6199af --- /dev/null +++ b/src/org/kne/codec/kif/IntImageBlock.java @@ -0,0 +1,198 @@ +package org.kne.codec.kif; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import jdk.internal.foreign.ArenaImpl; +import org.kne.membandboost.MembandBoost; + +public class IntImageBlock { + private MemorySegment image; // int[] 的 MemorySegment 封装 + private int width; + private int height; + private int posx; + private int posy; + + public IntImageBlock(MemorySegment image, int width, int height, int posx, int posy) { + this.image = image; + this.width = width; + this.height = height; + this.posx = posx; + this.posy = posy; + } + + public IntImageBlock(MemorySegment image, int width, int height) { + this.image = image; + this.width = width; + this.height = height; + this.posx = 0; + this.posy = 0; + } + + public IntImageBlock(int width2, int height2, int posx2, int posy2) { + this(((ArenaImpl) Arena.ofAuto()).allocateNoInit((width2 * height2) << 2, 4), width2, height2, posx2, posy2); + } + + public List getBlockCoords(int blockWidth, int blockHeight) { + return BlockCoord.calculateBlockCoords(width, height, blockWidth, blockHeight); + } + + public IntImageBlock createBlock(BlockCoord coord) { + return IntImageBlock.fromImage(image, width, height, coord.getPosX(), coord.getPosY(), coord.getWidth(), + coord.getHeight()); + } + + public static IntImageBlock createBlock(BufferedImage image, BlockCoord coord) { + return IntImageBlock.fromImage(image, image.getWidth(), image.getHeight(), coord.getPosX(), coord.getPosY(), + coord.getWidth(), coord.getHeight()); + } + + // ==================== 从 MemorySegment 提取块 ==================== + + public static IntImageBlock fromImage(MemorySegment src, int srcw, int srch, int posx, int posy, int blockw, + int blockh) { + if (src == null) { + throw new IllegalArgumentException("源图像不能为空"); + } + if (posx < 0 || posy < 0 || blockw <= 0 || blockh <= 0) { + throw new IllegalArgumentException("坐标和尺寸必须为正数"); + } + if (posx + blockw > srcw || posy + blockh > srch) { + throw new IllegalArgumentException("块区域超出源图像边界"); + } + + MemorySegment blockPixels = ((ArenaImpl) Arena.ofAuto()).allocateNoInit((blockw * blockh) << 2, 4); + + // 逐行拷贝(使用 MemorySegment.copy) + for (int row = 0; row < blockh; row++) { + long srcOffset = ((long) (posy + row) * srcw + posx) << 2; + long dstOffset = (long) row * blockw << 2; + MemorySegment.copy(src, srcOffset, blockPixels, dstOffset, (long) blockw << 2); + } + + return new IntImageBlock(blockPixels, blockw, blockh, posx, posy); + } + + public static IntImageBlock fromImage(BufferedImage src, int srcw, int srch, int posx, int posy, int blockw, + int blockh) { + if (src == null) { + throw new IllegalArgumentException("源图像不能为空"); + } + if (posx < 0 || posy < 0 || blockw <= 0 || blockh <= 0) { + throw new IllegalArgumentException("坐标和尺寸必须为正数"); + } + if (posx + blockw > srcw || posy + blockh > srch) { + throw new IllegalArgumentException("块区域超出源图像边界"); + } + + int type = src.getType(); + MemorySegment blockPixels = ((ArenaImpl) Arena.ofAuto()).allocateNoInit((long) blockw * blockh << 2, 4); + + // 情况1:TYPE_INT_ARGB 或 TYPE_INT_RGB → 零拷贝路径 + if (type == BufferedImage.TYPE_INT_ARGB || type == BufferedImage.TYPE_INT_RGB) { + DataBufferInt buffer = (DataBufferInt) src.getRaster().getDataBuffer(); + int[] pixels = buffer.getData(); + for (int row = 0; row < blockh; row++) { + int srcIdx = (posy + row) * srcw + posx; + MemorySegment.copy(pixels, srcIdx, blockPixels, ValueLayout.JAVA_INT, (long) row * blockw << 2, blockw); + } + return new IntImageBlock(blockPixels, blockw, blockh, posx, posy); + } + + // 情况2:其他格式 → 逐行读取,直接写入 MemorySegment + else { + int[] rowBuffer = new int[blockw]; + for (int row = 0; row < blockh; row++) { + // 一次性读取一行(1 次 JNI 调用) + src.getRGB(posx, posy + row, blockw, 1, rowBuffer, 0, blockw); + // 直接写入 MemorySegment(1 次 native 拷贝) + MemorySegment.copy(rowBuffer, 0, blockPixels, ValueLayout.JAVA_INT, (long) row * blockw << 2, blockw); + } + return new IntImageBlock(blockPixels, blockw, blockh, posx, posy); + } + } + + // ==================== 写入目标图像 ==================== + + public void toImage(MemorySegment dst, int dstw, int dsth) { + if (dst == null) { + throw new IllegalArgumentException("目标图像不能为空"); + } + if (posx < 0 || posy < 0) { + throw new IllegalArgumentException("块位置不能为负数"); + } + if (posx + width > dstw || posy + height > dsth) { + throw new IllegalArgumentException("块区域超出目标图像边界"); + } + + // 逐行拷贝到目标图像 + for (int row = 0; row < height; row++) { + long srcOffset = (long) row * width << 2; + long dstOffset = ((long) (posy + row) * dstw + posx) << 2; + MemorySegment.copy(image, srcOffset, dst, dstOffset, (long) width << 2); + } + } + + public void toImage(MemorySegment dst, int dstw, int dsth, int dstPosX, int dstPosY) { + if (dst == null) { + throw new IllegalArgumentException("目标图像不能为空"); + } + if (dstPosX < 0 || dstPosY < 0) { + throw new IllegalArgumentException("目标位置不能为负数"); + } + if (dstPosX + width > dstw || dstPosY + height > dsth) { + throw new IllegalArgumentException("块区域超出目标图像边界"); + } + + for (int row = 0; row < height; row++) { + long srcOffset = (long) row * width << 2; + long dstOffset = ((long) (dstPosY + row) * dstw + dstPosX) << 2; + MemorySegment.copy(image, srcOffset, dst, dstOffset, (long) width << 2); + } + } + + // ==================== 转换为 int[](兼容旧代码) ==================== + + public int[] toIntArray() { + int[] array = new int[width * height]; + MemorySegment.copy(image, ValueLayout.JAVA_INT, 0, array, 0, width * height); + return array; + } + + // ==================== Getter/Setter ==================== + + public MemorySegment getImage() { + return image; + } + + public void setImage(MemorySegment image) { + this.image = image; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getPosx() { + return posx; + } + + public int getPosy() { + return posy; + } + + @Override + public String toString() { + return "IntImageBlock[" + width + "x" + height + ",(" + posx + "," + posy + ")]"; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFCodec.cl b/src/org/kne/codec/kif/KIFCodec.cl new file mode 100644 index 0000000..18c769d --- /dev/null +++ b/src/org/kne/codec/kif/KIFCodec.cl @@ -0,0 +1,471 @@ + +/** + * Zigzag 映射:将有符号字节映射为非负字节 + * 0→0, -1→1, 1→2, -2→3, 2→4, ... + */ +uchar zigzag_map(char x) { + int v = (int)x; // char 转为 int(保留符号) + return (uchar)((v >= 0) ? (v << 1) : ((-v << 1) - 1)); +} + +/** + * Zigzag 逆映射:将非负字节还原为有符号字节 + * 0→0, 1→-1, 2→1, 3→-2, 4→2, ... + */ +char zigzag_unmap(uchar y) { + int v = (int)y; // 转为无符号 0~255 + return (char)((v & 1) == 0 ? (v >> 1) : (-((v + 1) >> 1))); +} + + + + +/** + * 像素安全加法:RGBA 四个通道分别相加,结果裁剪到 0~255 + */ +int pixelAdd(int a, int b) { + int ar = (a >> 16) & 0xFF; + int ag = (a >> 8) & 0xFF; + int ab = a & 0xFF; + int aa = (a >> 24) & 0xFF; + + int br = (b >> 16) & 0xFF; + int bg = (b >> 8) & 0xFF; + int bb = b & 0xFF; + int ba = (b >> 24) & 0xFF; + + int r = ar + br; + int g = ag + bg; + int bv = ab + bb; + int av = aa + ba; + + // 用 &0xFF 替代 min/max,保留低 8 位(自动溢出,等同于裁剪) + return ((av & 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + (bv & 0xFF); +} +/** + * 像素安全减法(等效于 Java 的 pixelSub) + * 逐通道相减,结果裁剪到 0~255,组装回 ARGB + */ +int pixelSub(int a, int b) { + int ar = (a >> 16) & 0xFF; + int ag = (a >> 8) & 0xFF; + int ab = a & 0xFF; + int aa = (a >> 24) & 0xFF; + + int br = (b >> 16) & 0xFF; + int bg = (b >> 8) & 0xFF; + int bb = b & 0xFF; + int ba = (b >> 24) & 0xFF; + + int r = ar - br; + int g = ag - bg; + int bv = ab - bb; + int av = aa - ba; + + return ((av & 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + (bv & 0xFF); +} + + +/** + * 位平面编码(矩阵转置风格)- 等效于 Java 版本 + * + * 核心逻辑: + * 对于输出数组的每个字节,依次从输入数组的8个位平面各取1个bit + * 即:输出字节的 bit i = 输入数组第 i 个位平面的当前位 + * + * 输出数组的每个字节,由8个位平面的各1个bit组成 + * 等价于:把 8×len 的 bit 矩阵转置为 len×8 的 bit 矩阵 + */ +__kernel void encodePacked( + __global const uchar* input, // 输入字节数组 + __global uchar* output, // 输出字节数组(长度与输入相同) + int len // 输入/输出长度 +) { + int outIdx = get_global_id(0); + if (outIdx >= len) return; + + int outByte = 0; + + // 从8个位平面各取1个bit,组装成一个字节 + for (int plane = 0; plane < 8; plane++) { + int bitpos = (outIdx << 3) + plane; + int inputPos = bitpos % len; + int bitShift = bitpos / len; + int bit = (input[inputPos] >> (7 - bitShift)) & 1; + outByte |= (bit << plane); + } + + output[outIdx] = (uchar)outByte; +} + +__kernel void decodePacked(__global const unsigned char* datain, + __global unsigned char* dataout, + int size) { + int outIdx = get_global_id(0); + + // 边界检查 + if (outIdx >= size) { + return; + } + + int outByte = 0; + + // 从 8 个位平面各取 1 个 bit,组装成一个字节 + // plane 0 → bit7, plane 1 → bit6, ..., plane 7 → bit0 + for (int plane = 0; plane < 8; plane++) { + int bitpos = plane * size + outIdx; + int inputpos = bitpos >> 3; + int inputshift = bitpos & 0b111; + int bit = (datain[inputpos] >> inputshift) & 1; + outByte |= (bit << (7 - plane)); + } + + dataout[outIdx] = (unsigned char)outByte; +} + +/** + * 序列化 + Zigzag 映射(RGBA 交错) + */ +__kernel void serializeRGBAWithZigzag( + __global int* pixels, + __global uchar* output, + int size +) { + int i = get_global_id(0); + if (i >= size) return; + + int pixel = pixels[i]; + + char r = (char)((pixel >> 16) & 0xFF); + char g = (char)((pixel >> 8) & 0xFF); + char b = (char)(pixel & 0xFF); + char a = (char)((pixel >> 24) & 0xFF); + + int idx = i * 4; + output[idx + 0] = zigzag_map(r); + output[idx + 1] = zigzag_map(g); + output[idx + 2] = zigzag_map(b); + output[idx + 3] = zigzag_map(a); +} +// 对应 deserializeRGBAWithZigzag +__kernel void deserializeRGBAWithZigzag( + __global const uchar* data, // 输入:Zigzag 映射后的字节流(RGBA 交错) + __global int* pixels, // 输出:像素数组(ARGB) + int size // 像素数量 (w * h) +) { + int i = get_global_id(0); + if (i >= size) return; + + int idx = i * 4; + + // 读取并逆映射四个通道 + uchar r_raw = data[idx]; + uchar g_raw = data[idx + 1]; + uchar b_raw = data[idx + 2]; + uchar a_raw = data[idx + 3]; + + char r = zigzag_unmap(r_raw); + char g = zigzag_unmap(g_raw); + char b = zigzag_unmap(b_raw); + char a = zigzag_unmap(a_raw); + + // 组装成 ARGB (Java 的 int 格式) + // 注意:OpenCL 的 int 是 32 位有符号,和 Java 一致 + pixels[i] = ((int)(a & 0xFF) << 24) | + ((int)(r & 0xFF) << 16) | + ((int)(g & 0xFF) << 8) | + (int)(b & 0xFF); +} +/** + * 序列化 + Zigzag 映射(平面 RGBA) + */ +__kernel void serializePlannarRGBAWithZigzag( + __global int* pixels, + __global uchar* output, + int size +) { + int i = get_global_id(0); + if (i >= size) return; + + int pixel = pixels[i]; + + char r = (char)((pixel >> 16) & 0xFF); + char g = (char)((pixel >> 8) & 0xFF); + char b = (char)(pixel & 0xFF); + char a = (char)((pixel >> 24) & 0xFF); + + int rOffset = 0; + int gOffset = size; + int bOffset = size * 2; + int aOffset = size * 3; + + output[rOffset + i] = zigzag_map(r); + output[gOffset + i] = zigzag_map(g); + output[bOffset + i] = zigzag_map(b); + output[aOffset + i] = zigzag_map(a); +} +// 对应 deserializePlannarRGBAWithZigzag +__kernel void deserializePlannarRGBAWithZigzag( + __global const uchar* data, // 输入:Zigzag 映射后的字节流(RRRR...GGGG...BBBB...AAAA...) + __global int* pixels, // 输出:像素数组(ARGB) + int size // 像素数量 (w * h) +) { + int i = get_global_id(0); + if (i >= size) return; + + int rOffset = 0; + int gOffset = size; + int bOffset = size * 2; + int aOffset = size * 3; + + // 从四个平面分别读取并逆映射 + uchar r_raw = data[rOffset + i]; + uchar g_raw = data[gOffset + i]; + uchar b_raw = data[bOffset + i]; + uchar a_raw = data[aOffset + i]; + + char r = zigzag_unmap(r_raw); + char g = zigzag_unmap(g_raw); + char b = zigzag_unmap(b_raw); + char a = zigzag_unmap(a_raw); + + // 组装成 ARGB + pixels[i] = ((int)(a & 0xFF) << 24) | + ((int)(r & 0xFF) << 16) | + ((int)(g & 0xFF) << 8) | + (int)(b & 0xFF); +} + +/** + * 水平预测(非原地,完美并行) + * residual = pixelSub(current, left) + * 每个线程独立处理一个像素 + */ +__kernel void horizontalPredictParallel( + __global const int* input, // 原始像素 + __global int* output, // 残差输出 + int w, + int h +) { + int idx = get_global_id(0); + int size = w * h; + if (idx >= size) return; + + int x = idx % w; + + if (x == 0) { + // 第一列:残差 = 原值(没有左邻居) + output[idx] = input[idx]; + } else { + int left = input[idx - 1]; + output[idx] = pixelSub(input[idx], left); + } +} + +/** + * 垂直预测(非原地,完美并行) + * residual = pixelSub(current, above) + * 每个线程独立处理一个像素 + */ +__kernel void verticalPredictParallel( + __global const int* input, // 原始像素(或水平预测后的残差) + __global int* output, // 残差输出 + int w, + int h +) { + int idx = get_global_id(0); + int size = w * h; + if (idx >= size) return; + + int y = idx / w; + + if (y == 0) { + // 第一行:残差 = 原值(没有上邻居) + output[idx] = input[idx]; + } else { + int above = input[idx - w]; + output[idx] = pixelSub(input[idx], above); + } +} + +/** + * 水平预测(使用 pixelSub) + * residual = pixelSub(current, left) + * 从右向左,保证 left 是原始值 + */ +__kernel void horizontalPredictInPlace( + __global int* data, + int w, + int h +) { + int row = get_global_id(0); + if (row >= h) return; + + int base = row * w; + + // 从右向左(第一列不变) + for (int x = w - 1; x >= 1; x--) { + int idx = base + x; + int left = data[idx - 1]; + data[idx] = pixelSub(data[idx], left); + } +} +/** + * 水平逆预测(原地操作) + * 每个工作组处理一行,行内从左到右串行 + * + * 数据依赖:左侧像素 (x-1, y) 必须先被还原 + *//** + * 水平逆预测(寄存器优化版) + * + * 核心优化:预测值在寄存器中传递,减少显存读取 + */ +__kernel void horizontalInverseInPlace( + __global int* data, + int w, + int h +) { + int row = get_global_id(0); + if (row >= h) return; + + int base = row * w; + + // 第一列保持不变 + // 直接用 data[base] 作为初始预测值 + int pred = data[base]; // ✅ 只读一次显存 + + // 行内从左到右串行,但 pred 在寄存器中传递 + for (int x = 1; x < w; x++) { + int idx = base + x; + int residual = data[idx]; // ✅ 只读残差 + int result = pixelAdd(pred, residual); + data[idx] = result; // ✅ 只写一次显存 + pred = result; // ✅ 寄存器传递(下次循环直接使用) + } + +} +/** + * 垂直预测(使用 pixelSub) + * residual = pixelSub(current, above) + * 从下向上,保证 above 是原始值 + */ +__kernel void verticalPredictInPlace( + __global int* data, + int w, + int h +) { + int col = get_global_id(0); + if (col >= w) return; + + // 从下向上(第一行不变) + for (int y = h - 1; y >= 1; y--) { + int idx = y * w + col; + int above = data[(y - 1) * w + col]; + data[idx] = pixelSub(data[idx], above); + } +} +/** + * 垂直逆预测(原地操作) + * 每个工作组处理一列,列内从上到下串行 + * + * 数据依赖:上方像素 (x, y-1) 必须先被还原 + *//** + * 垂直逆预测(寄存器优化版) + */ +__kernel void verticalInverseInPlace( + __global int* data, + int w, + int h +) { + int col = get_global_id(0); + if (col >= w) return; + + // 第一行:用 data[col] 作为初始预测值 + int pred = data[col]; // ✅ 只读一次显存 + + for (int y = 1; y < h; y++) { + int idx = y * w + col; + int residual = data[idx]; // ✅ 只读残差 + int result = pixelAdd(pred, residual); + data[idx] = result; + pred = result; // ✅ 寄存器传递 + } +} +/** + * RGBA 颜色变换(原地版本)- 等效于 Java 的 colorTransformRGBAInPlace + * + * 编码:R' = R - G, G' = G, B' = B - G, A' = A + * 结果存储为:A | R' | G | B'(与 Java 版本完全一致) + */ +__kernel void colorTransformRGBAInPlace( + __global int* data, // 输入/输出像素数组 + int size // 像素总数 (w * h) +) { + int idx = get_global_id(0); + if (idx >= size) return; + + int pixel = data[idx]; + + int r = (pixel >> 16) & 0xFF; + int g = (pixel >> 8) & 0xFF; + int b = pixel & 0xFF; + int a = (pixel >> 24) & 0xFF; + + int rg = r - g; // 范围 -255 ~ 255 + int bg = b - g; // 范围 -255 ~ 255 + + // 与 Java 版本完全一致: + // ((a & 0xFF) << 24) | ((rg & 0xFF) << 16) | ((g & 0xFF) << 8) | (bg & 0xFF) + data[idx] = ((a & 0xFF) << 24) | + ((rg & 0xFF) << 16) | + ((g & 0xFF) << 8) | + (bg & 0xFF); +} + +/** + * 颜色逆变换(RGBA,逐像素并行)- 与 colorTransformRGBAInPlace 完全对应 + * + * Java 版本: + * a = (packed >> 24) & 0xFF + * rg = (packed >> 16) & 0xFF + * g = (packed >> 8) & 0xFF + * bg = packed & 0xFF + * r = g + rg + * b = g + bg + * result = (a << 24) | (r << 16) | (g << 8) | b + */ +__kernel void colorInverseRGBAInPlace( + __global int* data, // 输入:颜色差分数据,输出:还原后的 RGBA + int size // 像素总数 (w * h) +) { + int idx = get_global_id(0); + if (idx >= size) return; + + int packed = data[idx]; + + // 提取各通道 + int a = (packed >> 24) & 0xFF; + int rg = (packed >> 16) & 0xFF; + int g = (packed >> 8) & 0xFF; + int bg = packed & 0xFF; + + // 还原 R 和 B + int r = g + rg; + int b = g + bg; + + // 裁剪到 0~255(用 &0xFF 保留低 8 位) + r = r & 0xFF; + b = b & 0xFF; + + // 组装回 ARGB + data[idx] = ((a & 0xFF) << 24) | + ((r & 0xFF) << 16) | + ((g & 0xFF) << 8) | + (b & 0xFF); +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFCodec.java b/src/org/kne/codec/kif/KIFCodec.java new file mode 100644 index 0000000..0b5ff0a --- /dev/null +++ b/src/org/kne/codec/kif/KIFCodec.java @@ -0,0 +1,192 @@ +package org.kne.codec.kif; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.AbstractExecutorService; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javax.imageio.stream.ImageInputStream; + +import org.kne.debug.TimeDebugger; +import org.kne.opencl64.OpenCLDevice; +import org.kne.opencl64.concurrent.OpenCLExecutors; + + +public class KIFCodec { + private static ThreadPoolExecutor cpupool ; + private static ThreadPoolExecutor gpupool ; + static { + cpupool= (ThreadPoolExecutor) Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "KIFCodec-CPU-Worker-" + threadNumber.getAndIncrement()); + t.setDaemon(true); // 设置为守护线程 + return t; + } + } + ); + try { + Listdevs=OpenCLDevice.getAllGPU(); + for(OpenCLDevice dev:devs) { + System.out.println(dev); + } + gpupool=(ThreadPoolExecutor) OpenCLExecutors.newFixedThreadPool(devs,4); + }catch(Throwable e) { + e.printStackTrace(); + } + } + public static KIFCompressedImageBlock encode(IntImageBlock input,KIFCompressMode mode) { + KIFEncodePipeline kip=new KIFEncodePipeline(input, mode); + kip.encodeStage1(); + return kip.encodeStage2(); + } + + public static Future encodeAsync(IntImageBlock input,KIFCompressMode mode) { + return encodeAsync(()->{return input;},mode); + } + + public static Future encodeAsync(Supplier input,KIFCompressMode mode) { + /*CompletableFuture f1=CompletableFuture.supplyAsync(()->{ + KIFEncodePipeline kip=new KIFEncodePipeline(input, mode); + kip.encodeStage1(); + return kip; + }, gpupool); + CompletableFuturef2=f1.thenApplyAsync((pipeline)->{ + return pipeline.encodeStage2(); + },cpupool);*/ + return getBestPool().submit(()->{ + KIFEncodePipeline kip=new KIFEncodePipeline(input.get(), mode); + kip.encodeStage1(); + return kip.encodeStage2(); + }); + } + + public static Future encodeAsyncMultimode(IntImageBlock block, List list) { + return encodeAsyncMultimode(()->{return block;},list); + } + public static Future encodeAsyncMultimode(Supplier< IntImageBlock> block, List list) { + // 为每个 mode 提交一个异步编码任务 + @SuppressWarnings("unchecked") + ArrayList> futures = new ArrayList>(list.size()); + + for (KIFCompressMode mode:list) { + // 这里假设 encodeAsync 是提交到线程池的方法 + futures.add( encodeAsync(block, mode)); + } + + // 返回 MultiModeFuture,它会等待所有任务完成,选择最小的 + return new MultiModeFuture(futures); + } + + + public static IntImageBlock decode( KIFCompressedImageBlock input) throws IOException { + KIFDecodePipeline kdec=new KIFDecodePipeline(input); + return kdec.decodeStage2(); + } + + + public static Future decodeAsync(Supplier< KIFCompressedImageBlock> input) { + + return decodeAndConsumeAsync(input,null); + } + + + + + public static Future decodeAndConsumeAsync(Supplier< KIFCompressedImageBlock> input,Consumer con) { + /*CompletableFuture f1=CompletableFuture.supplyAsync(()->{ + KIFDecodePipeline kip=new KIFDecodePipeline(input); + try { + kip.decodeStage1(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return kip; + }, cpupool); + CompletableFuturef2=f1.thenApplyAsync((pipeline)->{ + try { + return pipeline.decodeStage2(); + } catch (IOException e) { + throw new RuntimeException(e); + } + },gpupool);*/ + return getBestPool().submit(()->{ + KIFDecodePipeline kip=new KIFDecodePipeline(input.get()); + try { + kip.decodeStage1(); + } catch (IOException e) { + throw new RuntimeException(e); + } + IntImageBlock out=kip.decodeStage2(); + if(con!=null) + con.accept(out); + return out; + }); + } + + public static void gpuBackPressure() { + while(isGPUOverloaded()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + public static void cpuBackPressure() { + while(isCPUOverloaded()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + /** + * 判断是否过载(调用方可据此决定是否降级处理) + */ + public static boolean isGPUOverloaded() { + return gpupool.getQueue().size() > gpupool.getPoolSize() ; + } + /** + * 判断是否过载(调用方可据此决定是否降级处理) + */ + public static boolean isCPUOverloaded() { + return cpupool.getQueue().size() > cpupool.getPoolSize() ; + } + + public static void backPressure() { + while(isCPUOverloaded()&&isGPUOverloaded()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + + private static AbstractExecutorService getBestPool() { + if(gpupool==null) { + return cpupool; + } + if(isGPUOverloaded()) { + return cpupool; + }else { + return gpupool; + } + } + +} diff --git a/src/org/kne/codec/kif/KIFCompressMode.java b/src/org/kne/codec/kif/KIFCompressMode.java new file mode 100644 index 0000000..9952185 --- /dev/null +++ b/src/org/kne/codec/kif/KIFCompressMode.java @@ -0,0 +1,190 @@ +package org.kne.codec.kif; + +import java.util.ArrayList; +import java.util.List; + +public class KIFCompressMode { + private int mode; + + public KIFCompressMode(int mode) { + super(); + this.mode = mode; + } + + public int getMode() { + return mode; + } + + // ==================== 6个bit的getter方法 ==================== + + /** + * 是否启用水平预测 (H) + */ + public boolean isH() { + return (mode & 0b0001) != 0; + } + + /** + * 是否启用垂直预测 (V) + */ + public boolean isV() { + return (mode & 0b0010) != 0; + } + + /** + * 是否启用左上角预测 (TL) + */ + public boolean isTL() { + return (mode & 0b0100) != 0; + } + + /** + * 是否启用右上角预测 (TR) + */ + public boolean isTR() { + return (mode & 0b1000) != 0; + } + + /** + * 是否启用颜色差分 (CD) + */ + public boolean isCD() { + return (mode & 0b10000) != 0; + } + + /** + * 是否启用位平面 (BP) + */ + public boolean isBP() { + return (mode & 0b100000) != 0; + } + + + /** + * 是否启用平面序列化 (PL) + * 启用后,像素按 RRRGGGBBB 顺序存储,而非 RGBRGBRGB + * 对屏幕内容可能有更好的压缩率 + */ + public boolean isPL() { + return (mode & 0b1000000) != 0; + } + + // ==================== 批量判断 ==================== + + /** + * 检查是否启用了任何预测器 (H/V/TL/TR) + */ + public boolean hasAnyPredictor() { + return (mode & 0b1111) != 0; + } + + /** + * 获取预测器部分(低4位) + */ + public int getPredictorBits() { + return mode & 0b1111; + } + + /** + * 获取当前启用的预测器数量 + */ + public int getPredictorCount() { + int count = 0; + int n = mode & 0b1111; + while (n != 0) { + count++; + n &= (n - 1); + } + return count; + } + + public int getEnableCount() { + return bitcount(mode&0xff); + } + + private static int bitcount ( int n) + { + int count=0 ; + while (n!=0) { + count++ ; + n &= (n - 1) ; + } + return count ; + } + // ==================== 原有的hashCode/equals/toString ==================== + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + mode; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + KIFCompressMode other = (KIFCompressMode) obj; + if (mode != other.mode) + return false; + return true; + } + + /** + * 将模式编号转换为可读的预测器组合字符串 + */ + private static String modeToString(int mode) { + StringBuilder sb = new StringBuilder(); + if ((mode & 0b0001) != 0) sb.append("H "); + if ((mode & 0b0010) != 0) sb.append("V "); + if ((mode & 0b0100) != 0) sb.append("TL "); + if ((mode & 0b1000) != 0) sb.append("TR "); + if ((mode & 0b10000) != 0) sb.append("CD "); + if ((mode & 0b100000) != 0) sb.append("BP "); + if ((mode & 0b1000000) != 0) sb.append("PL "); + if (sb.length() == 0) sb.append("0 "); + return sb.toString().trim(); + } + + @Override + public String toString() { + return modeToString(mode) + "(" + mode + ")"; + } + + public static List getAllModes() { + List result=new ArrayList<>(64); + for(int i=0;i<128;i++) { + KIFCompressMode mode=new KIFCompressMode(i); + if(mode.isTL()||mode.isTR()) { + continue; + } + result.add(mode); + } + return result; + }public static List getNaturalImageModes() { + List result = new ArrayList<>(16); + // 自然图像中最常用的模式(按优先级排序) + result.add(new KIFCompressMode(115)); // H V CD BP PL(115) + result.add(new KIFCompressMode(114)); // V CD BP PL(114) + result.add(new KIFCompressMode(113)); // H CD BP PL(113) + return result; + } + + public static List getScreenImageModes() { + List result = new ArrayList<>(16); + // 屏幕图像中最常用的模式(按优先级排序) + result.add(new KIFCompressMode(0));//0(0) + result.add(new KIFCompressMode(114));// V CD BP PL(114) + result.add(new KIFCompressMode(16)); // CD(16) + result.add(new KIFCompressMode(80)); // CD PL(80) + result.add(new KIFCompressMode(17)); // H CD(17) + result.add(new KIFCompressMode(34)); // V BP(34) + result.add(new KIFCompressMode(115)); // H V CD BP PL(115) + return result; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFCompressedImageBlock.java b/src/org/kne/codec/kif/KIFCompressedImageBlock.java new file mode 100644 index 0000000..244a35b --- /dev/null +++ b/src/org/kne/codec/kif/KIFCompressedImageBlock.java @@ -0,0 +1,207 @@ +package org.kne.codec.kif; + +import java.io.DataInput; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Arrays; + +import org.kne.membandboost.MembandBoost; + +public class KIFCompressedImageBlock implements Comparable{ + private byte[] data; + private int sizeBeforeCompress; + private int width; + private int height; + private int posx; + private int posy; + private KIFCompressMode predictMode; // 预测模式 (0=H+V, 1=TL+TR, 等) + + public KIFCompressedImageBlock(byte[] data, int sizeBeforeCompress, int width, int height, + int posx, int posy, KIFCompressMode predictMode) { + super(); + this.data = data; + this.sizeBeforeCompress = sizeBeforeCompress; + this.width = width; + this.height = height; + this.posx = posx; + this.posy = posy; + this.predictMode = predictMode; + } + + // ==================== I/O 方法 ==================== + + /** + * 从 DataInput 读取一个压缩块 + * 格式: + * [predictMode 1B] [预留 3B] + * [width 4B] [height 4B] [posx 4B] [posy 4B] + * [sizeBeforeCompress 4B] [dataLength 4B] [data N B] + * + * @param in DataInput 源 + * @return 读取到的 KIFCompressedImageBlock + * @throws IOException 如果读取失败 + */ + public static KIFCompressedImageBlock readFromStream(DataInput in) throws IOException { + // 1. 读取预测模式 + byte predictMode = in.readByte(); + + // 2. 跳过预留字节 (3 bytes) + in.skipBytes(1); + + // 3. 读取元数据 + int width = in.readInt(); + int height = in.readInt(); + int posx = in.readInt(); + int posy = in.readInt(); + int sizeBeforeCompress = in.readInt(); + int dataLength = in.readInt(); + + // 4. 读取数据 + byte[] data = MembandBoost.allocateUninitializedByteArray(dataLength); + in.readFully(data); + + return new KIFCompressedImageBlock(data, sizeBeforeCompress, width, height, posx, posy,new KIFCompressMode( predictMode)); + } + + /** + * 将当前块写入 DataOutput + * + * @param out DataOutput 目标 + * @throws IOException 如果写入失败 + */ + public void writeToStream(DataOutput out) throws IOException { + // 1. 写入预测模式 + out.writeByte(predictMode.getMode()); + + // 2. 预留字节 (1 bytes) + out.writeByte(0); + + // 3. 写入元数据 + out.writeInt(width); + out.writeInt(height); + out.writeInt(posx); + out.writeInt(posy); + out.writeInt(sizeBeforeCompress); + out.writeInt(data.length); + + // 4. 写入数据 + out.write(data); + } + + /** + * 计算写入流时需要的字节数 + */ + public int getSerializedSize() { + return 1 + 3 + 6 * 4 + data.length; + } + + // ==================== 静态工厂方法 ==================== + + public static KIFCompressedImageBlock fromIntImageBlock(IntImageBlock block, byte[] compressedData, + int sizeBeforeCompress, KIFCompressMode predictMode) { + return new KIFCompressedImageBlock( + compressedData, + sizeBeforeCompress, + block.getWidth(), + block.getHeight(), + block.getPosx(), + block.getPosy(), + predictMode + ); + } + + // ==================== Getters/Setters ==================== + + public byte[] getData() { + return data; + } + + public int getSizeBeforeCompress() { + return sizeBeforeCompress; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getPosx() { + return posx; + } + + public int getPosy() { + return posy; + } + + public KIFCompressMode getPredictMode() { + return predictMode; + } + + + + // ==================== toString / hashCode / equals ==================== + + @Override + public String toString() { + return "KIFCompressedImageBlock[" + width + "x" + height + ",(" + posx + "," + posy + "),mode=" + predictMode + "," + data.length + "B,src=" + sizeBeforeCompress + "B]"; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(data); + result = prime * result + height; + result = prime * result + posx; + result = prime * result + posy; + result = prime * result + ((predictMode == null) ? 0 : predictMode.hashCode()); + result = prime * result + sizeBeforeCompress; + result = prime * result + width; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + KIFCompressedImageBlock other = (KIFCompressedImageBlock) obj; + if (!Arrays.equals(data, other.data)) + return false; + if (height != other.height) + return false; + if (posx != other.posx) + return false; + if (posy != other.posy) + return false; + if (predictMode == null) { + if (other.predictMode != null) + return false; + } else if (!predictMode.equals(other.predictMode)) + return false; + if (sizeBeforeCompress != other.sizeBeforeCompress) + return false; + if (width != other.width) + return false; + return true; + } + + @Override + public int compareTo(KIFCompressedImageBlock o) { + int v=Integer.compare(data.length, o.data.length); + if(v==0) { + return Integer.compare(predictMode.getEnableCount(), o.predictMode.getEnableCount()) ; + }else { + return v; + } + } + + + +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFDecodePipeline.java b/src/org/kne/codec/kif/KIFDecodePipeline.java new file mode 100644 index 0000000..2a4a89e --- /dev/null +++ b/src/org/kne/codec/kif/KIFDecodePipeline.java @@ -0,0 +1,178 @@ +package org.kne.codec.kif; + +import java.awt.geom.Area; +import java.io.IOException; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; + +import org.kne.debug.TimeDebugger; +import org.kne.membandboost.MembandBoost; +import org.kne.opencl64.OpenCLKernel; +import org.kne.opencl64.OpenCLMemory; +import org.kne.opencl64.OpenCLMemoryCopyType; +import org.kne.opencl64.OpenCLProgram; +import org.kne.opencl64.concurrent.OpenCLContextThread; +import org.kne.opencl64.concurrent.ThreadLocalOpenCLMemory; + +import jdk.internal.foreign.ArenaImpl; + +public class KIFDecodePipeline { + private KIFCompressedImageBlock input; + + + + public KIFDecodePipeline(KIFCompressedImageBlock input) { + super(); + this.input = input; + data = input.getData(); + width = input.getWidth(); + height = input.getHeight(); + mode = input.getPredictMode(); + } + private TimeDebugger tdb=new TimeDebugger("解码时间",false); + private volatile byte[] data; + private volatile int width; + private volatile int height; + private volatile KIFCompressMode mode; + private volatile byte[] dataserp; + + + public void decodeStage1() throws IOException { + dataserp = Compressors.decompressZstd(data,input.getSizeBeforeCompress()); + tdb.mark("熵解码"); + } + + public IntImageBlock decodeStage2() throws IOException { + + Thread t =Thread. currentThread(); + if (t instanceof OpenCLContextThread) { + OpenCLContextThread oct = (OpenCLContextThread) t; + return decodeStage2GPU(oct); + }else { + return decodeStage2CPU(); + } + } + private static ThreadLocalm1=ThreadLocal.withInitial(()->{ + return ((ArenaImpl)Arena.ofAuto()).allocateNoInit(1024*1024*4,4); + }); + + private static ThreadLocalOpenCLMemory tlm0=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlm1=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlm2=new ThreadLocalOpenCLMemory(1024*1024*4); + private IntImageBlock decodeStage2GPU(OpenCLContextThread oct) throws IOException { + tdb.mark("等待调度"); + OpenCLProgram prog = oct.getProgram("/org/kne/codec/kif/KIFCodec.cl"); + tdb.mark("加载程序"); + //OpenCLKernel krl4 = prog.createKernel("colorInverseRGBAInPlace"); + OpenCLMemory ocm1 = null; + OpenCLMemory ocm2 = null; + OpenCLMemory ocm3 = null; + int[]des; + MemorySegment dataserpseg= m1.get(); + tdb.mark("堆外内存分配"); + dataserpseg.asByteBuffer().put(dataserp).flip(); + tdb.mark("堆内存->堆外内存"); + ocm1=tlm0.get(oct.getOpenCLContext()); + tdb.mark("分配显存"); + oct.getOpenCLCommandQueue().executeWriteBuffer(ocm1, dataserpseg, false,0,0,dataserp.length); + tdb.mark("内存->显存"); + + OpenCLMemory dataser; + if(mode.isBP()) { + ocm2=tlm1.get(oct.getOpenCLContext()); + OpenCLKernel krl0 = prog.createKernel("decodePacked"); + krl0.putArg(ocm1); + krl0.putArg(ocm2); + krl0.putArg(dataserp.length); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl0, 0, dataserp.length); + dataser=ocm2;// + }else { + dataser=ocm1; + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("位平面解码"); + + ocm3=tlm2.get(oct.getOpenCLContext()); + if(mode.isPL()) { + OpenCLKernel krl1 = prog.createKernel("deserializePlannarRGBAWithZigzag"); + krl1.putArg(dataser); + krl1.putArg(ocm3); + krl1.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl1, 0,width*height); + }else { + OpenCLKernel krl2 = prog.createKernel("deserializeRGBAWithZigzag"); + krl2.putArg(dataser); + krl2.putArg(ocm3); + krl2.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl2, 0,width*height); + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("反序列化"); + + + if(mode.isCD()) { + OpenCLKernel krl4 = prog.createKernel("colorInverseRGBAInPlace"); + krl4.putArg(ocm3); + krl4.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl4, 0, width*height); + } + if(mode.isV()) { + OpenCLKernel krl3 = prog.createKernel("verticalInverseInPlace"); + krl3.putArg(ocm3); + krl3.putArg(width); + krl3.putArg(height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl3, 0, width); + } + if(mode.isH()) { + OpenCLKernel krl3 = prog.createKernel("horizontalInverseInPlace"); + krl3.putArg(ocm3); + krl3.putArg(width); + krl3.putArg(height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl3, 0, height); + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("预测解码"); + tdb.mark("解码"); + IntImageBlock result=new IntImageBlock( width, height, input.getPosx(), input.getPosy()); + tdb.mark("分配堆外内存"); + + oct.getOpenCLCommandQueue().executeReadBuffer(ocm3, result.getImage(), true,0,0,width*height*4); + tdb.mark("显存->内存"); + // MemorySegment.copy(dataserpseg, ValueLayout.JAVA_INT, 0, des, 0, des.length); + // tdb.mark("堆外内存->堆内存"); + tdb.print(); + + return result; + } + + private IntImageBlock decodeStage2CPU() throws IOException { + byte[]dataser; + if(mode.isBP()) { + dataser=BitPlane.decodePacked(dataserp); + }else { + dataser=dataserp; + } + tdb.mark("位平面解码"); + int[]des; + if(mode.isPL()) { + des=PixelSerializer.deserializePlannarRGBAWithZigzag(dataser, width, height); + }else { + des=PixelSerializer.deserializeRGBAWithZigzag(dataser, width, height); + } + tdb.mark("反序列化"); + if(mode.isTR()) + Predictor.inverseInPlace(des, width, height,1,-1); + if(mode.isTL()) + Predictor.inverseInPlace(des, width, height,-1,-1); + if(mode.isV()) + Predictor.inverseInPlace(des, width, height,0,-1); + if(mode.isH()) + Predictor.inverseInPlace(des, width, height,-1,0); + if(mode.isCD()) + ColorTransform.colorInverseRGBAInPlace(des, width, height); + tdb.mark("预测解码"); + tdb.print(); + return new IntImageBlock(MemorySegment.ofArray(des), width, height, input.getPosx(), input.getPosy()); + } +} diff --git a/src/org/kne/codec/kif/KIFEncodePipeline.java b/src/org/kne/codec/kif/KIFEncodePipeline.java new file mode 100644 index 0000000..c1a4023 --- /dev/null +++ b/src/org/kne/codec/kif/KIFEncodePipeline.java @@ -0,0 +1,186 @@ +package org.kne.codec.kif; + +import jdk.internal.foreign.ArenaImpl; +import java.lang.foreign.Arena; +import java.lang.foreign.MemorySegment; +import java.lang.foreign.ValueLayout; +import java.lang.foreign.ValueLayout.OfInt; + +import org.kne.debug.TimeDebugger; +import org.kne.membandboost.MembandBoost; +import org.kne.opencl64.OpenCLKernel; +import org.kne.opencl64.OpenCLMemory; +import org.kne.opencl64.OpenCLProgram; +import org.kne.opencl64.concurrent.OpenCLContextThread; +import org.kne.opencl64.concurrent.ThreadLocalOpenCLMemory; + +public class KIFEncodePipeline { + + private volatile IntImageBlock input; + private volatile KIFCompressMode mode; + private volatile TimeDebugger tdb=new TimeDebugger("编码时间",false); + + + + public KIFEncodePipeline(IntImageBlock input, KIFCompressMode mode) { + super(); + this.input = input; + this.mode = mode; + pixels = input.getImage(); + width = input.getWidth(); + height = input.getHeight(); + } + + private byte[] serialData2; + private MemorySegment pixels; + private int width; + private int height; + + public void encodeStage1() { + Thread t =Thread. currentThread(); + if (t instanceof OpenCLContextThread) { + OpenCLContextThread oct = (OpenCLContextThread) t; + encodeStage1GPU(oct); + }else { + encodeStage1CPU(); + } + } + + private void encodeStage1CPU() { + int[]pixels2=pixels.toArray(OfInt.JAVA_INT); + if(mode.isCD()) { + pixels2=ColorTransform.colorTransformRGBA(pixels2, width, height); + }else { + pixels2=pixels2.clone(); + } + if(mode.isH()) + Predictor.predictInPlace(pixels2, width, height,-1,0); + if(mode.isV()) + Predictor.predictInPlace(pixels2, width, height,0,-1); + if(mode.isTL()) + Predictor.predictInPlace(pixels2, width, height,-1,-1); + if(mode.isTR()) + Predictor.predictInPlace(pixels2, width, height,1,-1); + tdb.mark("预测编码"); + + byte[] serialData ; + if(mode.isPL()) { + serialData= PixelSerializer.serializePlannarRGBAWithZigzag(pixels2, width, height); + }else { + serialData= PixelSerializer.serializeRGBAWithZigzag(pixels2, width, height); + } + tdb.mark("序列化"); + + if(mode.isBP()) { + serialData2= BitPlane.encodePacked(serialData); + }else { + serialData2=serialData; + } + tdb.mark("位平面编码"); + } + + + private static ThreadLocalm1=ThreadLocal.withInitial(()->{ + return ((ArenaImpl)Arena.ofAuto()).allocateNoInit(1024*1024*4,4); + }); + + private static ThreadLocalOpenCLMemory tlm0=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlmpred1=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlmpred2=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlm1=new ThreadLocalOpenCLMemory(1024*1024*4); + private static ThreadLocalOpenCLMemory tlm2=new ThreadLocalOpenCLMemory(1024*1024*4); + private void encodeStage1GPU(OpenCLContextThread oct) { + OpenCLProgram prog = oct.getProgram("/org/kne/codec/kif/KIFCodec.cl"); + tdb.mark("加载程序"); + //OpenCLKernel krl4 = prog.createKernel("colorInverseRGBAInPlace"); + OpenCLMemory ocm1 = null; + OpenCLMemory ocm2 = null; + OpenCLMemory ocm3 = null; + MemorySegment dataserpseg= m1.get(); + tdb.mark("堆外内存分配"); + //MemorySegment.copy(pixels,0,dataserpseg,0, pixels.byteSize()); + tdb.mark("堆内存->堆外内存"); + ocm1=tlm0.get(oct.getOpenCLContext()); + tdb.mark("分配显存"); + oct.getOpenCLCommandQueue().executeWriteBuffer(ocm1, pixels, false,0,0,pixels.byteSize()); + tdb.mark("内存->显存"); + + if(mode.isH()) { + OpenCLKernel krl3 = prog.createKernel("horizontalPredictParallel"); + OpenCLMemory opred1=tlmpred1.get(oct.getOpenCLContext()); + krl3.putArg(ocm1); + krl3.putArg(opred1); + krl3.putArg(width); + krl3.putArg(height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl3, 0, width*height); + ocm1=opred1; + } + + if(mode.isV()) { + OpenCLKernel krl3 = prog.createKernel("verticalPredictParallel"); + OpenCLMemory opred2=tlmpred2.get(oct.getOpenCLContext()); + krl3.putArg(ocm1); + krl3.putArg(opred2); + krl3.putArg(width); + krl3.putArg(height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl3, 0, width*height); + ocm1=opred2; + } + if(mode.isCD()) { + OpenCLKernel krl4 = prog.createKernel("colorTransformRGBAInPlace"); + krl4.putArg(ocm1); + krl4.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl4, 0, width*height); + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("预测解码"); + ocm2=tlm1.get(oct.getOpenCLContext()); + if(mode.isPL()) { + OpenCLKernel krl1 = prog.createKernel("serializePlannarRGBAWithZigzag"); + krl1.putArg(ocm1); + krl1.putArg(ocm2); + krl1.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl1, 0,width*height); + }else { + OpenCLKernel krl2 = prog.createKernel("serializeRGBAWithZigzag"); + krl2.putArg(ocm1); + krl2.putArg(ocm2); + krl2.putArg(width*height); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl2, 0,width*height); + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("反序列化"); + OpenCLMemory dataser; + if(mode.isBP()) { + + ocm3=tlm2.get(oct.getOpenCLContext()); + OpenCLKernel krl0 = prog.createKernel("encodePacked"); + krl0.putArg(ocm2); + krl0.putArg(ocm3); + krl0.putArg((int)pixels.byteSize()); + oct.getOpenCLCommandQueue().execute1DRangeKernel(krl0, 0,pixels.byteSize()); + dataser=ocm3;// + }else { + dataser=ocm2; + } + // oct.getOpenCLCommandQueue().finish(); + // tdb.mark("位平面解码"); + + tdb.mark("编码"); + serialData2=MembandBoost.allocateUninitializedByteArray(width*height*4); + tdb.mark("分配堆内存"); + + oct.getOpenCLCommandQueue().executeReadBuffer(dataser, dataserpseg, true,0,0,width*height*4); + tdb.mark("显存->内存"); + MemorySegment.copy(dataserpseg, ValueLayout.JAVA_BYTE, 0, serialData2, 0,width*height*4); + tdb.mark("堆外内存->堆内存"); + + } + public KIFCompressedImageBlock encodeStage2() { + int sizeBefore=serialData2.length; + byte[]writeData=Compressors.compressZstd(serialData2); + tdb.mark("熵编码"); + tdb.print(); + return new KIFCompressedImageBlock(writeData,sizeBefore,width,height,input.getPosx(),input.getPosy(),mode); + } +} diff --git a/src/org/kne/codec/kif/KIFImageReader.java b/src/org/kne/codec/kif/KIFImageReader.java new file mode 100644 index 0000000..4a7a113 --- /dev/null +++ b/src/org/kne/codec/kif/KIFImageReader.java @@ -0,0 +1,205 @@ +package org.kne.codec.kif; + +import javax.imageio.ImageReadParam; +import javax.imageio.ImageReader; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; + +import org.kne.debug.TimeDebugger; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +/** + * KIF格式的实际解码器。 + * 这个类负责读取.kif文件并解码成BufferedImage。 + */ +public class KIFImageReader extends ImageReader { + + private ImageInputStream inputStream; + private int width = -1; + private int height = -1; + + protected KIFImageReader(ImageReaderSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public void setInput(Object input, boolean seekForwardOnly, boolean ignoreMetadata) { + super.setInput(input, seekForwardOnly, ignoreMetadata); + if (input instanceof ImageInputStream) { + this.inputStream = (ImageInputStream) input; + } else { + this.inputStream = null; + } + // 重置状态 + this.width = -1; + this.height = -1; + } + + /** + * 读取文件头,获取图像尺寸等元数据 + */ + private void readHeader() throws IOException { + if (inputStream == null) { + throw new IllegalStateException("Input not set"); + } + + + // 文件头结构(你需要根据实际格式调整): + // Magic: 3 bytes "KIF" + // Version: 1 byte + // Width: 4 bytes (int) + // Height: 4 bytes (int) + // 更多元数据... + + // 校验魔数 (已由Spi验证,但外部直接调用时仍需防御) + byte[] magic = new byte[3]; + inputStream.readFully(magic); + if (magic[0] != 0x4B || magic[1] != 0x49 || magic[2] != 0x46) { + throw new IOException("无效的 KIF 文件格式: 魔数不匹配 (期望 KIF, 实际 " + + String.format("%02X %02X %02X", magic[0], magic[1], magic[2]) + ")"); + } + + // 读取版本 + byte version = inputStream.readByte(); + + // 读取类型 + byte type = inputStream.readByte(); + + // 读取宽高 (假设为大端序) + this.width = inputStream.readInt(); + this.height = inputStream.readInt(); + + // 这里可以读取更多元数据,例如预测模式、压缩方式等 + + } + + @Override + public int getWidth(int imageIndex) throws IOException { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("Only image index 0 is supported"); + } + readHeader(); + return width; + } + + @Override + public int getHeight(int imageIndex) throws IOException { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("Only image index 0 is supported"); + } + readHeader(); + return height; + } + + @Override + public Iterator getImageTypes(int imageIndex) throws IOException { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("Only image index 0 is supported"); + } + readHeader(); + + // 返回支持的图像类型(这里只支持标准的RGB或灰度) + List types = new ArrayList<>(); + + // 支持 RGB (8-bit interleaved) + types.add(ImageTypeSpecifier.createInterleaved( + java.awt.color.ColorSpace.getInstance(java.awt.color.ColorSpace.CS_sRGB), + new int[]{0, 1, 2}, // band offsets: R, G, B + 0, // data type: TYPE_BYTE + false, // hasAlpha + false // isAlphaPremultiplied + )); + + // 支持灰度 + types.add(ImageTypeSpecifier.createGrayscale(8, 0, false)); + + return types.iterator(); + } + + @Override + public BufferedImage read(int imageIndex, ImageReadParam param) throws IOException { + if (imageIndex != 0) { + throw new IndexOutOfBoundsException("Only image index 0 is supported"); + } + TimeDebugger tdb=new TimeDebugger("读取时间",false); + readHeader(); + + tdb.mark("读取文件头"); + AtomicReferencearray=new AtomicReference<>(); + int blockcount=inputStream.readInt(); + ArrayList>decodeing=new ArrayList<>(blockcount); + KIFCodec.backPressure(); + for(int i=0;i{ + KIFCompressedImageBlock kip; + synchronized (inputStream) { + try { + kip=KIFCompressedImageBlock.readFromStream(inputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return kip; + },(out)->{ + while(array.get()==null) { + Thread.yield(); + } + out.toImage(MemorySegment.ofArray( array.get()), width, height); + })); + } + + tdb.mark("读取文件并提交解码"); + + + BufferedImage image =BufferedImageMembandBoost.createUninitializedBufferedImage(width, height); + int[] dest = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + array.set(dest); + tdb.mark("分配输出图像内存"); + for(Future compressed:decodeing) { + try { + compressed.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + ExceptionTool.throwIOException(e); + } + } + + tdb.mark("解码/拼接"); + tdb.print(); + return image; + } + + @Override + public IIOMetadata getStreamMetadata() throws IOException { + return null; // 本实现不提供流元数据 + } + + @Override + public IIOMetadata getImageMetadata(int imageIndex) throws IOException { + return null; // 本实现不提供图像元数据 + } + + @Override + public int getNumImages(boolean allowSearch) throws IOException { + return 1; // KIF格式只包含单张图像(文件版本) + } + + @Override + public void dispose() { + + super.dispose(); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFImageReaderSpi.java b/src/org/kne/codec/kif/KIFImageReaderSpi.java new file mode 100644 index 0000000..a08d8b7 --- /dev/null +++ b/src/org/kne/codec/kif/KIFImageReaderSpi.java @@ -0,0 +1,130 @@ +package org.kne.codec.kif; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.stream.ImageInputStream; +import java.io.IOException; +import java.util.Locale; + +/** + * KIF格式的ImageIO服务提供者。 + * 这个类负责告诉ImageIO:我能处理.kif文件,并且我能创建对应的ImageReader。 + */ +public class KIFImageReaderSpi extends ImageReaderSpi { + + // 格式标识信息 + private static final String VENDOR_NAME = "KIFCodec Project"; + private static final String VERSION = "1.0"; + private static final String[] FORMAT_NAMES = {"kif", "KIF"}; + private static final String[] SUFFIXES = {"kif"}; + private static final String[] MIME_TYPES = {"image/kif"}; + private static final String READER_CLASS_NAME = KIFImageReader.class.getName(); + + // 标记是否支持流式读取(不支持,因为需要随机访问) + private static final boolean SUPPORTS_STANDARD_STREAM_METADATA = false; + private static final boolean SUPPORTS_STANDARD_IMAGE_METADATA = false; + + // 单例实例,避免重复注册 + private static KIFImageReaderSpi instance; + + public KIFImageReaderSpi() { + super( + VENDOR_NAME, // vendorName + VERSION, // version + FORMAT_NAMES, // names + SUFFIXES, // suffixes + MIME_TYPES, // MIMETypes + READER_CLASS_NAME, // readerClassName + new Class[]{ImageInputStream.class}, // inputTypes + null, // writerSpiNames + SUPPORTS_STANDARD_STREAM_METADATA, + null, // nativeStreamMetadataFormatName + null, // nativeStreamMetadataFormatClassName + null, // extraStreamMetadataFormatNames + null, // extraStreamMetadataFormatClassNames + SUPPORTS_STANDARD_IMAGE_METADATA, + null, // nativeImageMetadataFormatName + null, // nativeImageMetadataFormatClassName + null, // extraImageMetadataFormatNames + null // extraImageMetadataFormatClassNames + ); + } + + /** + * 手动注册此插件到 ImageIO 的全局注册表。 + * 调用后,ImageIO 就能识别 .kif 格式。 + * + *

使用示例: + *

+     * // 在应用启动时调用一次
+     * KIFImageReaderSpi.register();
+     * 
+     * // 之后就可以正常使用 ImageIO 了
+     * BufferedImage img = ImageIO.read(new File("test.kif"));
+     * 
+ * + * @return true 如果注册成功,false 如果已经注册过 + */ + public static void register() { + IIORegistry registry = IIORegistry.getDefaultInstance(); + + // 创建实例并注册 + if (instance == null) { + instance = new KIFImageReaderSpi(); + registry.registerServiceProvider(instance); + } + } + + /** + * 从 ImageIO 的全局注册表中注销此插件。 + * + */ + public static void unregister() { + IIORegistry registry = IIORegistry.getDefaultInstance(); + + if (instance != null) { + registry.deregisterServiceProvider(instance); + + } + } + + + + @Override + public boolean canDecodeInput(Object source) throws IOException { + if (!(source instanceof ImageInputStream)) { + return false; + } + + ImageInputStream stream = (ImageInputStream) source; + // 保存当前位置 + long pos = stream.getStreamPosition(); + try { + // 读取文件头魔数(Magic Number) + byte[] magic = new byte[3]; + stream.readFully(magic); + + // 检查是否为 "KIF" (0x4B 0x49 0x46) + if (magic[0] == 0x4B && magic[1] == 0x49 && magic[2] == 0x46) { + return true; + } + + return false; + } finally { + // 恢复位置 + stream.seek(pos); + } + } + + @Override + public ImageReader createReaderInstance(Object extension) throws IOException { + return new KIFImageReader(this); + } + + @Override + public String getDescription(Locale locale) { + return "KIF (KIFCodec Image Format) Image Reader"; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFImageWriter.java b/src/org/kne/codec/kif/KIFImageWriter.java new file mode 100644 index 0000000..72704b6 --- /dev/null +++ b/src/org/kne/codec/kif/KIFImageWriter.java @@ -0,0 +1,231 @@ +package org.kne.codec.kif; + +import javax.imageio.IIOImage; +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.metadata.IIOMetadata; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; + +import org.kne.debug.TimeDebugger; + +import java.awt.AlphaComposite; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Image; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.lang.foreign.MemorySegment; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Supplier; +import java.util.zip.Deflater; + +/** + * KIF格式的实际编码器。 + * 这个类负责将BufferedImage编码为.kif文件。 + */ +public class KIFImageWriter extends ImageWriter { + private static final int BLOCK_SIZE=512; + private ImageOutputStream outputStream; + private static ModeStatisticsCollector modeCollector=new ModeStatisticsCollector(); + + public static ModeStatisticsCollector getModeCollector() { + return modeCollector; + } + + protected KIFImageWriter(ImageWriterSpi originatingProvider) { + super(originatingProvider); + } + + @Override + public void setOutput(Object output) { + super.setOutput(output); + if (output instanceof ImageOutputStream) { + this.outputStream = (ImageOutputStream) output; + } else { + this.outputStream = null; + } + } + + @Override + public IIOMetadata getDefaultStreamMetadata(ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata getDefaultImageMetadata(ImageTypeSpecifier imageType, ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata convertStreamMetadata(IIOMetadata inData, ImageWriteParam param) { + return null; + } + + @Override + public IIOMetadata convertImageMetadata(IIOMetadata inData, ImageTypeSpecifier imageType, ImageWriteParam param) { + return null; + } + + @Override + public void write(IIOMetadata streamMetadata, IIOImage image, ImageWriteParam param) throws IOException { + if (outputStream == null) { + throw new IllegalStateException("Output not set"); + } + + // 检查是否支持此图像类型 + if (image.getRenderedImage() == null) { + throw new IllegalArgumentException("Image contains no RenderedImage"); + } + + RenderedImage renderedImage = image.getRenderedImage(); + int width = renderedImage.getWidth(); + int height = renderedImage.getHeight(); + + + TimeDebugger tdb=new TimeDebugger("写入时间",false); + // 方法1:如果 RenderedImage 是 BufferedImage,可以直接操作 + BufferedImage bi = null; + if (renderedImage instanceof BufferedImage) { + bi = (BufferedImage) renderedImage; + } else { + // 如果不是 BufferedImage,创建一个副本(简化处理) + bi = BufferedImageMembandBoost.createUninitializedBufferedImage(width, height) ; + Graphics2D g= (Graphics2D) bi.getGraphics(); + g.setComposite(AlphaComposite.Src); + g .drawImage((Image) renderedImage, 0, 0, null); + } + /* + // 在 KIFImageWriter.write() 中 + BufferedImage original = (BufferedImage) image.getRenderedImage(); + + // 统一转换为 TYPE_INT_ARGB + BufferedImage rgbImage =null; + if(original.getType()!=BufferedImage.TYPE_INT_ARGB) { + rgbImage = BufferedImageMembandBoost.createUninitializedBufferedImage( original.getWidth(), original.getHeight() ); + Graphics2D g = rgbImage.createGraphics(); + g.setComposite(AlphaComposite.Src); + g.drawImage(original, 0, 0, null); + g.dispose(); + }else { + rgbImage=original; + }*/ + tdb.mark("格式转换"); + List>encoding=new ArrayList<>(); + Listbcd=BlockCoord.calculateBlockCoords(width,height, BLOCK_SIZE, BLOCK_SIZE); + KIFCodec.backPressure(); + for(BlockCoord crd:bcd) { + final BufferedImage bi2=bi; + encoding.add( KIFCodec.encodeAsyncMultimode(()->{ + return IntImageBlock.createBlock(bi2,crd); + },KIFCompressMode.getNaturalImageModes())); + } + tdb.mark("分块提交"); + + + // ===== 写入文件头 ===== + // 魔数 (3 bytes) "KIF" + outputStream.write(0x4B); // 'K' + outputStream.write(0x49); // 'I' + outputStream.write(0x46); // 'F' + + // 版本号 (1 byte) + outputStream.writeByte(1); + + // 图像类型 (1 byte): 0=灰度, 1=RGB, 2=RGBA + int imageType = bi.getType(); + // System.out.println("类型:"+imageType); + int typeFlag = 0; + if (imageType == BufferedImage.TYPE_INT_RGB || + imageType == BufferedImage.TYPE_3BYTE_BGR) { + typeFlag = 1; + } else if (imageType == BufferedImage.TYPE_INT_ARGB || + imageType == BufferedImage.TYPE_4BYTE_ABGR) { + typeFlag = 2; + } + outputStream.writeByte(typeFlag); + + // 宽度 (4 bytes, 大端序) + outputStream.writeInt(width); + + // 高度 (4 bytes, 大端序) + outputStream.writeInt(height); + + // ===== 写入压缩的图像数据 ===== + + outputStream.writeInt(encoding.size()); + tdb.mark("写入头"); + while(true) { + for (Iterator> iterator = encoding.iterator(); iterator.hasNext();) { + Future v = iterator.next(); + try { + if(v.isDone()) { + KIFCompressedImageBlock blk=v.get(); + blk.writeToStream(outputStream); + modeCollector.addRecord(blk.getPredictMode()); + iterator.remove(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + ExceptionTool.throwIOException(e); + } + } + if(encoding.isEmpty()) { + break; + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + tdb.mark("等待编码完成"); + // 注意:写入完成后记得 flush + outputStream.flush(); + tdb.mark("写入文件"); + tdb.print(); + } + + /** + * 将 BufferedImage 转换为平面像素数组(逐行,每个通道独立) + * 这是一个辅助方法,实际实现时可根据需要调整 + */ + private byte[][] getPixelBands(BufferedImage image) { + int width = image.getWidth(); + int height = image.getHeight(); + int bands = image.getSampleModel().getNumBands(); + + byte[][] bandData = new byte[bands][width * height]; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int idx = y * width + x; + int rgb = image.getRGB(x, y); + // 根据图像类型提取各个通道 + // 简化:假设为 RGB + if (bands >= 3) { + bandData[0][idx] = (byte) ((rgb >> 16) & 0xFF); // R + bandData[1][idx] = (byte) ((rgb >> 8) & 0xFF); // G + bandData[2][idx] = (byte) (rgb & 0xFF); // B + } else if (bands == 1) { + bandData[0][idx] = (byte) (rgb & 0xFF); // 灰度 + } + } + } + return bandData; + } + + @Override + public void dispose() { + super.dispose(); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/KIFImageWriterSpi.java b/src/org/kne/codec/kif/KIFImageWriterSpi.java new file mode 100644 index 0000000..8b87fc5 --- /dev/null +++ b/src/org/kne/codec/kif/KIFImageWriterSpi.java @@ -0,0 +1,94 @@ +package org.kne.codec.kif; + +import javax.imageio.ImageTypeSpecifier; +import javax.imageio.ImageWriter; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageWriterSpi; +import javax.imageio.stream.ImageOutputStream; +import java.awt.image.RenderedImage; +import java.io.IOException; +import java.util.Locale; + +/** + * KIF格式的ImageIO服务提供者(Writer端)。 + * 这个类负责告诉ImageIO:我能写入.kif格式,并且我能创建对应的ImageWriter。 + */ +public class KIFImageWriterSpi extends ImageWriterSpi { + + private static final String VENDOR_NAME = "KIFCodec Project"; + private static final String VERSION = "1.0"; + private static final String[] FORMAT_NAMES = {"kif", "KIF"}; + private static final String[] SUFFIXES = {"kif"}; + private static final String[] MIME_TYPES = {"image/kif"}; + private static final String WRITER_CLASS_NAME = KIFImageWriter.class.getName(); + + private static final boolean SUPPORTS_STANDARD_STREAM_METADATA = false; + private static final boolean SUPPORTS_STANDARD_IMAGE_METADATA = false; + + // 单例 + private static KIFImageWriterSpi instance; + + public KIFImageWriterSpi() { + super( + VENDOR_NAME, + VERSION, + FORMAT_NAMES, + SUFFIXES, + MIME_TYPES, + WRITER_CLASS_NAME, + new Class[]{ImageOutputStream.class}, + new String[]{KIFImageReaderSpi.class.getName()}, // readerSpiNames + SUPPORTS_STANDARD_STREAM_METADATA, + null, // nativeStreamMetadataFormatName + null, // nativeStreamMetadataFormatClassName + null, // extraStreamMetadataFormatNames + null, // extraStreamMetadataFormatClassNames + SUPPORTS_STANDARD_IMAGE_METADATA, + null, // nativeImageMetadataFormatName + null, // nativeImageMetadataFormatClassName + null, // extraImageMetadataFormatNames + null // extraImageMetadataFormatClassNames + ); + } + + // ========== 手动注册方法 ========== + + public static void register() { + IIORegistry registry = IIORegistry.getDefaultInstance(); + + if (instance == null) { + instance = new KIFImageWriterSpi(); + registry.registerServiceProvider(instance); + } + } + + public static void unregister() { + IIORegistry registry = IIORegistry.getDefaultInstance(); + if (instance != null) { + registry.deregisterServiceProvider(instance); + instance=null; + } + } + + + // ========== SPI 必须实现的方法 ========== + + @Override + public boolean canEncodeImage(ImageTypeSpecifier type) { + // 检查是否支持此图像类型 + // 简化版:支持大部分常见类型 + int sampleSize = type.getSampleModel().getSampleSize(0); + // 支持 8-bit 灰度或 RGB + return sampleSize == 8; + } + + @Override + public ImageWriter createWriterInstance(Object extension) throws IOException { + return new KIFImageWriter(this); + } + + @Override + public String getDescription(Locale locale) { + return "KIF (KIFCodec Image Format) Image Writer"; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/ModeStatisticsCollector.java b/src/org/kne/codec/kif/ModeStatisticsCollector.java new file mode 100644 index 0000000..8ebf11f --- /dev/null +++ b/src/org/kne/codec/kif/ModeStatisticsCollector.java @@ -0,0 +1,35 @@ +package org.kne.codec.kif; + +/** + * KIF 压缩模式统计收集器(线程安全) + * 专门统计 KIFCompressMode 的出现次数与概率 + */ +public class ModeStatisticsCollector extends StatisticsCollector { + + private static final ModeFormatter FORMATTER = new ModeFormatter(); + + /** + * 生成表格形式的统计报告 + */ + @Override + public String toString() { + return formatTable(FORMATTER); + } + + /** + * 生成 CSV 格式报告 + */ + public String toCSV() { + return super.toCSV(FORMATTER); + } + + /** + * KIFCompressMode 格式化器 + */ + private static class ModeFormatter implements ItemFormatter { + @Override + public String format(KIFCompressMode mode) { + return mode.toString(); + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/MultiModeFuture.java b/src/org/kne/codec/kif/MultiModeFuture.java new file mode 100644 index 0000000..f6e2bea --- /dev/null +++ b/src/org/kne/codec/kif/MultiModeFuture.java @@ -0,0 +1,247 @@ +package org.kne.codec.kif; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * 多模式并行编码的 Future 包装器 + * 等待所有子任务完成,选择体积最小的结果 + */ +public class MultiModeFuture implements Future { + + private final ArrayList> futures; + private volatile KIFCompressedImageBlock bestResult; + private volatile boolean done = false; + private volatile boolean cancelled = false; + private final Object lock = new Object(); + + public MultiModeFuture(ArrayList< Future> futures) { + if (futures == null || futures.size() == 0) { + throw new IllegalArgumentException("至少需要1个子任务"); + } + this.futures = futures; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (isDone()) { + return false; + } + cancelled = true; + boolean allCancelled = true; + for (Future f : futures) { + if (!f.cancel(mayInterruptIfRunning)) { + allCancelled = false; + } + } + return allCancelled; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public boolean isDone() { + // 如果已经标记完成,直接返回 true + if (done) { + return true; + } + + // 检查是否所有子任务都已完成 + boolean allDone = true; + for (Future f : futures) { + if (!f.isDone()) { + allDone = false; + break; + } + } + + if (allDone) { + // 所有子任务完成,自动合并结果(不阻塞) + synchronized (lock) { + if (!done) { + // 如果之前没有合并过,现在合并 + KIFCompressedImageBlock minBlock = null; + Exception lastException = null; + + for (Future f : futures) { + try { + KIFCompressedImageBlock block = f.get(); // 此时不会阻塞,因为 isDone() 已为 true + if (block != null) { + if (minBlock == null || block.compareTo(minBlock) < 0) { + minBlock = block; + } + } + } catch (Exception e) { + lastException = e; + } + } + + if (minBlock != null||(lastException!=null)) { + bestResult = minBlock; + done = true; + } + } + } + } + + return done; + } + + @Override + public KIFCompressedImageBlock get() throws InterruptedException, ExecutionException { + if (done && bestResult != null) { + return bestResult; + } + + synchronized (lock) { + if (done && bestResult != null) { + return bestResult; + } + + // 等待所有子任务完成 + Exception lastException = null; + KIFCompressedImageBlock minBlock = null; + + for (Future f : futures) { + try { + KIFCompressedImageBlock block = f.get(); + if (block != null) { + if (minBlock == null || block.compareTo(minBlock) < 0) { + minBlock = block; + } + } + } catch (ExecutionException e) { + lastException = e; + // 继续收集其他任务的结果 + } + } + + // 如果有至少一个成功的结果,返回最小的 + if (minBlock != null) { + bestResult = minBlock; + done = true; + return bestResult; + } + + // 全部失败,抛出最后一个异常 + if (lastException != null) { + throw new ExecutionException(lastException); + } + + // 理论上不会走到这里 + throw new ExecutionException(new IllegalStateException("所有子任务返回了 null")); + } + } + + @Override + public KIFCompressedImageBlock get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + if (done && bestResult != null) { + return bestResult; + } + + synchronized (lock) { + if (done && bestResult != null) { + return bestResult; + } + + long deadline = System.nanoTime() + unit.toNanos(timeout); + Exception lastException = null; + KIFCompressedImageBlock minBlock = null; + + for (Future f : futures) { + long remaining = deadline - System.nanoTime(); + if (remaining <= 0) { + throw new TimeoutException("等待超时"); + } + + try { + KIFCompressedImageBlock block = f.get(remaining, TimeUnit.NANOSECONDS); + if (block != null) { + if (minBlock == null || block.compareTo(minBlock) < 0) { + minBlock = block; + } + } + } catch (ExecutionException e) { + lastException = e; + } + } + + if (minBlock != null) { + bestResult = minBlock; + done = true; + return bestResult; + } + + if (lastException != null) { + throw new ExecutionException(lastException); + } + + throw new ExecutionException(new IllegalStateException("所有子任务返回了 null")); + } + } + /* @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("MultiModeFuture{\n"); + + // 获取各个子任务的模式(如果能取到的话) + // 由于 Future 可能还没完成,我们只能尽量获取已完成的信息 + sb.append(" 子任务数: ").append(futures.length).append("\n"); + + // 尝试获取每个子任务的结果(如果已完成) + for (int i = 0; i < futures.length; i++) { + Future f = futures[i]; + if (f.isDone()) { + try { + // 如果已经完成,可以安全获取 + KIFCompressedImageBlock block = f.get(); + if (block != null) { + sb.append(String.format(" 模式 %d: size=%d bytes, 压缩率=%.2f%%\n", + block.getPredictMode() & 0xFF, + block.getData().length, + (double) block.getData().length / block.getSizeBeforeCompress() * 100 + )); + } else { + sb.append(" 模式 ").append(i).append(": null\n"); + } + } catch (Exception e) { + sb.append(" 模式 ").append(i).append(": 获取失败 (").append(e.getMessage()).append(")\n"); + } + } else { + // 如果还没完成,尝试获取模式(需要额外传递模式信息) + // 这里无法直接从 Future 中提取模式,因为 Future 只存结果 + // 可以在调用时传入模式编号或使用自定义 Future 子类携带模式信息 + sb.append(" 子任务 ").append(i).append(": 未完成\n"); + } + } + + // 如果有最终结果,显示 + if (done && bestResult != null) { + sb.append(" 最终选择:\n"); + sb.append(String.format(" 模式: %d\n", bestResult.getPredictMode() & 0xFF)); + sb.append(String.format(" 体积: %d bytes\n", bestResult.getData().length)); + sb.append(String.format(" 压缩率: %.2f%%\n", + (double) bestResult.getData().length / bestResult.getSizeBeforeCompress() * 100 + )); + sb.append(" 宽x高: ").append(bestResult.getWidth()).append("x").append(bestResult.getHeight()); + } else if (done && bestResult == null) { + sb.append(" 最终结果: null (所有子任务失败)\n"); + } else { + sb.append(" 状态: 未完成,仍在等待子任务...\n"); + } + + sb.append("}"); + return sb.toString(); + }*/ + @Override + public String toString() { + return bestResult.toString(); + } + +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/PixelSerializer.java b/src/org/kne/codec/kif/PixelSerializer.java new file mode 100644 index 0000000..439c0ff --- /dev/null +++ b/src/org/kne/codec/kif/PixelSerializer.java @@ -0,0 +1,256 @@ +package org.kne.codec.kif; + +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import org.kne.membandboost.MembandBoost; + +/** + * int[] 像素数组与 byte[] 的相互转换工具 + * 每个像素按 R, G, B 三个字节依次写入 + */ +public class PixelSerializer { + + // ==================== 基础序列化/反序列化 (RGB, 3通道) ==================== + + public static byte[] serialize(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 3); + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + int idx = i * 3; + result[idx] = (byte) ((pixel >> 16) & 0xFF); + result[idx + 1] = (byte) ((pixel >> 8) & 0xFF); + result[idx + 2] = (byte) (pixel & 0xFF); + } + return result; + } + + public static int[] deserialize(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 3; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int[] pixels = MembandBoost.allocateUninitializedIntArray(w * h); + for (int i = 0; i < pixels.length; i++) { + int idx = i * 3; + int r = data[idx] & 0xFF; + int g = data[idx + 1] & 0xFF; + int b = data[idx + 2] & 0xFF; + pixels[i] = (r << 16) | (g << 8) | b; + } + return pixels; + } + + // ==================== 合并 Zigzag (RGB, 3通道) ==================== + + public static byte[] serializeWithZigzag(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 3); + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + byte r = (byte) ((pixel >> 16) & 0xFF); + byte g = (byte) ((pixel >> 8) & 0xFF); + byte b = (byte) (pixel & 0xFF); + byte mr = Zigzag.map(r); + byte mg = Zigzag.map(g); + byte mb = Zigzag.map(b); + int idx = i * 3; + result[idx] = mr; + result[idx + 1] = mg; + result[idx + 2] = mb; + } + return result; + } + + public static int[] deserializeWithZigzag(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 3; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int[] pixels = MembandBoost.allocateUninitializedIntArray(w * h); + for (int i = 0; i < pixels.length; i++) { + int idx = i * 3; + byte r = Zigzag.unmap(data[idx]); + byte g = Zigzag.unmap(data[idx + 1]); + byte b = Zigzag.unmap(data[idx + 2]); + pixels[i] = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return pixels; + } + + // ==================== 平面序列化 (RGB, 3通道) ==================== + + public static byte[] serializePlannarWithZigzag(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 3); + int rOffset = 0; + int gOffset = len; + int bOffset = len * 2; + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + byte r = (byte) ((pixel >> 16) & 0xFF); + byte g = (byte) ((pixel >> 8) & 0xFF); + byte b = (byte) (pixel & 0xFF); + result[rOffset + i] = Zigzag.map(r); + result[gOffset + i] = Zigzag.map(g); + result[bOffset + i] = Zigzag.map(b); + } + return result; + } + + public static int[] deserializePlannarWithZigzag(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 3; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int len = w * h; + int[] pixels = MembandBoost.allocateUninitializedIntArray(len); + int rOffset = 0; + int gOffset = len; + int bOffset = len * 2; + for (int i = 0; i < len; i++) { + byte r = Zigzag.unmap(data[rOffset + i]); + byte g = Zigzag.unmap(data[gOffset + i]); + byte b = Zigzag.unmap(data[bOffset + i]); + pixels[i] = ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return pixels; + } + + // ==================== RGBA 基础序列化 (4通道) ==================== + + /** + * RGBA 序列化:每个像素 [R, G, B, A] 四个字节(交错存储) + */ + public static byte[] serializeRGBA(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 4); + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + int idx = i * 4; + result[idx] = (byte) ((pixel >> 16) & 0xFF); + result[idx + 1] = (byte) ((pixel >> 8) & 0xFF); + result[idx + 2] = (byte) (pixel & 0xFF); + result[idx + 3] = (byte) ((pixel >> 24) & 0xFF); + } + return result; + } + + /** + * RGBA 反序列化 + */ + public static int[] deserializeRGBA(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 4; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int[] pixels = MembandBoost.allocateUninitializedIntArray(w * h); + for (int i = 0; i < pixels.length; i++) { + int idx = i * 4; + int r = data[idx] & 0xFF; + int g = data[idx + 1] & 0xFF; + int b = data[idx + 2] & 0xFF; + int a = data[idx + 3] & 0xFF; + pixels[i] = (a << 24) | (r << 16) | (g << 8) | b; + } + return pixels; + } + + // ==================== RGBA + Zigzag (交错) ==================== + + public static byte[] serializeRGBAWithZigzag(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 4); + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + byte r = (byte) ((pixel >> 16) & 0xFF); + byte g = (byte) ((pixel >> 8) & 0xFF); + byte b = (byte) (pixel & 0xFF); + byte a = (byte) ((pixel >> 24) & 0xFF); + byte mr = Zigzag.map(r); + byte mg = Zigzag.map(g); + byte mb = Zigzag.map(b); + byte ma = Zigzag.map(a); // Alpha 也做 Zigzag + int idx = i * 4; + result[idx] = mr; + result[idx + 1] = mg; + result[idx + 2] = mb; + result[idx + 3] = ma; + } + return result; + } + + public static int[] deserializeRGBAWithZigzag(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 4; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int[] pixels = MembandBoost.allocateUninitializedIntArray(w * h); + for (int i = 0; i < pixels.length; i++) { + int idx = i * 4; + byte r = Zigzag.unmap(data[idx]); + byte g = Zigzag.unmap(data[idx + 1]); + byte b = Zigzag.unmap(data[idx + 2]); + byte a = Zigzag.unmap(data[idx + 3]); + pixels[i] = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return pixels; + } + + // ==================== RGBA 平面序列化 (RRRR...GGGG...BBBB...AAAA...) ==================== + + public static byte[] serializePlannarRGBAWithZigzag(int[] pixels, int w, int h) { + int len = pixels.length; + byte[] result = MembandBoost.allocateUninitializedByteArray(len * 4); + int rOffset = 0; + int gOffset = len; + int bOffset = len * 2; + int aOffset = len * 3; + for (int i = 0; i < len; i++) { + int pixel = pixels[i]; + byte r = (byte) ((pixel >> 16) & 0xFF); + byte g = (byte) ((pixel >> 8) & 0xFF); + byte b = (byte) (pixel & 0xFF); + byte a = (byte) ((pixel >> 24) & 0xFF); + result[rOffset + i] = Zigzag.map(r); + result[gOffset + i] = Zigzag.map(g); + result[bOffset + i] = Zigzag.map(b); + result[aOffset + i] = Zigzag.map(a); // Alpha 单独一个平面 + } + return result; + } + + public static int[] deserializePlannarRGBAWithZigzag(byte[] data, int w, int h) throws IOException { + int expectedLen = w * h * 4; + if (data.length != expectedLen) { + throw new IOException("数据长度不匹配: 期望 " + expectedLen + " 字节, 实际 " + data.length + " 字节"); + } + int len = w * h; + int[] pixels = MembandBoost.allocateUninitializedIntArray(len); + int rOffset = 0; + int gOffset = len; + int bOffset = len * 2; + int aOffset = len * 3; + for (int i = 0; i < len; i++) { + byte r = Zigzag.unmap(data[rOffset + i]); + byte g = Zigzag.unmap(data[gOffset + i]); + byte b = Zigzag.unmap(data[bOffset + i]); + byte a = Zigzag.unmap(data[aOffset + i]); + pixels[i] = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF); + } + return pixels; + } + + // ==================== 工具方法 ==================== + + public static BufferedImage pixelsToBufferedImage(int[] pixels, int w, int h) { + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); + int[] data = ((DataBufferInt) image.getRaster().getDataBuffer()).getData(); + System.arraycopy(pixels, 0, data, 0, pixels.length); + return image; + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/PluginTest.java b/src/org/kne/codec/kif/PluginTest.java new file mode 100644 index 0000000..5059972 --- /dev/null +++ b/src/org/kne/codec/kif/PluginTest.java @@ -0,0 +1,39 @@ +package org.kne.codec.kif; + +import javax.imageio.ImageIO; +import javax.imageio.spi.IIORegistry; +import javax.imageio.spi.ImageReaderSpi; +import javax.imageio.spi.ImageWriterSpi; + +import java.util.Arrays; +import java.util.Iterator; + +public class PluginTest { + public static void main(String[] args) { + KIFImageReaderSpi.register(); + KIFImageWriterSpi.register(); + + IIORegistry registry = IIORegistry.getDefaultInstance(); + // 获取所有已注册的 ImageReaderSpi + Iterator readerSPIs = registry.getServiceProviders(ImageReaderSpi.class, true); + + System.out.println("=== 已注册的图像读取器插件 ==="); + for (Iterator iterator = readerSPIs; iterator.hasNext();) { + ImageReaderSpi spi = (ImageReaderSpi) iterator.next(); + + System.out.println("格式: " + Arrays.toString(spi.getFormatNames()) + + " | 后缀: " + Arrays.toString(spi.getFileSuffixes())); + } + // 获取所有已注册的 ImageReaderSpi + Iterator readerSPIsx = registry.getServiceProviders(ImageWriterSpi.class, true); + + System.out.println("=== 已注册的图像写入器插件 ==="); + for (Iterator iterator = readerSPIsx; iterator.hasNext();) { + ImageWriterSpi spi = (ImageWriterSpi) iterator.next(); + + System.out.println("格式: " + Arrays.toString(spi.getFormatNames()) + + " | 后缀: " + Arrays.toString(spi.getFileSuffixes())); + } + + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/Predictor.java b/src/org/kne/codec/kif/Predictor.java new file mode 100644 index 0000000..1fecb76 --- /dev/null +++ b/src/org/kne/codec/kif/Predictor.java @@ -0,0 +1,157 @@ +package org.kne.codec.kif; + +import org.kne.membandboost.MembandBoost; + +public class Predictor { + public static int[] predict(int[] data, int w, int h,int vx,int vy) { + int[] result=MembandBoost.allocateUninitializedIntArray(data.length); + if(vy==0) { + horizontalPredict0(data, result, w, h, vx, vy); + }else { + verticalPredict0(data, result, w, h, vx, vy); + } + return result; + } + public static int[] inverse(int[] data, int w, int h,int vx,int vy) { + int[] result=MembandBoost.allocateUninitializedIntArray( data.length); + if(vy==0) { + horizontalInverse0(data, result, w, h, vx, vy); + }else { + verticalInverse0(data, result, w, h, vx, vy); + } + return result; + } + public static void predictInPlace(int[] data, int w, int h,int vx,int vy) { + if(vy==0) { + horizontalPredict0(data, data, w, h, vx, vy); + }else { + verticalPredict0(data, data, w, h, vx, vy); + } + } + public static void inverseInPlace(int[] data, int w, int h,int vx,int vy) { + if(vy==0) { + horizontalInverse0(data, data, w, h, vx, vy); + }else { + verticalInverse0(data, data, w, h, vx, vy); + } + } + + + private static void horizontalPredict0(int[] datain,int[] datadout, int w, int h,int vx,int vy) { + for (int y = 0; y < h; y++) { + // 从右往左!因为左边是原始值,右边要用左边的原始值 + for (int x = w - 1; x >= 0; x--) { + int idx = y * w + x; + int px=x+vx; + int py=y+vy; + int pred=0; + if(px>=0&&px=0&&py=0&&px=0&&py= 0; y--) { + for (int x = 0; x < w; x++) { + int idx = y * w + x; + int px=x+vx; + int py=y+vy; + int pred=0; + if(px>=0&&px=0&&py=0&&px=0&&py> 16) & 0xFF; + int ag = (a >> 8) & 0xFF; + int ab = a & 0xFF; + int aa = (a >> 24) & 0xFF; + + int br = (b >> 16) & 0xFF; + int bg = (b >> 8) & 0xFF; + int bb = b & 0xFF; + int ba = (b >> 24) & 0xFF; + + int r = ar + br; + int g = ag + bg; + int bv = ab + bb; + int av = aa + ba; // Alpha 通道也要处理! + + // 用 &0xFF 替代 min/max,保留低 8 位(自动溢出,等同于裁剪) + return ((av & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (bv & 0xFF); + } + + /** + * 像素安全减法:计算 a - b,每个通道独立计算,结果裁剪到 0~255 + * 用于预测编码:残差 = 当前像素 - 预测值 + */ + public static int pixelSub(int a, int b) { + int ar = (a >> 16) & 0xFF; + int ag = (a >> 8) & 0xFF; + int ab = a & 0xFF; + int aa = (a >> 24) & 0xFF; + + int br = (b >> 16) & 0xFF; + int bg = (b >> 8) & 0xFF; + int bb = b & 0xFF; + int ba = (b >> 24) & 0xFF; + + int r = ar - br; + int g = ag - bg; + int bv = ab - bb; + int av = aa - ba; // Alpha 通道也要处理! + + // 用 &0xFF 替代 min/max,保留低 8 位(自动溢出,等同于裁剪) + return ((av & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (bv & 0xFF); + } + +} diff --git a/src/org/kne/codec/kif/StatisticsCollector.java b/src/org/kne/codec/kif/StatisticsCollector.java new file mode 100644 index 0000000..24b72cd --- /dev/null +++ b/src/org/kne/codec/kif/StatisticsCollector.java @@ -0,0 +1,219 @@ +package org.kne.codec.kif; + +import java.text.DecimalFormat; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +/** + * 通用统计收集器(线程安全) + * 统计任意对象的出现次数与概率 + * + * @param 被统计的对象类型 + */ +public class StatisticsCollector { + + protected final ConcurrentHashMap counts = new ConcurrentHashMap<>(); + protected final AtomicLong total = new AtomicLong(0); + protected final DecimalFormat df = new DecimalFormat("0.00"); + + /** + * 记录一次出现 + */ + public void addRecord(T item) { + counts.computeIfAbsent(item, k -> new AtomicLong(0)).incrementAndGet(); + total.incrementAndGet(); + } + + /** + * 记录多次出现 + */ + public void addRecord(T item, long count) { + counts.computeIfAbsent(item, k -> new AtomicLong(0)).addAndGet(count); + total.addAndGet(count); + } + + /** + * 记录另一个收集器的所有数据 + */ + public void merge(StatisticsCollector other) { + for (Map.Entry entry : other.counts.entrySet()) { + addRecord(entry.getKey(), entry.getValue().get()); + } + } + + /** + * 获取总次数 + */ + public long getTotal() { + return total.get(); + } + + /** + * 获取某个对象的出现次数 + */ + public long getCount(T item) { + AtomicLong count = counts.get(item); + return count == null ? 0 : count.get(); + } + + /** + * 获取某个对象的出现概率(百分比) + */ + public double getProbability(T item) { + long t = total.get(); + if (t == 0) return 0.0; + return getCount(item) * 100.0 / t; + } + + /** + * 获取去重对象数量 + */ + public int getUniqueCount() { + return counts.size(); + } + + /** + * 获取所有对象(按出现次数降序排列) + */ + public Map getSorted() { + return counts.entrySet().stream() + .sorted((a, b) -> Long.compare(b.getValue().get(), a.getValue().get())) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (old, neu) -> old, + java.util.LinkedHashMap::new + )); + } + + /** + * 获取所有对象(按出现次数升序排列) + */ + public Map getSortedAscending() { + return counts.entrySet().stream() + .sorted((a, b) -> Long.compare(a.getValue().get(), b.getValue().get())) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (old, neu) -> old, + java.util.LinkedHashMap::new + )); + } + + /** + * 重置统计 + */ + public void reset() { + counts.clear(); + total.set(0); + } + + /** + * 判断是否为空 + */ + public boolean isEmpty() { + return counts.isEmpty(); + } + + /** + * 生成表格形式的统计报告(需要子类实现格式化) + */ + public String formatTable(ItemFormatter formatter) { + long t = total.get(); + if (t == 0) { + return "(无统计数据)"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("╔═══════════════════════════════════════════════════════════════════════════════════╗\n"); + sb.append("║ 统计报告 ║\n"); + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + sb.append("║ 序号 │ 对象 │ 出现次数 │ 占比 │ 累积占比 ║\n"); + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + + Map sorted = getSorted(); + long cumulative = 0; + int index = 0; + + for (Map.Entry entry : sorted.entrySet()) { + T item = entry.getKey(); + long count = entry.getValue().get(); + double pct = count * 100.0 / t; + cumulative += count; + double cumPct = cumulative * 100.0 / t; + index++; + + String itemStr = formatter.format(item); + // 截断过长的字符串 + if (itemStr.length() > 27) { + itemStr = itemStr.substring(0, 24) + "..."; + } + + sb.append(String.format("║ %4d │ %-27s │ %8d │ %6.2f%% │ %6.2f%% ║\n", + index, + itemStr, + count, + pct, + cumPct + )); + } + + sb.append("╠═══════════════════════════════════════════════════════════════════════════════════╣\n"); + sb.append(String.format("║ 总次数: %d │ 唯一对象数: %d │ 覆盖率: %6.2f%% ║\n", + t, + getUniqueCount(), + 100.0 + )); + sb.append("╚═══════════════════════════════════════════════════════════════════════════════════╝"); + return sb.toString(); + } + + /** + * 生成 CSV 格式报告 + */ + public String toCSV(ItemFormatter formatter) { + long t = total.get(); + if (t == 0) return "无数据"; + + StringBuilder sb = new StringBuilder(); + sb.append("序号,对象,次数,占比(%),累积占比(%)\n"); + + Map sorted = getSorted(); + long cumulative = 0; + int index = 0; + + for (Map.Entry entry : sorted.entrySet()) { + T item = entry.getKey(); + long count = entry.getValue().get(); + double pct = count * 100.0 / t; + cumulative += count; + double cumPct = cumulative * 100.0 / t; + index++; + + sb.append(String.format("%d,%s,%d,%.2f,%.2f\n", + index, + formatter.format(item), + count, + pct, + cumPct + )); + } + return sb.toString(); + } + + @Override + public String toString() { + return formatTable((t)->{ + return t.toString(); + }); + } + + /** + * 对象格式化接口 + */ + public interface ItemFormatter { + String format(T item); + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kif/Zigzag.java b/src/org/kne/codec/kif/Zigzag.java new file mode 100644 index 0000000..96d8d28 --- /dev/null +++ b/src/org/kne/codec/kif/Zigzag.java @@ -0,0 +1,59 @@ +package org.kne.codec.kif; + +/** + * Zigzag 映射工具 + * 用于将预测残差(集中在0附近的有符号数)映射为熵编码友好的非负数序列 + */ +public class Zigzag { + + /** + * 映射单个 byte + * 0→0, -1→1, 1→2, -2→3, 2→4, ... + */ + public static byte map(byte x) { + int v = x; // byte 转为 int(保留符号) + return (byte) ((v >= 0) ? (v << 1) : ((-v << 1) - 1)); + } + + /** + * 逆映射单个 byte + * 0→0, 1→-1, 2→1, 3→-2, 4→2, ... + * + * 用按位与替代取余:v & 1 等价于 v % 2,但快 2-3 倍 + */ + public static byte unmap(byte y) { + int v = y & 0xFF; // 转为无符号 0~255 + return (byte) ((v & 1) == 0 ? (v >> 1) : (-((v + 1) >> 1))); + } + + /** + * 映射整个字节数组(原地修改) + */ + public static void mapInPlace(byte[] data) { + for (int i = 0; i < data.length; i++) { + data[i] = map(data[i]); + } + } + + /** + * 逆映射整个字节数组(原地修改) + */ + public static void unmapInPlace(byte[] data) { + for (int i = 0; i < data.length; i++) { + data[i] = unmap(data[i]); + } + } + + /** + * 映射整个 int 数组(每个元素的低 8 位) + * 注意:只处理每个 int 的低 8 位,高位保持不变 + */ + public static void mapInPlaceInt(int[] data) { + for (int i = 0; i < data.length; i++) { + int v = data[i] & 0xFF; // 取低 8 位作为有符号数 + // 处理符号扩展:如果最高位为 1,说明是负数 + byte b = (byte) v; + data[i] = (data[i] & 0xFFFFFF00) | (map(b) & 0xFF); + } + } +} \ No newline at end of file diff --git a/src/org/kne/codec/kvf/H264ToKVF.java b/src/org/kne/codec/kvf/H264ToKVF.java new file mode 100644 index 0000000..015220e --- /dev/null +++ b/src/org/kne/codec/kvf/H264ToKVF.java @@ -0,0 +1,76 @@ +package org.kne.codec.kvf; + +import org.jcodec.api.FrameGrab; +import org.jcodec.api.JCodecException; +import org.jcodec.common.io.FileChannelWrapper; +import org.jcodec.common.io.SeekableByteChannel; +import org.jcodec.common.model.Picture; +import org.jcodec.containers.mp4.demuxer.MP4Demuxer; +import org.jcodec.scale.AWTUtil; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.util.zip.ZipFile; + +/** + * JCodec 最小化 Demo:读取 MP4 文件,解码每一帧为 BufferedImage + * + * 用法:java JCodecDemo /path/to/video.mp4 + */ +public class H264ToKVF { + + public static void main(String[] args) throws IOException, JCodecException { + // 1. 注册 KIF 插件 + org.kne.codec.kif.KIFImageReaderSpi.register(); + org.kne.codec.kif.KIFImageWriterSpi.register(); + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); + if (args.length < 1) { + System.err.println("用法: java JCodecDemo "); + System.exit(1); + } + + File inputFile = new File(args[0]); + if (!inputFile.exists()) { + System.err.println("文件不存在: " + inputFile.getAbsolutePath()); + System.exit(1); + } + File outputFile=new File("output.kvf"); + + System.out.println("🎬 开始转码: " + inputFile.getName()); + + // 1. 打开 FileChannel,向上转型为 SeekableByteChannel + try ( KVFOutputStream kvo=new KVFOutputStream(new FileOutputStream(outputFile))){ + try (FileInputStream fis = new FileInputStream(inputFile); + FileChannel fileChannel = fis.getChannel()) { + + // 关键:用 SeekableByteChannel 类型接收 + SeekableByteChannel channel =new FileChannelWrapper( fileChannel); + // 2. 创建帧抓取器 + MP4Demuxer demux=MP4Demuxer.createMP4Demuxer(channel); + // 3. 逐帧解码 + int frameCount = 0; + long startTime = System.currentTimeMillis(); + + Picture picture; + /* while ((picture = grab.getNativeFrame()) != null) { + frameCount++; + + System.out.printf(" 已解码 %d 帧...\n", frameCount); + kvo.writeFrame(0, frameCount, picture, "kif"); + }*/ + + long elapsed = System.currentTimeMillis() - startTime; + System.out.printf("✅ 转码完成!共 %d 帧,耗时 %dms\n", frameCount, elapsed); + } + } + } + +} \ No newline at end of file diff --git a/src/org/kne/codec/kvf/KVFCodec.java b/src/org/kne/codec/kvf/KVFCodec.java new file mode 100644 index 0000000..610e9f4 --- /dev/null +++ b/src/org/kne/codec/kvf/KVFCodec.java @@ -0,0 +1,94 @@ +package org.kne.codec.kvf; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import javax.imageio.ImageIO; + +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.kne.opencl64.OpenCLDevice; +import org.kne.opencl64.concurrent.OpenCLExecutors; + +public class KVFCodec { + private static ThreadPoolExecutor cpupool ; + static { + cpupool= (ThreadPoolExecutor) Executors.newFixedThreadPool( + Runtime.getRuntime().availableProcessors(), + new ThreadFactory() { + private final AtomicInteger threadNumber = new AtomicInteger(1); + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r, "KVFCodec-CPU-Worker-" + threadNumber.getAndIncrement()); + t.setDaemon(true); // 设置为守护线程 + return t; + } + } + ); + } + public static Future encodeIFrame(BufferedImage frame,String formattype,Consumerresult) { + cpuBackPressure(); + return cpupool.submit(()->{ + ByteArrayOutputStream boos=new ByteArrayOutputStream(); + ImageIO.write(frame, formattype, boos); + byte[]resultdata=boos.toByteArray(); + if(result!=null) { + result.accept(resultdata); + } + return resultdata; + }); + } + + public static Future encodeIFrame(Picture picture,String formattype,Consumerresult) { + cpuBackPressure(); + return cpupool.submit(()->{ + BufferedImage frame = AWTUtil.toBufferedImage(picture); + ByteArrayOutputStream boos=new ByteArrayOutputStream(); + ImageIO.write(frame, formattype, boos); + byte[]resultdata=boos.toByteArray(); + if(result!=null) { + result.accept(resultdata); + } + return resultdata; + }); + } + + public static Future decodeIFrame(byte[]data,Consumerresult){ + cpuBackPressure(); + return cpupool.submit(()->{ + ByteArrayInputStream biis=new ByteArrayInputStream(data); + BufferedImage resultframe= ImageIO.read(biis); + biis.close(); + if(result!=null) { + result.accept(resultframe); + } + return resultframe; + }); + } + + + public static void cpuBackPressure() { + while(isCPUOverloaded()) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * 判断是否过载(调用方可据此决定是否降级处理) + */ + public static boolean isCPUOverloaded() { + return cpupool.getQueue().size() > cpupool.getPoolSize() * 2; + } +} diff --git a/src/org/kne/codec/kvf/KVFOutputStream.java b/src/org/kne/codec/kvf/KVFOutputStream.java new file mode 100644 index 0000000..738f0f7 --- /dev/null +++ b/src/org/kne/codec/kvf/KVFOutputStream.java @@ -0,0 +1,150 @@ +package org.kne.codec.kvf; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.locks.ReentrantLock; +import java.util.zip.Deflater; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.imageio.ImageIO; + +import org.jcodec.common.model.Picture; +import org.jcodec.scale.AWTUtil; +import org.kne.codec.kif.ExceptionTool; + +public class KVFOutputStream extends ZipOutputStream { + private ReentrantLock lock=new ReentrantLock(); + private List>tasks=Collections.synchronizedList(new ArrayList<>()); + public KVFOutputStream(OutputStream out, Charset charset) { + super(out, charset); + setLevel(Deflater.NO_COMPRESSION); + } + + public KVFOutputStream(OutputStream out) { + super(out); + setLevel(Deflater.NO_COMPRESSION); + } + + + /** + * 写入一个条目(自动处理锁) + */ + public void putEntry(String name, byte[] data) throws IOException { + lock.lock(); + try { + putNextEntry(new ZipEntry(name)); + write(data); + closeEntry(); + } finally { + lock.unlock(); + } + } + + + + /** + * 写入 meta.json(自动使用 UTF-8) + */ + public void writeMeta(String json) throws IOException { + putEntry("meta.json", json.getBytes(StandardCharsets.UTF_8)); + } + + + /** + * 写入一个帧(KIF 数据) + */ + public void writeFrame(int channelIndex, int frameIndex, byte[] kifData,String format) throws IOException { + putEntry(String.format("video/%d/frame_%04d.%s",channelIndex, frameIndex,format), kifData); + } + //private List + public void writeFrame(int channelIndex, int frameIndex,BufferedImage frame,String format) throws IOException { + removeDone(); + FutureencodeFuture= KVFCodec.encodeIFrame(frame, format, (result)->{ + try { + writeFrame(channelIndex, frameIndex, result,format); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + tasks.add(encodeFuture); + } + + public void writeFrame(int channelIndex, int frameIndex, Picture picture, String format) throws IOException { + removeDone(); + FutureencodeFuture= KVFCodec.encodeIFrame(picture, format, (result)->{ + try { + writeFrame(channelIndex, frameIndex, result,format); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + tasks.add(encodeFuture); + } + + private void waitForDone()throws IOException { + while(true) { + removeDone(); + if(tasks.isEmpty()) { + return; + } + try { + Thread.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + private void removeDone()throws IOException { + AtomicReferencee=new AtomicReference<>(); + tasks.removeIf((v)->{ + boolean b=v.isDone(); + if(b!=false) { + try{ + v.get(); + }catch(ExecutionException | InterruptedException thr) { + if(e.get()==null) { + e.set(thr); + } + } + } + return b; + }); + Throwable th=e.get(); + ExceptionTool.throwIOException(th); + } + + @Override + public void finish() throws IOException { + waitForDone(); + lock.lock(); + try { + super.finish(); + } finally { + lock.unlock(); + } + } + + + @Override + public void close() throws IOException { + waitForDone(); + lock.lock(); + try { + super.close(); + } finally { + lock.unlock(); + } + } +} diff --git a/src/org/kne/codec/kvf/MP4Analyzer.java b/src/org/kne/codec/kvf/MP4Analyzer.java new file mode 100644 index 0000000..e37f446 --- /dev/null +++ b/src/org/kne/codec/kvf/MP4Analyzer.java @@ -0,0 +1,269 @@ +package org.kne.codec.kvf; + +import org.jcodec.common.DemuxerTrack; +import org.jcodec.common.DemuxerTrackMeta; +import org.jcodec.common.io.NIOUtils; +import org.jcodec.common.io.SeekableByteChannel; +import org.jcodec.common.model.Packet; +import org.jcodec.common.model.Picture; +import org.jcodec.common.model.Size; +import org.jcodec.containers.mp4.MP4TrackType; +import org.jcodec.containers.mp4.boxes.*; +import org.jcodec.containers.mp4.demuxer.AbstractMP4DemuxerTrack; +import org.jcodec.containers.mp4.demuxer.MP4Demuxer; +import org.jcodec.containers.mp4.demuxer.MP4DemuxerTrack; + +import java.io.File; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * MP4 元数据分析器 + * 获取音视频轨道、帧率、GOP、编码格式等完整元数据 + */ +public class MP4Analyzer { + + public static void main(String[] args) throws Exception { + System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8)); + System.setErr(new PrintStream(System.err, true, StandardCharsets.UTF_8)); + if (args.length < 1) { + System.err.println("用法: java MP4Analyzer "); + System.exit(1); + } + + File file = new File(args[0]); + if (!file.exists()) { + System.err.println("文件不存在: " + file.getAbsolutePath()); + System.exit(1); + } + + System.out.println("========================================"); + System.out.println("📹 MP4 元数据分析: " + file.getName()); + System.out.println("========================================\n"); + + try (SeekableByteChannel channel = NIOUtils.readableChannel(file); + MP4Demuxer demuxer = MP4Demuxer.createMP4Demuxer(channel)) { + + // 1. 视频轨道信息 + analyzeVideoTrack(demuxer); + + // 2. 音频轨道信息 + analyzeAudioTracks(demuxer); + + // 3. 其他轨道 + analyzeOtherTracks(demuxer); + + // 4. 容器元数据 + analyzeContainerInfo(demuxer); + + // 5. GOP 结构分析 + analyzeGOP(demuxer); + + // 6. 统计信息 + printSummary(demuxer); + } + } + + /** + * 分析视频轨道 + */ + private static void analyzeVideoTrack(MP4Demuxer demuxer) throws Exception { + System.out.println("🎬 视频轨道 (Video Tracks)"); + System.out.println("───────────────────────────────────────────────"); + + List videoTracks = demuxer.getVideoTracks(); + if (videoTracks.isEmpty()) { + System.out.println(" ❌ 无视频轨道"); + System.out.println(); + return; + } + + for (int i = 0; i < videoTracks.size(); i++) { + DemuxerTrack track = videoTracks.get(i); + DemuxerTrackMeta meta = track.getMeta(); + + System.out.printf(" 轨道 %d:\n", i); + System.out.printf(" 编码格式: %s\n", meta.getCodec()); + System.out.printf(" 分辨率: %dx%d\n", meta.getVideoCodecMeta().getSize().getWidth(), + meta.getVideoCodecMeta().getSize().getHeight()); + System.out.printf(" 帧率: %.3f fps\n", meta.getTotalFrames() / meta.getTotalDuration()); + System.out.printf(" 总帧数: %d\n", meta.getTotalFrames()); + System.out.printf(" 时长: %.2f 秒\n", meta.getTotalDuration()); + System.out.printf(" 像素格式: %s\n", meta.getVideoCodecMeta().getColor()); + // System.out.printf(" 参考帧数: %d\n", meta.getVideoCodecMeta().getRefFrames()); + // System.out.printf(" 是否可变帧率: %s\n", meta.isVariableFps() ? "是" : "否"); + + // 尝试获取更多信息 + if (track instanceof MP4DemuxerTrack) { + MP4DemuxerTrack m4Track = (MP4DemuxerTrack) track; + // System.out.printf(" 轨道 ID: %d\n", m4Track.getTrackId()); + System.out.printf(" 轨道类型: %s\n", m4Track.getType()); + } + + System.out.println(); + } + } + + /** + * 分析音频轨道 + */ + private static void analyzeAudioTracks(MP4Demuxer demuxer) { + System.out.println("🎵 音频轨道 (Audio Tracks)"); + System.out.println("───────────────────────────────────────────────"); + + List audioTracks = demuxer.getAudioTracks(); + if (audioTracks.isEmpty()) { + System.out.println(" ❌ 无音频轨道"); + System.out.println(); + return; + } + + for (int i = 0; i < audioTracks.size(); i++) { + DemuxerTrack track = audioTracks.get(i); + DemuxerTrackMeta meta = track.getMeta(); + + System.out.printf(" 轨道 %d:\n", i); + System.out.printf(" 编码格式: %s\n", meta.getCodec()); + System.out.printf(" 采样率: %d Hz\n", meta.getAudioCodecMeta().getSampleRate()); + System.out.printf(" 声道数: %d\n", meta.getAudioCodecMeta().getChannelCount()); + System.out.printf(" 采样位数: %d bit\n", meta.getAudioCodecMeta().getSampleSize()); + System.out.printf(" 总帧数: %d\n", meta.getTotalFrames()); + System.out.printf(" 时长: %.2f 秒\n", meta.getTotalDuration()); + System.out.println(); + } + } + + /** + * 分析其他轨道(字幕等) + */ + private static void analyzeOtherTracks(MP4Demuxer demuxer) { + System.out.println("📝 其他轨道 (Other Tracks)"); + System.out.println("───────────────────────────────────────────────"); + + List otherTracks = demuxer.getTracks(); + if (otherTracks.isEmpty()) { + System.out.println(" ❌ 无其他轨道"); + System.out.println(); + return; + } + + for (DemuxerTrack track : otherTracks) { + DemuxerTrackMeta meta = track.getMeta(); + System.out.printf(" 轨道类型: %s, 编码: %s\n", + meta.getCodec(), meta.getCodec()); + } + System.out.println(); + } + + /** + * 分析容器元数据 + */ + private static void analyzeContainerInfo(MP4Demuxer demuxer) { + System.out.println("📦 容器信息 (Container Info)"); + System.out.println("───────────────────────────────────────────────"); + + // 获取 FileTypeBox (ftyp) + /* FileTypeBox ftyp = demuxer.getBoxes(FileTypeBox.class).get(0); + System.out.printf(" 主要品牌: %s\n", ftyp.getMajorBrand()); + System.out.printf(" 兼容品牌: %s\n", String.join(", ", ftyp.getCompatibleBrands())); + System.out.printf(" 版本: %d\n", ftyp.getVersion()); +*/ + // 获取 MovieBox (moov) + MovieBox moov = demuxer.getMovie(); + System.out.printf(" 时长 (timescale): %d\n", moov.getTimescale()); + System.out.println(); + } + + /** + * 分析 GOP 结构 + */ + private static void analyzeGOP(MP4Demuxer demuxer) throws Exception { + System.out.println("🎯 GOP 结构分析 (GOP Structure)"); + System.out.println("───────────────────────────────────────────────"); + + List videoTracks = demuxer.getVideoTracks(); + if (videoTracks.isEmpty()) { + System.out.println(" ❌ 无视频轨道"); + System.out.println(); + return; + } + + DemuxerTrack videoTrack = videoTracks.get(0); + DemuxerTrackMeta meta = videoTrack.getMeta(); + + // 获取 I 帧位置 + int[] seekFrames = meta.getSeekFrames(); + if (seekFrames == null || seekFrames.length == 0) { + System.out.println(" ⚠️ 无法获取 I 帧位置"); + System.out.println(); + return; + } + + System.out.printf(" I 帧数量: %d\n", seekFrames.length); + System.out.printf(" 平均 GOP 大小: %.1f 帧\n", + (double) meta.getTotalFrames() / seekFrames.length); + + // 显示前 10 个 GOP + System.out.println(" 前 10 个 I 帧位置:"); + for (int i = 0; i < Math.min(10, seekFrames.length); i++) { + int nextIFrame = (i + 1 < seekFrames.length) ? seekFrames[i + 1] : meta.getTotalFrames(); + int gopSize = nextIFrame - seekFrames[i]; + System.out.printf(" GOP %d: I 帧位置 %d, GOP 大小 %d 帧\n", + i, seekFrames[i], gopSize); + } + + // 检查是否有 B 帧 + boolean hasBFrame = false; + try { + // 重置轨道位置 + // 读取前 100 帧判断 + // 这里简化判断 + } catch (Exception e) { + // 忽略 + } + System.out.println(); + } + + /** + * 打印统计摘要 + */ + private static void printSummary(MP4Demuxer demuxer) { + System.out.println("📊 统计摘要"); + System.out.println("───────────────────────────────────────────────"); + + List videoTracks = demuxer.getVideoTracks(); + List audioTracks = demuxer.getAudioTracks(); + + System.out.printf(" 视频轨道: %d\n", videoTracks.size()); + System.out.printf(" 音频轨道: %d\n", audioTracks.size()); + + if (!videoTracks.isEmpty()) { + DemuxerTrackMeta meta = videoTracks.get(0).getMeta(); + System.out.printf(" 视频编码: %s\n", meta.getCodec()); + Size size = meta.getVideoCodecMeta().getSize(); + System.out.printf(" 分辨率: %dx%d\n", size.getWidth(), size.getHeight()); + System.out.printf(" 帧率: %.3f fps\n", meta.getTotalFrames() / meta.getTotalDuration()); + System.out.printf(" 总帧数: %d\n", meta.getTotalFrames()); + System.out.printf(" 时长: %.2f 秒 (%.2f 分钟)\n", + meta.getTotalDuration(), meta.getTotalDuration() / 60.0); + + int[] seekFrames = meta.getSeekFrames(); + if (seekFrames != null) { + System.out.printf(" GOP 数量: %d\n", seekFrames.length); + System.out.printf(" 平均 GOP 大小: %.1f 帧\n", + (double) meta.getTotalFrames() / seekFrames.length); + } + } + + if (!audioTracks.isEmpty()) { + DemuxerTrackMeta meta = audioTracks.get(0).getMeta(); + System.out.printf(" 音频编码: %s\n", meta.getCodec()); + System.out.printf(" 采样率: %d Hz\n", meta.getAudioCodecMeta().getSampleRate()); + System.out.printf(" 声道数: %d\n", meta.getAudioCodecMeta().getChannelCount()); + } + + System.out.println(); + System.out.println("✅ 分析完成!"); + } +} \ No newline at end of file diff --git a/src/org/kne/debug/Debuger.java b/src/org/kne/debug/Debuger.java new file mode 100644 index 0000000..d175d69 --- /dev/null +++ b/src/org/kne/debug/Debuger.java @@ -0,0 +1,129 @@ +package org.kne.debug; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + + +import java.util.Set; +final class Int implements Comparable,Cloneable { + public int value; + + public Int(int value) { + super(); + this.value = value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public Object clone() throws CloneNotSupportedException { + // TODO �Զ����ɵķ������ + return super.clone(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Int other = (Int) obj; + if (value != other.value) + return false; + return true; + } + + @Override + public String toString() { + return new Integer(value).toString(); + } + + @Override + public int compareTo(Int o) { + + return o.value-value; + } + + + +} + +public class Debuger extends Thread { + Map ,Object[]>md=new HashMap,Object[]>(); + @Override + public void run() { + while(true){ + for(int iz=0;iz<100;iz++){ + Map m=Thread.getAllStackTraces(); + Set> s=m.entrySet(); + Iterator> i=s.iterator(); + while(i.hasNext()){ + Entryelements =i.next(); + StackTraceElement[]value=elements.getValue(); + List l=new ArrayList(value.length); + for (int j = 0; j < value.length; j++) { + l.add(value[j]); + } + if(!l.isEmpty()) + if(md.containsKey(l)){ + ((Int)md.get(l)[0]).value++; + }else{ + md.put(l,new Object[]{new Int(1),elements.getKey()} ); + } + } + + } + ArrayList, Object[]>> tmp=new ArrayList(); + Set, Object[]>> sn=md.entrySet(); + Iterator, Object[]>> in=sn.iterator(); + while(in.hasNext()){ + Entry, Object[]>elements =in.next(); + + tmp.add(elements); + } + tmp.sort(new Comparator, Object[]>>() { + + @Override + public int compare(Entry, Object[]> o1, + Entry, Object[]> o2) { + + return ((Int)o2.getValue()[0]).value-((Int)o1.getValue()[0]).value; + } + }); + for(int d=0;d, Object[]>elements=tmp.get(d); + if(((Thread)(elements.getValue()[1])).getName().startsWith("Decode")){ + + }else{ + continue; + } + List k=elements.getKey(); + StringBuilder sb=new StringBuilder(); + for(int d2=0;d2 points; + private long lastNanoTime; + private long startNanoTime; + + /** + * 创建一个计时器 + * @param name 本次测量的名称(用于输出标题) + * @param enabled true=启用计时,false=所有操作跳过(零开销) + */ + public TimeDebugger(String name, boolean enabled) { + this.enabled = enabled; + this.name = name; + // enabled=false 时不分配 ArrayList 内存 + this.points = enabled ? new ArrayList<>() : null; + if (enabled) { + this.lastNanoTime = System.nanoTime(); + this.startNanoTime = this.lastNanoTime; + } else { + this.lastNanoTime = 0; + this.startNanoTime = 0; + } + } + + /** + * 标记一个时间点 + * @param label 当前阶段的名称 + */ + public void mark(String label) { + if (!enabled) return; + long now = System.nanoTime(); + long durationNs = now - lastNanoTime; + lastNanoTime = now; + points.add(new TimingPoint(label, durationNs)); + } + + /** + * 标记一个时间点,并打印到控制台(实时输出,方便跟踪长流程) + * @param label 当前阶段的名称 + */ + public void markAndPrint(String label) { + if (!enabled) return; + mark(label); + System.out.printf(" ⏱️ %s: %.3fms\n", label, getLastDurationMs()); + } + + /** + * 获取上一个阶段的耗时(毫秒) + */ + public double getLastDurationMs() { + if (!enabled || points.isEmpty()) return 0; + return points.get(points.size() - 1).durationMs(); + } + + /** + * 获取总耗时(毫秒) + */ + public double getTotalMs() { + if (!enabled) return 0; + return (lastNanoTime - startNanoTime) / 1_000_000.0; + } + + /** + * 打印所有计时结果 + */ + public void print() { + if (!enabled) return; + StringBuilder sb = new StringBuilder(); + sb.append("═══════════════════════════════════════════════\n"); + sb.append(" ⏱️ ").append(name).append("\n"); + sb.append("═══════════════════════════════════════════════\n"); + + double total = 0; + for (int i = 0; i < points.size(); i++) { + TimingPoint p = points.get(i); + double ms = p.durationMs(); + total += ms; + sb.append(String.format(" %-20s: %8.3fms (累计: %8.3fms)\n", + p.label, ms, total)); + } + + sb.append("───────────────────────────────────────────────\n"); + sb.append(String.format(" %-20s: %8.3fms\n", "总计", total)); + sb.append("═══════════════════════════════════════════════"); + System.err.println(sb.toString()); + } + + /** + * 重置计时器(不清除已有记录,用于分段测量) + */ + public void reset() { + if (!enabled) return; + this.lastNanoTime = System.nanoTime(); + this.startNanoTime = this.lastNanoTime; + if (points != null) { + this.points.clear(); + } + } + + /** + * 检查是否启用 + */ + public boolean isEnabled() { + return enabled; + } + + /** + * 内部计时点 + */ + private static final class TimingPoint { + final String label; + final long durationNs; + + TimingPoint(String label, long durationNs) { + this.label = label; + this.durationNs = durationNs; + } + + double durationMs() { + return durationNs / 1_000_000.0; + } + } + + // ==================== 静态方法:全局默认实例 ==================== + + private static TimeDebugger defaultDebugger; + + /** + * 获取全局默认计时器(简化用法,不需要 new) + * 默认启用,如需关闭请使用 get(boolean) + */ + public static TimeDebugger get() { + return get(true); + } + + /** + * 获取全局默认计时器(可指定开关) + * @param enabled true=启用,false=关闭 + */ + public static TimeDebugger get(boolean enabled) { + if (defaultDebugger == null || defaultDebugger.isEnabled() != enabled) { + defaultDebugger = new TimeDebugger("默认流程", enabled); + } + return defaultDebugger; + } + + /** + * 重置全局默认计时器 + */ + public static void resetDefault() { + if (defaultDebugger != null) { + defaultDebugger.reset(); + } + } +} \ No newline at end of file