Recherche d'un motif semblable à un zèbre dans l'image (détection de la ligne centrale de la frange de lumière structurée à partir de la photo)

12

Je travaille sur un projet où des franges sont projetées sur un sujet et une photo est prise. La tâche consiste à trouver les lignes centrales des franges, qui représentent, mathématiquement, la courbe d'intersection 3D entre le plan de la frange et la surface du sujet.

La photo est au format PNG (RVB), et les tentatives précédentes utilisaient des niveaux de gris puis des seuils de différence pour obtenir une photographie en noir et blanc "semblable à un zèbre", à partir de laquelle il était facile de trouver le milieu de chaque colonne de pixels de chaque frange. Le problème est que, par seuillage et également en prenant la hauteur moyenne d'une colonne de pixels discrets, nous avons une perte de précision et une quantification, ce qui n'est pas du tout souhaité.

Mon impression, en regardant les images, est que les traits d'axe pourraient être plus continus (plus de points) et plus lisses (non quantifiés) s'ils étaient détectés directement à partir de l'image sans seuil (RVB ou niveaux de gris), par une méthode de balayage statistique (quelques inondations / convolution itérative, peu importe).

Voici un exemple d'image réel:

entrez la description de l'image ici

Toute suggestion serait très appréciée!

heltonbiker
la source
c'est très intéressant. Mais au fait, je fais des recherches en utilisant une bande de couleur pour détecter un objet 3D. Parce qu'en utilisant une bande de couleur, il est facile de trouver la correspondance de chaque bande à partir du projecteur.Ainsi, en utilisant la trigonométrie, les informations 3D peuvent être calculées. Comment trouvez-vous la correspondance si la couleur est la même? Je suppose que votre projet concerne également la reconstruction 3D?
@johnyoung: veuillez ne pas ajouter de commentaires comme réponses. Je sais que vous avez besoin de réputation avant de pouvoir commenter, mais veuillez vous abstenir de votre plan d'action actuel. Je suggère de poser vos propres questions (connexes) ou de répondre aux questions des autres pour augmenter votre réputation.
Peter K.
Désolé pour une question de plus au lieu de donner la réponse, dans la méthode de décalage de phase, nous calculons la phase à chaque pixel de l'image projetée, mais voici pourquoi nous devons trouver la ligne médiane de la frange, ma question est peut-être trop idiote mais je ne le fais pas non, alors veuillez me donner la raison exacte. Vous pouvez supprimer ma question après avoir répondu
Ce sont des méthodes différentes. Je modélise une série de plans géométriques en projetant une série de bandes blanches (chacune formant un "plan" dans l'espace 3D). Ainsi, je dois trouver la ligne médiane des franges, car les plans n'ont pas d'épaisseur. Bien sûr, je pourrais effectuer une analyse de déphasage, mais il y a un problème: ma projection est binaire (bandes noires et blanches alternées), l'intensité ne varie pas sinusoïdalement, et donc je ne peux pas effectuer de déphasage (et je n'ai pas besoin de le faire, actuellement ).
heltonbiker

Réponses:

13

Je suggère les étapes suivantes:

  1. Trouvez un seuil pour séparer le premier plan de l'arrière-plan.
  2. Pour chaque goutte dans l'image binaire (une bande de zèbre), pour chacune x, trouvez le centre pondéré (par intensité de pixel) dans la ydirection.
  3. Éventuellement, lissez les yvaleurs pour éliminer le bruit.
  4. Reliez les (x,y)points en ajustant une sorte de courbe. Cet article pourrait vous aider. Vous pouvez également adapter un polynôme de haut niveau, bien que ce soit pire à mon avis.

Voici un code Matlab qui montre les étapes 1, 2 et 4. J'ai ignoré la sélection de seuil automatique. Au lieu de cela, j'ai choisi le manuel th=40:

Voici les courbes que l'on trouve en trouvant la moyenne pondérée par colonne: entrez la description de l'image ici

Ce sont les courbes après ajustement d'un polynôme: entrez la description de l'image ici

Voici le code:

function Zebra()
    im = imread('http://i.stack.imgur.com/m0sy7.png');
    im = uint8(mean(im,3));

    th = 40;
    imBinary = im>th;
    imBinary = imclose(imBinary,strel('disk',2));
    % figure;imshow(imBinary);
    labels = logical(imBinary);
    props =regionprops(labels,im,'Image','Area','BoundingBox');

    figure(1);imshow(im .* uint8(imBinary));
    figure(2);imshow(im .* uint8(imBinary));

    for i=1:numel(props)
        %Ignore small ones
        if props(i).Area < 10
            continue
        end
        %Find weighted centroids
        boundingBox = props(i).BoundingBox;
        ul = boundingBox(1:2)+0.5;
        wh = boundingBox(3:4);
        clipped = im( ul(2): (ul(2)+wh(2)-1), ul(1): (ul(1)+wh(1)-1) );
        imClip = double(props(i).Image) .* double(clipped);
        rows = transpose( 1:size(imClip,1) );
        %Weighted calculation
        weightedRows  = sum(bsxfun(@times, imClip, rows),1) ./ sum(imClip,1);
        %Calculate x,y
        x = ( 1:numel(weightedRows) ) + ul(1) - 1;
        y = ( weightedRows ) + ul(2) - 1;
        figure(1);
        hold on;plot(x,y,'b','LineWidth',2);
        try %#ok<TRYNC>
            figure(2);
            [xo,yo] = FitCurveByPolynom(x,y);
            hold on;plot(xo,yo,'g','LineWidth',2);
        end
        linkaxes( cell2mat(get(get(0,'Children'),'Children')) )
    end        
end

function [xo,yo] = FitCurveByPolynom(x,y)
   p = polyfit(x,y,15); 
   yo = polyval(p,x);
   xo = x;
end
Andrey Rubshtein
la source
J'ai trouvé cela très intéressant. J'utilise Python, mais de toute façon je vais devoir étudier la raison d'être de tout cela. En tant que commentaire indépendant, j'ai tendance à ne pas effectuer de traitement d'image classique (directement sur des conteneurs d'images quantifiés tels que des tableaux uint8), mais à la place tout charger en mémoire sous forme de tableaux flottants avant d'appliquer les opérations. De plus, je suis surpris des résultats de la moitié inférieure de votre image, les lignes bleues ne suivent pas les lignes médianes attendues ... (?). Merci pour l'instant, je vais apporter des commentaires dès que j'obtiendrai un résultat!
heltonbiker
@heltonbiker, veuillez vérifier la réponse mise à jour. Vous avez raison sur la virgule flottante, je l'ai utilisé lors de ma conversion double. À propos des résultats dans la moitié inférieure, je dois vérifier, il pourrait s'agir d'un bug logiciel
Andrey Rubshtein
1
@heltonbiker, c'est fait. Il s'agissait en effet d'un bug lié à l'indexation basée sur 1.
Andrey Rubshtein
Excellent! Incroyable, en effet. Avec cette technique, et pour mes besoins, le lissage non seulement ne sera même pas nécessaire, mais serait également nocif. Merci beaucoup pour votre intérêt!
heltonbiker
3

Je n'utiliserais pas l'image RVB. Les images en couleur sont généralement réalisées en plaçant un "filtre Bayer" sur le capteur de la caméra, ce qui réduit généralement la résolution que vous pouvez obtenir.

Si vous utilisez l'image en niveaux de gris, je pense que les étapes que vous avez décrites (binariser l'image "zèbre", trouver la ligne médiane) sont un bon début. Enfin, je voudrais

  • Prenez chaque point dans la ligne médiane que vous avez trouvée
  • prendre les valeurs de gris des pixels dans la ligne "zèbre" au-dessus et en dessous
  • adapter une parabole à ces valeurs de gris en utilisant les moindres carrés moyens
  • le sommet de cette parabole est une estimation améliorée de la position médiane
