10. Xử lí ảnh (2/2)

15 Dec 2021

Phụ lục:

1. Image Segmentation

Image Segmentation (phân khúc ảnh) là một bài toán lớn và xuất hiện khá lâu đời trong thị giác máy tính. Hiểu đơn giản thì input của bài toán là 1 bức ảnh hoặc frame của video và output sẽ là nhãn của từng giá trị pixel (trong thực tế có 2 bài toàn là sematic segmentation và instance segmentation, ở phần cuối mình sẽ trình bày sự khác nhau của 2 bài toán này). Ví dụ minh họa:

Tại thời điểm viết, các bài toán Image Segmentation được xử lí rất tốt bởi các mô hình Deep Learning tuy nhiên trước khi Deep Learning phát triển mạnh như ngày nay các thuật toán cổ điển làm việc như thế nào cùng tìm hiểu bên dưới, ngoài ra đôi khi với 1 số tác vụ đơn giản sẽ không cần ôm một mô hình Deep Learning hàng triệu tham số để tính toán là không cần thiết.

1.1. Thresholding algorithm

Phân khúc ảnh dựa trên ngưỡng (threshold) là một thuật toán khá đơn giản, bức ảnh ban đầu sẽ chuyển sang không gian grayscale sau đó sẽ xác định màu của mỗi pixel thuộc nhãn 0 (đen) hoặc 255 (trắng) tức ảnh đầu ra sẽ là một binary image. Để có thể làm hình ảnh grayscale thành binary ta sẽ xét một ngưỡng nguyên dương $T$ nằm trong đoạn (0, 255), với những pixel nhỏ hơn ngưỡng $T$ thì sẽ được xét là 0, ngược lại xét là 255. Với kết quả đầu ra của thuật toán threshold ta có thể tìm được các vùng có thể có đối tượng được quan tâm (ROI - Regions of interest) tách ra khỏi phần background của ảnh.

1.1.1. Simple thresholding

Simple thresholding sẽ cần ta xác định ngưỡng $T$, với những giá trị pixels nhỏ hơn $T$ ta sẽ xét bằng 0, ngược lại xét bằng 255.

\[\begin{equation} B(x,y)=\begin{cases} 0, & \text{if G(x,y) $\leq$ T}\\ 255, & \text{otherwise} \end{cases} \end{equation}\]

Việc thực hành với OpenCV khá đơn giản, thao tác với Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import numpy as np
import cv2
import matplotlib.pyplot as plt

gray_img = cv2.imread('coin.jpg',0)

T = 125 # threshold value

_, thresh_img = cv2.threshold(gray_img, T, 255, cv2.THRESH_BINARY)
_, thresh_img_inv = cv2.threshold(gray_img, T, 255, cv2.THRESH_BINARY_INV)
X = cv2.bitwise_and(gray_img, thresh_img_inv)

plt.figure(figsize=(16, 4))
plt.subplot(151),plt.imshow(gray_img,cmap='gray'),plt.title('Origin'),plt.axis(False)
plt.subplot(152),plt.imshow(thresh_img,cmap='gray'),plt.title('THRESH BINARY'),plt.axis(False)
plt.subplot(153),plt.imshow(thresh_img_inv,cmap='gray'),plt.title('THRESH BINARY INV'),plt.axis(False)
plt.subplot(154),plt.imshow(X,cmap='gray'),plt.title('Bitwise And'),plt.axis(False)
plt.show()

Kết quả:

Nhận xét:

  • OpenCV cung cấp hàm cv2.threshold gồm 4 đối số: ảnh grayscale, ngưỡng threshold $T$, giá trị nếu pixel lớn hơn $T$ (ở ví dụ trên thì với bất kì giá trị pixel nào lớn hơn 125 sẽ gán bằng 255, ngược lại gán = 0), cv2.THRESH_BINARY sẽ coi giá trị khi lớn là 0, nhỏ hơn là 255 và cv2.THRESH_BINARY_INV thì ngược lại (ở phần này bạn có thể nhìn hình kết quả và code để suy ra một cách đơn giản) và thêm khảo thêm 1 số cách khác tại đây.

  • Để visualize rõ ràng kết quả, mình đã sử dụng phép toán cv2.bitwise_and để kết hợp ảnh gốc với ảnh nhị phân. Nếu bạn chưa biết cách phép tính này hoạt động như thế nào bạn có thể xem lại tại đây.

  • Kết quả này đã giúp chúng ta phân biệt rõ được các đồng coin và background của chúng, tuy nhiên một hạn chế lớn của phương pháp này là ta đang phải chọn ngưỡng $T$ thủ công, tức sẽ phải tuning nhiều lần (bạn có thể thử thay đổi các giá trị $T$ và xem xét các kết quả khác nhau).

