Como achatar a imagem de um rótulo em uma jarra de comida?


40

Gostaria de tirar fotos de etiquetas em um pote de comida e poder transformá-las para que a etiqueta fique plana, com o lado direito e esquerdo redimensionados para ficar alinhado com o centro da imagem.

Idealmente, eu gostaria de usar o contraste entre o rótulo e o plano de fundo para encontrar as arestas e aplicar a correção. Caso contrário, posso pedir ao usuário que identifique de alguma forma os cantos e os lados da imagem.


Estou procurando técnicas e algoritmos gerais para obter uma imagem inclinada esfericamente (cilíndrica no meu caso) e que possa achatar a imagem. Atualmente, a imagem de uma etiqueta enrolada em uma jarra ou garrafa terá recursos e texto que serão reduzidos à medida que retrocedem para a direita ou esquerda da imagem. Além disso, as linhas que indicam a borda do rótulo serão paralelas apenas no centro da imagem e se inclinarão uma na outra, no extremo direito e esquerdo do rótulo.

Depois de manipular a imagem, gostaria de ficar com um retângulo quase perfeito, onde o texto e os recursos são de tamanho uniforme, como se eu tivesse tirado uma foto da etiqueta quando ela não estava na jarra ou na garrafa.

Além disso, eu gostaria que a técnica pudesse detectar automaticamente as bordas do rótulo, a fim de aplicar a correção adequada. Caso contrário, eu teria que pedir ao meu usuário para indicar os limites do rótulo.

Já pesquisei no Google e encontrei artigos como este: achatando documentos curvos , mas estou procurando algo um pouco mais simples, pois minhas necessidades são de etiquetas com uma curva simples.


Nikie tem o que parece ser uma solução abrangente. No entanto, fica muito mais simples se você souber que a câmera está sempre "alinhada" com a jarra, sem fundo confuso. Em seguida, você encontra as bordas da jarra e aplica a transformação trigonométrica simples (arcsine?), Sem muita brincadeira adicional. Depois que a imagem é achatada, você pode isolar o próprio rótulo.
Daniel R Hicks

@ Daniel Foi o que eu fiz aqui . Idealmente, também se levaria em consideração a projeção não perfeitamente paralela, mas não o fiz.
Szabolcs

o trabalho é muito bom mas o código mostra erro no meu sistema. Eu estou usando o Matlab 2017a é compatível com ele. obrigado,
Satish Kumar

Respostas:


60

Uma pergunta semelhante foi feita no Mathematica.Stackexchange . Minha resposta por lá evoluiu e ficou bastante longa no final, então vou resumir o algoritmo aqui.

Abstrato

A ideia básica é:

  1. Encontre o rótulo.
  2. Encontre as bordas do rótulo
  3. Encontre um mapeamento que mapeie as coordenadas da imagem para as coordenadas do cilindro, para que mapeie os pixels ao longo da borda superior do rótulo para ([qualquer coisa] / 0), os pixels ao longo da borda direita para (1 / [qualquer coisa]) e assim por diante.
  4. Transforme a imagem usando este mapeamento

O algoritmo funciona apenas para imagens em que:

  1. o rótulo é mais brilhante que o plano de fundo (isso é necessário para a detecção do rótulo)
  2. o rótulo é retangular (usado para medir a qualidade de um mapeamento)
  3. o jar é (quase) vertical (isso é usado para manter a função de mapeamento simples)
  4. o jar é cilíndrico (usado para manter a função de mapeamento simples)

No entanto, o algoritmo é modular. Pelo menos em princípio, você poderia escrever sua própria detecção de etiqueta que não requer um fundo escuro ou escrever sua própria função de medição de qualidade que pode lidar com etiquetas elípticas ou octogonais.

Resultados

Essas imagens foram processadas de forma totalmente automática, ou seja, o algoritmo obtém a imagem de origem, funciona por alguns segundos e mostra o mapeamento (à esquerda) e a imagem sem distorção (à direita):

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

insira a descrição da imagem aqui

As próximas imagens foram processadas com uma versão modificada do algoritmo, onde o usuário seleciona as bordas esquerda e direita do jar (não o rótulo), porque a curvatura do rótulo não pode ser estimada a partir da imagem em uma foto frontal (ou seja, o algoritmo totalmente automático retornaria imagens ligeiramente distorcidas):

insira a descrição da imagem aqui

insira a descrição da imagem aqui

Implementação:

1. Encontre o rótulo

O rótulo é brilhante diante de um fundo escuro, para que eu possa encontrá-lo facilmente usando a binarização:

