Avant-propos
J'interviens surtout parce que j'ai trouvé à la fois le tutoriel sur les bassins versants dans la documentation d'OpenCV (et Exemple C++ ) ainsi que Réponse de mmgp ci-dessus pour être assez confus. J'ai revisité plusieurs fois l'approche d'un bassin versant pour finalement abandonner par frustration. J'ai finalement réalisé que je devais au moins essayer cette approche et la voir en action. Voici ce que j'ai trouvé après avoir trié tous les tutoriels que j'ai rencontrés.
Outre le fait que je suis un novice en matière de vision par ordinateur, la plupart de mes difficultés étaient probablement liées à l'obligation d'utiliser la bibliothèque OpenCVSharp plutôt que Python. Le C# n'a pas d'opérateurs de tableaux puissants intégrés comme ceux que l'on trouve dans NumPy (bien que je réalise que cela a été porté via IronPython), donc j'ai eu beaucoup de mal à comprendre et à implémenter ces opérations en C#. En outre, pour mémoire, je méprise vraiment les nuances et les incohérences de la plupart de ces appels de fonction. OpenCVSharp est l'une des bibliothèques les plus fragiles avec lesquelles j'ai jamais travaillé. Mais bon, c'est un portage, alors à quoi je m'attendais ? Le meilleur de tous, cependant -- c'est gratuit.
Sans plus attendre, parlons de mon implémentation OpenCVSharp de la ligne de partage des eaux, et espérons-le, clarifions certains des points les plus épineux de l'implémentation de la ligne de partage des eaux en général.
Application
Tout d'abord, assurez-vous que le bassin versant correspond à ce que vous voulez et comprenez son utilisation. J'utilise des plaques cellulaires colorées, comme celle-ci :
Il m'a fallu un bon moment pour comprendre que je ne pouvais pas passer un seul coup de fil pour différencier toutes les cellules du champ. Au contraire, je devais d'abord isoler une partie du champ, puis appeler la ligne de partage des eaux sur cette petite partie. J'ai isolé ma région d'intérêt (ROI) via un certain nombre de filtres, que je vais expliquer brièvement ici :
- Commencez par l'image source (à gauche, recadrée à des fins de démonstration)
- Isoler le canal rouge (milieu gauche)
- Appliquer un seuil adaptatif (milieu droit)
- Trouver les contours puis éliminer ceux qui présentent de petites surfaces (à droite)
Une fois que nous avons nettoyé les contours résultant des opérations de seuillage ci-dessus, il est temps de trouver des candidats pour les bassins versants. Dans mon cas, j'ai simplement itéré sur tous les contours dépassant une certaine surface.
Code
Disons que nous avons isolé ce contour du champ ci-dessus comme notre ROI :
Voyons comment nous allons coder un bassin versant.
Nous allons commencer par un tapis vierge et dessiner uniquement le contour définissant notre ROI :
var isolatedContour = new Mat(source.Size(), MatType.CV_8UC1, new Scalar(0, 0, 0));
Cv2.DrawContours(isolatedContour, new List<List<Point>> { contour }, -1, new Scalar(255, 255, 255), -1);
Pour que l'appel du bassin versant fonctionne, il a besoin de quelques "indices" sur le ROI. Si, comme moi, vous êtes un débutant complet, je vous recommande de consulter la page Page du bassin versant de la CMM pour une introduction rapide. Il suffit de dire que nous allons créer des indices sur le ROI de gauche en créant la forme de droite :
Pour créer la partie blanche (ou " arrière-plan ") de cette forme " indice ", nous allons juste Dilate
la forme isolée comme ceci :
var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(2, 2));
var background = new Mat();
Cv2.Dilate(isolatedContour, background, kernel, iterations: 8);
Pour créer la partie noire au milieu (ou "premier plan"), nous allons utiliser une transformation de distance suivie d'un seuil, qui nous fait passer de la forme de gauche à celle de droite :
Cela prend quelques étapes, et vous devrez peut-être jouer avec la limite inférieure de votre seuil pour obtenir des résultats qui vous conviennent :
var foreground = new Mat(source.Size(), MatType.CV_8UC1);
Cv2.DistanceTransform(isolatedContour, foreground, DistanceTypes.L2, DistanceMaskSize.Mask5);
Cv2.Normalize(foreground, foreground, 0, 1, NormTypes.MinMax); //Remember to normalize!
foreground.ConvertTo(foreground, MatType.CV_8UC1, 255, 0);
Cv2.Threshold(foreground, foreground, 150, 255, ThresholdTypes.Binary);
Ensuite, nous soustrairons ces deux tapis pour obtenir le résultat final de notre forme "indice" :
var unknown = new Mat(); //this variable is also named "border" in some examples
Cv2.Subtract(background, foreground, unknown);
Encore une fois, si nous Cv2.ImShow
inconnu cela ressemblerait à ceci :
Joli ! C'était facile pour moi de me faire à l'idée. La partie suivante, par contre, m'a laissé assez perplexe. Voyons comment transformer notre "indice" en quelque chose de l'ordre du Watershed
peut utiliser. Pour cela, nous devons utiliser ConnectedComponents
qui est en fait une grande matrice de pixels regroupés en fonction de leur indice. Par exemple, si nous avions un tapis avec les lettres "HI", ConnectedComponents
pourrait retourner cette matrice :
0 0 0 0 0 0 0 0 0
0 1 0 1 0 2 2 2 0
0 1 0 1 0 0 2 0 0
0 1 1 1 0 0 2 0 0
0 1 0 1 0 0 2 0 0
0 1 0 1 0 2 2 2 0
0 0 0 0 0 0 0 0 0
Ainsi, 0 est le fond, 1 est la lettre "H", et 2 est la lettre "I". (Si vous arrivez à ce stade et que vous souhaitez visualiser votre matrice, je vous recommande de consulter le site suivant cette réponse instructive .) Maintenant, voici comment nous allons utiliser ConnectedComponents
pour créer les marqueurs (ou étiquettes) du bassin versant :
var labels = new Mat(); //also called "markers" in some examples
Cv2.ConnectedComponents(foreground, labels);
labels = labels + 1;
//this is a much more verbose port of numpy's: labels[unknown==255] = 0
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
//You may be able to just send "int" in rather than "char" here:
var labelPixel = (int)labels.At<char>(y, x); //note: x and y are inexplicably
var borderPixel = (int)unknown.At<char>(y, x); //and infuriatingly reversed
if (borderPixel == 255)
labels.Set(y, x, 0);
}
}
Notez que la fonction Watershed exige que la zone de bordure soit marquée par 0. Nous avons donc défini tous les pixels de bordure à 0 dans le tableau des étiquettes/marqueurs.
A ce stade, nous devrions être prêts à appeler Watershed
. Cependant, dans mon application particulière, il est utile de visualiser une petite partie de l'image source entière pendant cet appel. Cela peut être facultatif pour vous, mais je commence par masquer une petite partie de la source en la dilatant :
var mask = new Mat();
Cv2.Dilate(isolatedContour, mask, new Mat(), iterations: 20);
var sourceCrop = new Mat(source.Size(), source.Type(), new Scalar(0, 0, 0));
source.CopyTo(sourceCrop, mask);
Et ensuite faire l'appel magique :
Cv2.Watershed(sourceCrop, labels);
Résultats
Ce qui précède Watershed
L'appel modifiera labels
en place . Vous devrez revenir à la mémoire de la matrice résultant de ConnectedComponents
. La différence ici est que si le bassin versant a trouvé des barrages entre les bassins versants, ils seront marqués comme "-1" dans cette matrice. Comme le ConnectedComponents
Par conséquent, les différents bassins versants seront marqués d'une manière similaire par des nombres croissants. Pour mes besoins, je voulais les stocker dans des contours séparés, j'ai donc créé cette boucle pour les séparer :
var watershedContours = new List<Tuple<int, List<Point>>>();
for (int x = 0; x < labels.Width; x++)
{
for (int y = 0; y < labels.Height; y++)
{
var labelPixel = labels.At<Int32>(y, x); //note: x, y switched
var connected = watershedContours.Where(t => t.Item1 == labelPixel).FirstOrDefault();
if (connected == null)
{
connected = new Tuple<int, List<Point>>(labelPixel, new List<Point>());
watershedContours.Add(connected);
}
connected.Item2.Add(new Point(x, y));
if (labelPixel == -1)
sourceCrop.Set(y, x, new Vec3b(0, 255, 255));
}
}
Ensuite, je voulais imprimer ces contours avec des couleurs aléatoires, j'ai donc créé le tapis suivant :
var watershed = new Mat(source.Size(), MatType.CV_8UC3, new Scalar(0, 0, 0));
foreach (var component in watershedContours)
{
if (component.Item2.Count < (labels.Width * labels.Height) / 4 && component.Item1 >= 0)
{
var color = GetRandomColor();
foreach (var point in component.Item2)
watershed.Set(point.Y, point.X, color);
}
}
Ce qui donne le résultat suivant lorsqu'il est montré :
Si nous dessinons sur l'image source les barrages qui ont été marqués par un -1 plus tôt, nous obtenons ceci :
Edits :
J'ai oublié de noter : assurez-vous de nettoyer vos tapis après les avoir utilisés. Ils vont rester en mémoire et OpenCVSharp peut présenter un message d'erreur incompréhensible. Je devrais vraiment utiliser using
ci-dessus, mais mat.Release()
est également une option.
En outre, la réponse de mmgp ci-dessus comprend cette ligne : dt = ((dt - dt.min()) / (dt.max() - dt.min()) * 255).astype(numpy.uint8)
qui est une étape d'étirement de l'histogramme appliquée aux résultats de la transformation de distance. J'ai omis cette étape pour un certain nombre de raisons (principalement parce que je ne pensais pas que les histogrammes que je voyais étaient trop étroits pour commencer), mais vos habitudes peuvent varier.