1
0

main.py 18 KB

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