【问题标题】:Area of a closed contour on a plot using python openCV使用 python openCV 在绘图上的闭合轮廓区域
【发布时间】:2021-01-15 23:28:33
【问题描述】:

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

我输入了一个不同的image,为方便起见,python 默认添加的轴和框架被删除。我在第二步得到了我的期望,所以this image。但是,在enter image description here 中,原始轮廓和它包围的区域似乎都变成了白色,而我希望原始轮廓是黑色的,只有它包围的区域是白色的。我怎样才能做到这一点?

【问题讨论】:

  • 在我看来,如果您转换为灰度和阈值,使曲线为白色,外部为黑色,您应该能够在 findContours 中使用 cv2.RETR_EXTERNAL,然后获取其 cv2.contourArea。轮廓最适合二值图像。如果这对您不起作用,请告诉我,我将自己编写代码。
  • 您好,谢谢您的回答!我稍微修改了脚本,但它似乎还没有工作。我将编辑我的问题,以便您了解我现在在做什么以及我得到的结果
  • 你不需要所有的时刻和洪水填充。你可以简单地得到轮廓,然后 cv2.contour(area) 应该给你你想要的。但请确保在阈值后轮廓为白色,背景为黑色。您可能需要反转阈值图像。查看它以确定。如果您需要将其视为黑色填充的白色,则创建一个输入大小的新黑色图像,然后使用 drawContours() 在其上绘制白色填充轮廓并将线条粗细设置为 -1。

标签: python opencv contour


【解决方案1】:

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

  1. 将您的图像转换为灰度
  2. 应用 Otsu 的阈值处理 来获得 二值图像,让我们只使用黑白像素。
  3. 在图像位置 (0,0) 应用第一个 flood-fill 操作以消除外部空白。
  4. 使用区域过滤器过滤小斑点
  5. 找到“曲线画布”(包围曲线的空白区域)并将其起点定位并存储在(targetX, targetY)
  6. 应用第二个flood-fill位置(targetX, targetY)
  7. 使用cv2.countNonZero 获取隔离 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) 的角落用黑色去除第一个空白。这一步非常简单:

# 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

【讨论】:

  • 哇,非常感谢!我会试一试。周末愉快。编辑:它的工作!谢谢!
  • @Ariane 没问题,我的朋友。很高兴我能帮上忙。
【解决方案2】:

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

【讨论】:

  • 感谢您的帮助!当你把它画到最后的图像上时,你是如何填充轮廓的?我正在做你正在做的事情,只是将 [c] 替换为 bigContour,因为 [c] 引发了未定义的错误。还有,什么是c?我没有在您的代码中看到它,我很好奇。提前感谢您的帮助!
  • cv2.drawContours(result, [c], -1, 255, cv2.FILLED) 命令显示 cv2.FILLED。这就是填充轮廓的东西。您可以将 -1 用于同一字段,即轮廓线的粗细。
猜你喜欢
  • 1970-01-01
  • 2012-01-12
  • 2015-02-02
  • 2020-06-24
  • 1970-01-01
  • 2015-06-24
  • 2023-03-18
  • 1970-01-01
  • 2014-09-28
相关资源
最近更新 更多