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