У меня следующая проблема: мне нужно надежно обнаруживать значки на изображении. Изображение также содержит текст, а значки бывают разных размеров.
В настоящее время для этой задачи я использую Python с библиотекой cv2. Однако, к сожалению, текущий алгоритм обнаружения контуров с использованием cv2.findContours
не очень надежен. Вот как я сейчас это делаю:
gray = cv2.cvtColor(self.image, cv2.COLOR_BGR2GRAY)
binary = cv2.adaptiveThreshold(self.gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 17, 1)
contours, _ = cv2.findContours(self.binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Затем следует контурная фильтрация, объединение отфильтрованных контуров и повторная фильтрация.
Однако этот метод оказался ненадежным.
Я также пробовал использовать getStructuringElement
, но для значков на белом фоне это дает ненадежные результаты.
Я не могу раскрыть реальные входные данные, но я использовал логотип Amazon и создал пример, демонстрирующий проблему.
Для цветных иконок при использовании контуров я часто получаю две-три иконки неправильных размеров, а при их объединении теряется точный размер. Для значков на белом фоне подход с getStructuringElement
плохо определяет границу.
Мои вопросы: Что ты посоветуешь? Мои идеи:
Я открыт для любых предложений или дайте мне знать, если у кого-нибудь есть опыт решения такой проблемы.
Изображение: https://i.sstatic.net/bZYrnCTU.jpg
ПС: Для других людей, которых, возможно, заинтересует более или менее та же задача.
Ответ от @Tino-d действительно великолепен, Вы можете получить бота и иконку, которая более-менее будет разделена на большое количество простых многоугольников.
Например (после тщательной обработки)
Что вы можете сделать, так это просто нарисовать контуры большей толщины (например, 2), а затем заново создать контуры на новом изображении.
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
cv2.drawContours(edges, contours, -1, (255, 255, 255), 2)
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
Кажется более-менее супер-простым и очень быстрым алгоритмом слияния контуров)
Для повышения достоверности используйте данные контура для проверки площади контура. Значки кажутся больше по площади, чем текст, поэтому вы можете установить пороговое значение области, чтобы отфильтровать их, используя minAreaRectangle = cv2.minAreaRect(contour)
:
import cv2
import numpy as np
srcImg = cv2.imread("iconDetection.jpg")
grayImg = cv2.cvtColor(srcImg, cv2.COLOR_BGR2GRAY)
ret, bwImg = cv2.threshold( grayImg, 200, 255, cv2.THRESH_BINARY_INV)
cv2.imshow('Binary', bwImg) ; cv2.waitKey(0); cv2.destroyAllWindows()
Contours , _ = cv2.findContours(bwImg.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
BBoxes=[]
for contour in Contours:
minAreaRectangle = cv2.minAreaRect(contour)
bBox = cv2.boxPoints(minAreaRectangle)
if cv2.contourArea(bBox) > 60 : # exclude small objects
BBoxes += [ np.intp(bBox) ] # include only large areas
for bBox in BBoxes:
tgtImg = cv2.drawContours(srcImg ,[bBox],0,(0,255,0),3)
cv2.imshow('Bounding Rectangles', tgtImg) ; cv2.waitKey(0); cv2.destroyAllWindows()
дает:
@ fmw42: Я предполагаю, что minAreaRect охватывает больше случаев различных значков, где форма контура найденного значка по какой-то причине неправильная (например, это только крест). Почему вы считаете, что площади контура достаточно?
Если приведенные изображения репрезентативны, то потому, что они кажутся намного крупнее любого другого артефакта.
Да, я уже фильтрую, и иконки действительно крупнее всех остальных объектов(о смайлах здесь речь не идет). Но присмотритесь к второй иконке сверху; этот алгоритм не очень тщательно распознает его контур.
@VasiliyPlaton: см. мой другой ответ о другом подходе, не имеющем этого недостатка, по крайней мере, в случае предоставленного тестового изображения. Он менее тяжел в вычислениях и улавливает значки, внешний контур которых почти белый.
Я думаю, что обнаружение границ здесь может работать очень хорошо. Я сделал небольшой пример, который на данный момент работает:
im = cv2.imread("logos.jpg")
imGray = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(imGray,5, 20)
Это дало следующий результат:
После этого обнаружение контуров и фильтрация по площади будут работать очень хорошо, поскольку квадраты логотипов кажутся одинаковыми по размеру:
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
sortedContours = sorted(contours, key = cv2.contourArea, reverse = True)
for c in sortedContours:
print(cv2.contourArea(c))
Мы видим, что действительно три самых больших по площади контура имеют размер около 10500 пикселей:
10787.0
10715.0
10128.0
7391.5
4555.5
3539.0
3420.0
.
.
.
Заполните эти первые три контура:
im1 = cv2.drawContours(im.copy(), sortedContours, 2, (255,0,0), -1)
im2 = cv2.drawContours(im.copy(), sortedContours, 1, (0,255,0), -1)
im3 = cv2.drawContours(im.copy(), sortedContours, 0, (0,0,255), -1)
И вот что вы получите:
Я предполагаю, что вам нужна логическая маска, позволяющая получить эти пиксели. Итак, что-то вроде
mask = np.zeros_like(imGray)
mask = cv2.drawContours(mask, sortedContours, 2, 1, -1)
firstLogo = cv2.bitwise_and(im, im, mask = mask)
Может выполнить работу. Вы можете довольно легко автоматизировать это, отфильтровав контуры, я просто подталкиваю вас к POC.
E: простите за странные цвета, забыл преобразовать в RGB перед показом с помощью Matplotlib...
Протестировано Canny, WOW, работает с любыми данными как шарм. Не знаю, как вас отблагодарить, большое спасибо».
Вызвано комментарием ОП к другому вопросу, который использует расчет значения площади ограничивающих рамок для фильтрации значков из других обнаруженных объектов, здесь еще один подход, помогающий получить правильные контуры значков также в случае среднего значка на предоставленном изображении.
Основная идея такого рода обнаружения значков — фильтрация по размеру и форме обнаруженного объекта. Это позволяет использовать более широкий порог для захвата всех деталей значка, независимо от того, насколько тонким может быть внешний контур. При пороге 250 получаем:
как выглядит результат определения контура:
При таком наборе ограничивающих рамок фильтрация на больших площадях не даст правильных результатов, поэтому необходимо проанализировать форму и размер контуров и ограничить ее ожидаемым размером и формой значков.
Ниже показано, как выглядит результат при выборе диапазона ширины и высоты ограничивающих рамок найденных контуров от 105 до 120:
и код для получения этого результата:
import cv2
import numpy as np
srcImg = cv2.imread("iconDetection.jpg")
grayImg = cv2.cvtColor(srcImg, cv2.COLOR_BGR2GRAY)
ret, bwImg = cv2.threshold( grayImg, 250, 255, cv2.THRESH_BINARY_INV)
cv2.imshow('Binary', bwImg) ; cv2.waitKey(0); cv2.destroyAllWindows()
Contours , _ = cv2.findContours(bwImg.astype(np.uint8), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
BBoxes=[] # Bounding Boxes of found reaction fields on photographed strips
for contour in Contours:
x, y, w, h = cv2.boundingRect(contour)
#print(f"{x=}{y=}{w=}{h=}")
if w in range(105, 120) and h in range(105,120):
BBoxes += [ (x, y, w, h) ] # include only large areas
cv2.rectangle(srcImg, (x, y), (x + w, y + h), (0, 255, 0), 3)
cv2.imshow('Bounding Rectangles', srcImg) ; cv2.waitKey(0); cv2.destroyAllWindows()
Обратите внимание: если вы измените подход к использованию обнаружения краев Canny, это увеличит вычислительную нагрузку и потребует изменения диапазона высоты/ширины на ( 100 , 120 ), иначе логотип Amazon будет отсутствовать в найденных ограничивающих рамках. .
Ниже, как это будет выглядеть при замене:
ret, bwImg = cv2.threshold( grayImg, 250, 255, cv2.THRESH_BINARY_INV)
с
bwImg = cv2.Canny(grayImg, 5, 20)
без изменения диапазона размеров от (105, 120) до (100, 120):
Уже пользуюсь фильтрацией по размеру и соотношению сторон, спасибо!
Протестирован подход cv2.threshold. Вроде работает хорошо, поэтому держу это в уме и тестирую на большом объеме данных, напишу здесь, какой алгоритм работает наиболее надежно
Пороговый подход, конечно, гораздо быстрее, чем поиск ребер и последующее построение из них сложных контуров с вычислением площадей. Если это даст хорошие результаты, я полагаю, что это способ работы с большими объемами данных. Рад слышать, что вы дадите обратную связь... с нетерпением жду возможности прочитать об этом.
Зачем вам нужно получать область minAreaRect, если вы можете просто фильтровать область контура с помощью cv2.contourArea()