Niki Estner
la source
Belles pensées. Je prévois d'utiliser une sorte de parabole ou de spline le long des valeurs maximales de chaque colonne de pixels, mais je me demande toujours si je devrais examiner une colonne de pixels ou plutôt une "région" de pixels le long de la ligne ... Je vais attendre encore un peu plus de réponses. Merci pour l'instant!
heltonbiker
@heltonbiker - comme test rapide, utilisez uniquement le canal vert. Il y a normalement 2x plus de pixels verts sur un capteur de couleur et il est moins interpolé que le rouge et le bleu
Martin Beckett
@MartinBeckett Merci de votre intérêt, j'ai déjà analysé chaque canal, et en effet le vert semble être beaucoup plus résolu que, disons, le rouge. En traçant les valeurs d'intensité des coupes verticales pour chaque canal, cependant, le "motif de rayures" ne semble pas tellement changer entre les canaux, et je les mélange actuellement également lors de la conversion en niveaux de gris. Même si je prévois toujours d'étudier la meilleure combinaison linéaire entre les canaux pour obtenir le meilleur contraste, OU pour acquérir des images déjà en niveaux de gris. Merci encore!
heltonbiker
3

Voici encore une solution alternative à votre problème en modélisant votre question comme un «problème d'optimisation de chemin». Bien qu'elle soit plus compliquée que la simple solution de binarisation et d'ajustement de courbe, elle est plus robuste dans la pratique.

