1. Ý tưởng
Trong quá trình làm một dự án về AI thì phần mất nhiều thời gian cho project nhất chính là xử lý dữ liệu. Chính vì vậy có nhiều công cụ (tool) được tạo ra để giảm tải cho quá trình này. Xuất phát từ bài toán nhận diện khuôn mặt, mình có ý tưởng xây dựng một công cụ tự động cắt ảnh chỉ chứa phần khuôn mặt cho ảnh chân dung. Mục đích để xử lý loại bỏ phần tóc tai, cổ, áo và phần thân thừa, chỉ giữ lại phần ảnh đặc trưng khuôn mặt với vùng mặt chứa hai mắt, mũi, miệng. Mỗi bức ảnh sau khi qua tool xử lý sẽ được lưu lại thành ảnh mới.
Ta chỉ cần thu thập ảnh cho vào thư mục, chọn thư mục đầu ra, và tool sẽ cắt ảnh tự động. Thư mục dữ liệu ảnh sẽ có hai loại:
- Thư mục lớn chứa nhiều thư mục nhỏ với tên đã được đánh nhãn riêng ứng với nội dung ảnh trong thư mục nhỏ.
Thư mục "data" chứa các thư mục nhỏ chứa ảnh, tên từng thư mục được ghi tên của người nổi tiếng.
Sau khi được tool xử lý sẽ trả về các ảnh chỉ có phần khuôn mặt, giữ nguyên tên file ban đầu và nằm đúng theo thư mục nhỏ với tên ban đầu. - Thư mục chỉ chứa ảnh
Một thư mục chỉ chứa ảnh của Cha Eun Woo Cũng giống như trên, khi được tool xử lý sẽ trả về các ảnh chỉ có phần khuôn mặt, giữ nguyên tên file ban đầu.Với việc cắt ảnh như vậy giúp tiền xử lý ảnh về khuôn mặt nhanh và đỡ tốn công sức hơn. Có thể ứng dụng hỗ trợ xử lý dữ liệu ảnh để phục vụ train model cho các bài toán về nhận dạng khuôn mặt, phân loại cảm xúc, giới tính, dự đoán độ tuổi...
2. Giới thiệu về MTCNN và PyQt5
Chúng ta sẽ sử dụng MTCNN để phát hiện khuôn mặt và cắt khuôn mặt ra từ ảnh gốc và lưu lại thành một bức ảnh khác. MTCNN hiện nay đang là phương pháp detect face nhanh, tốt và đơn giản hơn so với dlib hay Haar Cascade. Sau khi hoàn thành được phần lõi của tool rồi, chúng ta tiến hành làm giao diện cho tool với PyQt5. Một trong những GUI desktop app cho python dễ làm và được ưa chuộng nhất hiện nay.
2.1 Phát hiện khuôn mặt với MTCNN
Khái niệm: MTCNN là viết tắt của Multi-task Cascaded Convolutional Networks. Nó là bao gồm 3 mạng CNN xếp chồng và đồng thời hoạt động khi detect khuôn mặt. Mỗi mạng có cấu trúc khác nhau và đảm nhiệm vai trò khác nhau trong task. Đầu ra của MTCNN là vị trí khuôn mặt và các điểm trên mặt như: mắt, mũi, miệng…
MTCNN hoạt động theo 3 bước, mỗi bước có một mạng neural riêng lần lượt là: P-Net, R-Net và O-net
Đầu vào hình ảnh được resize thành nhiều kích thước tạo thành một Image Pyramid. Sau đó pyramid sẽ được đưa vào P-Net:
- P-Net sử dụng 1 kernel 12x12 chạy qua mỗi bức hình để tìm kiếm khuôn mặt.
- Sau lớp convolution layer 3 thì mạng chia làm 2 lớp: convolution 4-1 để xác một định khuôn mặt nằm trong mỗi bounding boxes và convolution 4-2 cung cấp các tọa độ của của các bounding boxes.
- P-Net có nhiệm vụ là xác định các window ảnh bao gồm mặt người nhưng lại lấy nhiều, nhanh và thiếu chính xác.
R-Net: có cấu trúc tương tự P-Net nhưng có nhiều layer hơn. Nó sử dụng các bounding boxes được cung cấp từ R-Net. Và tương tự R-Net, P-Net chia làm 2 layer ở bước cuối, cung cấp 2 đầu ra đó là tọa độ mới của các bounding boxes, cùng độ tin tưởng của nó.
O-Net: lấy các bounding boxes từ R-Net làm đầu vào và đánh dấu các tọa độ của các mốc trên khuôn mặt. Kết quả 3 đầu ra bao gồm: xác suất của khuôn mặt nằm trong bounding box, tọa độ của bounding box và tọa độ của các mốc trên khuôn mặt (vị trí mắt, mũi, miệng)
Demo phát hiện khuôn mặt MTCNN: để sử dụng MTCCN thì rất đơn giản, chúng ta chỉ cần import thư viện, khởi tạo và gọi hàm detect_faces. MTCNN sẽ trả về một danh sách khuôn mặt với các khuôn phát hiện được. Với mỗi khuôn mặt sẽ có bounding box với tọa độ góc trái trên (x,y) và chiều rộng (width) và dài (height) của box; các tọa độ điểm mắt trái-phải, mũi, mép miệng trái-phải và xác suất có khuôn mặt.
2.2 Giao diện ứng dụng với PyQt5
Qt là một Application framework đa nền tảng viết trên ngôn ngữ C++ , được dùng để phát triển các ứng dụng trên desktop, hệ thống nhúng và mobile. Hỗ trợ cho các platform bao gồm : Linux, OS X, Windows, VxWorks, QNX, Android, iOS, BlackBerry, Sailfish OS và một số platform khác. PyQt là Python interface của Qt, kết hợp của ngôn ngữ lập trình Python và thư viện Qt, là một thư viện bao gồm các thành phần giao diện điều khiển (widgets , graphical control elements). PyQt API bao gồm các module bao gồm số lượng lớn với các classes và functions hỗ trợ cho việc thiết kế ra các giao diện giao tiếp với người dùng của các phần mềm chức năng. Hỗ trợ với Python 2.x và 3.x.
Để cài đặt PyQt5 ta mở cmd và gõ lệnh:
pip install PyQt5
Qt Designer: Qt sử dụng IDE tên Qt Creator với một tool thiết kế giao diện người dùng Qt Designer. Qt Designer có thể làm việc một mình độc lập với Qt Creator . Qt Designer sử dụng XML .ui file để lưu thiết kế và không sinh thêm bất kỳ mã nguồn nào của nó. User Interface Compiler (uic) đọc định dạng file XML (.ui) và xuất ra header file mã nguồn C++ tươn ứng. Qt có một class QUiLoader cho phép một ứng dụng tải một file .ui và tạo một giao diện động tương ứng.
Thiết kế giao diện với Qt Designer chúng ta chỉ cần tạo window main, sau đó kéo thả các object tương ứng từ cột bên trái sang. Đổi tên các obeject cho dễ nhớ khi bắt sự kiện trong code về sau. Sau khi thiết kế giao diện xong rồi, ta chuyển file .ui sang file .py để bắt sự kiện cho các object trong GUI bằng lệnh:
pyuic5 -x "tên file ui" -o "tên file py"
3. Xây dựng công cụ
Mình sử dụng IDE là Visual Studio Code để code tool này. Trước hết ta tạo cây thư mục cho project như sau:
Folder "data_in" chứa dữ liệu là các ảnh đầu vào được chia vào các folder nhỏ hơn, được ghi tên ứng với ảnh của người trong folder nhỏ. Folder "data_out" là folder để chứa ảnh sau khi tool xử lý xong. "images" là thư mục các ảnh dùng để hiện thị ở phần GUI cho tool. File "crop_face.py" là file code phát hiện khuôn mặt bằng MTCNN. Còn "main.py" là file để code bắt sự kiện cho GUI được tạo bởi PyQt5 designer.
3.1 Code phát hiện khuôn mặt và cắt ảnh
Đầu tiên là file code xử lý phát hiện khuôn mặt và lưu lại thành ảnh mới. Tại file crop_face.py sử dụng MTCNN để phát hiện khuôn mặt bằng hàm detect_face(). Với tham số truyền vào là đường dẫn file ảnh, đường dẫn folder lưu ảnh ra, tên ảnh. Ta dùng thư viện opencv để đọc và ghi ảnh mới sau khi được crop.
detector = MTCNN()
def detect_face(image, folder_out, file_name):
img = cv2.imread(image)
faces = detector.detect_faces(img)
for face in faces:
bounding_box = face['box']
im = img[ bounding_box[1]:bounding_box[1]+bounding_box[3],
bounding_box[0]:bounding_box[0]+bounding_box[2]]
file_path = folder_out +"/"+ file_name
cv2.imwrite(file_path, im)
Tiếp đó là việc lấy từng file ảnh cho vào hàm detect_face(). Như đã giới thiệu ở phần ý tưởng, có 2 loại thư mục đầu vào, với loại 1 thư mục lớn chứa nhiều thư mục nhỏ được gắn tên. Ta code một hàm crop_face_many_folders() lấy đường dẫn thư mục nhỏ rồi vào từng thư mục nhỏ để lấy đường dẫn của ảnh. Sử dụng thư viện os để lấy đường dẫn và tạo folder nhỏ mới bên trong folder_out (folder lớn chứa ảnh đầu ra).
#data_folder_in = "./data_in"
#data_folder_out = "./data_out"
def crop_face_many_folders(data_folder_in, data_folder_out):
for folder_name in os.listdir(data_folder_in):
os.mkdir(data_folder_out+"/"+folder_name)
folder_path = os.path.join(data_folder_in,folder_name)
for image_name in os.listdir(folder_path):
image_path = os.path.join(data_folder_in,folder_name,image_name)
folder_out = data_folder_out + "/" + folder_name
detect_face(image_path,folder_out,image_name)
Đối với loại thư mục đầu vào số 2 chỉ chứa ảnh, ta chỉ cần dùng một vòng for để lấy đường dẫn ảnh với hàm crop_face_one_folder():
#image_folder_in = ".\data_in\Amber Heard"
#image_folder_out =".\data_out\Amber"
def crop_face_one_folder(image_folder_in, image_folder_out):
for image_name in os.listdir(image_folder_in):
image_path = os.path.join(image_folder_in,image_name)
detect_face(image_path,image_folder_out,image_name)
File crop_face.py hoàn chỉnh như sau:
from mtcnn import MTCNN
import cv2
import os
detector = MTCNN()
def detect_face(image, folder_out, file_name):
img = cv2.imread(image)
faces = detector.detect_faces(img)
for face in faces:
bounding_box = face['box']
im = img[ bounding_box[1]:bounding_box[1]+bounding_box[3],
bounding_box[0]:bounding_box[0]+bounding_box[2]]
file_path = folder_out +"/"+ file_name
cv2.imwrite(file_path, im)
#data_folder_in = "./data_in"
#data_folder_out = "./data_out"
def crop_face_many_folders(data_folder_in, data_folder_out):
for folder_name in os.listdir(data_folder_in):
os.mkdir(data_folder_out+"/"+folder_name)
folder_path = os.path.join(data_folder_in,folder_name)
for image_name in os.listdir(folder_path):
image_path = os.path.join(data_folder_in,folder_name,image_name)
folder_out = data_folder_out + "/" + folder_name
detect_face(image_path,folder_out,image_name)
#image_folder_in = ".\data_in\Amber Heard"
#image_folder_out =".\data_out\Amber"
def crop_face_one_folder(image_folder_in, image_folder_out):
for image_name in os.listdir(image_folder_in):
image_path = os.path.join(image_folder_in,image_name)
detect_face(image_path,image_folder_out,image_name)
3.2 Code giao diện cho tool
Sau khi tải và cài đặt QtDesigner, ta thiết kế giao diện cho tool sẽ có 3 tab. Tab 1 sẽ là phần tool1 dành cho cắt ảnh từ thư mục đầu vào loại 1 (thư mục lớn chứa nhiều thư mục nhỏ có ảnh), nút "Crop" bắt sự kiện click đến hàm crop_face_many_folders().
pyuic5 -x view.ui -o main.py
self.button_choose1_tool1.setObjectName("button_choose1_tool1")
self.button_choose1_tool1.clicked.connect(self.chooseFolderInput1) #event
self.button_choose2_tool1.setObjectName("button_choose2_tool1")
self.button_choose2_tool1.clicked.connect(self.chooseFolderOutput1) #event
self.button_crop_tool1.setObjectName("button_crop_tool1")
self.button_crop_tool1.clicked.connect(self.cropTool1) #event
self.button_choose1_tool2.setObjectName("button_choose1_tool2")
self.button_choose1_tool2.clicked.connect(self.chooseFolderInput2) #event
self.button_choose2_tool2.setObjectName("button_choose2_tool2")
self.button_choose2_tool2.clicked.connect(self.chooseFolderOutput2) #event
self.button_crop_tool2.setObjectName("button_crop_tool2")
self.button_crop_tool2.clicked.connect(self.cropTool2) #event
Và tương ứng chúng ta sẽ có các hàm để thực thi với sự kiện click nút: #function for events
def chooseFolderInput1(self):
folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, "Select choose folder")
self.folder_input_path_tool1.setText(folder_path)
def chooseFolderOutput1(self):
folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, "Select choose folder")
self.folder_output_path_tool1.setText(folder_path)
def chooseFolderInput2(self):
folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, "Select choose folder")
self.folder_input_path_tool2.setText(folder_path)
def chooseFolderOutput2(self):
folder_path = QtWidgets.QFileDialog.getExistingDirectory(None, "Select choose folder")
self.folder_output_path_tool2.setText(folder_path)
def cropTool1(self):
ans = QtWidgets.QMessageBox.question(None, 'Confirm', "Are you crop image?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if ans == QtWidgets.QMessageBox.Yes:
folder_data_in = self.folder_input_path_tool1.text()
folder_data_out = self.folder_output_path_tool1.text()
if folder_data_in=="" or folder_data_out=="":
QtWidgets.QMessageBox.about(None,"Error","Empty folder path")
else:
crop_face_many_folders(folder_data_in,folder_data_out)
QtWidgets.QMessageBox.about(None,"Successful","Cropped images successful!")
def cropTool2(self):
ans = QtWidgets.QMessageBox.question(None, 'Confirm', "Are you crop image?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if ans == QtWidgets.QMessageBox.Yes:
folder_image_in = self.folder_input_path_tool2.text()
folder_image_out = self.folder_output_path_tool2.text()
if folder_image_in=="" or folder_image_out=="":
QtWidgets.QMessageBox.about(None,"Error","Empty folder path")
else:
crop_face_one_folder(folder_image_in,folder_image_out)
QtWidgets.QMessageBox.about(None,"Successful","Cropped images successful!")
Với mỗi nút Crop ta thêm hộp thoại MessageBox.question để xác nhận việc người dùng muốn cắt ảnh, nếu ấn "Yes thì" sẽ thực hiện.
ans = QtWidgets.QMessageBox.question(None, 'Confirm', "Are you crop image?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
if ans == QtWidgets.QMessageBox.Yes:
Và kiểm tra xem người dùng đã chọn thư mục dữ liệu nhập và xuất chưa, nếu họ chọn thiếu, ta sẽ hiển thị hộp thoại MessageBox.about báo chưa chọn thư mục.
if folder_image_in=="" or folder_image_out=="":
QtWidgets.QMessageBox.about(None,"Error","Empty folder path")
Sau khi thực hiện xử lý cắt ảnh xong, thông báo cho người dùng quá trình đã hoàn thành:
QtWidgets.QMessageBox.about(None,"Successful","Cropped images successful!")
3.3 Kết quả
Chạy file main.py khởi động tool. Ta sẽ được giao diện sau:
Kết quả cắt ảnh thành công với tool2 với ảnh khuôn mặt Tổng thống Putin đã được cắt ra từ ảnh gốc lưu ở folder cũ Vladimir Vladimirovich Putin vào folder mới Putin được chọn.
Link github cho toàn bộ code: https://github.com/nguyenthuy1681999/Crop_Face_Image.git
4. Tạo file thực thi cho tool chạy trên hệ điều hành Windows
Thay vì phải chạy file .py trong IDE hoặc cmd, ta sẽ tạo file thực thi (file .exe) cho tool cắt ảnh khuôn mặt của chúng ta để nó chạy trên hệ điều hành Windows như các ứng dụng khác trên desktop. Đầu tiên ta phải install pyinstaller:
pip install pyinstaller
Sau đó chạy lệnh pyinstaller --onefile main.py
để tạo file exe
Sau khi thực thi xong thì trong thư mục dist/main/ sẽ có file main .exe như sau:
5. Tài liệu tham khảo
https://www.youtube.com/watch?v=M0Y9_vBmYXU
https://realpython.com/qt-designer-python/
https://code24h.com/tim-hieu-mtcnn-va-ap-dung-de-xac-dinh-vi-tri-cac-khuon-mat-d26908.htm
https://datatofish.com/executable-pyinstaller/