From d1a6e859daadd08cbdc7bd4bfea2ecb13cfc35a5 Mon Sep 17 00:00:00 2001 From: NicolasBi Date: Tue, 22 May 2018 20:40:55 +0200 Subject: [PATCH] Added common package, splitted common functions, updatd picture showing, updated inpainting --- main.py | 8 ++--- rapport/rapport.tex | 6 ++++ src/common/__init__.py | 0 src/common/decorators.py | 17 +++++++++ src/{common.py => common/math.py} | 22 +----------- src/common/matplotlib.py | 36 +++++++++++++++++++ src/inpainting.py | 59 ++++++++++++------------------- src/picture_tools/examples.py | 5 +++ src/picture_tools/picture.py | 42 ++++++++++++++++------ 9 files changed, 124 insertions(+), 71 deletions(-) create mode 100644 src/common/__init__.py create mode 100644 src/common/decorators.py rename src/{common.py => common/math.py} (72%) create mode 100644 src/common/matplotlib.py diff --git a/main.py b/main.py index 2247b90..6a4931d 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from src.picture_tools.examples import CAMERAMAN, HOUSE, JETPLANE, LAKE, LENA_COLOR_256, LENA_COLOR_512, LENA_GRAY_256, \ LENA_GRAY_512, LIVINGROOM, MANDRIL_COLOR, MANDRIL_GRAY, PEPPERS_COLOR, PEPPERS_GRAY, PIRATE, WALKBRIDGE, \ - WOMAN_BLONDE, WOMAN_DARKHAIR + WOMAN_BLONDE, WOMAN_DARKHAIR, CASTLE, OUTDOOR from src.picture_tools.codage import Codage from src.picture_tools.picture import Picture, show_patch from src.usps_tools import test_all_usps_1_vs_all, test_all_usps @@ -20,13 +20,13 @@ def main(): picture.show() # Ajout du bruit - # picture.add_rectangle(3, 3, 10, 10) - picture.add_noise(0.01) + picture.add_rectangle(250, 190, 40, 50) + # picture.add_noise(0.01) picture.show() # On inpaint l'image ! inpainting = InPainting(PATCH_SIZE) - inpainting.inpaint(picture) + picture = inpainting.inpaint(picture) picture.show() diff --git a/rapport/rapport.tex b/rapport/rapport.tex index 04edb5e..5d8ca62 100644 --- a/rapport/rapport.tex +++ b/rapport/rapport.tex @@ -53,6 +53,12 @@ \subsubsection{Vecteur de poids} \newpage \section{LASSO et Inpainting} +\subsection{Introduction} +\subsubsection{Déroulement} + + +\subsubsection{Applications} +Marche bien pour recouvrir de larges zones. Permet de retirer des objets dans un but artistique (touristes sur une photo, défaut sur un visage). diff --git a/src/common/__init__.py b/src/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/common/decorators.py b/src/common/decorators.py new file mode 100644 index 0000000..bd8d36c --- /dev/null +++ b/src/common/decorators.py @@ -0,0 +1,17 @@ +def time_this(function: callable) -> callable: + """ Print the execution time of the wrapped function. """ + def wrapper(*args, **kwargs): + from time import time + time_begin = time() + result = function(*args, **kwargs) + time_end = time() + time_total = time_end - time_begin + second_or_seconds = "second" if (time_total < 1) else "seconds" + print("Execution time for \"{}\": {} {}".format( + function.__name__, time_total, second_or_seconds)) + return result + return wrapper + + +if __name__ == "__main__": + pass diff --git a/src/common.py b/src/common/math.py similarity index 72% rename from src/common.py rename to src/common/math.py index 4fe319a..2758472 100644 --- a/src/common.py +++ b/src/common/math.py @@ -1,11 +1,6 @@ -# -*- coding: utf-8 -*- -""" Ce module contient toutes les méthodes et fonctions n'ayant pas pu être rangées dans un module précis. Il est -souvent réutilisé dans plusieurs modules différents, et doit donc être en bout de chaîne pour éviter les dépendances -cycliques. -""" +from typing import Union import numpy as np -from typing import Union Number = Union[int, float] @@ -62,20 +57,5 @@ def make_grid(data=None, xmin=-5, xmax=5, ymin=-5, ymax=5, step=20): return grid, x, y -def time_this(function: callable) -> callable: - """ Print the execution time of the wrapped function. """ - def wrapper(*args, **kwargs): - from time import time - time_begin = time() - result = function(*args, **kwargs) - time_end = time() - time_total = time_end - time_begin - second_or_seconds = "second" if (time_total < 1) else "seconds" - print("Execution time for \"{}\": {} {}".format( - function.__name__, time_total, second_or_seconds)) - return result - return wrapper - - if __name__ == "__main__": pass diff --git a/src/common/matplotlib.py b/src/common/matplotlib.py new file mode 100644 index 0000000..a52f24c --- /dev/null +++ b/src/common/matplotlib.py @@ -0,0 +1,36 @@ +from typing import List + +import numpy as np +from matplotlib import pyplot as plt + + +def show_pictures(pictures: iter, titles: List[str] = None, columns: int = 1): + """ Display a list of images in a single figure with matplotlib. + + :param pictures: List of np.arrays compatible with plt.imshow + :param titles: List of titles corresponding to each image. Must have the same length as titles. + :param columns: Number of columns in figure (number of rows is set to np.ceil(n_images / float(cols))) + + Source : + https://gist.github.com/soply/f3eec2e79c165e39c9d540e916142ae1 + """ + assert ((titles is None) or (len(pictures) == len(titles))) + + # Set default title names for each picture + if titles is None: + titles = ['Image (%d)' % i for i in range(1, len(pictures) + 1)] + + fig = plt.figure() + for index_picture, (picture, title) in enumerate(zip(pictures, titles)): + sub_figure = fig.add_subplot(columns, np.ceil(len(pictures) / float(columns)), index_picture + 1) + if picture.ndim == 2: + plt.gray() + plt.imshow(picture) + sub_figure.set_title(title) + sub_figure.axis("off") + fig.set_size_inches(np.array(fig.get_size_inches()) * len(pictures)) + plt.show() + + +if __name__ == "__main__": + pass diff --git a/src/inpainting.py b/src/inpainting.py index 8f47175..0314dd2 100644 --- a/src/inpainting.py +++ b/src/inpainting.py @@ -3,44 +3,34 @@ import numpy as np from sklearn.linear_model import Lasso -from progressbar import ProgressBar, Percentage, Counter, Timer +from progressbar import ProgressBar, Percentage, Counter, Timer, ETA from src.common import time_this from src.picture_tools.picture import Picture, VALUE_MISSING_PIXEL, get_center, flatten, unflatten, show_patch from src.linear.cost_function import * -from src.linear.gradient_descent import DescenteDeGradient -from src.linear.linear_regression import Initialisation, LinearRegression -PB_WIDGETS = ["Inpainting: processed ", Counter(), " pixels [", Percentage(), "], ", - Timer()] +PB_WIDGETS = ["Inpainting: processed ", Counter(), " pixels [", Percentage(), "], ", Timer(), ", ", ETA()] class InPainting: - def __init__(self, patch_size: int, step: int = None, max_missing_pixel: int = 0, - value_missing_pixel: int = VALUE_MISSING_PIXEL, loss: callable = l1, loss_g: callable = l1_g, - max_iter: int = 10000, eps: float = 0.01, biais: bool = True, - type_descente: DescenteDeGradient = DescenteDeGradient.BATCH, taille_batch: int = 50, - initialisation: Initialisation = Initialisation.RANDOM, alpha: float = 0.001): + def __init__(self, patch_size: int, step: int = None, value_missing_pixel: int = VALUE_MISSING_PIXEL, + alpha: float = 1.0, max_iterations: int = 1000, tolerance: float = 0.0001): self.patch_size = patch_size self.step = patch_size if step is None else step - self.max_missing_pixel = max_missing_pixel - self.loss = loss - self.loss_g = loss_g - self.max_iter = max_iter - self.eps = eps - self.biais = biais - self.type_descente = type_descente - self.taille_batch = taille_batch - self.initialisation = initialisation self.alpha = alpha self.value_missing_pixel = value_missing_pixel - self.classifier = LinearRegression(loss=loss, loss_g=loss_g, max_iter=max_iter, eps=eps, biais=biais, - type_descente=type_descente, taille_batch=taille_batch, - initialisation=initialisation, alpha=alpha) + classifiers_kwaargs = {"alpha": alpha, "copy_X": True, "fit_intercept": True, "max_iter": max_iterations, + "normalize": False, "positive": False, "precompute": False, "random_state": None, + "selection": 'cyclic', "tol": tolerance, "warm_start": False} + self._classifier_hue = Lasso(**classifiers_kwaargs) + self._classifier_saturation = Lasso(**classifiers_kwaargs) + self._classifier_value = Lasso(**classifiers_kwaargs) + + def inpaint(self, picture: Picture) -> Picture: + picture = picture.copy() - def inpaint(self, picture: Picture): # Initialisation de la barre de progression progress_bar = ProgressBar(widgets=PB_WIDGETS, maxval=len(picture.pixels[picture.pixels == self.value_missing_pixel]) // 3, @@ -48,14 +38,13 @@ def inpaint(self, picture: Picture): progress_bar.start() # On récupère le dictionnaire - dictionary = picture.get_dictionnaire(self.patch_size, self.step, self.max_missing_pixel) + dictionary = picture.get_dictionnaire(self.patch_size, self.step, max_missing_pixel=0) while self.value_missing_pixel in picture.pixels: # On récupère le patch centré sur le prochain pixel à traiter next_pixel = self._get_next_pixel(picture.pixels) - # On reconstruit le pixel choisit - show_patch(picture.get_patch(*next_pixel, self.patch_size)) + # On reconstruit le pixel choisi next_pixel_value = self._get_next_pixel_value(picture, dictionary, *next_pixel) picture.pixels[next_pixel] = next_pixel_value @@ -63,8 +52,9 @@ def inpaint(self, picture: Picture): progress_bar.update(progress_bar.value + 1) progress_bar.finish() + return picture - def _get_next_pixel(self, pixels: np.ndarray) -> Tuple[int, int]: + def _get_next_pixel(self, pixels: np.ndarray, strategy) -> Tuple[int, int]: """ Return the next pixel to be painted. This pixel is found by assigning a priority value to all remaining pixels in the picture, then by returning the one with the maximal priority. @@ -99,18 +89,15 @@ def _get_next_pixel_value(self, picture: Picture, dictionary, next_pixel_x, next datay_value.append(patch[x, y, 2]) # Apprentissage - classifier_hue = Lasso() - classifier_saturation = Lasso() - classifier_value = Lasso() - classifier_hue.fit(datax_hue, datay_hue) - classifier_saturation.fit(datax_saturation, datay_saturation) - classifier_value.fit(datax_value, datay_value) + self._classifier_hue.fit(datax_hue, datay_hue) + self._classifier_saturation.fit(datax_saturation, datay_saturation) + self._classifier_value.fit(datax_value, datay_value) # Prédiction x, y = self.patch_size // 2, self.patch_size // 2 - hue = classifier_hue.predict(dictionary[:, x, y, 0].reshape(1, -1)) - saturation = classifier_saturation.predict(dictionary[:, x, y, 1].reshape(1, -1)) - value = classifier_value.predict(dictionary[:, x, y, 2].reshape(1, -1)) + hue = self._classifier_hue.predict(dictionary[:, x, y, 0].reshape(1, -1)) + saturation = self._classifier_saturation.predict(dictionary[:, x, y, 1].reshape(1, -1)) + value = self._classifier_value.predict(dictionary[:, x, y, 2].reshape(1, -1)) return np.hstack((hue, saturation, value)) diff --git a/src/picture_tools/examples.py b/src/picture_tools/examples.py index 40b0d6c..fa745cf 100644 --- a/src/picture_tools/examples.py +++ b/src/picture_tools/examples.py @@ -6,6 +6,7 @@ _PATH_DIR_PICTURES = join(dirname(__file__), "../../res/pictures") +# Famous pictures CAMERAMAN = join(_PATH_DIR_PICTURES, "cameraman.tif") HOUSE = join(_PATH_DIR_PICTURES, "house.tif") JETPLANE = join(_PATH_DIR_PICTURES, "jetplane.tif") @@ -24,6 +25,10 @@ WOMAN_BLONDE = join(_PATH_DIR_PICTURES, "woman_blonde.tif") WOMAN_DARKHAIR = join(_PATH_DIR_PICTURES, "woman_darkhair.tif") +# Personal pictures +CASTLE = join(_PATH_DIR_PICTURES, "castle.jpg") +OUTDOOR = join(_PATH_DIR_PICTURES, "outdoor.jpg") + if __name__ == "__main__": pass diff --git a/src/picture_tools/picture.py b/src/picture_tools/picture.py index 6064c77..216d661 100644 --- a/src/picture_tools/picture.py +++ b/src/picture_tools/picture.py @@ -13,10 +13,12 @@ from numpy import uint8 from src.picture_tools.codage import Codage, change_codage -from src.common import normalize, time_this +from src.common.math import normalize +from src.common.decorators import time_this + VALUE_MISSING_PIXEL = np.ones((3,)) * -100 -VALUE_SHOWING_MISSING_PIXEL = np.random.uniform(low=-1, high=1) +VALUE_SHOWING_MISSING_PIXEL = np.array([-1]) # np.random.uniform(low=-1, high=1) class Picture: @@ -42,12 +44,8 @@ def show(self, show: bool = True) -> None: """ Plot l'image sur matplotlib et l'affiche sur demande. :param: show, waut `True` si on affiche l'image après l'avoir plottée. """ - # Remove missing values - picture = np.copy(self.pixels) - picture[picture == VALUE_MISSING_PIXEL] = VALUE_SHOWING_MISSING_PIXEL - - picture = change_codage(picture, self.codage, Codage.RGB) - picture = normalize(picture, 0, 255, -1, 1).astype(uint8) + picture = self._get_showable_picture() + plt.axis("off") plt.imshow(picture) if show: plt.show() @@ -63,14 +61,21 @@ def save(self, picture_path: str = None) -> None: # Remove missing values picture = np.copy(self.pixels) - picture[picture == VALUE_MISSING_PIXEL] = np.random.uniform( - low=-1, high=1) + picture[picture == VALUE_MISSING_PIXEL] = VALUE_SHOWING_MISSING_PIXEL picture = change_codage(self.pixels, self.codage, Codage.RGB) picture = normalize(picture, 0, 255, -1, 1).astype(uint8) plt.imshow(picture) plt.savefig(picture_path) + def copy(self): + new_picture = Picture.__new__(Picture) + new_picture.picture_path = self.picture_path + new_picture.codage = self.codage + new_picture.hauteur, new_picture.largeur = self.hauteur, self.largeur + new_picture.pixels = np.copy(self.pixels) + return new_picture + def add_noise(self, threshold: float = 0.05): """ Ajoute aléatoirement du bruit dans l'image. :param: threshold, seuil en dessous duquel on bruite le pixel. @@ -180,6 +185,23 @@ def out_of_bounds_patch(self, x: int, y: int, size: int) -> bool: (y - (size // 2) <= 0) or \ (y + (size // 2) + 1 < self.pixels.shape[1]) + def _get_showable_picture(self) -> np.ndarray: + """ Return the picture in a showable format (as in a format which can be plotted by invocating `plt.imshow`on + it. + """ + pixels = np.copy(self.pixels) + + # Fill the missing pixels with an interpretable value + pixels[pixels == VALUE_MISSING_PIXEL] = VALUE_SHOWING_MISSING_PIXEL + + # Change the format of the picture to the RGB format (for better visibility) + pixels = change_codage(pixels, self.codage, Codage.RGB) + + # Normalise the values of the pixel from [-1, 1] to [0, 255] + pixels = normalize(pixels, 0, 255, -1, 1).astype(uint8) + + return pixels + def get_center(pixels: np.ndarray) -> np.ndarray: """ Return the pixel at the center of the pixels. """