You are on page 1of 16

PHÁT HIỆN ĐƯỜNG BIÊN

Chuẩn bị
- Cài đặt python 3.x
- Cài đặt numpy, skimage, matplotlib
Import
import numpy as np
import matplotlib.pyplot as plt
from time import time
from skimage import io

Phần 1 – Phát hiện đường biên

1. Phép tích chập


Cho ảnh F kích thước m x n và mặt nạ H kích thước k x l, phép tích chập (convolution) được định nghĩa như
sau:

'/# !/#
𝑘 𝑙
(𝐹 ∗ 𝐻)(𝑖, 𝑗) = + + 𝐹(𝑖 − 𝑢, 𝑗 − 𝑣). 𝐻(𝑢 + , 𝑣 + )
2 2
)%&'/# $%&!/#

F H

Nếu H đã được lật (flip) thì công thức sẽ là:

'/# !/#
𝑘 𝑙
(𝐹 ∗ 𝐻)(𝑖, 𝑗) = + + 𝐹(𝑖 + 𝑢, 𝑗 + 𝑣). 𝐻(𝑢 + , 𝑣 + )
2 2
)%&'/# $%&!/#

Áp dụng công thức này, cài đặt hàm conv trong file edge.py
Để xử lý hiệu ứng lề, người ta đã chèn (pad) thêm hàng và cột cho ảnh gốc bằng hàm np.pad(). Hãy xử lý trên
mảng padded và lưu kết quả của phép toán tích chập vào biến out.
Gợi ý:
- Cách 1: sử dụng 2 vòng for lồng nhau
- Cách 2: trích mảng con và nhân hai mảng con với nhau, dùng np.sum để tính tổng

1/16
Hàm conv()
def conv(image, kernel):
""" An implementation of convolution filter.

This function uses element-wise multiplication and np.sum()


to efficiently compute weighted sum of neighborhood at each
pixel.

Args:
image: numpy array of shape (Hi, Wi)
kernel: numpy array of shape (Hk, Wk)

Returns:
out: numpy array of shape (Hi, Wi)
"""
Hi, Wi = image.shape
Hk, Wk = kernel.shape
out = np.zeros((Hi, Wi))

# For this assignment, we will use edge values to pad the images.
# Zero padding will make derivatives at the image boundary very big,
# whereas we want to ignore the edges at the boundary.
pad_width0 = Hk // 2
pad_width1 = Wk // 2
pad_width = ((pad_width0, pad_width0), (pad_width1, pad_width1))
padded = np.pad(image, pad_width, mode='edge')

### YOUR CODE HERE


pass #Bỏ pass đi và viết code của bạn vào đây
### END YOUR CODE

return out

2/16
2. Tạo mặt nạ Gauss
Để khử nhiễu (hày làm trơn ảnh, smoothing) ta thường chập mặt nạ Gauss lên ảnh. Mặt nạ Gauss là một ma
trận xấp xỉ hàm Gauss và được tính theo công thức:

1 ()&')! ,($&')!
&
𝐻(𝑢, 𝑣) = 𝑒 #- !
2𝜋𝜎 #

với u,v chạy từ 0 đến 2k + 1.

Trong đó size = 2k + 1 là kích thước của mặt nạ H. Người ta thường chọn size là số lẻ !
Ví dụ: với size = 3 và sigma = 1 thì mặt nạ H sẽ là:

0.05854983 0.09653235 0.05854983


𝐻 = 8 0.09653235 0.15915494 0.09653235@
0.05854983 0.09653235 0.05854983

Hãy cài đặt hàm gaussian_kernel để tính toán và trả về mặt nạ H. Đầu vào của hàm này là size: kích thước của
mặt nạ (số lẻ) và sigma: phương sai của hàm Gauss.

def gaussian_kernel(size, sigma):


""" Implementation of Gaussian Kernel.
This function follows the gaussian kernel formula,
and creates a kernel matrix.

Hints:
- Use np.pi and np.exp to compute pi and exp

Args:
size: int of the size of output matrix
sigma: float of sigma to calculate kernel

Returns:
kernel: numpy array of shape (size, size)
"""

