OpenCV - 检测特定宽度的组件
OpenCV - detect components of certain width
我有一张检测到组件的图像。由此我需要检测形成特定宽度 "polyline" 的组件(下图中的白色和红色)。
在 OpenCV 中什么算法最适合这个?我试过一个一个地分离所有组件并使用形态学操作,但那很慢而且不完全准确。
注意:下图经过降采样处理。原始图像的分辨率为 8K,边框粗细约为。 30-40px.
你可以试试这个:
- 将图像转换为双色。对象是白色的,边框是黑色的。
- 所有对象被侵蚀 15-20 像素。我们得到了标记。
- 带有标记的原始图像的形态学重建。您得到的图像没有窄线。
- 第 1 段和第 3 段按位异或。
我在评论中提到了这个概念。实现这一目标的一种不优雅的方法可能是这样的:
_, ctrs, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
out = np.zeros(img.shape[:2], dtype="uint8")
epsilon = 1.0
desired_width = 30.0
for i in range(len(ctrs)):
if(hierarchy[0][i][3] != -1):
continue
a = cv2.contourArea(ctrs[i])
p = cv2.arcLength(ctrs[i], True)
print(a, p)
if a != 0 and p != 0 and abs(a/((p-2*desired_width)/2) - desired_width) < epsilon:
cv2.drawContours(out, [ctrs[i]], -1, 255, -1)
一些参数可能需要根据 opencv 计算面积和周长的方式进行调整。
编辑:添加具有 4 条宽度为 14-16 像素的波浪线的测试图像。当然,与您正在处理的图像相比,这些太简单了。
我喜欢你的问题 - 它有点像线条而不是颗粒的粒度。
我的方法是在图像中找到独特的颜色,然后,对于每种颜色:
- 将该颜色隔离为黑底白字
- 反复腐蚀3个像素,直到什么都没有为止
请注意,下面 20-30% 的代码仅用于调试和解释,还可以通过多处理和一些调整来加快速度。
#!/usr/bin/env python3
import cv2
import numpy as np
from skimage.morphology import medial_axis, erosion, disk
def getColoursAndCounts(im):
"""Returns list of unique colours in an image and their counts."""
# Make a single 24-bit number for each pixel - it's faster
f = np.dot(im.astype(np.uint32), [1,256,65536])
# Count unique colours in image and how often they occur
colours, counts = np.unique(f, return_counts=1)
# Convert found colours back from 24-bit number to BGR
return np.dstack((colours&255,(colours>>8)&255,colours>>16)).reshape((-1,3)), counts
if __name__ == "__main__":
# Load image and get colours present and their counts
im = cv2.imread('classes_fs.png',cv2.IMREAD_COLOR)
colours, counts = getColoursAndCounts(im)
# Iterate over unique colours/classes - this could easily be multi-processed
for index, colour in enumerate(colours):
b, g, r = colour
count = counts[index]
print(f'DEBUG: Processing class {index}, colour ({b},{g},{r}), area {count}')
# Generate this class in white on a black background for processing
m = np.where(np.all(im==[colour], axis=-1), 255, 0).astype(np.uint8)
# Create debug image - can be omitted
cv2.imwrite(f'class-{index}.png', m)
# DEBUG only - show progression of erosion
out = m.copy()
# You could trim the excess black around the shape here to speed up morphology
# Erode, repeatedly with disk of radius 3 to determine line width
radius = 3
selem = disk(radius)
for j in range(1,7):
# Erode again, see what's left
m = erosion(m,selem)
c = cv2.countNonZero(m)
percRem = int(c*100/count)
print(f' Iteration: {j}, nonZero: {c}, %remaining: {percRem}')
# DEBUG only
out = np.hstack((out, m))
if c==0:
break
# DEBUG only
cv2.imwrite(f'erosion-{index}.png', out)
因此,图像中的 35 种独特颜色在分离后会产生这些 classes:
这是输出:
DEBUG: Processing class 0, colour (0,0,0), area 629800
Iteration: 1, nonZero: 390312, %remaining: 61
Iteration: 2, nonZero: 206418, %remaining: 32
Iteration: 3, nonZero: 123643, %remaining: 19
Iteration: 4, nonZero: 73434, %remaining: 11
Iteration: 5, nonZero: 40059, %remaining: 6
Iteration: 6, nonZero: 21975, %remaining: 3
DEBUG: Processing class 1, colour (10,14,0), area 5700
Iteration: 1, nonZero: 2024, %remaining: 35
Iteration: 2, nonZero: 38, %remaining: 0
Iteration: 3, nonZero: 3, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
...
...
DEBUG: Processing class 22, colour (174,41,180), area 3600
Iteration: 1, nonZero: 1501, %remaining: 41
Iteration: 2, nonZero: 222, %remaining: 6
Iteration: 3, nonZero: 17, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
DEBUG: Processing class 23, colour (241,11,185), area 200
Iteration: 1, nonZero: 56, %remaining: 28
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 24, colour (247,23,185), area 44800
Iteration: 1, nonZero: 38666, %remaining: 86
Iteration: 2, nonZero: 32982, %remaining: 73
Iteration: 3, nonZero: 27904, %remaining: 62
Iteration: 4, nonZero: 23364, %remaining: 52
Iteration: 5, nonZero: 19267, %remaining: 43
Iteration: 6, nonZero: 15718, %remaining: 35
DEBUG: Processing class 25, colour (165,142,185), area 33800
Iteration: 1, nonZero: 30506, %remaining: 90
Iteration: 2, nonZero: 27554, %remaining: 81
Iteration: 3, nonZero: 24970, %remaining: 73
Iteration: 4, nonZero: 22603, %remaining: 66
Iteration: 5, nonZero: 20351, %remaining: 60
Iteration: 6, nonZero: 18206, %remaining: 53
DEBUG: Processing class 26, colour (26,147,198), area 2100
Iteration: 1, nonZero: 913, %remaining: 43
Iteration: 2, nonZero: 152, %remaining: 7
Iteration: 3, nonZero: 12, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
DEBUG: Processing class 27, colour (190,39,199), area 18500
Iteration: 1, nonZero: 6265, %remaining: 33
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 28, colour (149,210,201), area 2200
Iteration: 1, nonZero: 598, %remaining: 27
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 29, colour (188,169,216), area 10700
Iteration: 1, nonZero: 9643, %remaining: 90
Iteration: 2, nonZero: 8664, %remaining: 80
Iteration: 3, nonZero: 7763, %remaining: 72
Iteration: 4, nonZero: 6932, %remaining: 64
Iteration: 5, nonZero: 6169, %remaining: 57
Iteration: 6, nonZero: 5460, %remaining: 51
DEBUG: Processing class 30, colour (100,126,217), area 5624300
Iteration: 1, nonZero: 5565713, %remaining: 98
Iteration: 2, nonZero: 5511150, %remaining: 97
Iteration: 3, nonZero: 5464286, %remaining: 97
Iteration: 4, nonZero: 5420125, %remaining: 96
Iteration: 5, nonZero: 5377851, %remaining: 95
Iteration: 6, nonZero: 5337091, %remaining: 94
DEBUG: Processing class 31, colour (68,238,237), area 2100
Iteration: 1, nonZero: 1446, %remaining: 68
Iteration: 2, nonZero: 922, %remaining: 43
Iteration: 3, nonZero: 589, %remaining: 28
Iteration: 4, nonZero: 336, %remaining: 16
Iteration: 5, nonZero: 151, %remaining: 7
Iteration: 6, nonZero: 38, %remaining: 1
DEBUG: Processing class 32, colour (131,228,240), area 4000
Iteration: 1, nonZero: 3358, %remaining: 83
Iteration: 2, nonZero: 2788, %remaining: 69
Iteration: 3, nonZero: 2290, %remaining: 57
Iteration: 4, nonZero: 1866, %remaining: 46
Iteration: 5, nonZero: 1490, %remaining: 37
Iteration: 6, nonZero: 1154, %remaining: 28
DEBUG: Processing class 33, colour (0,0,255), area 8500
Iteration: 1, nonZero: 6046, %remaining: 71
Iteration: 2, nonZero: 3906, %remaining: 45
Iteration: 3, nonZero: 2350, %remaining: 27
Iteration: 4, nonZero: 1119, %remaining: 13
Iteration: 5, nonZero: 194, %remaining: 2
Iteration: 6, nonZero: 18, %remaining: 0
DEBUG: Processing class 34, colour (255,255,255), area 154300
Iteration: 1, nonZero: 117393, %remaining: 76
Iteration: 2, nonZero: 82930, %remaining: 53
Iteration: 3, nonZero: 51625, %remaining: 33
Iteration: 4, nonZero: 24842, %remaining: 16
Iteration: 5, nonZero: 6967, %remaining: 4
Iteration: 6, nonZero: 2020, %remaining: 1
如果我们看一下 class 34 - 你感兴趣的那个。连续的侵蚀看起来像这样 - 你可以看到形状完全消失了大约 15 个像素的半径,这相当于失去 15 30 像素宽形状的左侧像素和右侧 15 像素:
如果绘制每次连续侵蚀后剩余像素的百分比,您可以很容易地看到 class 34 之间的差异,在 5-6 次侵蚀 3 像素(即 15-18像素)和 class 25 不存在的地方:
备注:
对于希望 运行 我的代码的任何人,请注意我使用 ImageMagick 将输入图像(最近邻重采样)放大到其当前大小的 10 倍:
magick classes.png -scale 1000%x classes_fs.png
我有一张检测到组件的图像。由此我需要检测形成特定宽度 "polyline" 的组件(下图中的白色和红色)。
在 OpenCV 中什么算法最适合这个?我试过一个一个地分离所有组件并使用形态学操作,但那很慢而且不完全准确。
注意:下图经过降采样处理。原始图像的分辨率为 8K,边框粗细约为。 30-40px.
你可以试试这个:
- 将图像转换为双色。对象是白色的,边框是黑色的。
- 所有对象被侵蚀 15-20 像素。我们得到了标记。
- 带有标记的原始图像的形态学重建。您得到的图像没有窄线。
- 第 1 段和第 3 段按位异或。
我在评论中提到了这个概念。实现这一目标的一种不优雅的方法可能是这样的:
_, ctrs, hierarchy = cv2.findContours(img, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
out = np.zeros(img.shape[:2], dtype="uint8")
epsilon = 1.0
desired_width = 30.0
for i in range(len(ctrs)):
if(hierarchy[0][i][3] != -1):
continue
a = cv2.contourArea(ctrs[i])
p = cv2.arcLength(ctrs[i], True)
print(a, p)
if a != 0 and p != 0 and abs(a/((p-2*desired_width)/2) - desired_width) < epsilon:
cv2.drawContours(out, [ctrs[i]], -1, 255, -1)
一些参数可能需要根据 opencv 计算面积和周长的方式进行调整。
编辑:添加具有 4 条宽度为 14-16 像素的波浪线的测试图像。当然,与您正在处理的图像相比,这些太简单了。
我喜欢你的问题 - 它有点像线条而不是颗粒的粒度。
我的方法是在图像中找到独特的颜色,然后,对于每种颜色:
- 将该颜色隔离为黑底白字
- 反复腐蚀3个像素,直到什么都没有为止
请注意,下面 20-30% 的代码仅用于调试和解释,还可以通过多处理和一些调整来加快速度。
#!/usr/bin/env python3
import cv2
import numpy as np
from skimage.morphology import medial_axis, erosion, disk
def getColoursAndCounts(im):
"""Returns list of unique colours in an image and their counts."""
# Make a single 24-bit number for each pixel - it's faster
f = np.dot(im.astype(np.uint32), [1,256,65536])
# Count unique colours in image and how often they occur
colours, counts = np.unique(f, return_counts=1)
# Convert found colours back from 24-bit number to BGR
return np.dstack((colours&255,(colours>>8)&255,colours>>16)).reshape((-1,3)), counts
if __name__ == "__main__":
# Load image and get colours present and their counts
im = cv2.imread('classes_fs.png',cv2.IMREAD_COLOR)
colours, counts = getColoursAndCounts(im)
# Iterate over unique colours/classes - this could easily be multi-processed
for index, colour in enumerate(colours):
b, g, r = colour
count = counts[index]
print(f'DEBUG: Processing class {index}, colour ({b},{g},{r}), area {count}')
# Generate this class in white on a black background for processing
m = np.where(np.all(im==[colour], axis=-1), 255, 0).astype(np.uint8)
# Create debug image - can be omitted
cv2.imwrite(f'class-{index}.png', m)
# DEBUG only - show progression of erosion
out = m.copy()
# You could trim the excess black around the shape here to speed up morphology
# Erode, repeatedly with disk of radius 3 to determine line width
radius = 3
selem = disk(radius)
for j in range(1,7):
# Erode again, see what's left
m = erosion(m,selem)
c = cv2.countNonZero(m)
percRem = int(c*100/count)
print(f' Iteration: {j}, nonZero: {c}, %remaining: {percRem}')
# DEBUG only
out = np.hstack((out, m))
if c==0:
break
# DEBUG only
cv2.imwrite(f'erosion-{index}.png', out)
因此,图像中的 35 种独特颜色在分离后会产生这些 classes:
这是输出:
DEBUG: Processing class 0, colour (0,0,0), area 629800
Iteration: 1, nonZero: 390312, %remaining: 61
Iteration: 2, nonZero: 206418, %remaining: 32
Iteration: 3, nonZero: 123643, %remaining: 19
Iteration: 4, nonZero: 73434, %remaining: 11
Iteration: 5, nonZero: 40059, %remaining: 6
Iteration: 6, nonZero: 21975, %remaining: 3
DEBUG: Processing class 1, colour (10,14,0), area 5700
Iteration: 1, nonZero: 2024, %remaining: 35
Iteration: 2, nonZero: 38, %remaining: 0
Iteration: 3, nonZero: 3, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
...
...
DEBUG: Processing class 22, colour (174,41,180), area 3600
Iteration: 1, nonZero: 1501, %remaining: 41
Iteration: 2, nonZero: 222, %remaining: 6
Iteration: 3, nonZero: 17, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
DEBUG: Processing class 23, colour (241,11,185), area 200
Iteration: 1, nonZero: 56, %remaining: 28
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 24, colour (247,23,185), area 44800
Iteration: 1, nonZero: 38666, %remaining: 86
Iteration: 2, nonZero: 32982, %remaining: 73
Iteration: 3, nonZero: 27904, %remaining: 62
Iteration: 4, nonZero: 23364, %remaining: 52
Iteration: 5, nonZero: 19267, %remaining: 43
Iteration: 6, nonZero: 15718, %remaining: 35
DEBUG: Processing class 25, colour (165,142,185), area 33800
Iteration: 1, nonZero: 30506, %remaining: 90
Iteration: 2, nonZero: 27554, %remaining: 81
Iteration: 3, nonZero: 24970, %remaining: 73
Iteration: 4, nonZero: 22603, %remaining: 66
Iteration: 5, nonZero: 20351, %remaining: 60
Iteration: 6, nonZero: 18206, %remaining: 53
DEBUG: Processing class 26, colour (26,147,198), area 2100
Iteration: 1, nonZero: 913, %remaining: 43
Iteration: 2, nonZero: 152, %remaining: 7
Iteration: 3, nonZero: 12, %remaining: 0
Iteration: 4, nonZero: 0, %remaining: 0
DEBUG: Processing class 27, colour (190,39,199), area 18500
Iteration: 1, nonZero: 6265, %remaining: 33
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 28, colour (149,210,201), area 2200
Iteration: 1, nonZero: 598, %remaining: 27
Iteration: 2, nonZero: 0, %remaining: 0
DEBUG: Processing class 29, colour (188,169,216), area 10700
Iteration: 1, nonZero: 9643, %remaining: 90
Iteration: 2, nonZero: 8664, %remaining: 80
Iteration: 3, nonZero: 7763, %remaining: 72
Iteration: 4, nonZero: 6932, %remaining: 64
Iteration: 5, nonZero: 6169, %remaining: 57
Iteration: 6, nonZero: 5460, %remaining: 51
DEBUG: Processing class 30, colour (100,126,217), area 5624300
Iteration: 1, nonZero: 5565713, %remaining: 98
Iteration: 2, nonZero: 5511150, %remaining: 97
Iteration: 3, nonZero: 5464286, %remaining: 97
Iteration: 4, nonZero: 5420125, %remaining: 96
Iteration: 5, nonZero: 5377851, %remaining: 95
Iteration: 6, nonZero: 5337091, %remaining: 94
DEBUG: Processing class 31, colour (68,238,237), area 2100
Iteration: 1, nonZero: 1446, %remaining: 68
Iteration: 2, nonZero: 922, %remaining: 43
Iteration: 3, nonZero: 589, %remaining: 28
Iteration: 4, nonZero: 336, %remaining: 16
Iteration: 5, nonZero: 151, %remaining: 7
Iteration: 6, nonZero: 38, %remaining: 1
DEBUG: Processing class 32, colour (131,228,240), area 4000
Iteration: 1, nonZero: 3358, %remaining: 83
Iteration: 2, nonZero: 2788, %remaining: 69
Iteration: 3, nonZero: 2290, %remaining: 57
Iteration: 4, nonZero: 1866, %remaining: 46
Iteration: 5, nonZero: 1490, %remaining: 37
Iteration: 6, nonZero: 1154, %remaining: 28
DEBUG: Processing class 33, colour (0,0,255), area 8500
Iteration: 1, nonZero: 6046, %remaining: 71
Iteration: 2, nonZero: 3906, %remaining: 45
Iteration: 3, nonZero: 2350, %remaining: 27
Iteration: 4, nonZero: 1119, %remaining: 13
Iteration: 5, nonZero: 194, %remaining: 2
Iteration: 6, nonZero: 18, %remaining: 0
DEBUG: Processing class 34, colour (255,255,255), area 154300
Iteration: 1, nonZero: 117393, %remaining: 76
Iteration: 2, nonZero: 82930, %remaining: 53
Iteration: 3, nonZero: 51625, %remaining: 33
Iteration: 4, nonZero: 24842, %remaining: 16
Iteration: 5, nonZero: 6967, %remaining: 4
Iteration: 6, nonZero: 2020, %remaining: 1
如果我们看一下 class 34 - 你感兴趣的那个。连续的侵蚀看起来像这样 - 你可以看到形状完全消失了大约 15 个像素的半径,这相当于失去 15 30 像素宽形状的左侧像素和右侧 15 像素:
如果绘制每次连续侵蚀后剩余像素的百分比,您可以很容易地看到 class 34 之间的差异,在 5-6 次侵蚀 3 像素(即 15-18像素)和 class 25 不存在的地方:
备注:
对于希望 运行 我的代码的任何人,请注意我使用 ImageMagick 将输入图像(最近邻重采样)放大到其当前大小的 10 倍:
magick classes.png -scale 1000%x classes_fs.png