Площадь замкнутого контура на графике с использованием python openCV

Я пытаюсь найти область внутри замкнутой кривой произвольной формы, построенной на питоне (пример изображения ниже). До сих пор я пытался использовать как метод альфа-формы, так и метод многоугольника, чтобы добиться этого, но оба потерпели неудачу. Теперь я пытаюсь использовать OpenCV и метод заливки для подсчета количества пикселей внутри кривой, а затем я позже преобразую это в область с учетом области, которую один пиксель охватывает на графике. Пример изображения: testplot.jpg

Для этого я делаю следующее, которое я адаптировал из другого поста об OpenCV.

import cv2
import numpy as np

# Input image
img = cv2.imread('testplot.jpg', cv2.IMREAD_GRAYSCALE)

# Dilate to better detect contours
temp = cv2.dilate(temp, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))

# Find largest contour
cnts, _ = cv2.findContours(255-temp, cv2.RETR_TREE , cv2.CHAIN_APPROX_NONE) #255-img and cv2.RETR_TREE is to account for how cv2 expects the background to be black, not white, so I convert the background to black.
largestCnt = [] #I expect this to yield the blue contour
for cnt in cnts:
    if (len(cnt) > len(largestCnt)):
        largestCnt = cnt

# Determine center of area of largest contour
M = cv2.moments(largestCnt)
x = int(M["m10"] / M["m00"])
y = int(M["m01"] / M["m00"])

# Initial mask for flood filling, should cover entire figure
width, height = temp.shape
mask = img2 = np.ones((width + 2, height + 2), np.uint8) * 255
mask[1:width, 1:height] = 0

# Generate intermediate image, draw largest contour onto it, flood fill this contour
temp = np.zeros(temp.shape, np.uint8)
temp = cv2.drawContours(temp, largestCnt, -1, 255, cv2.FILLED)
_, temp, mask, _ = cv2.floodFill(temp, mask, (x, y), 255)
temp = cv2.morphologyEx(temp, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))

area = cv2.countNonZero(temp) #Number of pixels encircled by blue line

Я ожидаю от этого получить то же самое изображение, что и выше, но с центром контура, заполненным белым, а фон и исходный синий контур черным. Я заканчиваю с этим:

result.jpg

Хотя на первый взгляд кажется, что это точно превратило область внутри контура в белый цвет, белая область на самом деле больше, чем область внутри контура, и поэтому результат, который я получаю, переоценивает количество пикселей внутри него. Любой вклад в это будет принят с благодарностью. Я новичок в OpenCV, поэтому, возможно, я что-то неправильно понял.

РЕДАКТИРОВАТЬ: благодаря комментарию ниже я внес некоторые изменения, и теперь это мой код с отмеченными изменениями:

import cv2
import numpy as np

# EDITED INPUT IMAGE: Input image
img = cv2.imread('testplot2.jpg', cv2.IMREAD_GRAYSCALE)

# EDIT: threshold
_, temp = cv2.threshold(img, 250, 255, cv2.THRESH_BINARY_INV)

# EDIT, REMOVED: Dilate to better detect contours

# Find largest contour
cnts, _ = cv2.findContours(temp, cv2.RETR_EXTERNAL , cv2.CHAIN_APPROX_NONE)
largestCnt = [] #I expect this to yield the blue contour
for cnt in cnts:
    if (len(cnt) > len(largestCnt)):
        largestCnt = cnt

# Determine center of area of largest contour
M = cv2.moments(largestCnt)
x = int(M["m10"] / M["m00"])
y = int(M["m01"] / M["m00"])


# Initial mask for flood filling, should cover entire figure
width, height = temp.shape
mask = img2 = np.ones((width + 2, height + 2), np.uint8) * 255
mask[1:width, 1:height] = 0

# Generate intermediate image, draw largest contour, flood filled
temp = np.zeros(temp.shape, np.uint8)
temp = cv2.drawContours(temp, largestCnt, -1, 255, cv2.FILLED)
_, temp, mask, _ = cv2.floodFill(temp, mask, (x, y), 255)
temp = cv2.morphologyEx(temp, cv2.MORPH_OPEN, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)))