src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]

imagem binarizada

Simplesmente escolho o maior componente conectado e assumo que esse é o rótulo:

labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]

maior componente

2. Encontre as bordas do rótulo

Próxima etapa: encontre as bordas superior / inferior / esquerda / direita usando máscaras de convolução derivadas simples:

topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];

insira a descrição da imagem aqui

Essa é uma pequena função auxiliar que encontra todos os pixels brancos em uma dessas quatro imagens e converte os índices em coordenadas ( Positionretorna índices e os índices são baseados em 1 com base em {y, x} -tuplos, em que y = 1 está no topo de Mas todas as funções de processamento de imagem esperam coordenadas, com base em 0 (x, y} -tuples, em que y = 0 é a parte inferior da imagem):

{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];

3. Encontre um mapeamento da imagem para as coordenadas do cilindro

Agora eu tenho quatro listas separadas de coordenadas das bordas superior, inferior, esquerda e direita do rótulo. Defino um mapeamento das coordenadas da imagem para as coordenadas do cilindro:

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

Este é um mapeamento cilíndrico, que mapeia coordenadas X / Y na imagem de origem para coordenadas cilíndricas. O mapeamento possui 10 graus de liberdade para altura / raio / centro / perspectiva / inclinação. Eu usei a série Taylor para aproximar o arco seno, porque não consegui fazer a otimização trabalhar diretamente com o ArcSin. oClipchamadas são minha tentativa ad-hoc de impedir números complexos durante a otimização. Há uma troca aqui: por um lado, a função deve estar o mais próximo possível de um mapeamento cilíndrico exato, para fornecer a menor distorção possível. Por outro lado, se for muito complicado, fica muito mais difícil encontrar valores ótimos para os graus de liberdade automaticamente. (O bom de fazer o processamento de imagens com o Mathematica é que você pode brincar com modelos matemáticos como esse com muita facilidade, introduzir termos adicionais para diferentes distorções e usar as mesmas funções de otimização para obter resultados finais. Nunca consegui fazer nada usando o OpenCV ou o Matlab. Mas nunca experimentei a caixa de ferramentas simbólica do Matlab, talvez isso o torne mais útil.)

Em seguida, defino uma "função de erro" que mede a qualidade de uma imagem -> mapeamento de coordenadas do cilindro. É apenas a soma dos erros ao quadrado dos pixels da borda:

errorFunction =
  Flatten[{
    (mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
    (mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
    (mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
    (mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
    }];

Essa função de erro mede a "qualidade" de um mapeamento: é mais baixa se os pontos na borda esquerda são mapeados para (0 / [qualquer coisa]), os pixels na borda superior são mapeados para ([qualquer coisa] / 0) e assim por diante .

Agora posso dizer ao Mathematica para encontrar coeficientes que minimizem essa função de erro. Eu posso fazer "palpites" sobre alguns dos coeficientes (por exemplo, o raio e o centro do frasco na imagem). Eu os uso como pontos de partida da otimização:

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

FindMinimumencontra valores para os 10 graus de liberdade da minha função de mapeamento que minimizam a função de erro. Combine o mapeamento genérico e esta solução e recebo um mapeamento das coordenadas da imagem X / Y, que se ajustam à área da etiqueta. Eu posso visualizar esse mapeamento usando a ContourPlotfunção do 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]]]

insira a descrição da imagem aqui

4. Transforme a imagem

Por fim, uso a ImageForwardTransformfunção do Mathematica para distorcer a imagem de acordo com este mapeamento:

ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]

Isso fornece os resultados como mostrado acima.

Versão assistida manualmente

O algoritmo acima é totalmente automático. Não são necessários ajustes. Funciona razoavelmente bem desde que a foto seja tirada de cima ou de baixo. Mas se for uma foto frontal, o raio do frasco não pode ser estimado a partir da forma do rótulo. Nesses casos, obtenho resultados muito melhores se eu permitir que o usuário insira as bordas esquerda / direita do jar manualmente e defina explicitamente os graus de liberdade correspondentes no mapeamento.

Este código permite que o usuário selecione as bordas esquerda / direita:

LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}], 
 Dynamic[Show[src, 
   Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}], 
     Line[{{xRight, 0}, {xRight, h}}]}]]]]

LocatorPane

Este é o código de otimização alternativo, onde o centro e o raio são dados explicitamente.

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]

11
Remove sunglasses ... mother of god ... #
Spacey

Você tem uma referência ao mapeamento cilíndrico? E talvez equações para o mapeamento inverso? @ niki-estner
Ita
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.