1.1.2. Ostu thresholding

Một trong những hạn chế của phương pháp Simple thresholding là phải chọn ngưỡng $T$, việc chọn ngưỡng $T$ một cách thủ công có thể làm việc vẫn tốt trong trường hợp điều kiện ánh sáng trong ảnh khá rõ nét. Tuy nhiên trong thực tế, các hình ảnh thu thập sẽ không có điều kiện ánh sáng tốt và dễ dàng phân biệt, vì vậy thuật Ostu ra đời nhằm tìm ngưỡng $T$ một cách tối ưu nhất.

Ý tưởng của thuật toán Ostu tập trung vào việc khai thác những thông tin hữu ích của Histogram trong ảnh. Ý nghĩa của Histogram nhằm giúp ta có thể chọn ngưỡng $T$ một cách tốt nhất để phân biệt giữa Foreground và Background (Histogram là gì?). Để hiểu rõ cách hoạt động của phương pháp này hãy xem ví dụ sau:

Với bức ảnh 6 x 6 bên phải ta có một biểu đồ Histogram tương ứng bên trái. Giả sử ta chọn ngưỡng giá trị 3 làm ngưỡng để phân chia giữa Background và Foreground chia thành 2 nhãn (B - Background, F - Foreground), sau đó ta sẽ tính toán các tham số cho 2 nhãn này như sau:

Theo thuật toán Ostu, đầu tiên ta sẽ tính toán trọng 2 trọng số của nhãn B và nhãn F đồng thời cũng là xác suất xuất hiện của nhãn B và F trong ảnh ($W_b, W_f$). Tiếp theo ta sẽ tính toán các giá trị trung bình của 2 nhãn ($\mu_b, \mu_f$) và phương sai ($\sigma_b^2, \sigma_f^2$).

Tới đây Ostu đưa ra 2 phương pháp để có thể chọn ngưỡng như sau:

PP1. Within-Class Variance $\sigma_W^2 = W_b \sigma_b^2 + W_f\sigma_f^2 = 0.47220.4637 + 0.52780.5152 = 0.4909$. Bằng việc tính toán các bước như trên với tất cả các ngưỡng có thể ta sẽ được bảng sau:

  • Ta thấy rằng với giá trị $T = 3$ đưa ra được kết quả tốt nhất, tức sẽ chọn giá trị $\sigma_W^2$ nhỏ nhất. Tức tìm ngưỡng có tổng phương pháp có trọng số thấp nhất.

  • Tuy nhiên, điểm hạn chế lớn của phương pháp này là phải tính toán trên tất cả ngưỡng có thể (0 - 255 với 8bit image) điều này sẽ làm cho lượng tính toán nhiều làm cho thuật toán rất chậm. Tất nhiên là ta có thể giới hạn một khoảng nhất định như là: 50 - 200 hay 75 - 150 để xét ngưỡng có thể trên khoảng này nhưng vẫn không đáng kể. Từ đây Ostu đề xuất một phương pháp giúp giảm thiểu chi phí tính toán là between class variance.

PP2. Between-class variance ta sẽ đi tìm giá trị chênh lệch tối đa giữa phương sai giữa 2 ngưỡng màu. Và giá trị ngưỡng tối ưu $T$ sẽ là nghiệm của phương trình:

\[T = argmax(W_bW_f(\mu_b - \mu_f)^2)\]