area = cv2.countNonZero(temp) #Number of pixels encircled by blue line

Я ввожу другое изображение с осями и кадром, который python добавляет по умолчанию, удаленным для простота. Я получаю то, что ожидаю, на втором этапе, поэтому это изображение. Однако в поле введите здесь описание изображения как исходный контур, так и область, которую он окружает. сделать белым, тогда как я хочу, чтобы исходный контур был черным, и только область, которую он окружает, была белой. Как я могу этого добиться?


person Ariane    schedule 15.01.2021    source источник
comment
Мне кажется, что если вы конвертируете в оттенки серого и порог, чтобы кривая была белой, а внешняя часть черной, вы могли бы использовать cv2.RETR_EXTERNAL в findContours, а затем получить его cv2.contourArea. Контуры лучше всего работают с бинарными изображениями. Если это не сработает для вас, дайте мне знать, и я сам это закодирую.   -  person fmw42    schedule 16.01.2021
comment
Эй, спасибо за ваш ответ! Я немного изменил скрипт, но он пока не работает. Я отредактирую свой вопрос, чтобы вы видели, что я делаю сейчас, и результат, который я получаю.   -  person Ariane    schedule 16.01.2021
comment
Вам не нужны все моменты и заливка флуда. Вы можете просто получить контуры, а затем cv2.contour(area) должен дать вам то, что вы хотите. Но убедитесь, что после порога контур белый, а фон черный. Возможно, вам придется инвертировать пороговое изображение. Просмотрите его, чтобы убедиться. Если вам нужно увидеть его белым, заполненным черным, создайте новое черное изображение размером ввода, а затем нарисуйте на нем контур с белой заливкой, используя drawContours(), и установите толщину линии на -1.   -  person fmw42    schedule 17.01.2021


Ответы (2)


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

  1. Преобразуйте свое изображение в оттенки серого
  2. Примените пороговое значение Оцу, чтобы получить бинарное изображение, давайте работать только с черными и белыми пикселями.
  3. Примените первую операцию заливки к расположению изображения (0,0), чтобы избавиться от внешнего пробела.
  4. Отфильтровать небольшие капли с помощью фильтра по площади
  5. Найдите «Кривой холст» (пустое пространство, окружающее кривую), найдите и сохраните его начальную точку в (targetX, targetY).
  6. Примените вторую заливку к местоположению (targetX, targetY)
  7. Получите площадь изолированного большого двоичного объекта с помощью cv2.countNonZero

Давайте посмотрим на код:

import cv2
import numpy as np

# Set image path
path = "C:/opencvImages/"
fileName = "cLIjM.jpg"

# Read Input image
inputImage = cv2.imread(path+fileName)
inputCopy = inputImage.copy()

# Convert BGR to grayscale:
grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)

# Threshold via Otsu + bias adjustment:
threshValue, binaryImage = cv2.threshold(grayscaleImage, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)

Это двоичное изображение, которое вы получаете:

Теперь давайте flood-fill в углу, расположенном в (0,0), черным цветом, чтобы избавиться от первого пробела. Этот шаг очень прост:

# Flood-fill background, seed at (0,0) and use black color:
cv2.floodFill(binaryImage, None, (0, 0), 0)

Вот результат, обратите внимание, как исчезла первая большая белая область:

Давайте избавимся от маленьких пятен, применив фильтр области. Все, что ниже области 100, будет удалено:

# Perform an area filter on the binary blobs:
componentsNumber, labeledImage, componentStats, componentCentroids = \
cv2.connectedComponentsWithStats(binaryImage, connectivity=4)

# Set the minimum pixels for the area filter:
minArea = 100

# Get the indices/labels of the remaining components based on the area stat
# (skip the background component at index 0)
remainingComponentLabels = [i for i in range(1, componentsNumber) if componentStats[i][4] >= minArea]

