Как я могу получить точки внутреннего контура без избыточности в OpenCV - Python

Я новичок в OpenCV, и дело в том, что мне нужно получить все точки контура. Это простая установка режима cv2.RETR_TREE в методе findContours. Дело в том, что таким образом возвращаются избыточные координаты. Так, например, в этом полигоне я не хочу получать такие точки контура:

зеленый цвет - найденные контуры (3), красный цвет - найденные точки по этим контурам

Но вот так: введите здесь описание изображения

Итак, согласно первому изображению, зеленый цвет - это контуры, обнаруженные в режиме RETR_TREE, а точки 1-2, 3-5, 4-6, ... избыточны, потому что они очень близко друг к другу. Мне нужно собрать эти избыточные точки в одну и добавить ее в массив customContours. На данный момент у меня есть только код для первой картинки, устанавливающий расстояние между точками и координатами точек:

def getContours(img, minArea=20000, cThr=[100, 100]):
  font = cv2.FONT_HERSHEY_COMPLEX
  imgColor = img
  imgGray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
  imgBlur = cv2.GaussianBlur(imgGray, (5, 5), 1)
  imgCanny = cv2.Canny(imgBlur, cThr[0], cThr[1])
  kernel = np.ones((5, 5))
  imgDial = cv2.dilate(imgCanny, kernel, iterations=3)
  imgThre = cv2.erode(imgDial, kernel, iterations=2)
  cv2.imshow('threshold', imgThre)
  contours, hierachy = cv2.findContours(imgThre, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)

  customContours = []
  for cnt in contours:
    area = cv2.contourArea(cnt)
    if area > minArea:
        peri = cv2.arcLength(cnt, True)
        approx = cv2.approxPolyDP(cnt, 0.009*peri, True)
        bbox = cv2.boundingRect(approx)
        customContours.append([len(approx), area, approx, bbox, cnt])
        print('points: ', len(approx))
        n = approx.ravel()
        i = 0
        for j in n:
            if i % 2 == 0:
                x = n[i]
                y = n[i + 1]
                string = str(x)+" " + str(y)
                cv2.putText(imgColor, str(i//2+1) + ': ' + string, (x, y), font, 2, (0, 0, 0), 2)
            i = i + 1
  customContours = sorted(customContours, key=lambda x: x[1], reverse=True)
  for cnt in customContours:
    cv2.drawContours(imgColor, [cnt[2]], 0, (0, 0, 255), 5)
  return imgColor, customContours

Не могли бы вы помочь мне получить реальные очки относительно, например, второго изображения?

(ИЗМЕНИТЬ 07.01.21)

Мне нужно общее решение, потому что изображение может быть более сложным, например следующее: введите здесь описание изображения ПРИМЕЧАНИЕ: обратите внимание, что средняя стрелка (указывает на 17 и 18) не имеет замкнутой области, поэтому это не многоугольник для изучения. Тогда этот регион не заинтересован в получении его очков. Кроме того, обратите внимание, что порядок точек не важен, но если запись является изображением отверстия, она должна знать, что существует 4 многоугольника, поэтому для каждого многоугольника точки начинаются с 0, затем с 1 и т. д.


person Hamsi    schedule 30.06.2021    source источник
comment
Причина, по которой вы получаете внутренний контур, связана с детектором краев - он создает два края: первый - это первое пересечение нуля, когда интенсивность пикселя по измерению (по горизонтали или по вертикали) изменяется от 255 до 0. Второе ребро создается на пересечении второго нуля, от 0 до 255. Вы обнаруживаете края, чтобы обнаружить среднюю часть (точки 2–5 на последнем изображении) треугольника? Вас интересует только получение списка точек (от 1 до 4 на последнем изображении) или вам нужен полный контур?   -  person stateMachine    schedule 01.07.2021


Ответы (1)


Вот мой подход. В основном он основан на морфологии. Он включает в себя свертку изображения с помощью специального ядра. Эта свертка определяет конечные точки треугольника, а также точки пересечения, где присутствует средняя линия. В результате будет создана маска точек, содержащая пиксель, соответствующий искомым точкам. После этого мы можем применить немного морфологии, чтобы соединить возможные дублированные точки. Осталось получить список координат этих точек для дальнейшей обработки.

Вот шаги:

  1. Получите бинарное изображение ввода с помощью пороговой обработки Otsu.
  2. Получите скелет бинарного изображения.
  3. Определите специальное ядро и сверните изображение скелета.
  4. Примените морфологическое расширение, чтобы соединить возможные повторяющиеся точки.
  5. Получите центроиды точек и сохраните их в списке

Вот код:

# Imports:
import numpy as np
import cv2

# image path
path = "D://opencvImages//"
fileName = "triangle.png"

# Reading an image in default mode:
inputImage = cv2.imread(path + fileName)

# Prepare a deep copy for results:
inputImageCopy = inputImage.copy()

# Convert BGR to Grayscale
grayImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)

# Threshold via Otsu:
_, binaryImage = cv2.threshold(grayImage, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

Первый бит вычисляет двоичное изображение. Очень просто. Я использую это изображение в качестве основы, которое является просто очищенной версией того, что вы выложил без аннотаций. Это результирующее бинарное изображение:

Теперь, чтобы выполнить свертку, мы должны сначала получить скелет изображения. скелет — это версия двоичного изображения, в которой линии были нормализованы, чтобы иметь ширину 1 pixel. Это полезно, потому что мы можем затем свернуть изображение с ядром 3 x 3 и искать определенные шаблоны пикселей. Давайте вычислим скелет, используя модуль расширенной обработки изображений OpenCV:

# Get image skeleton:
skeleton = cv2.ximgproc.thinning(binaryImage, None, 1)

Вот такое изображение получилось:

Теперь мы можем применить свертку. Подход основан на информации Марка Сетчелла об этом запись. В посте в основном показан метод поиска конечных точек фигуры, но я расширил его, чтобы также идентифицировать пересечения линий, например среднюю часть треугольника. Основная идея заключается в том, что свертка дает очень конкретное значение, когда во входном изображении обнаруживаются шаблоны черных и белых пикселей. Обратитесь к посту за теорией этой идеи, но здесь мы ищем два значения: 110 и 40. Первый возникает, когда найдена конечная точка. Второй — при обнаружении пересечения линий. Настроим свертки:

# Threshold the image so that white pixels get a value of 0 and
# black pixels a value of 10:
_, binaryImage = cv2.threshold(skeleton, 128, 10, cv2.THRESH_BINARY)

# Set the convolution kernel:
h = np.array([[1, 1, 1],
              [1, 10, 1],
              [1, 1, 1]])

# Convolve the image with the kernel:
imgFiltered = cv2.filter2D(binaryImage, -1, h)

# Create list of thresholds:
thresh = [110, 40]

Первая часть сделана. Мы собираемся обнаружить конечные точки и пересечения в два отдельных шага. Каждый шаг будет давать частичный результат, мы можем OR оба результата получить окончательную маску:

# Prepare the final mask of points:
(height, width) = binaryImage.shape
pointsMask = np.zeros((height, width, 1), np.uint8)

# Perform convolution and create points mask:
for t in range(len(thresh)):
    # Get current threshold:
    currentThresh = thresh[t]
    # Locate the threshold in the filtered image:
    tempMat = np.where(imgFiltered == currentThresh, 255, 0)
    # Convert and shape the image to a uint8 height x width x channels
    # numpy array:
    tempMat = tempMat.astype(np.uint8)
    tempMat = tempMat.reshape(height,width,1)
    # Accumulate mask:
    pointsMask = cv2.bitwise_or(pointsMask, tempMat)

Это финальная маска точек:

Обратите внимание, что белые пиксели — это места, которые соответствуют нашим целевым шаблонам. Это те точки, которые мы ищем. Поскольку фигура не является идеальным треугольником, некоторые точки могут дублироваться. Мы можем объединить соседние капли, применив морфологическое расширение:

# Set kernel (structuring element) size:
kernelSize = 7
# Set operation iterations:
opIterations = 3
# Get the structuring element:
morphKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))

