diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/images/composed-ALPHA.png b/images/composed-ALPHA.png new file mode 100644 index 0000000..babdb25 Binary files /dev/null and b/images/composed-ALPHA.png differ diff --git a/images/composed-TV-GUINNES.png b/images/composed-TV-GUINNES.png new file mode 100644 index 0000000..174e158 Binary files /dev/null and b/images/composed-TV-GUINNES.png differ diff --git a/images/filters-GSHARP.png b/images/filters-GSHARP.png new file mode 100644 index 0000000..ba18b02 Binary files /dev/null and b/images/filters-GSHARP.png differ diff --git a/images/sample/Beach.jpg b/images/sample/Beach.jpg new file mode 100644 index 0000000..5b80c36 Binary files /dev/null and b/images/sample/Beach.jpg differ diff --git a/images/sample/ChromaKeyBlue.jpg b/images/sample/ChromaKeyBlue.jpg new file mode 100644 index 0000000..4792add Binary files /dev/null and b/images/sample/ChromaKeyBlue.jpg differ diff --git a/images/sample/ChromaKeyGreen.jpg b/images/sample/ChromaKeyGreen.jpg new file mode 100644 index 0000000..193519a Binary files /dev/null and b/images/sample/ChromaKeyGreen.jpg differ diff --git a/images/sample/Guinness.jpg b/images/sample/Guinness.jpg new file mode 100644 index 0000000..ca4ed0c Binary files /dev/null and b/images/sample/Guinness.jpg differ diff --git a/images/sample/Musician.jpg b/images/sample/Musician.jpg new file mode 100644 index 0000000..ace3829 Binary files /dev/null and b/images/sample/Musician.jpg differ diff --git a/images/sample/Sample.jpg b/images/sample/Sample.jpg new file mode 100644 index 0000000..c3110ac Binary files /dev/null and b/images/sample/Sample.jpg differ diff --git a/images/sample/SampleCol.jpg b/images/sample/SampleCol.jpg new file mode 100644 index 0000000..cdf8792 Binary files /dev/null and b/images/sample/SampleCol.jpg differ diff --git a/images/sample/TestChroma.jpg b/images/sample/TestChroma.jpg new file mode 100644 index 0000000..8d828bc Binary files /dev/null and b/images/sample/TestChroma.jpg differ diff --git a/images/sample/YosemiteBW.jpg b/images/sample/YosemiteBW.jpg new file mode 100644 index 0000000..2c89536 Binary files /dev/null and b/images/sample/YosemiteBW.jpg differ diff --git a/images/sample/salvini.jpg b/images/sample/salvini.jpg new file mode 100644 index 0000000..e3f4f5d Binary files /dev/null and b/images/sample/salvini.jpg differ diff --git a/images/sample/zztopBW.jpg b/images/sample/zztopBW.jpg new file mode 100644 index 0000000..38d6715 Binary files /dev/null and b/images/sample/zztopBW.jpg differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a52621f --- /dev/null +++ b/pom.xml @@ -0,0 +1,61 @@ + + + 4.0.0 + + berack96 + Multimedia + 1.0 + + + 17 + 17 + + + + com.aparapi + aparapi + 2.0.0 + + + org.junit.jupiter + junit-jupiter + RELEASE + test + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2 + + + jar-with-dependencies + + + + berack96.multimedia.view.Main + + + + + + package + + single + + + + + + + \ No newline at end of file diff --git a/src/main/java/berack96/multimedia/ImagesUtil.java b/src/main/java/berack96/multimedia/ImagesUtil.java new file mode 100644 index 0000000..cad28bb --- /dev/null +++ b/src/main/java/berack96/multimedia/ImagesUtil.java @@ -0,0 +1,178 @@ +package berack96.multimedia; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.io.PrintStream; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +/** + * Classe di utilita' contenente metodi per la manipolazione di immagini e per calcoli di trasformate + */ +public class ImagesUtil { + + /** + * Massimo valore dei colori + */ + public static final int MAX_COLOR = 255; + + /** + * Coseno da usare per la trasformata Discrete Cosine Transform + * @param ind l'indice dei valori dell'array che si cicla + * @param ind2 l'indice del valore che si sta' calcolando + * @param num il numero totale di valori discreti + * @return il coseno calcolato + */ + public static double cosDCT(double ind, double ind2, int num) { + return Math.cos(((2*ind + 1) * Math.PI * ind2) / (2 * num)); + } + + /** + * Permette di calcolare il valore di alpha per le varie trasformate + * @param index l'indice dell'array che si sta calcola + * @param length la lunghezza dell'array chje si sta calcolando + * @return il valore di alpha + */ + public static double getAlpha(int index, int length) { + return Math.sqrt((index == 0 ? 1.0 : 2.0) / length); + } + + /** + * Fa in modo di forzare il valore fra il minimo e il massimo valore consentito + * @param val il valore che si deve controllare + * @param min il valore minimo che puo' avere + * @param max il valore massimo che puo' avere + * @return il valore + */ + public static int range(int val, int min, int max) { + return Math.min(max, Math.max(val, min)); + } + + /** + * Fa in modo di forzare il valore fra il minimo e il massimo valore consentito per il colore del pixel + * @param val il valore che si deve controllare + * @return il valore + */ + public static int rangePx(double val) { + return Math.min(MAX_COLOR, Math.max((int)Math.round(val), 0)); + } + + /** + * Funzione che permette di far partire un processo su una qualche immagine ed aspettare che finisca.
+ * Questa funzione permette di avere un log che indica lo stato della computazione. + * @param name il nome da dare nel log + * @param program il programma da far partire + * @return il risultato del programma + */ + public static BufferedImage waitProcess(PrintStream out, String name, Supplier program) { + long time = System.currentTimeMillis(); + final AtomicReference processed = new AtomicReference<>(); + + System.out.println("Starting processing for " + name); + Thread thread = new Thread(() -> processed.set(program.get()), THREADS_NAME); + thread.start(); + + do { + try { Thread.sleep(10); } catch (Exception ignore) {} + out.print(Math.round(((float) count.get() / tot) * 100) + "%\r"); + } while (thread.isAlive()); + + time = System.currentTimeMillis() - time; + System.out.println("Ended in " + ((float) time / 1000) + "sec"); + tot = 0; + return processed.get(); + } + + /** + * Variabile che serve a settare il numero di threads che vengono creati quando + * viene richiamato il metodo forEachPixel.
+ * In caso il valore sia < 2, non verranno creati thread. + */ + public static int maxThreads = 6; + + /** + * Indica il nome dei vari thread che vengono avviati durante il forEach. + */ + public final static String THREADS_NAME = "ImageProcessing"; + + /** + * Una variabile che viene usata per conteggiare quanti pixel (o blocchi) sono stati elaborati.
+ * Qesta viene aggiornata ogni volta che viene fatta una iterazione con il metodo forEachPixel + */ + public final static AtomicInteger count = new AtomicInteger(); + + /** + * Una variabile che viene usata per indicare quanti pixel totali devono essere elaborati.
+ * Qesta viene aggiornata ogni volta che viene richiamato il metodo forEachPixel + */ + private static int tot = 0; + + /** + * Metodo che serve per iterare ogni pixel del raster passato e applicargli la funzione specificata
+ * Il consumer passato avra' in input le coordinate del pixel sottoforma (x,y)
+ * In base a come viene settata la variabile {@link #maxThreads} il metodo potrebbe creare dei + * threads per l'elaborazione dei pixel
+ * Questo metodo e' equvalente a {@link #forEachPixel(Raster, int, BiConsumer)} con delta = 1. + * @param raster il raster da elaborare + * @param consumer la funzione da applicargli + */ + public static void forEachPixel(Raster raster, BiConsumer consumer) { + forEachPixel(raster, 1, consumer); + } + + /** + * Metodo che serve per iterare ogni pixel del raster passato e applicargli la funzione specificata
+ * Il consumer passato avra' in input le coordinate del pixel sottoforma (x,y)
+ * In base a come viene settata la variabile {@link #maxThreads} il metodo potrebbe creare dei + * threads per l'elaborazione dei pixel
+ * Questo metodo accetta un delta che serve per spostarsi da un pixel a quello successivo saltando eventuali pixel intermedi
+ * Se si vogliono iterare tutti i pixel allra basta passare il delta a 1. + * @param raster il raster da elaborare + * @param delta di quanto mi devo spostare dopo ogni elaborazione per ogni asse + * @param consumer la funzione da applicargli + */ + public static synchronized void forEachPixel(final Raster raster, final int delta, final BiConsumer consumer) { + count.set(0); + tot = raster.getWidth() * raster.getHeight() / (delta * delta); + final int deltaF = Math.max(delta, 1); + final int numThreads = Math.min(maxThreads, Runtime.getRuntime().availableProcessors()); + + if (numThreads < 2) + forEachPixel(raster, deltaF, 0, 1, consumer); + else { + Thread[] threads = new Thread[numThreads - 1]; + for (int i = 0; i < threads.length; i++) { + final int num = i + 1; + threads[i] = new Thread(() -> forEachPixel(raster, deltaF, num, numThreads, consumer), THREADS_NAME); + threads[i].start(); + } + + forEachPixel(raster, deltaF, 0, numThreads, consumer); + for (Thread t : threads) + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + /** + * Metodo privato che serve per i threads.
+ * Itera per tutta l'immagine o solamente per alcune righe + * @param raster l'immagine da iterare + * @param delta quanti pixel si devono saltare dopo ogni iterazione + * @param num il numero del thread (ID) + * @param threads il numero di threads che svolgono la stessa operazione + * @param consumer l'operazione da svolgere + */ + private static void forEachPixel(Raster raster, int delta, int num, int threads, BiConsumer consumer) { + for (int height = num * delta; height < raster.getHeight(); height += delta * threads) + for (int width = 0; width < raster.getWidth(); width += delta) { + consumer.accept(width, height); + count.incrementAndGet(); + } + } +} diff --git a/src/main/java/berack96/multimedia/Transform.java b/src/main/java/berack96/multimedia/Transform.java new file mode 100644 index 0000000..ddd84b1 --- /dev/null +++ b/src/main/java/berack96/multimedia/Transform.java @@ -0,0 +1,15 @@ +package berack96.multimedia; + +/** + * Interfaccia generica per fare delle trasformate matematiche + * @param L'oggetto della trasformata + * @param L'oggetto trasformato + */ +public interface Transform { + /** + * Questo metodo applica la trasformata sull'oggetto passato in input + * @param obj l'oggetto a cui applicare la trasformata + * @return l'oggetto risultante + */ + R transform(O obj); +} diff --git a/src/main/java/berack96/multimedia/composting/AlphaBlend.java b/src/main/java/berack96/multimedia/composting/AlphaBlend.java new file mode 100644 index 0000000..e13f21f --- /dev/null +++ b/src/main/java/berack96/multimedia/composting/AlphaBlend.java @@ -0,0 +1,31 @@ +package berack96.multimedia.composting; + +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class AlphaBlend extends TwoImagesTransform { + + private double alphaF; + private double alphaB; + + public AlphaBlend(double alphaF, double alphaB) { + this.alphaF = Math.max(0, Math.min(alphaF, 1)); + this.alphaB = Math.max(0, Math.min(alphaB, 1)); + this.alphaB *= 1 - this.alphaF; + } + + @Override + public void transform(Raster foreground, Raster background, WritableRaster result) { + final double alphaDiv = alphaF + alphaB; + ImagesUtil.forEachPixel(result, (width, height) -> { + for(int color = 0; color < result.getNumBands(); color++) { + double pixelF = foreground.getSampleDouble(width, height, color) * alphaF; + double pixelB = background.getSampleDouble(width, height, color) * alphaB; + double pixel = (pixelF + pixelB) / alphaDiv; + result.setSample(width, height, color, ImagesUtil.rangePx(pixel)); + } + }); + } +} diff --git a/src/main/java/berack96/multimedia/composting/ChromaKeying.java b/src/main/java/berack96/multimedia/composting/ChromaKeying.java new file mode 100644 index 0000000..0e654d1 --- /dev/null +++ b/src/main/java/berack96/multimedia/composting/ChromaKeying.java @@ -0,0 +1,38 @@ +package berack96.multimedia.composting; + +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class ChromaKeying extends TwoImagesTransform { + + private final boolean useGreen; + public ChromaKeying() { this.useGreen = false; } + public ChromaKeying(boolean useGreen) { this.useGreen = useGreen; } + + @Override + protected void transform(Raster foreground, Raster background, WritableRaster result) { + if (foreground.getNumBands() != 3) + throw new IllegalArgumentException("The two images should have RGB colors"); + + final int key = useGreen ? 1 : 2; + final int other = useGreen ? 2 : 1; + ImagesUtil.forEachPixel(result, (width, height) -> { + double[] pixel = foreground.getPixel(width, height, (double[]) null); + double alpha = calcAlpha(pixel[0], pixel[other], pixel[key]); + + for (int i = 0; i < pixel.length; i++) + pixel[i] = calcPixel(alpha, pixel[i], background.getSampleDouble(width, height, i)); + result.setPixel(width, height, pixel); + }); + } + + static public double calcAlpha(double red, double other, double key) { + return 1 - (key - Math.max(other, red)) / ImagesUtil.MAX_COLOR; + } + + static public double calcPixel(double alpha, double colorF, double colorB) { + return ImagesUtil.rangePx(alpha * colorF + (1 - alpha) * colorB); + } +} diff --git a/src/main/java/berack96/multimedia/composting/ChromaKeying3D.java b/src/main/java/berack96/multimedia/composting/ChromaKeying3D.java new file mode 100644 index 0000000..e51f5ed --- /dev/null +++ b/src/main/java/berack96/multimedia/composting/ChromaKeying3D.java @@ -0,0 +1,44 @@ +package berack96.multimedia.composting; + +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class ChromaKeying3D extends ChromaKeying { + final private double[] keyColor = {0, 255, 0}; + private double keySphere; + private double keyTolerance; + + public ChromaKeying3D(double radius, double tolerance) { + this(radius, tolerance, 0, 255, 0); + } + public ChromaKeying3D(double radius, double tolerance, double red, double green, double blue) { + super(); + this.keySphere = radius < 1 ? 1 : radius * radius; + this.keyTolerance = tolerance < 1 ? 1 : tolerance * tolerance; + keyColor[0] = ImagesUtil.rangePx(red); + keyColor[1] = ImagesUtil.rangePx(green); + keyColor[2] = ImagesUtil.rangePx(blue); + } + + @Override + protected void transform(Raster foreground, Raster background, WritableRaster result) { + if (foreground.getNumBands() != 3) + throw new IllegalArgumentException("The two images should have RGB colors"); + + ImagesUtil.forEachPixel(result, (width, height) -> { + double[] pixel = foreground.getPixel(width, height, (double[]) null); + double x = pixel[0] - keyColor[0]; + double y = pixel[1] - keyColor[1]; + double z = pixel[2] - keyColor[2]; + double point = x * x + y * y + z * z; + double alpha = (point - keySphere) / keyTolerance; + alpha = Math.max(0, Math.min(alpha, 1)); + + for (int i = 0; i < pixel.length; i++) + pixel[i] = calcPixel(alpha, pixel[i], background.getSampleDouble(width, height, i)); + result.setPixel(width, height, pixel); + }); + } +} diff --git a/src/main/java/berack96/multimedia/composting/TwoImagesTransform.java b/src/main/java/berack96/multimedia/composting/TwoImagesTransform.java new file mode 100644 index 0000000..8c86bc8 --- /dev/null +++ b/src/main/java/berack96/multimedia/composting/TwoImagesTransform.java @@ -0,0 +1,31 @@ +package berack96.multimedia.composting; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.Transform; + +public abstract class TwoImagesTransform implements Transform { + @Override + public BufferedImage transform(BufferedImage...obj) { + if(obj == null || obj.length != 2) + throw new IllegalArgumentException("Need exactly 2 images"); + + final Raster foreground = obj[0].getRaster(); + final Raster background = obj[1].getRaster(); + + if(foreground.getHeight() != background.getHeight()) + throw new IllegalArgumentException("The 2 images must be equal in height"); + if(foreground.getWidth() != background.getWidth()) + throw new IllegalArgumentException("The 2 images must be equal in width"); + if(obj[0].getType() != obj[1].getType()) + throw new IllegalArgumentException("The 2 images must have the same color type"); + + BufferedImage result = new BufferedImage(obj[0].getWidth(), obj[0].getHeight(), obj[0].getType()); + transform(foreground, background, result.getRaster()); + return result; + } + + protected abstract void transform(Raster foreground, Raster background, WritableRaster result); +} diff --git a/src/main/java/berack96/multimedia/compression/JPEG.java b/src/main/java/berack96/multimedia/compression/JPEG.java new file mode 100644 index 0000000..3745a8e --- /dev/null +++ b/src/main/java/berack96/multimedia/compression/JPEG.java @@ -0,0 +1,166 @@ +package berack96.multimedia.compression; + +import javax.imageio.ImageIO; + +import berack96.multimedia.ImagesUtil; +import berack96.multimedia.transforms.DCT2D; +import berack96.multimedia.transforms.DCT2DInverse; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.io.File; +import java.io.IOException; + +/** + * Classe che converte una immagine nella sua rappresentazione JPEG + */ +public class JPEG { + static public final int BLOCK_SIZE = 8; + static public final int NORMALIZER = ImagesUtil.MAX_COLOR / 2 + 1; + static private final int[][] QT_LUMINANCE = { + {16, 11, 10, 16, 24, 40, 51, 61}, + {12, 12, 14, 19, 26, 58, 60, 55}, + {14, 13, 16, 24, 40, 57, 69, 56}, + {14, 17, 22, 29, 51, 87, 80, 62}, + {18, 22, 37, 56, 68, 109, 103, 77}, + {24, 35, 55, 64, 81, 104, 113, 92}, + {49, 64, 78, 87, 103, 121, 120, 101}, + {72, 92, 95, 98, 112, 100, 103, 99} + }; + static private final int[][] QT_CHROMINANCE = { + {17, 18, 24, 47, 99, 99, 99, 99}, + {18, 21, 26, 66, 99, 99, 99, 99}, + {24, 26, 56, 99, 99, 99, 99, 99}, + {47, 66, 99, 99, 99, 99, 99, 99}, + {99, 99, 99, 99, 99, 99, 99, 99}, + {99, 99, 99, 99, 99, 99, 99, 99}, + {99, 99, 99, 99, 99, 99, 99, 99}, + {99, 99, 99, 99, 99, 99, 99, 99} + }; + + + /** + * Processa una immagine nella sua rappresentazione JPEG + * @param imageFile la path del file da modificare + * @param factor il fattore moltiplicativo per la quantizzazione + * @return l'immagine modificata dalla quantizzazione JPEG + * @throws IOException nel caso l'accesso al file fallisca + */ + public BufferedImage process(String imageFile, double factor) throws IOException { + return process(ImageIO.read(new File(imageFile)), factor); + } + + /** + * Processa una immagine nella sua rappresentazione JPEG + * @param image l'immagine da modificare + * @param factor il fattore moltiplicativo per la quantizzazione + * @return l'immagine modificata dalla quantizzazione JPEG + */ + public BufferedImage process(BufferedImage image, double factor) { + final BufferedImage img = new BufferedImage(image.getWidth(), image.getHeight(), image.getType()); + final WritableRaster raster = img.getRaster(); + image.copyData(raster); + + ImagesUtil.forEachPixel(raster, BLOCK_SIZE, (x, y) -> processBlock(raster, x, y, factor)); + return img; + } + + /** + * Processa un singolo blocco di un raster e lo modifica dopo avergli applicato:
+ * - discrete cosine transform 2D + * - quantizzazione + * - dequantizzazione + * - discrete cosine transform 2D inverse + * + * @param img il raster a cui applicare l'algoritmo + * @param width il pixel in alto a sinistra del blocco da processare + * @param height il pixel in alto a sinistra del blocco da processare + * @param factor il fattore moltiplicativo della quantizzazione + */ + static public void processBlock(WritableRaster img, int width, int height, double factor) { + writeBlock(img, inverse(transform(readBlock(img, width, height), factor), factor), width, height); + } + + static private double[][][] readBlock(Raster raster, int width, int height) { + int numBands = raster.getNumBands(); + double[][][] block = new double[numBands][BLOCK_SIZE][BLOCK_SIZE]; + + for (int x = 0; x < BLOCK_SIZE; x++) + for (int y = 0; y < BLOCK_SIZE; y++) + if (x + width < raster.getWidth() && y + height < raster.getHeight()) { + for (int color = 0; color < numBands; color++) { + block[color][x][y] = -NORMALIZER; + block[color][x][y] += raster.getSampleDouble(x + width, y + height, color); + } + if (numBands == 3) + convertToYCC(block, x, y); + } + return block; + } + + static private void writeBlock(WritableRaster raster, double[][][] block, int width, int height) { + int numBands = raster.getNumBands(); + + for (int x = 0; x < BLOCK_SIZE; x++) + for (int y = 0; y < BLOCK_SIZE; y++) + if (x + width < raster.getWidth() && y + height < raster.getHeight()) { + if (numBands == 3) + convertToRGB(block, x, y); + for (int color = 0; color < numBands; color++) { + block[color][x][y] = ImagesUtil.rangePx(block[color][x][y] + NORMALIZER); + raster.setSample(x + width, y + height, color, block[color][x][y]); + } + } + } + + static private double[][][] transform(final double[][][] block, final double factor) { + final DCT2D dct = new DCT2D(); + for (int color = 0; color < block.length; color++) { + int[][] qTable = color == 0 ? QT_LUMINANCE : QT_CHROMINANCE; + + // TRANSFORM + block[color] = dct.transform(block[color]); + // QUANTIZE + for (int x = 0; x < BLOCK_SIZE; x++) + for (int y = 0; y < BLOCK_SIZE; y++) + block[color][x][y] = Math.round(block[color][x][y] / (qTable[x][y] * factor)); + } + return block; + } + + static private double[][][] inverse(final double[][][] block, final double factor) { + final DCT2DInverse inv = new DCT2DInverse(); + for (int color = 0; color < block.length; color++) { + int[][] qTable = color == 0 ? QT_LUMINANCE : QT_CHROMINANCE; + + // DE-QUANTIZE + for (int x = 0; x < BLOCK_SIZE; x++) + for (int y = 0; y < BLOCK_SIZE; y++) + block[color][x][y] *= qTable[x][y] * factor; + // INVERSE + block[color] = inv.transform(block[color]); + } + return block; + } + + + static private void convertToYCC(double[][][] block, int x, int y) { + double lum = 0.299 * block[0][x][y] + 0.587 * block[1][x][y] + 0.114 * block[2][x][y]; + double Cb = -0.147 * block[0][x][y] - 0.289 * block[1][x][y] + 0.436 * block[2][x][y]; + double Cr = 0.615 * block[0][x][y] - 0.515 * block[1][x][y] - 0.100 * block[2][x][y]; + block[0][x][y] = lum; + block[1][x][y] = Cb; + block[2][x][y] = Cr; + } + + static private void convertToRGB(double[][][] block, int x, int y) { + double red = block[0][x][y] + 1.140 * block[2][x][y]; + double blu = block[0][x][y] - 0.395 * block[1][x][y] - 0.581 * block[2][x][y]; + double gre = block[0][x][y] + 2.032 * block[1][x][y]; + block[0][x][y] = red; + block[1][x][y] = blu; + block[2][x][y] = gre; + } + +} diff --git a/src/main/java/berack96/multimedia/filters/Convolution.java b/src/main/java/berack96/multimedia/filters/Convolution.java new file mode 100644 index 0000000..211e821 --- /dev/null +++ b/src/main/java/berack96/multimedia/filters/Convolution.java @@ -0,0 +1,61 @@ +package berack96.multimedia.filters; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; +import java.util.Arrays; + +import berack96.multimedia.Transform; +import berack96.multimedia.ImagesUtil; + +public class Convolution implements Transform { + + private final double[][] kernel; + + public Convolution(double[][] kernel) { + this.kernel = normalize(kernel); + } + + @Override + public BufferedImage transform(BufferedImage source) { + final BufferedImage result = new BufferedImage(source.getWidth(), source.getHeight(), source.getType()); + final Raster src = source.getRaster(); + final WritableRaster res = result.getRaster(); + final int half = kernel.length / 2; + + ImagesUtil.forEachPixel(src, (width, height) -> { + double[] calc = new double[src.getNumBands()]; + double[] temp = null; + + for (int y = -half; y <= half; y++) { + int tempY = ImagesUtil.range(y + height, 0, src.getHeight() - 1); + for (int x = -half; x <= half; x++) { + int tempX = ImagesUtil.range(x + width, 0, src.getWidth() - 1); + + double kern = kernel[x + half][y + half]; + temp = src.getPixel(tempX, tempY, temp); + for (int color = 0; color < calc.length; color++) + calc[color] += temp[color] * kern; + } + } + for (int color = 0; color < calc.length; color++) + calc[color] = ImagesUtil.rangePx(calc[color]); + res.setPixel(width, height, calc); + }); + return result; + } + + static private double[][] normalize(double[][] kernel) { + double total = 0; + for (double[] line : kernel) { + assert line.length == kernel.length : "The kernel must be a square"; + total += Arrays.stream(line).sum(); + } + + if (total != 1.0 && total != 0.0) + for (double[] line : kernel) + for (int i = 0; i < line.length; i++) + line[i] /= total; + return kernel; + } +} diff --git a/src/main/java/berack96/multimedia/filters/FilterFactory.java b/src/main/java/berack96/multimedia/filters/FilterFactory.java new file mode 100644 index 0000000..fd2793c --- /dev/null +++ b/src/main/java/berack96/multimedia/filters/FilterFactory.java @@ -0,0 +1,62 @@ +package berack96.multimedia.filters; + +import java.util.Arrays; + +public class FilterFactory { + + static public double[][] getIdentity(int size) { + double[][] kernel = getEmptyKernel(size, 0); + kernel[size / 2][size / 2] = 1; + return kernel; + } + + static public double[][] getLowPass(int size) { + return getEmptyKernel(size, 1.0 / (size * size)); + } + + static public double[][] getHighPass(int size) { + double[][] kernel = getEmptyKernel(size, -1.0); + kernel[size / 2][size / 2] = size * size; + return kernel; + } + + static public double[][] getSharpen(int size) { + double[][] kernel = getEmptyKernel(size, -1.0 / (size * size)); + kernel[size / 2][size / 2] = (2.0 * size * size - 1) / (size * size); + return kernel; + } + + static public double[][] getGaussian(int size, double sigma) { + double[][] kernel = getEmptyKernel(size, 0); + final int half = size / 2; + final double div = -1.0 / (2 * sigma * sigma); + final double divPi = 1.0 / (2 * Math.PI * sigma * sigma); + + for (int i = -half; i <= half; i++) + for (int j = -half; j <= half; j++) + kernel[i + half][j + half] = divPi * Math.pow(Math.E, (i * i + j * j) * div); + return kernel; + } + + static public double[][] getGaussianSharp(int size, double sigma) { + return invert(getGaussian(size, sigma)); + } + + + static private double[][] invert(double[][] kernel) { + double[][] id = getIdentity(kernel.length); + for (int i = 0; i < kernel.length; i++) + for (int j = 0; j < kernel.length; j++) + kernel[i][j] = 2 * id[i][j] - kernel[i][j]; + return kernel; + } + + static private double[][] getEmptyKernel(int size, double initialVal) { + assert size > 2 && size % 2 == 1 : "The kernel must size should be an odd number > 1"; + + double[][] kernel = new double[size][size]; + for (double[] arr : kernel) + Arrays.fill(arr, initialVal); + return kernel; + } +} diff --git a/src/main/java/berack96/multimedia/resize/Bicubic.java b/src/main/java/berack96/multimedia/resize/Bicubic.java new file mode 100644 index 0000000..3e1b7be --- /dev/null +++ b/src/main/java/berack96/multimedia/resize/Bicubic.java @@ -0,0 +1,60 @@ +package berack96.multimedia.resize; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class Bicubic extends Resizing { + + public Bicubic(double ratio) { + super(ratio, ratio); + } + + @Override + public BufferedImage transform(BufferedImage obj) { + BufferedImage img = createResized(obj); + Raster oldImg = obj.getRaster(); + WritableRaster newImg = img.getRaster(); + + ImagesUtil.forEachPixel(newImg, (width, height) -> { + int x = (int) (width / ratioWidth); + int y = (int) (height / ratioHeight); + double a = (width / ratioWidth) - x; + double b = (height / ratioHeight) - y; + + // Pre calc all values + int[] x1 = new int[SIZE], y1 = new int[SIZE]; + double[] pa = new double[SIZE], pb = new double[SIZE]; + for (int k = 0; k < SIZE; k++) { + x1[k] = ImagesUtil.range(x + k - 1, 0, oldImg.getWidth() - 1); + y1[k] = ImagesUtil.range(y + k - 1, 0, oldImg.getHeight() - 1); + pa[k] = phi(k - 1, a); + pb[k] = phi(k - 1, b); + } + + for (int color = 0; color < oldImg.getNumBands(); color++) { + double sample = 0; + for (int k = 0; k < SIZE; k++) + for (int l = 0; l < SIZE; l++) + sample += oldImg.getSampleDouble(x1[k], y1[l], color) * pa[k] * pb[l]; + newImg.setSample(width, height, color, ImagesUtil.rangePx(sample)); + } + }); + return img; + } + + private static final int SIZE = 4; + private static double phi(int index, double h) { + double h2 = h * h; + double h3 = h2 * h; + return switch (index) { + case -1 -> (-h3 + (3 * h2) - (2 * h)) / 6; + case 0 -> (h3 - (2 * h2) - h + 2) / 2; + case 1 -> (-h3 + h2 + (2 * h)) / 2; + case 2 -> (h3 - h) / 6; + default -> 0; + }; + } +} diff --git a/src/main/java/berack96/multimedia/resize/Bilinear.java b/src/main/java/berack96/multimedia/resize/Bilinear.java new file mode 100644 index 0000000..e72c255 --- /dev/null +++ b/src/main/java/berack96/multimedia/resize/Bilinear.java @@ -0,0 +1,40 @@ +package berack96.multimedia.resize; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class Bilinear extends Resizing { + + public Bilinear(double ratio) { + super(ratio, ratio); + } + + @Override + public BufferedImage transform(BufferedImage obj) { + BufferedImage img = createResized(obj); + Raster oldImg = obj.getRaster(); + WritableRaster newImg = img.getRaster(); + + ImagesUtil.forEachPixel(newImg, (width, height) -> { + int x = (int) (width / ratioWidth); + int y = (int) (height / ratioHeight); + double a = (width / ratioWidth) - x; + double b = (height / ratioHeight) - y; + + int x1 = Math.min(x + 1, oldImg.getWidth() - 1); + int y1 = Math.min(y + 1, oldImg.getHeight() - 1); + + for (int color = 0; color < newImg.getNumBands(); color++) { + double sample = (1 - a) * (1 - b) * oldImg.getSampleDouble(x, y, color) + + (a) * (1 - b) * oldImg.getSampleDouble(x1, y, color) + + (1 - a) * (b) * oldImg.getSampleDouble(x, y1, color) + + (a) * (b) * oldImg.getSampleDouble(x1, y1, color); + newImg.setSample(width, height, color, ImagesUtil.rangePx(sample)); + } + }); + return img; + } +} diff --git a/src/main/java/berack96/multimedia/resize/NearestNeighbor.java b/src/main/java/berack96/multimedia/resize/NearestNeighbor.java new file mode 100644 index 0000000..38bd672 --- /dev/null +++ b/src/main/java/berack96/multimedia/resize/NearestNeighbor.java @@ -0,0 +1,27 @@ +package berack96.multimedia.resize; + +import java.awt.image.BufferedImage; +import java.awt.image.Raster; +import java.awt.image.WritableRaster; + +import berack96.multimedia.ImagesUtil; + +public class NearestNeighbor extends Resizing { + + public NearestNeighbor(double ratio) { + super(ratio, ratio); + } + + @Override + public BufferedImage transform(BufferedImage obj) { + BufferedImage img = createResized(obj); + Raster oldImg = obj.getRaster(); + WritableRaster newImg = img.getRaster(); + + ImagesUtil.forEachPixel(newImg, (width, height) -> { + for (int color = 0; color < newImg.getNumBands(); color++) + newImg.setSample(width, height, color, oldImg.getSampleDouble((int) (width / ratioWidth), (int) (height / ratioHeight), color)); + }); + return img; + } +} diff --git a/src/main/java/berack96/multimedia/resize/Resizing.java b/src/main/java/berack96/multimedia/resize/Resizing.java new file mode 100644 index 0000000..1b5f94e --- /dev/null +++ b/src/main/java/berack96/multimedia/resize/Resizing.java @@ -0,0 +1,43 @@ + +package berack96.multimedia.resize; + +import java.awt.image.BufferedImage; + +import berack96.multimedia.Transform; + +/** + * Interfaccia usata per il resizing delle immagini + */ +public abstract class Resizing implements Transform { + protected double ratioWidth = 1; + protected double ratioHeight = 1; + + public Resizing(double ratioWidth, double ratioHeight) { + setRatio(ratioWidth, ratioHeight); + } + + /** + * Setta il ratio per il resampling dell'immagine. Esso e' un numero > 0.
+ * Per valori 0 > x > 1 avverra' un down-scaling
+ * Per valori x > 1 avverra' un up-scaling + * @param ratioWidth il ratio della larghezza di resampling + * @param ratioHeight il ratio dell'altezza di resampling + * @return Questa classe in modo da poter concatenare le chiamate + */ + public Resizing setRatio(double ratioWidth, double ratioHeight) { + this.ratioWidth = ratioWidth > 0 ? ratioWidth : 1; + this.ratioHeight = ratioHeight > 0 ? ratioHeight : 1; + return this; + } + + /** + * Crea una nuova immagine ingrandita/rimpicciolita in base al valore della variabile ratio + * @param original l'immagine originale + * @return la nuova immagine vuota + */ + protected BufferedImage createResized(BufferedImage original) { + int newWidth = (int) (original.getWidth() * ratioWidth); + int newHeight = (int) (original.getHeight() * ratioHeight); + return new BufferedImage(newWidth, newHeight, original.getType()); + } +} diff --git a/src/main/java/berack96/multimedia/transforms/Complex.java b/src/main/java/berack96/multimedia/transforms/Complex.java new file mode 100644 index 0000000..2fd3066 --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/Complex.java @@ -0,0 +1,41 @@ +package berack96.multimedia.transforms; + +/** + * Classe di numeri complessi usata per la trasformata di Fourier + */ +public class Complex { + private double real = 0; + private double imaginary = 0; + + + public double getModule() { + return Math.sqrt(real*real + imaginary*imaginary); + } + public double getPhase() { + return Math.tanh(imaginary/real); + } + + public double getReal() { + return real; + } + + public void setReal(double real) { + this.real = real; + } + + public void addReal(double real) { + this.real += real; + } + + public double getImaginary() { + return imaginary; + } + + public void setImaginary(double imaginary) { + this.imaginary = imaginary; + } + + public void addImaginary(double imaginary) { + this.imaginary += imaginary; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DCT1D.java b/src/main/java/berack96/multimedia/transforms/DCT1D.java new file mode 100644 index 0000000..ac687b6 --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DCT1D.java @@ -0,0 +1,21 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.ImagesUtil; +import berack96.multimedia.Transform; + +public class DCT1D implements Transform { + @Override + public double[] transform(double[] data) { + final double[] result = new double[data.length]; + final double alpha0 = ImagesUtil.getAlpha(0, data.length); + final double alpha = ImagesUtil.getAlpha(1, data.length); + + for (int u = 0; u < result.length; u++) { + double sum = 0; + for (int x = 0; x < data.length; x++) + sum += data[x] * ImagesUtil.cosDCT(x, u, data.length); + result[u] = (u == 0 ? alpha0 : alpha) * sum; + } + return result; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DCT1DInverse.java b/src/main/java/berack96/multimedia/transforms/DCT1DInverse.java new file mode 100644 index 0000000..d6de6b0 --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DCT1DInverse.java @@ -0,0 +1,18 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.Transform; +import berack96.multimedia.ImagesUtil; + +public class DCT1DInverse implements Transform { + @Override + public double[] transform(double[] data) { + final double[] result = new double[data.length]; + final double alpha0 = ImagesUtil.getAlpha(0, data.length); + final double alpha = ImagesUtil.getAlpha(1, data.length); + + for (int i = 0; i < result.length; i++) + for (int x = 0; x < data.length; x++) + result[i] += (x == 0 ? alpha0 : alpha) * data[x] * ImagesUtil.cosDCT(i, x, data.length); + return result; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DCT2D.java b/src/main/java/berack96/multimedia/transforms/DCT2D.java new file mode 100644 index 0000000..106c786 --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DCT2D.java @@ -0,0 +1,23 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.Transform; +import berack96.multimedia.ImagesUtil; + +public class DCT2D implements Transform { + @Override + public double[][] transform(double[][] data) { + final double alpha0 = ImagesUtil.getAlpha(0, data.length); + final double alpha = ImagesUtil.getAlpha(1, data.length); + double[][] result = new double[data.length][data.length]; + + for (int u = 0; u < data.length; u++) + for (int v = 0; v < data.length; v++) { + double sum = 0; + for (int x = 0; x < data.length; x++) + for (int y = 0; y < data.length; y++) + sum += data[x][y] * ImagesUtil.cosDCT(x, u, data.length) * ImagesUtil.cosDCT(y, v, data.length); + result[u][v] = (u == 0 ? alpha0 : alpha) * (v == 0 ? alpha0 : alpha) * sum; + } + return result; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DCT2DInverse.java b/src/main/java/berack96/multimedia/transforms/DCT2DInverse.java new file mode 100644 index 0000000..103efbf --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DCT2DInverse.java @@ -0,0 +1,26 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.Transform; +import berack96.multimedia.ImagesUtil; + +public class DCT2DInverse implements Transform { + @Override + public double[][] transform(double[][] data) { + final double alpha0 = ImagesUtil.getAlpha(0, data.length); + final double alpha = ImagesUtil.getAlpha(1, data.length); + final double[][] result = new double[data.length][data.length]; + + for (int x = 0; x < data.length; x++) + for (int y = 0; y < data.length; y++) { + double sum = 0; + for (int u = 0; u < data.length; u++) + for (int v = 0; v < data.length; v++) { + final double alphaMul = (u == 0 ? alpha0 : alpha) * (v == 0 ? alpha0 : alpha); + final double cosMul = ImagesUtil.cosDCT(x, u, data.length) * ImagesUtil.cosDCT(y, v, data.length); + sum += alphaMul * data[u][v] * cosMul; + } + result[x][y] = sum; + } + return result; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DFT.java b/src/main/java/berack96/multimedia/transforms/DFT.java new file mode 100644 index 0000000..f395c9a --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DFT.java @@ -0,0 +1,26 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.Transform; + +public class DFT implements Transform { + + @Override + public Complex[] transform(double[] data) { + final Complex[] result = new Complex[data.length]; + + for (int k = 0; k < result.length; k++) { + Complex num = new Complex(); + for (int x = 0; x < data.length; x++) { + double angle = (2 * Math.PI * k * x) / data.length; + num.addReal(data[x] * Math.cos(angle)); + num.addImaginary(data[x] * Math.sin(angle)); + } + num.setReal(num.getReal() / data.length); + num.setImaginary(-num.getImaginary() / data.length); + + result[k] = num; + } + + return result; + } +} diff --git a/src/main/java/berack96/multimedia/transforms/DFTInverse.java b/src/main/java/berack96/multimedia/transforms/DFTInverse.java new file mode 100644 index 0000000..a388aa3 --- /dev/null +++ b/src/main/java/berack96/multimedia/transforms/DFTInverse.java @@ -0,0 +1,17 @@ +package berack96.multimedia.transforms; + +import berack96.multimedia.Transform; + +public class DFTInverse implements Transform { + @Override + public double[] transform(Complex[] data) { + final double[] result = new double[data.length]; + + for (int k = 0; k < result.length; k++) + for (int x = 0; x < data.length; x++) { + double angle = (2 * Math.PI * k * x) / data.length; + result[k] += data[x].getReal() * Math.cos(angle) - data[x].getImaginary() * Math.sin(angle); + } + return result; + } +} diff --git a/src/main/java/berack96/multimedia/view/Main.java b/src/main/java/berack96/multimedia/view/Main.java new file mode 100644 index 0000000..23460a5 --- /dev/null +++ b/src/main/java/berack96/multimedia/view/Main.java @@ -0,0 +1,145 @@ +package berack96.multimedia.view; + +import javax.imageio.ImageIO; +import javax.swing.ImageIcon; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileNameExtensionFilter; + +import berack96.multimedia.ImagesUtil; +import berack96.multimedia.composting.AlphaBlend; +import berack96.multimedia.composting.ChromaKeying; +import berack96.multimedia.composting.ChromaKeying3D; +import berack96.multimedia.compression.JPEG; +import berack96.multimedia.filters.Convolution; +import berack96.multimedia.filters.FilterFactory; +import berack96.multimedia.resize.Bicubic; +import berack96.multimedia.resize.Bilinear; +import berack96.multimedia.resize.NearestNeighbor; + +import java.awt.Toolkit; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.function.Supplier; + +public class Main { + final static String PATH = "src/resources/sample/"; + + public static void main(String[] args) throws Exception { + ImagesUtil.maxThreads = 4; + runEditor(); + } + + static private JFrame runEditor() { + var imageFrame = new JFrame("Simple Image Editor") { public BufferedImage image; public JLabel label;}; + + // All the menu options + var utility = new JMenu("File"); + utility.add(menuItem("Save", () -> showChooser(false, imageFrame.image))); + utility.add(menuItem("Load", () -> showChooser(true, imageFrame.image))); + + var compose = new JMenu("Compose"); + compose.add(menuItem("Alpha Blend", () -> new AlphaBlend(0.2, 0.8).transform(imageFrame.image, showChooser(true, imageFrame.image, true)))); + compose.add(menuItem("Chroma Keying", () -> new ChromaKeying(true).transform(imageFrame.image, showChooser(true, imageFrame.image, true)))); + compose.add(menuItem("Chroma Keying 3D", () -> new ChromaKeying3D(180, 120, 0, 255, 0).transform(imageFrame.image, showChooser(true, imageFrame.image, true)))); + + var compress = new JMenu("Compression"); + compress.add(menuItem("JPEG", () -> new JPEG().process(imageFrame.image, 1))); + + var filters = new JMenu("Filters"); + var kernelSize = 5; + filters.add(menuItem("Blur", () -> new Convolution(FilterFactory.getLowPass(kernelSize)).transform(imageFrame.image))); + filters.add(menuItem("Sharpen", () -> new Convolution(FilterFactory.getSharpen(kernelSize)).transform(imageFrame.image))); + filters.add(menuItem("Ridge Detection", () -> new Convolution(FilterFactory.getHighPass(kernelSize)).transform(imageFrame.image))); + filters.add(menuItem("Gaussian Blur", () -> new Convolution(FilterFactory.getGaussian(kernelSize, 1)).transform(imageFrame.image))); + filters.add(menuItem("Gaussian Sharpen", () -> new Convolution(FilterFactory.getGaussianSharp(kernelSize, 1)).transform(imageFrame.image))); + + var resizes = new JMenu("Resizes"); + var ratio = new float[]{1.5f, 0.75f}; + for(int i=0; i new NearestNeighbor(ratioVal).transform(imageFrame.image))); + resizes.add(menuItem(ratioStr + " Bilinear", () -> new Bilinear(ratioVal).transform(imageFrame.image))); + resizes.add(menuItem(ratioStr + " Bicubic", () -> new Bicubic(ratioVal).transform(imageFrame.image))); + } + + var menuBar = new JMenuBar(); + menuBar.add(utility); + menuBar.add(compress); + menuBar.add(compose); + menuBar.add(filters); + menuBar.add(resizes); + + // The frame initialization + var dim = Toolkit.getDefaultToolkit().getScreenSize(); + imageFrame.label = new JLabel(); + imageFrame.add(new JScrollPane(imageFrame.label)); + imageFrame.setJMenuBar(menuBar); + imageFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + imageFrame.setSize(dim.width / 2, dim.height / 2); + imageFrame.setLocationRelativeTo(null); + imageFrame.setResizable(true); + imageFrame.setVisible(true); + + return imageFrame; + } + + static private BufferedImage showChooser(boolean open, BufferedImage image) { + return showChooser(open, image, false); + } + static private BufferedImage showChooser(boolean open, BufferedImage image, boolean sameSize) { + var chooser = new JFileChooser(); + chooser.setCurrentDirectory(new File(".")); + chooser.setFileFilter(new FileNameExtensionFilter("PNG, JPEG", "png", "jpg", "jpeg")); + try { + if(open && chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { + var temp = ImageIO.read(chooser.getSelectedFile()); + if(sameSize && image != null && temp != null && (image.getWidth() != temp.getWidth() || image.getHeight() != temp.getHeight())) + temp = new Bicubic(1).setRatio((double) image.getWidth() / temp.getWidth(), (double) image.getHeight() / temp.getHeight()).transform(temp); + image = temp; + } + if(!open && chooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) { + var file = chooser.getSelectedFile(); + ImageIO.write(image, "png", file); + if(!file.getName().endsWith(".png")) + file.renameTo(new File(file.getPath() + ".png")); + } + } catch (IOException e) { + e.printStackTrace(); + } + return image; + } + + static private JMenuItem menuItem(String name, Supplier supplier) { + var item = new JMenuItem(name); + item.addActionListener((event) -> { + try { + var image = supplier.get(); + if(image == null) return; + + var popup = (JPopupMenu) item.getParent(); + var frame = (JFrame) SwingUtilities.getRoot(popup.getInvoker()); + + var fieldIco = frame.getClass().getField("label"); + ((JLabel) fieldIco.get(frame)).setIcon(new ImageIcon(image)); + + var fieldImg = frame.getClass().getField("image"); + fieldImg.set(frame, image); + + frame.pack(); + } catch (Exception e) { + e.printStackTrace(); + } + }); + return item; + } +} diff --git a/src/test/java/berack96/test/multimedia/MultimediaTest.java b/src/test/java/berack96/test/multimedia/MultimediaTest.java new file mode 100644 index 0000000..1422c77 --- /dev/null +++ b/src/test/java/berack96/test/multimedia/MultimediaTest.java @@ -0,0 +1,68 @@ +package berack96.test.multimedia; + +import org.junit.jupiter.api.Test; + +import berack96.multimedia.transforms.*; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class MultimediaTest { + final static double DECIMAL_ERR = 0.0000000001; + + @Test + public void testDCT1D() { + DCT1D dct = new DCT1D(); + DCT1DInverse inv = new DCT1DInverse(); + double[] src = new double[]{20, 12, 18, 56, 83, 110, 104, 115}; + double[] trs = dct.transform(src); + assertArrayEquals(new double[]{183.1, -113.0, -4.1, 22.1, 10.6, -1.5, 4.8, -8.7}, trs, 0.05); // round error + double[] rev = inv.transform(trs); + assertArrayEquals(src, rev, DECIMAL_ERR); + + src = new double[]{15, 186, 15, 498, 85, 45, 864, 846, 468, 7564, 4, 0, 39, 57, 84, 19}; + trs = inv.transform(dct.transform(src)); + assertArrayEquals(src, trs, DECIMAL_ERR); + + src = createRandom(1000); + trs = inv.transform(dct.transform(src)); + assertArrayEquals(src, trs, DECIMAL_ERR); + } + + @Test + public void testDCT2D() { + DCT2D dct = new DCT2D(); + DCT2DInverse inv = new DCT2DInverse(); + double[][] mrx = new double[][]{{15.62214, 45.6}, {30.36, 51.014}}; + double[][] res = inv.transform(dct.transform(mrx)); + for (int i = 0; i < res.length; i++) + assertArrayEquals(mrx[i], res[i], DECIMAL_ERR); + + int lenRand = 32; + mrx = new double[lenRand][]; + for (int i = 0; i < mrx.length; i++) + mrx[i] = createRandom(lenRand); + res = inv.transform(dct.transform(mrx)); + for (int i = 0; i < res.length; i++) + assertArrayEquals(mrx[i], res[i], DECIMAL_ERR); + } + + @Test + public void testDFT() { + DFT dft = new DFT(); + DFTInverse inv = new DFTInverse(); + double[] data = new double[]{15.62214, 45.6, 94.63, 10.85, 2.85}; + double[] res = inv.transform(dft.transform(data)); + assertArrayEquals(data, res, DECIMAL_ERR); + + data = createRandom(1000); + res = inv.transform(dft.transform(data)); + assertArrayEquals(data, res, DECIMAL_ERR); + } + + static private double[] createRandom(int length) { + double[] data = new double[length]; + for (int i = 0; i < data.length; i++) + data[i] = (Math.random() - 0.2) * 100; + return data; + } +}