Merge branch 'main' into deleteZone

This commit is contained in:
Kea Horvath committed 2023-04-01 20:49:06 +02:00
commit 661524d675
31 files changed
+1111 -477

No files matched your search

@@ -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<Parameter> 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<Parameter> getParameters() {
return this.parameters;
}
}
@@ -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<Parameter> parameters;
private final AlgorithmFunctionInterface function;
public Algorithm(String name, String path, List<Parameter> parameters, AlgorithmFunctionInterface function) {
this.name = name;
this.path = path;
this.parameters = parameters;
this.function = function;
}
public void apply(Planar<GrayU8> image) {
List<Object> 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<Parameter> 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;
}
}
@@ -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<GrayU8> image, List<Object> parameters);
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
}
@@ -0,0 +1,7 @@
package pdl.backend.Algorithm.Parameters;
public class ParameterNullPointerException extends NullPointerException {
public ParameterNullPointerException(String message) {
super(message);
}
}
@@ -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;
}
}
@@ -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<Algorithm> algorithms = new ArrayList<Algorithm>();
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;
}
}
@@ -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<Algorithm> algorithmList() {
final String[] borderTypes = { "SKIP", "ZERO", "NORMALIZED", "REFLECT", "EXTENDED", "WRAP" };
ArrayList<Algorithm> algorithms = new ArrayList<Algorithm>();
algorithms.add(new Algorithm("Changement de luminosité", "changeLuminosity",
Arrays.asList(new IntegerParameter("delta", "delta", -255, 255, 1)),
(Planar<GrayU8> input, List<Object> 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<GrayU8> input, List<Object> 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<GrayU8> input, List<Object> 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<GrayU8> input, List<Object> 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<Parameter>(),
(Planar<GrayU8> input, List<Object> para) -> {
ImageProcessing.gradientImageSobel(input);
}));
algorithms.add(new Algorithm("Égalisation d'histogramme", "histogram", new ArrayList<Parameter>(),
(Planar<GrayU8> input, List<Object> para) -> {
ImageProcessing.histogram(input);
}));
return algorithms;
}
@GetMapping(value = "/algorithms", produces = "application/json")
@ResponseBody
public ArrayNode getAlgorithmList() {
List<Algorithm> algorithms = AlgorithmController.algorithmList();
ArrayNode nodes = mapper.createArrayNode();
for (Algorithm a : algorithms) {
nodes.add(a.getNode(mapper));
}
return nodes;
}
}
@@ -1,4 +1,4 @@
package pdl.backend; package pdl.backend.Controller;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@@ -16,8 +16,13 @@ import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
import boofcv.io.image.ConvertBufferedImage; import boofcv.io.image.ConvertBufferedImage;
import boofcv.struct.border.BorderType;
import boofcv.struct.image.GrayU8; import boofcv.struct.image.GrayU8;
import boofcv.struct.image.Planar; 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.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.InputStreamResource;
@@ -135,6 +140,23 @@ public class ImageController {
return nodes; 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é * 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()) return ResponseEntity.ok().contentType(image.getMediaType())
.body(new InputStreamResource(inputStream)); .body(new InputStreamResource(inputStream));
} }
final List<Algorithm> algos = AlgorithmController.algorithmList();
// Vérifie la présence du paramètre "algorithm" // Vérifie la présence du paramètre "algorithm"
if (!parameters.containsKey("algorithm")) { if (!parameters.containsKey("algorithm")) {
return JSONError("First parameter should be 'algorithm'", HttpStatus.BAD_REQUEST); return JSONError("First parameter should be 'algorithm'", HttpStatus.BAD_REQUEST);
@@ -173,82 +196,25 @@ public class ImageController {
} }
Planar<GrayU8> input = ConvertBufferedImage.convertFromPlanar(bufImg, null, true, GrayU8.class); Planar<GrayU8> input = ConvertBufferedImage.convertFromPlanar(bufImg, null, true, GrayU8.class);
// applique l'algo a l'image en fonction des parametres et verifie la validite for (Algorithm a : algos) {
// des parametres if (a.getPath().equals(algo)) {
switch (algo) { for (Parameter p : a.getParameters()) {
case "changeLuminosity": if (!parameters.containsKey(p.getName())) {
if (!parameters.containsKey("delta") || parameters.size() != 2) return JSONError("Parameter '" + p.getName() + "' is missing", HttpStatus.BAD_REQUEST);
return JSONError("Algorithm 'changeLuminosity' requires one parameter 'delta'(integer)", }
HttpStatus.BAD_REQUEST); try {
try { p.setValue(parameters.get(p.getName()));
int i = Integer.parseInt(parameters.get("delta")); } catch (Exception e) {
ImageProcessing.changeLuminosity(input, i); return JSONError("Parameter '" + p.getName() + "' is invalid", HttpStatus.BAD_REQUEST);
} 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);
} }
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 { try {
int i = Integer.parseInt(parameters.get("size")); a.apply(input);
if (i % 2 == 0) } catch (Exception e) {
return JSONError("Parameter 'size' should be a positive odd integer", return JSONError("There was an internal server error", HttpStatus.INTERNAL_SERVER_ERROR);
HttpStatus.BAD_REQUEST);
Planar<GrayU8> clone = input.clone();
ImageProcessing.meanFilter(clone, input, i);
} catch (NumberFormatException e) {
return JSONError("Parameter 'size' should be a positive odd integer",
HttpStatus.BAD_REQUEST);
} }
break; 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<GrayU8> 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<GrayU8> 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 // transforme l'image en quelque chose que le navigateur puisse lire
@@ -1,4 +1,4 @@
package pdl.backend; package pdl.backend.Image;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
@@ -1,4 +1,4 @@
package pdl.backend; package pdl.backend.Image;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@@ -13,6 +13,8 @@ import java.util.Optional;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import pdl.backend.Dao;
@Repository @Repository
public class ImageDao implements Dao<Image> { public class ImageDao implements Dao<Image> {
@@ -21,7 +23,7 @@ public class ImageDao implements Dao<Image> {
private final Map<Long, Image> images = new HashMap<>(); private final Map<Long, Image> images = new HashMap<>();
public ImageDao() throws IOException { public ImageDao() throws IOException {
this("images"); this("/images");
} }
public ImageDao(final String folderPathStr) throws IOException { public ImageDao(final String folderPathStr) throws IOException {
@@ -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.GrayU8;
import boofcv.struct.image.Planar; import boofcv.struct.image.Planar;
@@ -9,7 +10,10 @@ import java.util.Random;
import boofcv.concurrency.BoofConcurrency; 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 * Convertit une image en couleur en une image en niveaux de gris
* *
* @param input l'image d'entrée en couleur * @param input l'image d'entrée en couleur
* @param output l'image de sortie en niveaux de gris
*/ */
public static void convertColorToGray(Planar<GrayU8> input, Planar<GrayU8> output) { public static void convertColorToGray(Planar<GrayU8> input) {
BoofConcurrency.loopFor(0, input.height, y -> { BoofConcurrency.loopFor(0, input.height, y -> {
for (int x = 0; x < input.width; x++) { for (int x = 0; x < input.width; x++) {
int[] rgb = getRGBValue(input, x, y); int[] rgb = getRGBValue(input, x, y);
int gray = (int) (0.3 * rgb[0] + 0.59 * rgb[1] + 0.11 * rgb[2]); 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<GrayU8> input) { public static void histogram(Planar<GrayU8> input) {
int h[] = new int[256]; int h[] = new int[256];
Planar<GrayU8> grayInput = input.createSameShape(); Planar<GrayU8> grayInput = input.clone();
convertColorToGray(input, grayInput); convertColorToGray(grayInput);
// On remplit le tableau avec le nombre de pixels pour chaque niveau de // On remplit le tableau avec le nombre de pixels pour chaque niveau de
// gris // gris
BoofConcurrency.loopFor(0, input.height, y -> { BoofConcurrency.loopFor(0, input.height, y -> {
@@ -218,7 +221,7 @@ public class ImageProcessing {
* @param hue La valeur de la teinte à appliquer * @param hue La valeur de la teinte à appliquer
* @param input L'image à modifier * @param input L'image à modifier
*/ */
public static void colorFilter(int hue, Planar<GrayU8> input) { public static void colorFilter(Planar<GrayU8> input, int hue) {
if (hue < 0 || hue > 360) { if (hue < 0 || hue > 360) {
throw new IllegalArgumentException("Hue doit être compris entre 0 et 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 * Calcule et stocke dans output l'image de la norme du gradient
* calculé sur input par convolution avec les filtres de Sobel. * calculé sur input par convolution avec les filtres de Sobel.
* *
* @param input L'image d'entrée en niveaux de gris. * @param input L'image d'entrée en niveaux de gris.
* @param output L'image de sortie en niveaux de gris.
*/ */
public static void gradientImageSobel(Planar<GrayU8> input, Planar<GrayU8> output) { public static void gradientImageSobel(Planar<GrayU8> input) {
Planar<GrayU8> output = input.createSameShape();
int h1[][] = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } }; int h1[][] = { { -1, -2, -1 }, { 0, 0, 0 }, { 1, 2, 1 } };
int h2[][] = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } }; int h2[][] = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
int half = h1.length / 2; int half = h1.length / 2;
Planar<GrayU8> inputGray = input.createSameShape(); Planar<GrayU8> inputGray = input.clone();
convertColorToGray(input, inputGray); convertColorToGray(inputGray);
BoofConcurrency.loopFor(0, input.height, y -> { BoofConcurrency.loopFor(0, input.height, y -> {
for (int x = 0; x < input.width; x++) { for (int x = 0; x < input.width; x++) {
if (y < half || x < half || y >= input.height - half || x >= input.width - half) { 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); 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 * 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 input image d'entrée
* @param output image de sortie * @param size taille du filtre à appliquer (doit être impaire)
* @param size taille du filtre à appliquer (doit être impaire) * @param borderType type de bord à utiliser pour la convolution
*/ */
public static void meanFilter(Planar<GrayU8> input, Planar<GrayU8> output, int size) { public static void meanFilter(Planar<GrayU8> input, int size, BorderType borderType) {
// Vérification que la taille est impaire // Vérification que la taille est impaire
if (size % 2 == 0) { if (size % 2 == 0 || size < 0) {
throw new IllegalArgumentException("La taille doit être impaire"); throw new IllegalArgumentException(
"La taille doit être impaire et positive");
} }
// Boucle pour parcourir tous les pixels de l'image d'entrée int[][] kernel = new int[size][size];
BoofConcurrency.loopFor(0, input.height, y -> { for (int i = 0; i < size; i++) {
for (int x = 0; x < input.width; x++) { for (int j = 0; j < size; j++) {
if (y < size / 2 || x < size / 2 || y >= input.height - size / 2 kernel[i][j] = 1;
|| 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);
} }
}); }
convolution(input, kernel, borderType);
} }
/** /**
* TODO
* Calcule le noyau gaussien de taille demandée
* *
* @param size taille du noyau gaussien * Applique un filtre gaussien de taille et de valeur données sur une image et
* @return noyau gaussien * 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) { public static void gaussienFilter(Planar<GrayU8> input, int size, double sigma, BorderType borderType) {
// Vérification que la taille est impaire if (size % 2 == 0 || size < 0 || sigma <= 0) {
if (size % 2 == 0 || size < 0) { throw new IllegalArgumentException(
throw new IllegalArgumentException("La taille doit être impaire et positive"); "La taille doit être impaire et positive et sigma doit être strictement positif");
} }
double[][] kernel = new double[size][size]; double[][] kernel = new double[size][size];
double sigma = 4 / 3; int half = size / 2;
double sum = 0.0; // For accumulating the kernel values double sum = 0;
for (int x = 0; x < size; ++x) for (int x = -half; x <= half; ++x) {
for (int y = 0; y < size; ++y) { for (int y = -half; y <= half; ++y) {
kernel[x][y] = 1 / (2 * Math.PI * sigma * sigma) * Math.exp(-(x * x + y * y) / (2 * sigma * sigma)); 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[][] kernelInt = new int[size][size];
int[][] kernelk = new int[size][size]; double factor = 1 / kernel[0][0];
for (int x = 0; x < size; ++x) // Si le facteur est trop grand (la valeur du coin est très très petite par
for (int y = 0; y < size; ++y) // rapport au reste), on le fixe à 1 million
kernelk[x][y] = (int) (kernel[x][y] / sum); if (factor > 1000000) {
factor = 1000000;
return kernelk; }
// 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) { public static int[][] createGaussienKernel(int size, double sigma) {
@@ -397,15 +406,14 @@ public class ImageProcessing {
* Effectue une convolution sur une image avec le noyau * Effectue une convolution sur une image avec le noyau
* donné et stocke le résultat dans l'image de sortie. * donné et stocke le résultat dans l'image de sortie.
* *
* @param input L'image d'entrée * @param input L'image d'entrée
* @param output L'image de sortie * @param kernel Le noyau de convolution à utiliser.
* @param kernel Le noyau de convolution à utiliser. * @param borderType Le type de bord à utiliser pour la convolution
*/ */
public static void gaussienFilter(Planar<GrayU8> input, Planar<GrayU8> output, public static void convolution(Planar<GrayU8> input, int[][] kernel, BorderType borderType) {
int size) { Planar<GrayU8> copy = input.clone();
int[][] kernel = { { 1, 2, 3, 2, 1 }, { 2, 6, 8, 6, 2 }, { 3, 8, 10, 8, 3 }, { 2, 6, 8, 6, 2 }, int size = kernel.length;
{ 1, 2, 3, 2, 1 } }; int half = kernel.length / 2;
int half = size / 2;
int coefs = 0; int coefs = 0;
// Calcul du nombre total de coefficients dans le noyau // Calcul du nombre total de coefficients dans le noyau
for (int i = 0; i < kernel.length; i++) { 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 // Parcours de l'image d'entrée et application de la convolution
BoofConcurrency.loopFor(0, input.height, y -> { BoofConcurrency.loopFor(0, input.height, y -> {
for (int x = 0; x < input.width; x++) { 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 - if (y < half || x < half || y >= input.height - half || x >= input.width -
half) { 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; continue;
} }
int currentJ = 0; int currentJ = 0;
@@ -430,7 +482,7 @@ public class ImageProcessing {
for (int j = y - half; j <= y + half; j++) { for (int j = y - half; j <= y + half; j++) {
for (int i = x - half; i <= x + half; i++) { for (int i = x - half; i <= x + half; i++) {
for (int k = 0; k < totalValue.length; k++) { 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++; currentI++;
} }
@@ -440,7 +492,7 @@ public class ImageProcessing {
for (int k = 0; k < totalValue.length; k++) { for (int k = 0; k < totalValue.length; k++) {
totalValue[k] /= totalCoefs; totalValue[k] /= totalCoefs;
} }
setRGBValue(output, x, y, totalValue); setRGBValue(input, x, y, totalValue);
} }
}); });
} }
@@ -1,4 +1,4 @@
package pdl.backend; package pdl.backend.Image;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
@@ -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;
}
}
@@ -22,6 +22,9 @@ import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import pdl.backend.Image.Image;
import pdl.backend.Image.ImageDao;
@SpringBootTest @SpringBootTest
@AutoConfigureMockMvc @AutoConfigureMockMvc
@TestMethodOrder(OrderAnnotation.class) @TestMethodOrder(OrderAnnotation.class)
@@ -7,6 +7,8 @@ import java.io.IOException;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import pdl.backend.Image.ImageDao;
public class ImageDaoTests { public class ImageDaoTests {
@Test @Test
+3 -44
View File
@@ -3,6 +3,7 @@ import Home from './components/Home.vue'
import Gallery from './components/Gallery.vue'; import Gallery from './components/Gallery.vue';
import NotFound from './components/NotFound.vue' import NotFound from './components/NotFound.vue'
import Upload from './components/Upload.vue' import Upload from './components/Upload.vue'
import StableDiff from './components/StableDiff.vue';
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { api } from '@/http-api' import { api } from '@/http-api'
import { ImageType, ImageClass } from './image'; import { ImageType, ImageClass } from './image';
@@ -48,6 +49,7 @@ export default defineComponent({
<li><router-link :to="{ name: 'home' }">Home</router-link></li> <li><router-link :to="{ name: 'home' }">Home</router-link></li>
<li><router-link :to="{ name: 'gallery' }">Gallery</router-link></li> <li><router-link :to="{ name: 'gallery' }">Gallery</router-link></li>
<li><router-link :to="{ name: 'upload' }">Upload</router-link></li> <li><router-link :to="{ name: 'upload' }">Upload</router-link></li>
<li><router-link :to="{ name: 'generate' }">Generate Images</router-link></li>
</ul> </ul>
</nav> </nav>
</div> </div>
@@ -55,12 +57,7 @@ export default defineComponent({
<RouterView :images="images" @delete="deleteFile" @updateImageList="updateImageList()" /> <RouterView :images="images" @delete="deleteFile" @updateImageList="updateImageList()" />
</template> </template>
<style scoped> <style scoped>
.navi { @import url("@/navi.css");
/* reduce size of children */
display: flex;
flex-direction: row-reverse;
pointer-events: none;
}
header { header {
display: block; display: block;
@@ -72,17 +69,6 @@ header {
pointer-events: none; pointer-events: none;
} }
ul>li+li {
margin-left: 30px;
}
ul>li {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.router-link-active { .router-link-active {
background-color: #646cff; background-color: #646cff;
} }
@@ -90,31 +76,4 @@ ul>li {
.router-link-active>a { .router-link-active>a {
color: rgb(205, 201, 194); color: rgb(205, 201, 194);
} }
a {
padding: 5px 15px;
border-radius: 60px;
color: rgb(172, 165, 154);
text-decoration: none;
font-family: "Aeonik", sans-serif;
}
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;
}
ul {
display: flex;
list-style: none;
padding-inline-start: 0;
}
</style> </style>
@@ -1,3 +1,47 @@
<template>
<div class="custom-select" @blur="open = false">
<div class="selected" :class="{ open: open }" @click="open = !open">
<span>{{ list[selected] }}</span>
</div>
<div class="items" :class="{ selectHide: !open }">
<div v-for="(name, index) in list" @click="open = false; $emit('updateSelected', index)">
<span>{{ name }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from 'vue'
</script>
<script lang="ts">
export default defineComponent({
props: {
list: {
type: Array as () => string[],
required: true,
},
selected: {
type: Number,
required: true,
},
},
emits: {
updateSelected(id: number) {
return id >= 0;
}
},
data() {
return {
open: false,
}
},
})
</script>
<style scoped>
.custom-select { .custom-select {
width: 100%; width: 100%;
text-align: left; text-align: left;
@@ -68,4 +112,5 @@
.selectHide { .selectHide {
display: none; display: none;
} }
</style>
+3 -4
View File
@@ -19,7 +19,7 @@ export default defineComponent({
<div class="flex" v-if="images.size > 0"> <div class="flex" v-if="images.size > 0">
<div v-for="[id, image] in images" :key="id" class="image"> <div v-for="[id, image] in images" :key="id" class="image">
<label>{{ image.name }}</label> <label>{{ image.name }}</label>
<Image class="gallery" :id="id" :images="images" redirect="home" /> <Image :id="id" :images="images" redirect="home" />
</div> </div>
</div> </div>
<h1 v-else>Aucune image trouvée</h1> <h1 v-else>Aucune image trouvée</h1>
@@ -59,9 +59,8 @@ label {
font-size: 1.5em; font-size: 1.5em;
color: white; color: white;
} }
</style>
<style> :deep(.imageContainer) {
.gallery.imageContainer {
margin: 2vw; margin: 2vw;
max-height: min(33vh, 33vw); max-height: min(33vh, 33vw);
max-width: min(33vw, 33vh); max-width: min(33vw, 33vh);
+35 -55
View File
@@ -22,7 +22,6 @@ export default defineComponent({
data() { data() {
return { return {
imageSelectedId: -1, imageSelectedId: -1,
timer: null as ReturnType<typeof setTimeout> | null,
editor: false, editor: false,
filter: undefined as Filter | undefined, filter: undefined as Filter | undefined,
} }
@@ -38,6 +37,9 @@ export default defineComponent({
'images.size': { 'images.size': {
handler() { this.updateImageSelectedId(); }, handler() { this.updateImageSelectedId(); },
}, },
imageSelectedId() {
this.filter = undefined;
}
}, },
methods: { methods: {
updateImageSelectedId(id: number = -1) { updateImageSelectedId(id: number = -1) {
@@ -47,35 +49,29 @@ export default defineComponent({
this.imageSelectedId = id; this.imageSelectedId = id;
else else
this.imageSelectedId = this.images.keys().next().value; this.imageSelectedId = this.images.keys().next().value;
},
startTimer() {
if (this.timer !== null)
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.editor = !this.editor;
}, 1900);
},
clearTimer() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
} }
} }
}) })
</script> </script>
<template> <template>
<div class="flex" v-if="images.size > 0"> <div class="flex" v-if="images.size > 0">
<h1 v-if="!editor">MODE SÉLECTION</h1> <div class="navi">
<h1 v-else>MODE ÉDITEUR</h1> <nav>
<ul>
<li>
<h3 :class="{ active: !editor }" @click="editor = false">Mode sélection</h3>
</li>
<li>
<h3 :class="{ active: editor }" @click="editor = true">Mode éditeur</h3>
</li>
</ul>
</nav>
</div>
<HomeSelect v-if="!editor" :images="images" :image-selected-id="imageSelectedId" @delete="id => $emit('delete', id)" <HomeSelect v-if="!editor" :images="images" :image-selected-id="imageSelectedId" @delete="id => $emit('delete', id)"
@updateImageSelectedId="id => updateImageSelectedId(id)"></HomeSelect> @updateImageSelectedId="id => updateImageSelectedId(id)"></HomeSelect>
<HomeEditor v-else @apply-filter="(newFilter) => filter = newFilter"></HomeEditor> <HomeEditor v-else @apply-filter="(newFilter) => filter = newFilter"></HomeEditor>
<h5 v-if="!editor">Restez sur l'image pour passer en mode éditeur</h5> <Image class="home" v-if="imageSelectedId !== undefined && imageSelectedId !== -1" :id="imageSelectedId"
<h5 v-else>Restez sur l'image pour passer en mode selection</h5> :images="images" :filter="filter">
<Image @mouseleave="clearTimer()" @mouseenter="startTimer()" class="home"
v-if="imageSelectedId !== undefined && imageSelectedId !== -1" :id="imageSelectedId" :images="images"
:filter="filter">
</Image> </Image>
<a class="button" :href="images.get(imageSelectedId)?.data" <a class="button" :href="images.get(imageSelectedId)?.data"
:download="images.get(imageSelectedId)?.name">Télécharger {{ images.get(imageSelectedId)?.name }}</a> :download="images.get(imageSelectedId)?.name">Télécharger {{ images.get(imageSelectedId)?.name }}</a>
@@ -83,16 +79,13 @@ export default defineComponent({
<h1 v-else>Aucune image trouvée</h1> <h1 v-else>Aucune image trouvée</h1>
</template> </template>
<style scoped> <style scoped>
@import url("@/navi.css");
h1 { h1 {
color: white; color: white;
margin: 0; margin: 0;
} }
h5 {
color: white;
margin: 1vw 0 0 0;
}
a { a {
text-decoration: none; text-decoration: none;
} }
@@ -106,37 +99,24 @@ html {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} gap: 4vh;
</style>
<style>
.home.imageContainer {
max-width: 60%;
width: auto;
height: auto;
max-height: 60vh;
margin: 0 0 2vw 0;
} }
div.home.imageContainer:hover { .active {
animation: zoom-in-zoom-out 2s cubic-bezier(1, 0, 0.51, 1.01); background-color: #646cff;
}
.active a {
color: rgb(205, 201, 194);
}
.navi h3 {
cursor: pointer;
}
:deep(.imageContainer) {
max-width: min(60vh, 60vw);
max-height: min(60vh, 60vw);
transform: none; transform: none;
} }
@keyframes zoom-in-zoom-out {
0% {
transform: scale(1);
}
60% {
transform: scale(1.5);
}
75% {
transform: scale(10);
}
100% {
transform: none;
}
}
</style> </style>
+93 -34
View File
@@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { Filter, clone } from '@/filter' import { Filter, clone, FilterType, RangeParameters, SelectParameters } from '@/filter'
import { api } from "@/http-api"; import { api } from "@/http-api";
import CustomSelector from './CustomSelector.vue';
</script> </script>
<script lang="ts"> <script lang="ts">
@@ -15,73 +16,120 @@ export default defineComponent({
data() { data() {
return { return {
filterSelectId: 0, filterSelectId: 0,
open: false,
filters: [] as Filter[], filters: [] as Filter[],
error: false, error: "",
} filterApplied: false,
};
}, },
created() { created() {
api.getAlgorithmList().then((filters) => { api.getAlgorithmList().then((filters) => {
this.filters = filters; this.filters = filters;
// fill value attribute
for (const filter of this.filters) {
for (const arg of filter.parameters) {
if (arg.type === FilterType.range) {
const range = arg as RangeParameters;
range.value = range.min;
}
else if (arg.type === FilterType.select) {
const select = arg as SelectParameters;
select.value = select.options[0];
}
}
}
this.error = this.areInputValid();
}); });
}, },
methods: { methods: {
applyFilter() { applyFilter() {
if (this.filterSelectId === 0) { if (this.error !== "")
this.$emit("applyFilter", undefined);
return; return;
} const filter = clone(this.filters[this.filterSelectId]);
const filter = clone(this.filters[this.filterSelectId - 1]);
for (const arg of filter.parameters) { for (const arg of filter.parameters) {
if (arg.value === undefined) { if (arg.value === undefined) {
this.error = true; this.error = "";
return; return;
} }
} }
this.filterApplied = true;
this.$emit("applyFilter", filter); this.$emit("applyFilter", filter);
},
areInputValid() {
for (const p of this.filters[this.filterSelectId].parameters) {
const input = document.getElementById(p.name) as HTMLInputElement;
if (input === null)
return "";
if (!input.checkValidity())
return "Paramètre(s) du filtre invalide(s)";
}
return "";
} }
} },
watch: {
filterSelectId() {
this.error = this.areInputValid();
},
filters: {
handler() {
this.error = this.areInputValid();
},
deep: true
}
},
components: { CustomSelector }
}) })
</script> </script>
<template> <template>
<div class="editor"> <div class="editor">
<label>Sélectionner un filtre: </label> <label>Sélectionner un filtre: </label>
<div class="custom-select" @blur="open = false"> <CustomSelector :list="filters.map((f) => { return f.name })" :selected="filterSelectId"
<div class="selected" :class="{ open: open }" @click="open = !open"> @update-selected="filterSelectId = $event" />
<span>{{ filterSelectId === 0 ? "Aucun filtre" : filters[filterSelectId - 1].name }}</span> <!-- Filter parameters -->
<div v-if="filters.length !== 0" v-for="parameter in filters[filterSelectId].parameters" class="rangeInput">
<div v-if="parameter.type === FilterType.range">
<div class="labelInput">
<label>{{ parameter.name }}: </label>
<input type="number" :min="(parameter as RangeParameters).min" :max="(parameter as RangeParameters).max"
:step="(parameter as RangeParameters).step" v-model.number="parameter.value" :id="parameter.name" />
</div>
<input type="range" :min="(parameter as RangeParameters).min" :max="(parameter as RangeParameters).max"
:step="(parameter as RangeParameters).step" v-model.number="(parameter as RangeParameters).value" />
</div> </div>
<div class="items" :class="{ selectHide: !open }"> <div v-else-if="parameter.type === FilterType.select" class="selectParam">
<div @click="open = false; filterSelectId = 0"> <label>{{ parameter.name }}: </label>
<span>Aucun filtre</span> <CustomSelector :list="(parameter as SelectParameters).options"
</div> :selected="(parameter as SelectParameters).options.findIndex((o) => o === (parameter as SelectParameters).value)"
<div v-for="(filter, index) in filters" @click="open = false; filterSelectId = index + 1"> @update-selected="parameter.value = (parameter as SelectParameters).options[$event]" />
<span>{{ filter.name }}</span>
</div>
</div> </div>
</div> </div>
<div v-if="filters.length != 0 && filterSelectId !== 0" v-for="parameter in filters[filterSelectId - 1].parameters" <button @mouseenter="error = areInputValid()" @mouseleave="error = areInputValid()" class="button"
class="rangeInput"> :class="{ errorClass: error !== '' }" @click="applyFilter()">
<label>{{ parameter.name }}: ({{ parameter.value }}) </label> {{ error !== "" ? error : "Appliquer le filtre" }}</button>
<input type="range" :min="parameter.min" :max="parameter.max" :step="parameter.step" <button v-if="filterApplied" class="button"
v-model.number="parameter.value" /> @click="$emit('applyFilter', undefined); filterApplied = false">Supprimer le filtre</button>
</div>
<button @mouseleave="error = false" class="button" :class="{ erreurClass: error }" @click="applyFilter()">
{{ error ? "Erreur" : "Appliquer le filtre" }}</button>
</div> </div>
</template> </template>
<style scoped> <style scoped>
@import url("@/customSelect.css");
label { label {
color: white; color: white;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0; flex-shrink: 0;
} }
.labelInput {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.labelInput input {
width: min-content;
height: 1em;
font-size: 0.8em;
}
.editor { .editor {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -90,8 +138,19 @@ label {
gap: 10px; gap: 10px;
} }
.erreurClass { .errorClass {
background-color: red; background-color: red;
color: white; color: white;
} }
input:invalid {
background-color: red;
}
.selectParam {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
</style> </style>
+3 -15
View File
@@ -2,7 +2,7 @@
import { defineComponent } from 'vue' import { defineComponent } from 'vue'
import { ImageType } from '@/image' import { ImageType } from '@/image'
import CustomSelector from './CustomSelector.vue';
</script> </script>
<script lang="ts"> <script lang="ts">
@@ -28,7 +28,6 @@ export default defineComponent({
data() { data() {
return { return {
sure: false, sure: false,
open: false,
} }
}, },
watch: { watch: {
@@ -52,25 +51,14 @@ export default defineComponent({
<template> <template>
<div class="selector"> <div class="selector">
<label>Sélectionner une image: </label> <label>Sélectionner une image: </label>
<div class="custom-select" @blur="open = false"> <CustomSelector :list="Array.from(images.values()).map((i) => { return i.name })" :selected="imageSelectedId"
<div class="selected" :class="{ open: open }" @click="open = !open"> @update-selected="$emit('updateImageSelectedId', $event)" />
<span>{{ images.get(imageSelectedId)?.name }}</span>
</div>
<div class="items" :class="{ selectHide: !open }">
<div v-for="[id, image] of images" :key="id"
@click="$emit('updateImageSelectedId', image.id); open = false;">
<span>{{ image.name }}</span>
</div>
</div>
</div>
<button class="button" @mouseleave="sure = false" @click="confirmDelete()" :class="{ confirm: sure }"> <button class="button" @mouseleave="sure = false" @click="confirmDelete()" :class="{ confirm: sure }">
{{ sure ? "Confirmer" : "Supprimer" }}</button> {{ sure ? "Confirmer" : "Supprimer" }}</button>
</div> </div>
</template> </template>
<style scoped> <style scoped>
@import url(@/customSelect.css);
.selector { .selector {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
+1 -3
View File
@@ -73,9 +73,7 @@ export default defineComponent({
<style> <style>
.imageContainer img { .imageContainer img {
max-width: 100%; max-width: 100%;
max-height: 100vh; max-height: 100%;
height: auto;
width: auto;
object-fit: contain; object-fit: contain;
} }
+248
View File
@@ -0,0 +1,248 @@
<template>
<div>
<div class="preview">
<div v-if="started" class="loadingbar">
<div class="loadedbar" :style="{ width: progress + '%' }">
<span>ETA: {{ eta }}s</span>
</div>
</div>
<img :src="generatedImage" />
</div>
<div class="dl" v-if="generatedImage != ''">
<a :href="generatedImage" download="generated.png" class="button">Download</a>
<button class="button" @click="sendImage()" :class="{ sent: sent }">{{ sent ? "Envoyé" : "Upload to server"
}}</button>
</div>
<div class="form">
<label for="promptImage">Prompt Image</label>
<textarea v-model="promptImage" />
<label for="negativePromptImage">Negative Prompt Image</label>
<textarea v-model="negativePromptImage" />
<div class="labelInput">
<label for="height">Height</label>
<input type="number" v-model.number="height" />
</div>
<input type="range" :min="minmaxHeight.min" :max="minmaxHeight.max" v-model.number="height" />
<div class="labelInput">
<label for="width">Width</label>
<input type="number" v-model.number="width" />
</div>
<input type="range" :min="minmaxWidth.min" :max="minmaxWidth.max" v-model.number="width" />
<div class="labelInput">
<label for="steps">Steps</label>
<input type="number" v-model.number="steps" />
</div>
<input type="range" :min="minmaxSteps.min" :max="minmaxSteps.max" v-model.number="steps" />
<div class="labelInput">
<label for="cfgScale">Cfg Scale</label>
<input type="number" v-model.number="cfgScale" />
</div>
<input type="range" :min="minmaxCfgScale.min" :max="minmaxCfgScale.max" v-model.number="cfgScale" />
<button :disabled="started" class="button" @click="generate">{{ started ? "Generation en cours" :
"Generate" }}</button>
</div>
</div>
</template>
<script setup lang="ts">
import { defineComponent } from 'vue';
import { api } from '@/http-api';
</script>
<script lang="ts">
const minmaxHeight = {
min: 400,
max: 512
}
const minmaxWidth = {
min: 400,
max: 512
}
const minmaxSteps = {
min: 10,
max: 40
}
const minmaxCfgScale = {
min: 0,
max: 30
}
export default defineComponent({
emits: ['updateImageList'],
data() {
return {
promptImage: '',
negativePromptImage: '',
height: 512,
width: 512,
steps: 20,
cfgScale: 7,
generatedImage: '',
started: false,
progress: 0,
eta: 0,
sent: false,
}
},
methods: {
async generate() {
api.generateImage(this.promptImage, this.negativePromptImage, this.height, this.width, this.steps, this.cfgScale).then((data) => {
this.generatedImage = "data:image/png;base64," + data.images[0];
this.started = false;
});
this.sent = false;
this.generatedImage = '';
this.started = true;
},
urltoFile(url: string, filename: string, mimeType: string) {
return (fetch(url)
.then(function (res) { return res.arrayBuffer(); })
.then(function (buf) { return new File([buf], filename, { type: mimeType }); })
);
},
async sendImage() {
if (!this.sent) {
this.sent = true;
const file = await this.urltoFile(this.generatedImage, "generated" + Date.now() + ".png", "image/png");
const formData = new FormData();
formData.append("file", file);
api.createImage(formData).then((data) => {
console.log(data);
this.$emit('updateImageList');
});
}
}
},
watch: {
height() {
if (this.height < minmaxHeight.min)
this.height = minmaxHeight.min;
else if (this.height > minmaxHeight.max)
this.height = minmaxHeight.max;
},
width() {
if (this.width < minmaxWidth.min)
this.width = minmaxWidth.min;
else if (this.width > minmaxWidth.max)
this.width = minmaxWidth.max;
},
steps() {
if (this.steps < minmaxSteps.min)
this.steps = minmaxSteps.min;
else if (this.steps > minmaxSteps.max)
this.steps = minmaxSteps.max;
},
cfgScale() {
if (this.cfgScale < minmaxCfgScale.min)
this.cfgScale = minmaxCfgScale.min;
else if (this.cfgScale > minmaxCfgScale.max)
this.cfgScale = minmaxCfgScale.max;
},
started() {
if (this.started) {
const interval = setInterval(() => {
api.progress().then((data) => {
this.progress = data.progress * 100;
this.eta = Math.floor(data.eta_relative);
if (data.current_image !== null) {
this.generatedImage = "data:image/png;base64," + data.current_image;
}
if (data.state.job_count === 0) {
clearInterval(interval);
}
if (!this.started) {
clearInterval(interval);
}
});
}, 1000);
}
},
}
})
</script>
<style scoped>
.form {
display: flex;
flex-direction: column;
align-items: center;
}
label {
color: white;
}
.sent {
color: green;
}
.form textarea {
width: 100%;
margin: 0;
margin-bottom: 1vh;
}
.form input {
width: 100%;
margin: 0;
margin-bottom: 1vh;
}
a.button {
text-decoration: none;
height: 1.5em;
width: min-content;
}
.dl {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
padding: 10px;
gap: 1vw;
}
.labelInput {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
}
.labelInput input {
width: 37px;
}
.loadingbar {
width: 100%;
height: 25px;
background-color: #c9c9c9;
margin-bottom: 25px;
}
.loadedbar {
height: 100%;
background-color: #4caf50;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
white-space: nowrap;
}
.preview {
display: flex;
flex-direction: column;
align-items: center;
}
.preview img {
width: 100%;
}
</style>
+8 -1
View File
@@ -13,7 +13,7 @@ interface InputFileEvent extends Event {
} }
export default defineComponent({ export default defineComponent({
emits: ['updateImageList', 'delete'], emits: ['updateImageList'],
data() { data() {
return { return {
file: null as FileList | null, file: null as FileList | null,
@@ -27,8 +27,15 @@ export default defineComponent({
images: { images: {
type: Map<number, ImageType>, type: Map<number, ImageType>,
required: true required: true
},
fileDefault: {
type: FileList,
default: null
} }
}, },
created() {
this.file = this.fileDefault;
},
methods: { methods: {
async submitImage() { async submitImage() {
if (this.classSendButton === "sent") { if (this.classSendButton === "sent") {
+57 -12
View File
@@ -1,22 +1,48 @@
export class Parameters { export enum FilterType {
"range" = "range",
"select" = "select",
}
export abstract class Parameters {
name: string name: string
displayName: string displayName: string
min: number type: FilterType
max: number value: any
step: number
value: number | undefined
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.name = name
this.displayName = displayName this.displayName = displayName
this.min = min this.type = type
this.max = max
this.step = step
this.value = value
} }
} }
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 { export class Filter {
name: string name: string
path: string path: string
@@ -31,9 +57,28 @@ export class Filter {
} }
export function getParameters(f: Filter): string { 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 { 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)
}
}))
} }
+56 -2
View File
@@ -2,9 +2,44 @@ import axios, { AxiosResponse, AxiosError, HttpStatusCode } from 'axios';
import { ImageType } from '@/image'; import { ImageType } from '@/image';
import { Filter, getParameters } from './filter'; 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({ const instance = axios.create({
baseURL: "/", 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; const responseBody = (response: AxiosResponse) => response.data;
@@ -14,7 +49,12 @@ const requests = {
get: (url: string, param: {}) => instance.get(url, param).then(responseBody), 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), 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), 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 = { export const api = {
@@ -23,4 +63,18 @@ export const api = {
getImage: (id: number, filter: Filter | undefined): Promise<Blob> => requests.get(`images/${id}${filter !== undefined ? "?algorithm=" + filter.path + getParameters(filter) : ""}`, { responseType: "blob" }), getImage: (id: number, filter: Filter | undefined): Promise<Blob> => requests.get(`images/${id}${filter !== undefined ? "?algorithm=" + filter.path + getParameters(filter) : ""}`, { responseType: "blob" }),
createImage: (form: FormData): Promise<HttpStatusCode> => requests.post('images', form), createImage: (form: FormData): Promise<HttpStatusCode> => requests.post('images', form),
deleteImage: (id: number): Promise<void> => requests.delete(`images/${id}`), deleteImage: (id: number): Promise<void> => requests.delete(`images/${id}`),
generateImage: (prompt: string, negativePrompt: string, height: number, width: number, steps: number, cfgScale: number): Promise<generateImageInterface> => 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<progressInterface> => stableDiffRequests.get('sdapi/v1/progress', {}),
}; };
+43
View File
@@ -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;
}
+5
View File
@@ -18,6 +18,11 @@ const router = createRouter({
name: "upload", name: "upload",
component: () => import('../components/Upload.vue') component: () => import('../components/Upload.vue')
}, },
{
path: '/generateImages',
name: "generate",
component: () => import('../components/StableDiff.vue')
},
{ {
path: '/:pathMatch(.*)*', path: '/:pathMatch(.*)*',
name: 'not-found', name: 'not-found',