1. Chuẩn bị môi trường phát triển
Để bắt đầu, bạn cần cài đặt các thư viện cần thiết trong môi trường ảo (virtual environment) của PyCharm. Mở terminal tại thư mục dự án và chạy các lệnh sau để cài đặt PyQt5 và các công cụ hỗ trợ thiết kế giao diện:
pip install PyQt5
pip install pyqt5-tools
Sau khi cài đặt thành công, công cụ thiết kế giao diện Qt Designer sẽ có sẵn trong thư mục venv/Lib/site-packages/pyqt5_tools.
2. Cấu hình công cụ ngoại vi (External Tools) trong PyCharm
Để tăng hiệu quả làm việc, hãy cấu hình phím tắt cho Qt Designer và trình chuyển đổi file UI ngay trong PyCharm:
- Qt Designer: Dùng để thiết kế giao diện kéo thả. Đường dẫn thường nằm tại
venv\Lib\site-packages\pyqt5_tools\designer.exe. - PyUIC: Công cụ chuyển đổi file
.uisang mã nguồn Python. Cấu hình Arguments:-o $FileNameWithoutExtension$.py $FileName$và Program trỏ tớipyuic5.exetrong thư mụcScripts.
3. Thiết kế giao diện người dùng (GUI)
Khởi động Qt Designer từ menu Tools của PyCharm. Tạo một Main Window mới. Kéo thả các widget cần thiết vào khung làm việc:
- QLabel: Dùng để hiển thị hình ảnh từ camera và kết quả xử lý. Đặt kích thước cố định (ví dụ 160x160) và đặt
objectNametương ứng (ví dụ:lbl_video,lbl_capture,lbl_result). - QPushButton: Tạo các nút bấm điều khiển như "Bật Camera", "Chụp ảnh", "Mở ảnh", "Xám hóa", "Phân ngưỡng". Đặt tên
objectNamerõ nghĩa để dễ quản lý.
Sử dụng cơ chế Signal/Slot của Qt để kết nối sự kiện clicked() của các nút bấm với các hàm xử lý tương ứng. Lưu file giao diện dưới tên mainwindow.ui.
4. Chuyển đổi và tách biệt logic chương trình
File .ui chỉ là mô tả giao diện dạng XML. Cần chuyển đổi nó sang mã Python bằng công cụ PyUIC đã cấu hình hoặc lệnh terminal:
pyuic5 -o ui_mainwindow.py mainwindow.ui
File ui_mainwindow.py sinh ra chứa class giao diện. Để giữ mã nguồn gọn gàng, không chỉnh sửa trực tiếp file này. Thay vào đó, tạo một file Python mới (ví dụ app_logic.py) để viết logic xử lý và kế thừa giao diện từ file vừa sinh.
5. Viết mã xử lý chính
Dưới đây là mã nguồn hoàn chỉnh tích hợp OpenCV để xử lý hình ảnh theo thời gian thực. Class VisionProcessor kế thừa từ cả QMainWindow và Ui_MainWindow (từ file UI), triển khai các chức năng: bật camera, chụp ảnh, chuyển thang xám và phân ngưỡng tự động Otsu.
import sys
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog
from PyQt5.QtCore import QTimer, Qt
from PyQt5.QtGui import QImage, QPixmap
# Import class giao diện từ file đã chuyển đổi
from ui_mainwindow import Ui_MainWindow
class VisionProcessor(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# Biến điều khiển camera
self.cam_device = cv2.VideoCapture(0)
self.is_streaming = False
self.frame_buffer = None
self.snapshot_data = None
# Thiết lập timer để cập nhật khung hình
self.fps_timer = QTimer(self)
self.fps_timer.timeout.connect(self.update_video_stream)
self.fps_timer.setInterval(30) # 30ms
# Kết nối các tín hiệu (Signals) với các hàm xử lý (Slots)
self.btn_stream.clicked.connect(self.toggle_stream)
self.btn_capture.clicked.connect(self.take_snapshot)
self.btn_load_img.clicked.connect(self.load_local_image)
self.btn_to_gray.clicked.connect(self.apply_grayscale)
self.btn_otsu.clicked.connect(self.apply_otsu_threshold)
def toggle_stream(self):
"""Bật hoặc tắt luồng video."""
if self.is_streaming:
self.fps_timer.stop()
self.btn_stream.setText("Bật Camera")
self.is_streaming = False
else:
self.fps_timer.start()
self.btn_stream.setText("Tắt Camera")
self.is_streaming = True
def update_video_stream(self):
"""Đọc khung hình từ camera và hiển thị lên Label."""
ret, frame = self.cam_device.read()
if ret:
self.frame_buffer = frame
# Chuyển đổi màu từ BGR (OpenCV) sang RGB (Qt)
rgb_img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
self.render_image(rgb_img, self.lbl_video)
def take_snapshot(self):
"""Lấy ảnh hiện tại từ luồng video."""
if self.frame_buffer is not None:
# Lưu bản copy để xử lý sau này
self.snapshot_data = self.frame_buffer.copy()
rgb_img = cv2.cvtColor(self.snapshot_data, cv2.COLOR_BGR2RGB)
self.render_image(rgb_img, self.lbl_capture)
def load_local_image(self):
"""Mở hộp thoại chọn file ảnh từ ổ cứng."""
file_path, _ = QFileDialog.getOpenFileName(self, "Chọn ảnh", "", "Image Files (*.jpg *.png *.bmp)")
if file_path:
self.snapshot_data = cv2.imread(file_path)
rgb_img = cv2.cvtColor(self.snapshot_data, cv2.COLOR_BGR2RGB)
self.render_image(rgb_img, self.lbl_capture)
def apply_grayscale(self):
"""Chuyển ảnh đang lưu sang thang xám."""
if self.snapshot_data is not None:
gray_img = cv2.cvtColor(self.snapshot_data, cv2.COLOR_BGR2GRAY)
self.render_image(gray_img, self.lbl_result, is_gray=True)
def apply_otsu_threshold(self):
"""Áp dụng phân ngưỡng Otsu."""
if self.snapshot_data is not None:
# Chuyển sang xám trước khi phân ngưỡng
gray_temp = cv2.cvtColor(self.snapshot_data, cv2.COLOR_BGR2GRAY)
_, binary_img = cv2.threshold(gray_temp, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
self.render_image(binary_img, self.lbl_result, is_gray=True)
def render_image(self, img_array, target_label, is_gray=False):
"""Hàm tiện ích hiển thị numpy array lên QLabel."""
h, w = img_array.shape[:2]
if is_gray:
# Ảnh xám hoặc nhị phân có 2 chiều (h, w)
bytes_per_line = w
q_img = QImage(img_array.data, w, h, bytes_per_line, QImage.Format_Indexed8)
else:
# Ảnh màu có 3 chiều (h, w, channels)
bytes_per_line = 3 * w
q_img = QImage(img_array.data, w, h, bytes_per_line, QImage.Format_RGB888)
# Scale ảnh vừa khít với Label và giữ tỉ lệ
pixmap = QPixmap.fromImage(q_img).scaled(target_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
target_label.setPixmap(pixmap)
def closeEvent(self, event):
"""Giải phóng tài nguyên camera khi đóng app."""
self.cam_device.release()
super().closeEvent(event)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = VisionProcessor()
window.show()
sys.exit(app.exec_())