Skip to content

Instantly share code, notes, and snippets.

Created February 13, 2016 11:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save anonymous/989ab8a1bb6ec14f6ea9 to your computer and use it in GitHub Desktop.
Save anonymous/989ab8a1bb6ec14f6ea9 to your computer and use it in GitHub Desktop.
package CompressAnImageToA4kibPreview;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.RenderingHints;
import java.awt.RenderingHints.Key;
import java.awt.geom.Path2D;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.zip.Deflater;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import javax.imageio.ImageIO;
/**
* Licensed under Revised BSD License:
*
* Copyright (c) 2016, Nicholas Klaebe
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the Nicholas Klaebe nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Nicholas Klaebe
*
*/
public class CompressAnImageToA4kibPreview
{
/**
* InputStream that allows 1-63 bitwise writes
* @author default
*
*/
static class BitInputStream
{
InputStream fis;
int count;
long totalCount;
byte value;
byte[] buffer;
int buffCount;
int buffSize;
boolean returnVal;
private static final int EOF_INT = -1;
/**
* Constructor
*
* @param fis1
* The base inputStream
* @param size
* size of the input buffer
*/
public BitInputStream(InputStream fis1, int size)
{
count = 0;
buffer = new byte[size];
buffCount = 0;
totalCount = 0;
fis = fis1;
}
/**
* read in a single bit
*
* @return bit read
* @throws IOException
*/
public boolean read() throws IOException
{
if (count == 0)
{
if (buffCount == 0)
{
buffSize = 0;
while (buffSize == 0)
{
buffSize = fis.read(buffer, 0, buffer.length);
}
if (buffSize == EOF_INT)
{
throw new EOFException("END OF FILE");
}
}
value = buffer[buffCount];
buffCount++;
if (buffCount == buffSize)
buffCount = 0;
}
if ((value >> (7 - count) & 0x01) > 0)
returnVal = true;
else
returnVal = false;
count++;
totalCount++;
if (count == 8)
count = 0;
return returnVal;
}
/**
* reads in the next char, if not on a 8bit boundary then the stream is aligned to the next byte
*
* @return byte read
* @throws IOException
*/
public char readChar() throws IOException
{
byte[] b = new byte[1];
alignToNextByte();
fis.read(b);
totalCount += 8;
return (char) b[0];
}
/**
* aligns the bit stream to the next byte boundary
*
* @throws IOException
*/
public void alignToNextByte() throws IOException
{
while (count != 0)
{
read();
}
}
/**
* read in a specified number of bits
*
* @param bits
* the number of bits to be read (between 1 and 64)
* @return a long integer containg the bits read
* @throws IOException
*/
public long read(int bits) throws IOException
{
long val = 0;
int mask = 0x01;
boolean[] bitsRead = new boolean[bits];
for (int i = 0; i < bits; i++)
{
bitsRead[i] = read();
}
for (int j = bits - 1; j > -1; j--)
{
val = val << 1;
if (bitsRead[j])
{
val = val | mask;
}
}
return val;
}
/**
* attempts to read bytes into the array given
*
* @param temp
* the array being populated
* @param start
* the starting index
* @param end
* the ending index
* @return the bytes read in
* @throws IOException
*/
public int read(byte[] temp, int start, int end) throws IOException
{
totalCount += (end - start) * 8;
return fis.read(temp, start, end);
}
public int read(byte[] temp) throws IOException
{
totalCount += temp.length * 8;
return fis.read(temp);
}
public int readByte() throws IOException
{
totalCount += 8;
return fis.read();
}
public void close() throws IOException
{
fis.close();
}
}
/**
* OutputStream that allows 1-63 bitwise reads
*/
static public class BitOutputStream
{
int value; // The byte in which the encoded bits are firstly stored.
int count; // The number of bits written into value.
byte[] buffer; // A byte buffer which is filled with 'value' each time value is full. Used for wirting to file.
int buffCount; // The current number of 'values' written into the buffer.
long masterCount; // The overall count of bits that have been written
OutputStream fos;
/**
* constructor
*
* @param fos1
* The outputstream which this bit stream writes to
*/
public BitOutputStream(OutputStream fos1)
{
fos = fos1;
value = 0;
count = 0;
buffer = new byte[4096];
buffCount = 0;
masterCount = 0;
}
/**
* Writes the passed value (temp) to the file using the given number of bits
*
* @param temp
* the value to be written
* @param bits
* the number if bits to write
* @throws IOException
*/
public void write(long temp, int bits) throws IOException
{
for (int j = 0, mask = 1; j < bits; j++, mask <<= 1)
{
value = value << 1;
count++;
if ((temp & mask) > 0)
{
value = value | 0x01;
}
addToBuffer();
}
}
/**
* write a single bit to the stream
*
* @param bit
* The bit to write
* @throws IOException
*/
public void write(boolean bit) throws IOException
{
value = value << 1;
count++;
if (bit)
{
value = value | 0x01;
}
addToBuffer();
}
/**
* writes a single char (converted to a byte) to the output stream aligning with the next byte boundary
*
* @param c
* the char to write
* @throws IOException
*/
public void write(char c) throws IOException
{
flush();
byte[] b = new byte[1];
b[0] = (byte) c;
fos.write(b);
masterCount += 8;
}
/**
* adds bits stored in 'value' to a buffer which will be saved to a file if the current bit count since last storing into the buffer is less than 8 then return without adding it to the buffer
*
* @throws IOException
*/
public void addToBuffer() throws IOException
{
masterCount++;
if (count < 8)
return;
// byte temp=(byte) (value);
buffer[buffCount] = (byte) (value);
;
buffCount++;
if (buffCount == buffer.length)
{
fos.write(buffer, 0, buffCount);
buffCount = 0;
}
value = 0;
count = 0;
}
/**
* writes a single byte to the output stream aligning with the next byte boundary
*
* @param b
* the byte to write
* @throws IOException
*/
public void write(byte[] b) throws IOException
{
flush();
fos.write(b);
}
/**
* writes a single byte to the output stream aligning with the next byte boundary
*
* @param b
* the byte to write
* @throws IOException
*/
public void write(byte b) throws IOException
{
flush();
fos.write(b);
}
public void write(byte[] b, int start, int count) throws IOException
{
flush();
fos.write(b, start, count);
}
/**
* align the output stream with the next byte boundary
*
* @throws IOException
*/
public void flush() throws IOException
{
// pad out the last byte if necessary
if (count > 0)
{
masterCount += (8 - count - 1);
value = value << (8 - count);
count = 8;
addToBuffer();
}
if (buffCount > 0)
{
fos.write(buffer, 0, buffCount);
buffCount = 0;
}
}
/**
* close the output stream
*
* @throws IOException
*/
public void close() throws IOException
{
flush();
fos.close();
}
}
static private final Random rand = new Random(0);
static private final int IMAGE_DIMENSION_BITS = 16;
static private final int LUMA_BLOCK_SIZE = 3; // 4 //4 //4 //4 //3
static private final int LUMA_SAMPLED_DIMENSION_BLOCKS = 32 * 2;// 18 //30 //16 //16 //8
static private final int LUMA_SAMPLED_DIMENSION = LUMA_BLOCK_SIZE * LUMA_SAMPLED_DIMENSION_BLOCKS;
static private final int MIN_SAMPLED_DIMENSION = LUMA_SAMPLED_DIMENSION_BLOCKS / 4;
static private final int MAX_SAMPLED_DIMENSION_SCALAR_BITS = 8;
static private final int MAX_SAMPLED_DIMENSION_SCALAR = (int) Math.pow(2, MAX_SAMPLED_DIMENSION_SCALAR_BITS);
static private final int LUMA_FILTER_BIT_DEPTH = 10;// 10;//16 //16 //12 //12
static private final int LUMA_FILTER_COUNT = (int) Math.pow(2, LUMA_FILTER_BIT_DEPTH);
static private final int LUMA_BLOCK_SAMPLE_SIZE = LUMA_FILTER_COUNT * 10; // 10000
static private final int LUMA_FILTERS_IMAGE_DIMENSION = (int) (Math.ceil(Math.sqrt(LUMA_FILTER_COUNT)));
static private final int CHROMA_BLOCK_SIZE = LUMA_BLOCK_SIZE * 2; // 8 //8 //8 //6
static private final int CHROMA_SAMPLED_DIMENSION_BLOCKS = LUMA_SAMPLED_DIMENSION_BLOCKS / 2; // 15 //8 //8 //4
static private final int CHROMA_SAMPLED_DIMENSION = CHROMA_BLOCK_SIZE * CHROMA_SAMPLED_DIMENSION_BLOCKS;
static private final int CHROMA_FILTER_BIT_DEPTH = 10;// 12;//10 //10 //7 //7
static private final int CHROMA_FILTER_COUNT = (int) Math.pow(2, CHROMA_FILTER_BIT_DEPTH);
static private final int CHROMA_BLOCK_SAMPLE_SIZE = CHROMA_FILTER_COUNT * 10; // 10000
static private final int CHROMA_FILTERS_IMAGE_DIMENSION = (int) (Math.ceil(Math.sqrt(CHROMA_FILTER_COUNT)));
private static final int LUMA_SIMILARITY_BIT_DEPTH = LUMA_FILTER_BIT_DEPTH;// 0;//2;//5;
private static final int LUMA_SIMILARITY_COUNT = (int) Math.pow(2, LUMA_SIMILARITY_BIT_DEPTH);
private static final int CHROMA_SIMILARITY_BIT_DEPTH = CHROMA_FILTER_BIT_DEPTH;// 0;//2;//5;
private static final int CHROMA_SIMILARITY_COUNT = (int) Math.pow(2, CHROMA_SIMILARITY_BIT_DEPTH);
static private final int LUMA = 0;
static private final int CHROMA_U = 1;
static private final int CHROMA_V = 2;
static private final float EIGHT_BIT_DIVISOR = 1.0F / 256;
static private final int GENERATE_NONE_ENUM = 0;
static private final int GENERATE_ONLY_IF_NEEDED_ENUM = 1;
static private final int GENERATE_ALWAYS_ENUM = 2;
private static int GENERATE = GENERATE_ONLY_IF_NEEDED_ENUM;
private static final boolean CLUSTER_LUMA = true;
private static final boolean CLUSTER_CHROMA = true;
private static final int DESIRED_COMPRESSED_SIZE_BYTES = 4096;
private static final boolean OUTPUT_SAMPLE_IMAGES = false;
/**
* FilterManager manages the lumenosity and chromenosity filters including creation, writing and reading.
* @author default
*
*/
static final class FilterManager
{
private static final int MAX_SAMPLE_IMAGE_SHAPE_SELECTOR = 20;
private static final int SAMPLE_IMAGE_SUBSHAPE_STROKE_DIVISOR = 20;
private static final float MIN_SAMPLE_IMAGE_SUBSHAPE_STROKE = 0.5F;
private static final int MIN_SAMPLE_IMAGE_SUBSHAPE_SIZE = 10;
private static final int SAMPLE_IMAGE_COUNT = 25;
Block[] lumaFilters;
Block[] chromaFilters;
int[][] lumaFilterSimilarityOrdering;
int[][] chromaFilterSimilarityOrdering;
private void loadFilters() throws IOException
{
lumaFilters = new Block[LUMA_FILTER_COUNT];
chromaFilters = new Block[CHROMA_FILTER_COUNT];
BufferedImage filtersImage = ImageIO.read(new File("data/images/working/lumaFilters_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".png"));
for (int i = 0; i < LUMA_FILTER_COUNT; i++)
{
float[][][] pixels = convertToYUVImage(filtersImage.getSubimage((i % LUMA_FILTERS_IMAGE_DIMENSION) * (LUMA_BLOCK_SIZE + 2), (i / LUMA_FILTERS_IMAGE_DIMENSION) * (LUMA_BLOCK_SIZE + 2), LUMA_BLOCK_SIZE, LUMA_BLOCK_SIZE));
lumaFilters[i] = new Block(pixels, 0, 0, LUMA_BLOCK_SIZE);
lumaFilters[i].closestMatchingFilterId = i;
}
filtersImage = ImageIO.read(new File("data/images/working/chromaFilters_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".png"));
for (int i = 0; i < CHROMA_FILTER_COUNT; i++)
{
float[][][] pixels = convertToYUVImage(filtersImage.getSubimage((i % CHROMA_FILTERS_IMAGE_DIMENSION) * (CHROMA_BLOCK_SIZE + 2), (i / CHROMA_FILTERS_IMAGE_DIMENSION) * (CHROMA_BLOCK_SIZE + 2), CHROMA_BLOCK_SIZE, CHROMA_BLOCK_SIZE));
chromaFilters[i] = new Block(pixels, 0, 0, CHROMA_BLOCK_SIZE);
chromaFilters[i].closestMatchingFilterId = i;
}
}
private void loadSimilarityOrdering() throws IOException
{
DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(new File("data/images/working/ordering_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".dat"))));
if (LUMA_SIMILARITY_BIT_DEPTH > 0)
{
lumaFilterSimilarityOrdering = new int[LUMA_FILTER_COUNT][];
for (int i = 0; i < lumaFilterSimilarityOrdering.length; i++)
{
lumaFilterSimilarityOrdering[i] = new int[LUMA_SIMILARITY_COUNT];
for (int j = 0; j < LUMA_SIMILARITY_COUNT; j++)
{
lumaFilterSimilarityOrdering[i][j] = dis.readInt();
}
}
}
if (CHROMA_SIMILARITY_BIT_DEPTH > 0)
{
chromaFilterSimilarityOrdering = new int[CHROMA_FILTER_COUNT][];
for (int i = 0; i < chromaFilterSimilarityOrdering.length; i++)
{
chromaFilterSimilarityOrdering[i] = new int[CHROMA_SIMILARITY_COUNT];
for (int j = 0; j < CHROMA_SIMILARITY_COUNT; j++)
{
chromaFilterSimilarityOrdering[i][j] = dis.readInt();
}
}
}
dis.close();
}
private void performSimilarityOrdering() throws IOException
{
System.out.println("Creating similarity ordered Lists...");
DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(new File("data/images/working/ordering_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".dat"))));
if (LUMA_SIMILARITY_BIT_DEPTH > 0)
{
lumaFilterSimilarityOrdering = new int[LUMA_FILTER_COUNT][];
for (int i = 0; i < lumaFilterSimilarityOrdering.length; i++)
{
lumaFilterSimilarityOrdering[i] = order(lumaFilters, lumaFilters[i], LumaComparator.comparator, LUMA_SIMILARITY_COUNT);
}
for (int i = 0; i < lumaFilterSimilarityOrdering.length; i++)
{
for (int j = 0; j < LUMA_SIMILARITY_COUNT; j++)
{
dos.writeInt(lumaFilterSimilarityOrdering[i][j]);
}
}
}
if (CHROMA_SIMILARITY_BIT_DEPTH > 0)
{
chromaFilterSimilarityOrdering = new int[CHROMA_FILTER_COUNT][];
for (int i = 0; i < chromaFilterSimilarityOrdering.length; i++)
{
chromaFilterSimilarityOrdering[i] = order(chromaFilters, chromaFilters[i], ChromaComparator.comparator, CHROMA_SIMILARITY_COUNT);
}
for (int i = 0; i < chromaFilterSimilarityOrdering.length; i++)
{
for (int j = 0; j < CHROMA_SIMILARITY_COUNT; j++)
{
dos.writeInt(chromaFilterSimilarityOrdering[i][j]);
}
}
}
dos.close();
System.out.println("Creating similarity ordered Lists - Done");
}
/**
* Performs similarity ordering of the given filter type as compared with the given filter.
*
* @return array of similarity ordered indices into the filter list.
*/
static private int[] order(Block[] filters, final Block comparisionFilter, final BlockComparator comparator, final int similarityCount)
{
final class Tuple implements Comparable<Tuple>
{
public Tuple(float difference, int id)
{
this.difference = difference;
this.id = id;
}
float difference;
int id;
@Override
public int compareTo(Tuple other)
{
float diff = difference - other.difference;
return diff < 0 ? -1 : (diff > 0 ? 1 : 0);
}
}
ArrayList<Tuple> tuples = new ArrayList<>();
for (int i = 0; i < filters.length; i++)
{
tuples.add(new Tuple(comparator.calculateDifference(filters[i], comparisionFilter), i));
}
Collections.sort(tuples);
int[] orderedIndecies = new int[similarityCount];
for (int i = 0; i < similarityCount; i++)
{
orderedIndecies[i] = tuples.get(i).id;
}
return orderedIndecies;
}
private void generateFilters() throws IOException
{
System.out.println("First time running (no filters found). Generating Sample Images...");
List<float[][][]> sampleImages = new ArrayList<float[][][]>();
BufferedImage sampleImage = new BufferedImage(LUMA_SAMPLED_DIMENSION, LUMA_SAMPLED_DIMENSION, BufferedImage.TYPE_INT_RGB);
BufferedImage image1 = new BufferedImage(1024, 1024, BufferedImage.TYPE_INT_RGB);
// generate sample images and convert and down sample them into YUV images
for (int j = 0; j < SAMPLE_IMAGE_COUNT; j++)
{
final int MAX_SHAPE_SIZE = MIN_SAMPLE_IMAGE_SUBSHAPE_SIZE + image1.getWidth() / 5;
Graphics2D g = (Graphics2D) image1.getGraphics();
Color colour1 = new Color(Color.HSBtoRGB(rand.nextFloat(), rand.nextFloat(), (float) Math.log(0.001 + rand.nextFloat() * 10)));
Color colour2 = new Color(Color.HSBtoRGB(rand.nextFloat(), rand.nextFloat(), (float) Math.log(0.001 + rand.nextFloat() * 10)));
GradientPaint gradientPaint = new GradientPaint(0, 0, colour1, 1024, 1024, colour2);
g.setPaint(gradientPaint);
g.fillRect(0, 0, image1.getWidth(), image1.getHeight());
for (int i = 0; i < 200; i++)
{
colour1 = new Color(Color.HSBtoRGB(rand.nextFloat(), rand.nextFloat(), (float) Math.log(0.001 + rand.nextFloat() * 10)));
colour2 = new Color(Color.HSBtoRGB(rand.nextFloat(), rand.nextFloat(), (float) Math.log(0.001 + rand.nextFloat() * 10)));
g.setStroke(new BasicStroke(MIN_SAMPLE_IMAGE_SUBSHAPE_STROKE + rand.nextFloat() * image1.getWidth() / SAMPLE_IMAGE_SUBSHAPE_STROKE_DIVISOR));
int x = rand.nextInt(image1.getWidth() + MAX_SHAPE_SIZE / 2) - MAX_SHAPE_SIZE / 2;
int y = rand.nextInt(image1.getHeight() - MAX_SHAPE_SIZE) + MAX_SHAPE_SIZE / 2;
int w = rand.nextInt(MAX_SHAPE_SIZE);
int h = rand.nextInt(MAX_SHAPE_SIZE);
gradientPaint = new GradientPaint(x, y, colour1, x + w, y + h, colour2);
g.setPaint(gradientPaint);
switch (rand.nextInt(MAX_SAMPLE_IMAGE_SHAPE_SELECTOR))
{
case 0:
g.fillRect(x, y, w, h);
break;
case 1:
g.drawRect(x, y, w, h);
break;
case 2:
g.drawOval(x, y, w, h);
break;
case 3:
g.fillOval(x, y, w, h);
break;
case 4:
g.drawLine(x, y, x + w, y + h);
break;
default:
Path2D prettyPoly = new Path2D.Double();
boolean isFirst = true;
for (int points = 0; points < 3 + rand.nextInt(MAX_SHAPE_SIZE); points++)
{
double xx = rand.nextDouble() * MAX_SHAPE_SIZE;
double yy = rand.nextDouble() * MAX_SHAPE_SIZE;
if (isFirst)
{
prettyPoly.moveTo(xx, yy);
isFirst = false;
}
else
{
prettyPoly.lineTo(xx, yy);
}
}
prettyPoly.closePath();
g.translate(x, y);
gradientPaint = new GradientPaint(0, 0, colour1, MAX_SHAPE_SIZE, MAX_SHAPE_SIZE, colour2);
g.setPaint(gradientPaint);
g.fill(prettyPoly);
g.translate(-x, -y);
break;
}
}
if (OUTPUT_SAMPLE_IMAGES)
{
File sampleFile = new File("data/images/working/sample" + j + ".png");
sampleFile.getParentFile().mkdirs();
ImageIO.write(image1, "png", sampleFile);
}
sampleImage.getGraphics().drawImage(image1.getScaledInstance(LUMA_SAMPLED_DIMENSION, LUMA_SAMPLED_DIMENSION, BufferedImage.SCALE_AREA_AVERAGING), 0, 0, null);
float[][][] pixels = convertToYUVImage(sampleImage);
sampleImages.add(pixels);
}
System.out.println("Generating Sample Images Completed.");
// perform k-clustering for lumenosity and 'chromanosity'
lumaFilters = clusterLuma(sampleImages);
chromaFilters = new Block[0];
if (!(CHROMA_FILTER_COUNT == 0 || CHROMA_SAMPLED_DIMENSION == 0))
{
chromaFilters = clusterChroma(sampleImages);
}
// construct and save clustered lumenosity filters image
BufferedImage lumaFiltersImage = new BufferedImage(LUMA_FILTERS_IMAGE_DIMENSION * (LUMA_BLOCK_SIZE + 2), LUMA_FILTERS_IMAGE_DIMENSION * (LUMA_BLOCK_SIZE + 2), BufferedImage.TYPE_INT_RGB);
BufferedImage lumaFilterImage = new BufferedImage(LUMA_BLOCK_SIZE, LUMA_BLOCK_SIZE, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < lumaFilters.length; i++)
{
renderYUV(lumaFilterImage, lumaFilters[i].pixels);
lumaFiltersImage.getGraphics().drawImage(lumaFilterImage, (i % LUMA_FILTERS_IMAGE_DIMENSION) * (LUMA_BLOCK_SIZE + 2), (i / LUMA_FILTERS_IMAGE_DIMENSION) * (LUMA_BLOCK_SIZE + 2), null);
}
new File("data/images/working").mkdirs();
ImageIO.write(lumaFiltersImage, "png", new File("data/images/working/lumaFilters_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".png"));
// construct and save clustered 'chromanosity' filters image
BufferedImage chromafiltersImage = new BufferedImage(CHROMA_FILTERS_IMAGE_DIMENSION * (CHROMA_BLOCK_SIZE + 2), CHROMA_FILTERS_IMAGE_DIMENSION * (CHROMA_BLOCK_SIZE + 2), BufferedImage.TYPE_INT_RGB);
BufferedImage chromafilterImage = new BufferedImage(CHROMA_BLOCK_SIZE, CHROMA_BLOCK_SIZE, BufferedImage.TYPE_INT_RGB);
for (int i = 0; i < chromaFilters.length; i++)
{
renderYUV(chromafilterImage, chromaFilters[i].pixels);
chromafiltersImage.getGraphics().drawImage(chromafilterImage, (i % CHROMA_FILTERS_IMAGE_DIMENSION) * (CHROMA_BLOCK_SIZE + 2), (i / CHROMA_FILTERS_IMAGE_DIMENSION) * (CHROMA_BLOCK_SIZE + 2), null);
}
ImageIO.write(chromafiltersImage, "png", new File("data/images/working/chromaFilters_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".png"));
}
/**
* Performs K-clustering of samples blocks to produce an array of filter blocks.
*
* @param sampleImages
* the collection of YUV down sampled sample images to obtain random blocks from
* @param sampledDimension
* the dimension of the down sampled image (width == height)
* @param blockSize
* the dimension of the filter block (width == height)
* @param filterCount
* the number of filters to cluster for
* @param blockSampleSize
* the number of blocks to cluster
* @param comparator
* the comparator to use when determining magnitude of difference between two blocks
* @return array of filters
*/
static private Block[] cluster(List<float[][][]> sampleImages, final int sampledDimension, final int blockSize, final int filterCount, final int blockSampleSize, BlockComparator comparator, boolean performCluster)
{
Block[] filters = new Block[filterCount];
Block[] samples = new Block[blockSampleSize];
for (int i = 0; i < blockSampleSize; i++)
{
samples[i] = new Block(sampleImages.get(rand.nextInt(sampleImages.size())), rand.nextInt(sampledDimension - blockSize), rand.nextInt(sampledDimension - blockSize), blockSize);
}
// select initial filters
for (int i = 0; i < filterCount; i++)
{
filters[i] = new Block(samples[i]);
filters[i].closestMatchingFilterId = i;
}
if (performCluster)
{
int iteration = 0;
boolean converged = false;
long stopTime = System.currentTimeMillis() + 1000 * 60 * 60;
// iterate until all blocks have converged into clusters or timeout occurs
while (!converged && System.currentTimeMillis() < stopTime)
{
iteration++;
System.out.print("iteration: " + iteration);
converged = true;
int blockSwapClusterCount = 0;
// identify closest cluster mid point for each sample block
for (int i = 0; i < blockSampleSize; i++)
{
Block block = samples[i];
float bestDifference = Float.MAX_VALUE;
int bestFilterIndex = -1;
for (int j = 0; j < filterCount; j++)
{
float difference = comparator.calculateDifference(block, filters[j]);
if (difference < bestDifference)
{
bestDifference = difference;
bestFilterIndex = j;
}
}
// sample block has changed to a different cluster or the assigned cluster's collection has been updated
if (bestDifference != block.closestDifference || bestFilterIndex != block.closestMatchingFilterId)
{
blockSwapClusterCount++;
converged = false;
block.closestMatchingFilterId = bestFilterIndex;
block.closestDifference = bestDifference;
}
}
if (!converged)
{
int orphanedClusterCount = 0;
// determine new cluster centre (i.e. filter)
for (int j = 0; j < filterCount; j++)
{
int clusterSize = 0;
Block filter = filters[j];
filter.zeroise();
for (int i = 0; i < blockSampleSize; i++)
{
if (samples[i].closestMatchingFilterId == j)
{
clusterSize++;
filter.add(samples[i]);
}
}
// the cluster no longer has any members... lets choose another random sample to be our this cluster centre
if (clusterSize == 0)
{
orphanedClusterCount++;
filter = new Block(samples[rand.nextInt(blockSampleSize)]);
filter.closestMatchingFilterId = j;
filters[j] = filter;
}
else
{
filter.divide(clusterSize);
}
}
if (orphanedClusterCount > 0)
{
System.out.print(" orphanedClusterCount: " + orphanedClusterCount);
}
}
if (blockSwapClusterCount > 0)
{
System.out.print(" blockSwapClusterCount: " + blockSwapClusterCount);
}
System.out.println();
}
System.out.println("Clustering complete");
}
return filters;
}
/**
* Perform k-clustering based on pixel lumenosity to derive lumenosity filters
*
* @param sampleImages
* the collection of YUV down sampled sample images to obtain random blocks from
* @return array of filters
*/
static private Block[] clusterLuma(List<float[][][]> sampleImages)
{
if (CLUSTER_LUMA)
{
System.out.println("K-Cluster Luma filters...");
}
return cluster(sampleImages, LUMA_SAMPLED_DIMENSION, LUMA_BLOCK_SIZE, LUMA_FILTER_COUNT, LUMA_BLOCK_SAMPLE_SIZE, LumaComparator.comparator, CLUSTER_LUMA);
}
/**
* Perform k-clustering based on pixel 'chromanosity' to derive 'chromanosity' filters
*
* @param sampleImages
* the collection of YUV down sampled sample images to obtain random blocks from
* @return array of filters
*/
static private Block[] clusterChroma(List<float[][][]> sampleImages)
{
if (CLUSTER_CHROMA)
{
System.out.println("K-Cluster Chroma filters...");
}
return cluster(sampleImages, CHROMA_SAMPLED_DIMENSION, CHROMA_BLOCK_SIZE, CHROMA_FILTER_COUNT, CHROMA_BLOCK_SAMPLE_SIZE, ChromaComparator.comparator, CLUSTER_CHROMA);
}
public static FilterManager create() throws IOException
{
FilterManager filterManager = new FilterManager();
filterManager.generateFilters();
filterManager.performSimilarityOrdering();
return filterManager;
}
public static FilterManager load() throws IOException
{
FilterManager filterManager = new FilterManager();
filterManager.loadFilters();
filterManager.loadSimilarityOrdering();
return filterManager;
}
}
/**
* A square block holding YUV pixels
*/
public static final class Block
{
float[][][] pixels;
int closestMatchingFilterId = -1;
float closestDifference = -1;
final int blockSize;
public Block(float[][][] sourceImage, int x, int y, final int blockSize)
{
this.blockSize = blockSize;
pixels = new float[blockSize][blockSize][3];
for (int i = 0; i < blockSize; i++)
{
for (int j = 0; j < blockSize; j++)
{
float[] pixel = sourceImage[x + j][y + i];
pixels[j][i][LUMA] = pixel[LUMA];
pixels[j][i][CHROMA_U] = pixel[CHROMA_U];
pixels[j][i][CHROMA_V] = pixel[CHROMA_V];
}
}
}
/**
* Copy Constructor
*
* @param other
*/
public Block(Block other)
{
this.blockSize = other.blockSize;
pixels = new float[blockSize][blockSize][3];
for (int i = 0; i < blockSize; i++)
{
for (int j = 0; j < blockSize; j++)
{
pixels[j][i][LUMA] = other.pixels[j][i][LUMA];
pixels[j][i][CHROMA_U] = other.pixels[j][i][CHROMA_U];
pixels[j][i][CHROMA_V] = other.pixels[j][i][CHROMA_V];
}
}
}
/**
* sets all pixel colour components to 0
*/
public void zeroise()
{
pixels = new float[blockSize][blockSize][3];
}
/**
* adds each pixel's colour components to this block's corresponding pixel colour components
*
* @param other
* the other block to add
*/
public void add(Block other)
{
for (int i = 0; i < blockSize; i++)
{
for (int j = 0; j < blockSize; j++)
{
pixels[j][i][LUMA] += other.pixels[j][i][LUMA];
pixels[j][i][CHROMA_U] += other.pixels[j][i][CHROMA_U];
pixels[j][i][CHROMA_V] += other.pixels[j][i][CHROMA_V];
}
}
}
/**
* divides all pixels colour components by the provided divisor
*
* @param divisor
*/
public void divide(float divisor)
{
for (int i = 0; i < blockSize; i++)
{
for (int j = 0; j < blockSize; j++)
{
pixels[j][i][LUMA] /= divisor;
pixels[j][i][CHROMA_U] /= divisor;
pixels[j][i][CHROMA_V] /= divisor;
}
}
}
}
/**
* Abstract Class providing the method signature to calculate the difference between blocks
*/
public static abstract class BlockComparator
{
/**
* Calculates and returns the magnitude of difference between the two provided blocks. The result has only has meaning when comparing against other results obtained by calling this method.
*
* @param block1
* @param block2
* @return the magnitude of difference between the two provided blocks
*/
abstract public float calculateDifference(Block block1, Block block2);
}
/**
* Concrete implementation of BlockComparator with the difference calculated based on lumenosity
*/
public static abstract class LumaComparator extends BlockComparator
{
public static final LumaComparator comparator = new LumaComparator()
{
public float calculateDifference(Block block1, Block block2)
{
float diffMetric = 0;
for (int i = 0; i < LUMA_BLOCK_SIZE; i++)
{
for (int j = 0; j < LUMA_BLOCK_SIZE; j++)
{
diffMetric += (block1.pixels[j][i][LUMA] - block2.pixels[j][i][LUMA]) * (block1.pixels[j][i][LUMA] - block2.pixels[j][i][LUMA]);
}
}
return diffMetric;
}
};
}
/**
* Concrete implementation of BlockComparator with the difference calculated based on 'chromanosity'
*/
public static abstract class ChromaComparator extends BlockComparator
{
public static final ChromaComparator comparator = new ChromaComparator()
{
public float calculateDifference(Block block1, Block block2)
{
float diff;
float diffMetric = 0;
for (int i = 0; i < CHROMA_BLOCK_SIZE; i++)
{
for (int j = 0; j < CHROMA_BLOCK_SIZE; j++)
{
diff = (block1.pixels[j][i][CHROMA_U] - block2.pixels[j][i][CHROMA_U]);
diffMetric += diff * diff * diff * diff;
diff = (block1.pixels[j][i][CHROMA_V] - block2.pixels[j][i][CHROMA_V]);
diffMetric += diff * diff * diff * diff;
}
}
return diffMetric;
}
};
}
public static void printCommandLineOptions()
{
System.out.println("Usage: \n" +
" For single image compression: java CompressAnImageToA4kibPreview -c <INPUT IMAGE> [<COMPRESSED IMAGE>]\n" +
" For multiple image compression: java CompressAnImageToA4kibPreview -c <INPUT IMAGES DIR> [<COMPRESSED IMAGE DIR>]\n" +
" For single image decompression: java CompressAnImageToA4kibPreview -d <COMPRESSED IMAGE> [<DECOMPRESSED IMAGE>\n" +
" For multiple image decompression: java CompressAnImageToA4kibPreview -d <COMPRESSED IMAGE DIR> [<DECOMPRESSED IMAGES DIR>]\n" +
"\n" +
"If optional parameters are not set then defaults will be used:\n" +
" For single image compression, compressed image will be created in same directory are input image and have '.compressed' file extension.\n" +
" For multiple image compression, compressed images will be created in a new 'out' sub directory of <INPUT IMAGES DIR> and have '.compressed' file extensions.\n" +
" For single image decompression, decompressed image will be created in same directory are input image and have '.out.png' file extension.\n" +
" For multiple image decompression, decompressed images will be created a new 'out' sub directory of <COMPRESSED IMAGE DIR> and have '.png' file extensions.\n" +
"\n" +
"The first time this application is run, required files will be generated and saved in a directory relative to execution working dir. This may take a few minutes.");
}
public static void main(String[] args) throws Exception
{
if (args.length < 2 || args.length > 3)
{
printCommandLineOptions();
return;
}
File filterFile = new File("data/images/working/lumaFilters_" + LUMA_BLOCK_SIZE + "_" + CHROMA_BLOCK_SIZE + "_" + LUMA_FILTER_BIT_DEPTH + "_" + CHROMA_FILTER_BIT_DEPTH + ".png");
FilterManager filters = (GENERATE == GENERATE_ALWAYS_ENUM || (GENERATE == GENERATE_ONLY_IF_NEEDED_ENUM && !filterFile.exists())) ? FilterManager.create() : FilterManager.load();
File outputFile;
File inputFile = new File(args[1]);
boolean compress = args[0].equals("-c");
if (compress)
{
if (inputFile.exists())
{
if (inputFile.isDirectory())
{
if (args.length == 3)
{
outputFile = new File(args[2]);
if (outputFile.exists() && outputFile.isFile())
{
System.out.println("ERROR: " + outputFile + " is not a directory.\n\n");
printCommandLineOptions();
return;
}
}
else
{
outputFile = new File(args[1] + "/out");
if (outputFile.exists() && outputFile.isFile())
{
System.out.println("ERROR: " + outputFile + " is not a directory.\n\n");
printCommandLineOptions();
return;
}
}
outputFile.mkdirs();
}
else
{
if (args.length == 3)
{
outputFile = new File(args[2]);
if (outputFile.exists() && outputFile.isDirectory())
{
System.out.println("ERROR: " + outputFile + " is a directory.\n\n");
printCommandLineOptions();
return;
}
}
else
{
outputFile = new File(args[1] + ".out.compressed");
if (outputFile.exists() && outputFile.isDirectory())
{
System.out.println("ERROR: " + outputFile + " is a directory.\n\n");
printCommandLineOptions();
return;
}
}
}
}
else
{
System.out.println("ERROR: " + inputFile + " does not exist.\n\n");
printCommandLineOptions();
return;
}
compress(inputFile, outputFile, filters);
}
else
{
if (inputFile.exists())
{
if (inputFile.isDirectory())
{
if (args.length == 3)
{
outputFile = new File(args[2]);
if (outputFile.exists() && outputFile.isFile())
{
System.out.println("ERROR: " + outputFile + " is not a directory.\n\n");
printCommandLineOptions();
return;
}
}
else
{
outputFile = new File(args[1] + "/out");
if (outputFile.exists() && outputFile.isFile())
{
System.out.println("ERROR: " + outputFile + " is not a directory.\n\n");
printCommandLineOptions();
return;
}
}
outputFile.mkdirs();
}
else
{
if (args.length == 3)
{
outputFile = new File(args[2]);
if (outputFile.exists() && outputFile.isDirectory())
{
System.out.println("ERROR: " + outputFile + " is a directory.\n\n");
printCommandLineOptions();
return;
}
}
else
{
outputFile = new File(args[1] + ".out.png");
if (outputFile.exists() && outputFile.isDirectory())
{
System.out.println("ERROR: " + outputFile + " is a directory.\n\n");
printCommandLineOptions();
return;
}
}
}
}
else
{
System.out.println("ERROR: " + inputFile + " does not exist.\n\n");
printCommandLineOptions();
return;
}
decompress(inputFile, outputFile, filters);
}
}
/**
* Compresses the input image(s) and writes to origOutputFile
* @param origInputFile
* @param origOutputFile
* @param filters
* @throws Exception
*/
private static void compress(File origInputFile, File origOutputFile, FilterManager filters) throws Exception
{
String[] files = new String[] { origInputFile.getAbsolutePath() };
if (origInputFile.isDirectory())
{
List<String> list = new ArrayList<String>();
for (File file : origInputFile.listFiles())
{
list.add(file.getAbsolutePath());
}
files = list.toArray(new String[0]);
}
String outputDir = origOutputFile.getAbsolutePath();
for (String inputFileString : files)
{
ByteArrayOutputStream defBaos = new ByteArrayOutputStream();
int compressedSize = 0;
int sampledDimensionScalar = MIN_SAMPLED_DIMENSION;
File inputFile = new File(inputFileString);
String outputFileString = (origInputFile.isFile() ? origOutputFile : new File(outputDir + File.separator + inputFile.getName())).getAbsolutePath() + ".compressed";
File outputFile = new File(outputFileString);
if (inputFile.isFile())
{
System.out.println("Compressing "+inputFile.getAbsolutePath()+"...");
BufferedImage inputImage = ImageIO.read(inputFile);
outputFile.getParentFile().mkdirs();
while (compressedSize < DESIRED_COMPRESSED_SIZE_BYTES && sampledDimensionScalar < MAX_SAMPLED_DIMENSION_SCALAR)
{
sampledDimensionScalar += 1;
if (compressedSize > 0)
{
System.out.println("Compressed Size: " + compressedSize + " Scalar: " + (sampledDimensionScalar - 1));
byte[] data = defBaos.toByteArray();
defBaos.reset();
FileOutputStream fos = new FileOutputStream(outputFile);
fos.write(data);
fos.close();
}
int previousFrameChromaFilter = 0;
int previousFrameLumaFilter = 0;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BitOutputStream bos = new BitOutputStream(baos);
// Down sample input image
int sampledDimension = CHROMA_BLOCK_SIZE * sampledDimensionScalar;
BufferedImage scaledImage = new BufferedImage(sampledDimension, sampledDimension, BufferedImage.TYPE_INT_RGB);
scaledImage.getGraphics().drawImage(inputImage.getScaledInstance(sampledDimension, sampledDimension, BufferedImage.SCALE_AREA_AVERAGING), 0, 0, null);
float[][][] pixels = convertToYUVImage(scaledImage);
// save image dimensions
bos.write(inputImage.getWidth(), IMAGE_DIMENSION_BITS);
bos.write(inputImage.getHeight(), IMAGE_DIMENSION_BITS);
bos.write(sampledDimensionScalar, MAX_SAMPLED_DIMENSION_SCALAR_BITS);
// match sampled image blocks' lumenosity to filters...
float[][][] categorisedPixels = new float[sampledDimension][sampledDimension][3];
for (int i = 0; i < sampledDimension; i += LUMA_BLOCK_SIZE)
{
for (int j = 0; j < sampledDimension; j += LUMA_BLOCK_SIZE)
{
Block block = new Block(pixels, j, i, LUMA_BLOCK_SIZE);
Block leastDifferenceLumaFilter = null;
float leastDifference = Float.MAX_VALUE;
float difference;
for (Block filter : filters.lumaFilters)
{
difference = LumaComparator.comparator.calculateDifference(filter, block);
if (difference < leastDifference)
{
leastDifference = difference;
leastDifferenceLumaFilter = filter;
}
}
// save luma block
if (LUMA_SIMILARITY_BIT_DEPTH == 0)
{
bos.write(leastDifferenceLumaFilter.closestMatchingFilterId, LUMA_FILTER_BIT_DEPTH);
}
else
out:
{
int[] lumaFilterSimilarityOrdering = filters.lumaFilterSimilarityOrdering[previousFrameLumaFilter];
for (int k = 0; k < LUMA_SIMILARITY_COUNT; k++)
if (lumaFilterSimilarityOrdering[k] == leastDifferenceLumaFilter.closestMatchingFilterId)
{
if (LUMA_FILTER_COUNT > LUMA_SIMILARITY_COUNT)
bos.write(true);
bos.write(k, LUMA_SIMILARITY_BIT_DEPTH);
break out;
}
if (LUMA_FILTER_COUNT > LUMA_SIMILARITY_COUNT)
bos.write(false);
bos.write(leastDifferenceLumaFilter.closestMatchingFilterId, LUMA_FILTER_BIT_DEPTH);
}
previousFrameLumaFilter = leastDifferenceLumaFilter.closestMatchingFilterId;
// copy matched filter's lumenosity into corresponding output image's block
for (int y = 0; y < LUMA_BLOCK_SIZE; y++)
{
for (int x = 0; x < LUMA_BLOCK_SIZE; x++)
{
categorisedPixels[j + x][i + y][LUMA] = leastDifferenceLumaFilter.pixels[x][y][LUMA];
}
}
}
}
// match sampled image blocks' 'chromanosity' to filters...
for (int i = 0; i < sampledDimension; i += CHROMA_BLOCK_SIZE)
{
for (int j = 0; j < sampledDimension; j += CHROMA_BLOCK_SIZE)
{
Block block = new Block(pixels, j, i, CHROMA_BLOCK_SIZE);
Block leastDifferenceChromaFilter = null;
float leastDifference = Float.MAX_VALUE;
float difference;
for (Block filter : filters.chromaFilters)
{
difference = ChromaComparator.comparator.calculateDifference(filter, block);
if (difference < leastDifference)
{
leastDifference = difference;
leastDifferenceChromaFilter = filter;
}
}
// save chroma block
if (CHROMA_SIMILARITY_BIT_DEPTH == 0)
{
bos.write(leastDifferenceChromaFilter.closestMatchingFilterId, CHROMA_FILTER_BIT_DEPTH);
}
else
out:
{
int[] chromaFilterSimilarityOrdering = filters.chromaFilterSimilarityOrdering[previousFrameChromaFilter];
for (int k = 0; k < CHROMA_SIMILARITY_COUNT; k++)
if (chromaFilterSimilarityOrdering[k] == leastDifferenceChromaFilter.closestMatchingFilterId)
{
if (CHROMA_FILTER_COUNT > CHROMA_SIMILARITY_COUNT)
bos.write(true);
bos.write(k, CHROMA_SIMILARITY_BIT_DEPTH);
break out;
}
if (CHROMA_FILTER_COUNT > CHROMA_SIMILARITY_COUNT)
bos.write(false);
bos.write(leastDifferenceChromaFilter.closestMatchingFilterId, CHROMA_FILTER_BIT_DEPTH);
}
previousFrameChromaFilter = leastDifferenceChromaFilter.closestMatchingFilterId;
// copy matched filter's 'chromanosity' into corresponding output image's block
for (int y = 0; y < CHROMA_BLOCK_SIZE; y++)
{
for (int x = 0; x < CHROMA_BLOCK_SIZE; x++)
{
categorisedPixels[j + x][i + y][CHROMA_U] = leastDifferenceChromaFilter.pixels[x][y][CHROMA_U];
categorisedPixels[j + x][i + y][CHROMA_V] = leastDifferenceChromaFilter.pixels[x][y][CHROMA_V];
}
}
}
}
bos.close();
DeflaterOutputStream deflater = new DeflaterOutputStream(defBaos, new Deflater(Deflater.BEST_COMPRESSION));
deflater.write(baos.toByteArray());
deflater.close();
compressedSize = defBaos.size();
}
System.out.println("Compressed "+inputFile.getAbsolutePath()+".");
}
}
}
/**
* Decompresses the input image(s) and writes to origOutputFile
*/
private static void decompress(File origInputFile, File origOutputFile, FilterManager filters) throws Exception
{
String[] files = new String[] { origInputFile.getAbsolutePath() };
if (origInputFile.isDirectory())
{
List<String> list = new ArrayList<String>();
for (File file : origInputFile.listFiles())
{
list.add(file.getAbsolutePath());
}
files = list.toArray(new String[0]);
}
String outputDir = origOutputFile.getAbsolutePath();
for (String inputFileString : files)
{
File inputFile = new File(inputFileString);
if (inputFile.isFile())
{
File decompressedFile = origInputFile.isFile() ? origOutputFile : new File(outputDir + File.separator + inputFile.getName()+".png");
filters = FilterManager.load();
decodeFrame(decompressedFile, filters, new FileInputStream(inputFile));
}
}
}
static void decodeFrame(File outputFile, FilterManager filters, InputStream is) throws IOException
{
InflaterInputStream inflater = new InflaterInputStream(is);
BitInputStream bis = new BitInputStream(inflater, 8);
int previousFrameLumaFilter = 0;
int previousFrameChromaFilter = 0;
int width = (int) bis.read(IMAGE_DIMENSION_BITS);
int height = (int) bis.read(IMAGE_DIMENSION_BITS);
int sampledDimensionScalar = (int) bis.read(MAX_SAMPLED_DIMENSION_SCALAR_BITS);
int sampledDimension = CHROMA_BLOCK_SIZE * sampledDimensionScalar;
float[][][] pixels = new float[sampledDimension][sampledDimension][3];
for (int i = 0; i < sampledDimension; i += LUMA_BLOCK_SIZE)
{
for (int j = 0; j < sampledDimension; j += LUMA_BLOCK_SIZE)
{
int filterId;
if (LUMA_SIMILARITY_BIT_DEPTH == 0 || (LUMA_FILTER_COUNT > LUMA_SIMILARITY_COUNT && !bis.read()))
{
filterId = (int) bis.read(LUMA_FILTER_BIT_DEPTH);
}
else
{
filterId = filters.lumaFilterSimilarityOrdering[previousFrameLumaFilter][(int) bis.read(LUMA_SIMILARITY_BIT_DEPTH)];
}
previousFrameLumaFilter = filterId;
Block filter = filters.lumaFilters[filterId];
// copy matched filter's lumenosity into corresponding output image's block
for (int y = 0; y < LUMA_BLOCK_SIZE; y++)
{
for (int x = 0; x < LUMA_BLOCK_SIZE; x++)
{
pixels[j + x][i + y][LUMA] = filter.pixels[x][y][LUMA];
}
}
}
}
// match sampled image blocks' 'chromanosity' to filters...
for (int i = 0; i < sampledDimension; i += CHROMA_BLOCK_SIZE)
{
for (int j = 0; j < sampledDimension; j += CHROMA_BLOCK_SIZE)
{
int filterId;
if (CHROMA_SIMILARITY_BIT_DEPTH == 0 || (CHROMA_FILTER_COUNT > CHROMA_SIMILARITY_COUNT && !bis.read()))
{
filterId = (int) bis.read(CHROMA_FILTER_BIT_DEPTH);
}
else
{
filterId = filters.chromaFilterSimilarityOrdering[previousFrameChromaFilter][(int) bis.read(CHROMA_SIMILARITY_BIT_DEPTH)];
}
previousFrameChromaFilter = filterId;
Block filter = filters.chromaFilters[filterId];
// copy matched filter's 'chromanosity' into corresponding output image's block
for (int y = 0; y < CHROMA_BLOCK_SIZE; y++)
{
for (int x = 0; x < CHROMA_BLOCK_SIZE; x++)
{
pixels[j + x][i + y][CHROMA_U] = filter.pixels[x][y][CHROMA_U];
pixels[j + x][i + y][CHROMA_V] = filter.pixels[x][y][CHROMA_V];
}
}
}
}
// render the scaled output image
BufferedImage yuvImage = new BufferedImage(sampledDimension, sampledDimension, BufferedImage.TYPE_INT_RGB);
renderYUV(yuvImage, pixels);
// save the output image, resized to the input image's dimensons
ImageIO.write(blurResize(yuvImage, width, height), "png", outputFile);
// BufferedImage scaledImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// scaledImage.getGraphics().drawImage(yuvImage.getScaledInstance(width, height, Image.SCALE_FAST), 0, 0, null);
// ImageIO.write(scaledImage, "png", new File(outputFile.getAbsolutePath() + ".nonBlurred.png"));
System.out.println("Decompressed "+outputFile.getAbsolutePath());
}
/**
* Converts the given buffered image into a 3-dimensional array of YUV pixels
*
* @param image
* the buffered image to convert
* @return 3-dimensional array of YUV pixels
*/
static private float[][][] convertToYUVImage(BufferedImage image)
{
final int width = image.getWidth();
final int height = image.getHeight();
float[][][] yuv = new float[width][height][3];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int rgb = image.getRGB(x, y);
yuv[x][y] = rgb2yuv(rgb);
}
}
return yuv;
}
/**
* Renders the given YUV image into the given buffered image.
*
* @param image
* the buffered image to render to
* @param pixels
* the YUV image to render.
* @param dimension
* the
*/
static private void renderYUV(BufferedImage image, float[][][] pixels)
{
final int height = pixels.length;
final int width = pixels[0].length;
int rgb;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
rgb = yuv2rgb(pixels[x][y]);
image.setRGB(x, y, rgb);
}
}
}
/**
* Converts a RGB pixel into a YUV pixel
*
* @param rgb
* a pixel encoded as 24 bit RGB
* @return array representing a pixel. Consisting of Y,U and V components
*/
static float[] rgb2yuv(int rgb)
{
float red = EIGHT_BIT_DIVISOR * ((rgb >> 16) & 0xFF);
float green = EIGHT_BIT_DIVISOR * ((rgb >> 8) & 0xFF);
float blue = EIGHT_BIT_DIVISOR * (rgb & 0xFF);
float Y = 0.299F * red + 0.587F * green + 0.114F * blue;
float U = (blue - Y) * 0.565F;
float V = (red - Y) * 0.713F;
return new float[] { Y, U, V };
}
/**
* Converts a YUV pixel into a RGB pixel
*
* @param yuv
* array representing a pixel. Consisting of Y,U and V components
* @return a pixel encoded as 24 bit RGB
*/
static int yuv2rgb(float[] yuv)
{
int red = (int) ((yuv[0] + 1.403 * yuv[2]) * 256);
int green = (int) ((yuv[0] - 0.344 * yuv[1] - 0.714 * yuv[2]) * 256);
int blue = (int) ((yuv[0] + 1.77 * yuv[1]) * 256);
// clamp to 0-255 range
red = red < 0 ? 0 : red > 255 ? 255 : red;
green = green < 0 ? 0 : green > 255 ? 255 : green;
blue = blue < 0 ? 0 : blue > 255 ? 255 : blue;
return (red << 16) | (green << 8) | blue;
}
/**
* Blurs an image using a 3x3 weighted kernel.
*
* @param image
* the image to blur from
* @return the blurred image
*/
public static BufferedImage blurImage(BufferedImage image)
{
float[] blurKernel = { 0.0375f, 0.0625f, 0.0375f, 0.0625f, 0.6f, 0.0625f, 0.0375f, 0.0625f, 0.0375f };
Map<Key, Object> map = new HashMap<Key, Object>();
map.put(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
map.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
map.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
RenderingHints hints = new RenderingHints(map);
BufferedImageOp op = new ConvolveOp(new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, hints);
return op.filter(image, null);
}
/**
* Resizes the input image to the given width and height.
*
* Algorithm: 1. check to see if doubling image will exceed desired width or height, if so then goto 5 2. resize the image so that it is double the width and height it was before. 3. blur the image 4. goto 1 5. resize image to desired with and height
*
* @param image
* the image to resize
* @param newWidth
* the desired width of the resized image
* @param newHeight
* the desired height of the resized image
* @return a new resized image
*/
public static BufferedImage blurResize(BufferedImage image, int newWidth, int newHeight)
{
int width = image.getWidth();
int height = image.getHeight();
while (width * 2 <= newWidth && height * 2 <= newHeight)
{
BufferedImage newImage = new BufferedImage(width * 2, height * 2, BufferedImage.TYPE_INT_RGB);
newImage.getGraphics().drawImage(image.getScaledInstance(width * 2, height * 2, Image.SCALE_AREA_AVERAGING), 0, 0, null);
image = blurImage(newImage);
width = image.getWidth();
height = image.getHeight();
}
BufferedImage newImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
newImage.getGraphics().drawImage(image.getScaledInstance(newWidth, newHeight, Image.SCALE_AREA_AVERAGING), 0, 0, null);
return newImage;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment