在 Mathematica.Stackexchange 上提出了类似的问题。我在那边的答案演变并最终变得很长,所以我将在这里总结一下算法。
抽象的
基本思想是:
- 找到标签。
- 找到标签的边界
- 找到一个将图像坐标映射到圆柱坐标的映射,以便将标签上边界的像素映射到 ([anything] / 0),将右边界的像素映射到 (1 / [anything]),依此类推。
- 使用此映射转换图像
该算法仅适用于以下图像:
- 标签比背景亮(这是标签检测所必需的)
- 标签是矩形的(用于衡量映射的质量)
- 罐子(几乎)是垂直的(这用于保持映射功能简单)
- 罐子是圆柱形的(这是为了保持映射功能简单)
但是,该算法是模块化的。至少在原则上,您可以编写自己的不需要深色背景的标签检测,或者您可以编写自己的质量测量函数来处理椭圆或八角形标签。
结果
这些图像是完全自动处理的,即算法获取源图像,工作几秒钟,然后显示映射(左)和未失真的图像(右):
接下来的图像是用算法的修改版本处理的,如果用户选择罐子的左右边界(不是标签),因为标签的曲率无法从正面拍摄的图像中估计(即全自动算法将返回略微失真的图像):
执行:
1.找到标签
标签在深色背景前是明亮的,所以我可以使用二值化轻松找到它:
src = Import["http://i.stack.imgur.com/rfNu7.png"];
binary = FillingTransform[DeleteBorderComponents[Binarize[src]]]
我只是选择最大的连接组件并假设这是标签:
labelMask = Image[SortBy[ComponentMeasurements[binary, {"Area", "Mask"}][[All, 2]], First][[-1, 2]]]
2.找到标签的边框
下一步:使用简单的导数卷积掩码找到上/下/左/右边界:
topBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1}, {-1}}]];
bottomBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1}, {1}}]];
leftBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{1, -1}}]];
rightBorder = DeleteSmallComponents[ImageConvolve[labelMask, {{-1, 1}}]];
这是一个小辅助函数,用于查找这四个图像之一中的所有白色像素并将索引转换为坐标(Position
返回索引,并且索引是基于 1 的 {y,x} 元组,其中 y=1 位于顶部图像。但是所有图像处理函数都需要坐标,它们是基于 0 的 {x,y}-元组,其中 y=0 是图像的底部):
{w, h} = ImageDimensions[topBorder];
maskToPoints = Function[mask, {#[[2]]-1, h - #[[1]]+1} & /@ Position[ImageData[mask], 1.]];
3. 找到从图像到圆柱坐标的映射
现在我有四个单独的标签顶部、底部、左侧、右侧边框坐标列表。我定义了从图像坐标到圆柱坐标的映射:
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]}]]
}
这是一个圆柱映射,将源图像中的 X/Y 坐标映射到圆柱坐标。映射对于高度/半径/中心/透视/倾斜有 10 个自由度。我使用泰勒级数来近似反正弦,因为我无法直接使用 ArcSin 进行优化。这Clip
调用是我在优化过程中防止复数的临时尝试。这里有一个权衡:一方面,函数应该尽可能接近精确的圆柱映射,以提供尽可能低的失真。另一方面,如果它太复杂,自动找到自由度的最佳值变得更加困难。(使用 Mathematica 进行图像处理的好处是您可以非常轻松地使用这样的数学模型,为不同的失真引入附加项并使用相同的优化函数来获得最终结果。我从来没有做过任何事情就像使用 OpenCV 或 Matlab 一样。但我从未尝试过 Matlab 的符号工具箱,也许这使它更有用。)
接下来我定义了一个“误差函数”来衡量图像的质量 -> 圆柱坐标映射。它只是边界像素的平方误差之和:
errorFunction =
Flatten[{
(mapping[#][[1]])^2 & /@ maskToPoints[leftBorder],
(mapping[#][[1]] - 1)^2 & /@ maskToPoints[rightBorder],
(mapping[#][[2]] - 1)^2 & /@ maskToPoints[topBorder],
(mapping[#][[2]])^2 & /@ maskToPoints[bottomBorder]
}];
此误差函数衡量映射的“质量”:如果左边框上的点映射到 (0 / [anything]),上边框上的像素映射到 ([anything] / 0) 等,则它是最低的.
现在我可以告诉 Mathematica 找到最小化这个误差函数的系数。我可以对一些系数(例如图像中罐子的半径和中心)做出“有根据的猜测”。我使用这些作为优化的起点:
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
找到最小化误差函数的映射函数的 10 个自由度的值。结合通用映射和这个解决方案,我从 X/Y 图像坐标中得到一个适合标签区域的映射。我可以使用 Mathematica 的ContourPlot
函数可视化这个映射:
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.变换图像
最后,我使用 Mathematica 的ImageForwardTransform
函数根据这个映射扭曲图像:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
这给出了如上所示的结果。
手动辅助版本
上面的算法是全自动的。无需调整。只要图片是从上方或下方拍摄的,它就可以很好地工作。但如果是正面照,罐子的半径就无法从标签的形状来估计。在这些情况下,如果我让用户手动输入 jar 的左/右边界,并在映射中明确设置相应的自由度,我会得到更好的结果。
此代码允许用户选择左/右边框:
LocatorPane[Dynamic[{{xLeft, y1}, {xRight, y2}}],
Dynamic[Show[src,
Graphics[{Red, Line[{{xLeft, 0}, {xLeft, h}}],
Line[{{xRight, 0}, {xRight, h}}]}]]]]
这是替代优化代码,其中明确给出了中心和半径。
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]