Du niveau très élevé, nous devons considérer cette image comme un graphique, où

  1. chaque pixel d'image est un nœud sur ce graphique

  2. chaque nœud est connecté à d'autres nœuds, appelés voisins, et cette définition de connexion est souvent référée à la topologie de ce graphique.

  3. chaque nœud a un poids (fonctionnalité, coût, énergie, ou ce que vous voulez appeler), reflétant la probabilité que ce nœud soit dans une ligne centrale optimale que nous recherchons.

Tant que nous pouvons modéliser cette probabilité, votre problème de trouver `` les lignes centrales des franges '' devient le problème de trouver des chemins locaux optimaux sur le graphique , qui peuvent être résolus efficacement par programmation dynamique, par exemple l'algorithme de Viterbi.

Voici quelques avantages d'adopter cette approche:

  1. tous vos résultats seront continus (contrairement à la méthode de seuil qui pourrait casser une ligne centrale en morceaux)

  2. beaucoup de libertés pour construire un tel graphique, vous pouvez sélectionner différentes fonctionnalités et la topologie du graphique.

  3. vos résultats sont optimaux dans le sens d'optimisations de chemin

  4. votre solution sera plus robuste contre le bruit, car tant que le bruit est réparti également entre tous les pixels, ces chemins optimaux restent stables.

Voici une courte démonstration de l'idée ci-dessus. Comme je n'utilise aucune connaissance préalable pour spécifier les nœuds de début et de fin possibles, je décode simplement wrt chaque nœud de départ possible. Chemins Viterbi décodés

Pour les terminaisons floues, cela est dû au fait que nous recherchons des chemins optimaux pour tous les nœuds de terminaison possibles. Par conséquent, bien que pour certains nœuds situés dans des zones sombres, le chemin en surbrillance reste son optimal local.

Pour le chemin flou, vous pouvez soit le lisser après l'avoir trouvé, soit utiliser des fonctions lissées au lieu de l'intensité brute.

Il est possible de restaurer des chemins partiels en modifiant les nœuds de début et de fin.

Il ne sera pas difficile d'élaguer ces chemins optimaux locaux indésirables. Parce que nous avons les probabilités de tous les chemins après le décodage viterbi, et vous pouvez utiliser diverses connaissances antérieures (par exemple, nous voyons qu'il est vrai que nous n'avons besoin que d'un chemin optimal pour ceux qui partagent la même source.)

Pour plus de détails, vous pouvez vous référer au document.

 Wu, Y.; Zha, S.; Cao, H.; Liu, D., & Natarajan, P.  (2014, February). A Markov Chain Line Segmentation Method for Text Recognition. In IS&T/SPIE 26th Annual Symposium on Electronic Imaging (DRR), pp. 90210C-90210C.

Voici un court morceau de code python utilisé pour créer le graphique ci-dessus.


import cv2
import numpy as np
from matplotlib import pyplot
# define your image path
image_path = ;
# read in an image
img = cv2.imread( image_path, 0 );
rgb = cv2.imread( image_path, -1 );

