Newer
Older
package azgracompress.compression;
import azgracompress.cache.QuantizationCacheManager;
import azgracompress.compression.exception.ImageCompressionException;
import azgracompress.data.Range;
import azgracompress.fileformat.QuantizationType;
import azgracompress.io.InputData;
import azgracompress.io.loader.IPlaneLoader;
import azgracompress.io.loader.PlaneLoaderFactory;
import azgracompress.quantization.vector.*;
import azgracompress.utilities.Stopwatch;
import azgracompress.utilities.Utils;
import org.jetbrains.annotations.NotNull;
import java.io.DataOutputStream;
import java.io.IOException;
public class VQImageCompressor extends CompressorDecompressorBase implements IImageCompressor {
public VQImageCompressor(CompressionOptions options) {
super(options);
}
/**
* Train vector quantizer from plane vectors.
*
* @param planeVectors Image vectors.
* @return Trained vector quantizer with codebook of set size.
*/
private VectorQuantizer trainVectorQuantizerFromPlaneVectors(final int[][] planeVectors) {
LBGVectorQuantizer vqInitializer = new LBGVectorQuantizer(planeVectors,
getCodebookSize(),
options.getWorkerCount(),
options.getQuantizationVector());
LBGResult vqResult = vqInitializer.findOptimalCodebook();
return new VectorQuantizer(vqResult.getCodebook());
}
/**
* Write the vector codebook to the compress stream.
*
* @param quantizer Quantizer with the codebook.
* @param compressStream Stream with compressed data.
* @throws ImageCompressionException When unable to write quantizer.
*/
private void writeQuantizerToCompressStream(final VectorQuantizer quantizer,
DataOutputStream compressStream) throws ImageCompressionException {
final CodebookEntry[] codebook = quantizer.getCodebookVectors();
try {
for (final CodebookEntry entry : codebook) {
final int[] entryVector = entry.getVector();
for (final int vecVal : entryVector) {
compressStream.writeShort(vecVal);
}
final long[] frequencies = quantizer.getFrequencies();
for (final long symbolFrequency : frequencies) {
compressStream.writeLong(symbolFrequency);
}
} catch (IOException ioEx) {
throw new ImageCompressionException("Unable to write codebook to compress stream.", ioEx);
if (options.isVerbose()) {
reportStatusToListeners("Wrote quantization vectors to compressed stream.");
/**
* Load quantizer from cached codebook.
*
* @return Vector quantizer with cached codebook.
* @throws ImageCompressionException when fails to read cached codebook.
*/
private VectorQuantizer loadQuantizerFromCache() throws ImageCompressionException {
QuantizationCacheManager cacheManager = new QuantizationCacheManager(options.getCodebookCacheFolder());
if (!cacheManager.doesVQCacheExists(options.getInputDataInfo().getCacheFileName(),
getCodebookSize(),
options.getQuantizationVector())) {
trainAndSaveCodebook();
}
final VQCodebook codebook = cacheManager.loadVQCodebook(options.getInputDataInfo().getCacheFileName(),
getCodebookSize(),
options.getQuantizationVector());
if (codebook == null) {
throw new ImageCompressionException("Failed to read quantization vectors from cache.");
/**
* Compress the image file specified by parsed CLI options using vector quantization.
*
* @param compressStream Stream to which compressed data will be written.
* @throws ImageCompressionException When compress process fails.
public long[] compress(DataOutputStream compressStream) throws ImageCompressionException {
if (options.getQuantizationType() == QuantizationType.Vector3D) {
return compressVoxels(compressStream);
}
assert (options.getQuantizationVector().getZ() == 1);
return compress1D2DVectors(compressStream);
}
@NotNull
private long[] compress1D2DVectors(DataOutputStream compressStream) throws ImageCompressionException {
final InputData inputDataInfo = options.getInputDataInfo();
Stopwatch stopwatch = new Stopwatch();
final boolean hasGeneralQuantizer = options.getCodebookType() != CompressionOptions.CodebookType.Individual;
final IPlaneLoader planeLoader;
final int[] huffmanSymbols = createHuffmanSymbols(getCodebookSize());
planeLoader = PlaneLoaderFactory.getPlaneLoaderForInputFile(inputDataInfo);
throw new ImageCompressionException("Unable to create plane reader. " + e.getMessage());
if (options.getCodebookType() == CompressionOptions.CodebookType.Global) {
reportStatusToListeners("Loading codebook from cache file.");
quantizer = loadQuantizerFromCache();
huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
reportStatusToListeners("Cached quantizer with huffman coder created.");
writeQuantizerToCompressStream(quantizer, compressStream);
} else if (options.getCodebookType() == CompressionOptions.CodebookType.MiddlePlane) {
reportStatusToListeners("Training vector quantizer from middle plane.");
final int[][] refPlaneVectors = planeLoader.loadVectorsFromPlaneRange(options, Utils.singlePlaneRange(getMiddlePlaneIndex()));
quantizer = trainVectorQuantizerFromPlaneVectors(refPlaneVectors);
huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
writeQuantizerToCompressStream(quantizer, compressStream);
reportStatusToListeners("Middle plane codebook created in: " + stopwatch.getElapsedTimeString());
}
final int[] planeIndices = getPlaneIndicesForCompression();
long[] planeDataSizes = new long[planeIndices.length];
int planeCounter = 0;
for (final int planeIndex : planeIndices) {
reportStatusToListeners(String.format("Loading plane %d.", planeIndex));
final int[][] planeVectors = planeLoader.loadVectorsFromPlaneRange(options, Utils.singlePlaneRange(planeIndex));
reportStatusToListeners(String.format("Training vector quantizer from plane %d.", planeIndex));
quantizer = trainVectorQuantizerFromPlaneVectors(planeVectors);
huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
writeQuantizerToCompressStream(quantizer, compressStream);
}
assert (quantizer != null);
// Use BestBinFirst KDTree for codebook lookup.
// final int[] indices = quantizer.quantizeIntoIndicesUsingKDTree(planeVectors, options.getWorkerCount());
// Use BruteForce for codebook lookup.
final int[] indices = quantizer.quantizeIntoIndices(planeVectors, options.getWorkerCount());
planeDataSizes[planeCounter++] = writeHuffmanEncodedIndices(compressStream, huffman, indices);
if (options.isConsoleApplication()) {
reportStatusToListeners("Finished compression of plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString());
} else {
reportProgressToListeners(planeIndex, planeIndices.length,
"Finished compression of plane %d in %s.", planeIndex, stopwatch.getElapsedTimeString());
}
return planeDataSizes;
@Override
public void trainAndSaveCodebook() throws ImageCompressionException {
reportStatusToListeners("Loading image data...");
final IPlaneLoader planeLoader;
planeLoader = PlaneLoaderFactory.getPlaneLoaderForInputFile(options.getInputDataInfo());
throw new ImageCompressionException("Unable to create plane reader. " + e.getMessage());
int[][] trainingData;
if (options.getInputDataInfo().isPlaneIndexSet()) {
reportStatusToListeners("VQ: Loading single plane data.");
final int planeIndex = options.getInputDataInfo().getPlaneIndex();
trainingData = planeLoader.loadVectorsFromPlaneRange(options, new Range<>(planeIndex, planeIndex + 1));
} else if (options.getInputDataInfo().isPlaneRangeSet()) {
reportStatusToListeners("VQ: Loading plane range data.");
trainingData = planeLoader.loadVectorsFromPlaneRange(options, options.getInputDataInfo().getPlaneRange());
} else {
reportStatusToListeners("VQ: Loading all planes data.");
trainingData = planeLoader.loadVectorsFromPlaneRange(options,
new Range<>(0, options.getInputDataInfo().getDimensions().getZ()));
LBGVectorQuantizer vqInitializer = new LBGVectorQuantizer(trainingData,
getCodebookSize(),
options.getWorkerCount(),
options.getQuantizationVector());
reportStatusToListeners("Starting LBG optimization.");
vqInitializer.setStatusListener(this::reportStatusToListeners);
LBGResult lbgResult = vqInitializer.findOptimalCodebook();
reportStatusToListeners("Learned the optimal codebook.");
QuantizationCacheManager cacheManager = new QuantizationCacheManager(options.getCodebookCacheFolder());
final String cacheFilePath = cacheManager.saveCodebook(options.getInputDataInfo().getCacheFileName(), lbgResult.getCodebook());
reportStatusToListeners("Saved cache file to %s", cacheFilePath);
throw new ImageCompressionException("Unable to write VQ cache.", e);
reportStatusToListeners("Operation completed.");
/**
* Calculate the number of voxel layers needed for dataset of plane count.
*
* @param datasetPlaneCount Dataset plane count
* @param voxelDepth Z dimension of voxel.
* @return Number of voxel layers.
*/
public static int calculateVoxelLayerCount(final int datasetPlaneCount, final int voxelDepth) {
return (datasetPlaneCount / voxelDepth);
}
public long[] compressVoxels(DataOutputStream compressStream) throws ImageCompressionException {
assert (options.getCodebookType() == CompressionOptions.CodebookType.Global);
final IPlaneLoader planeLoader;
final int[] huffmanSymbols = createHuffmanSymbols(getCodebookSize());
try {
planeLoader = PlaneLoaderFactory.getPlaneLoaderForInputFile(options.getInputDataInfo());
planeLoader.setWorkerCount(options.getWorkerCount());
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
} catch (Exception e) {
throw new ImageCompressionException("Unable to create plane reader. " + e.getMessage());
}
final int voxelLayerDepth = options.getQuantizationVector().getZ();
final int voxelLayerCount = calculateVoxelLayerCount(options.getInputDataInfo().getDimensions().getZ(), voxelLayerDepth);
long[] voxelLayersSizes = new long[voxelLayerCount];
final VectorQuantizer quantizer = loadQuantizerFromCache();
final Huffman huffman = createHuffmanCoder(huffmanSymbols, quantizer.getFrequencies());
writeQuantizerToCompressStream(quantizer, compressStream);
int[][] voxelData;
Stopwatch stopwatch = new Stopwatch();
for (int voxelLayerIndex = 0; voxelLayerIndex < voxelLayerCount; voxelLayerIndex++) {
stopwatch.restart();
final int fromZ = (voxelLayerIndex * voxelLayerDepth);
// TODO(Moravec): There is a problem!
// If dataset.Z is not divisible by voxel.Z we end up creating a lot stupid voxels.
// Those stupid voxels have only one or two layers of actual data and the rest are zeros.
// This ends up increasing the file size because they have quite long Huffman codes.
final int toZ = (voxelLayerIndex == voxelLayerCount - 1)
? options.getInputDataInfo().getDimensions().getZ()
: (voxelLayerDepth + (voxelLayerIndex * voxelLayerDepth));
final Range<Integer> voxelLayerRange = new Range<>(fromZ, toZ);
try {
voxelData = planeLoader.loadVoxels(options.getQuantizationVector(), voxelLayerRange);
} catch (IOException e) {
throw new ImageCompressionException("Unable to load voxels from voxel layer " + voxelLayerRange, e);
}
final int[] indices = quantizer.quantizeIntoIndices(voxelData, options.getWorkerCount());
voxelLayersSizes[voxelLayerIndex] = writeHuffmanEncodedIndices(compressStream, huffman, indices);
stopwatch.stop();
if (options.isConsoleApplication()) {
reportStatusToListeners("%d/%d Finished voxel layer %s compression pass in %s",
voxelLayerIndex, voxelLayerCount, voxelLayerRange.toString(), stopwatch.getElapsedTimeString());
} else {
reportProgressToListeners(voxelLayerIndex, voxelLayerCount,
"%d/%d Finished voxel layer %s compression pass in %s",
voxelLayerIndex, voxelLayerCount, voxelLayerRange.toString(), stopwatch.getElapsedTimeString());
}
return voxelLayersSizes;