Commit 9076bbf4 authored by robin.danz's avatar robin.danz
Browse files

Code & comment finished, readme in progress

parent a0439dc7
Loading
Loading
Loading
Loading
+59 −2
Original line number Diff line number Diff line
# TDS CannyProject
## Introduction
Ce projet entre dans le cadre du cours de Traitement du Signal de 2ème année DLM de la HE-Arc. Il nous a été demandé de réaliser un projet (au choix parmis plusieurs). Dans notre cas, nous avons décidé de réaliser une application Python capable de détecter les contours d'une image à l'aide de l'algorithme de Canny.

## Useful links
## Project Tree
Voici l'arborescence du projet. Des images prêtes à l'emploi sont disponible dans le dossier `.\res`
```
tds-cannyproject
├─ canny.py
├─ filters.py
├─ fourierTransform.py
├─ imageReader.py
├─ main.py
├─ README.md
└─ res
   ├─ dice.png
   ├─ dog.png
   ├─ lemon.png
   └─ mario.png
```

## Description
Description des differents modules du projet:
 - `main.py` : Lance l'ensemble de l'application
 - `filters.py` : Contient les fonctions appliquants les filtres basiques (RGB, CNY, GreyScale)
 - `canny.py` : Applique l'algorithme de Canny à l'image. Le seul module contenant une classe (`CannyEdgeDetector`)
 - `fourierTransform.py` : Contient une fonction pour calculer la transformée de Fourier et deux fonctions pour afficher les résultats.
 - `imageReader.py` : module utilitaire utilisé pour la lecture d'une image.

## Utilisation
### Général
Le module `main.py` lance l'ensemble de l'application. Il est possible de le lancer en ligne de commande (depuis le répértoire du projet):

```
python main.py
```

Il est également possible de lancer l'application depuis VSCode directement. Aucun autre editeur n'a été testé mais celà devrait être également compatible.


L'image lu par l'application peut être changé. Pour celà il faut se rendre à la ligne 10 du module `main.py`
```python
img = imageReader.open("res\lemon.png")
```

Pour effectuer le traitement sur une autre image il suffit de remplacer le chemin de l'image `res\lemon.png` par celle que vous souhaitez traiter.

### Modules séparés
Chaque module est exécutable indépendemment des autres. Celà permet de voir exactement ce que fait un module en particulier.
Pour les lancers, la techniques est la même que pour le module `main.py`

En ligne de commande :

```
python [module à tester].py
```


## Crédits
 - https://towardsdatascience.com/canny-edge-detection-step-by-step-in-python-computer-vision-b49c3a2d8123

canny.py

0 → 100644
+189 −0
Original line number Diff line number Diff line
import filters

import numpy as np

import matplotlib.pyplot as plt
import matplotlib.image as npimage

from scipy.ndimage.filters import convolve

