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 é:
- Encontre o rótulo.
- Encontre as bordas do rótulo
- 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.
- Transforme a imagem usando este mapeamento
O algoritmo funciona apenas para imagens em que:
- o rótulo é mais brilhante que o plano de fundo (isso é necessário para a detecção do rótulo)
- o rótulo é retangular (usado para medir a qualidade de um mapeamento)
- o jar é (quase) vertical (isso é usado para manter a função de mapeamento simples)
- 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):
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):
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]]]
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]]]
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}}]];
Essa é uma pequena função auxiliar que encontra todos os pixels brancos em uma dessas quatro imagens e converte os índices em coordenadas ( Position
retorna í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. oClip
chamadas 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]]
FindMinimum
encontra 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 ContourPlot
funçã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]]]
4. Transforme a imagem
Por fim, uso a ImageForwardTransform
funçã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}}]}]]]]
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]