Các bước thực hiện thuật toán này cũng không khó, mình sẽ so sánh cách tự implement và thư viện có cùng ra 1 ngưỡng $T$ không. Thao tác với Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def otsu_implement(gray):
    # Mean weight to calculate probability
    mean_weight = 1.0/gray.shape[0] * gray.shape[1]
    
    # Histogram and bins edge horizontal axis
    his, bins = np.histogram(gray, np.arange(0,256))

    T = -1 # threshold value
    max_value = -1
    intensity_arr = np.arange(255)
    
    for t in bins[1:-1]:
        
        # Weight/probability of Foreground and Background
        pcb = np.sum(his[:t])
        pcf = np.sum(his[t:])
        Wb = pcb * mean_weight
        Wf = pcf * mean_weight
        
        # Mean of Foreground and Background
        mub = np.sum(intensity_arr[:t]*his[:t]) / float(pcb)
        muf = np.sum(intensity_arr[t:]*his[t:]) / float(pcf)
        
        # Formula must be maximum
        value = Wb * Wf * (mub - muf) ** 2

        if value > max_value:
            T = t
            max_value = value

    gray[gray > T] = 255
    gray[gray < T] = 0
    return T, gray

Hàm otsu_implement này sẽ nhận đầu vào là một grayscale image, sau đó tính toán biểu đồ histogram trên ảnh đó. Còn lại việc tính toán ra các giá trị trọng số/xác suất, trung bình và công thức cần tối ưu khá đơn giản. Nhìn chung hàm này sẽ duyệt qua các giá ngưỡng $T$ từ 0 - 255 để tìm giá trị phù hợp nhất. Tiếp theo sẽ là hàm của OpenCV:

1
2
3
4
def otsu_built(gray):
    (T, threshImg) = cv2.threshold(gray, 0, 255, \
                cv2.THRESH_BINARY | cv2.THRESH_OTSU)
    return T,threshImg

Hàm otsu_built cũng nhận đầu vào là một ảnh grayscale, sau đó ta sẽ sử dụng hàm cv2.threshold để phân ngưỡng, trong hàm này bao gồm 4 đối số: ảnh đầu vào, giá trị min, giá trị max, cách định nghĩa ngưỡng đầu ra (nếu bạn chưa hiểu có thể xem lại phần trên) + thuật toán sử dụng: cv2.THRESH_OTSU.

Cuối cùng hãy xem kết quả thực hiện của bài toán:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
gray_img = cv2.imread('coin.jpg',0)
gray_img2 = gray_img.copy() 

otsu_implement_threshold,thresh_img = otsu_implement(gray_img)
otsu_built_threshold,threshImg = otsu_built(gray_img2)

plt.figure(figsize=(16, 4))
plt.subplot(151)
plt.imshow(gray_img2,cmap='gray')
plt.title('Origin')
plt.axis(False)

plt.subplot(152)
plt.imshow(thresh_img,cmap='gray')
plt.title('T-implement = ' + str(otsu_implement_threshold))
plt.axis(False)

plt.subplot(153)
plt.imshow(threshImg,cmap='gray')
plt.title('T-built = ' + str(otsu_built_threshold))
plt.axis(False)

Kết quả:

Nhận xét:

  • Với hàm tự implement và thư viện có sẵn, 2 giá đầu ra có giá trị gần như giống nhau. Với $T$ tự implement là 118 và hàm có sẵn là 117, câu hỏi là tại sao lại có giá trị khác nhau? Thực ra trong thuật toán Otsu, tác giả cho rằng sẽ có những giá trị $T$ khác nhau đưa ra một giá trị tối đa cho hàm Between-class variance, vì vậy kết quả cuối cùng sẽ là trung bình cộng với những giá trị $T$ đó. Nhưng ở phần implement của mình chưa có động tới vấn đề này. Tuy nhiên, sẽ khó mà có trường hợp nào tìm ra 2 giá trị $T$ lệch nhau lớn, vì vậy việc giá trị ngưỡng lệch nhau +- 5 không quá tồi.

  • Mặc dù phương pháp Otsu đã đưa ra được cách tìm ngưỡng một cách tự động thay vì xác định thủ công, nhưng ngưỡng của Otsu và thuật toán Simple thresholding đều là những ngưỡng toàn cục (Global thresholding) vì vậy khi ảnh có dải màu khá đồng đều (tức biểu đồ histogram trải đều) thì tìm ngưỡng toàn cục sẽ không thể tối ưu được nhất. Vì vậy ở phần dưới ta sẽ tìm hiểu thuật toán khác phục vấn đề này.

1.1.3. Adaptive thresholding

