Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Détecte les segments dans une image.
5"""
6import math
7import copy
8import time
9import numpy
10from PIL import Image, ImageDraw
11from .queue_binom import tabule_queue_binom
12from .geometrie import Point
13from .detection_segment_segangle import SegmentBord
14from .detection_nfa import LigneGradient, InformationPoint
17def convert_array2PIL(img, mode=None):
18 """
19 Convertit une image donnée sous la forme d'un array
20 au format :epkg:`numpy:array`.
22 @param img :epkg:`numpy:array`
23 @param mode voir `modes <https://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#modes>`_,
24 si None, essaye de deviner.
25 @return *PIL*
27 Le mode ``'binary'`` convertit une image issue
28 de la fonction @see fn random_noise_image.
29 """
30 if mode == 'binary':
31 fimg = img.astype(numpy.float32)
32 img255 = (- fimg + 1) * 255
33 img = img255.astype(numpy.uint8)
34 mode = None
35 return _load_image(img, 'PIL', mode=mode)
38def convert_PIL2array(img):
39 """
40 Convertit une image donnée sous la forme d'une image :epkg:`Pillow`
41 au format :epkg:`numpy:array`.
43 @param img :epkg:`Pillow`
44 @return :epkg:`numpy:array`
45 """
46 return _load_image(img, 'array')
49def _load_image(img, format='PIL', mode=None):
50 """
51 Charge une image en différents formats.
53 @param img image (*array*, *PIL*, filename)
54 @param format *array* ou *PIL*
55 @param mode voir `modes <https://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#modes>`_,
56 si None, essaye de deviner.
57 @return *PIL*
58 """
59 if isinstance(img, str):
60 img = Image.open(img)
61 return _load_image(img, format)
62 if isinstance(img, Image.Image):
63 if format == 'PIL':
64 return img
65 if format == 'array':
66 d1, d0 = img.size[1], img.size[0]
67 img = numpy.array(img.getdata(), dtype=numpy.uint8)
68 if len(img.shape) == 1:
69 gray = img.shape[0] - d1 * d0
70 elif len(img.shape) == 2:
71 gray = img.shape[0] * img.shape[1] - d1 * d0
72 elif(img.shape) == 3:
73 gray = img.shape[0] * img.shape[1] * img.shape[2] - d1 * d0
74 else:
75 raise ValueError( # pragma: no cover
76 "Unexpected shape {0}".format(img.shape))
77 if gray == 0:
78 img = img.reshape((d1, d0))
79 else:
80 img = img.reshape((d1, d0, 3))
81 return img
82 raise ValueError( # pragma: no cover
83 "Unexpected value for fomat: '{0}'".format(format))
84 if isinstance(img, numpy.ndarray):
85 if format == 'array':
86 return img
87 if format == 'PIL':
88 return Image.fromarray(img, mode=mode)
89 raise ValueError( # pragma: no cover
90 "Unexpected value for fomat: '{0}'".format(format))
91 raise TypeError( # pragma: no cover
92 "numpy array expected not {0}".format(type(img)))
95def compute_gradient(img, color=None):
96 """
97 Retourne le gradient d'une image sous forme d'une matrice
98 de Point, consideres ici comme des vecteurs.
99 """
100 return _calcule_gradient(img, color=color)
103def _calcule_gradient(img, color=None):
104 """
105 Retourne le gradient d'une image sous forme d'une matrice
106 de Point, consideres ici comme des vecteurs.
108 @param img *fichier*, *array*, *PIL* (image en niveau de gris)
109 @param method ``'fast'`` or not
110 @param color calcule le gradient pour cette couleur, None
111 si l'image est en niveau de gris
112 @return array of *shape (y, x, 2)*, first dimension is *dx*,
113 second one is *dy*
114 """
115 img = _load_image(img, 'array')
116 img = img.astype(numpy.float32)
117 if color is not None:
118 img = img[:, :, color]
120 dx1 = img[:, 1:-1] - img[:, :-2]
121 dx2 = img[:, 2:] - img[:, 1:-1]
122 dx = (dx1 + dx2) / 2
124 dy1 = img[1:-1, :] - img[:-2, :]
125 dy2 = img[2:, :] - img[1:-1, :]
126 dy = (dy1 + dy2) / 2
127 res = numpy.zeros(img.shape + (2,))
128 res[:, 1:-1, 0] = dx
129 res[1:-1, :, 1] = dy
130 return res
133def plot_gradient(image, gradient, more=None, direction=-1):
134 """
135 Construit une image a partir de la matrice de gradient
136 afin de pouvoir l'afficher grace au module pygame,
137 cette fonction place directement le resultat dans image,
138 si direction > 0, cette fonction affiche egalement le gradient sur
139 l'image tous les 10 pixels si direction vaut 10.
140 """
141 image_ = _load_image(image, 'PIL')
142 image = ImageDraw.Draw(image_)
143 X, Y = image_.size
144 if direction != -1:
145 for x in range(0, X - 1):
146 for y in range(0, Y - 1):
147 n = gradient[y, x]
148 if more is None:
149 v = int((n[0]**2 + n[1] ** 2)**0.5 + 0.5)
150 elif more == "x":
151 v = int(n[0] / 2 + 127 + 0.5)
152 else:
153 v = int(n[1] / 2 + 127 + 0.5)
154 image.line([(x, y), (x, y)], fill=(v, v, v))
155 if direction in (0, -1):
156 pass
157 elif direction > 0:
158 # on dessine des petits gradients dans l'image
159 for x in range(0, X, direction):
160 for y in range(0, Y, direction):
161 n = gradient[y, x]
162 t = (n[0]**2 + n[1] ** 2)**0.5
163 if t == 0:
164 continue
165 m = copy.copy(n)
166 m /= t
167 if t > direction:
168 t = direction
169 if t < 2:
170 t = 2
171 m *= t
172 image.line([(x, y), (x + int(m[0]), y + int(m[1]))],
173 fill=(255, 255, 0))
174 elif direction == -2:
175 # derniere solution, la couleur represente l'orientation
176 # en chaque point de l'image
177 for x in range(0, X):
178 for y in range(0, Y):
179 n = gradient[y, x]
180 i = int(-n[0] * 10 + 128)
181 j = int(n[1] * 10 + 128)
182 i, j = min(i, 255), min(j, 255)
183 i, j = max(i, 0), max(j, 0)
184 image.line([(x, y), (x, y)], fill=(0, j, i))
185 else:
186 raise ValueError( # pragma: no cover
187 "Unexpected value for direction={0}".format(direction))
189 return image_
192def plot_segments(image, segments, outfile=None, color=(255, 0, 0)):
193 """
194 Dessine les segments produits par la fonction
195 @see fn detect_segments
197 @param image image (*fichier*, *array*, *PIL*)
198 @param segments résultats de la fonction @see fn detect_segments
199 @param outfile fichier de sortie
200 @param color couleur
201 @return nom de fichier ou image
202 """
203 image = _load_image(image, 'PIL')
204 draw = ImageDraw.Draw(image)
205 for seg in segments:
206 draw.line([(seg.a.x, seg.a.y), (seg.b.x, seg.b.y)], fill=color)
207 if outfile is not None:
208 image.save(outfile)
209 return outfile
210 return image
213def detect_segments(image, proba_bin=1.0 / 16,
214 cos_angle=math.cos(1.0 / 16 / 2 * (math.pi * 2)),
215 seuil_nfa=1e-5, seuil_norme=2, angle=math.pi / 24.0,
216 stop=-1, verbose=False):
217 """
218 Détecte les segments dans une image.
220 @param image image (*fichier*, *array*, *PIL*)
221 @param proba_bin est en fait un secteur angulaire (360 / 16)
222 qui determine la proximite de deux directions
223 @param cos_angle est le cosinus de l'angle correspondant à ce secteur angulaire
224 @param seuil_nfa au delà de ce seuil, on considere qu'un segment
225 génère trop de fausses alertes pour être sélectionné
226 @param seuil_norme norme en deça de laquelle un gradient est trop
227 petit pour etre significatif (c'est du bruit)
228 @param angle lorsqu'on balaye l'image pour détecter les segments,
229 on tourne en rond selon les angles 0, angle, 2*angle,
230 3*angle, ...
231 @param stop arrête après avoir collecté tant de segments
232 @param verbose affiche l'avancement
233 @return les segments
234 """
235 gray_image = _load_image(image, 'PIL').convert('L')
236 grad = _calcule_gradient(gray_image)
238 # on calcule les tables de la binomiale pour eviter d'avoir a le fait a
239 # chaque fois qu'on en a besoin
240 yy, xx = grad.shape[:2]
241 nbbin = int(math.ceil(math.sqrt(xx * xx + yy * yy)))
242 binomiale = tabule_queue_binom(nbbin, proba_bin)
244 # nb_seg est le nombre total de segment de l'image
245 # il y a xx * yy pixels possibles dont (xx*yy)^2 couples de pixels (donc de segments)
246 nb_seg = xx * xx * yy * yy
248 # on cree une instance de la classe permettant de parcourir
249 # tous les segments de l'image reliant deux points du contour
250 seg = SegmentBord(Point(xx, yy))
252 # initialisation avant de parcourir l'image
253 segment = [] # resultat, ensemble des segments significatifs
254 ti = time.perf_counter() # memorise l'heure de depart
255 # pour savoir combien de segments on a deja visite (seg)
256 n = 0
257 cont = True # condition d'arret de la boucle
259 # on cree une classe permettant de recevoir les informations relatives
260 # a l'image et au gradient pour un segment reliant deux points
261 # du contour de l'image
262 points = [InformationPoint(Point(0, 0), False, 0)
263 for i in range(0, xx + yy)]
264 ligne = LigneGradient(points, seuil_norme=seuil_norme, seuil_nfa=seuil_nfa)
266 # premier segment
267 seg.premier()
269 # autres variables a decouvrir en cours de route
270 not_aligned = 0
272 # tant qu'on a pas fini
273 while cont:
275 # calcule les informations relative a un segment de l'image reliant deux bords
276 # position des pixels, norme du gradient, alignement avec le segment
277 seg.decoupe_gradient(grad, cos_angle, ligne, seuil_norme)
279 if len(ligne) > 3 and ligne.has_aligned_point():
280 # si le segment contient plus de trois pixels
281 # alors on peut se demander s'il inclut des sous-segments significatifs
282 res = ligne.segments_significatifs(binomiale, nb_seg)
284 # on ajoute les resultats à la liste
285 segment.extend(res)
286 if len(segment) >= stop > 0:
287 break
288 else:
289 not_aligned += 1
291 # on passe au segment suivant
292 cont = seg.next() # pylint: disable=E1102
293 n += 1
295 # pour verifier que cela avance
296 if verbose and n % 1000 == 0:
297 print( # pragma: no cover
298 "n = ", n, " ... ", len(segment), " temps ",
299 "%2.2f" % (time.perf_counter() - ti), " sec",
300 "nalign", not_aligned)
302 return segment