# some feature to reflect how likely a node is in an optimal path
img = cv2.equalizeHist( img ); # equalization
img = img - img.mean(); # substract DC
img_pmax = img.max(); # get brightest intensity
img_nmin = img.min(); # get darkest intensity
# express our preknowledge
img[ img > 0 ] *= +1.0  / img_pmax; 
img[ img = 1 :
    prev_idx = vt_path[ -1 ].astype('int');
    vt_path.append( path_buffer[ prev_idx, time ] );
    time -= 1;
vt_path.reverse();    
vt_path = np.asarray( vt_path ).T;

# plot found optimal paths for every 7 of them
pyplot.imshow( rgb, 'jet' ),
for row in range( 0, h, 7 ) :
    pyplot.hold(True), pyplot.plot( vt_path[row,:], c=np.random.rand(3,1), lw = 2 );
pyplot.xlim( ( 0, w ) );
pyplot.ylim( ( h, 0 ) );
piège
la source
Il s'agit d'une approche très intéressante. J'avoue que le sujet des "graphes" m'a été obscur jusqu'à récemment quand (sur ce même projet) je n'ai pu résoudre un autre problème en utilisant des graphes. Après l'avoir "compris", j'ai réalisé à quel point ces algorithmes de chemins les plus courts peuvent être puissants. Votre idée est très intéressante et il n'est pas impossible que je la réimplémente pour celle-ci si j'en ai le besoin / l'opportunité. Merci beaucoup.
heltonbiker
Quant à vos résultats actuels, d'après mon expérience, il serait probablement préférable de lisser d'abord l'image avec un filtre gaussien et / ou médian, avant de construire le graphique. Cela donnerait des lignes beaucoup plus lisses (et plus correctes). En outre, une astuce possible consiste à étendre le voisinage pour permettre un "saut direct" sur deux pixels ou plus (jusqu'à une limite donnée, disons 8 ou 10 pixels). Bien sûr, une fonction de coût appropriée devrait être choisie, mais je pense qu'elle est facile à régler.
heltonbiker
Oh oui. J'ai simplement choisi quelque chose à portée de main, vous pouvez certainement utiliser d'autres fonctions de topologie et d'énergie. En fait, ce cadre est également formable. En particulier, vous commencez avec l'intensité brute, décodez pour des chemins optimaux, ne récupérez que les nœuds optimaux avec des confidences élevées, et de cette façon vous obtenez des «données étiquetées». Avec cette petite partie de données étiquetées automatiquement, vous pouvez apprendre de nombreux types de choses utiles.
écueil
3

J'ai pensé que je devrais poster ma réponse car elle est un peu différente des autres approches. J'ai essayé cela dans Matlab.

  • additionner tous les canaux et créer une image, de sorte que tous les canaux soient pondérés de manière égale
  • effectuer la fermeture morphologique et le filtrage gaussien sur cette image
  • pour chaque colonne de l'image résultante, trouvez les maxima locaux et construisez une image
  • trouver les composants connectés de cette image

Un inconvénient que je vois ici est que cette approche ne fonctionnera pas bien pour certaines orientations des rayures. Dans ce cas, nous devons corriger son orientation et appliquer cette procédure.

Voici le code Matlab:

im = imread('m0sy7.png');
imsum = sum(im, 3); % sum all channels
h = fspecial('gaussian', 3);
im2 = imclose(imsum, ones(3)); % close
im2 = imfilter(im2, h); % smooth
% for each column, find regional max
mx = zeros(size(im2));
for c = 1:size(im2, 2)
    mx(:, c) = imregionalmax(im2(:, c));
end
% find connected components
ccomp = bwlabel(mx);

Par exemple, si vous prenez la colonne centrale de l'image, son profil devrait ressembler à ceci: (en bleu est le profil. En vert sont les maxima locaux) profil moyen et maxima locaux

Et l'image contenant les maxima locaux pour toutes les colonnes ressemble à ceci: entrez la description de l'image ici

Voici les composants connectés (bien que certaines bandes soient cassées, la plupart d'entre elles ont une région continue):

entrez la description de l'image ici

dhanushka
la source
C'est en fait ce que nous faisons maintenant, la seule différence étant de savoir comment trouver des maxima locaux pour chaque colonne de pixels: nous utilisons une interpolation parabolique pour trouver le sommet exact de la parabole passant par le pixel avec la valeur maximale et ses voisins supérieurs et inférieurs. . Cela permet au résultat d'être «entre» pixels, ce qui représente mieux la finesse subtile des lignes. Merci pour votre réponse!
heltonbiker