在边缘处理中需要通过计算梯度来检测定位边缘,在Opencv中可以使用sobel算子来分别计算出水平和垂直方向的梯度,再通过梯度来计算出梯度幅值和方向。
sobel算子计算梯度原理:
sobel算子使用两个3*3的卷积核来计算水平和垂直方向的梯度,卷积核如下:
水平方向卷积核:
垂直方向卷积核:
计算过程如下:
对于给定的图像块 I:
水平方向梯度Gx计算:
垂直方向梯度Gy计算:
在计算完图像的X和Y方向的梯度值之后再根据公式计算梯度幅值
计算公式如下:
计算梯度方向:
梯度的应用:
梯度应用最常见的地方就在于对图像边缘的提取,下面我用opnecv来模拟实现canny的边缘轮廓提取。
import cv2
import numpy as np
def test_canny():
# 读取图像
image = cv2.imread(r'D:\wangtianqing\code\script\iou\data\front\front_1_1_1565.png')
# 转换为灰度图像
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯滤波器平滑图像
blurred = cv2.GaussianBlur(gray, (3, 3), 1.4)
# 使用Canny边缘检测
edges = cv2.Canny(blurred, threshold1=60, threshold2=130)
# 显示结果
cv2.imshow('Edges', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("canny_edges.jpg", edges)
def test_reproduce_canny():
# 读取图像
image = cv2.imread(r'D:\wangtianqing\code\script\iou\data\front\front_1_1_1565.png')
# 转换为灰度图像
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯滤波器平滑图像
blurred = cv2.GaussianBlur(gray, (3, 3), 1.4)
# cv2.imshow("gray", gray)
# cv2.imshow("blurred", blurred)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# 使用Sobel算子计算梯度
grad_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
# cv2.imshow("sobel-grad_x", grad_x)
# cv2.imshow("sobel-grad_y", grad_y)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# # 计算梯度幅值和方向
magnitude = np.sqrt(grad_x ** 2 + grad_y ** 2)
direction = np.arctan2(grad_y, grad_x)
# print(f"magnitude: {magnitude}, direction: {direction}")
# 获取梯度方向的角度(0-180度)
angle = np.degrees(direction)
# angle = direction * 180.0 / np.pi
angle[angle < 0] += 180
# 创建一个空的非极大值抑制后的图像
nms = np.zeros_like(magnitude, dtype=np.uint8)
for i in range(1, magnitude.shape[0] - 1):
for j in range(1, magnitude.shape[1] - 1):
q = 255
r = 255
ang = angle[i, j]
# Angle 0
if (0 <= ang < 22.5) or (157.5 <= ang <= 180):
q = magnitude[i, j + 1]
r = magnitude[i, j - 1]
# Angle 45
elif 22.5 <= ang < 67.5:
# q = magnitude[i + 1, j - 1]
# r = magnitude[i - 1, j + 1]
q = magnitude[i - 1, j - 1]
r = magnitude[i + 1, j + 1]
# Angle 90
elif 67.5 <= ang < 112.5:
q = magnitude[i + 1, j]
r = magnitude[i - 1, j]
# Angle 135
elif 112.5 <= ang < 157.5:
# q = magnitude[i - 1, j - 1]
# r = magnitude[i + 1, j + 1]
q = magnitude[i - 1, j + 1]
r = magnitude[i + 1, j - 1]
magt = magnitude[i, j]
if magt >= q and magt >= r:
nms[i, j] = magt
else:
nms[i, j] = 0
cv2.imshow('nms', nms)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 设置双阈值
low_threshold = 60
high_threshold = 130
# 创建一个空的双阈值图像
thresholded = np.zeros_like(nms, dtype=np.uint8)
# 强边缘
strong_i, strong_j = np.where(nms >= high_threshold)
# 弱边缘
weak_i, weak_j = np.where((nms <= high_threshold) & (nms >= low_threshold))
# 标记强边缘和弱边缘
thresholded[strong_i, strong_j] = 255
thresholded[weak_i, weak_j] = 50
cv2.imshow("StrongAndWeak", thresholded)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 获取图像大小
M, N = thresholded.shape
# extend_num = 1
# 连接边缘
for i in range(1, M - 1):
for j in range(1, N - 1):
if thresholded[i, j] == 50:
if (255 in thresholded[i - 1:i + 2, j - 1:j + 2]):
thresholded[i, j] = 255
else:
thresholded[i, j] = 0
# 最终边缘图像
edges = thresholded
# 显示结果
cv2.imshow('Edges', edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("Edges-0.jpg", edges)
if __name__ == '__main__':
# test_canny()
test_reproduce_canny()
运行结果:
一样的滤波值以及双阈值的canny算子运行结果:
以上与canny运行的结果来看效果差不少,canny的结果更加贴合实际。经调查自写的算法与canny算子实现上有以下差距:
- 实现细节的差异
OpenCV的Canny函数在内部有一些优化和细节处理,比如更精确的边缘连接和更高效的算法实现,这些细节可能在手动实现时容易被忽略或实现不当。
- 参数选择和调整
OpenCV的Canny函数在内部可能会对参数进行一些自动调整或优化,而手动实现时,参数选择不当可能导致结果不佳。特别是高斯滤波器的标准差、Sobel算子的核大小、以及高低阈值的选择,都需要仔细调节。
- 边缘连接的实现
双阈值处理阶段的弱边缘连接实现起来比较复杂,OpenCV在这一阶段可能有更智能的处理方法,从而获得更连贯的边缘。
- 非极大值抑制的精度
非极大值抑制阶段的实现也需要很高的精度,OpenCV可能在这一阶段有一些优化策略,而手动实现时可能会在梯度方向的计算或比较过程中引入误差。
改进方向:
- 调整参数
调节高斯滤波的参数,使得噪音更少,并且边缘轮廓保持清晰
调节双阈值参数,使得强和弱两种边缘轮廓更合理
调节sobel核大小,使得提取的梯度信息尽可能准确
2. 优化非极大值抑制
确保梯度方向的计算和比较过程准确无误,避免误差积累。
优化非极大值抑制代码,保留更多的局部最大值
3. 优化边缘连接策略
优化强弱边缘的连接策略。
优化后代码:
import cv2
import numpy as np
def non_max_suppression(magnitude, angle):
rows, cols = magnitude.shape
nms = np.zeros((rows, cols), dtype=np.float32)
angle = angle * 180.0 / np.pi # 将弧度转换为角度
angle[angle < 0] += 180
for i in range(1, rows - 1):
for j in range(1, cols - 1):
q = 255
r = 255
if (0 <= angle[i, j] < 22.5) or (157.5 <= angle[i, j] <= 180):
q = magnitude[i, j + 1]
r = magnitude[i, j - 1]
elif 22.5 <= angle[i, j] < 67.5:
q = magnitude[i + 1, j - 1]
r = magnitude[i - 1, j + 1]
elif 67.5 <= angle[i, j] < 112.5:
q = magnitude[i + 1, j]
r = magnitude[i - 1, j]
elif 112.5 <= angle[i, j] < 157.5:
q = magnitude[i - 1, j - 1]
r = magnitude[i + 1, j + 1]
if (magnitude[i, j] >= q) and (magnitude[i, j] >= r):
nms[i, j] = magnitude[i, j]
else:
nms[i, j] = 0
return nms
def double_threshold(nms, low_threshold, high_threshold):
strong_edges = (nms >= high_threshold).astype(np.uint8)
weak_edges = ((nms >= low_threshold) & (nms < high_threshold)).astype(np.uint8)
edges = np.zeros_like(nms, dtype=np.uint8)
edges[strong_edges == 1] = 255
edges[weak_edges == 1] = 75
rows, cols = edges.shape
for i in range(1, rows - 1):
for j in range(1, cols - 1):
if edges[i, j] == 75:
if (edges[i + 1, j - 1] == 255 or edges[i + 1, j] == 255 or edges[i + 1, j + 1] == 255
or edges[i, j - 1] == 255 or edges[i, j + 1] == 255
or edges[i - 1, j - 1] == 255 or edges[i - 1, j] == 255 or edges[i - 1, j + 1] == 255):
edges[i, j] = 255
else:
edges[i, j] = 0
return edges
# 读取图像并转换为灰度图像
image = cv2.imread(r'D:\wangtianqing\code\script\iou\data\front\front_1_1_1565.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
# 使用高斯滤波器平滑图像
blurred = cv2.GaussianBlur(gray, (5, 5), 1.4)
# 使用Sobel算子计算x方向和y方向上的梯度
grad_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
grad_y = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
# 计算梯度幅值和方向
magnitude = np.sqrt(grad_x**2 + grad_y**2)
angle = np.arctan2(grad_y, grad_x)
# 非极大值抑制
nms = non_max_suppression(magnitude, angle)
# 双阈值化
low_threshold = 50
high_threshold = 150
edges = double_threshold(nms, low_threshold, high_threshold)
# 使用OpenCV的Canny函数进行对比
opencv_edges = cv2.Canny(blurred, low_threshold, high_threshold)
# 显示结果
# cv2.imshow('Original Image', image)
# cv2.imshow('Gray Image', gray)
# cv2.imshow('Blurred Image', blurred)
cv2.imshow('Custom Canny Edges', edges)
cv2.imshow('OpenCV Canny Edges', opencv_edges)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("CustomEdges.jpg", edges)
cv2.imwrite("CannyEdges.jpg", opencv_edges)
对比优化前,轮廓的连接要好一些,整体上更贴近实际轮廓,但仍旧跟canny的轮廓提取有不小差距。
这里贴上测试用的原图: