# -*- coding: utf-8 -*-
"""
Détecte les segments dans une image.
:githublink:`%|py|6`
"""
import math
import copy
import time
import numpy
from PIL import Image, ImageDraw
from .queue_binom import tabule_queue_binom
from .geometrie import Point
from .detection_segment_segangle import SegmentBord
from .detection_nfa import LigneGradient, InformationPoint
[docs]def convert_array2PIL(img, mode=None):
"""
Convertit une image donnée sous la forme d'un array
au format :epkg:`numpy:array`.
:param img: :epkg:`numpy:array`
:param mode: voir `modes <https://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#modes>`_,
si None, essaye de deviner.
:return: *PIL*
Le mode ``'binary'`` convertit une image issue
de la fonction :func:`random_noise_image <mlstatpy.image.detection_segment.random_image.random_noise_image>`.
:githublink:`%|py|29`
"""
if mode == 'binary':
fimg = img.astype(numpy.float32)
img255 = (- fimg + 1) * 255
img = img255.astype(numpy.uint8)
mode = None
return _load_image(img, 'PIL', mode=mode)
[docs]def convert_PIL2array(img):
"""
Convertit une image donnée sous la forme d'une image :epkg:`Pillow`
au format :epkg:`numpy:array`.
:param img: :epkg:`Pillow`
:return: :epkg:`numpy:array`
:githublink:`%|py|45`
"""
return _load_image(img, 'array')
[docs]def _load_image(img, format='PIL', mode=None):
"""
Charge une image en différents formats.
:param img: image (*array*, *PIL*, filename)
:param format: *array* ou *PIL*
:param mode: voir `modes <https://pillow.readthedocs.io/en/3.1.x/handbook/concepts.html#modes>`_,
si None, essaye de deviner.
:return: *PIL*
:githublink:`%|py|58`
"""
if isinstance(img, str):
img = Image.open(img)
return _load_image(img, format)
if isinstance(img, Image.Image):
if format == 'PIL':
return img
if format == 'array':
d1, d0 = img.size[1], img.size[0]
img = numpy.array(img.getdata(), dtype=numpy.uint8)
if len(img.shape) == 1:
gray = img.shape[0] - d1 * d0
elif len(img.shape) == 2:
gray = img.shape[0] * img.shape[1] - d1 * d0
elif(img.shape) == 3:
gray = img.shape[0] * img.shape[1] * img.shape[2] - d1 * d0
else:
raise ValueError( # pragma: no cover
"Unexpected shape {0}".format(img.shape))
if gray == 0:
img = img.reshape((d1, d0))
else:
img = img.reshape((d1, d0, 3))
return img
raise ValueError( # pragma: no cover
"Unexpected value for fomat: '{0}'".format(format))
if isinstance(img, numpy.ndarray):
if format == 'array':
return img
if format == 'PIL':
return Image.fromarray(img, mode=mode)
raise ValueError( # pragma: no cover
"Unexpected value for fomat: '{0}'".format(format))
raise TypeError( # pragma: no cover
"numpy array expected not {0}".format(type(img)))
[docs]def compute_gradient(img, color=None):
"""
Retourne le gradient d'une image sous forme d'une matrice
de Point, consideres ici comme des vecteurs.
:githublink:`%|py|99`
"""
return _calcule_gradient(img, color=color)
[docs]def _calcule_gradient(img, color=None):
"""
Retourne le gradient d'une image sous forme d'une matrice
de Point, consideres ici comme des vecteurs.
:param img: *fichier*, *array*, *PIL* (image en niveau de gris)
:param method: ``'fast'`` or not
:param color: calcule le gradient pour cette couleur, None
si l'image est en niveau de gris
:return: array of *shape (y, x, 2)*, first dimension is *dx*,
second one is *dy*
:githublink:`%|py|114`
"""
img = _load_image(img, 'array')
img = img.astype(numpy.float32)
if color is not None:
img = img[:, :, color]
dx1 = img[:, 1:-1] - img[:, :-2]
dx2 = img[:, 2:] - img[:, 1:-1]
dx = (dx1 + dx2) / 2
dy1 = img[1:-1, :] - img[:-2, :]
dy2 = img[2:, :] - img[1:-1, :]
dy = (dy1 + dy2) / 2
res = numpy.zeros(img.shape + (2,))
res[:, 1:-1, 0] = dx
res[1:-1, :, 1] = dy
return res
[docs]def plot_gradient(image, gradient, more=None, direction=-1):
"""
Construit une image a partir de la matrice de gradient
afin de pouvoir l'afficher grace au module pygame,
cette fonction place directement le resultat dans image,
si direction > 0, cette fonction affiche egalement le gradient sur
l'image tous les 10 pixels si direction vaut 10.
:githublink:`%|py|140`
"""
image_ = _load_image(image, 'PIL')
image = ImageDraw.Draw(image_)
X, Y = image_.size
if direction != -1:
for x in range(0, X - 1):
for y in range(0, Y - 1):
n = gradient[y, x]
if more is None:
v = int((n[0]**2 + n[1] ** 2)**0.5 + 0.5)
elif more == "x":
v = int(n[0] / 2 + 127 + 0.5)
else:
v = int(n[1] / 2 + 127 + 0.5)
image.line([(x, y), (x, y)], fill=(v, v, v))
if direction in (0, -1):
pass
elif direction > 0:
# on dessine des petits gradients dans l'image
for x in range(0, X, direction):
for y in range(0, Y, direction):
n = gradient[y, x]
t = (n[0]**2 + n[1] ** 2)**0.5
if t == 0:
continue
m = copy.copy(n)
m /= t
if t > direction:
t = direction
if t < 2:
t = 2
m *= t
image.line([(x, y), (x + int(m[0]), y + int(m[1]))],
fill=(255, 255, 0))
elif direction == -2:
# derniere solution, la couleur represente l'orientation
# en chaque point de l'image
for x in range(0, X):
for y in range(0, Y):
n = gradient[y, x]
i = int(-n[0] * 10 + 128)
j = int(n[1] * 10 + 128)
i, j = min(i, 255), min(j, 255)
i, j = max(i, 0), max(j, 0)
image.line([(x, y), (x, y)], fill=(0, j, i))
else:
raise ValueError( # pragma: no cover
"Unexpected value for direction={0}".format(direction))
return image_
[docs]def plot_segments(image, segments, outfile=None, color=(255, 0, 0)):
"""
Dessine les segments produits par la fonction
:func:`detect_segments <mlstatpy.image.detection_segment.detection_segment.detect_segments>`
:param image: image (*fichier*, *array*, *PIL*)
:param segments: résultats de la fonction :func:`detect_segments <mlstatpy.image.detection_segment.detection_segment.detect_segments>`
:param outfile: fichier de sortie
:param color: couleur
:return: nom de fichier ou image
:githublink:`%|py|202`
"""
image = _load_image(image, 'PIL')
draw = ImageDraw.Draw(image)
for seg in segments:
draw.line([(seg.a.x, seg.a.y), (seg.b.x, seg.b.y)], fill=color)
if outfile is not None:
image.save(outfile)
return outfile
return image
[docs]def detect_segments(image, proba_bin=1.0 / 16,
cos_angle=math.cos(1.0 / 16 / 2 * (math.pi * 2)),
seuil_nfa=1e-5, seuil_norme=2, angle=math.pi / 24.0,
stop=-1, verbose=False):
"""
Détecte les segments dans une image.
:param image: image (*fichier*, *array*, *PIL*)
:param proba_bin: est en fait un secteur angulaire (360 / 16)
qui determine la proximite de deux directions
:param cos_angle: est le cosinus de l'angle correspondant à ce secteur angulaire
:param seuil_nfa: au delà de ce seuil, on considere qu'un segment
génère trop de fausses alertes pour être sélectionné
:param seuil_norme: norme en deça de laquelle un gradient est trop
petit pour etre significatif (c'est du bruit)
:param angle: lorsqu'on balaye l'image pour détecter les segments,
on tourne en rond selon les angles 0, angle, 2*angle,
3*angle, ...
:param stop: arrête après avoir collecté tant de segments
:param verbose: affiche l'avancement
:return: les segments
:githublink:`%|py|234`
"""
gray_image = _load_image(image, 'PIL').convert('L')
grad = _calcule_gradient(gray_image)
# on calcule les tables de la binomiale pour eviter d'avoir a le fait a
# chaque fois qu'on en a besoin
yy, xx = grad.shape[:2]
nbbin = int(math.ceil(math.sqrt(xx * xx + yy * yy)))
binomiale = tabule_queue_binom(nbbin, proba_bin)
# nb_seg est le nombre total de segment de l'image
# il y a xx * yy pixels possibles dont (xx*yy)^2 couples de pixels (donc de segments)
nb_seg = xx * xx * yy * yy
# on cree une instance de la classe permettant de parcourir
# tous les segments de l'image reliant deux points du contour
seg = SegmentBord(Point(xx, yy))
# initialisation avant de parcourir l'image
segment = [] # resultat, ensemble des segments significatifs
ti = time.perf_counter() # memorise l'heure de depart
# pour savoir combien de segments on a deja visite (seg)
n = 0
cont = True # condition d'arret de la boucle
# on cree une classe permettant de recevoir les informations relatives
# a l'image et au gradient pour un segment reliant deux points
# du contour de l'image
points = [InformationPoint(Point(0, 0), False, 0)
for i in range(0, xx + yy)]
ligne = LigneGradient(points, seuil_norme=seuil_norme, seuil_nfa=seuil_nfa)
# premier segment
seg.premier()
# autres variables a decouvrir en cours de route
not_aligned = 0
# tant qu'on a pas fini
while cont:
# calcule les informations relative a un segment de l'image reliant deux bords
# position des pixels, norme du gradient, alignement avec le segment
seg.decoupe_gradient(grad, cos_angle, ligne, seuil_norme)
if len(ligne) > 3 and ligne.has_aligned_point():
# si le segment contient plus de trois pixels
# alors on peut se demander s'il inclut des sous-segments significatifs
res = ligne.segments_significatifs(binomiale, nb_seg)
# on ajoute les resultats à la liste
segment.extend(res)
if len(segment) >= stop > 0:
break
else:
not_aligned += 1
# on passe au segment suivant
cont = seg.next()
n += 1
# pour verifier que cela avance
if verbose and n % 1000 == 0:
print( # pragma: no cover
"n = ", n, " ... ", len(segment), " temps ",
"%2.2f" % (time.perf_counter() - ti), " sec",
"nalign", not_aligned)
return segment