THẾ GIỚI ĐIỂM ẢNH & BẮT LUỒNG VIDEO TRỰC TIẾP

PHẦN 1: BẢN CHẤT CỦA ẢNH SỐ (DƯỚI GÓC NHÌN LẬP TRÌNH)

1. Máy tính nhìn bức ảnh như thế nào?

  • Với con người, ảnh là màu sắc và hình dạng. Nhưng với máy tính, một bức ảnh thực chất là một bảng số (Ma trận - Matrix 2D/3D).

  • Mỗi ô vuông nhỏ nhất trên màn hình gọi là một Điểm ảnh (Pixel - Picture Element).

  • Mỗi điểm ảnh được gán một giá trị số biểu thị độ sáng hoặc màu sắc:

    • Ảnh đen trắng (Grayscale): Mỗi pixel là 1 con số từ 0 (Đen tuyền) đến 255 (Trắng tinh).

    • Ảnh màu: Cần tới 3 con số ghép lại để tạo ra màu hoàn chỉnh.

Plaintext
       Ảnh Grayscale (1 Kênh)                Ảnh Màu (3 Lớp Ma Trận Xếp Chồng)
       [ 0   12  255 ]                                 [ B ]
       [ 45  128  90 ]                           [ G ]
       [ 200 210  10 ]                     [ R ]

2. Sự khác biệt: RGB vs BGR

  • Đa số chúng ta quen với hệ màu RGB (Red - Green - Blue).

  • Tuy nhiên, thư viện OpenCV mặc định đọc ảnh theo thứ tự BGR (Blue - Green - Red). Đây là chi tiết thú vị mà các lập trình viên mới làm quen với OpenCV hay bị nhầm (khiến ảnh hiển thị người bị ám xanh da trời).

3. Video là gì?

Video không phải thứ gì ma thuật; video đơn giản là một chuỗi các bức ảnh tĩnh được chụp và chiếu liên tiếp thật nhanh (thường từ 24 đến 60 ảnh/giây, gọi là FPS - Frames Per Second). Con mắt người có hiện tượng lưu ảnh nên ta thấy chuyển động mượt mà.

PHẦN 2: THIẾT LẬP MÔI TRƯỜNG LẬP TRÌNH

Hai bố con mở Terminal (hoặc Command Prompt) trong máy tính và gõ lệnh sau để cài đặt 2 thư viện nền tảng:

Bash
pip install opencv-python numpy

  • opencv-python: Thư viện xử lý thị giác máy tính chính (import cv2).

  • numpy: Thư viện xử lý ma trận và mảng số học cực nhanh của Python.

PHẦN 3: VIẾT CHƯƠNG TRÌNH ĐẦU TIÊN BẰNG TƯ DUY OOP

Giả định đã có nền tảng hướng đối tượng (OOP), ta sẽ không viết code tuần tự theo dạng script rời rạc mà đóng gói thành một lớp CameraApp. Cách này giúp code gọn gàng, dễ mở rộng sang các buổi sau (như truyền qua Socket).

Tạo một file tên là buoi1_camera.py:

Python
import cv2
import os
from datetime import datetime

