Thử làm bác sĩ chuẩn đoán X-Quang cùng YOLO v5 (Phần 1/2)

Hi all anh em, hôm nay chúng ta sẽ cùng nhau tìm hiểu về YOLO v5 và làm luôn bài toán chẩn đoán X-Quang cùng bộ dữ liệu của VinBigData nhé!

Phần 1 này chúng ta tìm hiểu lý thuyết, tải dữ liệu, xử lý và chia dữ liệu để chuẩn bị cho Phần 2 sẽ train và đưa model lên web.

Trước tiên mình xin phép được sử dụng dữ liệu của VinBigDatađể làm bài này. Bài này hoàn toàn phi lợi nhuận, vì mục đích giáo dục.

YOLOv5 được yêu cầu rất nhiều bởi members trên Mì AI và đó là lý do mình ra bài này. Khác với các bài trước về YOLO, bài này mình sẽ guide các bạn từ A-Z để xây dựng một sản phẩm hoàn thiện.

Phần 1 – Lịch sử dòng họ YOLO

YOLO phiên bản 1 ra mắt lần đầu năm 2016 và sau đó đã qua nhiều phiên bản YOLO9000 (2017), YOLOv3 (2018) và YOLOv4 (2020) với nhiều cải tiến vượt trội về tốc độ và độ chính xác.

Nguồn: Tại đây

Trên Mì AI cũng đã có một loạt bài về YOLO v3, v4 rồi. Các bạn có thể truy cập vào link này để đọc nhé: https://www.miai.vn/?s=yolo

Tuy nhiên, có một cái lạ là ngay sau khi YOLOv4 ra được vài ngày thì tòi ra ông YOLOv5. Lúc đó có hai luồng tư tưởng:

  • Một bên ủng hộ YOLOv5
  • Một bên bảo không được đặc YOLOv5 vì chưa có paper chứng thực gì cả.

Thôi thì với anh em ứng dụng thì cứ ngon là anh em quất, quan trọng gì tên nó là gì đâu nhỉ :D. Mạng này thì ngon vãi luôn nè:

Nguồn: Tại đây

Do đó, anh em nếu có quất YOLO thì quất luôn YOLO v5 cho nó hoành nhé!

Phần 2 – Tìm hiểu về YOLO v5 “hơi sâu” một tý

Gọi là tìm hiểu hơi sâu thôi nhé, vì chúng ta sẽ tìm hiểu để áp dụng được thôi. Bạn nào cần viết paper, cần báo cáo đồ án thì chịu khó đọc thêm chi tiết về kiến trúc, về mã nguồn…. nhé!

Nói một cách chung chung thì YOLOv5 sử dụng Pytorch – một framework deep learning khác nhé anh em, ah em nào chưa biết thì có thể đọc tại link này.

Khác với các bản YOLO trước, YOLOv5 cung cấp luôn 4 phiển bản với kiến trúc mạng khác nhau:

  • yolov5-s which is a small version
  • yolov5-m which is a medium version
  • yolov5-l which is a large version
  • yolov5-x which is an extra-large version
Nguồn: Tại đây

Tất nhiên, mạng to thì độ chính xác cao hơn nhưng tốc độ train và inference sẽ thấp hơn rồi. Tùy vào nhu cầu của bạn để bạn lựa chọn tốc độ hay độ chính xác nhé. Anh em có thể tham khảo tốc độ và độ chính xác qua hình vẽ bên trên.

Trong bài này mình sẽ sử dụng model Yolo5x – mạng to nhất cho nó máu nhé 😀

Phần 3 – Chuẩn bị dữ liệu train YOLO v5