Như đã đề cập ở trên, khi dải màu trong ảnh được trải rộng thì một ngưỡng $T$ sẽ có sai số khá nhiều. Hơn nữa, với global thresholding ta chỉ đang phân vùng được Foreground và Background của anh. Vì vậy thuật toán adaptive thresholding ra đời nhằm giải quyết vấn đề này khá tốt bằng cách tìm những vùng lân cận liền kề trong ảnh giá trị ngưỡng $T$ tối ưu nhất có thể. Do đó sẽ có nhiều hơn 1 giá trị $T$ khác nhau trong mỗi vùng ảnh nên thuật toán này có thể gọi là local thresholding.

Ở phần này mình sẽ sử dụng luôn thư viện do OpenCV cung cấp để thực hiện thuật toán này, tuy nhiên thuật toán này cũng khá dễ để implement. Thao tác với Python và kết quả:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
gray_img = cv2.imread('text.jpg',0)
blurred = cv2.GaussianBlur(gray_img,(5,5),0)

# Apply threhold
_, thresh_img = cv2.threshold(blurred, 125, 255, cv2.THRESH_BINARY)
_, otsu_img = cv2.threshold(blurred, 0, 255, \
                cv2.THRESH_BINARY | cv2.THRESH_OTSU)
thresh = cv2.adaptiveThreshold(blurred, 255,
	cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 21, 4)

fig = plt.figure(figsize=(12, 10))

fig.add_subplot(221)
plt.title('Original'),plt.set_cmap('gray'),plt.axis('off'),plt.imshow(gray_img)

fig.add_subplot(222)
plt.title('T = 125'),plt.axis('off'),plt.imshow(thresh_img),plt.set_cmap('gray')

fig.add_subplot(223)
plt.title('Otsu'),plt.axis('off'),plt.set_cmap('gray'),plt.imshow(otsu_img)

fig.add_subplot(224)
plt.title('Adaptive'),plt.imshow(thresh),plt.axis('off'),plt.set_cmap('gray')

plt.show()

Kết quả:

Giải thích:

  • Như đã đề cập ở phía trên, thuật toán Adaptive threshold sẽ tìm ra nhiều ngưỡng với các vùng cục bộ trong ảnh, và từ đó sẽ phân ngưỡng từng vùng trong ảnh theo giá trị ngưỡng tìm được.

  • Ở hàm cv2.adaptiveThreshold bao gồm 6 đối số lần lượt là: ảnh gốc, giá trị max, cv2.ADAPTIVE_THRESH_GAUSSIAN_C hoặc cv2.ADAPTIVE_THRESH_MEAN_C, cách xác định giá trị đầu ra cv2.THRESH_BINARY, blockSize = 21, C = 4

  • Để xét ngưỡng của mỗi vùng trong ảnh, ta cần xác định kích thước vùng chữ nhật đó và ở đây chính là blockSize (số lẻ). Tiếp theo ta cần xác định thuật toán sử dụng để tính ra giá trị trung bình:

    • với cv2.ADAPTIVE_THRESH_MEAN_C ta sẽ tính ra giá trị trung bình tại vùng đó rồi trừ C làm ngưỡng.
    • với cv2.ADAPTIVE_THRESH_GAUSSIAN_C ta sẽ cần 1 Gaussian window có kích thước blockSize x blockSize với công thức mình đã trình bày tại đây sau đó trừ C làm ngưỡng.

Nhận xét:

  • Bước đầu ta sẽ sử dụng bộ lọc GaussianBlur để giảm nhiễu ảnh.

  • Như đã thấy, với thuật toán Simple threshold và Otsu (Global threshold) trong trường hợp này làm việc rất tệ. Nguyên nhân là do lúc này ảnh gốc đang đang bị che khuất khá nhiều nên lúc này giữa Foreground và Background không hoàn toàn rõ ràng để máy tính có thể hiểu.

  • Ngược lại hoàn toàn với 2 thuật trên, thuật toán Adaptive threshold (local threshold) đưa ra một kết quả vô cùng tuyệt vời, các con số, dòng kẻ được highlight gần như rất chính xác. Vậy điều gì làm thuật toán này mạnh mẽ tới vậy? Cùng xem các tham số bên trong thuật toán này làm như thế nào.

  • Các thuật toán threshold khá dễ dàng để hiểu và thực hiện tuy nhiên trong 1 số task yêu cầu các chi tiết về object có trong ảnh thì thuật toán này sẽ không phù hợp vì nó sẽ chỉ ouput được một binary image.

