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;
+ }
+}