1. Предыстория проблемы
На бумаге есть серебряная монета в один доллар, вы можетеCanny
иHough
найти его координатное уравнение с помощью ?
Чтобы определить уравнение координат круга, сначала нам нужно обнаружить его край, а затем найти его относительное положение и радиус на бумаге.
В этой статье мы используемCanny
Алгоритмы обнаружения краев серебряных монет на бумаге.
2. Хитрый алгоритм
Canny можно использовать для получения края объекта на изображении, шаги следующие:
- Гауссово сглаживание
- Рассчитать градиент изображения (зафиксировать его интенсивность, направление)
- немаксимальное подавление
- Отслеживание запаздывающих краев
После выполнения вышеуказанных четырех шагов эффект рендеринга извлечения края монеты на бумаге, который мы получили, выглядит следующим образом.
(1), сглаживание по Гауссу
class GaussianSmoothingNet(nn.Module):
def __init__(self) -> None:
super(GaussianSmoothingNet, self).__init__()
filter_size = 5
# shape为(1, 5), 方差为 1.0 的高斯滤波核
generated_filters = gaussian(filter_size,std=1.0).reshape([1,filter_size])
# GFH(V): gaussian filter of horizontal(vertical) 水平(竖直)方向的高斯滤波核
self.GFH = nn.Conv2d(1, 1, kernel_size=(1,filter_size), padding=(0,filter_size//2))
self.GFV = nn.Conv2d(1, 1, kernel_size=(filter_size,1), padding=(filter_size//2,0))
# 设置 w 的值为 高斯平滑核, b 的值为 0.0
init_parameter(self.GFH, generated_filters, np.array([0.0]))
init_parameter(self.GFV, generated_filters.T, np.array([0.0]))
def forward(self, img):
img_r = img[:,0:1] # 取出RGB三个通道的数据
img_g = img[:,1:2]
img_b = img[:,2:3]
# 对图片的三个通道进行水平、垂直滤波
blurred_img_r = self.GFV(self.GFH(img_r))
blurred_img_g = self.GFV(self.GFH(img_g))
blurred_img_b = self.GFV(self.GFH(img_b))
# 合并成一张图
blurred_img = torch.stack([blurred_img_r, blurred_img_g, blurred_img_b], dim=1)
blurred_img = torch.stack([torch.squeeze(blurred_img)])
return blurred_img
Изображение после сглаживания по Гауссу (размытие) более размыто, чем исходное изображение, как показано на серебряной монете в правой части рисунка ниже.
Полный код смотрите по адресу:gaussian_smoothing
(2) Оператор Собеля вычисляет градиент
PAI = 3.1415926
class SobelFilterNet(nn.Module):
def __init__(self) -> None:
super(SobelFilterNet, self).__init__()
sobel_filter = np.array([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]])
self.SFH = nn.Conv2d(1, 1, kernel_size=sobel_filter.shape, padding=sobel_filter.shape[0]//2)
self.SFV = nn.Conv2d(1, 1, kernel_size=sobel_filter.shape, padding=sobel_filter.shape[0]//2)
init_parameter(self.SFH, sobel_filter, np.array([0.0]))
init_parameter(self.SFV, sobel_filter.T, np.array([0.0]))
def forward(self, img):
img_r = img[:,0:1]
img_g = img[:,1:2]
img_b = img[:,2:3]
# # SFH(V): sobel filter of horizontal(vertical) 水平(竖直)方向的Sobel滤波
grad_r_x = self.SFH(img_r) # 通道 R 的 x 方向梯度
grad_r_y = self.SFV(img_r)
grad_g_x = self.SFH(img_g)
grad_g_y = self.SFV(img_g)
grad_b_x = self.SFH(img_b)
grad_b_y = self.SFV(img_b)
# 计算强度(magnitude) 和 方向(orientation)
magnitude_r = torch.sqrt(grad_r_x**2 + grad_r_y**2) # Gr^2 = Grx^2 + Gry^2
magnitude_g = torch.sqrt(grad_g_x**2 + grad_g_y**2)
magnitude_b = torch.sqrt(grad_b_x**2 + grad_b_y**2)
grad_magnitude = magnitude_r + magnitude_g + magnitude_b
grad_y = grad_r_y + grad_g_y + grad_b_y
grad_x = grad_r_x + grad_g_x + grad_b_x
# tanθ = grad_y / grad_x 转化为角度 (方向角)
grad_orientation = (torch.atan2(grad_y, grad_x) * (180.0 / PAI))
grad_orientation = torch.round(grad_orientation / 45.0) * 45.0 # 转化为 45 的倍数
return grad_magnitude, grad_orientation
Интенсивность градиента выводится в виде картинки, и получается самая правая картинка на нижней правой картинке.Видно, что значение градиента краевой области монеты больше (чем больше, тем больше, тем ярче)
Полный код смотрите по адресу:sobel_filter
(3) Подавление не максимизации
Процесс подавления немаксимизации (NMS):
- матрица силы градиента
grad_magnitude
Каждая точка используется в качестве центрального пикселя, и сравнивается сила градиента двух соседних точек (всего 8) в том же или противоположном направлении. - Если градиент центральной точки меньше градиента в этих двух направлениях, значение градиента центра точки устанавливается равным 0.
После двух вышеперечисленных шагов эффект градиентной крыши можно заменить шириной в один пиксель, сохранив при этом силу градиента (максимальный градиент) крыши.
class NonMaxSupression(nn.Module):
def __init__(self) -> None:
super(NonMaxSupression, self).__init__()
all_orient_magnitude = np.stack([filter_0, filter_45, filter_90, filter_135, filter_180, filter_225, filter_270, filter_315])
'''
directional_filter功能见下面详细说明
'''
self.directional_filter = nn.Conv2d(1, 8, kernel_size=filter_0.shape, padding=filter_0.shape[-1] // 2)
init_parameter(self.directional_filter, all_filters[:, None, ...], np.zeros(shape=(all_filters.shape[0],)))
def forward(self, grad_magnitude, grad_orientation):
all_orient_magnitude = self.directional_filter(grad_magnitude) # 当前点梯度分别与其其他8个方向邻域点做差(相当于二阶梯度)
'''
\ 3|2 /
\ | /
4 \|/ 1
-----------|------------
5 /|\ 8
/ | \
/ 6|7 \
注: 各个区域都是45度
'''
positive_orient = (grad_orientation / 45) % 8 # 设置正方向的类型,一共有八种不同类型的方向
negative_orient = ((grad_orientation / 45) + 4) % 8 # +4 = 4 * 45 = 180 即旋转180度(如 1 -(+4)-> 5)
height = positive_orient.size()[2] # 得到图片的宽高
width = positive_orient.size()[3]
pixel_count = height * width # 计算图片所有的像素点数
pixel_offset = torch.FloatTensor([range(pixel_count)])
position = (positive_orient.view(-1).data * pixel_count + pixel_offset).squeeze() # 角度 * 像素数 + 像素所在位置
# 拿到图像中所有点与其正向邻域点的梯度的梯度(当前点梯度 - 正向邻域点梯度,根据其值与0的大小判断当前点是不是邻域内最大的)
channel_select_filtered_positive = all_orient_magnitude.view(-1)[position.long()].view(1, height, width)
position = (negative_orient.view(-1).data * pixel_count + pixel_offset).squeeze()
# 拿到图像中所有点与其反向邻域点的梯度的梯度
channel_select_filtered_negative = all_orient_magnitude.view(-1)[position.long()].view(1, height, width)
# 组合成两个通道
channel_select_filtered = torch.stack([channel_select_filtered_positive, channel_select_filtered_negative])
is_max = channel_select_filtered.min(dim=0)[0] > 0.0 # 如果min{当前梯度-正向点梯度, 当前梯度-反向点梯度} > 0,则当前梯度最大
is_max = torch.unsqueeze(is_max, dim=0)
thin_edges = grad_magnitude.clone()
thin_edges[is_max==0] = 0.0
return thin_edges
directional_filter
В чем польза?
# 输入
tensor([[[[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]]]])
# 输出
tensor([[[[0., 0., 1.],
[0., 0., 1.],
[0., 0., 1.]],
[[0., 0., 1.],
[0., 0., 1.],
[1., 1., 1.]],
[[0., 0., 0.],
[0., 0., 0.],
[1., 1., 1.]],
[[1., 0., 0.],
[1., 0., 0.],
[1., 1., 1.]],
[[1., 0., 0.],
[1., 0., 0.],
[1., 0., 0.]],
[[1., 1., 1.],
[1., 0., 0.],
[1., 0., 0.]],
[[1., 1., 1.],
[0., 0., 0.],
[0., 0., 0.]],
[[1., 1., 1.],
[0., 0., 1.],
[0., 0., 1.]]]], grad_fn=<ThnnConv2DBackward0>)
Известно, что он получает значения градиента в восьми направлениях ввода (в коде текущего проекта для получения разницы между градиентом текущей точки и градиентом других 8 направлений)
По силе и направлению градиента направления делятся на 8 категорий (то есть для каждой точки имеется восемь возможных направлений), как показано на графике типа «метр» в коде выше.
Ниже приведен процесс расчета силы градиента соседних точек прямой окрестности текущей точки (обратная аналогична)
Направление градиентаgrad_orientation
: 0, 1,, 2, 3, 4, 5, 6, 7
(всего 8 направлений)
Градиент силы во всех направленияхall_orient_magnitude
: [[..方向0的梯度..], [..方向1的梯度..], ..., [..方向7的梯度..]]
Итак, для направленияi
, положение которого в силе градиента равноall_orient_magnitude[i][x, y]
,будетall_orient_magnitude
После перехода к одномерному вектору соответствующее положение равноposition = current_orient × pixel_count + pixel_offset
, мы можем получить разницу между текущей точкой и силой градиента ее прямых соседних точек на основе этой информации о положении (то же самое можно получить и в обратном направлении).
Ниже приведены вспомогательные схемы:
Конечный эффект показан на рисунке справа внизу (рисунок слева — это рисунок без максимального подавления)
Полный код смотрите по адресу:nonmax_supression
(4) Отслеживание края задержки
Поразмыслив над этим, мы обнаружили, что пока есть еще следующие проблемы:
- Если на изображении есть шум, могут появиться независимые от края точки (псевдокрая).
- Край облачный и яркий
Итак, наконец, нам нужно выполнить отслеживание края задержки, шаги следующие:
- Установите два порога (один высокий и один низкий), установите интенсивность градиента пикселей, интенсивность градиента которых меньше нижнего порога, на 0 и получите изображение A
- Установите силу градиента пикселей, сила градиента которых меньше верхнего порога, на 0, чтобы получить изображение B.
Мы знаем, что из-за более низкого порога A сохранение края более полное и непрерывность лучше, но может быть больше ложных краев, а B как раз противоположно A.
Исходя из этого, представляем себе, что В — это основа, а А дополняется, а недостающие пиксели в В дополняются рекурсивной трассировкой.
to_bw = lambda image: (image > 0.0).astype(float)
class HysteresisThresholding(nn.Module):
def __init__(self, low_threshold=1.0, high_threshold=3.0) -> None:
super(HysteresisThresholding, self).__init__()
self.low_threshold = low_threshold
self.high_threshold = high_threshold
def thresholding(self, low_thresh: torch.Tensor, high_thresh: torch.Tensor):
died = torch.zeros_like(low_thresh).squeeze()
low_thresh = low_thresh.squeeze()
final_image = high_thresh.squeeze().clone()
height = final_image.shape[0]
width = final_image.shape[1]
def trace(direction):
if not direction: return
for x in range(1, width - 1):
cx = x
if direction == 'left-top' or direction == 'left-bottom':
cx = width - 1 - x
left = cx - 1
right = cx + 1
for y in range(1, height - 1):
cy = y
if direction == 'left-top' or direction == 'right-top':
cy = height - 1 - y
top = cy - 1
bottom = cy + 1
if final_image[cy, cx]: # 当前点有连接
if low_thresh[top, left] > 0.0:
final_image[top, left] = low_thresh[top, left] # 左上
if low_thresh[top, cx] > 0.0:
final_image[top, cx] = low_thresh[top, cx] # 正上
if low_thresh[top, right] > 0.0:
final_image[top, right] = low_thresh[top, right] # 右上
if low_thresh[cy, left] > 0.0:
final_image[cy, left] = low_thresh[cy, left] # 正左
if low_thresh[cy, right] > 0.0:
final_image[cy, right] = low_thresh[cy, right] # 正右
if low_thresh[bottom, left] > 0.0:
final_image[bottom, left] = low_thresh[bottom, left] # 左下
if low_thresh[bottom, cx] > 0.0:
final_image[bottom, cx] = low_thresh[bottom, cx] # 正下
if low_thresh[bottom, right] > 0.0:
final_image[bottom, right] = low_thresh[bottom, right] # 右下
trace('right-bottom')
trace('left-top')
trace('right-top')
trace('left-bottom')
final_image = final_image.unsqueeze(dim=0).unsqueeze(dim=0)
return final_image
def forward(self, thin_edges):
low_thresholded: torch.Tensor = thin_edges.clone()
low_thresholded[thin_edges<self.low_threshold] = 0.0
high_threshold: torch.Tensor = thin_edges.clone()
high_threshold[thin_edges<self.high_threshold] = 0.0
final_thresholded = self.thresholding(low_thresholded, high_threshold)
return low_thresholded, high_threshold, final_thresholded
На следующем рисунке показано влияние низкого порога и высокого порога в порядке
Ниже приведен рендеринг отслеживания края отставания.
Можно видеть, что по сравнению с левым изображением выше, некоторые ложные края были устранены, а деталей больше, чем на правом изображении.
Полный код смотрите по адресу:hysteresis_thresholding
3. Резюме
На данный момент мы закончили использоватьCanny
Процесс выделения края оператором. Следующая статьяМы собираемся использоватьHough
Алгоритм поиска координат серебряной монеты на картинке выше.