在质量控制的检测世界里,检测手段多种多样,除了传统的接触式检测,还有一些非接触式检测方法,比如使用工业相机,激光三角,白光共聚焦、光干涉、 TOF式传感器检测。
在工业相机的检测应用中,当我们选择的放大倍率大到可以清晰地看到单个像素时,便会呈现出十分独特的效果。
我们知道,像素是组成图像的最小单元,这些最小的单元可以携带灰度或者彩色信息,从而为我们呈现出丰富多彩、细节丰富的图像。从计算机内部处理的角度来看,图像实际上就是一个矩阵。这个矩阵中的每一个元素都对应着图像中的一个像素,并且存储着该像素的相关信息,如灰度值、颜色值等。我们可以通过opencv这个工具集去读取一幅照片然后打印输出
。
![](https://i-blog.csdnimg.cn/direct/92934a7dc3d543d69f721bff96d9e4cd.png)
imread(img_path,flag) 读取图片,返回图片对象
img_path: 图片的路径,即使路径错误也不会报错,但打印返回的图片对象为None
flag:cv2.IMREAD_COLOR,读取彩色图片,图片透明性会被忽略,为默认参数,也可以传入1
cv2.IMREAD_GRAYSCALE,按灰度模式读取图像,也可以传入0
cv2.IMREAD_UNCHANGED,读取图像,包括其alpha通道,也可以传入-1
import cv2
img = cv2.imread("./img/tool.png")
print(img)
输出的二维数组,可以把它看作一个矩阵
图像– 轮廓处理
每个像素都被分配了一个介于黑白之间的灰度值
![](https://i-blog.csdnimg.cn/direct/8ff14876d9424c71a9aa37f6eddbf13b.png)
![](https://i-blog.csdnimg.cn/direct/9b5be7e52c004b3fa2b5c112f7f0e8de.png)
我们模拟上面的划分也通过opencv实现
import cv2
def divide_image_and_draw(image_path, grid_size):
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
height, width = image.shape
rows = height // grid_size
cols = width // grid_size
for i in range(rows):
for j in range(cols):
# 计算当前格子的边界
start_x = j * grid_size
end_x = start_x + grid_size if start_x + grid_size < width else width
start_y = i * grid_size
end_y = start_y + grid_size if start_y + grid_size < height else height
# 提取当前格子的图像区域
grid_image = image[start_y:end_y, start_x:end_x]
# 计算平均灰度
avg_gray = int(grid_image.mean())
# 在格子上写平均灰度值
cv2.putText(image, str(avg_gray), (start_x + 5, start_y + 15), cv2.FONT_HERSHEY_SIMPLEX, 0.3, (0, 255, 0), 1)
# 画格子边框
cv2.rectangle(image, (start_x, start_y), (end_x - 1, end_y - 1), (0, 255, 0), 1)
# 显示处理后的图像
cv2.imshow('Average pixel', image)
cv2.waitKey(0)
cv2.destroyAllWindows()
image_path = "./img/image.png"
grid_size = 35
divide_image_and_draw(image_path, grid_size)
这里为了模拟像素的概念把图片划分为一小块一小块区域,每个区的灰度值取了这个格子灰度值的平均值。
在工件的边缘有所谓的灰度过渡。图像处理软件通过数字图像的灰度值和过渡信息来确定工件边缘上的单点。该分辨率优于照相机的像素分辨率(子像素化)
,允许进行干扰识别和过滤。
![](https://i-blog.csdnimg.cn/direct/f4701cf4f3f5427592c4b88730e6ba9f.png)
![](https://i-blog.csdnimg.cn/direct/ee6705b34157439594ea1b5099ba1751.png)
子像素轮廓上的单点被用来计算几何要素。
![](https://i-blog.csdnimg.cn/direct/d82ba470a0974acb8945d104656f3838.png)
![](https://i-blog.csdnimg.cn/direct/e1bfaa4b69254184b1e0182e3f80a52f.png)
镜头的放大倍率和精度关系
![](https://i-blog.csdnimg.cn/direct/c3c97d7b90f6424daabc9d974f68e770.png)
在更高的放大倍率下,测量会更精确,因为目标会有更高的分辨率。这使得一些其他情况下可能丢失的细节可以被测量。然而,放大倍率越高,视野区域越小。
![](https://i-blog.csdnimg.cn/direct/68a1ace749e341f28aac3172c7163ce7.png)
前面的基本概念交代完成,我们来一个实际提取边界的案例来完整整个实验,实验的要求是提取边界的点,当然在opencv中有许多函数可以直接用来提取边界的,比如:
-
Canny 算子:是一种非常流行且有效的边缘检测算法,它通过应用高斯平滑来减少噪声,然后计算梯度的幅度和方向,最后通过双阈值处理来确定边缘。
-
Sobel 算子:Sobel 算子是一种常用的梯度算子,它可以计算图像在水平和垂直方向上的梯度。通过对梯度图像进行阈值处理,可以得到图像的边界。
-
Laplacian 算子:Laplacian 算子是一种二阶导数算子,对噪声比较敏感,因此常需要配合高斯滤波一起使用。它可以检测图像中的边缘,并且对边缘的定位比较准确。
-
Roberts 交叉算子:Roberts 交叉算子是一种简单的梯度算子,它可以计算图像在对角线上的梯度。该算子对边缘的定位比较准确,但对噪声比较敏感。
-
Prewitt 算子:Prewitt 算子是一种类似于 Sobel 算子的梯度算子,它可以计算图像在水平和垂直方向上的梯度。与 Sobel 算子相比,Prewitt 算子对噪声的抑制能力更强。
这里我们尝试一下最常用的canny函数,
Canny边缘检测是一种经典的边缘检测算法,由John F.Canny在1986年提出。它被广泛应用于计算机视觉和图像处理领域,是一种多阶段的边缘检测算法,能够有效地检测图像中的边缘并抑制噪声。
Canny边缘检测的主要步骤如下:
-
噪声抑制:首先,通过使用高斯滤波器对图像进行平滑处理,以去除图像中的噪声。高斯滤波器可以有效地平滑图像,同时保持边缘的细节。
-
计算梯度幅值和方向:使用Sobel算子计算图像中每个像素点的水平和垂直方向的梯度值。然后,根据梯度值计算每个像素点的梯度幅值和方向。
-
非极大值抑制:在计算得到的梯度幅值图像上进行非极大值抑制。这一步的目的是将边缘宽度变窄,使得边缘更加细化和明确。
-
双阈值处理:根据设定的高阈值和低阈值,将梯度幅值图像中的像素点分为强边缘、弱边缘和非边缘三类。通常选择高阈值和低阈值使得强边缘像素点的梯 度幅值大于高阈值,非边缘像素点的梯度幅值小于低阈值,而弱边缘像素点的梯度幅值处于高阈值和低阈值之间。
-
边缘连接:最后,通过连接强边缘像素点和与之相邻的弱边缘像素点,得到完整的边缘图像。
下面用canny函数举例去提取轮廓边界:
import cv2
def calculate_average_gray(image_path):
# 读取图像
image = cv2.imread(image_path)
# 将图像转换为灰度图像
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 计算灰度平均值
average_gray = cv2.mean(gray_image)[0]
print(average_gray)
return average_gray
def detect_edges(image_path,l,h):
# 读取图像
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# 应用 Canny 边缘检测
edges = cv2.Canny(image, l,h)
# 显示结果
cv2.imshow('ORI',image)
#cv2.waitKey(0)
cv2.imshow('Edges', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
image_path = './img/image.png'
average = calculate_average_gray(image_path)
detect_edges(image_path,0,average)
但是这里我们返归事物的本源通过提取灰度边界上的点来拟合我们想要的元素,对于点的提取,基本思想是找寻一幅图相中灰度强度变化最强的位置。所谓变化最强,即指梯度方向。对于变化率而言就是取灰度的的一阶和二阶导数,也可以称为灰度梯度, 利用图像的灰度值最强梯度方向确定工件边缘上的单点的位置。但是导数通常对噪声来说太敏感因此必须采用滤波器来改善与噪声有关的边缘提取的性能。常见的滤波方法主要有高斯滤波,平均值、中值滤波等。
import cv2
def gaussian_filter(gray_image):
# 高斯滤波,ksize 为核的大小,sigmaX 为 X 方向的高斯核标准偏差
gaussian_filtered_image = cv2.GaussianBlur(gray_image, (5,5), 0)
return gaussian_filtered_image
def mean_filter(gray_image):
# 均值滤波,ksize 为核的大小
mean_filtered_image = cv2.blur(gray_image, (5, 5))
return mean_filtered_image
def median_filter(gray_image):
# 中值滤波,ksize 为核的大小
median_filtered_image = cv2.medianBlur(gray_image, 5)
return median_filtered_image
image = cv2.imread('./img/tool.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gaussian_filtered_image = gaussian_filter(gray_image)
mean_filtered_image = mean_filter(gray_image)
median_filtered_image = median_filter(gray_image)
cv2.imshow('ORI', gray_image)
cv2.imshow('gaussian_Filtered Image', gaussian_filtered_image)
cv2.imshow('mean_Filtered Image', mean_filtered_image)
cv2.imshow('median_Filtered Image', median_filtered_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
高斯滤波即采用离散化的高斯函数产生一组归一化的高斯核,然后基于高斯核函数对图像灰度矩阵的每一点进行加权求和。这个上面在介绍canny算子的时候已经提到,一般用高斯滤波来去除噪声,下面是常见的3X3的高斯卷积核模板:
其实就是根据待滤波的像素点及其邻域点的灰度值按照一定的参数规则进行加权平均。 对于取梯度问题这里我们用opencv的sobel函数,Sobel函数是一种离散微分算子,结合了高斯平滑和微分求导,能够检测图像中的水平和垂直边缘。符合我们上面提及的方法。对平滑后的图像使用Sobel算子计算水平方向和竖直方向的一阶导数[图像梯度。
sobel函数语法为:
dst=cv2.Sobel(src,ddepth,dx,dy(,ksize(,scale(,delta(,bordertype)))))
式中:
-
dst代表目标图像。
-
src代表原始图像。
-
ddepth代表输出图像的深度。
-
dx代表x方向上的求导阶数,dy代表y方向上的求导阶数。
-
- 在水平方向上计算一阶导数,即横向梯度。
-
- 在垂直方向上计算一阶导数,即纵向梯度。
-
ksize代表sobel核的大小。该值为-1时,则会使用Scharr算子进行运算。
-
scale代表计算导数值时所采用的缩放因子,默认情况下该值是1,是没有缩放的。
-
delta代表加在目标图像dst上的值,该值是可选的,默认为0。
-
bordertype代表边界样式。
例如,使用以下代码可以计算图像 `gray_image` 的水平和垂直方向上的一阶导数(梯度):
sobelx = cv2.Sobel(gray_image, cv2.CV_64F, 1, 0, ksize=5) # 计算水平方向上的梯度
sobely = cv2.Sobel(gray_image, cv2.CV_64F, 0, 1, ksize=5) # 计算垂直方向上的梯度
这将返回 `sobelx` 和 `sobely`,它们分别包含了图像在水平和垂直方向上的梯度值。
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.ndimage import gaussian_filter1d
# 初始化全局变量
drawing = False # 如果鼠标按下,则为真
point1 = ()
point2 = ()
max_gradient_points = [] # 用于存储最大梯度点
# 鼠标回调函数
def draw_line(event, x, y, flags, param):
global point1, point2, drawing, image, gray_image, max_gradient_points
if event == cv2.EVENT_LBUTTONDOWN:
drawing = True
point1 = (x, y)
elif event == cv2.EVENT_MOUSEMOVE:
if drawing:
temp_img = image.copy()
cv2.line(temp_img, point1, (x, y), (0, 0, 255), 1, cv2.LINE_AA)
cv2.imshow('image', temp_img)
elif event == cv2.EVENT_LBUTTONUP:
drawing = False
point2 = (x, y)
cv2.line(image, point1, point2, (0, 0, 255), 1, cv2.LINE_AA)
analyze_line()
elif event == cv2.EVENT_RBUTTONDOWN:
if max_gradient_points:
# 标记所有最大梯度点
for (mark_x, mark_y) in max_gradient_points:
cv2.circle(image, (mark_x, mark_y), 3, (255, 0, 0), -1)
cv2.imshow('image', image)
fit_and_draw_line()
max_gradient_points = [] # 重置最大梯度点
def analyze_line():
global image, gray_image, max_gradient_points
# 采样虚线上多个点的灰度值
num_samples = 300
x_vals = np.linspace(point1[0], point2[0], num_samples, dtype=np.int32)
y_vals = np.linspace(point1[1], point2[1], num_samples, dtype=np.int32)
gray_values = gray_image[y_vals, x_vals]
# 使用高斯滤波平滑灰度值
smoothed_gray_values = gaussian_filter1d(gray_values, sigma=2)
# 计算 Sobel 梯度
sobelx = cv2.Sobel(gray_image, cv2.CV_64F, 1, 0, ksize=5)
sobely = cv2.Sobel(gray_image, cv2.CV_64F, 0, 1, ksize=5)
gradient_magnitude = np.sqrt(sobelx**2 + sobely**2)
line_gradients = gradient_magnitude[y_vals, x_vals]
# 使用高斯滤波平滑灰度梯度值
smoothed_line_gradients = gaussian_filter1d(line_gradients, sigma=2)
# 找到梯度最高的点
max_grad_idx = np.argmax(line_gradients)
mark_x = x_vals[max_grad_idx]
mark_y = y_vals[max_grad_idx]
# 存储最大梯度点
max_gradient_points.append((mark_x, mark_y))
# 在图像上标记梯度最高的点
cv2.circle(image, (mark_x, mark_y), 1, (0, 255, 0), -1)
cv2.imshow('image', image)
# 在 Matplotlib 中标记点
plt.figure(figsize=(12, 6))
plt.subplot(2, 1, 1)
plt.plot(gray_values, label='Original Gray Values')
plt.plot(smoothed_gray_values, label='Smoothed Gray Values')
plt.ylabel('Gray Value')
plt.legend()
plt.title('Gray Value along the Line')
plt.subplot(2, 1, 2)
plt.plot(line_gradients, label='Gradient Magnitude')
plt.plot(smoothed_line_gradients, label='Smoothed Gray Values')
plt.xlabel('Sample Points')
plt.ylabel('Gradient Magnitude')
plt.legend()
plt.title('Gradient Magnitude along the Line')
plt.scatter(max_grad_idx, line_gradients[max_grad_idx], marker='x', color='red') # 使用 scatter 函数标记点
plt.show()
def fit_and_draw_line():
global image, max_gradient_points
# 从max_gradient_points中提取x和y坐标
if len(max_gradient_points) > 1:
x_coords = [p[0] for p in max_gradient_points]
y_coords = [p[1] for p in max_gradient_points]
# 拟合直线
fit = np.polyfit(x_coords, y_coords, 1)
fit_fn = np.poly1d(fit)
# 在图像上绘制拟合的直线
x_min, x_max = min(x_coords), max(x_coords)
cv2.line(image, (x_min, int(fit_fn(x_min))), (x_max, int(fit_fn(x_max))), (0, 255, 255), 2)
cv2.imshow('image', image)
# 读取图像
image = cv2.imread('./img/tool.png')
gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 创建一个窗口并绑定鼠标事件
cv2.namedWindow('image')
cv2.setMouseCallback('image', draw_line)
while True:
cv2.imshow('image', image)
if cv2.waitKey(1) & 0xFF == 27: # 按下 'ESC' 键退出
break
cv2.destroyAllWindows()