class CameraApp:
    def __init__(self, camera_index=0):
        """Khởi tạo camera và thư mục lưu trữ ảnh"""
        self.camera_index = camera_index
        self.cap = cv2.VideoCapture(self.camera_index)
        self.output_dir = "captured_photos"
        
        # Tạo thư mục lưu ảnh nếu chưa tồn tại
        if not os.path.exists(self.output_dir):
            os.makedirs(self.output_dir)

    def run(self):
        """Vòng lặp đọc và hiển thị từng khung hình (frame)"""
        if not self.cap.isOpened():
            print(f"❌ Không thể mở camera index {self.camera_index}")
            return

        print(" Camera đang chạy...")
        print(" -> Nhấn [SPACE] để chụp ảnh")
        print(" -> Nhấn [Q] để thoát chương trình")

        while True:
            # 1. Đọc một khung hình từ camera
            # ret: True/False (có đọc được không), frame: ma trận ảnh
            ret, frame = self.cap.read()
            if not ret:
                print("❌ Mất kết nối khung hình.")
                break

            # 2. Hiển thị thông số kích thước khung hình lên góc ảnh
            height, width, channels = frame.shape
            info_text = f"Size: {width}x{height} | Channels: {channels}"
            cv2.putText(frame, info_text, (20, 30), 
                        cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)

            # 3. Hiển thị ảnh ra cửa sổ giao diện
            cv2.imshow("Buoi 1: OpenCV Stream", frame)

            # 4. Bắt sự kiện bàn phím
            key = cv2.waitKey(1) & 0xFF
            
            # Nhấn 'q' để thoát
            if key == ord('q'):
                break
            # Nhấn phím cách (Space) để chụp ảnh
            elif key == ord(' '):
                self.take_photo(frame)

        # 5. Giải phóng camera và đóng tất cả cửa sổ khi kết thúc
        self.cleanup()

    def take_photo(self, frame):
        """Lưu khung hình hiện tại thành file ảnh có gắn thời gian"""
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"photo_{timestamp}.jpg"
        filepath = os.path.join(self.output_dir, filename)
        
        # Ghi ảnh ra đĩa
        cv2.imwrite(filepath, frame)
        print(f"📸 Đã lưu ảnh: {filepath}")

    def cleanup(self):
        """Dọn dẹp tài nguyên phần cứng"""
        self.cap.release()
        cv2.destroyAllWindows()
        print(" Đã tắt camera thành công.")

if __name__ == "__main__":
    app = CameraApp(camera_index=0)
    app.run()

 

 

PHẦN 4: THỬ THÁCH NHỎ CHO BÉ (MINI-CHALLENGES)

Sau khi chạy thành công chương trình trên, anh hãy đưa ra 2 bài toán nhỏ để bé tự mày mò sửa code:

  1. Thử thách 1 (Khám phá Kênh màu BGR):

    • Nhiệm vụ: Tìm cách chuyển camera sang ảnh đen trắng (Grayscale) trước khi hiển thị ra màn hình.

    • Gợi ý cho con: Tìm hiểu hàm cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY).

  2. Thử thách 2 (Gương phản chiếu - Mirror mode):

    • Hiện tượng: Khi giơ tay phải, camera hiển thị như người đối diện (bị ngược với nhìn gương).

    • Nhiệm vụ: Làm cho video lật ngang như gương thật.

    • Gợi ý cho con: Dùng hàm cv2.flip(frame, 1).

HƯỚNG DẪN BẢN CHẤT CHO BÉ

1. Thử thách 1: Chuyển sang ảnh đen trắng (Grayscale)

  • Bản chất toán học: Một điểm ảnh màu cần 3 giá trị $(B, G, R)$. Khi chuyển sang thang xám (Grayscale), OpenCV tính độ sáng trung bình có trọng số của 3 kênh theo công thức:

    $$Y = 0.299 \cdot R + 0.587 \cdot G + 0.114 \cdot B$$

    (Mắt người nhạy cảm nhất với màu xanh lá $G$, nên hệ số của $G$ lớn nhất).

  • Cú pháp:

    Python
    gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
  • Lưu ý thú vị khi bé kiểm tra kích thước:

    • frame.shape ban đầu là (height, width, 3).

    • gray_frame.shape chỉ còn (height, width) vì ma trận lúc này chỉ có 1 kênh duy nhất (tiết kiệm $2/3$ dung lượng bộ nhớ).

2. Thử thách 2: Chế độ lật gương (Mirror mode)

  • Bản chất ma trận: Lật ngang thực chất là đảo thứ tự các cột trong ma trận ảnh (cột đầu tiên đổi chỗ cho cột cuối cùng).

  • Cú pháp:

    Python
    mirrored_frame = cv2.flip(frame, 1)
    
    • Tham số thứ hai (flipCode):

      • 1: Lật theo trục dọc (lật ngang trái $\leftrightarrow$ phải, giống soi gương).

      • 0: Lật theo trục ngang (lật ngược trên $\updownarrow$ dưới).

      • -1: Lật cả 2 trục (vừa đảo ngược vừa đảo trái phải).

