diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..14d58de --- /dev/null +++ b/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + org.andrewla + convolution + 1.0-SNAPSHOT + + + 11 + 11 + UTF-8 + + + + + commons-cli + commons-cli + 1.11.0 + compile + + + + diff --git a/src/main/java/org/andrewla/ImageProcessor.java b/src/main/java/org/andrewla/ImageProcessor.java new file mode 100644 index 0000000..ce7eaec --- /dev/null +++ b/src/main/java/org/andrewla/ImageProcessor.java @@ -0,0 +1,13 @@ +package org.andrewla; + +import javax.imageio.ImageReader; +import java.awt.image.BufferedImage; +import java.util.List; + +public interface ImageProcessor { + void addKernel(Kernel kernel); + + void addImageReader(ImageReader reader); + + List applyFilters(); +} diff --git a/src/main/java/org/andrewla/Kernel.java b/src/main/java/org/andrewla/Kernel.java new file mode 100644 index 0000000..0dcac28 --- /dev/null +++ b/src/main/java/org/andrewla/Kernel.java @@ -0,0 +1,15 @@ +package org.andrewla; + +public interface Kernel { + int getSize(); + + double getValue(int x, int y); + + double getBias(); + + double getFactor(); + + Kernel getResized(int newSize); + + Kernel getExpanded(int newSize); +} diff --git a/src/main/java/org/andrewla/Main.java b/src/main/java/org/andrewla/Main.java new file mode 100644 index 0000000..12fb261 --- /dev/null +++ b/src/main/java/org/andrewla/Main.java @@ -0,0 +1,7 @@ +package org.andrewla; + +public class Main { + public static void main(String[] args) { + System.out.println("Image filters"); + } +} diff --git a/src/main/java/org/andrewla/kernels/AbstractKernel.java b/src/main/java/org/andrewla/kernels/AbstractKernel.java new file mode 100644 index 0000000..0e5457b --- /dev/null +++ b/src/main/java/org/andrewla/kernels/AbstractKernel.java @@ -0,0 +1,111 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +import java.util.Objects; + +public abstract class AbstractKernel implements Kernel { + private int size; + + private double[][] data; + + private double factor; + private double bias; + + protected AbstractKernel(int size) { + if (size % 2 == 0) { + throw new IllegalArgumentException("Size of kernel matrix should be odd"); + } + + this.size = size; + this.data = new double[size][size]; + this.factor = 1; + this.bias = 0; + } + + @Override + public int getSize() { + return size; + } + + @Override + public double getValue(int x, int y) { + if (x < 0 || x > size) { + throw new IllegalArgumentException("Invalid X coordinate"); + } + + if (y < 0 || y > size) { + throw new IllegalArgumentException("Invalid Y coordinate"); + } + + return data[y][x]; + } + + protected void setValue(int x, int y, double value) { + if (x < 0 || x > size) { + throw new IllegalArgumentException("Invalid X coordinate"); + } + + if (y < 0 || y > size) { + throw new IllegalArgumentException("Invalid Y coordinate"); + } + + data[y][x] = value; + } + + @Override + public double getBias() { + return bias; + } + + protected void setBias(double bias) { + this.bias = bias; + } + + @Override + public double getFactor() { + return factor; + } + + protected void setFactor() { + var sum = 0.0; + + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + sum += data[i][j]; + } + } + + factor = 1 / sum; + } + + protected Kernel expandWithZeros(int newSize) { + if (newSize <= this.size) { + return this; + } + + if (newSize % 2 == 0) { + throw new IllegalArgumentException("Size of kernel matrix should be odd"); + } + + var newData = new double[newSize][newSize]; + var expansion = newSize - size; + + for (int i = 0; i < size; i++) { + System.arraycopy(data[i], 0, newData[i + expansion], expansion, size); + } + + this.data = newData; + this.size = newSize; + + return this; + } + + @Override + public boolean equals(Object other) { + if (other == null || getClass() != other.getClass()) return false; + + AbstractKernel that = (AbstractKernel) other; + return size == that.size && Double.compare(factor, that.factor) == 0 && Objects.deepEquals(data, that.data); + } +} diff --git a/src/main/java/org/andrewla/kernels/BoxBlur.java b/src/main/java/org/andrewla/kernels/BoxBlur.java new file mode 100644 index 0000000..94d19fe --- /dev/null +++ b/src/main/java/org/andrewla/kernels/BoxBlur.java @@ -0,0 +1,22 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class BoxBlur extends AbstractKernel { + private final double blurRadius; + + public BoxBlur(int size, double blurRadius) { + super(size); + this.blurRadius = blurRadius; + } + + @Override + public Kernel getResized(int newSize) { + return null; + } + + @Override + public Kernel getExpanded(int newSize) { + return new BoxBlur(getSize(), blurRadius).expandWithZeros(newSize); + } +} diff --git a/src/main/java/org/andrewla/kernels/Emboss.java b/src/main/java/org/andrewla/kernels/Emboss.java new file mode 100644 index 0000000..83ba514 --- /dev/null +++ b/src/main/java/org/andrewla/kernels/Emboss.java @@ -0,0 +1,34 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class Emboss extends AbstractKernel { + public Emboss(int size) { + super(size); + + for (int i = 0; i < size; i++) { + for (int j = 0; j <= i; j++) { + setValue(i, j, -1); + } + + setValue(i, size - 1 - i, 0); + + for (int j = i + 1; j < size; j++) { + setValue(i, j, 1); + } + } + + setFactor(); + setBias(128); + } + + @Override + public Kernel getResized(int newSize) { + return new Emboss(newSize); + } + + @Override + public Kernel getExpanded(int newSize) { + return new Emboss(getSize()).expandWithZeros(newSize); + } +} diff --git a/src/main/java/org/andrewla/kernels/GaussianBlur.java b/src/main/java/org/andrewla/kernels/GaussianBlur.java new file mode 100644 index 0000000..745795a --- /dev/null +++ b/src/main/java/org/andrewla/kernels/GaussianBlur.java @@ -0,0 +1,41 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class GaussianBlur extends AbstractKernel { + private final double blurRadius; + + public GaussianBlur(int size, double blurRadius) { + super(size); + this.blurRadius = blurRadius; + + var center = size / 2; + + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + var x = (double) (j - center); + var y = (double) (i - center); + + var s2 = 2 * blurRadius * blurRadius; + var r2 = x * x + y * y; + + var value = Math.exp(-r2 / s2) / (Math.PI * s2); + + setValue(i, j, value); + } + } + + setFactor(); + setBias(0); + } + + @Override + public Kernel getResized(int newSize) { + return new GaussianBlur(newSize, blurRadius); + } + + @Override + public Kernel getExpanded(int newSize) { + return new GaussianBlur(getSize(), blurRadius).expandWithZeros(newSize); + } +} diff --git a/src/main/java/org/andrewla/kernels/Identity.java b/src/main/java/org/andrewla/kernels/Identity.java new file mode 100644 index 0000000..f896608 --- /dev/null +++ b/src/main/java/org/andrewla/kernels/Identity.java @@ -0,0 +1,29 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class Identity extends AbstractKernel { + public Identity(int size) { + super(size); + + var center = size / 2; + + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + setValue(i, j, 0); + } + } + + setValue(center, center, 1); + } + + @Override + public Kernel getResized(int newSize) { + return new Identity(newSize); + } + + @Override + public Kernel getExpanded(int newSize) { + return new Identity(newSize); + } +} diff --git a/src/main/java/org/andrewla/kernels/MotionBlur.java b/src/main/java/org/andrewla/kernels/MotionBlur.java new file mode 100644 index 0000000..6330352 --- /dev/null +++ b/src/main/java/org/andrewla/kernels/MotionBlur.java @@ -0,0 +1,40 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class MotionBlur extends AbstractKernel { + private final double angle; + + public MotionBlur(int size, double angle) { + super(size); + this.angle = angle; + + var angleRadians = Math.toRadians(angle); + + var sin = Math.sin(angleRadians); + var cos = Math.cos(angleRadians); + + var center = size / 2; + + for (int i = 0; i < size; i++) { + int x = (int) Math.round((i - center) * cos); + int y = (int) Math.round((i - center) * sin); + if (Math.abs(x) <= center && Math.abs(y) <= center) { + setValue(x + center, y + center, 1); + } + } + + setFactor(); + setBias(0.0); + } + + @Override + public Kernel getResized(int newSize) { + return new MotionBlur(newSize, angle); + } + + @Override + public Kernel getExpanded(int newSize) { + return new MotionBlur(newSize, angle).expandWithZeros(newSize); + } +} diff --git a/src/main/java/org/andrewla/kernels/Sharpen.java b/src/main/java/org/andrewla/kernels/Sharpen.java new file mode 100644 index 0000000..740cd53 --- /dev/null +++ b/src/main/java/org/andrewla/kernels/Sharpen.java @@ -0,0 +1,51 @@ +package org.andrewla.kernels; + +import org.andrewla.Kernel; + +public class Sharpen extends AbstractKernel { + public Sharpen(int size) { + super(size); + + if (size == 3) { + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + setValue(i, j, -1); + } + } + + setValue(1, 1, 9); + } + else if (size == 5) { + for (int i = 0; i < 5; i++) { + setValue(i, 0, -1); + setValue(i, 4, -1); + setValue(0, i, -1); + setValue(4, i, -1); + } + for (int i = 1; i < 4; i++) { + setValue(i, 1, 2); + setValue(i, 3, 2); + setValue(1, i, 2); + setValue(3, i, 2); + } + + setValue(2, 2, 8); + } + else { + throw new IllegalArgumentException("Size of sharpen kernel can be either 3 or 5"); + } + + setFactor(); + setBias(0); + } + + @Override + public Kernel getResized(int newSize) { + return new Sharpen(newSize); + } + + @Override + public Kernel getExpanded(int newSize) { + return new Sharpen(getSize()).expandWithZeros(newSize); + } +} diff --git a/src/main/java/org/andrewla/processors/BaseMultipleProcessor.java b/src/main/java/org/andrewla/processors/BaseMultipleProcessor.java new file mode 100644 index 0000000..8447693 --- /dev/null +++ b/src/main/java/org/andrewla/processors/BaseMultipleProcessor.java @@ -0,0 +1,23 @@ +package org.andrewla.processors; + +import org.andrewla.ImageProcessor; +import org.andrewla.Kernel; + +import javax.imageio.ImageReader; +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseMultipleProcessor implements ImageProcessor { + private final List kernels = new ArrayList<>(); + private final List readers = new ArrayList<>(); + + @Override + public void addKernel(Kernel kernel) { + kernels.add(kernel); + } + + @Override + public void addImageReader(ImageReader reader) { + readers.add(reader); + } +} diff --git a/src/main/java/org/andrewla/processors/BaseSingleProcessor.java b/src/main/java/org/andrewla/processors/BaseSingleProcessor.java new file mode 100644 index 0000000..3c0eb8a --- /dev/null +++ b/src/main/java/org/andrewla/processors/BaseSingleProcessor.java @@ -0,0 +1,27 @@ +package org.andrewla.processors; + +import org.andrewla.ImageProcessor; +import org.andrewla.Kernel; + +import javax.imageio.ImageReader; +import java.util.ArrayList; +import java.util.List; + +public abstract class BaseSingleProcessor implements ImageProcessor { + private final List kernels = new ArrayList<>(); + private ImageReader reader = null; + + @Override + public void addKernel(Kernel kernel) { + kernels.add(kernel); + } + + @Override + public void addImageReader(ImageReader reader) { + if (this.reader == null) { + this.reader = reader; + } else { + throw new RuntimeException("Image reader was already set"); + } + } +} diff --git a/src/main/java/org/andrewla/processors/ParallelSingleProcessor.java b/src/main/java/org/andrewla/processors/ParallelSingleProcessor.java new file mode 100644 index 0000000..53cc1e4 --- /dev/null +++ b/src/main/java/org/andrewla/processors/ParallelSingleProcessor.java @@ -0,0 +1,11 @@ +package org.andrewla.processors; + +import java.awt.image.BufferedImage; +import java.util.List; + +public class ParallelSingleProcessor extends BaseSingleProcessor { + @Override + public List applyFilters() { + return List.of(); + } +} diff --git a/src/main/java/org/andrewla/processors/SequentialSingleProcessor.java b/src/main/java/org/andrewla/processors/SequentialSingleProcessor.java new file mode 100644 index 0000000..65877f5 --- /dev/null +++ b/src/main/java/org/andrewla/processors/SequentialSingleProcessor.java @@ -0,0 +1,71 @@ +package org.andrewla.processors; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.util.List; + +public class SequentialSingleProcessor extends BaseSingleProcessor { + @Override + public List applyFilters() { + BufferedImage src; + + try { + src = reader.read(0); + } catch (IOException e) { + throw new RuntimeException(e); + } + + final var width = src.getWidth() / 2; + final var height = src.getHeight() / 2; + + BufferedImage out = copyImage(src); + + for (var k : kernels) { + final var kSize = k.getSize(); + final var kCenter = kSize / 2; + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + var r = 0.0; + var g = 0.0; + var b = 0.0; + + for (int kx = 0; kx < kSize; kx++) { + for (int ky = 0; ky < kSize; ky++) { + final var px = clamp(x + kx, 0, width - 1); + final var py = clamp(y + ky, 0, height - 1); + + final var rgb = src.getRGB(px, py); + final var red = (rgb >> 16) & 0xFF; + final var green = (rgb >> 8) & 0xFF; + final var blue = rgb & 0xFF; + + final var value = k.getValue(kx - kCenter, ky - kCenter); + + r += red * value; + g += green * value; + b += blue * value; + } + } + + final var bias = k.getBias(); + final var factor = k.getFactor(); + + final var nr = clampPixel((int) (r * factor + bias)); + final var ng = clampPixel((int) (g * factor + bias)); + final var nb = clampPixel((int) (b * factor + bias)); + + var pixel = 0xFF000000; + + pixel |= nr << 16; + pixel |= ng << 8; + pixel |= nb; + + out.setRGB(x, y, pixel); + } + } + } + + return List.of(out); + } +}