kernel = np.zeros((size, size))

### YOUR CODE HERE


pass #bỏ pass đi và viết code của bạn ở đây.
### END YOUR CODE

return kernel

Khi cài đặt xong có thể thử gọi:

kernel = gaussian_kernel(3, 1)
print(kernel)

Nếu kết quả giống như H ở bên trên là okie.

3/16
3. Ứng dụng hàm Gauss làm trơn ảnh
Sau khi đã cài đặt và kiểm thử hàm gauss_kernel, hãy áp dụng nó để làm trơn ảnh.

Hãy viết mã lệnh để đọc ảnh đầu vào iguana.png (đi kèm với bài tập, tất cả để cùng thư mục cho dễ xử lý). Áp
dụng tích chập mặt nạ Gauss với kernel_size = 5, sigma = 1.4 lên ảnh đầu vào. Hiển thị ảnh kết quả:

Sử dụng thư viện skimage để đọc ảnh:

from skimage import io


img = io.imread('iguana.png', as_grey=True)

Nhớ dán ảnh đầu vào, code xử lý và ảnh kết quả vào báo cáo !

4/16
4. Tính đạo hàm (gradient) ảnh
Ảnh được xem như 1 hàm 2 biến (y, x) nên ta có thể lấy đạo hàm riêng của ảnh theo x và theo y.

Quy ước: vì ảnh được lưu theo kiểu ma trận nên ta truy xuất các phần tử của ảnh cũng theo kiểu ma trận là
hàng trước, cột sau. Vì vậy ta sử dụng ký hiệu I(y, x) thay vì I(x, y) như lý thuyết.

Gọi I là ảnh đầu vào, đạo hàm riêng của I theo x và theo y tại điểm (y, x) là:

𝜕𝐼 𝐼(𝑦, 𝑥 + 1) − 𝐼(𝑦, 𝑥 − 1)
=
𝜕𝑥 2

𝜕𝐼 𝐼(𝑦 + 1, 𝑥) − 𝐼(𝑦 − 1, 𝑥)
=
𝜕𝑦 2

Mặt nạ (chưa lật) của hai phép toán này là:

𝐻. = [1 0 −1]

1
𝐻/ = 8 0 @
−1

Hãy sử dùng hàm conv ở trên để cài đặt phép tính đạo hàm ảnh: partial_x và partial_y.

def partial_x(img):
""" Computes partial x-derivative of input img.

Hints:
- You may use the conv function in defined in this file.

Args:
img: numpy array of shape (H, W)
Returns:
out: x-derivative image
"""

out = None

### YOUR CODE HERE


pass
### END YOUR CODE

return out

5/16
def partial_y(img):
""" Computes partial y-derivative of input img.

Hints:
- You may use the conv function in defined in this file.

Args:
img: numpy array of shape (H, W)
Returns:
out: y-derivative image
"""

out = None
### YOUR CODE HERE
pass
### END YOUR CODE

return out

Sau khi cài xong, thử gọi với ví dụ này để kiểm tra mình cài có đúng không.
# Test input
I = np.array(
[[0, 0, 0],
[0, 1, 0],
[0, 0, 0]]
)

# Expected outputs
I_x_test = np.array(
[[0, 0, 0],
[0.5, 0, -0.5],
[0, 0, 0]]
)

I_y_test = np.array(
[[0, 0.5, 0],
[0, 0, 0],
[0, -0.5, 0]]
)

# Compute partial derivatives


I_x = partial_x(I)
I_y = partial_y(I)

# Test correctness of partial_x and partial_y


if not np.all(I_x == I_x_test):
print('partial_x incorrect')

if not np.all(I_y == I_y_test):


print('partial_y incorrect')

6/16
5. Áp dụng đạo hàm ảnh
Hãy viết mã lệnh để đọc ảnh đầu vào iguana.png. Áp dụng tích chập mặt nạ Gauss với kernel_size = 5, sigma =
1.4 lên ảnh đầu vào. Gọi ảnh kết quả là smooth. Tiếp tục áp dụng đạo hàm riêng theo x lên ảnh smooth, và đạo
hàm riêng theo y lên ảnh smooth. Hiển thị ảnh kết quả (2 ảnh).

