Podobne pytanie został poproszony o Mathematica.Stackexchange . Moja odpowiedź tam ewoluowała i ostatecznie wydłużyła się, więc podsumuję algorytm tutaj.
Abstrakcyjny
Podstawową ideą jest:
- Znajdź etykietę.
- Znajdź granice etykiety
- Znajdź odwzorowanie, które odwzorowuje współrzędne obrazu na współrzędne walca, aby odwzorował piksele wzdłuż górnej granicy etykiety na ([cokolwiek] / 0), piksele wzdłuż prawej granicy na (1 / [cokolwiek]) i tak dalej.
- Przekształć obraz za pomocą tego mapowania
Algorytm działa tylko w przypadku obrazów, w których:
- etykieta jest jaśniejsza niż tło (jest to konieczne do wykrycia etykiety)
- etykieta jest prostokątna (służy do pomiaru jakości odwzorowania)
- słoik jest (prawie) pionowy (służy to do uproszczenia funkcji mapowania)
- słoik jest cylindryczny (służy do uproszczenia funkcji mapowania)
Algorytm jest jednak modułowy. Przynajmniej w zasadzie możesz napisać własne wykrywanie etykiet, które nie wymaga ciemnego tła, lub możesz napisać własną funkcję pomiaru jakości, która poradzi sobie z etykietami eliptycznymi lub ośmiokątnymi.
Wyniki
Te obrazy zostały przetworzone w pełni automatycznie, tj. Algorytm pobiera obraz źródłowy, działa przez kilka sekund, a następnie pokazuje mapowanie (po lewej) i niezniekształcony obraz (po prawej):
Kolejne obrazy zostały przetworzone przy użyciu zmodyfikowanej wersji algorytmu, w przypadku gdy użytkownik wybierze lewą i prawą ramkę słoika (nie etykietę), ponieważ krzywizny etykiety nie można oszacować na podstawie obrazu na zdjęciu przednim (tj. w pełni automatyczny algorytm zwraca obrazy, które są nieco zniekształcone):
Realizacja:
1. Znajdź etykietę
Etykieta jest jasna na ciemnym tle, więc mogę ją łatwo znaleźć za pomocą binaryzacji:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Po prostu wybieram największy podłączony komponent i zakładam, że to etykieta:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Znajdź granice etykiety
Następny krok: znajdź górną / dolną / lewą / prawą ramkę za pomocą prostych pochodnych masek splotu:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
Jest to mała funkcja pomocnicza, która znajduje wszystkie białe piksele na jednym z tych czterech obrazów i konwertuje wskaźniki na współrzędne ( Position
zwraca indeksy, a indeksy są oparte na 1-rzędach {y, x}, gdzie y = 1 znajduje się na górze obraz. Ale wszystkie funkcje przetwarzania obrazu oczekują współrzędnych, które są oparciami 0 x {y, y}, gdzie y = 0 jest dolną częścią obrazu:
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Znajdź odwzorowanie z obrazka na współrzędne walca
Teraz mam cztery osobne listy współrzędnych górnej, dolnej, lewej i prawej krawędzi etykiety. Definiuję odwzorowanie od współrzędnych obrazu na współrzędne walca:
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]}]]
}
Jest to odwzorowanie cylindryczne, które odwzorowuje współrzędne X / Y na obrazie źródłowym na współrzędne cylindryczne. Mapowanie ma 10 stopni swobody dla wysokości / promienia / środka / perspektywy / pochylenia. Użyłem serii Taylora do przybliżenia sinusoidy łuku, ponieważ nie mogłem uzyskać optymalizacji działającej bezpośrednio z ArcSin. TheClip
połączenia to moja doraźna próba zapobiegania liczbom złożonym podczas optymalizacji. Jest tu kompromis: z jednej strony funkcja powinna być jak najbardziej zbliżona do dokładnego odwzorowania cylindrycznego, aby zapewnić możliwie najniższe zniekształcenie. Z drugiej strony, jeśli jest to skomplikowane, znacznie trudniej jest automatycznie znaleźć optymalne wartości stopni swobody. (Zaletą robienia przetwarzania obrazu za pomocą Mathematiki jest to, że możesz bardzo łatwo bawić się takimi modelami matematycznymi, wprowadzać dodatkowe warunki dla różnych zniekształceń i używać tych samych funkcji optymalizacyjnych, aby uzyskać ostateczne wyniki. Nigdy nie byłem w stanie nic zrobić podobnie jak przy użyciu OpenCV lub Matlab. Ale nigdy nie próbowałem symbolicznego zestawu narzędzi dla Matlaba, może to czyni go bardziej użytecznym.)
Następnie definiuję „funkcję błędu”, która mierzy jakość obrazu -> mapowanie współrzędnych walca. To tylko suma błędów kwadratu dla pikseli granicy:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Ta funkcja błędu mierzy „jakość” odwzorowania: najniższa, jeśli punkty na lewej ramce są odwzorowane na (0 / [cokolwiek]), piksele na górnej granicy są zmapowane na ([cokolwiek] / 0) i tak dalej .
Teraz mogę powiedzieć Mathematica, aby znalazł współczynniki, które minimalizują tę funkcję błędu. Potrafię zgadywać na temat niektórych współczynników (np. Promień i środek słoika na obrazie). Używam ich jako punktów początkowych optymalizacji:
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
znajduje wartości 10 stopni swobody mojej funkcji mapowania, które minimalizują funkcję błędu. Połącz ogólne mapowanie z tym rozwiązaniem, a otrzymam mapowanie ze współrzędnych obrazu X / Y, które pasuje do obszaru etykiety. Mogę wizualizować to mapowanie za pomocą ContourPlot
funkcji 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. Przekształć obraz
Na koniec używam ImageForwardTransform
funkcji Mathematiki do zniekształcania obrazu zgodnie z tym odwzorowaniem:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
To daje wyniki, jak pokazano powyżej.
Wersja wspomagana ręcznie
Powyższy algorytm jest w pełni automatyczny. Nie są wymagane żadne korekty. Działa dość dobrze, o ile zdjęcie jest robione z góry lub z dołu. Ale jeśli jest to strzał z przodu, promienia słoika nie można oszacować na podstawie kształtu etykiety. W takich przypadkach uzyskuję znacznie lepsze wyniki, jeśli pozwolę użytkownikowi ręcznie wprowadzić lewą / prawą granicę słoika i wyraźnie ustawić odpowiednie stopnie swobody w odwzorowaniu.
Ten kod pozwala użytkownikowi wybrać lewą / prawą ramkę:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
Jest to alternatywny kod optymalizacji, w którym środek i promień są podane jawnie.
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]