Bài này như đã nói mình sẽ sử dụng bộ dữ liệu của cuộc thi VinBigData Chest X-ray Abnormalities Detection ( https://www.kaggle.com/c/vinbigdata-chest-xray-abnormalities-detection/data ).

Tải dữ liệu

Dữ liệu cuộc thi này khá lớn (tận 190GB) do ghi ở dạng ảnh DICOM format (link tìm hiểu thêm tại đây), một dạng ảnh sử dụng lưu trữ trong Y học. Chúng ta có thể tự dùng thư viện PyDicom để đọc và sau đó loại bỏ các thông tin không cần thiết, chỉ lưu lại phần ảnh với mục đích giảm dung lượng dataset.

Chú ý: Bạn đọc kỹ phần mô tả của bộ dataset này để sau còn hiểu Class 14, 13, 1… là cái gì nhé. Cụ thể:

0 - Aortic enlargement
1 - Atelectasis
2 - Calcification
3 - Cardiomegaly
4 - Consolidation
5 - ILD
6 - Infiltration
7 - Lung Opacity
8 - Nodule/Mass
9 - Other lesion
10 - Pleural effusion
11 - Pleural thickening
12 - Pneumothorax
13 - Pulmonary fibrosis
Nguồn: Tại đây

Tuy nhiên, có một cách nhanh hơn là ta có thể tải sẵn bộ dataset đã convert sang PNG tại Thư viện Mì AI: https://www.miai.vn/thu-vien-mi-ai/.

Sau khi tải về chúng ta sẽ có một file tên là VINAI_Chest_Xray.zip (4GB – File này do một bạn tên Nguyễn Thành Trung convert sẵn nhé, mình chỉ tải về thôi, hiện chưa tìm lại được link tải để đưa vào đây). Giải nén file này ra chúng ta sẽ có :

  • Một file train_downsampled.csv – chứa thông tin về ảnh, nhãn của các vùng chẩn đoán trong ảnh.
  • Một folder train: Chứa các ảnh để train model
  • Một folder test: Chứa các ảnh để test model.

Rồi bây giờ ta sang phần tiếp theo là tiền xử lý dữ liệu!

Tiền xử lý dữ liệu, convert về YOLO5 format

Quan sát nhanh tập dữ liệu ta có các thông tin sau đây:

  • Các file ảnh trong tập train không có kích thước cố định nên không thể hardcode width height khi convert.
  • Trong file csv cũng không có width, height của ảnh nên ta sẽ phải đọc ảnh để biết kích thước trước khi convert.
  • Một file ảnh có nhiều kết quả chẩn đoán của nhiều bác sĩ khác nhau. Chỗ này tối ưu tốt là có giải nhưng mình gà và bài này ở mức demo nên mình cứ để nguyên và train nhé.
  • Do là bài toán detection nên ta sẽ bỏ qua các class 14 (không có bệnh) trong file csv
yolo v5 vin ai

Vậy là cách làm như sau:

  • Đọc file CSV, lấy tên file và tọa độ box, nhãn của các chẩn đoán trong file đó.
  • Đọc file ảnh để biết kích thước ảnh .
  • Convert sang YOLO format và ghi vào folder train (nằm cạnh file ảnh) với tên giống file ảnh và phần mở rộng là txt.

Anh em chú ý một điều nữa là :

  • Trong file csv ghi tọa độ góc trên và dưới của box chẩn đoán theo pixel.
  • Còn YOLO format thì lại là tọa độ tâm của box và kích thước rộng, dài và tất cả được chia cho width, height của ảnh để chuẩn hóa về khoảng [0,1]

Và đây là code convert của mình. (Chú ý: Dữ liệu này search trên mạng có thể có sẵn và không cần convert, nhưng mình sẽ viết để các bạn hiểu cách làm, và có thể cách này của mình chưa phải là cách tối ưu nha :D)

import pandas as pd
import cv2
import os
import pickle

# Đọc file CSV
csv_file = "G:\\VINAI_Chest_Xray\\train_downsampled.csv"
df = pd.read_csv(csv_file)

print(df.head())

# Lặp qua các file trong thư mục train
raw_folder = "G:\\VINAI_Chest_Xray\\train\\train"
idx = 0
file_list = []
for file in os.listdir(raw_folder):
    if file[0] != ".": # Ignore temp file
        print("Xử lý file {}- {}".format(idx, file))
        idx += 1
        # Tìm xem trong CSV có nhãn nào cho file và chỉ lọc các bản ghi có class_id khác 14
        df_find = df[(df.image_id == file[:-4]) & (df.class_id != 14)]

        # Nếu tìm thấy bản ghi phù hợp
        if len(df_find) > 0:
            # Đọc file ảnh để lấy kích thước
            raw_image = cv2.imread(os.path.join(raw_folder, file), 0)
            image_width, image_height = raw_image.shape[1], raw_image.shape[0]

            labels = []

            # Lặp qua từng bản ghi trong df_find để tính toán
            for index, row in df_find.iterrows():

                # Tính tọa độ tâm và kích thước theo pixels
                box_width = row[6] - row[4]
                box_height = row[7] - row[5]
                box_center_x = (row[6] + row[4]) / 2
                box_center_y = (row[7] + row[5]) / 2

                # Thực hiện chuẩn hóa
                box_width_normalized = box_width / image_width
                box_height_normalized = box_height / image_height
                box_center_x_normalized = box_center_x / image_width
                box_center_y_normalized = box_center_y / image_height

                # Ghi thông tin nhãn vào list
                labels.append([row[2], box_center_x_normalized, box_center_y_normalized, box_width_normalized, box_height_normalized])

            # Lặp qua list và ghi vào file txt
            txt_file = file[:-4] + ".txt"
            with open(os.path.join(raw_folder, txt_file),'w') as f:
                for label in labels:
                    f.write('{} {} {} {} {}\n'.format(label[0],label[1],label[2],label[3],label[4]))
            print("Ghi xong nhãn ", txt_file)
            file_list.append(file)
            print("Số file có nhãn = ", len(file_list))

# Ghi file_list vào file pickle
with open('file_list.pkl','wb') as f:
    pickle.dump(file_list,f)Code language: PHP (php)

Okie, sau khi chạy xong đoạn lệnh này thì anh em hãy mở thử thư mục train xem, sẽ thấy ngay một loạn file txt đã được tạo ra ghi nhãn! Một số file không có file txt đi kèm vì file ảnh đó không có chẩn đoán bệnh.

yolo v5
yolo v5 label

Ngoài ra, ta cũng tạo ra một file file_list.pkl chứa các ảnh có nhãn.

Okie rồi đó, vậy là công tác tạo data đã xong. Đến bước tổ chức lại dữ liệu thô này để train model.

Chia dữ liệu thành train,val theo đúng cấu trúc Yolo v5

Chú ý: Trong đoạn code trên hoàn toàn có thể thực hiện việc chia train,val ngay khi tạo ra txt nhưng mình muốn tách thành hai process để các bạn mới tiếp cận đỡ nhầm lẫn. Sau khi quen rồi các bạn có thể merge 2 code vào cho tiện.

Cấu trúc mà YOLO v5 yêu cầu gồm:

  • Một thư mục images chứa ảnh: trong đó có 2 thư mục train và val để chứa ảnh train và ảnh validation.
  • Một thư mục labels chứa nhãn (các file txt đó) và cũng có 2 thư mục tương tự như images.

Và đây là code để chia 20% ảnh cho validation và 80% cho train:

import shutil
import os
import random
import pickle

raw_folder = "G:\\VINAI_Chest_Xray\\train\\train"

with open('file_list.pkl','rb') as f:
    file_list = pickle.load(f)


total_files = len(file_list) # Tổng số file có nhãn trong thư mục train
print("Tổng số file  = ", total_files)

# Anh em tạo sẵn thư mục này nếu chưa có nhé :D Mình khỏi viết hàm check ở đây hehe
train_folder = "G:\\VINAI_Chest_Xray\\yolo_data\\images\\train"
val_folder = "G:\\VINAI_Chest_Xray\\yolo_data\\images\\val"

train_labels_folder = "G:\\VINAI_Chest_Xray\\yolo_data\\labels\\train"
val_labels_folder = "G:\\VINAI_Chest_Xray\\yolo_data\\labels\\val"


# Tạo ra index cho train, val
total_files_validation = int(0.2 * total_files) # 20% cho validation
validaiton_files = random.choices(file_list, k=total_files_validation)
print("Số bản ghi validation = " , len(validaiton_files))

# Copy images và labels to validation folder
for file in validaiton_files:
    print("Validation file ", file)
    # Copy images
    shutil.copy(os.path.join(raw_folder, file), os.path.join(val_folder, file))

    # Copy labels
    shutil.copy(os.path.join(raw_folder, file[:-3] + 'txt'), os.path.join(val_labels_folder, file[:-3] + 'txt'))


# Copy images và labels to train folder
for file in total_files:
    if not (file in validaiton_files):
        print("Train file ", file)
        # Copy images
        shutil.copy(os.path.join(raw_folder, file), os.path.join(train_folder, file))

        # Copy labels
        shutil.copy(os.path.join(raw_folder, file[:-3] + 'txt'), os.path.join(train_labels_folder, file[:-3] + 'txt'))

Code language: PHP (php)

Okie, sau khi chạy xong bạn sẽ có các thư mục chứa đầy ảnh và nhãn sẵn sàng cho train model.

Tuy nhiên, bài này cũng dài rồi nên mình sẽ tách phần train và triển khai sang bài khác nhé. Mình sẽ viết ngay thôi, để các bạn đỡ phải đợi! Toàn bộ source code sẽ được chia sẻ trong Phần 2 nha!

Phần 2 đã có tại đây: https://www.miai.vn/2021/03/03/thu-lam-bac-si-chuan-doan-x-quang-cung-yolo-v5-phan-2-2/

Chúc các bạn thành công!

#MìAI

Fanpage: http://facebook.com/miaiblog
Group trao đổi, chia sẻ: https://www.facebook.com/groups/miaigroup
Website: https://miai.vn
Youtube: http://bit.ly/miaiyoutube

Related Post

11 Replies to “Thử làm bác sĩ chuẩn đoán X-Quang cùng YOLO v5 (Phần 1/2)”

  1. Anh ơi,
    Em cảm ơn các bài viết hay và bổ ích của anh ạ. Nhất là bài này vì em đang gặp một số lỗi khi train yolo với data này.
    Nhưng mà em có góp ý nho nhỏ ạ, bởi vì Vingroup có nhiều P&L về AI nên có thể mọi người sẽ hơi nhầm lẫn tý. Bộ dataset anh dùng ở trên là bộ dataset public của VIn BIgdata, còn VinAI là 1 P&L hoạt động độc lập khác của VIn.
    Em mong anh có thể dành chút thời gian để chỉnh sửa lại bài viết để mọi người đỡ nhầm <3

  2. Dạ Anh ơi. em làm đến cái bước #Copy images và labels to train folder :
    ngây chỗ dòng code : for file in total_files:
    Nó báo lỗi là : TypeError: ‘int object is not iterable. Mong anh chỉ em ạ. em cám ơn ạ

Leave a Reply

Your email address will not be published. Required fields are marked *