Аналогичный вопрос был задан на Mathematica.Stackexchange . Мой ответ там развивался и в итоге получился довольно длинным, поэтому я кратко изложу алгоритм здесь.
Аннотация
Основная идея:
- Найдите этикетку.
- Найдите границы метки
- Найдите отображение, которое отображает координаты изображения на координаты цилиндра, чтобы оно отображало пиксели вдоль верхней границы метки на ([что угодно] / 0), пиксели вдоль правой границы на (1 / [что угодно]) и так далее.
- Преобразуйте изображение, используя это отображение
Алгоритм работает только для изображений, где:
- метка ярче фона (это необходимо для определения метки)
- метка прямоугольная (используется для измерения качества отображения)
- банка (почти) вертикальна (это используется для упрощения функции отображения)
- банка является цилиндрической (это используется для упрощения функции отображения)
Тем не менее, алгоритм является модульным. По крайней мере, в принципе, вы можете написать собственное обнаружение меток, которое не требует темного фона, или вы можете написать свою собственную функцию измерения качества, которая может справиться с эллиптическими или восьмиугольными метками.
Полученные результаты
Эти изображения были обработаны полностью автоматически, то есть алгоритм берет исходное изображение, работает в течение нескольких секунд, затем показывает отображение (слева) и неискаженное изображение (справа):
Следующие изображения были обработаны с помощью модифицированной версии алгоритма, в которой пользователь выбирает левую и правую границы банки (не метки), поскольку кривизна метки не может быть оценена по изображению во фронтальном снимке (т.е. полностью автоматический алгоритм будет возвращать изображения, которые слегка искажены):
Реализация:
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 / [что угодно]), пиксели в верхней границе отображаются в ([что угодно] / 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, которое соответствует области метки. Я могу визуализировать это отображение, используя ContourPlot
функцию 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. Преобразуйте изображение
Наконец, я использую ImageForwardTransform
функцию Mathematica для искажения изображения в соответствии с этим отображением:
ImageForwardTransformation[src, mapping[#] /. solution &, {400, 300}, DataRange -> Full, PlotRange -> {{0, 1}, {0, 1}}]
Это дает результаты, как показано выше.
Версия с ручным управлением
Алгоритм выше полностью автоматический. Никаких корректировок не требуется. Это работает достаточно хорошо, если изображение сделано сверху или снизу. Но если это фронтальный выстрел, радиус банки нельзя оценить по форме этикетки. В этих случаях я получаю гораздо лучшие результаты, если я позволю пользователю вручную ввести левую / правую границу банки и явно установлю соответствующие степени свободы в отображении.
Этот код позволяет пользователю выбрать левую / правую границы:
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]