Se hizo una pregunta similar en Mathematica.Stackexchange . Mi respuesta allí evolucionó y se hizo bastante larga al final, así que resumiré el algoritmo aquí.
Abstracto
La idea básica es:
- Encuentra la etiqueta.
- Encuentra los bordes de la etiqueta
- Busque una asignación que asigne las coordenadas de la imagen a las coordenadas del cilindro de modo que asigne los píxeles a lo largo del borde superior de la etiqueta a ([cualquier cosa] / 0), los píxeles a lo largo del borde derecho a (1 / [cualquier cosa]) y así sucesivamente.
- Transforma la imagen usando este mapeo
El algoritmo solo funciona para imágenes donde:
- la etiqueta es más brillante que el fondo (esto es necesario para la detección de la etiqueta)
- la etiqueta es rectangular (esto se usa para medir la calidad de un mapeo)
- el frasco es (casi) vertical (esto se usa para mantener la función de mapeo simple)
- el frasco es cilíndrico (esto se usa para mantener la función de mapeo simple)
Sin embargo, el algoritmo es modular. Al menos en principio, podría escribir su propia detección de etiquetas que no requiera un fondo oscuro, o podría escribir su propia función de medición de calidad que pueda hacer frente a etiquetas elípticas u octogonales.
Resultados
Estas imágenes se procesaron de forma totalmente automática, es decir, el algoritmo toma la imagen de origen, funciona durante unos segundos y luego muestra el mapeo (izquierda) y la imagen sin distorsión (derecha):
Las siguientes imágenes se procesaron con una versión modificada del algoritmo, donde el usuario selecciona los bordes izquierdo y derecho del frasco (no la etiqueta), porque la curvatura de la etiqueta no puede estimarse a partir de la imagen en una toma frontal (es decir, el algoritmo completamente automático devolvería imágenes que están ligeramente distorsionadas):
Implementación:
1. Encuentra la etiqueta
La etiqueta es brillante frente a un fondo oscuro, por lo que puedo encontrarla fácilmente usando la binarización:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
Simplemente elijo el componente conectado más grande y asumo que esa es la etiqueta:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2. Encuentra los bordes de la etiqueta
Siguiente paso: encuentre los bordes superior / inferior / izquierdo / derecho utilizando máscaras de convolución 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}}]];
Esta es una pequeña función auxiliar que encuentra todos los píxeles blancos en una de estas cuatro imágenes y convierte los índices en coordenadas ( Position
devuelve índices, y los índices están basados en 1 {y, x} -tuplas, donde y = 1 está en la parte superior de la imagen, pero todas las funciones de procesamiento de imágenes esperan coordenadas, que son tuplas {x, y} basadas en 0, donde y = 0 es la parte inferior de la imagen):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. Encuentre un mapeo de la imagen a las coordenadas del cilindro
Ahora tengo cuatro listas separadas de coordenadas de los bordes superior, inferior, izquierdo y derecho de la etiqueta. Defino una asignación de coordenadas de imagen a coordenadas de 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]}]]
}
Esta es una asignación cilíndrica, que asigna coordenadas X / Y en la imagen de origen a coordenadas cilíndricas. El mapeo tiene 10 grados de libertad para altura / radio / centro / perspectiva / inclinación. Utilicé la serie Taylor para aproximar el seno del arco, porque no pude lograr que la optimización funcionara directamente con ArcSin. losClip
Las llamadas son mi intento ad-hoc para evitar números complejos durante la optimización. Aquí hay una compensación: por un lado, la función debe estar lo más cerca posible de un mapeo cilíndrico exacto, para dar la menor distorsión posible. Por otro lado, si es demasiado complicado, se vuelve mucho más difícil encontrar valores óptimos para los grados de libertad automáticamente. (Lo bueno de procesar imágenes con Mathematica es que puedes jugar con modelos matemáticos como este muy fácilmente, introducir términos adicionales para diferentes distorsiones y usar las mismas funciones de optimización para obtener resultados finales. Nunca he podido hacer nada así usando OpenCV o Matlab. Pero nunca probé la caja de herramientas simbólica para Matlab, quizás eso lo haga más útil).
A continuación defino una "función de error" que mide la calidad de una imagen -> mapeo de coordenadas de cilindro. Es solo la suma de los errores al cuadrado de los píxeles del borde:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
Esta función de error mide la "calidad" de una asignación: es más baja si los puntos en el borde izquierdo se asignan a (0 / [cualquier cosa]), los píxeles en el borde superior se asignan a ([cualquier cosa] / 0) y así sucesivamente .
Ahora puedo decirle a Mathematica que busque coeficientes que minimicen esta función de error. Puedo hacer "conjeturas educadas" sobre algunos de los coeficientes (por ejemplo, el radio y el centro del frasco en la imagen). Los uso como puntos de partida de la optimización:
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
encuentra valores para los 10 grados de libertad de mi función de mapeo que minimizan la función de error. Combino el mapeo genérico y esta solución y obtengo un mapeo de coordenadas de imagen X / Y, que se ajusta al área de la etiqueta. Puedo visualizar este mapeo usando la ContourPlot
función de 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. Transforma la imagen
Finalmente, uso la ImageForwardTransform
función de Mathematica para distorsionar la imagen de acuerdo con este mapeo:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Eso da los resultados como se muestra arriba.
Versión asistida manualmente
El algoritmo anterior es completamente automático. No se requieren ajustes. Funciona razonablemente bien siempre que la imagen se tome desde arriba o desde abajo. Pero si se trata de un disparo frontal, el radio del frasco no puede estimarse a partir de la forma de la etiqueta. En estos casos, obtengo resultados mucho mejores si dejo que el usuario ingrese los bordes izquierdo / derecho del frasco manualmente y establezca los grados de libertad correspondientes en el mapeo explícitamente.
Este código le permite al usuario seleccionar los bordes izquierdo / derecho:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
Este es el código de optimización alternativo, donde el centro y el radio se dan explícitamente.
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]