1.2. K-means based

K-means là một thuật toán học không giám sát (unsupervised learning), mục tiêu chính của thuật toán này nhằm tìm nhãn cho toàn bộ dữ liệu dựa trên sự tương đồng lẫn nhau của chúng. Chi tiết về thuật toán và cách hoạt động của nó bạn có thể xem tại đây. Tuy nhiên, việc tìm nhãn cho toàn bộ dữ liệu với mỗi cụm sẽ có một nhãn khác nhau hoàn toàn có thể ứng dụng để phân vùng ảnh (image segmentation). Những pixel có giá trị tương đồng nhau càng cao thì càng dễ có màu sắc giống nhau, dựa trên ý tưởng này ta có thể xem với K-means thì việc segment sẽ xảy ra như thế nào với đoạn code sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import numpy as np
import cv2
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans

# Load image
image = cv2.imread('nature.jpg')
image = cv2.cvtColor(image,cv2.COLOR_BGR2RGB)
w,h,_ = image.shape

# Transform channels -> vectors
image_vec = np.reshape(image,(-1,3))

# Init K-means 
kmeans = KMeans(n_clusters=3)

# Training
kmeans.fit(image_vec)

# Get labels for each pixel
labels = kmeans.predict(image_vec)

# Get centroids
centroids = kmeans.cluster_centers_

# Init zeros image vec
image_result = np.zeros_like(image_vec)

# Update each pixel equal to corresponding centroid
for i in range(len(image_result)):
    image_result[i] = centroids[labels[i]]

image_result = image_result.reshape(w,h,3)

plt.figure(figsize=(16, 2))
plt.subplot(151)
plt.imshow(image)
plt.title('Origin')
plt.axis(False)

plt.subplot(152)
plt.imshow(image_result)
plt.title('K = 3')
plt.axis(False)

Kết quả:

Giải thích:

  • Bức ảnh đầu vào có shape lần lượt là width, height, channels và với ảnh màu sẽ có channels = 3. Sau đó ta sẽ chuyển màu BGR sang RGB vì OpenCV sẽ đọc ảnh theo vị trí 3 kênh màu là B->G->R.

  • Vì K-means trong sklearn sẽ cần nhận vào một tensor có kích thước <= 2D, nên ta cần reshape bức ảnh ban đầu mỗi channel thành 1 vector nên biến image_vec lúc này sẽ có shape = (width*height,3).

  • Tiếp theo ta sẽ sử dụng các method, attributes có sẵn trong sklearn để huấn luyện và lấy ra các nhãn, tâm cụm (centroids). Với số lượng cụm được xác định ở trên bằng 3 nên ta sẽ chỉ có 3 nhãn (0, 1, 2) và 3 tâm cụm (centroids).

  • Tiếp theo ta chỉ cần gán toàn bộ tâm cụm với giá trị nhãn tương ứng vào một biến image_result có shape giống với image_vec nhưng gồm toàn giá trị 0.

  • Cuối cùng, ta sẽ show kết quả với hình bên trên.

Nhận xét:

  • Với K = 3, ta thấy bức ảnh kết quả so với bức ảnh ban đầu đã phân được các vùng có giá trị giống nhau thành 1 màu (giá trị những pixel có độ tương đồng cao - L2 norm nhỏ thì sẽ về cùng 1 màu).

  • Với K = 3 ta thấy ảnh đã chia được các vùng như: tán cây màu xanh lục, mặt nước màu xanh dương, bầu trời màu xám trắng. Tuy nhiên 1 số tán cây và viên đá đang có màu xám trắng giống với bầu trời vì giá trị L2 của nó gần các giá trị trong bầu trời hơn là tán cây. Để ảnh có thể mô tả chi tiết, ta chỉ cần tăng số lượng cụm K lên và kết quả như sau:

Toàn bộ sourcecode sẽ được lưu tại đây.

2. Contour detection

2.1. Ý tưởng

Contour detection là một phương pháp phát hiện đường viên quanh những đối tượng có trong ảnh, những đường viền này có thể bao gồm nhiều hình dạng khác nhau như: hình chữ nhật, hình tròn, đa giác… tức là tập hợp các điểm liên tục để tạo nê đường nối giữa các điểm tạo nên hình dáng. Việc tìm được các contours chính xác giúp ích rất nhiều trong một số bài toán trong thị giác máy tính như: Object detection, Object tracking…