Trả lời thêm câu hỏi phụ: Tại sao ta cần phải làm trơn ảnh (smooth) trước khi áp dụng đạo hàm lên ảnh ?

6. Tính độ lớn (magnitude) và hướng (direction) của gradient


Đạo hàm riêng theo x và đạo hàm riêng theo y của ảnh được gọi là gradient của ảnh. Từ hai đạo hàm riêng
này, ta sẽ tính được độ lớn và hướng của gradient:

𝐺 = H𝐺.# + 𝐺/#
𝐺/
Θ = 𝑎𝑟𝑐𝑡𝑎𝑛 O P
𝐺.
Hãy cài đặt hàm gradient để tính và trả về G và Theta.
Gợi ý:
- Phải áp dụng partial_x() và partial_y() lên image
- Sử dụng 2 công thức trên để tính G và Theta
- Sử dụng np.arctan2 để tính arctan.

def gradient(img):
""" Returns gradient magnitude and direction of input img.

Args:
img: Grayscale image. Numpy array of shape (H, W)

Returns:
G: Magnitude of gradient at each pixel in img.
Numpy array of shape (H, W)
theta: Direction(in degrees, 0 <= theta < 360) of gradient
at each pixel in img. Numpy array of shape (H, W)
"""
G = np.zeros(img.shape)
theta = np.zeros(img.shape)

### YOUR CODE HERE


pass
### END YOUR CODE

return G, theta

7. Áp dụng gradient lên ảnh


Hãy viết mã lệnh để đọc ảnh đầu vào iguana.png. Áp dụng tích chập mặt nạ Gauss với kernel_size = 5, sigma =
1.4 lên ảnh đầu vào. Gọi ảnh kết quả là smooth.
Gọi hàm gradient(smooth) để tính gradient. Hiển thị ảnh độ lớn của gradient G. Đưa vào báo cáo.

7/16
8. Loại bỏ các điểm không phải cực đại (non-maximum suppression)
Sau khi tính được độ lớn G của gradient, các điểm có G lớn là các điểm có thể là đường biên. Để tránh trả về kết
quả đường biên quá dày (nhiều hơn 1 nét), ta sẽ tiến hành xem xét và loại bỏ các điểm không phải cực đại trong
lân cận của nó.
Cụ thể, ta sẽ xét theo hướng theta của gradient và tìm 2 lân cận của điểm đang xét, nếu độ lớn G của nó là lớn
nhất thì giữ nó lại, ngược lại ta xoá điểm này đi bằng cách cho giá trị tại điểm đó = 0.

Giải thuật:
Duyệt qua từng điểm (y, x), với mỗi điểm:
- Làm tròn giá trị hướng Theta[y, x] về gần với bội số của 45o. Ví dụ: nếu Theta[y,x] = 100, nó sẽ được làm
tròn về 90o, 40 làm tròn về 45o, 140 về 135o, … tương đương với lân cận 8 của 1 ô. Ta chỉ cần đưa về 4
hướng là đủ: 0, 45, 90, 135. Hướng 180 trùng với hướng 0.
- Tuỳ theo hướng của gradient, tìm hai điểm xung quanh điểm (x, y) là (x’, y’) và (x’’, y’’). Ví dụ nếu hướng
tại (x, y) là hướng 90o, thì (x’,y’) là (x, y-1) và (x’’, y’’) là (x, y+1): hai điểm trên và dưới (x, y). Chú ý: mặc
dù viết là các điểm (x, y) cho thuận với lý thuyết hình học nhưng khi truy xuất các ma trận thỉ phải dùng
chỉ số hàng trước cột sau nhé: G[y, x] chứ không phải G[x, y] !!!
- So sánh giá trị G của 3 điểm này và quyết định giữ lại điểm (x, y) nếu G của nó lớn nhất.

Hãy cài đặt hàm non_maximum_suppression().


Trong hàm này, người ta đã làm tròn số các hướng về bội số của 45o cho các bạn. Các bạn cũng có thể học
được thêm kiến thức từ công thức làm tròn số này.

def non_maximum_suppression(G, theta):