class cannyEdgeDetector:
    """
    Classe s'occupant de la detection des contours en utilisant l'algorithme de Canny
    """

    def __init__(self, img):
        """
        Constructeur de la classe
        Input: une image (ndarray)
        """

        self.img = img
        self.imgFinal = []
        self.sigma = 1
        self.kernelSize = 5
        self.lowThreshold = 0.05
        self.highThreshold = 0.09
        self.weakPixel = 25
        self.strongPixel = 255

        #Passe l'image en niveau de gris
        rgb_weights = [0.299, 0.587, 0.114]
        self.img = np.dot(self.img[..., :3], rgb_weights)

    def __gaussianKernel__(self, size=5, sigma=1):
        """
        Suppression du bruit de l'image avec un flou Gaussien
        """

        size = int(size) // 2
        x, y = np.mgrid[-size:size+1, -size:size+1]
        normal = 1 / (2.0 * np.pi * sigma**2)
        g = np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal
        return g

    def __sobelFilter__(self, img):
        """
        Filtre de Sobel, ce filtre calcul la direction de la plus forte variation du clair au sombre
        """

        Kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], np.float32)
        Ky = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], np.float32)

        Ix = convolve(img, Kx)
        Iy = convolve(img, Ky)

        G = np.hypot(Ix, Iy)
        G = G/G.max()*255
        theta = np.arctan2(Iy, Ix)
        return (G, theta)

    
    def __nonMaxSuppression__(self, img, D):
        """
        Traitement des contours approximatifs et trop epais. Parcours la matrice retournée par la fonction SobelFilter et
        trouve les pixels avec la valeur maximale dans la direction du contour.
        """

        M, N = img.shape
        Z = np.zeros((M, N), dtype=np.int32)
        angle = D * 180. / np.pi
        angle[angle < 0] += 180

        for i in range(1, M-1):
            for j in range(1, N-1):
                try:
                    q = 255
                    r = 255

                # angle 0
                    if (0 <= angle[i, j] < 22.5) or (157.5 <= angle[i, j] <= 180):
                        q = img[i, j+1]
                        r = img[i, j-1]
                    # angle 45
                    elif (22.5 <= angle[i, j] < 67.5):
                        q = img[i+1, j-1]
                        r = img[i-1, j+1]
                    # angle 90
                    elif (67.5 <= angle[i, j] < 112.5):
                        q = img[i+1, j]
                        r = img[i-1, j]
                    # angle 135
                    elif (112.5 <= angle[i, j] < 157.5):
                        q = img[i-1, j-1]
                        r = img[i+1, j+1]

                    if (img[i, j] >= q) and (img[i, j] >= r):
                        Z[i, j] = img[i, j]
                    else:
                        Z[i, j] = 0

                except IndexError as e:
                    pass
        return Z

    def __threshold__(self, img):
        """
        Identifie 3 types de pixel: fort, faible ou sans importance. 
        Les pixels détecté comme "fort" vont être accentuées. Les pixels "faibles" vont être atténuées.
        """

        highThreshold = img.max() *  self.highThreshold
        lowThreshold = highThreshold * self.lowThreshold

        M, N = img.shape
        res = np.zeros((M, N), dtype=np.int32)

        weak = np.int32(25)
        strong = np.int32(255)

        strong_i, strong_j = np.where(img >= highThreshold)
        zeros_i, zeros_j = np.where(img < lowThreshold)

        weak_i, weak_j = np.where((img <= highThreshold) & (img >= lowThreshold))

        res[strong_i, strong_j] = strong
        res[weak_i, weak_j] = weak

        return res

    def __hysteresis__(self, img):
        """
        Se base sur les resultats de "threshold()". Cette fonction consiste a transfomer les pixels faibles en pixels fort
        si l'un des pixels adjacents a celui traite est fort.
        """

        M, N = img.shape  
        for i in range(1, M-1):
            for j in range(1, N-1):
                if (img[i,j] == self.weakPixel):
                    try:
                        if ((img[i+1, j-1] == self.strongPixel) or (img[i+1, j] == self.strongPixel) or (img[i+1, j+1] == self.strongPixel)
                            or (img[i, j-1] == self.strongPixel) or (img[i, j+1] == self.strongPixel)
                            or (img[i-1, j-1] == self.strongPixel) or (img[i-1, j] == self.strongPixel) or (img[i-1, j+1] == self.strongPixel)):
                            img[i, j] = self.strongPixel
                        else:
                            img[i, j] = 0
                    except IndexError as e:
                        pass
        return img

    def detectEdges(self):
        """
        Lance les fonctions de la classe dans l'ordre prevu pour l'algorithme de Canny.
        C'est la seul fonction qu'il faut utiliser en dehors de la classe
        """

        imgSmooth = convolve(self.img, self.__gaussianKernel__(self.kernelSize, self.sigma))
        gradientMat, thetaMat = self.__sobelFilter__(imgSmooth)
        nonMaxImg = self.__nonMaxSuppression__(gradientMat, thetaMat)

        self.imgFinal = self.__hysteresis__(self.__threshold__(nonMaxImg))

        self.imgFinal = self.__hysteresis__(self.__threshold__(nonMaxImg))
        return self.imgFinal

    def plotResult(self):
        """
        Affiche le resultat de la detection de contour
        """
        imgplot = plt.imshow(self.imgFinal, cmap="gray")
        plt.title("Canny edge detection")
        plt.show(block = "false")

"""
Permet de tester les fonctions creees dans ce module
"""
if __name__ == "__main__":
    import imageReader

    img = imageReader.open("res\dog.png")

    testCanny = cannyEdgeDetector(img)
    imgFinal = testCanny.detectEdges()
    testCanny.plotResult()




    
 No newline at end of file
+167 −0
Original line number Diff line number Diff line
import numpy as np

import matplotlib.pyplot as plt

import matplotlib.image as npimage


from scipy.ndimage import gaussian_filter
from scipy.ndimage.filters import convolve


