从图像中提取网格特征的大小
我正在处理一张图片。
这是原始图片:
我使用了一些技术,比如 Canny
边缘检测和 Hough
变换来找出图片中的线条,得到了这个结果:
输出结果:
现在我想提取网格单元中每一边的厚度信息。我需要计算特定部分的面积:
要找的部分:
你能帮我解答这个问题吗?
# Dilate the edge image to make the edges thicker
dialted_edge_image = cv2.dilate(edge_canny_image,kernel,iterations = 2)
# Perform a Hough Transform to detect lines
lines = probabilistic_hough_line(edge_canny_image, threshold=40, line_length=1, line_gap=2)
# Create a separate image for line detection
line_detected_image = np.dstack([edge_canny_image] * 3) # Convert to RGB for colored lines
for line in lines:
p0, p1 = line
cv2.line(line_detected_image, p0, p1, (255, 255, 255), 2) `
2 个回答
3
图像分割
第一个小问题是要把三种不同的结构分开:网格、缝隙和每个网格单元内的负空间。由于像素的强度分布几乎不重叠,可以通过多重Otsu阈值法来实现:
网格单元宽度
第二个小问题是确定网格单元的宽度。这里的方法是找到中间大负空间的面积,然后计算平方根来得到像素宽度:519.1 像素
网格线宽度
第三个小问题是找出网格线的宽度。基本思路是创建一个干净的网格线掩膜,对这个掩膜进行骨架化处理,然后把非零掩膜像素的数量除以骨架像素的数量。不过,骨架并不完美,特别是每条网格线的末端捕捉得不好。因此,我们分别处理每个可以识别的直线网格线段。具体步骤如下:
- 隔离并清理网格掩膜。
- 对网格掩膜进行骨架化处理。
- 将非零骨架掩膜像素转换为连接图,在这个图中,如果相应的非零像素相邻,则节点是连接的。
- 在连接图中找到所有链。
- 隔离与每个链对应的网格线段。
- 计算该线段中非零像素的数量,并除以链的长度,以确定网格线段的平均宽度:50.5 像素
缝隙宽度
最后一个小问题是找出缝隙的宽度。由于缝隙宽度是可变的,因此确定缝隙宽度的分布似乎最为有用。这里的方法是使用从最大网格单元的中心到其边缘的轮廓线,来确定每条轮廓线上的非零像素数量。
代码
import numpy as np
from matplotlib import pyplot as plt
import networkx as nx
from itertools import product
from skimage.io import imread
from skimage.filters import threshold_multiotsu
from skimage.morphology import (
binary_opening,
binary_closing,
skeletonize,
)
from skimage.measure import (
label,
find_contours,
centroid,
profile_line,
)
from skimage.draw import polygon2mask
def to_graph(skeleton_image, connectivity=8):
"""Convert a skeleton image to a networkx graph object."""
perpendicular_steps = set([(-1, 0), (1, 0), (0, -1), (0, 1)])
diagonal_steps = set([(-1, 1), (-1, -1), (1, -1), (1, 1)])
if connectivity == 8:
allowed_steps = perpendicular_steps.union(diagonal_steps)
elif connectivity == 4:
allowed_steps = perpendicular_steps
else:
raise ValueError(f"The parameter connectivity is either 4 or 8 not {connectivity}.")
nodes = list(zip(*np.where(skeleton_image)))
edges = [((x, y), (x+dx, y+dy)) for (x, y), (dx, dy) in product(nodes, allowed_steps) if (x+dx, y+dy) in nodes]
return nx.Graph(edges)
def get_orthogonal_unit_vector(v):
"""Determine the orthogonal unit vector to a given vector.
Parameters
----------
v : numpy.array
The input vector.
Returns
-------
w : numpy.array
The output vector.
Notes
-----
Adapted from https://stackoverflow.com/a/16890776/2912349
"""
v = np.atleast_2d(v)
if not np.all(np.isclose(v, 0)):
v = v / np.linalg.norm(v, axis=-1)[:, None] # unit vector
w = np.c_[-v[:,1], v[:,0]] # orthogonal vector
w = w / np.linalg.norm(w, axis=-1)[:, None] # orthogonal unit vector
return np.squeeze(w)
else:
raise ValueError("Cannot determine the orthogonal vector. Input vector has zero length.")
if __name__ == "__main__":
img = imread("~/wdir/tmp/grid.jpg")
bw = img.mean(axis=-1)
# --------------------------------------------------------------------------------
# image decomposition
fig, axes = plt.subplots(1, 3, figsize=(10, 5))
axes = axes.ravel()
axes[0].imshow(bw, cmap="gray")
axes[1].hist(bw.ravel(), bins=100)*2
thresholds = threshold_multiotsu(bw)
for threshold in thresholds:
axes[1].axvline(threshold, color="k", linestyle="--")
axes[1].set_xlabel("Pixel intensity")
axes[1].set_ylabel("Count")
regions = np.digitize(bw, bins=thresholds)
axes[2].imshow(regions, cmap="bwr")
axes[1].set_aspect("auto")
fig.tight_layout()
# --------------------------------------------------------------------------------
# determine grid cell width by finding the area of the largest negative space (LNS)
fig, axes = plt.subplots(1, 3, figsize=(10, 5))
axes = axes.ravel()
negative_space = regions < regions.max()
cleaned = binary_opening(negative_space, np.ones((25, 25), dtype=bool))
axes[0].imshow(cleaned, cmap="gray")
labelled = label(cleaned)
largest_negative_space_label = np.argmax(np.bincount(labelled.ravel())[1:]) + 1
largest_negative_space_mask = labelled == largest_negative_space_label
axes[1].imshow(largest_negative_space_mask, cmap="gray")
lns_contour = sorted(find_contours(largest_negative_space_mask), key=lambda x: len(x))[-1]
axes[2].imshow(bw, cmap="gray")
axes[2].plot(lns_contour[:, 1], lns_contour[:, 0], color="blue")
area = largest_negative_space_mask.sum()
interior_width = np.sqrt(area)
print(f"Grid cell interior width: {interior_width:.1f} pixels")
lns_centroid = centroid(largest_negative_space_mask)
row, col = lns_centroid
x = [col - interior_width/2, col + interior_width/2]
y = [row, row]
axes[2].plot(x, y, color="yellow")
fig.tight_layout()
# --------------------------------------------------------------------------------
# determine grid line width
fig, axes = plt.subplots(2, 2)
axes = axes.ravel()
axes[0].imshow(bw, cmap="gray")
mask_gridline = regions == regions.max()
axes[1].imshow(mask_gridline, cmap="gray")
# morphological cleaning
d = 20
selem = np.ones((d, d), dtype=bool)
clean_gridline = binary_opening(binary_closing(mask_gridline, selem), selem)
# skeletonize
skeleton = skeletonize(clean_gridline)
axes[2].imshow(clean_gridline.astype(int) + skeleton.astype(int), cmap="gray")
axes[3].imshow(clean_gridline.astype(int) + skeleton.astype(int), cmap="gray")
# get a first estimate of the width
area = clean_gridline.sum()
length = skeleton.sum()
estimate = area / length
# decompose skeleton into chains and estimate area of each chain
g = to_graph(skeleton)
chains = list(nx.connected_components(nx.subgraph(g, [node for node, degree in g.degree() if degree == 2])))
chain_widths = []
chain_lengths = []
for chain in chains:
if len(chain) > 100: # exclude short chains
# ensure nodes in chain are correctly ordered
start, stop = [node for node, degree in nx.subgraph(g, chain).degree() if degree == 1]
chain = nx.shortest_path(g, start, stop)
chain = np.array(list(chain))
# find a polygon enclosing the grid segment corresponding to the chain
start = chain[0]
stop = chain[-1]
delta = stop - start
v = get_orthogonal_unit_vector(np.atleast_2d(delta)) * (1.5 * estimate / 2)
polygon = np.array([start - v, start + v, stop + v, stop - v], dtype=int)
polygon_mask = polygon2mask(bw.shape, polygon)
# determine segment area and average width
area = clean_gridline[polygon_mask].sum()
length = np.linalg.norm(delta)
width = area / length
chain_widths.append(width)
chain_lengths.append(length)
# plot individual estimates
color = np.random.rand(3)
axes[3].plot(chain[:, 1], chain[:, 0], color=color)
axes[3].plot(np.r_[polygon[:, 1], polygon[0, 1]], np.r_[polygon[:, 0], polygon[0, 0]], color=color)
r, c = chain[int(len(chain) / 2)]
dr, dc = get_orthogonal_unit_vector(np.atleast_2d(delta)) * width / 2
axes[3].plot([c - dc, c + dc], [r - dr, r + dr], color=color)
# estimate mean width weighted by chain length
gridline_width = np.sum(np.array(chain_widths) * np.array(chain_lengths) / np.sum(chain_lengths).astype(float))
print(f"Grid line width: {gridline_width:.1f} pixels")
# --------------------------------------------------------------------------------
# determine seam widths
seam = regions == 1
fig, axes = plt.subplots(2, 2)
axes = axes.ravel()
axes[0].imshow(seam, cmap="gray")
# isolate seam within largest grid cell
clean_seam = seam.copy()
clean_seam[clean_gridline] = 0
clean_seam[~largest_negative_space_mask] = 0
axes[1].imshow(clean_seam, cmap="gray")
# walk around contour; determine seam widths
seam_thickness = []
src = lns_centroid
for ii, idx in enumerate(np.linspace(0, len(lns_contour), 72)[:-1]): # i.e. every 5 degrees
dst = lns_contour[int(idx)]
color = np.random.rand(3)
if (ii % 2) == 0: # i.e. every 10 degrees
axes[1].plot([src[1], dst[1]], [src[0], dst[0]], color=color)
y = profile_line(clean_seam, src, dst)
seam_thickness.append(np.sum(y))
axes[2].plot(y, color=color)
axes[2].set_ylabel("Intensity")
axes[2].set_xlabel("Line length")
axes[2].set_title("Example profile line")
axes[3].hist(seam_thickness)
axes[3].set_ylabel("Count")
axes[3].set_xlabel("Seam thickness [pixel]")
fig.tight_layout()
plt.show()
0
好的,我不太确定你需要哪些具体的计算,但我可以给你一个找到边缘厚度的方法。
为了确保将来的计算准确和稳定:
- 你需要确保每次不同样本的图像光线水平保持相对一致,这样 inRange 方法才能正确过滤。
- 我为每个边缘的厚度计算使用了特定的感兴趣区域(roi),你需要确保每个亮边缘都保持在对应的 roi 内,适用于不同的图像。
我会在亮边缘附近裁剪图像,由于厚度在边缘处是变化的,我会从不同的位置测量厚度,并计算出最小值、最大值和平均值。你可以选择最适合你需求的方法。
这是我的代码:
import cv2
def estimateThickness(img):
#Determine if it is a vertical or a horizantal edege
height,width = img.shape
if height<=width:
img = cv2.rotate(img,cv2.ROTATE_90_CLOCKWISE)
height,width = img.shape
#Estimate the thickness of sides from various locations
#and extract min max average thickness
thicknesess = []
for nh in range(height//10):
first,last = None,None
for nw in range(width):
# print(nl,ns)
#Find the first white pixel on the direction
if img[10*nh][nw] == 255 and first is None:
first = nw
#Find the last white pixel on the direction
if img[10*nh][width-nw-1] == 255 and last is None:
last = width-nw-1
if first is not None and last is not None:
thicknesess.append(last-first)
return max(thicknesess),min(thicknesess),sum(thicknesess)/len(thicknesess)
#Read the image
src_image = cv2.imread('img\grid.png')
gray = cv2.cvtColor(src_image,cv2.COLOR_BGR2GRAY)
#Extract the bright part in the image and filter the rest to measure thickness
bright_part = cv2.inRange(gray,110,255)
bright_part = cv2.morphologyEx(bright_part,cv2.MORPH_OPEN,cv2.getStructuringElement(cv2.MORPH_RECT,(3,3)))
bright_part = cv2.morphologyEx(bright_part,cv2.MORPH_CLOSE,cv2.getStructuringElement(cv2.MORPH_RECT,(15,15)))
#Crop top left bot and right edges from the filtered image
left_edge = bright_part[200:500,180:280]
right_edge = bright_part[200:500,750:850]
top_edge = bright_part[20:120,400:700]
bot_edge = bright_part[580:680,400:700]
#Use the defined function with cropped image
minL,maxL,avgL = estimateThickness(left_edge)
minR,maxR,avgR = estimateThickness(right_edge)
minT,maxT,avgT = estimateThickness(top_edge)
minB,maxB,avgB = estimateThickness(bot_edge)
print('L',minL,maxL,avgL)
print('R',minR,maxR,avgR)
print('T',minT,maxT,avgT)
print('B',minB,maxB,avgB)
cv2.imshow('L',left_edge)
cv2.imshow('R',right_edge)
cv2.imshow('T',top_edge)
cv2.imshow('B',bot_edge)
cv2.imshow('Bright Part',bright_part)
cv2.imshow('Source',src_image)
key = cv2.waitKey(0)
如果你能进一步解释其他计算,我可以尝试帮助你。