Jak spłaszczyć wizerunek etykiety na słoiku z jedzeniem?

40

Chciałbym zrobić zdjęcia etykiet na słoiku z jedzeniem i móc je przekształcić, tak aby etykieta była płaska, a prawa i lewa strona została przeskalowana tak, aby była równa ze środkiem obrazu.

Idealnie chciałbym użyć kontrastu między etykietą a tłem, aby znaleźć krawędzie i zastosować poprawkę. W przeciwnym razie mogę poprosić użytkownika o identyfikację narożników i boków obrazu.


Szukam ogólnych technik i algorytmów, aby zrobić zdjęcie, które jest przekrzywione sferycznie (w moim przypadku cylindrycznie) i które może spłaszczyć obraz. Obecnie obraz etykiety owiniętej wokół słoika lub butelki będzie zawierał funkcje i tekst, które zmniejszają się w miarę cofania w prawo lub w lewo od obrazu. Również linie, które oznaczają krawędź etykiety, będą równoległe tylko na środku obrazu i będą przechylać się względem siebie po prawej i lewej stronie etykiety.

Po zmanipulowaniu obrazu chciałbym pozostać z niemal idealnym prostokątem, w którym tekst i cechy są jednakowo duże, tak jakbym zrobił zdjęcie etykiety, gdy nie było jej na słoiku lub butelce.

Chciałbym również, aby technika mogła automatycznie wykryć krawędzie etykiety, aby zastosować odpowiednią korekcję. W przeciwnym razie musiałbym poprosić użytkownika o wskazanie granic etykiety.

Już googlowałem i znalazłem artykuły takie jak ten: spłaszczanie zakrzywionych dokumentów , ale szukam czegoś nieco prostszego, ponieważ moje potrzeby dotyczą etykiet z prostą krzywą.

Mahboudz
źródło
Nikie ma coś, co wydaje się wszechstronne. Jest to jednak znacznie prostsze, jeśli wiesz, że kamera jest zawsze „kwadratowa” względem słoika, bez mylącego tła. Następnie odnajdź krawędzie słoika i zastosuj prostą transformację trygonometryczną (arcsine?), Bez większego zbędnego majstrowania. Po spłaszczeniu obrazu możesz odizolować samą etykietę.
Daniel R Hicks
@Daniel To właśnie zrobiłem tutaj . Idealnie byłoby wziąć pod uwagę również niezupełnie równoległą projekcję, ale ja tego nie zrobiłem.
Szabolcs
praca jest bardzo dobra. ale kod pokazujący błąd w moim systemie. używam Matlaba 2017a, czy jest z nim kompatybilny. dziękuję
Satish Kumar,

Odpowiedzi:

60

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:

  1. Znajdź etykietę.
  2. Znajdź granice etykiety
  3. 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.
  4. Przekształć obraz za pomocą tego mapowania

Algorytm działa tylko w przypadku obrazów, w których:

  1. etykieta jest jaśniejsza niż tło (jest to konieczne do wykrycia etykiety)
  2. etykieta jest prostokątna (służy do pomiaru jakości odwzorowania)
  3. słoik jest (prawie) pionowy (służy to do uproszczenia funkcji mapowania)
  4. 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):

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

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):

wprowadź opis zdjęcia tutaj

wprowadź opis zdjęcia tutaj

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]]]

obraz binarny

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]]]

największy składnik

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}}]];

wprowadź opis zdjęcia tutaj

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 ( Positionzwraca 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. TheClippołą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]]

FindMinimumznajduje 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ą ContourPlotfunkcji 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]]]

wprowadź opis zdjęcia tutaj

4. Przekształć obraz

Na koniec używam ImageForwardTransformfunkcji 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}}]}]]]]

LocatorPane

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]
Niki Estner
źródło
11
Usuwa okulary przeciwsłoneczne ... Matka Boska ...
Spacey
Czy zdarza ci się mieć odniesienie do mapowania cylindrycznego? A może równania odwrotnego mapowania? @ niki-estner
Ita