图像处理:55°英制螺纹与60°美制螺纹的识别与检测
0 背景
在实际生产中,要根据英制和美制螺纹的特点,妥善地处理2种螺纹的使用问题。在机械紧固的领域中,螺纹紧固是作为其中最具代表性的一种方式。由于其复杂的表面结构和多样化的参数,目前在大多数的工厂中只能采用传统的螺纹卡规和螺纹塞规进行测量,不能进行实 时的、高效的在线检测,严重影响了螺纹的加工效率。传统检测的方法,不仅检测速度较慢,检测工序较多,同时检测成本较高。因此本研究针对传统方法的低效耗时等缺点开发出一套基于图像处理技术来识别和检测英制螺纹与美制螺纹。
(1)美制螺纹介绍
美制统一螺纹是参照英制惠氏螺纹标准制定的,在直径、螺距系列和公差方面与英制惠氏螺纹很接近,但牙型角为60°,削平高度为Ⅱ/8,不同于英制惠氏螺纹(牙型角 55°,削平高度Ⅱ/6),目的则为避开惠氏螺纹圆弧牙顶和牙底的加工困难。标记方式: 公称直径+螺纹牙数+螺纹系列代号+公差系列代号+检验体系代号。例如: 3 /8-24UNF-2A ( 21 ) ,5 /16-28UN,3 /8-16UNC,其中UNF表示细螺纹,UNC表示粗螺纹,UN表示常规螺纹。螺纹示意图如下:
(2)英制螺纹介绍
起源于英制惠氏螺纹,按1 /16锥度关系(大约锥度1°47’),惠氏螺纹的径向直径公差转化为英制密封管螺纹的轴向牙数公差。英制管螺纹分英制密封管螺纹(Rc、Rp、R1、R2)和英制非密封管螺纹(G)。英制密封管螺纹为一般用途的密封管螺纹,使用时要在螺纹副中加入密封填料。英制管螺纹体系没有干密封管螺纹,其内螺纹有圆锥内螺纹(Rc)和圆柱内螺纹 (Rp)之分,外螺纹只有圆锥外螺纹,但分R1与Rp配合使用、R2与Rc配合使用。英制密封管螺纹是按ISO7-1生产的螺纹,等同于DIN 2999,BS21,JISB0203标准的螺纹。标记方式:螺纹特征代号+螺纹尺寸代号+旋向代号。例如:Rc3/4 LH。螺纹示意图如下:
技术方案
从上面的介绍可以看出,要想用图像处理技术来区分英制与美制螺纹,最直接的方式是通过测量螺纹的牙型角来进行识别。那么首先第一步要分割出单个螺牙,第二步对单个螺牙进行边缘检测,将边缘的像素点拟合出直线,第三步再进行两条直线夹角的计算,成功区分出55°角和60°角螺牙。本博客给出的具体技术方案如下:
1、图像采集
做任何视觉项目,图像数据很重要!!!螺纹图像的高质量采集是实现后续步骤的前提。由于螺纹很小,普通的镜头采集螺纹图像会产生畸变,所以后续检测肯定是不准确的。如何解决这个问题?答案就是换一个镜头,采用远心镜头就可以很好的解决图像畸变的问题。本项目采用的方案是远心镜头+逆光的方式,现场图如下:
采集的图像图如下,可以看出远心镜头采集的螺纹还是比较清晰的。大家如果不想采集,我已经给大家采集了40张左右的图片(英制与美制图片https://download.csdn.net/download/qq_31631311/90575307),大家可以自行下载,里面有干净的螺纹,也有带毛刺干扰的。希望大家可以在我工作的基础上开发出更好的识别算法。
2、螺牙分割
按照前面的技术方案,首先图像预处理:高斯滤波、Otsu 全局自适应阈值等处理成二值图像,处理结果如下:
第二步:进行图像腐蚀+保留最大连通域,这点是为了消除螺牙边缘附近的毛刺,防止毛刺太长,粘连两个相邻的螺牙,处理结果如下:
第三步:将图像进行垂直方向投影,分割出螺牙的顶部和底部,也就是y轴的坐标,处理结果如下:
第四步:将步骤二的结果进行水平投影,分割出单个螺牙的x轴坐标。这个处理需要寻找二值图像的峰值和谷值,然后更具谷值的坐标确定单个螺牙的x轴坐标。寻找谷值的算法,我们采用股票交易市场上寻找峰值的算法库findpeaks,处理结果如下:
再剔除异常值,就可以分割出单个螺纹:
至此,单个螺纹分割完成,完整代码如下:
import cv2
import numpy as np
# import math
import matplotlib.pyplot as plt
# from sklearn.linear_model import RANSACRegressor
# from skimage.measure import label, regionprops
from findpeaks import findpeaks
def luowen_dingwei(img):
"""
:param img: 输入的RGB图像
:return:
img_roi: 分割出单个螺纹的图像
y_zuobiao: 单个螺纹的y轴坐标
x_zuobiao: 单个螺纹的x轴坐
"""
# 灰度图像
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
# 应用 Otsu 全局自适应阈值
thresh_val, binary_img = cv2.threshold(
blurred,
0, # 初始阈值(Otsu 会忽略此值)
255, # 最大值
cv2.THRESH_BINARY + cv2.THRESH_OTSU # 阈值方法组合
)
binary_img = 255 - binary_img
plt.figure()
plt.imshow(binary_img)
plt.show()
# 创建水平直线型结构元素(3x10 的矩形,确保宽度为3、长度为10)
kernel = np.ones((3, 10), dtype=np.uint8)
# 执行腐蚀操作
eroded1 = cv2.erode(binary_img, kernel, iterations=1)
# 创建垂直直线型结构元素(10x3 的矩形,确保宽度为10、长度为3)
kernel2 = np.ones((10, 3), dtype=np.uint8)
# 执行腐蚀操作
eroded2 = cv2.erode(eroded1, kernel2, iterations=1)
# 进行连通组件分析 保留最大的连通域
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(eroded2, connectivity=8)
# 找到面积最大的连通域
max_area = 0
max_label = 1 # 初始化为第一个前景区域
for label in range(1, num_labels):
if stats[label, cv2.CC_STAT_AREA] > max_area:
max_area = stats[label, cv2.CC_STAT_AREA]
max_label = label
# 创建掩膜
mask = np.zeros_like(eroded2)
mask[labels == max_label] = 255
# plt.figure()
# plt.imshow(mask)
# plt.show()
size = gray.shape # 返回高和宽
h = size[0]
w = size[1]
# 图像水平投影
projection_Horizontal = np.zeros(h, dtype=np.int32)
for i in range(h):
projection_Horizontal[i] = np.count_nonzero(mask[i, :])
# 扣出螺纹的高度
a_max = max(projection_Horizontal)
a_min = min(projection_Horizontal)
for i in range(0, len(projection_Horizontal)):
if projection_Horizontal[i] != a_min:
h1 = i
break
for j in range(0, len(projection_Horizontal)):
if projection_Horizontal[j] == a_max:
h2 = j
break
img_roi1 = img[h1:(h2-20), :]
y_zuobiao = [h1, (h2-20)]
# plt.figure()
# plt.imshow(img_roi1)
# plt.show()
# 图像垂直投影
projection_Vertical = np.zeros(w, dtype=np.int32)
for i in range(w):
projection_Vertical[i] = np.count_nonzero(mask[:, i])
# 寻找图像垂直投影的峰值和谷值
# 初始化
fp = findpeaks(method='peakdetect', whitelist=['valley'], lookahead=200, interpolate=1)
# 拟合
results = fp.fit(projection_Vertical)
# 提取谷值位置
valley_positions = results['df']['x'][results['df']['valley'] == True].keys()
# # 可视化
# fp.plot()
img_roi = []
x_zuobiao = []
for i in range(len(valley_positions) - 1):
img2 = img_roi1[:, valley_positions[i]:valley_positions[i + 1]]
if (valley_positions[i + 1] - valley_positions[i] > 200 and valley_positions[i + 1] - valley_positions[i] < 800):
img_roi.append(img2)
x_zuobiao.append([valley_positions[i], valley_positions[i + 1]])
return img_roi, x_zuobiao, y_zuobiao
分割效果如图所示:
3、螺牙角测量
将分割出来的单个螺牙进行便边缘检测,根据检测出来的边缘点进行拟合。前面虽然对单个螺纹进行了分割,但那只是粗分割,要想根据螺纹边缘拟合出比较准确的直线,还是需要首先精准的分割,这步的图像预处理是为了将单个螺纹下面的螺纹根部去除。代码如下:
import cv2
import numpy as np
import matplotlib.pyplot as plt
from networkx.classes import edges
from skimage.measure import LineModelND, ransac
def img_process(img):
"""
对螺纹进行精确分割,使螺纹居中
:param img: 输入图像 RGB格式
:return: 预处理后的图像
"""
# 灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
# # 自适应阈值处理
# binary_img = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
# cv2.THRESH_BINARY_INV, 11, 2)
# 应用 Otsu 全局自适应阈值
thresh_val, binary_img = cv2.threshold(
blurred,
0, # 初始阈值(Otsu 会忽略此值)
255, # 最大值
cv2.THRESH_BINARY + cv2.THRESH_OTSU # 阈值方法组合
)
binary_img = 255 - binary_img
# 创建水平直线型结构元素(3x10 的矩形,确保宽度为3、长度为10)
kernel = np.ones((3, 10), dtype=np.uint8)
# 执行腐蚀操作
eroded1 = cv2.erode(binary_img, kernel, iterations=1)
# 创建垂直直线型结构元素(10x3 的矩形,确保宽度为10、长度为3)
kernel2 = np.ones((10, 3), dtype=np.uint8)
# 执行腐蚀操作
eroded2 = cv2.erode(eroded1, kernel2, iterations=1)
# 进行连通组件分析 保留最大的连通域
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(eroded2, connectivity=8)
# 找到面积最大的连通域
max_area = 0
max_label = 1 # 初始化为第一个前景区域
for label in range(1, num_labels):
if stats[label, cv2.CC_STAT_AREA] > max_area:
max_area = stats[label, cv2.CC_STAT_AREA]
max_label = label
# 创建掩膜
mask = np.zeros_like(eroded2)
mask[labels == max_label] = 255
# # 寻找轮廓并提取最大轮廓
# contours, _ = cv2.findContours(eroded2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# max_contour = max(contours, key=cv2.contourArea)
#
# # 创建掩膜以仅保留目标物体
# mask = np.zeros_like(eroded2)
# cv2.drawContours(mask, [max_contour], -1, 255, cv2.FILLED)
# plt.figure()
# plt.subplot(121), plt.imshow(eroded2), plt.title('eroded2')
# plt.subplot(122), plt.imshow(mask), plt.title('mask')
# plt.show()
# 膨胀操作
mask_dil = cv2.dilate(mask, kernel2, iterations=1)
mask_dil2 = cv2.dilate(mask_dil, kernel, iterations=1)
horizontal_projection = np.sum(mask_dil2, axis=0)
start, peak_pos, end = find_peak_boundaries(horizontal_projection, smooth_window=7, threshold_ratio=0.15)
print("峰值位置:", peak_pos)
print("每个峰值的开始和结束位置:", [start, end])
vertical_projection = np.sum(mask_dil2, axis=1)
arr = np.array(vertical_projection)
non_zero_indices = np.nonzero(arr)[0].tolist()
img3 = img[non_zero_indices[0]:, start:end, :]
# img2 = img[:, start:end, :]
# plt.figure()
# plt.subplot(221), plt.imshow(img), plt.title('Original img')
# plt.subplot(222), plt.imshow(mask_dil2), plt.title('mask_dil2')
# plt.subplot(223), plt.imshow(img2), plt.title('img_mask')
# plt.subplot(224), plt.imshow(img3), plt.title('Result graph')
# plt.show()
return img3
def find_peak_boundaries(data, smooth_window=11, threshold_ratio=0.01):
"""
参数说明:
data: 输入的一维时序数据
smooth_window: 数据平滑的滑动窗口大小(奇数)
threshold_ratio: 阈值比例(基于峰值高度的比例)
返回:
(start, peak_pos, end) 包含三个元素的元组
"""
# 数据平滑处理(消除小波动)
smoothed = np.convolve(data, np.ones(smooth_window) / smooth_window, mode='same')
# 确定基线水平(使用首尾各10%的数据)
baseline = np.mean(np.concatenate([smoothed[:len(data) // 10], smoothed[-len(data) // 10:]]))
# 找到全局最大峰值位置
peak_pos = np.argmax(smoothed)
peak_value = smoothed[peak_pos]
# 计算动态阈值(基于基线到峰顶的高度)
height_threshold = baseline + (peak_value - baseline) * threshold_ratio
# height_threshold = baseline
# print(height_threshold)
# 向前搜索起始点(从左往右找第一个超过阈值的位置)
start = 0
for i in range(peak_pos):
if smoothed[i] > height_threshold and \
all(smoothed[i:i + 3] > smoothed[i - 1] if i > 0 else True):
start = i
break
# 向后搜索结束点(从右往左找最后一个超过阈值的位置)
end = len(data) - 1
for i in range(len(data) - 1, peak_pos, -1):
if smoothed[i] > height_threshold and \
all(smoothed[i - 2:i + 1] > smoothed[i + 1] if i < len(data) - 1 else True):
end = i
break
# 二次校验:确保起始点前有上升趋势
while start > 0 and smoothed[start] <= smoothed[start - 1]:
start -= 1
# 二次校验:确保结束点后有下降趋势
while end < len(data) - 1 and smoothed[end] <= smoothed[end + 1]:
end += 1
return (start, peak_pos, end)
这两个函数的功能主要是寻找螺纹的开始位置和结束位置,除了螺纹的部分,其他的全部剪裁掉,这也可以进一步去除干扰,其实这步可以根据自己的数据情况看,如果前面分割的比较好,也可以不要,如果分割和我一样,效果比较差,还是增加上回比较好。具体效果如图所示:
下步就是图像二值化,边缘检测,提取螺纹两边的边缘点。注意:这时候有些螺纹边缘上又杂质或者毛刺干扰。我试了很多办法,都不能在不损失螺纹边缘像素的情况下,去除这些干扰,索性就不去除了,直接通过算法在边缘拟合的时候降低干扰,我对比了几种直线拟合的算法,还是使用ransac算法,抗干扰性比较好,所以提取螺纹两边的边缘点后,直接使用ransac算法拟合直线,然后再求两条直线的夹角。从而判断螺纹是60°还是55°。代码如下:
def detect_thread_edges(img):
"""
对单个螺纹图像进行边缘检测
:param img: 螺纹图像
:return: 边缘检测后的图像
"""
# 灰度化
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 自适应阈值处理
adaptive_thresh = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2)
# 形态学操作,去除小的噪声点
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_OPEN, kernel)
# Canny 边缘检测
edges = cv2.Canny(opening, 50, 150)
return edges
def separate_edges(edge_image):
"""
将边缘图像的边缘点聚类分成左右两个图像
:param edge_image: 边缘检测后的图像
:return: 左边边缘图像和右边边缘图像
"""
# 获取边缘点的坐标
rows, cols = np.where(edge_image != 0)
if len(rows) == 0:
# 如果没有边缘点,返回两个空白图像
height, width = edge_image.shape[:2]
return np.zeros((height, width), dtype=np.uint8), np.zeros((height, width), dtype=np.uint8)
# 计算边缘点的质心
centroid_x = int(np.mean(cols))
# 创建左右两个空白图像
height, width = edge_image.shape[:2]
left_edge_image = np.zeros((height, width), dtype=np.uint8)
right_edge_image = np.zeros((height, width), dtype=np.uint8)
# 遍历边缘点
for i in range(len(rows)):
y = rows[i]
x = cols[i]
if x < centroid_x:
left_edge_image[y, x] = 255
else:
right_edge_image[y, x] = 255
return left_edge_image, right_edge_image
def fit_line_using_ransac(binary_image, min_samples=10, residual_threshold=2, max_trials=100):
"""
使用 RANSAC 算法将二值图像中大部分成直线趋势的点拟合成直线
:param binary_image: 二值图像
:param min_samples: 拟合直线所需的最小样本数
:param residual_threshold: 点到拟合直线的最大允许残差(误差)
:param max_trials: RANSAC 算法的最大迭代次数
:return: 拟合直线的模型参数,内点的索引
"""
# 获取二值图像中非零像素点的坐标
rows, cols = np.where(binary_image != 0)
points = np.column_stack((cols, rows))
# 使用 RANSAC 算法拟合直线
model_robust, inliers = ransac(points, LineModelND, min_samples=min_samples,
residual_threshold=residual_threshold, max_trials=max_trials)
return model_robust, inliers
def line_calculations(model):
"""
计算直线的斜率和截距
:param model: 拟合直线的模型参数
:return: 斜率和截距
"""
# 计算拟合直线的斜率和截距
point_on_line = model.params[0]
direction_vector = model.params[1]
if direction_vector[0] != 0:
slope = direction_vector[1] / direction_vector[0]
intercept = point_on_line[1] - slope * point_on_line[0]
print(f"拟合直线的斜率: {slope}")
print(f"拟合直线的截距: {intercept}")
else:
print("拟合直线垂直于 x 轴,斜率不存在。")
return slope, intercept
程序处理效果如下:
可以看出,即时存在轻微干扰也可以比较准确的测量出螺纹的牙型角。这里同样给出完整代码:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# from networkx.classes import edges
from skimage.measure import LineModelND, ransac
def img_process(img):
"""
对螺纹进行精确分割,使螺纹居中
:param img: 输入图像 RGB格式
:return: 预处理后的图像
"""
# 灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 高斯滤波
blurred = cv2.GaussianBlur(gray, (11, 11), 0)
# # 自适应阈值处理
# binary_img = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
# cv2.THRESH_BINARY_INV, 11, 2)
# 应用 Otsu 全局自适应阈值
thresh_val, binary_img = cv2.threshold(
blurred,
0, # 初始阈值(Otsu 会忽略此值)
255, # 最大值
cv2.THRESH_BINARY + cv2.THRESH_OTSU # 阈值方法组合
)
binary_img = 255 - binary_img
# 创建水平直线型结构元素(3x10 的矩形,确保宽度为3、长度为10)
kernel = np.ones((3, 10), dtype=np.uint8)
# 执行腐蚀操作
eroded1 = cv2.erode(binary_img, kernel, iterations=1)
# 创建垂直直线型结构元素(10x3 的矩形,确保宽度为10、长度为3)
kernel2 = np.ones((10, 3), dtype=np.uint8)
# 执行腐蚀操作
eroded2 = cv2.erode(eroded1, kernel2, iterations=1)
# 进行连通组件分析 保留最大的连通域
num_labels, labels, stats, centroids = cv2.connectedComponentsWithStats(eroded2, connectivity=8)
# 找到面积最大的连通域
max_area = 0
max_label = 1 # 初始化为第一个前景区域
for label in range(1, num_labels):
if stats[label, cv2.CC_STAT_AREA] > max_area:
max_area = stats[label, cv2.CC_STAT_AREA]
max_label = label
# 创建掩膜
mask = np.zeros_like(eroded2)
mask[labels == max_label] = 255
# # 寻找轮廓并提取最大轮廓
# contours, _ = cv2.findContours(eroded2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# max_contour = max(contours, key=cv2.contourArea)
#
# # 创建掩膜以仅保留目标物体
# mask = np.zeros_like(eroded2)
# cv2.drawContours(mask, [max_contour], -1, 255, cv2.FILLED)
# plt.figure()
# plt.subplot(121), plt.imshow(eroded2), plt.title('eroded2')
# plt.subplot(122), plt.imshow(mask), plt.title('mask')
# plt.show()
# 膨胀操作
mask_dil = cv2.dilate(mask, kernel2, iterations=1)
mask_dil2 = cv2.dilate(mask_dil, kernel, iterations=1)
horizontal_projection = np.sum(mask_dil2, axis=0)
start, peak_pos, end = find_peak_boundaries(horizontal_projection, smooth_window=7, threshold_ratio=0.15)
print("峰值位置:", peak_pos)
print("每个峰值的开始和结束位置:", [start, end])
vertical_projection = np.sum(mask_dil2, axis=1)
arr = np.array(vertical_projection)
non_zero_indices = np.nonzero(arr)[0].tolist()
img3 = img[non_zero_indices[0]:, start:end, :]
# img2 = img[:, start:end, :]
# plt.figure()
# plt.subplot(221), plt.imshow(img), plt.title('Original img')
# plt.subplot(222), plt.imshow(mask_dil2), plt.title('mask_dil2')
# plt.subplot(223), plt.imshow(img2), plt.title('img_mask')
# plt.subplot(224), plt.imshow(img3), plt.title('Result graph')
# plt.show()
return img3
def find_peak_boundaries(data, smooth_window=11, threshold_ratio=0.01):
"""
参数说明:
data: 输入的一维时序数据
smooth_window: 数据平滑的滑动窗口大小(奇数)
threshold_ratio: 阈值比例(基于峰值高度的比例)
返回:
(start, peak_pos, end) 包含三个元素的元组
"""
# 数据平滑处理(消除小波动)
smoothed = np.convolve(data, np.ones(smooth_window) / smooth_window, mode='same')
# 确定基线水平(使用首尾各10%的数据)
baseline = np.mean(np.concatenate([smoothed[:len(data) // 10], smoothed[-len(data) // 10:]]))
# 找到全局最大峰值位置
peak_pos = np.argmax(smoothed)
peak_value = smoothed[peak_pos]
# 计算动态阈值(基于基线到峰顶的高度)
height_threshold = baseline + (peak_value - baseline) * threshold_ratio
# height_threshold = baseline
# print(height_threshold)
# 向前搜索起始点(从左往右找第一个超过阈值的位置)
start = 0
for i in range(peak_pos):
if smoothed[i] > height_threshold and \
all(smoothed[i:i + 3] > smoothed[i - 1] if i > 0 else True):
start = i
break
# 向后搜索结束点(从右往左找最后一个超过阈值的位置)
end = len(data) - 1
for i in range(len(data) - 1, peak_pos, -1):
if smoothed[i] > height_threshold and \
all(smoothed[i - 2:i + 1] > smoothed[i + 1] if i < len(data) - 1 else True):
end = i
break
# 二次校验:确保起始点前有上升趋势
while start > 0 and smoothed[start] <= smoothed[start - 1]:
start -= 1
# 二次校验:确保结束点后有下降趋势
while end < len(data) - 1 and smoothed[end] <= smoothed[end + 1]:
end += 1
return (start, peak_pos, end)
def detect_thread_edges(img):
"""
对单个螺纹图像进行边缘检测
:param img: 螺纹图像
:return: 边缘检测后的图像
"""
# 灰度化
image = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 自适应阈值处理
adaptive_thresh = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2)
# 形态学操作,去除小的噪声点
kernel = np.ones((3, 3), np.uint8)
opening = cv2.morphologyEx(adaptive_thresh, cv2.MORPH_OPEN, kernel)
# Canny 边缘检测
edges = cv2.Canny(opening, 50, 150)
return edges
def separate_edges(edge_image):
"""
将边缘图像的边缘点聚类分成左右两个图像
:param edge_image: 边缘检测后的图像
:return: 左边边缘图像和右边边缘图像
"""
# 获取边缘点的坐标
rows, cols = np.where(edge_image != 0)
if len(rows) == 0:
# 如果没有边缘点,返回两个空白图像
height, width = edge_image.shape[:2]
return np.zeros((height, width), dtype=np.uint8), np.zeros((height, width), dtype=np.uint8)
# 计算边缘点的质心
centroid_x = int(np.mean(cols))
# 创建左右两个空白图像
height, width = edge_image.shape[:2]
left_edge_image = np.zeros((height, width), dtype=np.uint8)
right_edge_image = np.zeros((height, width), dtype=np.uint8)
# 遍历边缘点
for i in range(len(rows)):
y = rows[i]
x = cols[i]
if x < centroid_x:
left_edge_image[y, x] = 255
else:
right_edge_image[y, x] = 255
return left_edge_image, right_edge_image
def fit_line_using_ransac(binary_image, min_samples=10, residual_threshold=2, max_trials=100):
"""
使用 RANSAC 算法将二值图像中大部分成直线趋势的点拟合成直线
:param binary_image: 二值图像
:param min_samples: 拟合直线所需的最小样本数
:param residual_threshold: 点到拟合直线的最大允许残差(误差)
:param max_trials: RANSAC 算法的最大迭代次数
:return: 拟合直线的模型参数,内点的索引
"""
# 获取二值图像中非零像素点的坐标
rows, cols = np.where(binary_image != 0)
points = np.column_stack((cols, rows))
# 使用 RANSAC 算法拟合直线
model_robust, inliers = ransac(points, LineModelND, min_samples=min_samples,
residual_threshold=residual_threshold, max_trials=max_trials)
return model_robust, inliers
def line_calculations(model):
"""
计算直线的斜率和截距
:param model: 拟合直线的模型参数
:return: 斜率和截距
"""
# 计算拟合直线的斜率和截距
point_on_line = model.params[0]
direction_vector = model.params[1]
if direction_vector[0] != 0:
slope = direction_vector[1] / direction_vector[0]
intercept = point_on_line[1] - slope * point_on_line[0]
print(f"拟合直线的斜率: {slope}")
print(f"拟合直线的截距: {intercept}")
else:
print("拟合直线垂直于 x 轴,斜率不存在。")
return slope, intercept
最后,我这里给出完整项目的代码,我将螺纹分割的代码保存成luowen_dingwei.py,把螺牙角测量的代码保存成jiaodu_celiang.py。然后再写个主函数(main.py)调用,主程序如下:
from luowen_dingwei import *
from jiaodu_celiang import *
img = cv2.imread('C:/Users/windows/Desktop/luowen/60/60_11.bmp')
img_roi, x_zuobiao, y_zuobiao = luowen_dingwei(img)
JIAODU = []
for i in range(0, len(img_roi)):
img_pro = img_process(img_roi[i])
# 进行边缘检测
edges = detect_thread_edges(img_pro)
# 分离左右边缘
left_edge, right_edge = separate_edges(edges)
# 进行左边直线拟合
left_model, left_inliers = fit_line_using_ransac(left_edge)
# 获取内点(大部分成直线趋势的点)
left_inlier_points = np.column_stack((np.where(left_edge != 0)[1], np.where(left_edge != 0)[0]))[left_inliers]
# 创建一个新的空白图像
left_new_image = np.zeros_like(left_edge)
# 将内点绘制到新图像上
for point in left_inlier_points:
left_new_image[point[1], point[0]] = 255
# 进行右边直线拟合
right_model, right_inliers = fit_line_using_ransac(right_edge)
# 获取内点(大部分成直线趋势的点)
right_inlier_points = np.column_stack((np.where(right_edge != 0)[1], np.where(right_edge != 0)[0]))[right_inliers]
# 创建一个新的空白图像
right_new_image = np.zeros_like(right_edge)
# 将内点绘制到新图像上
for point in right_inlier_points:
right_new_image[point[1], point[0]] = 255
# 计算左右边缘直线的斜率和截距
slope_left, intercept_left = line_calculations(left_model)
slope_right, intercept_right = line_calculations(right_model)
# 计算两条直线的夹角
if slope_left is not None and slope_right is not None:
angle = np.arctan(np.abs((slope_left - slope_right) / (1 + slope_left * slope_right)))
angle_degrees = np.rad2deg(angle)
print(f"两条直线的夹角(度): {angle_degrees}")
else:
print("无法计算夹角,因为至少有一条直线斜率不存在。")
angle_degrees = None
if angle_degrees > 50 and angle_degrees < 65:
JIAODU.append(angle_degrees)
print(JIAODU)
JIAODU_mean = np.sum(JIAODU) / len(JIAODU)
print(JIAODU_mean)
这个程序功能是读取完整螺纹图片,然后将螺纹分割成单个螺牙,对每个螺牙进行角度测量,因为我的项目只是区分55°和60°角的螺牙,所以我在最后增加了一个异常值去除机制,只要50°~65°范围的角度,然后求平均值,看平均值最靠近哪个角度,就判断是哪个角度的螺纹。
总结
这个项目从制定方案,到采集图像,设计算法,最后达到现在的效果,前前后后花了好几个月的时间。目前算法达到的效果是干净、无畸变的螺纹都能准确的识别并区分开来,那种有污渍、毛刺甚至形变的螺纹,区分效果还是差点。不过小的毛刺干扰还是可以做到准确区分的,那些大的干扰,大的毛刺,甚至是螺纹形变还是没办法处理,感觉是传统图像的限制,要想不损失螺牙边缘像素的前提下,去除大的干扰,还是做不到。也想过用深度学习的方法去做,直接语义分割出螺纹的边缘,然后拟合求直线夹角的方案。目前时间有限,先做到这种程度,后续等有空了再回头试一下语义分割的方式提取边缘,求夹角。本项目分享给大家,希望对你们的项目也有启发。