💻 CODE HOÀN CHỈNH NÂNG CẤP VỚI CÁC PHÍM TẮT

Anh và bé có thể cập nhật file buoi1_camera.py để bổ sung 2 thuộc tính trạng thái self.is_mirror và self.is_gray:

  • Nhấn phím M: Bật/tắt chế độ lật gương.

  • Nhấn phím G: Bật/tắt chế độ Grayscale (đen trắng).

  • Nhấn SPACE: Chụp ảnh đúng theo hiệu ứng đang hiển thị.

import cv2
import os
from datetime import datetime

class CameraApp:
    def __init__(self, camera_index=0):
        self.camera_index = camera_index
        self.cap = cv2.VideoCapture(self.camera_index)
        self.output_dir = "captured_photos"
        
        # Biến trạng thái (Flags) điều khiển hiệu ứng
        self.is_mirror = True   # Mặc định bật chế độ gương cho tự nhiên
        self.is_gray = False    # Mặc định là ảnh màu
        
        if not os.path.exists(self.output_dir):
            os.makedirs(self.output_dir)

    def run(self):
        if not self.cap.isOpened():
            print(f"❌ Không thể mở camera index {self.camera_index}")
            return

        print(" Camera đang chạy...")
        print(" -> Phím [M]: Bật/Tắt chế độ gương (Mirror)")
        print(" -> Phím [G]: Bật/Tắt ảnh đen trắng (Grayscale)")
        print(" -> Phím [SPACE]: Chụp ảnh")
        print(" -> Phím [Q]: Thoát chương trình")

        while True:
            ret, frame = self.cap.read()
            if not ret:
                print("❌ Mất kết nối khung hình.")
                break

            # 1. Xử lý lật gương nếu đang bật chế độ Mirror
            if self.is_mirror:
                frame = cv2.flip(frame, 1)

            # 2. Xử lý chuyển thang xám nếu đang bật chế độ Grayscale
            if self.is_gray:
                display_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                # Lấy kích thước ảnh 1 kênh (không có channels trong tuple shape)
                height, width = display_frame.shape
                channels = 1
            else:
                display_frame = frame
                height, width, channels = display_frame.shape

            # 3. Hiển thị thông số và trạng thái lên góc ảnh
            status_text = f"Size: {width}x{height} | Channels: {channels} | Mirror: {self.is_mirror} | Gray: {self.is_gray}"
            cv2.putText(display_frame, status_text, (20, 30), 
                        cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

            # 4. Hiển thị khung hình
            cv2.imshow("Buoi 1: OpenCV Stream", display_frame)

            # 5. Bắt phím bấm
            key = cv2.waitKey(1) & 0xFF
            
            if key == ord('q'):
                break
            elif key == ord(' '):
                self.take_photo(display_frame)
            elif key == ord('m'):
                self.is_mirror = not self.is_mirror
                print(f"🪞 Chế độ gương: {'BẬT' if self.is_mirror else 'TẮT'}")
            elif key == ord('g'):
                self.is_gray = not self.is_gray
                print(f" Chế độ thang xám: {'BẬT' if self.is_gray else 'TẮT'}")

        self.cleanup()

    def take_photo(self, frame):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        filename = f"photo_{timestamp}.jpg"
        filepath = os.path.join(self.output_dir, filename)
        
        cv2.imwrite(filepath, frame)
        print(f"📸 Đã lưu ảnh: {filepath}")

    def cleanup(self):
        self.cap.release()
        cv2.destroyAllWindows()
        print(" Đã tắt camera thành công.")

if __name__ == "__main__":
    app = CameraApp(camera_index=1)
    app.run()