Une question similaire a été posée sur Mathematica.Stackexchange . Ma réponse a évolué et a été assez longue à la fin, donc je vais résumer l’algorithme ici.
Abstrait
L'idée de base est:
- Trouvez l'étiquette.
- Trouver les bordures de l'étiquette
- Trouvez un mappage qui mappe les coordonnées de l'image aux coordonnées du cylindre afin qu'il mappe les pixels le long du bord supérieur de l'étiquette sur ([tout] / 0), les pixels le long du bord droit sur (1 / [tout]), etc.
- Transformer l'image en utilisant ce mappage
L'algorithme ne fonctionne que pour les images où:
- l'étiquette est plus claire que l'arrière-plan (cela est nécessaire pour la détection de l'étiquette)
- l'étiquette est rectangulaire (elle sert à mesurer la qualité d'une cartographie)
- le pot est (presque) vertical (cela permet de garder la fonction de cartographie simple)
- le pot est cylindrique (ceci permet de garder la fonction de cartographie simple)
Cependant, l'algorithme est modulaire. Au moins en principe, vous pouvez écrire votre propre détection d’étiquette qui ne nécessite pas d’arrière-plan sombre ou votre propre fonction de mesure de la qualité, compatible avec les étiquettes elliptiques ou octogonales.
Résultats
Ces images ont été traitées de manière entièrement automatique, c’est-à-dire que l’algorithme prend l’image source, fonctionne pendant quelques secondes, puis affiche le mappage (à gauche) et l’image non déformée (à droite):
Les images suivantes ont été traitées avec une version modifiée de l’algorithme, si l’utilisateur sélectionne les bords gauche et droit du pot (et non l’étiquette), car la courbure de l’étiquette ne peut pas être estimée à partir de l’image dans un plan frontal (c.-à-d. Le algorithme entièrement automatique renvoie des images légèrement déformées):
La mise en oeuvre:
1. Trouver l'étiquette
L'étiquette est brillante devant un fond sombre, je peux donc la trouver facilement en utilisant la binarisation:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Je choisis simplement le plus grand composant connecté et suppose que c'est l'étiquette:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Trouver les bordures de l'étiquette
Étape suivante: recherchez les bordures haut / bas / gauche / droite à l’aide de simples masques de convolution dérivés:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
Il s’agit d’une petite fonction d’aide qui trouve tous les pixels blancs dans l’une de ces quatre images et convertit les index en coordonnées ( Position
renvoie les index et les index sont des {{, x} -tuples basés sur 1, où y = 1 est au sommet de l’image, mais toutes les fonctions de traitement de l’image attendent des coordonnées, qui sont des {x, y} -tuples basés sur 0, où y = 0 est le bas de l’image):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Trouver une correspondance entre les coordonnées de l’image et les coordonnées du cylindre
Maintenant, j'ai quatre listes distinctes de coordonnées des bordures supérieure, inférieure, gauche et droite de l'étiquette. Je définis une correspondance entre les coordonnées de l'image et les coordonnées du cylindre:
arcSinSeries = Normal[Series[ArcSin[\[Alpha]], {\[Alpha], 0, 10}]]
Clear[mapping];
mapping[{x_, y_}] :=
{
c1 + c2*(arcSinSeries /. \[Alpha] -> (x - cx)/r) + c3*y + c4*x*y,
top + y*height + tilt1*Sqrt[Clip[r^2 - (x - cx)^2, {0.01, \[Infinity]}]] + tilt2*y*Sqrt[Clip[r^2 - (x - cx)^2, {0.01, \[Infinity]}]]
}
Il s'agit d'une application cylindrique, qui mappe les coordonnées X / Y de l'image source en coordonnées cylindriques. La cartographie présente 10 degrés de liberté hauteur / rayon / centre / perspective / inclinaison. J'ai utilisé la série de Taylor pour approximer l'arc sinus, car je ne pouvais pas utiliser l'optimisation directement avec ArcSin. leClip
les appels sont ma tentative ad hoc d’empêcher les nombres complexes lors de l’optimisation. Il y a un compromis à faire ici: d'une part, la fonction doit être aussi proche que possible d'une représentation cylindrique exacte, afin de produire la distorsion la plus faible possible. D'autre part, si c'est trop compliqué, il devient beaucoup plus difficile de trouver automatiquement des valeurs optimales pour les degrés de liberté. (Ce qui est bien avec le traitement des images avec Mathematica, c’est que vous pouvez manipuler des modèles mathématiques comme celui-ci très facilement, introduire des termes supplémentaires pour différentes distorsions et utiliser les mêmes fonctions d’optimisation pour obtenir des résultats finaux. Je n’ai jamais rien pu faire. comme cela avec OpenCV ou Matlab. Mais je n’ai jamais essayé la boîte à outils symbolique pour Matlab, c’est peut-être plus utile.)
Ensuite, je définis une "fonction d'erreur" qui mesure la qualité d'une représentation image -> cylindre. C'est juste la somme des erreurs au carré pour les pixels de bordure:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Cette fonction d'erreur mesure la "qualité" d'un mappage: sa valeur la plus basse si les points du bord gauche sont mappés sur (0 / [rien]), les pixels du bord supérieur sont mappés sur ([rien] / 0), etc. .
Je peux maintenant dire à Mathematica de trouver des coefficients qui minimisent cette fonction d'erreur. Je peux faire des "suppositions éclairées" sur certains des coefficients (par exemple, le rayon et le centre du pot dans l'image). Je les utilise comme points de départ de l'optimisation:
leftMean = Mean[maskToPoints[leftBorder]][[1]];
rightMean = Mean[maskToPoints[rightBorder]][[1]];
topMean = Mean[maskToPoints[topBorder]][[2]];
bottomMean = Mean[maskToPoints[bottomBorder]][[2]];
solution =
FindMinimum[
Total[errorFunction],
{{c1, 0}, {c2, rightMean - leftMean}, {c3, 0}, {c4, 0},
{cx, (leftMean + rightMean)/2},
{top, topMean},
{r, rightMean - leftMean},
{height, bottomMean - topMean},
{tilt1, 0}, {tilt2, 0}}][[2]]
FindMinimum
trouve des valeurs pour les 10 degrés de liberté de ma fonction de mappage qui minimisent la fonction d'erreur. Combinez le mappage générique et cette solution et vous obtenez un mappage à partir des coordonnées d'image X / Y, qui correspond à la zone d'étiquette. Je peux visualiser cette cartographie en utilisant la ContourPlot
fonction de Mathematica :
Show[src,
ContourPlot[mapping[{x, y}][[1]] /. solution, {x, 0, w}, {y, 0, h},
ContourShading -> None, ContourStyle -> Red,
Contours -> Range[0, 1, 0.1],
RegionFunction -> Function[{x, y}, 0 <= (mapping[{x, y}][[2]] /. solution) <= 1]],
ContourPlot[mapping[{x, y}][[2]] /. solution, {x, 0, w}, {y, 0, h},
ContourShading -> None, ContourStyle -> Red,
Contours -> Range[0, 1, 0.2],
RegionFunction -> Function[{x, y}, 0 <= (mapping[{x, y}][[1]] /. solution) <= 1]]]
4. Transformer l'image
Enfin, j'utilise la ImageForwardTransform
fonction Mathematica pour déformer l'image en fonction de ce mappage:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Cela donne les résultats comme indiqué ci-dessus.
Version assistée manuellement
L'algorithme ci-dessus est complètement automatique. Aucun ajustement requis. Cela fonctionne raisonnablement bien tant que la photo est prise d'en haut ou en bas. Mais s'il s'agit d'un tir frontal, le rayon du pot ne peut pas être estimé à partir de la forme de l'étiquette. Dans ces cas, j'obtiens de bien meilleurs résultats si je laisse l'utilisateur entrer manuellement les bordures gauche / droite du pot et définir explicitement les degrés de liberté correspondants dans le mappage.
Ce code permet à l'utilisateur de sélectionner les bordures gauche / droite:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
C'est le code d'optimisation alternatif, où le centre et le rayon sont donnés explicitement.
manualAdjustments = {cx -> (xLeft + xRight)/2, r -> (xRight - xLeft)/2};
solution =
FindMinimum[
Total[minimize /. manualAdjustments],
{{c1, 0}, {c2, rightMean - leftMean}, {c3, 0}, {c4, 0},
{top, topMean},
{height, bottomMean - topMean},
{tilt1, 0}, {tilt2, 0}}][[2]]
solution = Join[solution, manualAdjustments]