diff --git a/backend/src/main/java/pdl/backend/Algorithm.java b/backend/src/main/java/pdl/backend/Algorithm.java deleted file mode 100644 index e59987d..0000000 --- a/backend/src/main/java/pdl/backend/Algorithm.java +++ /dev/null @@ -1,56 +0,0 @@ -package pdl.backend; - -import java.util.ArrayList; - -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; - -public class Algorithm { - private final String name; - private final String path; - private final ArrayList parameters; - - public Algorithm(String name, String path) { - this.name = name; - this.path = path; - this.parameters = new ArrayList<>(); - switch (path) { - case "changeLuminosity": - Parameter param1 = new Parameter("delta", "delta", -255, 255, 1); - this.parameters.add(param1); - break; - case "histogram": - break; - case "colorFilter": - Parameter param2 = new Parameter("hue", "Teinte", 0, 359, 1); - this.parameters.add(param2); - break; - case "meanFilter": - Parameter param3 = new Parameter("size", "Taille du filtre", 1, 101, 2); - this.parameters.add(param3); - break; - case "gaussienFilter": - // TODO - // Parameter param4 = new Parameter("size", "Taille du filtre", 1, 101, 2); - // this.parameters.add(param4); - break; - case "contours": - break; - default: - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid path"); - - } - } - - public String getName() { - return this.name; - } - - public String getPath() { - return this.path; - } - - public ArrayList getParameters() { - return this.parameters; - } -} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Algorithm.java b/backend/src/main/java/pdl/backend/Algorithm/Algorithm.java new file mode 100644 index 0000000..b20c71c --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Algorithm.java @@ -0,0 +1,59 @@ +package pdl.backend.Algorithm; + +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import boofcv.struct.image.GrayU8; +import boofcv.struct.image.Planar; +import pdl.backend.Algorithm.Parameters.Parameter; +import pdl.backend.Algorithm.Parameters.ParameterNullPointerException; + +public class Algorithm { + private final String name; + private final String path; + private final List parameters; + private final AlgorithmFunctionInterface function; + + public Algorithm(String name, String path, List parameters, AlgorithmFunctionInterface function) { + this.name = name; + this.path = path; + this.parameters = parameters; + this.function = function; + } + + public void apply(Planar image) { + List para = this.parameters.stream().map(p -> p.getValue()).toList(); + for (Object p : para) { + if (p == null) { + throw new ParameterNullPointerException("Parameter not set"); + } + } + this.function.apply(image, para); + } + + public String getName() { + return this.name; + } + + public String getPath() { + return this.path; + } + + public List getParameters() { + return this.parameters; + } + + public ObjectNode getNode(ObjectMapper mapper) { + ObjectNode node = mapper.createObjectNode(); + node.put("name", this.getName()); + node.put("path", this.getPath()); + ArrayNode parameters = node.putArray("parameters"); + for (Parameter p : this.getParameters()) { + parameters.add(p.getNode(mapper)); + } + return node; + } +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/AlgorithmFunctionInterface.java b/backend/src/main/java/pdl/backend/Algorithm/AlgorithmFunctionInterface.java new file mode 100644 index 0000000..c036737 --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/AlgorithmFunctionInterface.java @@ -0,0 +1,10 @@ +package pdl.backend.Algorithm; + +import java.util.List; + +import boofcv.struct.image.GrayU8; +import boofcv.struct.image.Planar; + +public interface AlgorithmFunctionInterface { + void apply(Planar image, List parameters); +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Parameters/DoubleParameter.java b/backend/src/main/java/pdl/backend/Algorithm/Parameters/DoubleParameter.java new file mode 100644 index 0000000..f4ea1eb --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Parameters/DoubleParameter.java @@ -0,0 +1,42 @@ +package pdl.backend.Algorithm.Parameters; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class DoubleParameter extends Parameter { + private final double min; + private final double max; + private final double step; + + public DoubleParameter(String name, String displayName, double min, double max, double step) { + super(name, displayName, "range"); + this.max = max; + this.min = min; + this.step = step; + } + + public double getMin() { + return this.min; + } + + public double getMax() { + return this.max; + } + + public double getStep() { + return this.step; + } + + @Override + public void setValue(String value) { + super.setValueObject(Double.parseDouble(value)); + } + + public ObjectNode getNode(ObjectMapper mapper) { + ObjectNode node = super.getNode(mapper); + node.put("min", this.min); + node.put("max", this.max); + node.put("step", this.step); + return node; + } +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Parameters/IntegerParameter.java b/backend/src/main/java/pdl/backend/Algorithm/Parameters/IntegerParameter.java new file mode 100644 index 0000000..53e4f50 --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Parameters/IntegerParameter.java @@ -0,0 +1,42 @@ +package pdl.backend.Algorithm.Parameters; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class IntegerParameter extends Parameter { + private final int min; + private final int max; + private final int step; + + public IntegerParameter(String name, String displayName, int min, int max, int step) { + super(name, displayName, "range"); + this.max = max; + this.min = min; + this.step = step; + } + + public int getMin() { + return this.min; + } + + public int getMax() { + return this.max; + } + + public int getStep() { + return this.step; + } + + @Override + public void setValue(String value) { + super.setValueObject(Integer.parseInt(value)); + } + + public ObjectNode getNode(ObjectMapper mapper) { + ObjectNode node = super.getNode(mapper); + node.put("min", this.min); + node.put("max", this.max); + node.put("step", this.step); + return node; + } +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Parameters/Parameter.java b/backend/src/main/java/pdl/backend/Algorithm/Parameters/Parameter.java new file mode 100644 index 0000000..d4e23d1 --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Parameters/Parameter.java @@ -0,0 +1,49 @@ +package pdl.backend.Algorithm.Parameters; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public abstract class Parameter { + private final String name; + private final String displayName; + private final String type; + private Object value; + + public Parameter(String name, String displayName, String type) { + this.name = name; + this.displayName = displayName; + this.type = type; + } + + public String getName() { + return this.name; + } + + public String getDisplayName() { + return this.displayName; + } + + public String getType() { + return this.type; + } + + public ObjectNode getNode(ObjectMapper mapper) { + ObjectNode node = mapper.createObjectNode(); + node.put("name", this.name); + node.put("displayName", this.displayName); + node.put("type", this.type); + return node; + } + + public void setValueObject(Object value) { + this.value = value; + } + + public void setValue(String value) { + this.value = value; + } + + public Object getValue() { + return this.value; + } +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Parameters/ParameterNullPointerException.java b/backend/src/main/java/pdl/backend/Algorithm/Parameters/ParameterNullPointerException.java new file mode 100644 index 0000000..a2041bd --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Parameters/ParameterNullPointerException.java @@ -0,0 +1,7 @@ +package pdl.backend.Algorithm.Parameters; + +public class ParameterNullPointerException extends NullPointerException { + public ParameterNullPointerException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/pdl/backend/Algorithm/Parameters/SelectParameter.java b/backend/src/main/java/pdl/backend/Algorithm/Parameters/SelectParameter.java new file mode 100644 index 0000000..d00d8d8 --- /dev/null +++ b/backend/src/main/java/pdl/backend/Algorithm/Parameters/SelectParameter.java @@ -0,0 +1,33 @@ +package pdl.backend.Algorithm.Parameters; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; + +public class SelectParameter extends Parameter { + private final String[] options; + + public SelectParameter(String name, String displayName, String[] options) { + super(name, displayName, "select"); + this.options = options; + } + + public String[] getOptions() { + return this.options; + } + + @Override + public void setValue(String value) { + super.setValueObject(value); + } + + public ObjectNode getNode(ObjectMapper mapper) { + ObjectNode node = super.getNode(mapper); + ArrayNode optionsNode = mapper.createArrayNode(); + for (String option : this.options) { + optionsNode.add(option); + } + node.set("options", optionsNode); + return node; + } +} diff --git a/backend/src/main/java/pdl/backend/AlgorithmController.java b/backend/src/main/java/pdl/backend/AlgorithmController.java deleted file mode 100644 index 3ec0955..0000000 --- a/backend/src/main/java/pdl/backend/AlgorithmController.java +++ /dev/null @@ -1,57 +0,0 @@ -package pdl.backend; - -import java.util.ArrayList; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.ResponseBody; -import org.springframework.web.bind.annotation.RestController; - -@RestController -public class AlgorithmController { - - @Autowired - private ObjectMapper mapper; - - @GetMapping(value = "/algorithms", produces = "application/json") - @ResponseBody - public ArrayNode getAlgorithmList() { - ArrayList algorithms = new ArrayList(); - Algorithm algo = new Algorithm("Changement de luminosité", "changeLuminosity"); - algorithms.add(algo); - algo = new Algorithm("Changement de teinte", "colorFilter"); - algorithms.add(algo); - algo = new Algorithm("Filtre Moyenneur", "meanFilter"); - algorithms.add(algo); - algo = new Algorithm("Filtre Gaussien", "gaussienFilter"); - algorithms.add(algo); - algo = new Algorithm("Détection de contours", "contours"); - algorithms.add(algo); - algo = new Algorithm("Égalisation d'histogramme", "histogram"); - algorithms.add(algo); - - ArrayNode nodes = mapper.createArrayNode(); - for (Algorithm a : algorithms) { - ObjectNode objectNode = mapper.createObjectNode(); - objectNode.put("name", a.getName()); - objectNode.put("path", a.getPath()); - ArrayNode parameters = objectNode.putArray("parameters"); - for (Parameter p : a.getParameters()) { - ObjectNode paramNode = mapper.createObjectNode(); - paramNode.put("name", p.getName()); - paramNode.put("displayName", p.getDisplayName()); - paramNode.put("min", p.getMin()); - paramNode.put("max", p.getMax()); - paramNode.put("step", p.getStep()); - parameters.add(paramNode); - } - nodes.add(objectNode); - } - return nodes; - } - -} diff --git a/backend/src/main/java/pdl/backend/Controller/AlgorithmController.java b/backend/src/main/java/pdl/backend/Controller/AlgorithmController.java new file mode 100644 index 0000000..73fddee --- /dev/null +++ b/backend/src/main/java/pdl/backend/Controller/AlgorithmController.java @@ -0,0 +1,87 @@ +package pdl.backend.Controller; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; + +import boofcv.struct.border.BorderType; +import boofcv.struct.image.GrayU8; +import boofcv.struct.image.Planar; +import pdl.backend.Algorithm.Algorithm; +import pdl.backend.Algorithm.Parameters.DoubleParameter; +import pdl.backend.Algorithm.Parameters.IntegerParameter; +import pdl.backend.Algorithm.Parameters.SelectParameter; +import pdl.backend.Image.ImageProcessing; +import pdl.backend.Algorithm.Parameters.Parameter; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class AlgorithmController { + + @Autowired + private ObjectMapper mapper; + + public static List algorithmList() { + final String[] borderTypes = { "SKIP", "ZERO", "NORMALIZED", "REFLECT", "EXTENDED", "WRAP" }; + ArrayList algorithms = new ArrayList(); + algorithms.add(new Algorithm("Changement de luminosité", "changeLuminosity", + Arrays.asList(new IntegerParameter("delta", "delta", -255, 255, 1)), + (Planar input, List para) -> { + ImageProcessing.changeLuminosity(input, (int) para.get(0)); + })); + algorithms.add(new Algorithm("Changement de teinte", "colorFilter", + Arrays.asList(new IntegerParameter("hue", "Teinte", 0, 359, 1)), + (Planar input, List para) -> { + ImageProcessing.colorFilter(input, (int) para.get(0)); + })); + + // TODO: TESTS + algorithms.add(new Algorithm("Filtre Moyenneur", "meanFilter", Arrays.asList( + new IntegerParameter("size", "Taille du filtre", 1, 21, 2), + new SelectParameter("borderType", "Type de bordure", borderTypes)), + (Planar input, List para) -> { + ImageProcessing.meanFilter(input, (int) para.get(0), BorderType.valueOf((String) para.get(1))); + })); + + // TODO: TESTS + algorithms.add(new Algorithm("Filtre Gaussien", "gaussienFilter", Arrays.asList( + new IntegerParameter("size", "Taille du filtre", 1, 21, 2), + new DoubleParameter("sigma", "Sigma", 0.1, 2, 0.1), + new SelectParameter("borderType", "Type de bordure", borderTypes)), + (Planar input, List para) -> { + ImageProcessing.gaussienFilter(input, (int) para.get(0), (double) para.get(1), + BorderType.valueOf((String) para.get(2))); + })); + + algorithms.add(new Algorithm("Détection de contours", "contours", new ArrayList(), + (Planar input, List para) -> { + ImageProcessing.gradientImageSobel(input); + })); + + algorithms.add(new Algorithm("Égalisation d'histogramme", "histogram", new ArrayList(), + (Planar input, List para) -> { + ImageProcessing.histogram(input); + })); + + return algorithms; + } + + @GetMapping(value = "/algorithms", produces = "application/json") + @ResponseBody + public ArrayNode getAlgorithmList() { + List algorithms = AlgorithmController.algorithmList(); + ArrayNode nodes = mapper.createArrayNode(); + for (Algorithm a : algorithms) { + nodes.add(a.getNode(mapper)); + } + return nodes; + } + +} diff --git a/backend/src/main/java/pdl/backend/ImageController.java b/backend/src/main/java/pdl/backend/Controller/ImageController.java similarity index 69% rename from backend/src/main/java/pdl/backend/ImageController.java rename to backend/src/main/java/pdl/backend/Controller/ImageController.java index 406ca3d..4121894 100644 --- a/backend/src/main/java/pdl/backend/ImageController.java +++ b/backend/src/main/java/pdl/backend/Controller/ImageController.java @@ -1,4 +1,4 @@ -package pdl.backend; +package pdl.backend.Controller; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -16,8 +16,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import boofcv.io.image.ConvertBufferedImage; +import boofcv.struct.border.BorderType; import boofcv.struct.image.GrayU8; import boofcv.struct.image.Planar; +import pdl.backend.Algorithm.Algorithm; +import pdl.backend.Algorithm.Parameters.Parameter; +import pdl.backend.Image.Image; +import pdl.backend.Image.ImageDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.InputStreamResource; @@ -135,6 +140,23 @@ public class ImageController { return nodes; } + public BorderType getBorderType(String borderType) { + switch (borderType) { + case "EXTENDED": + return BorderType.EXTENDED; + case "REFLECT": + return BorderType.REFLECT; + case "REFLECT_101": + return BorderType.NORMALIZED; + case "WRAP": + return BorderType.WRAP; + case "ZERO": + return BorderType.ZERO; + default: + return null; + } + } + /** * récupère une image et la modifie si un algorithme est renseigné * @@ -159,6 +181,7 @@ public class ImageController { return ResponseEntity.ok().contentType(image.getMediaType()) .body(new InputStreamResource(inputStream)); } + final List algos = AlgorithmController.algorithmList(); // Vérifie la présence du paramètre "algorithm" if (!parameters.containsKey("algorithm")) { return JSONError("First parameter should be 'algorithm'", HttpStatus.BAD_REQUEST); @@ -173,82 +196,25 @@ public class ImageController { } Planar input = ConvertBufferedImage.convertFromPlanar(bufImg, null, true, GrayU8.class); - // applique l'algo a l'image en fonction des parametres et verifie la validite - // des parametres - switch (algo) { - case "changeLuminosity": - if (!parameters.containsKey("delta") || parameters.size() != 2) - return JSONError("Algorithm 'changeLuminosity' requires one parameter 'delta'(integer)", - HttpStatus.BAD_REQUEST); - try { - int i = Integer.parseInt(parameters.get("delta")); - ImageProcessing.changeLuminosity(input, i); - } catch (NumberFormatException e) { - return JSONError("Parameter 'delta' should be an integer", HttpStatus.BAD_REQUEST); - } - break; - case "histogram": - if (parameters.size() != 1) - return JSONError("Algorithm 'histogram' doesn't require any parameters", HttpStatus.BAD_REQUEST); - ImageProcessing.histogram(input); - break; - case "colorFilter": - if (!parameters.containsKey("hue") || parameters.size() != 2) - return JSONError("Algorithm 'colorFilter' requires one parameter 'hue'(integer)", - HttpStatus.BAD_REQUEST); - try { - int i = Integer.parseInt(parameters.get("hue")); - if (i < 0) { - return JSONError("Parameter 'hue' should be an integer between 0 and 360", - HttpStatus.BAD_REQUEST); + for (Algorithm a : algos) { + if (a.getPath().equals(algo)) { + for (Parameter p : a.getParameters()) { + if (!parameters.containsKey(p.getName())) { + return JSONError("Parameter '" + p.getName() + "' is missing", HttpStatus.BAD_REQUEST); + } + try { + p.setValue(parameters.get(p.getName())); + } catch (Exception e) { + return JSONError("Parameter '" + p.getName() + "' is invalid", HttpStatus.BAD_REQUEST); } - ImageProcessing.colorFilter(i, input); - // hue entre 0 et 360? - } catch (NumberFormatException e) { - return JSONError("Parameter 'hue' should be an integer between 0 and 360", - HttpStatus.BAD_REQUEST); } - break; - case "meanFilter": - if (!parameters.containsKey("size") || parameters.size() != 2) - return JSONError("Algorithm 'meanFilter' requires one parameter 'size'(positive odd integer)", - HttpStatus.BAD_REQUEST); try { - int i = Integer.parseInt(parameters.get("size")); - if (i % 2 == 0) - return JSONError("Parameter 'size' should be a positive odd integer", - HttpStatus.BAD_REQUEST); - Planar clone = input.clone(); - ImageProcessing.meanFilter(clone, input, i); - } catch (NumberFormatException e) { - return JSONError("Parameter 'size' should be a positive odd integer", - HttpStatus.BAD_REQUEST); + a.apply(input); + } catch (Exception e) { + return JSONError("There was an internal server error", HttpStatus.INTERNAL_SERVER_ERROR); } break; - case "gaussienFilter": - if (/* !parameters.containsKey("size") || */ parameters.size() != 1) - return JSONError("Algorithm 'gaussienFilter' requires no parameters", HttpStatus.BAD_REQUEST); - try { - // int i = Integer.parseInt(parameters.get("size")); - // if (i % 2 == 0) - // return JSONError("Parameter 'size' should be a positive odd integer", - // HttpStatus.BAD_REQUEST); - Planar clone = input.clone(); - ImageProcessing.gaussienFilter(clone, input, 5); - } catch (NumberFormatException e) { - return JSONError("Parameter 'size' should be a positive odd integer", - HttpStatus.BAD_REQUEST); - } - break; - case "contours": - if (parameters.size() != 1) - return JSONError("Algorithm 'contours' doesn't require any parameters", HttpStatus.BAD_REQUEST); - Planar clone = input.clone(); - ImageProcessing.gradientImageSobel(clone, input); - break; - default: - return JSONError("Requested algorithm doesn't exist", HttpStatus.BAD_REQUEST); - + } } // transforme l'image en quelque chose que le navigateur puisse lire diff --git a/backend/src/main/java/pdl/backend/Image.java b/backend/src/main/java/pdl/backend/Image/Image.java similarity index 98% rename from backend/src/main/java/pdl/backend/Image.java rename to backend/src/main/java/pdl/backend/Image/Image.java index b47f2f5..7b006a6 100644 --- a/backend/src/main/java/pdl/backend/Image.java +++ b/backend/src/main/java/pdl/backend/Image/Image.java @@ -1,4 +1,4 @@ -package pdl.backend; +package pdl.backend.Image; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; diff --git a/backend/src/main/java/pdl/backend/ImageDao.java b/backend/src/main/java/pdl/backend/Image/ImageDao.java similarity index 96% rename from backend/src/main/java/pdl/backend/ImageDao.java rename to backend/src/main/java/pdl/backend/Image/ImageDao.java index 9b0c660..b4b53cd 100644 --- a/backend/src/main/java/pdl/backend/ImageDao.java +++ b/backend/src/main/java/pdl/backend/Image/ImageDao.java @@ -1,4 +1,4 @@ -package pdl.backend; +package pdl.backend.Image; import java.io.File; import java.io.IOException; @@ -13,6 +13,8 @@ import java.util.Optional; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Repository; +import pdl.backend.Dao; + @Repository public class ImageDao implements Dao { @@ -21,7 +23,7 @@ public class ImageDao implements Dao { private final Map images = new HashMap<>(); public ImageDao() throws IOException { - this("images"); + this("/images"); } public ImageDao(final String folderPathStr) throws IOException { diff --git a/backend/src/main/java/pdl/backend/ImageProcessing.java b/backend/src/main/java/pdl/backend/Image/ImageProcessing.java similarity index 76% rename from backend/src/main/java/pdl/backend/ImageProcessing.java rename to backend/src/main/java/pdl/backend/Image/ImageProcessing.java index 7cc0044..0913e78 100644 --- a/backend/src/main/java/pdl/backend/ImageProcessing.java +++ b/backend/src/main/java/pdl/backend/Image/ImageProcessing.java @@ -1,5 +1,6 @@ -package pdl.backend; +package pdl.backend.Image; +import boofcv.struct.border.BorderType; import boofcv.struct.image.GrayU8; import boofcv.struct.image.Planar; @@ -9,7 +10,10 @@ import java.util.Random; import boofcv.concurrency.BoofConcurrency; -public class ImageProcessing { +public final class ImageProcessing { + + private ImageProcessing() { + } /** * @@ -46,15 +50,14 @@ public class ImageProcessing { * * Convertit une image en couleur en une image en niveaux de gris * - * @param input l'image d'entrée en couleur - * @param output l'image de sortie en niveaux de gris + * @param input l'image d'entrée en couleur */ - public static void convertColorToGray(Planar input, Planar output) { + public static void convertColorToGray(Planar input) { BoofConcurrency.loopFor(0, input.height, y -> { for (int x = 0; x < input.width; x++) { int[] rgb = getRGBValue(input, x, y); int gray = (int) (0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2]); - setRGBValue(output, x, y, new int[] { gray, gray, gray }); + setRGBValue(input, x, y, new int[] { gray, gray, gray }); } }); } @@ -93,8 +96,8 @@ public class ImageProcessing { */ public static void histogram(Planar input) { int h[] = new int[256]; - Planar grayInput = input.createSameShape(); - convertColorToGray(input, grayInput); + Planar grayInput = input.clone(); + convertColorToGray(grayInput); // On remplit le tableau avec le nombre de pixels pour chaque niveau de // gris BoofConcurrency.loopFor(0, input.height, y -> { @@ -218,7 +221,7 @@ public class ImageProcessing { * @param hue La valeur de la teinte à appliquer * @param input L'image à modifier */ - public static void colorFilter(int hue, Planar input) { + public static void colorFilter(Planar input, int hue) { if (hue < 0 || hue > 360) { throw new IllegalArgumentException("Hue doit être compris entre 0 et 360"); } @@ -240,15 +243,15 @@ public class ImageProcessing { * Calcule et stocke dans output l'image de la norme du gradient * calculé sur input par convolution avec les filtres de Sobel. * - * @param input L'image d'entrée en niveaux de gris. - * @param output L'image de sortie en niveaux de gris. + * @param input L'image d'entrée en niveaux de gris. */ - public static void gradientImageSobel(Planar input, Planar output) { + public static void gradientImageSobel(Planar input) { + Planar output = input.createSameShape(); int h1[][] = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } }; int h2[][] = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } }; int half = h1.length / 2; - Planar inputGray = input.createSameShape(); - convertColorToGray(input, inputGray); + Planar inputGray = input.clone(); + convertColorToGray(inputGray); BoofConcurrency.loopFor(0, input.height, y -> { for (int x = 0; x < input.width; x++) { if (y < half || x < half || y >= input.height - half || x >= input.width - half) { @@ -279,78 +282,84 @@ public class ImageProcessing { setRGBValue(output, x, y, rgb); } }); + for (int i = 0; i < input.width; i++) { + for (int j = 0; j < input.height; j++) { + int[] rgb = getRGBValue(output, i, j); + setRGBValue(input, i, j, rgb); + } + } } /** * Applique un filtre moyenneur de taille indiquée à l'image - * passée en paramètre (le bord de l'image n'est pas modifié) + * passée en paramètre et stocke le résultat dans l'image de sortie. * - * @param input image d'entrée - * @param output image de sortie - * @param size taille du filtre à appliquer (doit être impaire) + * @param input image d'entrée + * @param size taille du filtre à appliquer (doit être impaire) + * @param borderType type de bord à utiliser pour la convolution */ - public static void meanFilter(Planar input, Planar output, int size) { + public static void meanFilter(Planar input, int size, BorderType borderType) { // Vérification que la taille est impaire - if (size % 2 == 0) { - throw new IllegalArgumentException("La taille doit être impaire"); + if (size % 2 == 0 || size < 0) { + throw new IllegalArgumentException( + "La taille doit être impaire et positive"); } - // Boucle pour parcourir tous les pixels de l'image d'entrée - BoofConcurrency.loopFor(0, input.height, y -> { - for (int x = 0; x < input.width; x++) { - if (y < size / 2 || x < size / 2 || y >= input.height - size / 2 - || x >= input.width - size / 2) { - setRGBValue(output, x, y, getRGBValue(input, x, y)); - continue; - } - int totalValue[] = new int[3]; - // Boucle pour parcourir le carré size x size autour du pixel actuel - for (int j = y - size / 2; j < y + size / 2 + 1; j++) { - for (int i = x - size / 2; i < x + size / 2 + 1; i++) { - for (int k = 0; k < totalValue.length; k++) { - totalValue[k] += input.getBand(k).get(i, j); - } - } - } - // Calcule la moyenne des pixels du carré et l'applique au pixel actuel dans - // l'image de - // sortie - for (int k = 0; k < totalValue.length; k++) { - totalValue[k] /= size * size; - } - setRGBValue(output, x, y, totalValue); + int[][] kernel = new int[size][size]; + for (int i = 0; i < size; i++) { + for (int j = 0; j < size; j++) { + kernel[i][j] = 1; } - }); - + } + convolution(input, kernel, borderType); } /** - * TODO - * Calcule le noyau gaussien de taille demandée * - * @param size taille du noyau gaussien - * @return noyau gaussien + * Applique un filtre gaussien de taille et de valeur données sur une image et + * stocke le résultat dans l'image de sortie. + * + * @param input L'image d'entrée + * @param size La taille du filtre à appliquer (doit être impaire) + * @param sigma La valeur du sigma à utiliser pour le filtre gaussien. + * @param borderType Le type de bord à utiliser pour la convolution */ - public static int[][] createGaussienKernel(int size) { - // Vérification que la taille est impaire - if (size % 2 == 0 || size < 0) { - throw new IllegalArgumentException("La taille doit être impaire et positive"); + public static void gaussienFilter(Planar input, int size, double sigma, BorderType borderType) { + if (size % 2 == 0 || size < 0 || sigma <= 0) { + throw new IllegalArgumentException( + "La taille doit être impaire et positive et sigma doit être strictement positif"); } double[][] kernel = new double[size][size]; - double sigma = 4 / 3; - double sum = 0.0; // For accumulating the kernel values - for (int x = 0; x < size; ++x) - for (int y = 0; y < size; ++y) { - kernel[x][y] = 1 / (2 * Math.PI * sigma * sigma) * Math.exp(-(x * x + y * y) / (2 * sigma * sigma)); - + int half = size / 2; + double sum = 0; + for (int x = -half; x <= half; ++x) { + for (int y = -half; y <= half; ++y) { + double gaussianValue = 1 / (2 * Math.PI * sigma * sigma) + * Math.exp(-(x * x + y * y) / (2 * sigma * sigma)); + kernel[x + half][y + half] = gaussianValue; + sum += gaussianValue; } + } + // On récupère le pourcentage de chaque valeur par rapport à la somme + for (int x = -half; x <= half; ++x) { + for (int y = -half; y <= half; ++y) { + kernel[x + half][y + half] = kernel[x + half][y + half] / sum; + } + } - // Normalize the kernel - int[][] kernelk = new int[size][size]; - for (int x = 0; x < size; ++x) - for (int y = 0; y < size; ++y) - kernelk[x][y] = (int) (kernel[x][y] / sum); - - return kernelk; + int[][] kernelInt = new int[size][size]; + double factor = 1 / kernel[0][0]; + // Si le facteur est trop grand (la valeur du coin est très très petite par + // rapport au reste), on le fixe à 1 million + if (factor > 1000000) { + factor = 1000000; + } + // normalisation + for (int x = 0; x < size; ++x) { + for (int y = 0; y < size; ++y) { + kernelInt[x][y] = (int) (kernel[x][y] * factor); + } + } + convolution(input, kernelInt, borderType); } public static int[][] createGaussienKernel(int size, double sigma) { @@ -397,15 +406,14 @@ public class ImageProcessing { * Effectue une convolution sur une image avec le noyau * donné et stocke le résultat dans l'image de sortie. * - * @param input L'image d'entrée - * @param output L'image de sortie - * @param kernel Le noyau de convolution à utiliser. + * @param input L'image d'entrée + * @param kernel Le noyau de convolution à utiliser. + * @param borderType Le type de bord à utiliser pour la convolution */ - public static void gaussienFilter(Planar input, Planar output, - int size) { - int[][] kernel = { { 1, 2, 3, 2, 1 }, { 2, 6, 8, 6, 2 }, { 3, 8, 10, 8, 3 }, { 2, 6, 8, 6, 2 }, - { 1, 2, 3, 2, 1 } }; - int half = size / 2; + public static void convolution(Planar input, int[][] kernel, BorderType borderType) { + Planar copy = input.clone(); + int size = kernel.length; + int half = kernel.length / 2; int coefs = 0; // Calcul du nombre total de coefficients dans le noyau for (int i = 0; i < kernel.length; i++) { @@ -417,10 +425,54 @@ public class ImageProcessing { // Parcours de l'image d'entrée et application de la convolution BoofConcurrency.loopFor(0, input.height, y -> { for (int x = 0; x < input.width; x++) { - // Vérification si le carré sera à l'intérieur de l'image + // On traite d'abord les bords if (y < half || x < half || y >= input.height - half || x >= input.width - half) { - setRGBValue(output, x, y, getRGBValue(input, x, y)); + if (borderType == BorderType.SKIP) { + setRGBValue(input, x, y, getRGBValue(copy, x, y)); + } else if (borderType == BorderType.ZERO) { + setRGBValue(input, x, y, new int[] { 0, 0, 0 }); + } else { + int totalValue[] = new int[3]; + int coefsUsed = 0; + for (int j = y - half; j < y + half + 1; j++) { + int tmpj = j; + if (j < 0 || j >= input.height) { + if (borderType == BorderType.EXTENDED) { + tmpj = j < 0 ? 0 : input.height - 1; + } else if (borderType == BorderType.NORMALIZED) { + continue; + } else if (borderType == BorderType.REFLECT) { + tmpj = j < 0 ? -j : (input.height - 1) - (j - (input.height - 1)); + } else if (borderType == BorderType.WRAP) { + tmpj = j < 0 ? j + (input.height - 1) : (j - (input.height - 1)); + } + } + for (int i = x - size / 2; i < x + size / 2 + 1; i++) { + int tmpi = i; + if (i < 0 || i >= input.width) { + if (borderType == BorderType.EXTENDED) { + tmpi = i < 0 ? 0 : input.width - 1; + } else if (borderType == BorderType.NORMALIZED) { + continue; + } else if (borderType == BorderType.REFLECT) { + tmpi = i < 0 ? -i : (input.width - 1) - (i - (input.width - 1)); + } else if (borderType == BorderType.WRAP) { + tmpi = i < 0 ? i + (input.width - 1) : (i - (input.width - 1)); + } + } + for (int k = 0; k < totalValue.length; k++) { + totalValue[k] += copy.getBand(k).get(tmpi, tmpj) + * kernel[i + (-x + half)][j + (-y + half)]; + } + coefsUsed += kernel[i + (-x + half)][j + (-y + half)]; + } + } + for (int k = 0; k < totalValue.length; k++) { + totalValue[k] /= coefsUsed; + } + setRGBValue(input, x, y, totalValue); + } continue; } int currentJ = 0; @@ -430,7 +482,7 @@ public class ImageProcessing { for (int j = y - half; j <= y + half; j++) { for (int i = x - half; i <= x + half; i++) { for (int k = 0; k < totalValue.length; k++) { - totalValue[k] += input.getBand(k).get(i, j) * kernel[currentJ][currentI]; + totalValue[k] += copy.getBand(k).get(i, j) * kernel[currentJ][currentI]; } currentI++; } @@ -440,7 +492,7 @@ public class ImageProcessing { for (int k = 0; k < totalValue.length; k++) { totalValue[k] /= totalCoefs; } - setRGBValue(output, x, y, totalValue); + setRGBValue(input, x, y, totalValue); } }); } diff --git a/backend/src/main/java/pdl/backend/ImageResourceMissingException.java b/backend/src/main/java/pdl/backend/Image/ImageResourceMissingException.java similarity index 87% rename from backend/src/main/java/pdl/backend/ImageResourceMissingException.java rename to backend/src/main/java/pdl/backend/Image/ImageResourceMissingException.java index fe78b22..843d3d7 100644 --- a/backend/src/main/java/pdl/backend/ImageResourceMissingException.java +++ b/backend/src/main/java/pdl/backend/Image/ImageResourceMissingException.java @@ -1,4 +1,4 @@ -package pdl.backend; +package pdl.backend.Image; import java.io.FileNotFoundException; diff --git a/backend/src/main/java/pdl/backend/Parameter.java b/backend/src/main/java/pdl/backend/Parameter.java deleted file mode 100644 index ad37078..0000000 --- a/backend/src/main/java/pdl/backend/Parameter.java +++ /dev/null @@ -1,37 +0,0 @@ -package pdl.backend; - -public class Parameter { - private final String name; - private final String displayName; - private final int min; - private final int max; - private final int step; - - public Parameter(String name, String displayName, int min, int max, int step) { - this.name = name; - this.displayName = displayName; - this.max = max; - this.min = min; - this.step = step; - } - - public String getName() { - return this.name; - } - - public String getDisplayName() { - return this.displayName; - } - - public int getMin() { - return this.min; - } - - public int getMax() { - return this.max; - } - - public int getStep() { - return this.step; - } -} diff --git a/backend/src/test/java/pdl/backend/ImageControllerTests.java b/backend/src/test/java/pdl/backend/ImageControllerTests.java index a633f1b..5a2e198 100644 --- a/backend/src/test/java/pdl/backend/ImageControllerTests.java +++ b/backend/src/test/java/pdl/backend/ImageControllerTests.java @@ -22,6 +22,9 @@ import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import pdl.backend.Image.Image; +import pdl.backend.Image.ImageDao; + @SpringBootTest @AutoConfigureMockMvc @TestMethodOrder(OrderAnnotation.class) diff --git a/backend/src/test/java/pdl/backend/ImageDaoTests.java b/backend/src/test/java/pdl/backend/ImageDaoTests.java index fad6c1a..ed93842 100644 --- a/backend/src/test/java/pdl/backend/ImageDaoTests.java +++ b/backend/src/test/java/pdl/backend/ImageDaoTests.java @@ -7,6 +7,8 @@ import java.io.IOException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import pdl.backend.Image.ImageDao; + public class ImageDaoTests { @Test diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3dafe85..5ebea8c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -3,6 +3,7 @@ import Home from './components/Home.vue' import Gallery from './components/Gallery.vue'; import NotFound from './components/NotFound.vue' import Upload from './components/Upload.vue' +import StableDiff from './components/StableDiff.vue'; import { defineComponent } from 'vue' import { api } from '@/http-api' import { ImageType, ImageClass } from './image'; @@ -48,6 +49,7 @@ export default defineComponent({
  • Home
  • Gallery
  • Upload
  • +
  • Generate Images
  • @@ -55,12 +57,7 @@ export default defineComponent({ diff --git a/frontend/src/customSelect.css b/frontend/src/components/CustomSelector.vue similarity index 61% rename from frontend/src/customSelect.css rename to frontend/src/components/CustomSelector.vue index 0c9eafc..5e089a0 100644 --- a/frontend/src/customSelect.css +++ b/frontend/src/components/CustomSelector.vue @@ -1,3 +1,47 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/components/Gallery.vue b/frontend/src/components/Gallery.vue index 29906fb..e6d4e9a 100644 --- a/frontend/src/components/Gallery.vue +++ b/frontend/src/components/Gallery.vue @@ -19,7 +19,7 @@ export default defineComponent({
    - +

    Aucune image trouvée

    @@ -59,9 +59,8 @@ label { font-size: 1.5em; color: white; } - - - diff --git a/frontend/src/components/HomeEditor.vue b/frontend/src/components/HomeEditor.vue index a5b8275..09ce47e 100644 --- a/frontend/src/components/HomeEditor.vue +++ b/frontend/src/components/HomeEditor.vue @@ -1,8 +1,9 @@ \ No newline at end of file diff --git a/frontend/src/components/HomeSelect.vue b/frontend/src/components/HomeSelect.vue index 3300f11..53ee3bf 100644 --- a/frontend/src/components/HomeSelect.vue +++ b/frontend/src/components/HomeSelect.vue @@ -2,7 +2,7 @@ import { defineComponent } from 'vue' import { ImageType } from '@/image' - +import CustomSelector from './CustomSelector.vue'; + + \ No newline at end of file diff --git a/frontend/src/components/Upload.vue b/frontend/src/components/Upload.vue index a046963..55685b6 100644 --- a/frontend/src/components/Upload.vue +++ b/frontend/src/components/Upload.vue @@ -13,7 +13,7 @@ interface InputFileEvent extends Event { } export default defineComponent({ - emits: ['updateImageList', 'delete'], + emits: ['updateImageList'], data() { return { file: null as FileList | null, @@ -27,8 +27,15 @@ export default defineComponent({ images: { type: Map, required: true + }, + fileDefault: { + type: FileList, + default: null } }, + created() { + this.file = this.fileDefault; + }, methods: { async submitImage() { if (this.classSendButton === "sent") { diff --git a/frontend/src/filter.ts b/frontend/src/filter.ts index 0c6fa5d..f2a07b8 100644 --- a/frontend/src/filter.ts +++ b/frontend/src/filter.ts @@ -1,22 +1,48 @@ -export class Parameters { +export enum FilterType { + "range" = "range", + "select" = "select", +} + + +export abstract class Parameters { name: string displayName: string - min: number - max: number - step: number - value: number | undefined + type: FilterType + value: any - constructor(name: string, displayName: string, min: number, max: number, step: number = 1, value: number | undefined = undefined) { + constructor(name: string, displayName: string, type: FilterType) { this.name = name this.displayName = displayName - this.min = min - this.max = max - this.step = step - this.value = value + this.type = type } } +export class RangeParameters extends Parameters { + min: number + max: number + step: number + value: number + constructor(name: string, displayName: string, min: number, max: number, step: number, value: number = min) { + super(name, displayName, FilterType.range) + this.value = value; + this.min = min + this.max = max + this.step = step + } +} + +export class SelectParameters extends Parameters { + options: string[] + value: string + constructor(name: string, displayName: string, options: string[], value = options[0]) { + super(name, displayName, FilterType.select) + this.value = value; + this.options = options + } +} + + export class Filter { name: string path: string @@ -31,9 +57,28 @@ export class Filter { } export function getParameters(f: Filter): string { - return '&' + f.parameters.map((param) => param.name + "=" + param.value).join("&"); + return '&' + f.parameters.map((param) => { + switch (param.type) { + case FilterType.range: + const paramCastRange = param as RangeParameters + return paramCastRange.name + "=" + paramCastRange.value + case FilterType.select: + const paramCastSelect = param as SelectParameters + return paramCastSelect.name + "=" + paramCastSelect.value + } + } + ).join("&") } export function clone(f: Filter): Filter { - return new Filter(f.name, f.path, f.parameters.map((arg) => new Parameters(arg.name, arg.displayName, arg.min, arg.max, arg.step, arg.value))) + return new Filter(f.name, f.path, f.parameters.map((param) => { + switch (param.type) { + case FilterType.range: + const paramCastRange = param as RangeParameters + return new RangeParameters(param.name, param.displayName, paramCastRange.min, paramCastRange.max, paramCastRange.step, paramCastRange.value) + case FilterType.select: + const paramCastSelect = param as SelectParameters + return new SelectParameters(param.name, param.displayName, paramCastSelect.options, paramCastSelect.value) + } + })) } \ No newline at end of file diff --git a/frontend/src/http-api.ts b/frontend/src/http-api.ts index a4acb8e..061b298 100644 --- a/frontend/src/http-api.ts +++ b/frontend/src/http-api.ts @@ -2,9 +2,44 @@ import axios, { AxiosResponse, AxiosError, HttpStatusCode } from 'axios'; import { ImageType } from '@/image'; import { Filter, getParameters } from './filter'; +interface stateInterface { + skipped: boolean; + interrupted: boolean; + job: string; + job_count: number; + job_timestamp: string; + job_no: number; + sampling_step: number; + sampling_steps: number; +} + +interface progressInterface { + progress: number; + eta_relative: number; + state: stateInterface; + current_image: string | null; + text_info: string | null; +} + +interface generateImageInterface { + images: string[]; + parameters: {}; + info: string; +} + + +export const cancelRequest = axios.CancelToken.source(); + const instance = axios.create({ baseURL: "/", - timeout: 15000, + timeout: 0, + cancelToken: cancelRequest.token +}); + +const stableDiffInstance = axios.create({ + baseURL: "http://stable.nessar.fr/", + timeout: 0, + cancelToken: cancelRequest.token }); const responseBody = (response: AxiosResponse) => response.data; @@ -14,7 +49,12 @@ const requests = { get: (url: string, param: {}) => instance.get(url, param).then(responseBody), post: (url: string, body: {}) => instance.post(url, body, { headers: { "Content-Type": "multipart/form-data" }, }).then(responseStatus), put: (url: string, body: {}) => instance.put(url, body).then(responseBody), - delete: (url: string) => instance.delete(url).then(responseBody) + delete: (url: string) => instance.delete(url).then(responseBody), +}; + +const stableDiffRequests = { + get: (url: string, param: {}) => stableDiffInstance.get(url, param).then(responseBody), + post: (url: string, body: {}) => stableDiffInstance.post(url, body, { headers: { "Content-Type": "application/json" }, }).then(responseBody), }; export const api = { @@ -23,4 +63,18 @@ export const api = { getImage: (id: number, filter: Filter | undefined): Promise => requests.get(`images/${id}${filter !== undefined ? "?algorithm=" + filter.path + getParameters(filter) : ""}`, { responseType: "blob" }), createImage: (form: FormData): Promise => requests.post('images', form), deleteImage: (id: number): Promise => requests.delete(`images/${id}`), + generateImage: (prompt: string, negativePrompt: string, height: number, width: number, steps: number, cfgScale: number): Promise => stableDiffRequests.post('sdapi/v1/txt2img', { + "prompt": prompt, + "negative_prompt": negativePrompt, + "height": height, + "width": width, + "steps": steps, + "cfg_scale": cfgScale, + "do_not_save_samples": true, + "do_not_save_grid": true, + "sampler_index": "Euler a", + "send_images": true, + "save_images": false, + }), + progress: (): Promise => stableDiffRequests.get('sdapi/v1/progress', {}), }; \ No newline at end of file diff --git a/frontend/src/navi.css b/frontend/src/navi.css new file mode 100644 index 0000000..35e037f --- /dev/null +++ b/frontend/src/navi.css @@ -0,0 +1,43 @@ +.navi { + /* reduce size of children */ + display: flex; + flex-direction: row-reverse; + pointer-events: none; + gap: 3vw; +} + +.navi ul>li { + position: relative; + display: flex; + align-items: center; + justify-content: center; +} + +.navi h3, +.navi a { + min-width: 90px; + padding: 5px 15px; + border-radius: 60px; + color: rgb(172, 165, 154); + text-decoration: none; + font-family: "Aeonik", sans-serif; +} + +.navi nav { + display: flex; + justify-content: center; + padding-left: 20px; + height: 60px; + padding-right: 20px; + border-radius: 60px; + background-color: rgb(24, 26, 27); + margin-right: 20px; + pointer-events: auto; +} + + +.navi ul { + display: flex; + list-style: none; + padding-inline-start: 0; +} \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 89c7a40..b42b3cb 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -18,6 +18,11 @@ const router = createRouter({ name: "upload", component: () => import('../components/Upload.vue') }, + { + path: '/generateImages', + name: "generate", + component: () => import('../components/StableDiff.vue') + }, { path: '/:pathMatch(.*)*', name: 'not-found',