1
0

main.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. ### Modules ###
  2. from PIL import Image, ImageFilter
  3. from numpy import asarray, ndarray, array, uint8, delete
  4. from random import choices, randint, random
  5. from copy import deepcopy
  6. import drawsvg as dw
  7. ### Fonction pour importer une image ###
  8. def import_image(chemin: str, resize: tuple = (0, 0), lap: bool = False) -> ndarray :
  9. """Charge l'image donnée, la passe en nuances de gris, et la convertit en tableau (de numpy)"""
  10. ## Chargement de l'image ##
  11. # On essaie d'ouvrir l'image avec la librairie PIL
  12. try:
  13. image = Image.open(chemin)
  14. # Gestion d'erreur si le fichier n'est pas accessible ou n'existe pas
  15. except FileNotFoundError:
  16. print("Le chemin d'accès est invalide.")
  17. return asarray([[]])
  18. # Gestion d'erreur dans les autres cas
  19. except:
  20. print("Une erreur s'est produite")
  21. return asarray([[]])
  22. ## Conversion de l'image en nuances de gris ##
  23. # On utilise la méthode .convert() de PIL
  24. image = image.convert("L")
  25. if resize != (0, 0):
  26. image = image.resize(resize)
  27. if lap:
  28. image = image.filter(ImageFilter.FIND_EDGES)
  29. ## Conversion en tableau ##
  30. # On utilise la fonction asarrray() de numpy
  31. image_array = asarray(image)
  32. return image_array
  33. ### Fonctions pour générer des matrice-images vierges ###
  34. def tab_image_blanche(longueur: int, hauteur: int) -> ndarray:
  35. """Génère un tableau contenant des pixels blancs avec les dimensions données"""
  36. # On crée autant de lignes que de pixels de hauteur, et autant de pixels que de hauteur
  37. tab = [ [255] * longueur] * hauteur
  38. return array(tab)
  39. def tab_image_noire(longueur: int, hauteur: int) -> ndarray:
  40. """Génère un tableau contenant des pixels noirs avec les dimensions données"""
  41. # On crée autant de lignes que de pixels de hauteur, et autant de pixels que de hauteur
  42. tab = [ [0] * longueur] * hauteur
  43. return array(tab)
  44. # On l'utilise pas finalement mais on la laisse pour la postérité
  45. def tab_image_grise(longueur: int, hauteur: int) -> ndarray:
  46. """Génère un tableau contenant des pixels gris avec les dimensions données"""
  47. # On crée autant de lignes que de pixels de hauteur, et autant de pixels que de hauteur
  48. tab = [ [127] * longueur] * hauteur
  49. return array(tab)
  50. ### Fonctions pour récupérer les dimensions d'une matrice-image ###
  51. def get_longueur_img(tab_img: ndarray) -> int:
  52. """Renvoie la longueur du tableau à deux dimensions (représentant une image) donné"""
  53. return len(tab_img[0])
  54. def get_hauteur_img(tab_img: ndarray) -> int:
  55. """Renvoie la hauteur du tableau à deux dimensions (représentant une image) donné"""
  56. return len(tab_img)
  57. ### Fonctions pour modifier la valeur des pixels ###
  58. def assombrir_pixel(tab_img: ndarray, ligne: int, colonne: int) -> ndarray:
  59. """Réduit de 51 (parce que c'est 255/5) les valeurs des couleurs du pixel à la position donnée."""
  60. if tab_img[ligne][colonne] >= 51:
  61. tab_img[ligne][colonne] = tab_img[ligne][colonne] - 51
  62. return tab_img
  63. def eclaircir_pixel(tab_img: ndarray, ligne: int, colonne: int) -> ndarray:
  64. """Augmente de 51 (parce que c'est 255/5) les valeurs des couleurs du pixel à la position donnée."""
  65. if tab_img[ligne][colonne] <= 214:
  66. tab_img[ligne][colonne] += 51
  67. return tab_img
  68. ### Fonctions pour générer des clous ###
  69. def generer_clous(tab_img: ndarray) -> list:
  70. """Génère un tableau qui contient des tuples qui représentent des clous : deux coordonnées et un numéro de côté -> 0 pour le haut, 1 pour la droite, 2 pour le bas, 3 pour la gauche"""
  71. liste_res = []
  72. h = get_hauteur_img(tab_img)
  73. l = get_longueur_img(tab_img)
  74. for i in range(0, l, 10):
  75. liste_res = liste_res + [(i, 0, 0)]
  76. for i in range(10, h, 10):
  77. liste_res = liste_res + [(l - 1, i, 1)]
  78. for i in range(l - 11, -1, - 10):
  79. liste_res = liste_res + [(i, h - 1, 2)]
  80. for i in range(h - 11, 0, -10):
  81. liste_res = liste_res + [(0, i, 3)]
  82. return liste_res
  83. def generer_clous_plus_clous_aleatoires(tab_img: ndarray, nombre_clous: int):
  84. """Génère un tableau de clous classiques (cf generer_clous) + des clous aleatoires au milieu de l'image"""
  85. h = get_hauteur_img(tab_img)
  86. l = get_longueur_img(tab_img)
  87. liste_res = generer_clous(tab_img)
  88. for i in range(nombre_clous):
  89. liste_res.append((randint(20, l - 20), randint(20, h - 20), i + 4))
  90. return liste_res
  91. ### Fonctions pour tracer des droites ###
  92. def visible(clou1: tuple, clou2: tuple) -> bool:
  93. """Renvoie True si les clous se voient, False sinon"""
  94. return clou1[2] != clou2[2]
  95. def tracer_droite(clou1: tuple, clou2: tuple, image: ndarray) -> ndarray:
  96. """Trace une droite noire d'un clou à un autre."""
  97. if visible(clou1, clou2):
  98. liste_points = liste_points_traverses(clou1[0], clou1[1], clou2[0], clou2[1])
  99. for i in range(0, len(liste_points)):
  100. ligne = liste_points[i][1]
  101. col = liste_points[i][0]
  102. image = assombrir_pixel(image, ligne, col)
  103. return image
  104. def tracer_droite_blanche(clou1: tuple, clou2: tuple, image: ndarray) -> ndarray:
  105. """Trace une droite blanche d'un clou à un autre."""
  106. if visible(clou1, clou2):
  107. liste_points = liste_points_traverses(clou1[0], clou1[1], clou2[0], clou2[1])
  108. for i in range(0, len(liste_points)):
  109. ligne = liste_points[i][1]
  110. col = liste_points[i][0]
  111. image = eclaircir_pixel(image, ligne, col)
  112. return image
  113. def liste_points_traverses(x1: int, y1: int, x2: int, y2: int) -> list:
  114. """Renvoie les points que le segment [(x1, y1), (x2, y2)] traverse"""
  115. liste_res = []
  116. a, b = calcul_droite(x1, y1, x2, y2)
  117. # Si l'angle entre l'axe des abscisses et la droite est compris entre -45° et 45°
  118. if -1 <= a <= 1:
  119. if x1 > x2:
  120. # On cherche y pour chaque x, donc on commence du point dont le x est le plus proche de 0
  121. x1, y1, x2, y2 = x2, y2, x1, y1
  122. x = x1 + 1
  123. y = y1
  124. liste_res.append((x1, y))
  125. while x <= x2:
  126. y = round(a * x + b)
  127. liste_res.append((x, y))
  128. x += 1
  129. else:
  130. if y1 > y2:
  131. # On cherche x pour chaque y, donc on commence du point dont le y est le plus proche de 0
  132. x1, y1, x2, y2 = x2, y2, x1, y1
  133. if a != 999999999 and b != 999999999:
  134. x = x1
  135. y = y1 + 1
  136. liste_res.append((x, y1))
  137. while y <= y2:
  138. x = round((y - b) / a)
  139. liste_res.append((x, y))
  140. y += 1
  141. # Gestion des droites verticales
  142. else:
  143. x = x1
  144. y = y1 + 1
  145. liste_res.append((x, y1))
  146. for i in range(y, y2 + 1):
  147. liste_res.append((x, i))
  148. return liste_res
  149. def calcul_droite(x1: int, y1: int, x2: int, y2: int) -> tuple:
  150. """Renvoie le coefficient directeur et l'ordonnée à l'origine de la droite passant par (x1, y1) et (x2, y2)"""
  151. if x1 != x2:
  152. a = (y2 - y1) / (x2 - x1)
  153. b = y1 - x1 * a
  154. # Gestion des droites verticales
  155. else:
  156. a = 999999999
  157. b = 999999999
  158. return (a, b)
  159. def liste_clous_al(liste_clous : list, taille_l : int) -> list:
  160. """On créer une liste de clou choisis aléatoirement"""
  161. lst_res = choices(liste_clous, k=taille_l)
  162. return lst_res
  163. ### Fonctions pour les calculs de différence d'erreur entre deux images ###
  164. def reduction_erreur_droite(clou_dep: tuple, clou_arr: tuple, img1: ndarray, img2: ndarray) -> float:
  165. """Renvoie la différence entre l'erreur de l'image cible et de l'image actuelle, et l'erreur de l'image cible et de l'image actuelel si on traçait la droite noire donnée"""
  166. if not visible(clou_dep, clou_arr):
  167. return 999999999
  168. lst_pts = liste_points_traverses(clou_dep[0], clou_dep[1], clou_arr[0], clou_arr[1])
  169. err_act = 0
  170. nb_pts = len(lst_pts)
  171. for i in range(0, nb_pts):
  172. err_act += erreur_point(lst_pts[i], img1, img2)
  173. new_err = 0
  174. for i in range(0, nb_pts):
  175. new_err += erreur_point_assombri(lst_pts[i], img1, img2)
  176. return (new_err - err_act) / nb_pts
  177. def reduction_erreur_droite_blanche(clou_dep: tuple, clou_arr: tuple, img1: ndarray, img2: ndarray) -> float:
  178. """Renvoie la différence entre l'erreur de l'image cible et de l'image actuelle, et l'erreur de l'image cible et de l'image actuelel si on traçait la droite blanche donnée"""
  179. if not visible(clou_dep, clou_arr):
  180. return 999999999
  181. lst_pts = liste_points_traverses(clou_dep[0], clou_dep[1], clou_arr[0], clou_arr[1])
  182. err_act = 0
  183. nb_pts = len(lst_pts)
  184. for i in range(0, nb_pts):
  185. err_act += erreur_point(lst_pts[i], img1, img2)
  186. new_err = 0
  187. for i in range(0, nb_pts):
  188. new_err += erreur_point_eclairci(lst_pts[i], img1, img2)
  189. return (new_err - err_act) / nb_pts
  190. def erreur_point(p: tuple, img1: ndarray, img2: ndarray):
  191. """Renvoie la différence entre la valeur du pixel sur l'image 1 et la valeur du pixel sur l'image 2"""
  192. return abs(img1[p[1]][p[0]] - img2[p[1]][p[0]])
  193. def erreur_point_assombri(p: tuple, img1: ndarray, img2: ndarray):
  194. """Renvoie la différence entre la valeur du pixel sur l'image 1 et la valeur du pixel sur l'image 2 lorsqu'on assombrit ce pixel"""
  195. if img2[p[1]][p[0]] >= 51:
  196. return abs(img1[p[1]][p[0]] - img2[p[1]][p[0]] + 51)
  197. else:
  198. return erreur_point(p, img1, img2)
  199. def erreur_point_eclairci(p: tuple, img1: ndarray, img2: ndarray):
  200. """Renvoie la différence entre la valeur du pixel sur l'image 1 et la valeur du pixel sur l'image 2 lorsqu'on éclaircit ce pixel"""
  201. if img2[p[1]][p[0]] <= 51:
  202. return abs(img1[p[1]][p[0]] - img2[p[1]][p[0]] - 51)
  203. else:
  204. return erreur_point(p, img1, img2)
  205. def trouve_meilleure_droite(img1: ndarray, img2: ndarray, clou_dep: tuple, lst_clous: list) -> tuple:
  206. """Renvoie le clou dont la droite le traversant lui et le clou de départ réduit le plus l'erreur entre les deux images"""
  207. liste_c = liste_clous_al(lst_clous, len(lst_clous) // 2)
  208. liste_red_err = []
  209. for i in range(0, len(liste_c)):
  210. liste_red_err.append(reduction_erreur_droite(clou_dep, liste_c[i], img1, img2))
  211. red_err = min(liste_red_err)
  212. meilleur_clou = liste_c[liste_red_err.index(red_err)]
  213. return (meilleur_clou, red_err)
  214. def trouve_meilleure_droite_blanche(img1: ndarray, img2: ndarray, clou_dep: tuple, lst_clous: list) -> tuple:
  215. """Renvoie le clou dont la droite blanche le traversant lui et le clou de départ réduit le plus l'erreur entre les deux images"""
  216. liste_c = liste_clous_al(lst_clous, len(lst_clous) // 2)
  217. liste_red_err = []
  218. for i in range(0, len(liste_c)):
  219. liste_red_err.append(reduction_erreur_droite_blanche(clou_dep, liste_c[i], img1, img2))
  220. red_err = min(liste_red_err)
  221. meilleur_clou = liste_c[liste_red_err.index(red_err)]
  222. return (meilleur_clou, red_err)
  223. ### Fonctions pour le laplacien ###
  224. def generer_clous_lap(tab_img: ndarray, nombre_clous: int) -> list:
  225. """Génère un tableau qui contient des tuples qui représentent des clous : deux coordonnées et un numéro de côté"""
  226. liste_res = []
  227. # On crée la matrice qui contient pour chaque pixel, la somme de sa valeur et de celle de tous les pixels d'avant
  228. tab_somme = somme_cumulative_matrice(tab_img)
  229. # Le dernier élément est la somme de la valeur de tous les pixels
  230. somme_valeurs_px = tab_somme[-1][-1]
  231. for i in range(nombre_clous):
  232. nb_rd = random() * somme_valeurs_px
  233. pixel = trouve_pixel(tab_somme, nb_rd)
  234. clou = (pixel[1], pixel[0], i)
  235. liste_res.append(clou)
  236. return liste_res
  237. def somme_cumulative_matrice(tab: ndarray) -> list:
  238. """Renvoie la somme cumulative de la matrice"""
  239. liste_res = []
  240. for i in range(len(tab)):
  241. liste_res.append([])
  242. for j in range(len(tab[i])):
  243. if j == 0:
  244. if i == 0:
  245. liste_res[i].append(int(tab[i][j]) ** 2)
  246. else:
  247. liste_res[i].append(int(tab[i][j]) ** 2 + liste_res[i - 1][-1])
  248. else:
  249. liste_res[i].append(int(tab[i][j]) ** 2 + liste_res[i][j - 1])
  250. return liste_res
  251. def trouve_pixel(tab: list, nb: float) -> tuple:
  252. """Renvoie les coordonnées du premier nombre dans la matrice qui est plus grand que le nombre donné"""
  253. for i in range(len(tab)):
  254. for j in range(len(tab[i])):
  255. if tab[i][j] >= nb:
  256. return (i, j)
  257. ### Fonction principale ###
  258. def filage_image(chemin_img_source: str, nom_img_sortie: str, format_sortie: str = "svg", resize: tuple = (0, 0), placement_clous: str = "laplacien", nombre_clous: int = 100, couleur_traits: str = "noir", affiche_infos: bool = True):
  259. """
  260. Retrace l'image donnée en source comme si elle était filée sur une planche avec des clous.
  261. Entrées : chemin_img_source, une chaîne de caractère indiquant le chemin absolu ou relatif vers l'image originale. L'extension est à préciser !
  262. nom_img_sortie, une chaine de caractère qui correspond au nom sous lequel sera sauvegardée l'image résultat. L'extension peut être précisée grâce au paramètre format_sortie.
  263. format_sortie, une chaine de caractère qui correspond au format de l'image de sortie. Les valeurs possibles sont 'svg' (par défaut), 'png', 'jpg', 'png+svg' et 'jpg+svg'.
  264. resize, un tuple qui définit les dimenssions de l'image de sortie. Le tuple (0, 0) (la valeur par défaut) utilise les dimensions de l'image originale.
  265. placement_clous, une chaine de caractère qui définit le mode de placement des clous sur l'image. Les valeurs possibles sont 'laplacien' (par défaut), 'bords', 'classique', 'aleatoire' et 'random'. 'classique' est un alias de 'bords', et 'random' un alias de 'aleatoire'.
  266. nombre_clous, un entier qui correspond au nombre de clous si le mode de placement de clous sélectionné est le laplacien, aleatoire ou random. La valeur par défaut est 100.
  267. couleur_traits: une chaine de caractère qui définit la couleur des traits, et donc celle du fond, qui sera la couleur opposée. Les valeurs possibles sont 'noir' (par défaut) et 'blanc'.
  268. affiche_infos: un booléen qui active l'affichage de la réduction de l'erreur et des coordonnées des droites tracées lorsqu'il est vrai.
  269. Sortie : Cette fonction ne renvoie rien, mais elle sauvegarde des fichiers dans le répertoire './resultats'.
  270. """
  271. # Import de l'image source
  272. image = import_image(chemin_img_source, resize)
  273. # Création de l'image de travail et de l'image résultat
  274. if couleur_traits == "noir":
  275. new_img = tab_image_blanche(get_longueur_img(image), get_hauteur_img(image))
  276. if format_sortie in ("svg", "png+svg", "jpg+svg"):
  277. svg = dw.Drawing(get_longueur_img(image), get_hauteur_img(image))
  278. svg.append(dw.Rectangle(x=0, y=0, width='100%', height='100%', fill='white'))
  279. elif couleur_traits == "blanc":
  280. new_img = tab_image_noire(get_longueur_img(image), get_hauteur_img(image))
  281. if format_sortie in ("svg", "png+svg", "jpg+svg"):
  282. svg = dw.Drawing(get_longueur_img(image), get_hauteur_img(image))
  283. svg.append(dw.Rectangle(x=0, y=0, width='100%', height='100%', fill='black'))
  284. else:
  285. print("Cette couleur est invalide. Les couleurs valides sont 'noir' et 'blanc'.")
  286. return
  287. # Génération des clous
  288. if placement_clous == "laplacien":
  289. image_lap = import_image(chemin_img_source, resize, lap=True)
  290. clous = generer_clous_lap(image_lap, nombre_clous)
  291. elif placement_clous in ("bords", "classique"):
  292. clous = generer_clous(new_img)
  293. elif placement_clous in ("aleatoire", "random"):
  294. clous = generer_clous_plus_clous_aleatoires(new_img, nombre_clous)
  295. else:
  296. print("Cette façon de placer les clous n'existe pas. Les valeurs valides sont 'laplacien', 'bords', 'classique', 'aleatoire' et 'random'.")
  297. return
  298. # Initialisation des variables de la boucle
  299. clou_dep = clous[0]
  300. cpt_augmentation_err = 0
  301. cpt_droites = 0
  302. # Boucle
  303. while cpt_augmentation_err < 3:
  304. if couleur_traits == "noir":
  305. meilleur_clou, reduction_err = trouve_meilleure_droite(image, new_img, clou_dep, clous)
  306. else:
  307. meilleur_clou, reduction_err = trouve_meilleure_droite_blanche(image, new_img, clou_dep, clous)
  308. if affiche_infos:
  309. print(reduction_err, clou_dep, meilleur_clou)
  310. if reduction_err >= 0:
  311. cpt_augmentation_err += 1
  312. else:
  313. cpt_augmentation_err = 0
  314. if couleur_traits == "noir":
  315. new_img = tracer_droite(clou_dep, meilleur_clou, new_img)
  316. if format_sortie in ("svg", "png+svg", "jpg+svg"):
  317. svg.append(dw.Line(clou_dep[0], clou_dep[1], meilleur_clou[0], meilleur_clou[1], stroke='black', stroke_opacity=0.2, stroke_width=1))
  318. else:
  319. new_img = tracer_droite_blanche(clou_dep, meilleur_clou, new_img)
  320. if format_sortie in ("svg", "png+svg", "jpg+svg"):
  321. svg.append(dw.Line(clou_dep[0], clou_dep[1], meilleur_clou[0], meilleur_clou[1], stroke='white', stroke_opacity=0.2, stroke_width=1))
  322. clou_dep = meilleur_clou
  323. cpt_droites += 1
  324. if affiche_infos:
  325. print(cpt_droites, " droites tracées")
  326. if format_sortie in ("png", "jpg", "png+svg", "jpg+svg"):
  327. image_res = Image.fromarray(uint8(new_img))
  328. if format_sortie in ("png", "png+svg"):
  329. image_res.save("resultats/" + nom_img_sortie + ".png")
  330. else:
  331. image_res.save("resultats/" + nom_img_sortie + ".jpg")
  332. if format_sortie in ("svg", "png+svg", "jpg+svg"):
  333. svg.save_svg("resultats/" + nom_img_sortie + ".svg")