Có một số lưu ý trước khi đi vào chi tiết với bài toán Contour ta cần 1 số bước tiền xử lí sau:

  • Ảnh đầu vào của thuật toán tìm Contours cần là một binary image. Vì vậy ta sẽ có thể sử dụng thuật toán Canny (bạn có thể xem lại tại đây) hoặc thuật toán Thresholding mình đã trình bày ở trên.

  • Khi thực hiện với OpenCV, ảnh gốc sẽ bị thay đổi sau khi đi qua thuật toán Contours vì vậy ta cần copy ảnh gốc ra một biến khác trước khi đi qua thuật toán nếu vẫn muốn sử dụng giá trị biến ban đầu.

Thao tác với Python:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
img = cv2.imread('coin2.jpg')
rgb_img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img2 = rgb_img.copy() 

# Blur image by GaussianBlur
blurred = cv2.GaussianBlur(gray_img,(11,11),0)

# Edge detection by Canny -> binary image
edge = cv2.Canny(blurred,30,180)

# Finds contours
contours, hierarchy = cv2.findContours(edge, cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
draw_img = cv2.drawContours(img2, contours, -1, (0, 255, 0), 2)

fig = plt.figure(figsize=(16, 4))

fig.add_subplot(151)
plt.title('Original'),plt.axis('off'),plt.imshow(rgb_img)

fig.add_subplot(152)
plt.title('Edge'),plt.axis('off'),plt.imshow(edge)

fig.add_subplot(153)
plt.title('Contours'),plt.axis('off'),plt.imshow(draw_img)

plt.show()

Kết quả:

Giải thích:

  • Bước đầu ta sẽ Blur Image bằng thuật toán GaussianBlur và kết hợp với thuật toán Canny để phát hiện cạnh cho ảnh grayscale. Lúc này ảnh đầu ra sẽ là một binary image (biến edge dòng 10).

  • Tiếp theo ta sẽ sử dụng hàm cv2.findContours để tìm ra những contours có trong ảnh. Hàm này bao gồm 3 đối số lần lượt là: ảnh gốc, hierarchy typecontour approximation method, trong đó:

    • hierarchy type hiểu đơn giản là phương pháp trích xuất contours. Với cv2.RETR_EXTERNAL thì phương pháp trích xuất này sẽ chỉ tập trung vào khai thác những contours bên ngoài (viền ngoài) của object. Ngược lại, cv2.RETR_TREE sẽ đi sâu và tìm những contours một cách chi tiết hơn (ví dụ đôi khi bên trong 1 đối tượng lớn sẽ là 1 đối tượng nhỏ hơn). Ngoài ra có 1 số phương pháp tương tự như: cv2.RETR_LIST, cv2.RETR_COMP.

    • contour approximation method là phương pháp xấp xỉ tìm contours và cv2.CHAIN_APPROX_SIMPLE sẽ đưa ra kết quả tốt cũng như tiết kiệm bộ nhớ nhất.

  • Sau khi sử dụng hàm cv2.findContours sẽ trả về 2 tham số:

    • contours là một tuple chứa các numpy array, mỗi numpy array sẽ chứa các tọa độ (x,y) của mỗi điểm thuộc object.

    • hierarchy là danh sách các vector, chứa mối quan hệ giữa các contours.

  • Cuối cùng là hàm cv2.drawContours sẽ sử dụng để vẽ những contours tìm được lên ảnh, bao gồm 5 tham số lần lượt là: ảnh, contours tìm được, index của contours (-1 sẽ là vẽ toàn bộ contours), màu sắc, độ dày của nét vẽ.

Để hiểu kĩ hơn xem bên trong biến contours cách lưu trữ và giá trị như thế nào cùng xem bên dưới:

1
2
print(type(contours), len(contours))
>>> <class 'tuple'> 9

Qua ví dụ dưới đây bạn sẽ hiểu các giá trị bên trong contours:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
img = cv2.imread('coin2.jpg')
rgb_img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img2 = rgb_img.copy() 
img3 = rgb_img.copy() 

# Blur image by GaussianBlur
blurred = cv2.GaussianBlur(gray_img,(11,11),0)

# Edge detection by Canny -> binary image
edge = cv2.Canny(blurred,30,150)

# Finds contours
contours, hierarchy = cv2.findContours(edge.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

# Vẽ contours thứ 5
draw_img1 = cv2.drawContours(img2, contours, 5, (0, 255, 0), 2)

# Vẽ điểm thứ 10 trong contours thứ 4 
draw_img2 = cv2.drawContours(img3, contours[4], 10, (0, 255, 0), 10)
    
fig = plt.figure(figsize=(16, 4))

fig.add_subplot(151)
plt.title('Original'),plt.axis('off'),plt.imshow(rgb_img)

fig.add_subplot(152)
plt.title('5st Contours'),plt.axis('off'),plt.imshow(draw_img1)

fig.add_subplot(153)
plt.title('10st of Contour 4'),plt.axis('off'),plt.imshow(draw_img2)

plt.show()

Kết quả:

2.2. Shape

2.2.1. Bounding Box and Circle box

Trong những ứng dụng thực tế, đặc biệt là trong các bài toán về Object Detection. Thường các Object sẽ được visualize bằng cách vẽ các bounding box (hình chữ nhật) bao quanh các object có trong ảnh. Hoặc trong một số bài toán sẽ cần tìm ra những đường tròn bao quanh object (lưu ý ở phần trên hình ảnh Contours thực chất đường viền màu xanh không phải đường tròn mà là đường nối toàn bộ điểm tìm được với nhau và có hình dáng khá giống hình tròn).

Đầu tiên hãy xem đoạn code sau:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
img = cv2.imread('coin2.jpg')
rgb_img = cv2.cvtColor(img,cv2.COLOR_BGR2RGB)
gray_img = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
img2 = rgb_img.copy()
# Blur image by GaussianBlur
blurred = cv2.GaussianBlur(gray_img,(11,11),0)
# Edge detection by Canny -> binary image
edge = cv2.Canny(blurred,30,150)
# Finds contours
contours, hierarchy = cv2.findContours(edge.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)

for c in contours:
    # find bounding box coordinates
    x,y,w,h = cv2.boundingRect(c)
    cv2.rectangle(img, (x,y), (x+w, y+h), (0, 255, 0), 2)
    # calculate center and radius of minimum enclosing circle
    (x, y), radius = cv2.minEnclosingCircle(c)
    # cast to integers
    center = (int(x), int(y))
    radius = int(radius)
    # draw the circle
    img = cv2.circle(img, center, radius, (255, 255, 0), 2)
draw_img = cv2.drawContours(img2, contours, -1, (0, 255, 0), 2)

fig = plt.figure(figsize=(16, 4))
fig.add_subplot(151)
plt.title('Original'),plt.axis('off'),plt.imshow(rgb_img)
fig.add_subplot(152)
plt.title('Contours'),plt.axis('off'),plt.imshow(draw_img)
fig.add_subplot(153)
plt.title('Other shape'),plt.axis('off'),plt.imshow(img)
plt.show()

Kết quả:

Giải thích:

Ở trên ta cần lưu ý hàm cv2.boundingRectcv2.minEnclosingCircle:

  • cv2.boundingRect là hàm dùng để tính toán ra tọa độ (x,y) và width, height của bounding box của object dựa trên những points đã tìm tìm trong contours.

  • cv2.minEnclosingCircle là hàm dùng để tính toán ra tọa độ tâm (x,y) và bán kính (radius) của đường tròn dựa trên những points đã tìm tìm trong contours.

3. Kết luận

  • Qua phần 1 - 9. Xử lí ảnh (1/2)phần 2 - 10. Xử lí ảnh (2/2) mình đã giới thiệu những kiến thức cơ bản nhất về xử lí ảnh. Còn rất nhiều kiến thức nâng cao và hay ho hơn bạn hoàn toàn có thể tìm hiểu nếu đã hiểu được 2 bài mình trình bày.

  • Nhìn chung, các bài toán xử lí ảnh thuần giờ không còn là xu thế mà được thay thế bởi Deep Learning vì tính mạnh mẽ và độ lớn của dữ liệu ngày càng tăng.

4. Tham khảo

[1] Howse, J. and Minichino, J., n.d. Learning OpenCV 4 computer vision with Python 3.

[2] Otsu Thresholding by The Lab Book Pages

[3] Adaptive Thresholding with OpenCV by Adrian Rosebrock