Load of the 'finished' project
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
target/
|
||||||
BIN
images/composed-ALPHA.png
Normal file
|
After Width: | Height: | Size: 495 KiB |
BIN
images/composed-TV-GUINNES.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
images/filters-GSHARP.png
Normal file
|
After Width: | Height: | Size: 702 KiB |
BIN
images/sample/Beach.jpg
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
images/sample/ChromaKeyBlue.jpg
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
images/sample/ChromaKeyGreen.jpg
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
images/sample/Guinness.jpg
Normal file
|
After Width: | Height: | Size: 919 KiB |
BIN
images/sample/Musician.jpg
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
images/sample/Sample.jpg
Normal file
|
After Width: | Height: | Size: 457 KiB |
BIN
images/sample/SampleCol.jpg
Normal file
|
After Width: | Height: | Size: 752 KiB |
BIN
images/sample/TestChroma.jpg
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
images/sample/YosemiteBW.jpg
Normal file
|
After Width: | Height: | Size: 409 KiB |
BIN
images/sample/salvini.jpg
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
images/sample/zztopBW.jpg
Normal file
|
After Width: | Height: | Size: 386 KiB |
61
pom.xml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<groupId>berack96</groupId>
|
||||||
|
<artifactId>Multimedia</artifactId>
|
||||||
|
<version>1.0</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.aparapi</groupId>
|
||||||
|
<artifactId>aparapi</artifactId>
|
||||||
|
<version>2.0.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>RELEASE</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-jar-plugin</artifactId>
|
||||||
|
<version>2.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-assembly-plugin</artifactId>
|
||||||
|
<version>2.2</version>
|
||||||
|
<configuration>
|
||||||
|
<descriptorRefs>
|
||||||
|
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||||
|
</descriptorRefs>
|
||||||
|
<archive>
|
||||||
|
<manifest>
|
||||||
|
<mainClass>berack96.multimedia.view.Main</mainClass>
|
||||||
|
</manifest>
|
||||||
|
</archive>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>single</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
178
src/main/java/berack96/multimedia/ImagesUtil.java
Normal file
@@ -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.<br>
|
||||||
|
* 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<BufferedImage> program) {
|
||||||
|
long time = System.currentTimeMillis();
|
||||||
|
final AtomicReference<BufferedImage> 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.<br>
|
||||||
|
* 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.<br>
|
||||||
|
* 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.<br>
|
||||||
|
* 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<br>
|
||||||
|
* Il consumer passato avra' in input le coordinate del pixel sottoforma (x,y)<br>
|
||||||
|
* In base a come viene settata la variabile {@link #maxThreads} il metodo potrebbe creare dei
|
||||||
|
* threads per l'elaborazione dei pixel<br>
|
||||||
|
* 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<Integer, Integer> consumer) {
|
||||||
|
forEachPixel(raster, 1, consumer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metodo che serve per iterare ogni pixel del raster passato e applicargli la funzione specificata<br>
|
||||||
|
* Il consumer passato avra' in input le coordinate del pixel sottoforma (x,y)<br>
|
||||||
|
* In base a come viene settata la variabile {@link #maxThreads} il metodo potrebbe creare dei
|
||||||
|
* threads per l'elaborazione dei pixel<br>
|
||||||
|
* Questo metodo accetta un delta che serve per spostarsi da un pixel a quello successivo saltando eventuali pixel intermedi<br>
|
||||||
|
* 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<Integer, Integer> 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.<br>
|
||||||
|
* 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<Integer, Integer> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/main/java/berack96/multimedia/Transform.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package berack96.multimedia;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interfaccia generica per fare delle trasformate matematiche
|
||||||
|
* @param <O> L'oggetto della trasformata
|
||||||
|
* @param <R> L'oggetto trasformato
|
||||||
|
*/
|
||||||
|
public interface Transform<O, R> {
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
31
src/main/java/berack96/multimedia/composting/AlphaBlend.java
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<BufferedImage[], BufferedImage> {
|
||||||
|
@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);
|
||||||
|
}
|
||||||
166
src/main/java/berack96/multimedia/compression/JPEG.java
Normal file
@@ -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:<br>
|
||||||
|
* - 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
61
src/main/java/berack96/multimedia/filters/Convolution.java
Normal file
@@ -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<BufferedImage, BufferedImage> {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/main/java/berack96/multimedia/filters/FilterFactory.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/main/java/berack96/multimedia/resize/Bicubic.java
Normal file
@@ -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;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/main/java/berack96/multimedia/resize/Bilinear.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/main/java/berack96/multimedia/resize/Resizing.java
Normal file
@@ -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<BufferedImage, BufferedImage> {
|
||||||
|
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.<br>
|
||||||
|
* Per valori 0 > x > 1 avverra' un down-scaling<br>
|
||||||
|
* 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/main/java/berack96/multimedia/transforms/Complex.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/main/java/berack96/multimedia/transforms/DCT1D.java
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.ImagesUtil;
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
|
||||||
|
public class DCT1D implements Transform<double[], double[]> {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
import berack96.multimedia.ImagesUtil;
|
||||||
|
|
||||||
|
public class DCT1DInverse implements Transform<double[], double[]> {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/main/java/berack96/multimedia/transforms/DCT2D.java
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
import berack96.multimedia.ImagesUtil;
|
||||||
|
|
||||||
|
public class DCT2D implements Transform<double[][], double[][]> {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
import berack96.multimedia.ImagesUtil;
|
||||||
|
|
||||||
|
public class DCT2DInverse implements Transform<double[][], double[][]> {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/main/java/berack96/multimedia/transforms/DFT.java
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
|
||||||
|
public class DFT implements Transform<double[], Complex[]> {
|
||||||
|
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/main/java/berack96/multimedia/transforms/DFTInverse.java
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package berack96.multimedia.transforms;
|
||||||
|
|
||||||
|
import berack96.multimedia.Transform;
|
||||||
|
|
||||||
|
public class DFTInverse implements Transform<Complex[], double[]> {
|
||||||
|
@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;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
src/main/java/berack96/multimedia/view/Main.java
Normal file
@@ -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<ratio.length; i++) {
|
||||||
|
final var ratioVal = ratio[i];
|
||||||
|
final var ratioStr = String.format("x%1.2f", ratioVal);
|
||||||
|
resizes.add(menuItem(ratioStr + " NearestNeighbor", () -> 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<BufferedImage> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
68
src/test/java/berack96/test/multimedia/MultimediaTest.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||