""" Performs non-maximum suppression

This function performs non-maximum suppression along the direction


of gradient (theta) on the gradient magnitude image (G).

Args:
G: gradient magnitude image with shape of (H, W)
theta: direction of gradients with shape of (H, W)

Returns:
out: non-maxima suppressed image
"""
H, W = G.shape
out = np.zeros((H, W))

# Round the gradient direction to the nearest 45 degrees


theta = np.floor((theta + 22.5) / 45) * 45

### BEGIN YOUR CODE


pass
### END YOUR CODE

return out

8/16
9. Kiểm tra hàm loại bỏ các điểm không cực đại
Sau khi cài đặt xong, hãy kiểm tra với đoạn mã bên dưới.
# Test input
g = np.array(
[[0.4, 0.5, 0.6],
[0.3, 0.5, 0.7],
[0.4, 0.5, 0.6]]
)

# Print out non-maximum suppressed output


# varying theta
for angle in range(0, 180, 45):
print('Thetas:', angle)
t = np.ones((3, 3)) * angle # Initialize theta
print(non_maximum_suppression(g, t))

Hãy đưa các kết quả thu được kèm giải thích vào báo cáo.

Viết thêm mã lệnh làm các việc sau:


- Đọc file ảnh iguana.png
- Tính tích chập mặt nạ Gauss với kernel_size = 5, sigma = 1.4 lên ảnh đầu vào
- Tính gradient để thu được G và theta
- Gọi hàm non_maximum_suppression: nms = non_maximum_suppression(G, theta)
- Hiển thị ảnh kết quả: nms

Đưa mã lệnh + các kết quả vào báo cáo.

10. Phân ngưỡng kép (double thresholding)


Sau khi đã loại bỏ các điểm không cực đại, ta thu được ảnh nms.
Bước tiếp theo là xác định các strong edge và weak edge đồng thời loại bỏ các điểm không phải đường biên.

Giải thuật:
Duyệt qua từng điểm ảnh (x, y), so sánh giá trị của nó với 2 ngưỡng: low và high
- Nếu nhỏ hơn low => bỏ đi vì không phải đường biên
- Nếu lớn hơn high => strong edge
- Còn lại => weak edge

9/16
Hãy cài đặt hàm double_threshoding()
Trong hàm này, ảnh đầu và là ảnh đã qua bước non-maximum suppression. Hai ngưỡng được sử dụng là high
và low. Người ta đã khởi tạo hai ma trận strong_edge và weak_edge = 0. Nếu điểm (x, y) là strong_edge thì
gán strong_edger[y, x] = 1. Tương tự như thế tho weak_edge.

def double_thresholding(img, high, low):


"""
Args:
img: numpy array of shape (H, W) representing NMS edge response
high: high threshold(float) for strong edges
low: low threshold(float) for weak edges

Returns:
strong_edges: Boolean array representing strong edges.
Strong edeges are the pixels with the values above
the higher threshold.
weak_edges: Boolean array representing weak edges.
Weak edges are the pixels with the values below the
higher threshould and above the lower threshold.
"""

strong_edges = np.zeros(img.shape)
weak_edges = np.zeros(img.shape)

### YOUR CODE HERE


pass
### END YOUR CODE

return strong_edges, weak_edges

11. Kiểm tra kết quả


Cài đặt xong, hãy kiểm tra với đoạn chương trình bên dưới.Trong đó, nms là ảnh kết quả của phần trên (xem
mục 9).
low_threshold = 0.02
high_threshold = 0.03

strong_edges, weak_edges = double_thresholding(nms, high_threshold, low_threshold)


assert(np.sum(strong_edges & weak_edges) == 0)

edges=strong_edges * 1.0 + weak_edges * 0.5

plt.subplot(1,2,1)
plt.imshow(strong_edges)
plt.title('Strong Edges')
plt.axis('off')

plt.subplot(1,2,2)
plt.imshow(edges)
plt.title('Strong+Weak Edges')
plt.axis('off')
plt.show()

