LoginSignup
0
0

More than 1 year has passed since last update.

【画像処理】Numpyでハフ変換による直線検出

Posted at

Numpyでハフ変換による直線検出を実装してみます。

直線を検出する画像を読み込みます。

import numpy as np
import matplotlib.pyplot as plt

original_image = plt.imread(image_name)
if np.issubdtype(original_image.dtype, np.integer):
  original_image = original_image / np.iinfo(original_image.dtype).max
gray_image = 0.2116 * original_image[:,:,0] + 0.7152 * original_image[:,:,1] + 0.0722 * original_image[:,:,2]
plt.imshow(gray_image, cmap='gray')

GrayImage.png

エッジ画像を作成します。ここでは、Cannyのエッジ検出を用います。Cannyのエッジ検出については以下の記事を参照してください。

【画像処理】NumpyでCannyエッジ検出 - Qiita

def _convolve2d(image, kernel):
  shape = (image.shape[0] - kernel.shape[0] + 1, image.shape[1] - kernel.shape[1] + 1) + kernel.shape
  strides = image.strides * 2
  strided_image = np.lib.stride_tricks.as_strided(image, shape, strides)
  return np.einsum('kl,ijkl->ij', kernel, strided_image)

def _convolve2d_multichannel(image, kernel):
  convolved_image = np.empty((image.shape[0] - kernel.shape[0] + 1, image.shape[1] - kernel.shape[1] + 1, image.shape[2]))
  for i in range(image.shape[2]):
    convolved_image[:,:,i] = _convolve2d(image[:,:,i], kernel)
  return convolved_image

def _pad_singlechannel_image(image, kernel_shape, boundary):
  return np.pad(image, ((int(kernel_shape[0] / 2),), (int(kernel_shape[1] / 2),)), boundary)

def _pad_multichannel_image(image, kernel_shape, boundary):
  return  np.pad(image, ((int(kernel_shape[0] / 2),), (int(kernel_shape[1] / 2),), (0,)), boundary)

def convolve2d(image, kernel, boundary='edge'):
  if image.ndim == 2:
    pad_image = _pad_singlechannel_image(image, kernel.shape, boundary) if boundary is not None else image
    return _convolve2d(pad_image, kernel)
  elif image.ndim == 3:
    pad_image = _pad_multichannel_image(image, kernel.shape, boundary) if boundary is not None else image
    return _convolve2d_multichannel(pad_image, kernel)

def create_gaussian_kernel(size=(3, 3), sigma=1):
  center = ((size[0] - 1) / 2, (size[1] - 1) / 2)
  sigma2 = 2 * sigma * sigma
  kernel = np.fromfunction(lambda y, x: np.exp(-((x - center[1]) ** 2 + (y - center[0]) ** 2) / sigma2), size)
  kernel = kernel / np.sum(kernel)
  return kernel 

def _suppress_nonmaximum(array):
  direction = array[9]
  if direction < 0:
    direction += np.pi
  if direction < 0.125 * np.pi:
    v0, v1, v2 = (array[3], array[4], array[5])
  elif direction < 0.375 * np.pi:
    v0, v1, v2 = (array[2], array[4], array[6])
  elif direction < 0.625 * np.pi:
    v0, v1, v2 = (array[1], array[4], array[7])
  elif direction < 0.875 * np.pi:
    v0, v1, v2 = (array[0], array[4], array[8])
  else:
    v0, v1, v2 = (array[3], array[4], array[5])
  return v1 if v1 >= v0 and v1 >= v2 else 0

def suppress_nonmaximum(intensity_image, direction_image):
  pad_image = np.pad(intensity_image, 1, 'edge')
  shape = intensity_image.shape + (3, 3)
  strides = pad_image.strides * 2
  strided_image = np.lib.stride_tricks.as_strided(pad_image, shape, strides).reshape(shape[0], shape[1], 9)
  return np.apply_along_axis(_suppress_nonmaximum, 2, np.dstack((strided_image, direction_image)))

def histeresis_thresholding(image, threshold):
  edge_image = np.where(image >= threshold[1], 1.0, 0.0)
  while True:
    pad_edge_image = np.pad(edge_image, 1, 'edge')
    shape = image.shape + (3, 3)
    strides = pad_edge_image.strides * 2
    neighbour_edge_image = np.lib.stride_tricks.as_strided(pad_edge_image, shape, strides)
    edge_candidate_index = np.where((image >= threshold[0]) & (edge_image != 1.0))

    changed = False
    for index in list(zip(*edge_candidate_index)):
      if np.any(neighbour_edge_image[index] == 1.0):
        edge_image[index] = 1.0
        changed = True
    if not changed:
      break;
  return edge_image

def canny(image, gaussian_size=(3, 3), gaussian_sigma=1, threshold=(0.02, 0.05)):
  gaussian_kernel = create_gaussian_kernel(size=gaussian_size, sigma=gaussian_sigma)
  gaussian_image = convolve2d(gray_image, gaussian_kernel)
  sobel_x_kernel = np.array([[-1, 0, 1], [-2, 0, 2], [-1, 0, 1]])
  sobel_x_image = convolve2d(gaussian_image, sobel_x_kernel) / 8
  sobel_y_kernel = np.array(([[1, 2, 1], [0, 0, 0], [-1, -2, -1]]))
  sobel_y_image = convolve2d(gaussian_image, sobel_y_kernel) / 8
  sobel_intensity_image = np.sqrt(sobel_x_image ** 2 + sobel_y_image ** 2)
  sobel_direction_image = np.arctan(np.divide(sobel_y_image, sobel_x_image, out=np.zeros_like(sobel_y_image), where=sobel_x_image!=0))
  nonmaximum_suppression_image = suppress_nonmaximum(sobel_intensity_image, sobel_direction_image)
  return histeresis_thresholding(nonmaximum_suppression_image, threshold)

