从图像中提取网格特征的大小

1 投票
2 回答
146 浏览
提问于 2025-04-14 17:45

我正在处理一张图片。

这是原始图片:

actualimage

我使用了一些技术,比如 Canny 边缘检测和 Hough 变换来找出图片中的线条,得到了这个结果:

输出结果:

transformed

现在我想提取网格单元中每一边的厚度信息。我需要计算特定部分的面积:

要找的部分:

tobefind

你能帮我解答这个问题吗?

# 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 像素

这里输入图片描述

网格线宽度

第三个小问题是找出网格线的宽度。基本思路是创建一个干净的网格线掩膜,对这个掩膜进行骨架化处理,然后把非零掩膜像素的数量除以骨架像素的数量。不过,骨架并不完美,特别是每条网格线的末端捕捉得不好。因此,我们分别处理每个可以识别的直线网格线段。具体步骤如下:

  1. 隔离并清理网格掩膜。
  2. 对网格掩膜进行骨架化处理。
  3. 将非零骨架掩膜像素转换为连接图,在这个图中,如果相应的非零像素相邻,则节点是连接的。
  4. 在连接图中找到所有链。
  5. 隔离与每个链对应的网格线段。
  6. 计算该线段中非零像素的数量,并除以链的长度,以确定网格线段的平均宽度: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

好的,我不太确定你需要哪些具体的计算,但我可以给你一个找到边缘厚度的方法。

为了确保将来的计算准确和稳定:

  1. 你需要确保每次不同样本的图像光线水平保持相对一致,这样 inRange 方法才能正确过滤。
  2. 我为每个边缘的厚度计算使用了特定的感兴趣区域(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)

如果你能进一步解释其他计算,我可以尝试帮助你。

撰写回答