Load of the 'finished' project

This commit is contained in:
2022-10-06 21:50:32 +02:00
parent 5f25b8d8e5
commit 26679d3d2a
38 changed files with 1243 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

BIN
images/composed-ALPHA.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 495 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
images/filters-GSHARP.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

BIN
images/sample/Beach.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

BIN
images/sample/Guinness.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 919 KiB

BIN
images/sample/Musician.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

BIN
images/sample/Sample.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

BIN
images/sample/SampleCol.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 KiB

BIN
images/sample/salvini.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
images/sample/zztopBW.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 KiB

61
pom.xml Normal file
View 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>

View 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();
}
}
}

View 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);
}

View 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));
}
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
}

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

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

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

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

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

View File

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

View 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());
}
}

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

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

View File

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

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

View File

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

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

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

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

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