KVFCodec
This commit is contained in:
@@ -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 ? "✅" : "❌"));
|
||||
}
|
||||
}
|
||||
@@ -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<BlockCoord> calculateBlockCoords(int width, int height, int blockWidth, int blockHeight) {
|
||||
int blocksX = (width + blockWidth - 1) / blockWidth;
|
||||
int blocksY = (height + blockHeight - 1) / blockHeight;
|
||||
List<BlockCoord> 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Integer>() {
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -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<Byte> {
|
||||
|
||||
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<Byte, AtomicLong> sorted = getSorted();
|
||||
if (sorted.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return sorted.entrySet().iterator().next().getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计后返回最常见字节值的出现次数
|
||||
*/
|
||||
public long getMostCommonCount() {
|
||||
Map<Byte, AtomicLong> 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<Byte> {
|
||||
@Override
|
||||
public String format(Byte item) {
|
||||
int v = item & 0xFF;
|
||||
return String.format("0x%02X", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package org.kne.codec.kif;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 颜色统计收集器
|
||||
* 用于统计图像中各种颜色出现的频率
|
||||
* 继承自 StatisticsCollector<Color>
|
||||
*/
|
||||
public class ColorStatisticsCollector extends StatisticsCollector<Color> {
|
||||
|
||||
|
||||
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<Color, AtomicLong> sorted = getSorted();
|
||||
if (sorted.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return sorted.entrySet().iterator().next().getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最罕见的颜色(出现次数最少的颜色)
|
||||
*/
|
||||
public Color getLeastFrequentColor() {
|
||||
Map<Color, AtomicLong> sorted = getSortedAscending();
|
||||
if (sorted.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return sorted.entrySet().iterator().next().getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取前 N 种最常见的颜色
|
||||
*/
|
||||
public Map<Color, AtomicLong> getTopColors(int n) {
|
||||
Map<Color, AtomicLong> 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<Color>() {
|
||||
@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<Color, AtomicLong> sorted = getSorted();
|
||||
long cumulative = 0;
|
||||
int index = 0;
|
||||
|
||||
for (Map.Entry<Color, AtomicLong> 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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String> INPUT_FORMATS = new HashSet<>(Arrays.asList(
|
||||
"png", "bmp", "kif"
|
||||
));
|
||||
// 要测试的输出格式(从 ImageIO 注册的 Writer 中获取)
|
||||
private static Set<String> 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<Path> imageFiles = getTestImages(inputDir);
|
||||
if (imageFiles.isEmpty()) {
|
||||
System.err.println("错误: input 目录中没有找到图片!");
|
||||
return;
|
||||
}
|
||||
System.out.println("找到 " + imageFiles.size() + " 张测试图片\n");
|
||||
// 5.预热
|
||||
warmup(outputDir, imageFiles,30000000000L);
|
||||
// 6. 执行实验
|
||||
List<ExperimentResult> results = runExperiment(outputDir, imageFiles);
|
||||
|
||||
// 7. 输出汇总报告
|
||||
printSummary(results);
|
||||
|
||||
System.out.println(KIFImageWriter.getModeCollector());
|
||||
}
|
||||
private static void warmup(Path outputDir, List<Path> 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<ExperimentResult> runExperiment(Path outputDir, List<Path> imageFiles) {
|
||||
List<ExperimentResult> 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<String> getAvailableWriterFormats() {
|
||||
Set<String> formats = new HashSet<>();
|
||||
IIORegistry registry = IIORegistry.getDefaultInstance();
|
||||
Iterator<ImageWriterSpi> 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<Path> getTestImages(Path inputDir) throws IOException {
|
||||
List<Path> images = new ArrayList<>();
|
||||
try (DirectoryStream<Path> 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<ExperimentResult> results) {
|
||||
System.out.println("\n\n");
|
||||
System.out.println("╔═══════════════════════════════════════════════════════════════════════════════════════════════════╗");
|
||||
System.out.println("║ 汇总报告 ║");
|
||||
System.out.println("╚═══════════════════════════════════════════════════════════════════════════════════════════════════╝");
|
||||
|
||||
// 按格式分组统计
|
||||
Map<String, List<ExperimentResult>> grouped = results.stream()
|
||||
.collect(Collectors.groupingBy(r -> r.format));
|
||||
|
||||
// 表头 - 压缩率统计
|
||||
System.out.println("\n┌──────────┬────────────┬──────────────┬─────────────┬────────────┬──────────┐");
|
||||
System.out.println("│ 格式 │ 测试数量 │ 平均压缩率 │ 最大压缩率 │ 最小压缩率 │ 无损率 │");
|
||||
System.out.println("├──────────┼────────────┼──────────────┼─────────────┼────────────┼──────────┤");
|
||||
|
||||
for (Map.Entry<String, List<ExperimentResult>> entry : grouped.entrySet()) {
|
||||
String format = entry.getKey();
|
||||
List<ExperimentResult> 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<String, List<ExperimentResult>> entry : grouped.entrySet()) {
|
||||
String format = entry.getKey();
|
||||
List<ExperimentResult> 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<ExperimentResult> 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<ExperimentResult> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ZstdCompressor> ZSTD_COMPRESSOR =
|
||||
ThreadLocal.withInitial(ZstdCompressor::new);
|
||||
|
||||
private static final ThreadLocal<ZstdDecompressor> ZSTD_DECOMPRESSOR =
|
||||
ThreadLocal.withInitial(ZstdDecompressor::new);
|
||||
|
||||
// ==================== Deflate (ZLIB) ====================
|
||||
|
||||
private static final ThreadLocal<JdkDeflateCompressor> DEFLATE_COMPRESSOR =
|
||||
ThreadLocal.withInitial(JdkDeflateCompressor::new);
|
||||
|
||||
private static final ThreadLocal<JdkInflateDecompressor> 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<BlockCoord> 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 + ")]";
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
List<OpenCLDevice>devs=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<KIFCompressedImageBlock> encodeAsync(IntImageBlock input,KIFCompressMode mode) {
|
||||
return encodeAsync(()->{return input;},mode);
|
||||
}
|
||||
|
||||
public static Future<KIFCompressedImageBlock> encodeAsync(Supplier<IntImageBlock> input,KIFCompressMode mode) {
|
||||
/*CompletableFuture<KIFEncodePipeline> f1=CompletableFuture.supplyAsync(()->{
|
||||
KIFEncodePipeline kip=new KIFEncodePipeline(input, mode);
|
||||
kip.encodeStage1();
|
||||
return kip;
|
||||
}, gpupool);
|
||||
CompletableFuture<KIFCompressedImageBlock>f2=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<KIFCompressedImageBlock> encodeAsyncMultimode(IntImageBlock block, List<KIFCompressMode> list) {
|
||||
return encodeAsyncMultimode(()->{return block;},list);
|
||||
}
|
||||
public static Future<KIFCompressedImageBlock> encodeAsyncMultimode(Supplier< IntImageBlock> block, List<KIFCompressMode> list) {
|
||||
// 为每个 mode 提交一个异步编码任务
|
||||
@SuppressWarnings("unchecked")
|
||||
ArrayList<Future<KIFCompressedImageBlock>> futures = new ArrayList<Future<KIFCompressedImageBlock>>(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<IntImageBlock> decodeAsync(Supplier< KIFCompressedImageBlock> input) {
|
||||
|
||||
return decodeAndConsumeAsync(input,null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static Future<IntImageBlock> decodeAndConsumeAsync(Supplier< KIFCompressedImageBlock> input,Consumer<IntImageBlock> con) {
|
||||
/*CompletableFuture<KIFDecodePipeline> f1=CompletableFuture.supplyAsync(()->{
|
||||
KIFDecodePipeline kip=new KIFDecodePipeline(input);
|
||||
try {
|
||||
kip.decodeStage1();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return kip;
|
||||
}, cpupool);
|
||||
CompletableFuture<IntImageBlock>f2=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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<KIFCompressMode> getAllModes() {
|
||||
List<KIFCompressMode> 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<KIFCompressMode> getNaturalImageModes() {
|
||||
List<KIFCompressMode> 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<KIFCompressMode> getScreenImageModes() {
|
||||
List<KIFCompressMode> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<KIFCompressedImageBlock>{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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 ThreadLocal<MemorySegment>m1=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());
|
||||
}
|
||||
}
|
||||
@@ -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 ThreadLocal<MemorySegment>m1=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);
|
||||
}
|
||||
}
|
||||
@@ -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<ImageTypeSpecifier> getImageTypes(int imageIndex) throws IOException {
|
||||
if (imageIndex != 0) {
|
||||
throw new IndexOutOfBoundsException("Only image index 0 is supported");
|
||||
}
|
||||
readHeader();
|
||||
|
||||
// 返回支持的图像类型(这里只支持标准的RGB或灰度)
|
||||
List<ImageTypeSpecifier> 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("读取文件头");
|
||||
AtomicReference<int[]>array=new AtomicReference<>();
|
||||
int blockcount=inputStream.readInt();
|
||||
ArrayList<Future<IntImageBlock>>decodeing=new ArrayList<>(blockcount);
|
||||
KIFCodec.backPressure();
|
||||
for(int i=0;i<blockcount;i++) {
|
||||
decodeing.add( KIFCodec.decodeAndConsumeAsync(()->{
|
||||
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<IntImageBlock> 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();
|
||||
}
|
||||
}
|
||||
@@ -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 格式。
|
||||
*
|
||||
* <p>使用示例:
|
||||
* <pre>
|
||||
* // 在应用启动时调用一次
|
||||
* KIFImageReaderSpi.register();
|
||||
*
|
||||
* // 之后就可以正常使用 ImageIO 了
|
||||
* BufferedImage img = ImageIO.read(new File("test.kif"));
|
||||
* </pre>
|
||||
*
|
||||
* @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";
|
||||
}
|
||||
}
|
||||
@@ -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<Future<KIFCompressedImageBlock>>encoding=new ArrayList<>();
|
||||
List<BlockCoord>bcd=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<Future<KIFCompressedImageBlock>> iterator = encoding.iterator(); iterator.hasNext();) {
|
||||
Future<KIFCompressedImageBlock> 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();
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.kne.codec.kif;
|
||||
|
||||
/**
|
||||
* KIF 压缩模式统计收集器(线程安全)
|
||||
* 专门统计 KIFCompressMode 的出现次数与概率
|
||||
*/
|
||||
public class ModeStatisticsCollector extends StatisticsCollector<KIFCompressMode> {
|
||||
|
||||
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<KIFCompressMode> {
|
||||
@Override
|
||||
public String format(KIFCompressMode mode) {
|
||||
return mode.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<KIFCompressedImageBlock> {
|
||||
|
||||
private final ArrayList<Future<KIFCompressedImageBlock>> 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<KIFCompressedImageBlock>> 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<KIFCompressedImageBlock> 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<KIFCompressedImageBlock> f : futures) {
|
||||
if (!f.isDone()) {
|
||||
allDone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allDone) {
|
||||
// 所有子任务完成,自动合并结果(不阻塞)
|
||||
synchronized (lock) {
|
||||
if (!done) {
|
||||
// 如果之前没有合并过,现在合并
|
||||
KIFCompressedImageBlock minBlock = null;
|
||||
Exception lastException = null;
|
||||
|
||||
for (Future<KIFCompressedImageBlock> 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<KIFCompressedImageBlock> 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<KIFCompressedImageBlock> 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<KIFCompressedImageBlock> 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<ImageReaderSpi> 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<ImageWriterSpi> 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()));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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<w&&py>=0&&py<h) {
|
||||
pred=datain[py*w+px];
|
||||
}
|
||||
datadout[idx] = pixelSub(datain[idx] , pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private static void horizontalInverse0(int[] datain,int[] datadout, int w, int h,int vx,int vy) {
|
||||
for (int y = 0; y < h; 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<w&&py>=0&&py<h) {
|
||||
pred=datadout[py*w+px];
|
||||
}
|
||||
// 还原:当前 = 左邻居 + 残差
|
||||
datadout[idx] =pixelAdd( pred , datain[idx]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static void verticalPredict0(int[] datain,int[] datadout, int w, int h,int vx,int vy) {
|
||||
for (int y = h - 1; y >= 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<w&&py>=0&&py<h) {
|
||||
pred=datain[py*w+px];
|
||||
}
|
||||
datadout[idx] = pixelSub(datain[idx] , pred);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void verticalInverse0(int[] datain,int[] datadout, int w, int h,int vx,int vy) {
|
||||
for (int y = 0; y < h; 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<w&&py>=0&&py<h) {
|
||||
pred=datadout[py*w+px];
|
||||
}
|
||||
datadout[idx] =pixelAdd( pred , datain[idx]);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 像素安全加法:将两个 RGBA 像素相加,每个通道独立计算,结果裁剪到 0~255
|
||||
* 用于预测逆运算:还原 = 预测值 + 残差
|
||||
*/
|
||||
public static 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; // 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <T> 被统计的对象类型
|
||||
*/
|
||||
public class StatisticsCollector<T> {
|
||||
|
||||
protected final ConcurrentHashMap<T, AtomicLong> 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<T> other) {
|
||||
for (Map.Entry<T, AtomicLong> 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<T, AtomicLong> 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<T, AtomicLong> 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<T> 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<T, AtomicLong> sorted = getSorted();
|
||||
long cumulative = 0;
|
||||
int index = 0;
|
||||
|
||||
for (Map.Entry<T, AtomicLong> 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<T> formatter) {
|
||||
long t = total.get();
|
||||
if (t == 0) return "无数据";
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("序号,对象,次数,占比(%),累积占比(%)\n");
|
||||
|
||||
Map<T, AtomicLong> sorted = getSorted();
|
||||
long cumulative = 0;
|
||||
int index = 0;
|
||||
|
||||
for (Map.Entry<T, AtomicLong> 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<T> {
|
||||
String format(T item);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <video.mp4>");
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<byte[]> encodeIFrame(BufferedImage frame,String formattype,Consumer<byte[]>result) {
|
||||
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<byte[]> encodeIFrame(Picture picture,String formattype,Consumer<byte[]>result) {
|
||||
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<BufferedImage> decodeIFrame(byte[]data,Consumer<BufferedImage>result){
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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<Future<byte[]>>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();
|
||||
Future<byte[]>encodeFuture= 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();
|
||||
Future<byte[]>encodeFuture= 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 {
|
||||
AtomicReference<Throwable>e=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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <video.mp4>");
|
||||
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<DemuxerTrack> 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<DemuxerTrack> 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<AbstractMP4DemuxerTrack> 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<DemuxerTrack> 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<DemuxerTrack> videoTracks = demuxer.getVideoTracks();
|
||||
List<DemuxerTrack> 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("✅ 分析完成!");
|
||||
}
|
||||
}
|
||||
@@ -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<Int>,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 <List<StackTraceElement>,Object[]>md=new HashMap<List<StackTraceElement>,Object[]>();
|
||||
@Override
|
||||
public void run() {
|
||||
while(true){
|
||||
for(int iz=0;iz<100;iz++){
|
||||
Map<Thread, StackTraceElement[]> m=Thread.getAllStackTraces();
|
||||
Set<Entry<Thread, StackTraceElement[]>> s=m.entrySet();
|
||||
Iterator<Entry<Thread, StackTraceElement[]>> i=s.iterator();
|
||||
while(i.hasNext()){
|
||||
Entry<Thread, StackTraceElement[]>elements =i.next();
|
||||
StackTraceElement[]value=elements.getValue();
|
||||
List<StackTraceElement> l=new ArrayList<StackTraceElement>(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<Entry<List<StackTraceElement>, Object[]>> tmp=new ArrayList();
|
||||
Set<Entry<List<StackTraceElement>, Object[]>> sn=md.entrySet();
|
||||
Iterator<Entry<List<StackTraceElement>, Object[]>> in=sn.iterator();
|
||||
while(in.hasNext()){
|
||||
Entry<List<StackTraceElement>, Object[]>elements =in.next();
|
||||
|
||||
tmp.add(elements);
|
||||
}
|
||||
tmp.sort(new Comparator<Entry<List<StackTraceElement>, Object[]>>() {
|
||||
|
||||
@Override
|
||||
public int compare(Entry<List<StackTraceElement>, Object[]> o1,
|
||||
Entry<List<StackTraceElement>, Object[]> o2) {
|
||||
|
||||
return ((Int)o2.getValue()[0]).value-((Int)o1.getValue()[0]).value;
|
||||
}
|
||||
});
|
||||
for(int d=0;d<tmp.size();d++){
|
||||
|
||||
Entry<List<StackTraceElement>, Object[]>elements=tmp.get(d);
|
||||
if(((Thread)(elements.getValue()[1])).getName().startsWith("Decode")){
|
||||
|
||||
}else{
|
||||
continue;
|
||||
}
|
||||
List<StackTraceElement> k=elements.getKey();
|
||||
StringBuilder sb=new StringBuilder();
|
||||
for(int d2=0;d2<k.size();d2++){
|
||||
sb.append(' ');sb.append(k.get(d2));
|
||||
}
|
||||
System.err.println(elements.getValue()[0]+":"+elements.getValue()[1]+"="+sb);
|
||||
}
|
||||
System.err.println("--------------------------------------------------------------------------------------------------------------------");
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// TODO �Զ����ɵ� catch ��
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package org.kne.debug;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 高性能插桩计时器
|
||||
* 用于在代码中插入时间测量点,输出各阶段耗时
|
||||
*
|
||||
* 用法:
|
||||
* private static final boolean DEBUG = false; // 生产环境关掉
|
||||
* TimeDebugger td = new TimeDebugger("编码流程", DEBUG);
|
||||
* td.mark("读取图片");
|
||||
* // ... 读取图片代码 ...
|
||||
* td.mark("预测");
|
||||
* // ... 预测代码 ...
|
||||
* td.mark("压缩");
|
||||
* // ... 压缩代码 ...
|
||||
* td.print();
|
||||
*
|
||||
* 当 DEBUG = false 时,所有方法调用被 JIT 优化为空操作,零开销
|
||||
*/
|
||||
public final class TimeDebugger {
|
||||
|
||||
private final boolean enabled;
|
||||
private final String name;
|
||||
private final List<TimingPoint> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user