def redFilter(img_orig):
    """
    Applique un filtre rouge à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -17,8 +16,13 @@ def redFilter(img_orig):
            im[i, j] = (r, 0, 0, a)
    return im


def blueFilter(img_orig):
    """
    Applique un filtre bleu à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -26,8 +30,13 @@ def blueFilter(img_orig):
            im[i, j] = (0, 0, b, a)
    return im


def greenFilter(img_orig):
    """
    Applique un filtre vert à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -35,8 +44,13 @@ def greenFilter(img_orig):
            im[i, j] = (0, v, 0, a)
    return im


def cyanFilter(img_orig):
    """
    Applique un filtre cyan à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -44,8 +58,13 @@ def cyanFilter(img_orig):
            im[i, j] = (r, 1, 1, a)
    return im


def magentaFilter(img_orig):
    """
    Applique un filtre magenta à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -53,8 +72,13 @@ def magentaFilter(img_orig):
            im[i, j] = (1, v, 1, a)
    return im


def yellowFilter(img_orig):
    """
    Applique un filtre jaune à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -62,8 +86,13 @@ def yellowFilter(img_orig):
            im[i, j] = (1, 1, b, a)
    return im


def blackFilter(img_orig):
    """
    Applique un filtre en niveau de gris à l'image.
    Input: image
    Output: image
    """

    im = np.copy(img_orig)  # On fait une copie de l'original
    for i in range(im.shape[0]):
        for j in range(im.shape[1]):
@@ -72,119 +101,11 @@ def blackFilter(img_orig):
            im[i, j] = (y, y, y, a)
    return im


def gaussian_kernel(size, sigma=1):
    size = int(size) // 2
    x, y = np.mgrid[-size:size+1, -size:size+1]
    normal = 1 / (2.0 * np.pi * sigma**2)
    g = np.exp(-((x**2 + y**2) / (2.0*sigma**2))) * normal

    return g


def sobel_filters(img):
    Kx = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]], np.float32)
    Ky = np.array([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], np.float32)

    Ix = convolve(img, Kx)
    Iy = convolve(img, Ky)

    G = np.hypot(Ix, Iy)
    G = G/G.max()*255
    theta = np.arctan2(Iy, Ix)

    return (G, theta)


def non_max_suppression(img, D):
    M, N = img.shape
    Z = np.zeros((M, N), dtype=np.int32)
    angle = D * 180. / np.pi
    angle[angle < 0] += 180

    for i in range(1, M-1):
        for j in range(1, N-1):
            try:
                q = 255
                r = 255

               # angle 0
                if (0 <= angle[i, j] < 22.5) or (157.5 <= angle[i, j] <= 180):
                    q = img[i, j+1]
                    r = img[i, j-1]
                # angle 45
                elif (22.5 <= angle[i, j] < 67.5):
                    q = img[i+1, j-1]
                    r = img[i-1, j+1]
                # angle 90
                elif (67.5 <= angle[i, j] < 112.5):
                    q = img[i+1, j]
                    r = img[i-1, j]
                # angle 135
                elif (112.5 <= angle[i, j] < 157.5):
                    q = img[i-1, j-1]
                    r = img[i+1, j+1]

                if (img[i, j] >= q) and (img[i, j] >= r):
                    Z[i, j] = img[i, j]
                else:
                    Z[i, j] = 0

            except IndexError as e:
                pass

    return Z

def threshold(img, lowThresholdRatio=0.05, highThresholdRatio=0.09):

    highThreshold = img.max() * highThresholdRatio
    lowThreshold = highThreshold * lowThresholdRatio

    M, N = img.shape
    res = np.zeros((M, N), dtype=np.int32)

    weak = np.int32(25)
    strong = np.int32(255)

    strong_i, strong_j = np.where(img >= highThreshold)
    zeros_i, zeros_j = np.where(img < lowThreshold)

    weak_i, weak_j = np.where((img <= highThreshold) & (img >= lowThreshold))

    res[strong_i, strong_j] = strong
    res[weak_i, weak_j] = weak

    return res

