Created
February 13, 2016 11:11
-
-
Save anonymous/989ab8a1bb6ec14f6ea9 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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