Eine ähnliche Frage wurde bei Mathematica.Stackexchange gestellt . Meine Antwort dort hat sich weiterentwickelt und ist am Ende ziemlich lang geworden, deshalb werde ich den Algorithmus hier zusammenfassen.
Abstrakt
Die Grundidee ist:
- Suchen Sie das Etikett.
- Finden Sie die Ränder des Etiketts
- Suchen Sie eine Zuordnung, die Bildkoordinaten Zylinderkoordinaten so zuordnet, dass sie die Pixel am oberen Rand des Etiketts ([etwas] / 0), die Pixel am rechten Rand (1 / [etwas]) usw. zuordnet.
- Transformieren Sie das Bild mithilfe dieser Zuordnung
Der Algorithmus funktioniert nur für Bilder, bei denen:
- das Etikett ist heller als der Hintergrund (dies wird für die Etikettenerkennung benötigt)
- Das Etikett ist rechteckig (dies wird verwendet, um die Qualität eines Mappings zu messen).
- das Glas ist (fast) vertikal (dies wird verwendet, um die Zuordnungsfunktion einfach zu halten)
- das Gefäß ist zylindrisch (dies wird verwendet, um die Zuordnungsfunktion einfach zu halten)
Der Algorithmus ist jedoch modular. Zumindest im Prinzip könnten Sie Ihre eigene Etikettenerkennung schreiben, die keinen dunklen Hintergrund erfordert, oder Sie könnten Ihre eigene Qualitätsmessfunktion schreiben, die mit elliptischen oder achteckigen Etiketten umgehen kann.
Ergebnisse
Diese Bilder wurden vollautomatisch verarbeitet, dh der Algorithmus nimmt das Quellbild auf, arbeitet einige Sekunden und zeigt dann das Mapping (links) und das unverzerrte Bild (rechts):
Die nächsten Bilder wurden mit einer modifizierten Version des Algorithmus verarbeitet, wobei der Benutzer den linken und den rechten Rand des Glases (nicht das Etikett) auswählte, da die Krümmung des Etiketts nicht aus dem Bild in einer Frontalaufnahme (dh dem Ein vollautomatischer Algorithmus würde Bilder zurückgeben, die leicht verzerrt sind.
Implementierung:
1. Suchen Sie das Etikett
Das Etikett ist hell vor einem dunklen Hintergrund, sodass ich es mithilfe der Binarisierung leicht finden kann:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Ich wähle einfach die größte verbundene Komponente aus und gehe davon aus, dass dies die Bezeichnung ist:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Finden Sie die Ränder des Etiketts
Nächster Schritt: Finden Sie die oberen / unteren / linken / rechten Ränder mit einfachen abgeleiteten Faltungsmasken:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
Dies ist eine kleine Hilfsfunktion, die alle weißen Pixel in einem dieser vier Bilder findet und die Indizes in Koordinaten umwandelt ( Position
gibt Indizes zurück, und Indizes sind 1-basierte {y, x} -Tupel, wobei y = 1 am oberen Rand von steht Alle Bildverarbeitungsfunktionen erwarten jedoch Koordinaten, die auf 0-basierenden {x, y} -Tupeln basieren (wobei y = 0 der untere Teil des Bildes ist):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Suchen Sie eine Zuordnung von Bild zu Zylinderkoordinaten
Jetzt habe ich vier separate Koordinatenlisten für den oberen, unteren, linken und rechten Rand des Etiketts. Ich definiere eine Zuordnung von Bildkoordinaten zu Zylinderkoordinaten:
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]}]]
}
Dies ist eine zylindrische Abbildung, die X / Y-Koordinaten im Quellbild auf zylindrische Koordinaten abbildet. Die Zuordnung hat 10 Freiheitsgrade für Höhe / Radius / Mitte / Perspektive / Neigung. Ich habe die Taylor-Reihe verwendet, um den Arcussinus zu approximieren, da die Optimierung nicht direkt mit ArcSin funktioniert. DasClip
Anrufe sind mein Ad-hoc-Versuch, komplexe Nummern während der Optimierung zu verhindern. Hier gibt es einen Kompromiss: Einerseits sollte die Funktion einer exakten zylindrischen Abbildung so nahe wie möglich kommen, um eine möglichst geringe Verzerrung zu erzielen. Wenn es andererseits zu kompliziert ist, wird es viel schwieriger, automatisch optimale Werte für die Freiheitsgrade zu finden. (Das Schöne an der Bildverarbeitung mit Mathematica ist, dass Sie mit solchen mathematischen Modellen sehr einfach herumspielen, zusätzliche Begriffe für verschiedene Verzerrungen einfügen und dieselben Optimierungsfunktionen verwenden können, um endgültige Ergebnisse zu erzielen. Ich habe noch nie etwas tun können so mit OpenCV oder Matlab. Aber ich habe die symbolische Toolbox für Matlab nie ausprobiert, vielleicht macht das es nützlicher.)
Als nächstes definiere ich eine "Fehlerfunktion", die die Qualität eines Bildes misst -> Zylinderkoordinatenzuordnung. Es ist nur die Summe der Fehlerquadrate für die Randpixel:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Diese Fehlerfunktion misst die "Qualität" einer Zuordnung: Sie ist am niedrigsten, wenn die Punkte am linken Rand (0 / [irgendetwas]) zugeordnet sind, Pixel am oberen Rand ([irgendetwas] / 0) usw. .
Jetzt kann ich Mathematica anweisen, Koeffizienten zu finden, die diese Fehlerfunktion minimieren. Ich kann über einige der Koeffizienten "Vermutungen anstellen" (z. B. den Radius und die Mitte des Glases im Bild). Ich benutze diese als Ausgangspunkte für die Optimierung:
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
findet Werte für die 10 Freiheitsgrade meiner Zuordnungsfunktion, die die Fehlerfunktion minimieren. Kombiniere das generische Mapping und diese Lösung und ich erhalte ein Mapping von X / Y-Bildkoordinaten, das zum Beschriftungsbereich passt. Ich kann dieses Mapping mit der ContourPlot
Funktion von Mathematica visualisieren :
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. Transformieren Sie das Bild
Schließlich verwende ich Mathematics ImageForwardTransform
Funktion, um das Bild gemäß dieser Zuordnung zu verzerren:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Das ergibt die oben gezeigten Ergebnisse.
Manuell unterstützte Version
Der obige Algorithmus ist vollautomatisch. Keine Anpassungen erforderlich. Es funktioniert einigermaßen gut, solange das Bild von oben oder unten aufgenommen wird. Wenn es sich jedoch um eine Frontalaufnahme handelt, kann der Radius des Glases nicht anhand der Form des Etiketts geschätzt werden. In diesen Fällen erhalte ich viel bessere Ergebnisse, wenn der Benutzer den linken / rechten Rand des Glases manuell eingibt und die entsprechenden Freiheitsgrade im Mapping explizit einstellt.
Mit diesem Code kann der Benutzer den linken / rechten Rand auswählen:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
Dies ist der alternative Optimierungscode, bei dem Mittelpunkt und Radius explizit angegeben werden.
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]