# Perform Dilate:
morphoImage = cv2.morphologyEx(pointsMask, cv2.MORPH_DILATE, morphKernel, None, None, opIterations, cv2.BORDER_REFLECT101)

Вот результат:

Очень хорошо, теперь у нас есть большие кластеры пикселей (или капли). Чтобы получить их координаты, одним из возможных подходов было бы получение ограничивающих прямоугольников этих контуров и вычисление их центроидов:

# Look for the outer contours (no children):
contours, _ = cv2.findContours(morphoImage, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# Store the points here:
pointsList = []

# Loop through the contours:
for i, c in enumerate(contours):

    # Get the contours bounding rectangle:
    boundRect = cv2.boundingRect(c)

    # Get the centroid of the rectangle:
    cx = int(boundRect[0] + 0.5 * boundRect[2])
    cy = int(boundRect[1] + 0.5 * boundRect[3])

    # Store centroid into list:
    pointsList.append( (cx,cy) )

    # Set centroid circle and text:
    color = (0, 0, 255)
    cv2.circle(inputImageCopy, (cx, cy), 3, color, -1)
    font = cv2.FONT_HERSHEY_COMPLEX
    string = str(cx) + ", " + str(cy)
    cv2.putText(inputImageCopy, str(i) + ':' + string, (cx, cy), font, 0.5, (255, 0, 0), 1)

    # Show image:
    cv2.imshow("Circles", inputImageCopy)
    cv2.waitKey(0)

Это точки, расположенные в исходном вводе:

Обратите также внимание, что я сохранил их координаты в списке pointsList:

# Print the list of points:
print(pointsList)

Это печатает центроиды как кортеж (centroidX, centroidY):

[(717, 971), (22, 960), (183, 587), (568, 586), (388, 98)]
person stateMachine    schedule 01.07.2021
comment
Отличный ответ !!! - person Yunus Temurlenk; 01.07.2021
comment
Привет, это потрясающе, но должно работать только в этом примере. Я хочу что-то общее, я установил треугольник со средней границей, но может быть что-то другое и сложное, проверьте редактирование. - person Hamsi; 01.07.2021