使用 python openCV 绘制的封闭等高线面积

Area of a closed contour on a plot using python openCV

我试图找到 python 中绘制的任意形状闭合曲线内的区域(下图示例)。到目前为止,我已经尝试使用 alphashape 和多边形方法来实现这一点,但都失败了。我现在正尝试使用 OpenCV 和 floodfill 方法来计算曲线内的像素数量,然后我稍后会将其转换为一个区域,给定单个像素在图上所包围的区域。 示例图像: testplot.jpg

为了做到这一点,我正在做以下事情,我改编自另一个关于 OpenCV 的 post。

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

我输入了一个不同的 image with the axes and the frame that python adds by default removed for ease. I get what I expect at the second step, so this image. However, in the enter image description here 原来的轮廓和它包围的区域似乎都变成了白色,而我想要原来的轮廓是黑色的,只有它包围的区域是白色的。我该如何实现?

问题出在你最后的 opening 操作上。这个形态学操作在末尾包含一个 dilation 来扩展白色轮廓,增加它的面积。让我们尝试一种不涉及形态学的不同方法。这些是步骤:

  1. 将图像转换为灰度
  2. 应用Otsu 的阈值 得到二值图像,让我们只处理黑白像素。
  3. 在图像位置 (0,0) 应用第一个 flood-fill 操作以去除外部白色 space.
  4. 使用区域过滤器过滤 小斑点
  5. 找到“曲线Canvas”(包围曲线的白色space)并将其起点定位并存储在(targetX, targetY)
  6. 应用第二个 flood-fill al location (targetX, targetY)
  7. cv2.countNonZero
  8. 得到孤立 blob 的 面积

我们来看看代码:

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) 的拐角处用黑色去掉第一个白色 space。这一步非常简单:

# 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

这是另一种使用 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