Skip to content
Snippets Groups Projects
VQImageDecompressor.java 22.8 KiB
Newer Older
  • Learn to ignore specific revisions
  • package azgracompress.compression;
    
    
    import azgracompress.cache.ICacheFile;
    import azgracompress.cache.VQCacheFile;
    
    import azgracompress.compression.exception.ImageDecompressionException;
    
    import azgracompress.data.*;
    import azgracompress.fileformat.QCMPFileHeader;
    
    import azgracompress.fileformat.QuantizationType;
    
    import azgracompress.huffman.Huffman;
    import azgracompress.huffman.HuffmanNode;
    
    import azgracompress.io.InBitStream;
    
    import azgracompress.quantization.vector.VQCodebook;
    
    import azgracompress.utilities.Stopwatch;
    
    import azgracompress.utilities.TypeConverter;
    
    import java.io.DataInputStream;
    import java.io.DataOutputStream;
    import java.io.IOException;
    
    public class VQImageDecompressor extends CompressorDecompressorBase implements IImageDecompressor {
    
        private VQCodebook cachedCodebook = null;
        private Huffman cachedHuffman = null;
    
    
        private interface DecompressCallback {
            void process(final Block imageBlock, final int planeIndex) throws ImageDecompressionException;
        }
    
    
        private interface DecompressVoxelCallback {
            void process(final Voxel decompressedVoxel,
                         final int[][] decompressedVoxelData,
                         final int planeOffset) throws ImageDecompressionException;
        }
    
    
        public VQImageDecompressor(final CompressionOptions options) {
    
            super(options);
        }
    
        private long calculatePlaneVectorCount(final QCMPFileHeader header) {
            final int vectorXCount = (int) Math.ceil((double) header.getImageSizeX() / (double) header.getVectorSizeX());
            final int vectorYCount = (int) Math.ceil((double) header.getImageSizeY() / (double) header.getVectorSizeY());
            // Number of vectors per plane.
            return (vectorXCount * vectorYCount);
        }
    
    
        private VQCodebook readCodebook(final DataInputStream compressedStream,
    
                                        final int codebookSize,
                                        final int vectorSize) throws ImageDecompressionException {
    
            final int[][] codebookVectors = new int[codebookSize][vectorSize];
    
            final long[] frequencies = new long[codebookSize];
    
            try {
                for (int codebookIndex = 0; codebookIndex < codebookSize; codebookIndex++) {
    
                    //                final int[] vector = new int[vectorSize];
    
                    for (int vecIndex = 0; vecIndex < vectorSize; vecIndex++) {
    
                        codebookVectors[codebookIndex][vecIndex] = compressedStream.readUnsignedShort();
    
                }
                for (int codebookIndex = 0; codebookIndex < codebookSize; codebookIndex++) {
                    frequencies[codebookIndex] = compressedStream.readLong();
    
            } catch (final IOException ioEx) {
    
                throw new ImageDecompressionException("Unable to read quantization values from compressed stream.", ioEx);
    
    
            // We don't care about vector dimensions in here.
            return new VQCodebook(new V3i(0), codebookVectors, frequencies);
    
        @Override
        public void preloadGlobalCodebook(final ICacheFile codebookCacheFile) {
            assert (codebookCacheFile instanceof VQCacheFile) : "Incorrect codebook cache file type for VQImageDecompressor";
    
            final VQCacheFile codebookCache = (VQCacheFile) codebookCacheFile;
    
    
            cachedCodebook = codebookCache.getCodebook();
            cachedHuffman = createHuffmanCoder(createHuffmanSymbols(cachedCodebook.getCodebookSize()), cachedCodebook.getVectorFrequencies());
        }
    
    
        private Block reconstructImageFromQuantizedVectors(final int[][] vectors,
                                                           final V2i qVector,
                                                           final V3i imageDims) {
    
            final Block reconstructedChunk = new Block(new V2i(imageDims.getX(), imageDims.getY()));
    
            if (qVector.getY() > 1) {
    
                reconstructedChunk.reconstructFrom2DVectors(vectors, qVector);
    
            } else {
                // 1D vector
                reconstructedChunk.reconstructFromVectors(vectors);
            }
    
            return reconstructedChunk;
    
        public long getExpectedDataSize(final QCMPFileHeader header) {
    
            // Vector count in codebook
    
            final int codebookSize = (int) Math.pow(2, header.getBitsPerCodebookIndex());
    
    
            // Single vector size in bytes.
            final int vectorDataSize = 2 * header.getVectorSizeX() * header.getVectorSizeY() * header.getVectorSizeZ();
    
            // Total codebook size in bytes.
    
            final long codebookDataSize = ((codebookSize * vectorDataSize) + (codebookSize * LONG_BYTES)) *
                    (header.isCodebookPerPlane() ? header.getImageSizeZ() : 1);
    
            // Indices are encoded using huffman. Plane data size is written in the header.
    
            final long[] planeDataSizes = header.getPlaneDataSizes();
    
            long totalPlaneDataSize = 0;
            for (final long planeDataSize : planeDataSizes) {
                totalPlaneDataSize += planeDataSize;
            }
            return (codebookDataSize + totalPlaneDataSize);
    
        public void decompress(final DataInputStream compressedStream,
                               final DataOutputStream decompressStream,
                               final QCMPFileHeader header) throws ImageDecompressionException {
    
            if (header.getQuantizationType() == QuantizationType.Vector3D) {
                decompressVoxels(compressedStream, decompressStream, header);
                return;
            }
    
            decompressImpl(compressedStream, header, (imageBlock, planeIndex) -> {
    
                    decompressStream.write(TypeConverter.unsignedShortArrayToByteArray(imageBlock.getData(), false));
    
                } catch (final IOException e) {
    
                    throw new ImageDecompressionException("Unable to write decompressed data to decompress stream.", e);
                }
    
        public void decompressImpl(final DataInputStream compressedStream,
                                   final QCMPFileHeader header,
                                   final DecompressCallback callback) throws ImageDecompressionException {
    
            final int codebookSize = (int) Math.pow(2, header.getBitsPerCodebookIndex());
            assert (header.getVectorSizeZ() == 1);
            final int vectorSize = header.getVectorSizeX() * header.getVectorSizeY() * header.getVectorSizeZ();
            final int planeCountForDecompression = header.getImageSizeZ();
            final long planeVectorCount = calculatePlaneVectorCount(header);
            final V2i qVector = new V2i(header.getVectorSizeX(), header.getVectorSizeY());
            final int[] huffmanSymbols = createHuffmanSymbols(codebookSize);
    
    
            VQCodebook codebook = null;
            Huffman huffman = null;
            if (!header.isCodebookPerPlane()) {
                // There is only one codebook.
                codebook = readCodebook(compressedStream, codebookSize, vectorSize);
                huffman = createHuffmanCoder(huffmanSymbols, codebook.getVectorFrequencies());
            }
    
            for (int planeIndex = 0; planeIndex < planeCountForDecompression; planeIndex++) {
                if (header.isCodebookPerPlane()) {
                    codebook = readCodebook(compressedStream, codebookSize, vectorSize);
                    huffman = createHuffmanCoder(huffmanSymbols, codebook.getVectorFrequencies());
                }
                assert (codebook != null && huffman != null);
    
    
                final int planeDataSize = (int) header.getPlaneDataSizes()[planeIndex];
    
                try (final InBitStream inBitStream = new InBitStream(compressedStream,
                                                                     header.getBitsPerCodebookIndex(),
                                                                     planeDataSize)) {
    
                    inBitStream.readToBuffer();
                    inBitStream.setAllowReadFromUnderlyingStream(false);
    
    
                    final int[][] decompressedVectors = new int[(int) planeVectorCount][vectorSize];
    
                    for (int vecIndex = 0; vecIndex < planeVectorCount; vecIndex++) {
                        HuffmanNode currentHuffmanNode = huffman.getRoot();
                        boolean bit;
                        while (!currentHuffmanNode.isLeaf()) {
                            bit = inBitStream.readBit();
                            currentHuffmanNode = currentHuffmanNode.traverse(bit);
                        }
    
                        System.arraycopy(codebook.getVectors()[currentHuffmanNode.getSymbol()],
    
                                         0, decompressedVectors[vecIndex], 0, vectorSize);
    
                    final Block decompressedPlane = reconstructImageFromQuantizedVectors(decompressedVectors,
                                                                                         qVector,
                                                                                         header.getImageDims());
    
                    callback.process(decompressedPlane, planeIndex);
    
                } catch (final Exception ex) {
    
                    throw new ImageDecompressionException("VQImageDecompressor::decompressToBuffer() - Unable to read indices from " +
                                                                  "InBitStream.",
                                                          ex);
    
                }
                reportProgressToListeners(planeIndex, planeCountForDecompression,
    
                                          "Decompressed plane %d.", planeIndex);
    
        @SuppressWarnings("DuplicatedCode")
    
        public void decompressStreamModelImpl(final DataInputStream compressedStream,
                                              final QCMPFileHeader header,
                                              final DecompressCallback callback) throws ImageDecompressionException {
    
    
            assert (cachedCodebook != null && cachedHuffman != null);
            assert (header.getVectorSizeZ() == 1);
            final int planeCountForDecompression = header.getImageSizeZ();
            final long planeVectorCount = calculatePlaneVectorCount(header);
            final V2i qVector = new V2i(header.getVectorSizeX(), header.getVectorSizeY());
            final int vectorSize = qVector.multiplyTogether();
    
    
            for (int planeIndex = 0; planeIndex < planeCountForDecompression; planeIndex++) {
    
                final int planeDataSize = (int) header.getPlaneDataSizes()[planeIndex];
    
                try (final InBitStream inBitStream = new InBitStream(compressedStream,
                                                                     header.getBitsPerCodebookIndex(),
                                                                     planeDataSize)) {
    
                    inBitStream.readToBuffer();
                    inBitStream.setAllowReadFromUnderlyingStream(false);
    
    
                    final int[][] decompressedVectors = new int[(int) planeVectorCount][vectorSize];
    
                    int huffmanIndex;
                    for (int vecIndex = 0; vecIndex < planeVectorCount; vecIndex++) {
                        huffmanIndex = decodeHuffmanSymbol(cachedHuffman, inBitStream);
                        System.arraycopy(cachedCodebook.getVectors()[huffmanIndex], 0, decompressedVectors[vecIndex], 0, vectorSize);
                    }
    
    
                    final Block decompressedPlane = reconstructImageFromQuantizedVectors(decompressedVectors, qVector, header.getImageDims());
    
                    callback.process(decompressedPlane, planeIndex);
    
                } catch (final Exception ex) {
    
                    throw new ImageDecompressionException("VQImageDecompressor::decompressToBuffer() - Unable to read indices from " +
                                                                  "InBitStream.",
                                                          ex);
                }
                reportProgressToListeners(planeIndex, planeCountForDecompression,
                                          "Decompressed plane %d.", planeIndex);
            }
        }
    
    
        public void decompressToBuffer(final DataInputStream compressedStream,
                                       final short[][] buffer,
                                       final QCMPFileHeader header) throws ImageDecompressionException {
    
            if (header.getQuantizationType() == QuantizationType.Vector3D) {
                decompressVoxelsToBuffer(compressedStream, buffer, header);
                return;
            }
            decompressImpl(compressedStream, header, (imageBlock, planeIndex) -> {
                buffer[planeIndex] = TypeConverter.intArrayToShortArray(imageBlock.getData());
            });
        }
    
    
    
        private void decompressVoxelsImpl(final DataInputStream compressedStream,
                                          final QCMPFileHeader header,
                                          final DecompressVoxelCallback callback) throws ImageDecompressionException {
    
            assert (header.getQuantizationType() == QuantizationType.Vector3D);
            assert (!header.isCodebookPerPlane()); // SHOULD ALWAYS BE GLOBAL.
    
            final int codebookSize = (int) Math.pow(2, header.getBitsPerCodebookIndex());
            final V3i voxelDims = new V3i(header.getVectorSizeX(), header.getVectorSizeY(), header.getVectorSizeZ());
            final int vectorSize = (int) voxelDims.multiplyTogether();
    
            final int voxelLayerDepth = voxelDims.getZ();
    
            final int[] huffmanSymbols = createHuffmanSymbols(codebookSize);
    
    
            final VQCodebook codebook = readCodebook(compressedStream, codebookSize, vectorSize);
            final Huffman huffman = createHuffmanCoder(huffmanSymbols, codebook.getVectorFrequencies());
    
            final int voxelLayerCount = VQImageCompressor.calculateVoxelLayerCount(header.getImageSizeZ(), header.getVectorSizeZ());
    
            final Stopwatch stopwatch = new Stopwatch();
    
            for (int voxelLayerIndex = 0; voxelLayerIndex < voxelLayerCount; voxelLayerIndex++) {
                stopwatch.restart();
    
                final int fromZ = (voxelLayerIndex * voxelLayerDepth);
                final int toZ = (voxelLayerIndex == voxelLayerCount - 1)
                        ? header.getImageSizeZ()
                        : (voxelLayerDepth + (voxelLayerIndex * voxelLayerDepth));
                final V3i currentVoxelLayerDims = new V3i(header.getImageSizeX(), header.getImageSizeY(), toZ - fromZ);
                final int voxelLayerDataSize = (int) header.getPlaneDataSizes()[voxelLayerIndex];
                final int voxelLayerVoxelCount = Voxel.calculateRequiredVoxelCount(currentVoxelLayerDims, voxelDims);
    
    
                final int[][] decompressedVoxels = new int[voxelLayerVoxelCount][vectorSize];
    
                try (final InBitStream inBitStream = new InBitStream(compressedStream, header.getBitsPerCodebookIndex(), voxelLayerDataSize)) {
    
                    inBitStream.readToBuffer();
                    inBitStream.setAllowReadFromUnderlyingStream(false);
    
                    for (int voxelIndex = 0; voxelIndex < voxelLayerVoxelCount; voxelIndex++) {
                        final int huffmanSymbol = decodeHuffmanSymbol(huffman, inBitStream);
    
                        System.arraycopy(codebook.getVectors()[huffmanSymbol], 0, decompressedVoxels[voxelIndex], 0, vectorSize);
    
                } catch (final Exception e) {
    
                    throw new ImageDecompressionException("VQImageDecompressor::decompressVoxels() - Unable to read indices from InBitStream.",
                                                          e);
    
                final Voxel currentVoxel = new Voxel(currentVoxelLayerDims);
                callback.process(currentVoxel, decompressedVoxels, (voxelLayerIndex * voxelLayerDepth));
    
                if (options.isConsoleApplication()) {
                    reportStatusToListeners("Decompressed voxel layer %d/%d in %s",
                                            voxelLayerIndex, voxelLayerCount, stopwatch.getElapsedTimeString());
                } else {
                    reportProgressToListeners(voxelLayerIndex, voxelLayerCount,
                                              "Decompressed voxel layer %d/%d in %s",
                                              voxelLayerIndex, voxelLayerCount, stopwatch.getElapsedTimeString());
                }
    
        @SuppressWarnings("DuplicatedCode")
    
        private void decompressVoxelsStreamModeImpl(final DataInputStream compressedStream,
                                                    final QCMPFileHeader header,
                                                    final DecompressVoxelCallback callback) throws ImageDecompressionException {
    
    
            assert (header.getQuantizationType() == QuantizationType.Vector3D);
            assert (!header.isCodebookPerPlane()); // SHOULD ALWAYS BE GLOBAL.
    
    
            final V3i voxelDims = new V3i(header.getVectorSizeX(), header.getVectorSizeY(), header.getVectorSizeZ());
            final int vectorSize = (int) voxelDims.multiplyTogether();
            final int voxelLayerDepth = voxelDims.getZ();
    
    
            final int voxelLayerCount = VQImageCompressor.calculateVoxelLayerCount(header.getImageSizeZ(), header.getVectorSizeZ());
    
            final Stopwatch stopwatch = new Stopwatch();
    
            for (int voxelLayerIndex = 0; voxelLayerIndex < voxelLayerCount; voxelLayerIndex++) {
                stopwatch.restart();
    
                final int fromZ = (voxelLayerIndex * voxelLayerDepth);
                final int toZ = (voxelLayerIndex == voxelLayerCount - 1)
                        ? header.getImageSizeZ()
                        : (voxelLayerDepth + (voxelLayerIndex * voxelLayerDepth));
                final V3i currentVoxelLayerDims = new V3i(header.getImageSizeX(), header.getImageSizeY(), toZ - fromZ);
                final int voxelLayerDataSize = (int) header.getPlaneDataSizes()[voxelLayerIndex];
                final int voxelLayerVoxelCount = Voxel.calculateRequiredVoxelCount(currentVoxelLayerDims, voxelDims);
    
    
                final int[][] decompressedVoxels = new int[voxelLayerVoxelCount][vectorSize];
    
                try (final InBitStream inBitStream = new InBitStream(compressedStream, header.getBitsPerCodebookIndex(), voxelLayerDataSize)) {
    
                    inBitStream.readToBuffer();
                    inBitStream.setAllowReadFromUnderlyingStream(false);
    
                    for (int voxelIndex = 0; voxelIndex < voxelLayerVoxelCount; voxelIndex++) {
                        final int huffmanSymbol = decodeHuffmanSymbol(cachedHuffman, inBitStream);
                        System.arraycopy(cachedCodebook.getVectors()[huffmanSymbol], 0, decompressedVoxels[voxelIndex], 0, vectorSize);
                    }
    
    
                } catch (final Exception e) {
    
                    throw new ImageDecompressionException("VQImageDecompressor::decompressVoxels() - Unable to read indices from InBitStream.",
                                                          e);
                }
    
                final Voxel currentVoxel = new Voxel(currentVoxelLayerDims);
                callback.process(currentVoxel, decompressedVoxels, (voxelLayerIndex * voxelLayerDepth));
    
                stopwatch.stop();
                if (options.isConsoleApplication()) {
                    reportStatusToListeners("Decompressed voxel layer %d/%d in %s",
                                            voxelLayerIndex, voxelLayerCount, stopwatch.getElapsedTimeString());
                } else {
                    reportProgressToListeners(voxelLayerIndex, voxelLayerCount,
                                              "Decompressed voxel layer %d/%d in %s",
                                              voxelLayerIndex, voxelLayerCount, stopwatch.getElapsedTimeString());
                }
            }
        }
    
    
        private void decompressVoxelsToBuffer(final DataInputStream compressedStream,
                                              final short[][] buffer,
                                              final QCMPFileHeader header) throws ImageDecompressionException {
    
    
            final V3i voxelDims = new V3i(header.getVectorSizeX(), header.getVectorSizeY(), header.getVectorSizeZ());
    
            decompressVoxelsImpl(compressedStream, header, (decompressedVoxel, decompressedVoxelData, planeOffset) ->
                    decompressedVoxel.reconstructFromVoxels(voxelDims, decompressedVoxelData, buffer, planeOffset));
        }
    
    
        private void decompressVoxels(final DataInputStream compressedStream,
                                      final DataOutputStream decompressStream,
                                      final QCMPFileHeader header) throws ImageDecompressionException {
    
    
            final V3i voxelDims = new V3i(header.getVectorSizeX(), header.getVectorSizeY(), header.getVectorSizeZ());
            decompressVoxelsImpl(compressedStream, header, (voxel, voxelData, planeOffset) -> {
    
    
                final ImageU16Dataset currentVoxelLayer = voxel.reconstructFromVoxelsToDataset(voxelDims, voxelData);
    
    
                for (int layer = 0; layer < voxel.getDims().getZ(); layer++) {
                    try {
                        decompressStream.write(TypeConverter.unsignedShortArrayToByteArray(currentVoxelLayer.getPlaneData(layer), false));
    
                    } catch (final IOException e) {
    
                        throw new ImageDecompressionException("Unable to write to decompress stream.", e);
                    }
                }
            });
        }
    
    
        private int decodeHuffmanSymbol(final Huffman huffman, final InBitStream inBitStream) throws IOException {
    
            HuffmanNode currentHuffmanNode = huffman.getRoot();
            while (!currentHuffmanNode.isLeaf()) {
                currentHuffmanNode = currentHuffmanNode.traverse(inBitStream.readBit());
            }
            return currentHuffmanNode.getSymbol();
        }
    
    
    
        @Override
        public short[] decompressStreamMode(final DataInputStream compressedStream,
                                            final QCMPFileHeader header) throws ImageDecompressionException {
            final short[] buffer = new short[(int) header.getImageDims().multiplyTogether()];
    
            if (header.getQuantizationType() == QuantizationType.Vector3D) {
                final V3i voxelDim = new V3i(header.getVectorSizeX(), header.getVectorSizeY(), header.getVectorSizeZ());
    
                decompressVoxelsStreamModeImpl(compressedStream, header, (voxel, voxelData, planeOffset) -> {
    
                    final ImageU16Dataset decompressedVoxel = voxel.reconstructFromVoxelsToDataset(voxelDim, voxelData);
                    assert (decompressedVoxel.getPlaneCount() == voxel.getDims().getZ());
                    final int expectedVoxelPlaneSize = header.getImageSizeX() * header.getImageSizeY();
    
                    final int baseOffset = planeOffset * expectedVoxelPlaneSize;
    
                    for (int voxelLayerIndex = 0; voxelLayerIndex < decompressedVoxel.getPlaneCount(); voxelLayerIndex++) {
                        final short[] voxelLayerData = decompressedVoxel.getPlaneData(voxelLayerIndex);
                        assert (voxelLayerData.length == expectedVoxelPlaneSize);
                        final int bufferPos = baseOffset + (voxelLayerIndex * expectedVoxelPlaneSize);
                        System.arraycopy(voxelLayerData, 0, buffer, bufferPos, voxelLayerData.length);
    
                    }
                });
                return buffer;
            } else {
                final int planePixelCount = header.getImageDims().toV2i().multiplyTogether();
                decompressStreamModelImpl(compressedStream, header, (imageBlock, planeIndex) -> {
                    final int offset = planePixelCount * planeIndex;
                    final int[] data = imageBlock.getData();
                    for (int i = 0; i < planePixelCount; i++) {
                        buffer[offset + i] = (short) data[i];
                    }
                });
            }