edge_image = canny(gray_image, gaussian_size=(3, 3), gaussian_sigma=0.2, threshold=(0.05, 0.2))
plt.imshow(edge_image, cmap='gray')

EdgeImage.png

直線を次のように$(r, \theta)$で表します。$r$は原点から直線までの最短距離、$\theta$は原点からの直線までの最短地点を結んだ直線と原点がなす角度です。

r = x\cos\theta + y\sin\theta

上式よりエッジ画素$(x_e, y_e)$を通る直線は以下のようになります。

r = x_e\cos\theta + y_e\sin\theta

$(r, \theta)$パラメータ空間を一定間隔で区切り、エッジ画素ごとに$\theta$を少しづつ変えながら$r$を求めます。求めた$(r, \theta)$の組み合わせをカウント(投票)していきます。

edges = np.where(edge_image == 1.0)
r_max = np.sqrt(edge_image.shape[0] ** 2 + edge_image.shape[1] ** 2)
votes = np.zeros((int(r_max), 180), dtype=int)
for edge in list(zip(edges[1], edges[0])):
  for i in range(votes.shape[1]):
    ang = np.deg2rad(i)
    r = edge[0] * np.cos(ang) + edge[1] * np.sin(ang)
    votes[int(r)][i] += 1

plt.figure(figsize=(8,8))
plt.imshow(votes, aspect=0.5)
plt.colorbar()

VoteImage.png

多数の投票が集まった$(r,\theta)$が検出された直線になります。検出された直線を画像上に描画してみます。直線の描画については以下の記事を参照してください。
【画像処理】Numpyで図形描画 - Qiita

def is_local_maximum(image, size=(5, 5), boundary='edge'):
  pad_image = np.pad(image, ((int(size[0] / 2),), (int(size[1] / 2),)), boundary)
  areas = np.lib.stride_tricks.as_strided(pad_image, image.shape + size, pad_image.strides * 2)
  local_maximum = np.max(areas, axis=(2, 3))
  return image == local_maximum

def draw_line(image, color, weight, point1, point2):
  coord = np.fromfunction(lambda y, x: np.dstack((y + 0.5, x + 0.5)), image.shape[:2])
  p12 = np.array([point2[1] - point1[1], point2[0] - point1[0]])
  p12_norm = np.linalg.norm(p12)
  p12_dir = p12 / p12_norm
  p1c = np.dstack(([coord[:,:,0] - point1[1], coord[:,:,1] - point1[0]]))
  dot = np.einsum('ijk,k->ij', p1c, p12_dir)
  dist = np.sqrt(p1c[:,:,0] ** 2 + p1c[:,:,1] ** 2 - dot ** 2)
  condition = (dot >= 0) & (dot <= p12_norm) & (dist <= weight * 0.5)
  if image.ndim == 3:
    condition = np.tile(condition.reshape(condition.shape + (1,)), (1, 1, image.shape[2]))
  return np.where(condition, color, image)

def draw_hough_lines(image, lines, color, weight):
  for line in lines:
    s = np.sin(line[1])
    c = np.cos(line[1])
    if s > 0.01:
      p1 = (0.0, line[0] / s)
      p2 = (image.shape[1], (line[0] - image.shape[1] * c) / s)
      image = draw_line(image, color, weight, p1, p2)
    else:
      p1 = (line[0] / c, 0.0)
      p2 = ((line[0] - image.shape[0] * s) / c, image.shape[0])
      image = draw_line(image, color, weight, p1, p2)
  return image

hough_lines = np.where(is_local_maximum(votes, size=(5, 5)) & (votes >= 50))
hough_line_image = draw_hough_lines(original_image, list(zip(hough_lines[0], np.deg2rad(hough_lines[1]))), np.array([1.0, 0.0, 1.0]), 2.0)
plt.imshow(hough_line_image)

HoughLineImage.png

これでハフ変換による直線の検出ができました。


ハフ変換による直線検出を1つの関数にまとめると次のようになります。

def detect_hough_lines(edge_image, vote_size, threshold, local_maximum_size):
  votes = np.zeros(vote_size, dtype=int)
  r_step = np.sqrt(edge_image.shape[0] ** 2 + edge_image.shape[1] ** 2) / vote_size[0]
  theta_step = np.pi / vote_size[1]
  edges = np.where(edge_image)
  for edge in list(zip(edges[1], edges[0])):
    for i in range(vote_size[1]):
      angle = i * theta_step
      r = edge[0] * np.cos(angle) + edge[1] * np.sin(angle)
      votes[int(r / r_step)][i] += 1
  lines = np.where(is_local_maximum(votes) & (votes >= threshold))
  return list(zip(lines[0] * r_step, lines[1] * theta_step))

r_max = np.sqrt(edge_image.shape[0] ** 2 + edge_image.shape[1] ** 2)
hough_lines = detect_hough_lines(edge_image == 1.0, (int(r_max), 180), 50, (5, 5))
hough_line_image = draw_hough_lines(original_image, hough_lines, np.array([1.0, 0.0, 1.0]), 2.0)
plt.imshow(hough_line_image)

HoughLineImageSummary.png


実装したコードはGoogle Colaboratoryに置いてあります。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0