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
+1097 -463

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.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<Algorithm> 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<GrayU8> 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);
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 {
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);
p.setValue(parameters.get(p.getName()));
} catch (Exception e) {
return JSONError("Parameter '" + p.getName() + "' is invalid", 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);
a.apply(input);
} catch (Exception e) {
return JSONError("There was an internal server error", HttpStatus.INTERNAL_SERVER_ERROR);
}
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<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;
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
@@ -1,4 +1,4 @@
package pdl.backend;
package pdl.backend.Image;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
@@ -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<Image> {
@@ -21,7 +23,7 @@ public class ImageDao implements Dao<Image> {
private final Map<Long, Image> images = new HashMap<>();
public ImageDao() throws IOException {
this("images");
this("/images");
}
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.Planar;
@@ -9,7 +10,10 @@ import java.util.Random;
import boofcv.concurrency.BoofConcurrency;
public class ImageProcessing {
public final class ImageProcessing {
private ImageProcessing() {
}
/**
*
@@ -47,14 +51,13 @@ 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
*/
public static void convertColorToGray(Planar<GrayU8> input, Planar<GrayU8> output) {
public static void convertColorToGray(Planar<GrayU8> 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<GrayU8> input) {
int h[] = new int[256];
Planar<GrayU8> grayInput = input.createSameShape();
convertColorToGray(input, grayInput);
Planar<GrayU8> 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<GrayU8> input) {
public static void colorFilter(Planar<GrayU8> input, int hue) {
if (hue < 0 || hue > 360) {
throw new IllegalArgumentException("Hue doit être compris entre 0 et 360");
}
@@ -241,14 +244,14 @@ public class ImageProcessing {
* 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.
*/
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 h2[][] = { { -1, 0, 1 }, { -2, 0, 2 }, { -1, 0, 1 } };
int half = h1.length / 2;
Planar<GrayU8> inputGray = input.createSameShape();
convertColorToGray(input, inputGray);
Planar<GrayU8> 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 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
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);
int[][] kernel = new int[size][size];
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
kernel[i][j] = 1;
}
}
}
// 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
* @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<GrayU8> 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) {
@@ -398,14 +407,13 @@ public class ImageProcessing {
* 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 borderType Le type de bord à utiliser pour la convolution
*/
public static void gaussienFilter(Planar<GrayU8> input, Planar<GrayU8> 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<GrayU8> input, int[][] kernel, BorderType borderType) {
Planar<GrayU8> 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);
}
});
}
@@ -1,4 +1,4 @@
package pdl.backend;
package pdl.backend.Image;
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.request.MockMvcRequestBuilders;
import pdl.backend.Image.Image;
import pdl.backend.Image.ImageDao;
@SpringBootTest
@AutoConfigureMockMvc
@TestMethodOrder(OrderAnnotation.class)
@@ -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
+3 -44
View File
@@ -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({
<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: 'upload' }">Upload</router-link></li>
<li><router-link :to="{ name: 'generate' }">Generate Images</router-link></li>
</ul>
</nav>
</div>
@@ -55,12 +57,7 @@ export default defineComponent({
<RouterView :images="images" @delete="deleteFile" @updateImageList="updateImageList()" />
</template>
<style scoped>
.navi {
/* reduce size of children */
display: flex;
flex-direction: row-reverse;
pointer-events: none;
}
@import url("@/navi.css");
header {
display: block;
@@ -72,17 +69,6 @@ header {
pointer-events: none;
}
ul>li+li {
margin-left: 30px;
}
ul>li {
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.router-link-active {
background-color: #646cff;
}
@@ -90,31 +76,4 @@ ul>li {
.router-link-active>a {
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>
@@ -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 {
width: 100%;
text-align: left;
@@ -69,3 +113,4 @@
.selectHide {
display: none;
}
</style>
+3 -4
View File
@@ -19,7 +19,7 @@ export default defineComponent({
<div class="flex" v-if="images.size > 0">
<div v-for="[id, image] in images" :key="id" class="image">
<label>{{ image.name }}</label>
<Image class="gallery" :id="id" :images="images" redirect="home" />
<Image :id="id" :images="images" redirect="home" />
</div>
</div>
<h1 v-else>Aucune image trouvée</h1>
@@ -59,9 +59,8 @@ label {
font-size: 1.5em;
color: white;
}
</style>
<style>
.gallery.imageContainer {
:deep(.imageContainer) {
margin: 2vw;
max-height: min(33vh, 33vw);
max-width: min(33vw, 33vh);
+35 -55
View File
@@ -22,7 +22,6 @@ export default defineComponent({
data() {
return {
imageSelectedId: -1,
timer: null as ReturnType<typeof setTimeout> | null,
editor: false,
filter: undefined as Filter | undefined,
}
@@ -38,6 +37,9 @@ export default defineComponent({
'images.size': {
handler() { this.updateImageSelectedId(); },
},
imageSelectedId() {
this.filter = undefined;
}
},
methods: {
updateImageSelectedId(id: number = -1) {
@@ -47,35 +49,29 @@ export default defineComponent({
this.imageSelectedId = id;
else
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>
<template>
<div class="flex" v-if="images.size > 0">
<h1 v-if="!editor">MODE SÉLECTION</h1>
<h1 v-else>MODE ÉDITEUR</h1>
<div class="navi">
<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)"
@updateImageSelectedId="id => updateImageSelectedId(id)"></HomeSelect>
<HomeEditor v-else @apply-filter="(newFilter) => filter = newFilter"></HomeEditor>
<h5 v-if="!editor">Restez sur l'image pour passer en mode éditeur</h5>
<h5 v-else>Restez sur l'image pour passer en mode selection</h5>
<Image @mouseleave="clearTimer()" @mouseenter="startTimer()" class="home"
v-if="imageSelectedId !== undefined && imageSelectedId !== -1" :id="imageSelectedId" :images="images"
:filter="filter">
<Image class="home" v-if="imageSelectedId !== undefined && imageSelectedId !== -1" :id="imageSelectedId"
:images="images" :filter="filter">
</Image>
<a class="button" :href="images.get(imageSelectedId)?.data"
: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>
</template>
<style scoped>
@import url("@/navi.css");
h1 {
color: white;
margin: 0;
}
h5 {
color: white;
margin: 1vw 0 0 0;
}
a {
text-decoration: none;
}
@@ -106,37 +99,24 @@ html {
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<style>
.home.imageContainer {
max-width: 60%;
width: auto;
height: auto;
max-height: 60vh;
margin: 0 0 2vw 0;
gap: 4vh;
}
div.home.imageContainer:hover {
animation: zoom-in-zoom-out 2s cubic-bezier(1, 0, 0.51, 1.01);
.active {
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;
}
@keyframes zoom-in-zoom-out {
0% {
transform: scale(1);
}
60% {
transform: scale(1.5);
}
75% {
transform: scale(10);
}
100% {
transform: none;
}
}
</style>
+91 -32
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { defineComponent } from 'vue'
import { Filter, clone } from '@/filter'
import { Filter, clone, FilterType, RangeParameters, SelectParameters } from '@/filter'
import { api } from "@/http-api";
import CustomSelector from './CustomSelector.vue';
</script>
<script lang="ts">
@@ -15,73 +16,120 @@ export default defineComponent({
data() {
return {
filterSelectId: 0,
open: false,
filters: [] as Filter[],
error: false,
}
error: "",
filterApplied: false,
};
},
created() {
api.getAlgorithmList().then((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: {
applyFilter() {
if (this.filterSelectId === 0) {
this.$emit("applyFilter", undefined);
if (this.error !== "")
return;
}
const filter = clone(this.filters[this.filterSelectId - 1]);
const filter = clone(this.filters[this.filterSelectId]);
for (const arg of filter.parameters) {
if (arg.value === undefined) {
this.error = true;
this.error = "";
return;
}
}
this.filterApplied = true;
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>
<template>
<div class="editor">
<label>Sélectionner un filtre: </label>
<div class="custom-select" @blur="open = false">
<div class="selected" :class="{ open: open }" @click="open = !open">
<span>{{ filterSelectId === 0 ? "Aucun filtre" : filters[filterSelectId - 1].name }}</span>
<CustomSelector :list="filters.map((f) => { return f.name })" :selected="filterSelectId"
@update-selected="filterSelectId = $event" />
<!-- 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>
<div class="items" :class="{ selectHide: !open }">
<div @click="open = false; filterSelectId = 0">
<span>Aucun filtre</span>
<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 v-for="(filter, index) in filters" @click="open = false; filterSelectId = index + 1">
<span>{{ filter.name }}</span>
<div v-else-if="parameter.type === FilterType.select" class="selectParam">
<label>{{ parameter.name }}: </label>
<CustomSelector :list="(parameter as SelectParameters).options"
:selected="(parameter as SelectParameters).options.findIndex((o) => o === (parameter as SelectParameters).value)"
@update-selected="parameter.value = (parameter as SelectParameters).options[$event]" />
</div>
</div>
</div>
<div v-if="filters.length != 0 && filterSelectId !== 0" v-for="parameter in filters[filterSelectId - 1].parameters"
class="rangeInput">
<label>{{ parameter.name }}: ({{ parameter.value }}) </label>
<input type="range" :min="parameter.min" :max="parameter.max" :step="parameter.step"
v-model.number="parameter.value" />
</div>
<button @mouseleave="error = false" class="button" :class="{ erreurClass: error }" @click="applyFilter()">
{{ error ? "Erreur" : "Appliquer le filtre" }}</button>
<button @mouseenter="error = areInputValid()" @mouseleave="error = areInputValid()" class="button"
:class="{ errorClass: error !== '' }" @click="applyFilter()">
{{ error !== "" ? error : "Appliquer le filtre" }}</button>
<button v-if="filterApplied" class="button"
@click="$emit('applyFilter', undefined); filterApplied = false">Supprimer le filtre</button>
</div>
</template>
<style scoped>
@import url("@/customSelect.css");
label {
color: white;
display: flex;
align-items: center;
justify-content: center;
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 {
display: flex;
flex-direction: row;
@@ -90,8 +138,19 @@ label {
gap: 10px;
}
.erreurClass {
.errorClass {
background-color: red;
color: white;
}
input:invalid {
background-color: red;
}
.selectParam {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
</style>
+3 -15
View File
@@ -2,7 +2,7 @@
import { defineComponent } from 'vue'
import { ImageType } from '@/image'
import CustomSelector from './CustomSelector.vue';
</script>
<script lang="ts">
@@ -28,7 +28,6 @@ export default defineComponent({
data() {
return {
sure: false,
open: false,
}
},
watch: {
@@ -52,25 +51,14 @@ export default defineComponent({
<template>
<div class="selector">
<label>Sélectionner une image: </label>
<div class="custom-select" @blur="open = false">
<div class="selected" :class="{ open: open }" @click="open = !open">
<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>
<CustomSelector :list="Array.from(images.values()).map((i) => { return i.name })" :selected="imageSelectedId"
@update-selected="$emit('updateImageSelectedId', $event)" />
<button class="button" @mouseleave="sure = false" @click="confirmDelete()" :class="{ confirm: sure }">
{{ sure ? "Confirmer" : "Supprimer" }}</button>
</div>
</template>
<style scoped>
@import url(@/customSelect.css);
.selector {
display: flex;
flex-direction: row;
+1 -3
View File
@@ -73,9 +73,7 @@ export default defineComponent({
<style>
.imageContainer img {
max-width: 100%;
max-height: 100vh;
height: auto;
width: auto;
max-height: 100%;
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({
emits: ['updateImageList', 'delete'],
emits: ['updateImageList'],
data() {
return {
file: null as FileList | null,
@@ -27,8 +27,15 @@ export default defineComponent({
images: {
type: Map<number, ImageType>,
required: true
},
fileDefault: {
type: FileList,
default: null
}
},
created() {
this.file = this.fileDefault;
},
methods: {
async submitImage() {
if (this.classSendButton === "sent") {
+54 -9
View File
@@ -1,21 +1,47 @@
export class Parameters {
export enum FilterType {
"range" = "range",
"select" = "select",
}
export abstract class Parameters {
name: string
displayName: string
type: FilterType
value: any
constructor(name: string, displayName: string, type: FilterType) {
this.name = name
this.displayName = displayName
this.type = type
}
}
export class RangeParameters extends Parameters {
min: number
max: number
step: number
value: number | undefined
constructor(name: string, displayName: string, min: number, max: number, step: number = 1, value: number | undefined = undefined) {
this.name = name
this.displayName = displayName
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
this.value = value
}
}
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
@@ -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)
}
}))
}
+56 -2
View File
@@ -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<Blob> => requests.get(`images/${id}${filter !== undefined ? "?algorithm=" + filter.path + getParameters(filter) : ""}`, { responseType: "blob" }),
createImage: (form: FormData): Promise<HttpStatusCode> => requests.post('images', form),
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",
component: () => import('../components/Upload.vue')
},
{
path: '/generateImages',
name: "generate",
component: () => import('../components/StableDiff.vue')
},
{
path: '/:pathMatch(.*)*',
name: 'not-found',