From 4453b77e0f24afa2aa8ce4eab9aca8c7e158fc49 Mon Sep 17 00:00:00 2001 From: Pacien TRAN-GIRARD Date: Sat, 10 Oct 2015 10:39:03 +0200 Subject: Bootstrap project --- src/main/java/Color.java | 120 ++++++++++++++++++++++++++ src/main/java/Filter.java | 76 +++++++++++++++++ src/main/java/Helper.java | 140 ++++++++++++++++++++++++++++++ src/main/java/Main.java | 51 +++++++++++ src/main/java/Seam.java | 75 ++++++++++++++++ src/main/java/SignatureChecks.java | 37 ++++++++ src/test/java/Tests.java | 169 +++++++++++++++++++++++++++++++++++++ 7 files changed, 668 insertions(+) create mode 100644 src/main/java/Color.java create mode 100644 src/main/java/Filter.java create mode 100644 src/main/java/Helper.java create mode 100644 src/main/java/Main.java create mode 100644 src/main/java/Seam.java create mode 100644 src/main/java/SignatureChecks.java create mode 100644 src/test/java/Tests.java (limited to 'src') diff --git a/src/main/java/Color.java b/src/main/java/Color.java new file mode 100644 index 0000000..4597b26 --- /dev/null +++ b/src/main/java/Color.java @@ -0,0 +1,120 @@ +/** + * @author Pacien TRAN-GIRARD + * @author Timothée FLOURE + */ +public final class Color { + + /** + * Returns red component from given packed color. + * + * @param rgb 32-bits RGB color + * @return a float between 0.0 and 1.0 + * @see #getGreen + * @see #getBlue + * @see #getRGB(float, float, float) + */ + public static float getRed(int rgb) { + // TODO getRed + return 0.0f; + } + + /** + * Returns green component from given packed color. + * + * @param rgb 32-bits RGB color + * @return a float between 0.0 and 1.0 + * @see #getRed + * @see #getBlue + * @see #getRGB(float, float, float) + */ + public static float getGreen(int rgb) { + // TODO getGreen + return 0.0f; + } + + /** + * Returns blue component from given packed color. + * + * @param rgb 32-bits RGB color + * @return a float between 0.0 and 1.0 + * @see #getRed + * @see #getGreen + * @see #getRGB(float, float, float) + */ + public static float getBlue(int rgb) { + // TODO getBlue + return 0.0f; + } + + /** + * Returns the average of red, green and blue components from given packed color. + * + * @param rgb 32-bits RGB color + * @return a float between 0.0 and 1.0 + * @see #getRed + * @see #getGreen + * @see #getBlue + * @see #getRGB(float) + */ + public static float getGray(int rgb) { + // TODO getGray + return 0.0f; + } + + /** + * Returns packed RGB components from given red, green and blue components. + * + * @param red a float between 0.0 and 1.0 + * @param green a float between 0.0 and 1.0 + * @param blue a float between 0.0 and 1.0 + * @return 32-bits RGB color + * @see #getRed + * @see #getGreen + * @see #getBlue + */ + public static int getRGB(float red, float green, float blue) { + // TODO getRGB + return 0; + } + + /** + * Returns packed RGB components from given grayscale value. + * + * @param red a float between 0.0 and 1.0 + * @param green a float between 0.0 and 1.0 + * @param blue a float between 0.0 and 1.0 + * @return 32-bits RGB color + * @see #getGray + */ + public static int getRGB(float gray) { + // TODO getRGB + return 0; + } + + /** + * Converts packed RGB image to grayscale float image. + * + * @param image a HxW int array + * @return a HxW float array + * @see #toRGB + * @see #getGray + */ + public static float[][] toGray(int[][] image) { + // TODO toGray + return null; + } + + /** + * Converts grayscale float image to packed RGB image. + * + * @param channels a HxW float array + * @return a HxW int array + * @see #toGray + * @see #getRGB(float) + */ + public static int[][] toRGB(float[][] gray) { + // TODO toRGB + return null; + } + +} diff --git a/src/main/java/Filter.java b/src/main/java/Filter.java new file mode 100644 index 0000000..ddc3e92 --- /dev/null +++ b/src/main/java/Filter.java @@ -0,0 +1,76 @@ +/** + * @author Pacien TRAN-GIRARD + * @author Timothée FLOURE + */ +public final class Filter { + + /** + * Get a pixel without accessing out of bounds + * + * @param gray a HxW float array + * @param row Y coordinate + * @param col X coordinate + * @return nearest valid pixel color + */ + public static float at(float[][] gray, int row, int col) { + // TODO at + return 0.0f; + } + + /** + * Convolve a single-channel image with specified kernel. + * + * @param gray a HxW float array + * @param kernel a MxN float array, with M and N odd + * @return a HxW float array + */ + public static float[][] filter(float[][] gray, float[][] kernel) { + // TODO filter + return null; + } + + /** + * Smooth a single-channel image + * + * @param gray a HxW float array + * @return a HxW float array + */ + public static float[][] smooth(float[][] gray) { + // TODO smooth + return null; + } + + /** + * Compute horizontal Sobel filter + * + * @param gray a HxW float array + * @return a HxW float array + */ + public static float[][] sobelX(float[][] gray) { + // TODO sobelX + return null; + } + + /** + * Compute vertical Sobel filter + * + * @param gray a HxW float array + * @return a HxW float array + */ + public static float[][] sobelY(float[][] gray) { + // TODO sobelY + return null; + } + + /** + * Compute the magnitude of combined Sobel filters + * + * @param gray a HxW float array + * @return a HxW float array + */ + public static float[][] sobel(float[][] gray) { + // TODO sobel + return null; + } + +} diff --git a/src/main/java/Helper.java b/src/main/java/Helper.java new file mode 100644 index 0000000..b7a139a --- /dev/null +++ b/src/main/java/Helper.java @@ -0,0 +1,140 @@ +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +/** + * Provide simple tools to read, write and show pictures. + */ +public final class Helper { + + // Convert specified BufferedImage into an array + private static int[][] fromBufferedImage(BufferedImage image) { + int width = image.getWidth(); + int height = image.getHeight(); + int[][] array = new int[height][width]; + for (int row = 0; row < height; ++row) { + for (int col = 0; col < width; ++col) { + array[row][col] = image.getRGB(col, row); + } + } + return array; + } + + // Convert specified array int a BufferedImage + private static BufferedImage toBufferedImage(int[][] array) { + int width = array[0].length; + int height = array.length; + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + for (int row = 0; row < height; ++row) { + for (int col = 0; col < width; ++col) { + image.setRGB(col, row, array[row][col]); + } + } + return image; + } + + /** + * Reads specified image from disk. + * + * @param path Input file path + * @return HxW array of packed RGB colors, or null on failure + * @see #write + */ + public static int[][] read(String path) { + try { + BufferedImage image = ImageIO.read(new File(path)); + return fromBufferedImage(image); + } catch (IOException e) { + return null; + } + } + + /** + * Writes specified image to disk. + * + * @param path Output file path + * @param array HxW array of packed RGB colors + * @return {@code true} if write operation was successful, {@code false} otherwise + * @see #read + */ + public static boolean write(String path, int[][] array) { + + // Convert array to Java image + BufferedImage image = toBufferedImage(array); + + // Get desired file format + int index = path.lastIndexOf('.'); + if (index < 0) + return false; + String extension = path.substring(index + 1); + + // Export image + try { + return ImageIO.write(image, extension, new File(path)); + } catch (IOException e) { + return false; + } + + } + + /** + * Shows specified image in a window. + * + * @param array HxW array of packed RGB colors + * @param title title to be displayed + */ + public static void show(int[][] array, String title) { + + // Convert array to Java image + final BufferedImage image = toBufferedImage(array); + + // Create a panel to render this image + @SuppressWarnings("serial") + JPanel panel = new JPanel() { + @Override + protected void paintComponent(Graphics g) { + super.paintComponent(g); + g.drawImage(image, 0, 0, getWidth(), getHeight(), null, null); + } + }; + + // Create a frame to hold this panel + final JFrame frame = new JFrame(title); + frame.add(panel); + frame.pack(); + frame.setSize(image.getWidth(), image.getHeight()); + + // Register closing event + frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE); + frame.addWindowListener(new WindowAdapter() { + @Override + public void windowClosing(WindowEvent e) { + frame.setVisible(false); + synchronized (frame) { + frame.notifyAll(); + } + } + }); + + // Show this frame + frame.setVisible(true); + + // Wait for close operation + try { + synchronized (frame) { + while (frame.isVisible()) + frame.wait(); + } + } catch (InterruptedException e) { + // Empty on purpose + } + frame.dispose(); + + } + +} diff --git a/src/main/java/Main.java b/src/main/java/Main.java new file mode 100644 index 0000000..43571bb --- /dev/null +++ b/src/main/java/Main.java @@ -0,0 +1,51 @@ +/** + * @author Pacien TRAN-GIRARD + * @author Timothée FLOURE + */ +public final class Main { + + public static void main(String[] args) { + + // Load image + System.out.println("Load image..."); + int[][] image = Helper.read("doves.jpg"); + Helper.show(image, "Original"); + + // Convert to grayscale + System.out.println("Convert to grayscale..."); + float[][] gray = Color.toGray(image); + Helper.show(Color.toRGB(gray), "Grayscale"); + + // Smooth it + System.out.println("Smooth image..."); + float[][] smooth = Filter.smooth(gray); + Helper.show(Color.toRGB(smooth), "Smooth"); + + // Apply Sobel + System.out.println("Compute Sobel filter..."); + float[][] sobel = Filter.sobel(smooth); + Helper.show(Color.toRGB(sobel), "Sobel"); + + // Find best seam + System.out.println("Find best seam..."); + int[] seam = Seam.find(sobel); + Helper.show(Seam.merge(image, seam), "Best seam"); + + // Shrink until it is a square + int count = image[0].length - image.length; + System.out.println("Shrink by removing " + count + " seams..."); + for (int i = 0; i < count; ++i) { + sobel = Filter.sobel(Filter.smooth(Color.toGray(image))); + seam = Seam.find(sobel); + image = Seam.shrink(image, seam); + System.out.println("Seam " + (i + 1) + "/" + count); + } + System.out.println("Done"); + Helper.show(image, "Shrink"); + + // Save result + Helper.write("doves.shrink.jpg", image); + + } + +} diff --git a/src/main/java/Seam.java b/src/main/java/Seam.java new file mode 100644 index 0000000..1cb4a0c --- /dev/null +++ b/src/main/java/Seam.java @@ -0,0 +1,75 @@ +/** + * @author Pacien TRAN-GIRARD + * @author Timothée FLOURE + */ +public final class Seam { + + /** + * Compute shortest path between {@code from} and {@code to} + * + * @param successors adjacency list for all vertices + * @param costs weight for all vertices + * @param from first vertex + * @param to last vertex + * @return a sequence of vertices, or {@code null} if no path exists + */ + public static int[] path(int[][] successors, float[] costs, int from, int to) { + // TODO path + return null; + } + + /** + * Find best seam + * + * @param energy weight for all pixels + * @return a sequence of x-coordinates (the y-coordinate is the index) + */ + public static int[] find(float[][] energy) { + // TODO find + return null; + } + + /** + * Draw a seam on an image + * + * @param image original image + * @param seam a seam on this image + * @return a new image with the seam in blue + */ + public static int[][] merge(int[][] image, int[] seam) { + // Copy image + int width = image[0].length; + int height = image.length; + int[][] copy = new int[height][width]; + for (int row = 0; row < height; ++row) + for (int col = 0; col < width; ++col) + copy[row][col] = image[row][col]; + + // Paint seam in blue + for (int row = 0; row < height; ++row) + copy[row][seam[row]] = 0x0000ff; + + return copy; + } + + /** + * Remove specified seam + * + * @param image original image + * @param seam a seam on this image + * @return the new image (width is decreased by 1) + */ + public static int[][] shrink(int[][] image, int[] seam) { + int width = image[0].length; + int height = image.length; + int[][] result = new int[height][width - 1]; + for (int row = 0; row < height; ++row) { + for (int col = 0; col < seam[row]; ++col) + result[row][col] = image[row][col]; + for (int col = seam[row] + 1; col < width; ++col) + result[row][col - 1] = image[row][col]; + } + return result; + } + +} diff --git a/src/main/java/SignatureChecks.java b/src/main/java/SignatureChecks.java new file mode 100644 index 0000000..f8a4355 --- /dev/null +++ b/src/main/java/SignatureChecks.java @@ -0,0 +1,37 @@ +public class SignatureChecks { + + @SuppressWarnings("unused") + public static void main(String[] argv) { + + // Check if the signatures of all required function in class Color are correct + float color; + int value; + float[][] gray = new float[4][4]; + int[][] image; + color = Color.getRed(0); + color = Color.getGreen(0); + color = Color.getBlue(0); + color = Color.getGray(0); + value = Color.getRGB(0.0f, 0.0f, 0.0f); + value = Color.getRGB(0.0f); + image = Color.toRGB(gray); + gray = Color.toGray(image); + + // Check if the signatures of all required function in class Filter are correct + float[][] kernel = new float[3][3]; + color = Filter.at(gray, -1, -1); + gray = Filter.filter(gray, kernel); + gray = Filter.smooth(gray); + gray = Filter.sobel(gray); + + // Check if the signatures of all required function in class Filter are correct + int[] path, seam; + float[] costs = new float[]{1, 1, 1}; + int[][] successors = new int[][]{{0, 1}, {}, {4}}; + float[][] energy = new float[4][4]; + path = Seam.path(successors, costs, 0, 2); + seam = Seam.find(energy); + image = Seam.merge(image, seam); + + } +} diff --git a/src/test/java/Tests.java b/src/test/java/Tests.java new file mode 100644 index 0000000..cbb154f --- /dev/null +++ b/src/test/java/Tests.java @@ -0,0 +1,169 @@ +import org.junit.Assert; +import org.junit.Test; +import org.junit.internal.InexactComparisonCriteria; + +public class Tests { + + // Tolerance used to compare two floating point numbers + private static final float EPSILON = 0.0001f; + + // Check whether value is equal to expected integer + private void assertEquals(int expected, int actual) { + Assert.assertEquals(expected, actual); + } + + // Check whether value is approximately equal to expected floating point number + private void assertEquals(float expected, float actual) { + Assert.assertEquals(expected, actual, EPSILON); + } + + // Check whether integer array has correct size and values + private void assertEquals(int[] expected, int[] actual) { + Assert.assertArrayEquals(expected, actual); + } + + // Check whether integer two-dimensional array has correct size and values + private void assertEquals(int[][] expected, int[][] actual) { + Assert.assertArrayEquals(expected, actual); + } + + // Check whether floating point two-dimensional array has correct size and similar values + private void assertEquals(float[][] expected, float[][] actual) { + new InexactComparisonCriteria(EPSILON).arrayEquals(null, expected, actual); + } + + // Test based on example in section 3.1 (pixel coding) + @Test + public void testColorSinglePixel() { + + // Check RGB components and gray level of this bluish color + int color = 0b00100000_11000000_11111111; + assertEquals(0.1254902f, Color.getRed(color)); + assertEquals(0.7529412f, Color.getGreen(color)); + assertEquals(1.0f, Color.getBlue(color)); + assertEquals(0.6261438f, Color.getGray(color)); + + // Encode some colors + assertEquals(0x0000ff, Color.getRGB(0.0f, 0.0f, 1.0f)); + assertEquals(0x7f7f7f, Color.getRGB(0.5f)); + assertEquals(0x0000ff, Color.getRGB(-0.5f, 0, 2)); + assertEquals(0x000000, Color.getRGB(-1.0f)); + + } + + // Test based on example in section 3.2 (image coding) + @Test + public void testColorWholeImage() { + + // A 2x2 image + int[][] image = { + {0x20c0ff, 0x123456}, + {0xffffff, 0x000000} + }; + + // Convert to grayscale + float[][] gray = { + {0.62614375f, 0.20392157f}, + {1.0f, 0.0f} + }; + assertEquals(gray, Color.toGray(image)); + + // Convert back to integer representation + int[][] back = { + {0x9f9f9f, 0x343434}, + {0xffffff, 0x000000} + }; + assertEquals(back, Color.toRGB(gray)); + + } + + // Test based on example in section 4.1 (image filtering) + @Test + public void testFilterConvolution() { + + // A 2x2 grayscale image + float[][] gray = { + {0.5f, 1.0f}, + {0.2f, 0.0f} + }; + + // Access some pixels + assertEquals(0.5f, Filter.at(gray, 0, 0)); + assertEquals(0.2f, Filter.at(gray, 1, 0)); + assertEquals(0.2f, Filter.at(gray, 2, -1)); + + // Apply smoothing and Sobel + assertEquals(new float[][]{ + {0.49f, 0.62f}, + {0.3f, 0.29f} + }, Filter.smooth(gray)); + assertEquals(new float[][]{ + {1.3f, 1.3f}, + {-0.1f, -0.1f} + }, Filter.sobelX(gray)); + assertEquals(new float[][]{ + {-1.9f, -3.3f}, + {-1.9f, -3.3f} + }, Filter.sobelY(gray)); + assertEquals(new float[][]{ + {2.302173f, 3.5468295f}, + {1.9026297f, 3.3015149f} + }, Filter.sobel(gray)); + + } + + // Test based on example in section 5.1 (shortest path) + @Test + public void testSeamPath() { + + // Simple graph stored as an adjacency list + int[][] successors = new int[][]{ + /* 0 -> */ {1, 3}, + /* 1 -> */ {2}, + /* 2 -> */ {}, + /* 3 -> */ {4}, + /* 4 -> */ {1, 5}, + /* 5 -> */ {2} + }; + float[] costs = new float[]{ + /* 0 : */ 1.0f, + /* 1 : */ 9.0f, + /* 2 : */ 2.0f, + /* 3 : */ 1.0f, + /* 4 : */ 3.0f, + /* 5 : */ 1.0f + }; + + // Compute and check shortest path + int[] vertices = Seam.path(successors, costs, 0, 2); + assertEquals(new int[]{0, 3, 4, 5, 2}, vertices); + + } + + // Test based on example in section 5.2 (resizing) + @Test + public void testSeamResize() { + + // A 3x3 image + int[][] image = { + {0x888888, 0x666666, 0xcccccc}, + {0x000000, 0x111111, 0x222222}, + {0xbbbbbb, 0xffffff, 0x666666} + }; + float[][] gray = Color.toGray(image); + + // Find best seam, using gray level as energy + int[] seam = Seam.find(gray); + assertEquals(new int[]{1, 1, 2}, seam); + + // Remove seam + int[][] resized = Seam.shrink(image, seam); + assertEquals(new int[][]{ + {0x888888, 0xcccccc}, + {0x000000, 0x222222}, + {0xbbbbbb, 0xffffff} + }, resized); + + } + +} -- cgit v1.2.3