# Filter the labeled pixels based on the remaining labels,
# assign pixel intensity to 255 (uint8) for the remaining pixels
filteredImage = np.where(np.isin(labeledImage, remainingComponentLabels) == True, 255, 0).astype('uint8')

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

Теперь осталась вторая белая область, мне нужно найти ее начальную точку, потому что я хочу применить вторую операцию flood-fill в этом месте. Я пройдусь по изображению, чтобы найти первый белый пиксель. Нравится:

# Get Image dimensions:
height, width = filteredImage.shape

# Store the flood-fill point here:
targetX = -1
targetY = -1

for i in range(0, width):
    for j in range(0, height):
        # Get current binary pixel:
        currentPixel = filteredImage[j, i]
        # Check if it is the first white pixel:
        if targetX == -1 and targetY == -1 and currentPixel == 255:
            targetX = i
            targetY = j

print("Flooding in X = "+str(targetX)+" Y: "+str(targetY))

Вероятно, есть более элегантный, Python-ориентированный способ сделать это, но я все еще изучаю язык. Не стесняйтесь улучшать сценарий (и делиться им здесь). Цикл, однако, дает мне местоположение первого белого пикселя, поэтому теперь я могу применить второй flood-fill в этом точном месте:

# Flood-fill background, seed at (targetX, targetY) and use black color:
cv2.floodFill(filteredImage, None, (targetX, targetY), 0)

Вы в конечном итоге с этим:

Как видите, просто подсчитайте количество ненулевых пикселей:

# Get the area of the target curve:
area = cv2.countNonZero(filteredImage)

print("Curve Area is: "+str(area))

Результат:

Curve Area is: 1510
person stateMachine    schedule 16.01.2021
comment
Вау, спасибо большое! Я попробую. Хороших выходных. РЕДАКТИРОВАТЬ: Это работает! Спасибо! - person Ariane; 16.01.2021
comment
@Ariane Нет проблем, мой друг. Рад, что смог помочь. - person stateMachine; 16.01.2021

Вот еще один подход с использованием Python/OpenCV.

  • Чтение ввода
  • преобразовать в цветовое пространство HSV
  • Порог по цветовому диапазону синего
  • Найдите самый большой контур
  • Получите его площадь и распечатайте это
  • нарисуйте контур как белый заполненный контур на черном фоне
  • Сохраните результаты

Вход:

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

import cv2
import numpy as np

# read image as grayscale
img = cv2.imread('closed_curve.jpg')

# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

#select blu color range in hsv
lower = (24,128,115)
upper = (164,255,255)

# threshold on blue in hsv
thresh = cv2.inRange(hsv, lower, upper)

# get largest contour
contours = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
contours = contours[0] if len(contours) == 2 else contours[1]
big_contour = max(contours, key=cv2.contourArea)
area = cv2.contourArea(c)
print("Area =",area)

# draw filled contour on black background
result = np.zeros_like(thresh)
cv2.drawContours(result, [c], -1, 255, cv2.FILLED)

# save result
cv2.imwrite("closed_curve_thresh.jpg", thresh)
cv2.imwrite("closed_curve_result.jpg", result)

# view result
cv2.imshow("threshold", thresh)
cv2.imshow("result", result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Пороговое изображение:

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

Результат заполнения контура на черном фоне:

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

Область Результат:

Площадь = 2347,0

person fmw42    schedule 17.01.2021
comment
Спасибо за вашу помощь! Как вы заполняли контур, когда рисовали его на изображении в конце? Я делаю то же, что и вы, только с заменой [c] на bigContour, потому что [c] выдал неопределенную ошибку. Кроме того, что такое с? Я не вижу, чтобы это было определено в вашем коде, и мне любопытно. Заранее спасибо за помощь! - person Ariane; 19.01.2021
comment
cv2.drawContours(result, [c], -1, 255, cv2.FILLED) Команда говорит cv2.FILLED. Это то, что заполняет контур. Вы можете использовать -1 для того же поля, которое является толщиной линии для контура. - person fmw42; 20.01.2021