10/16
12. Liên kết đường biên (edge tracking)
Strong edge chắc chắn là đường biên rồi, còn weak_edge thì chưa. Weak_edge sẽ trở thành strong_edge nếu
nó đứng gần 1 strong_edge.
Hãy cài đặt hàm link_edges().
Gợi ý: sử dụng duyệt theo chiều rộng.
- Dùng một hàng đợi indices lưu các strong_edge.
- Trong khi hàng đợi chưa rỗng
o Lấy phần tử đầu hàng đợi ra => có được toạ độ y, x của nó
o Đặt edges[y, x] = 1
o Dùng hàm get_neighbors để lấy ra các lân cận của nó. Duyệt qua các lân cận này.
§ Nếu là weak_edge và chưa có mặt trong indices thì đưa nó vào indices.

def link_edges(strong_edges, weak_edges):


""" Find weak edges connected to strong edges and link them.

Iterate over each pixel in strong_edges and perform breadth first


search across the connected pixels in weak_edges to link them.
Here we consider a pixel (a, b) is connected to a pixel (c, d)
if (a, b) is one of the eight neighboring pixels of (c, d).

Args:
strong_edges: binary image of shape (H, W)
weak_edges: binary image of shape (H, W)
Returns:
edges: numpy array of shape(H, W)
"""

H, W = strong_edges.shape
indices = np.stack(np.nonzero(strong_edges)).T
edges = np.zeros((H, W))

### YOUR CODE HERE


pass
### END YOUR CODE

return edges

11/16
13. Kiểm tra link_edges
Chạy thử đoạn mã bên dưới và đưa kết quả vào báo cáo.

test_strong = np.array(
[[1, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 1]]
)

test_weak = np.array(
[[0, 0, 0, 1],
[0, 1, 0, 0],
[1, 0, 0, 0],
[0, 0, 1, 0]]
)

test_linked = link_edges(test_strong, test_weak)

plt.subplot(1, 3, 1)
plt.imshow(test_strong)
plt.title('Strong edges')

plt.subplot(1, 3, 2)
plt.imshow(test_weak)
plt.title('Weak edges')

plt.subplot(1, 3, 3)
plt.imshow(test_linked)
plt.title('Linked edges')
plt.show()

12/16
14. Hàm canny
Gom hết tất cả các bước bên trên để cài đặt hàm canny.
def canny(img, kernel_size=5, sigma=1.4, high=20, low=15):
""" Implement canny edge detector by calling functions above.

Args:
img: binary image of shape (H, W)
kernel_size: int of size for kernel matrix
sigma: float for calculating kernel
high: high threshold for strong edges
low: low threashold for weak edges
Returns:
edge: numpy array of shape(H, W)
"""
### YOUR CODE HERE
pass
### END YOUR CODE

return edge

Kiểm thử với ảnh iguana.png và các tham số: kernel_size=5, sigma=1.4, high=0.03, low=0.02
Nếu bạn làm đúng, kết quả sẽ như hình bên dưới.

13/16
Phần 2 - Ứng dụng phát hiện vạch kẻ đường

Trong phần này, ta sẽ sử dụng phương pháp Canny và biến đổi Hough để tìm vạch kẻ đường trên xa lộ.
Quy trình gồm 3 bước chính như sau:
- Tìm đường biên bằng phương pháp Canny
- Chỉ lấy phần quan tâm mà thôi, không lấy hết ảnh. Trong ví dụ này: phần vạch kẻ đường nằm ở nửa
dưới của ảnh: một tam giác có đỉnh ở tâm ảnh và 2 đỉnh còn lại là 2 đỉnh dưới của ảnh.
- Chạy thuật toán biến đôit Hough để tìm phương trình đường thẳng của 2 vạch kẻ đường.

15. Tìm đường biên


Viết mã lệnh:
- Đọc file đầu vào: road.jpg
- Gọi hàm canny để tìm đường biên với các tham số: kernel_size=5, sigma=1.4, high=0.03, low=0.02
- Hiển thị ảnh đầu vào và ảnh kết quả.

16. Trích vùng quan tâm (ROI)


Tạo mặt nạ hình tam giác chứa vùng quan tâm để trích vùng quan tâm ra và xử lý. Vùng quan tâm là tam giác
màu vàng.

H, W = img.shape

