Найдите обнаружение края монет на бумаге

компьютерное зрение
Найдите обнаружение края монет на бумаге

1. Предыстория проблемы

На бумаге есть серебряная монета в один доллар, вы можетеCannyиHoughнайти его координатное уравнение с помощью ?

image.png

Чтобы определить уравнение координат круга, сначала нам нужно обнаружить его край, а затем найти его относительное положение и радиус на бумаге.

В этой статье мы используемCannyАлгоритмы обнаружения краев серебряных монет на бумаге.

2. Хитрый алгоритм

Canny можно использовать для получения края объекта на изображении, шаги следующие:

  1. Гауссово сглаживание
  2. Рассчитать градиент изображения (зафиксировать его интенсивность, направление)
  3. немаксимальное подавление
  4. Отслеживание запаздывающих краев

После выполнения вышеуказанных четырех шагов эффект рендеринга извлечения края монеты на бумаге, который мы получили, выглядит следующим образом.

image.png

(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

Изображение после сглаживания по Гауссу (размытие) более размыто, чем исходное изображение, как показано на серебряной монете в правой части рисунка ниже.

image.png

Полный код смотрите по адресу: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

Интенсивность градиента выводится в виде картинки, и получается самая правая картинка на нижней правой картинке.Видно, что значение градиента краевой области монеты больше (чем больше, тем больше, тем ярче)

image.png

Полный код смотрите по адресу:sobel_filter

(3) Подавление не максимизации

Процесс подавления немаксимизации (NMS):

  1. матрица силы градиентаgrad_magnitudeКаждая точка используется в качестве центрального пикселя, и сравнивается сила градиента двух соседних точек (всего 8) в том же или противоположном направлении.
  2. Если градиент центральной точки меньше градиента в этих двух направлениях, значение градиента центра точки устанавливается равным 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, мы можем получить разницу между текущей точкой и силой градиента ее прямых соседних точек на основе этой информации о положении (то же самое можно получить и в обратном направлении).

Ниже приведены вспомогательные схемы:

image.png

Конечный эффект показан на рисунке справа внизу (рисунок слева — это рисунок без максимального подавления)

image.png

Полный код смотрите по адресу:nonmax_supression

(4) Отслеживание края задержки

Поразмыслив над этим, мы обнаружили, что пока есть еще следующие проблемы:

  • Если на изображении есть шум, могут появиться независимые от края точки (псевдокрая).
  • Край облачный и яркий

Итак, наконец, нам нужно выполнить отслеживание края задержки, шаги следующие:

  1. Установите два порога (один высокий и один низкий), установите интенсивность градиента пикселей, интенсивность градиента которых меньше нижнего порога, на 0 и получите изображение A
  2. Установите силу градиента пикселей, сила градиента которых меньше верхнего порога, на 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

На следующем рисунке показано влияние низкого порога и высокого порога в порядке

image.png

Ниже приведен рендеринг отслеживания края отставания.

image.png

Можно видеть, что по сравнению с левым изображением выше, некоторые ложные края были устранены, а деталей больше, чем на правом изображении.

Полный код смотрите по адресу:hysteresis_thresholding

3. Резюме

На данный момент мы закончили использоватьCannyПроцесс выделения края оператором. Следующая статьяМы собираемся использоватьHoughАлгоритм поиска координат серебряной монеты на картинке выше.