def hysteresis(img, weak=25, strong=255):
def plotAllFilters(img):
    """
    if img.dtype != np.uint8: # Si le résultat n'est pas un tableau d'entiers
        img = (img * 255).astype(np.uint8)
    Fonction permettant d'appliquer tous les filtres du module "filters" a l'image "img". Cette fonction s'occupe aussi d'afficher
    les resultats apres application des filtres.
    """
    M, N = img.shape  
    for i in range(1, M-1):
        for j in range(1, N-1):
            if (img[i,j] == weak):
                try:
                    if ((img[i+1, j-1] == strong) or (img[i+1, j] == strong) or (img[i+1, j+1] == strong)
                        or (img[i, j-1] == strong) or (img[i, j+1] == strong)
                        or (img[i-1, j-1] == strong) or (img[i-1, j] == strong) or (img[i-1, j+1] == strong)):
                        img[i, j] = strong
                    else:
                        img[i, j] = 0
                except IndexError as e:
                    pass
    return img


if __name__ == "__main__":
    # Lecture de l'image
    img = npimage.imread("res/dog.png")
    img = np.array(img, copy=True)
    rgb_weights = [1, 1, 1]
    img_rescale = np.dot(img[..., :3], rgb_weights)
    
    #Plot RGB

    plt.subplot(221)
    plt.title("Base picture")
@@ -203,6 +124,7 @@ if __name__ == "__main__":
    plt.title("Blue filter")
    imgplot = plt.imshow(blueFilter(img))

    plt.subplots_adjust(wspace=0.3, hspace=0.3)
    plt.show()    

    #Plot CNY
@@ -222,23 +144,24 @@ if __name__ == "__main__":
    plt.title("Yellow filter")
    imgplot = plt.imshow(yellowFilter(img))

    plt.subplots_adjust(wspace=0.3, hspace=0.3)
    plt.show()

    #Plot black & white
    imgplot = plt.imshow(blackFilter(img))
    plt.title("B&W filter")
    plt.show(block = "false");
    plt.title("GreyScale filter")
    plt.show();

    #Plot canny

    imgSmooth = convolve(img_rescale, gaussian_kernel(5, 1))
    gradientMat, thetaMat = sobel_filters(imgSmooth)
"""
Permet de tester les fonctions créées dans ce module
"""
if __name__ == "__main__": 
    import imageReader

    G, t = sobel_filters(imgSmooth)
    nonMaxImg = non_max_suppression(G, t)
    #Open image
    img = imageReader.open("res\dog.png")

    imgFinal = hysteresis(threshold(nonMaxImg))
    #Plot results
    plotAllFilters(img)
   
 No newline at end of file
    imgplot = plt.imshow(imgFinal, cmap="gray")
    plt.title("Canny edge detection")
    plt.show(block = "false")

fourierTransform.py

0 → 100644
+54 −0
Original line number Diff line number Diff line

import numpy as np
import imageReader
from matplotlib import pyplot as plt


def fourierTransform(img):
    """
    Applique une transformation de Fourier sur un tableau à deux dimensions
    Input: image (ndarray, tableau de pixels)
    Ouput: transformation de Fourier (ndarray)
    """
    fft = np.fft.fft2(img)
    return fft

def plotResult(img, img_fft):
    plt.subplot(211)
    plt.title("Base picture")
    imgplot = plt.imshow(img)

    


    plt.subplot(212)
    plt.title("Picture after FFT")
    imgplot = plt.imshow(abs(img_fft))

    plt.subplots_adjust(hspace=0.3)
    plt.show()

def plotCannyResult(cannyImg, cannyImg_fft):
    plt.subplot(211)
    plt.title("Base picture")
    imgplot = plt.imshow(cannyImg, cmap="gray")


    plt.subplot(212)
    plt.title("Picture after FFT")
    imgplot = plt.imshow(abs(cannyImg_fft), cmap="gray")

    plt.subplots_adjust(hspace=0.3)
    plt.show()

"""
Permet de tester la fonctions créée dans ce module
"""
if __name__ == "__main__":
    img = imageReader.open("res\dice.png");
    fft = fourierTransform(img)

    plotResult(img, fft)
    

    
 No newline at end of file

imageReader.py

0 → 100644
+23 −0
Original line number Diff line number Diff line
import numpy as np

import matplotlib.image as npimage

"""
Ouvre une image et retourne un tableau contenant chaque pixel
Input: chemin de l'image
Output: image sous forme de ndarray
"""
def open(path):
    img = npimage.imread(path)
    return np.array(img, copy=True)

"""
Permet de tester la fonction créée dans ce module
"""
if __name__ == "__main__":
    from matplotlib import pyplot as plt

    img = open("res\lemon.png")

    plt.imshow(img)
    plt.show()
 No newline at end of file
Loading