# Generate mask for ROI (Region of Interest)


mask = np.zeros((H, W))
for i in range(H):
for j in range(W):
if i > (H / W) * j and i > -(H / W) * j + H:
mask[i, j] = 1

# Extract edges in ROI


roi = edges * mask

Với edges là đường biên tìm được bằng phương pháp canny ở bước 15.

14/16
17. Tìm đường thẳng bằng biến đổi Hough
Cài đặt biến đổi Hough để tìm đường thẳng.
Áp dụng phương trình đường thẳng:
𝜌 = 𝑥𝑐𝑜𝑠(𝜃) + 𝑡 𝑠𝑖𝑛(𝜃) (∗)
Chia khoảng theta từ -90 đến 90 độ, mỗi khoảng 1 độ.
Chia khoảng rho từ -diag_len đến diag_len với dia_len là chiều dài đường chéo của ảnh. Có tất cả diag_len *
2.0 + 1 khoảng.
Mảng accumulator lưu các đường cong (trong không gian biến đổi) đi qua các ô. Ô có giá trị lớn nhất tương
ứng với đường thẳng bên không gian gốc có nhiều điểm nhất.
ys, xs là toạ độ của của các điểm đường biên.
Hãy duyệt qua các điểm ys, xs, với mỗi điểm tăng ô accumulator[rho, theta] lên 1 nêu rho và theta thoả
phương trình đường thẳng (*).
def hough_transform(img):
""" Transform points in the input image into Hough space.

Use the parameterization:


rho = x * cos(theta) + y * sin(theta)
to transform a point (x,y) to a sine-like function in Hough space.

Args:
img: binary image of shape (H, W)

Returns:
accumulator: numpy array of shape (m, n)
rhos: numpy array of shape (m, )
thetas: numpy array of shape (n, )
"""
# Set rho and theta ranges
W, H = img.shape
diag_len = int(np.ceil(np.sqrt(W * W + H * H)))
rhos = np.linspace(-diag_len, diag_len, diag_len * 2.0 + 1)
thetas = np.deg2rad(np.arange(-90.0, 90.0))

# Cache some reusable values


cos_t = np.cos(thetas)
sin_t = np.sin(thetas)
num_thetas = len(thetas)

# Initialize accumulator in the Hough space


accumulator = np.zeros((2 * diag_len + 1, num_thetas), dtype=np.uint64)
ys, xs = np.nonzero(img)

# Transform each point (x, y) in image


# Find rho corresponding to values in thetas
# and increment the accumulator in the corresponding coordiate.
### YOUR CODE HERE
pass
### END YOUR CODE
return accumulator, rhos, thetas

15/16
18. Kiểm tra kết quả cài đặt
Hãy kiểm tra kết quả của biến đổi Hough với đoạn lệnh sau đây:
# Perform Hough transform on the ROI
acc, rhos, thetas = hough_transform(roi)

# Coordinates for right lane


xs_right = []
ys_right = []

# Coordinates for left lane


xs_left = []
ys_left = []
for i in range(20):
idx = np.argmax(acc)
r_idx = idx // acc.shape[1]
t_idx = idx % acc.shape[1]
acc[r_idx, t_idx] = 0 # Zero out the max value in accumulator

rho = rhos[r_idx]
theta = thetas[t_idx]

# Transform a point in Hough space to a line in xy-space.


a = - (np.cos(theta) / np.sin(theta)) # slope of the line
b = (rho / np.sin(theta)) # y-intersect of the line

# Break if both right and left lanes are detected


if xs_right and xs_left:
break

if a < 0: # Left lane


if xs_left:
continue
xs = xs_left
ys = ys_left
else: # Right Lane
if xs_right:
continue
xs = xs_right
ys = ys_right

for x in range(img.shape[1]):
y = a * x + b
if y > img.shape[0] * 0.6 and y < img.shape[0]:
xs.append(x)
ys.append(int(round(y)))

plt.imshow(img)
plt.plot(xs_left, ys_left, linewidth=5.0)
plt.plot(xs_right, ys_right, linewidth=5.0)
plt.axis('off')
plt.show()

Enjoy it !!!

16/16

You might also like