Coverage for src/mlstatpy/image/detection_segment/detection_segment.py: 94%

139 statements  

« prev     ^ index     » next       coverage.py v7.1.0, created at 2023-02-27 05:59 +0100

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 

15 

16 

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`. 

21 

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* 

26 

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) 

36 

37 

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`. 

42 

43 @param img :epkg:`Pillow` 

44 @return :epkg:`numpy:array` 

45 """ 

46 return _load_image(img, 'array') 

47 

48 

49def _load_image(img, format='PIL', mode=None): 

50 """ 

51 Charge une image en différents formats. 

52 

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 len(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 f"Unexpected shape {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 f"Unexpected value for fomat: '{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 f"Unexpected value for fomat: '{format}'") 

91 raise TypeError( # pragma: no cover 

92 f"numpy array expected not {type(img)}") 

93 

94 

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) 

101 

102 

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. 

107 

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] 

119 

120 dx1 = img[:, 1:-1] - img[:, :-2] 

121 dx2 = img[:, 2:] - img[:, 1:-1] 

122 dx = (dx1 + dx2) / 2 

123 

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 

131 

132 

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 f"Unexpected value for direction={direction}") 

188 

189 return image_ 

190 

191 

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 

196 

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 

211 

212 

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. 

219 

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) 

237 

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) 

243 

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 

247 

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)) 

251 

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 

258 

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) 

265 

266 # premier segment 

267 seg.premier() 

268 

269 # autres variables a decouvrir en cours de route 

270 not_aligned = 0 

271 

272 # tant qu'on a pas fini 

273 while cont: 

274 

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) 

278 

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) 

283 

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 

290 

291 # on passe au segment suivant 

292 cont = seg.next() # pylint: disable=E1102 

293 n += 1 

294 

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 f"{time.perf_counter() - ti:2.2f}", " sec", 

300 "nalign", not_aligned) 

301 

302 return segment