⭐ Vision Engineer · Chặng 1Bài 9/11Bài học~75 phút

NumPy cơ bản: mảng, slicing, mặt nạ và vector hóa

shape, dtype, cắt mảng theo [hàng, cột], view và copy, mặt nạ boolean, axis, broadcasting: nền tảng trước khi xử lý ảnh.

NumPy cơ bản: mảng, slicing, mặt nạ và vector hóa

Học xong bài này, bạn sẽ

  • Tạo mảng, hiểu shape và dtype
  • Cắt mảng bằng slicing và mặt nạ boolean
  • Thay vòng lặp bằng phép toán vector hóa

Nên học trước: Xử lý thư mục ảnh, CSV và log

Nội dung bài viết
  1. 1. Tạo mảng
  2. 2. shape, ndim, dtype: ba thuộc tính phải nhìn đầu tiên
  3. 3. Truy cập và cắt mảng
  4. 4. Vector hóa: bỏ vòng lặp
  5. 5. Mặt nạ boolean: lọc theo điều kiện
  6. 6. Thống kê và trục (axis)
  7. 7. Broadcasting: phép toán giữa mảng khác kích thước
  8. 8. reshape, nối mảng
  9. Tóm tắt
  10. Bài tập

Ảnh, dữ liệu cảm biến, đầu ra của mô hình AI: tất cả đều là mảng số. NumPy là thư viện làm việc với mảng số nhanh gấp hàng chục đến hàng trăm lần so với list Python, và là nền móng của OpenCV, PyTorch, scikit-learn. Bài này dạy những gì bạn cần trước khi chạm vào ảnh: tạo mảng, shape, dtype, cắt mảng, mặt nạ boolean và tư duy vector hóa.

Cài đặt (trong môi trường ảo đã tạo ở bài trước):

pip install numpy

1. Tạo mảng

import numpy as np

a = np.array([12.48, 12.51, 12.47, 12.52])        # từ list
z = np.zeros((3, 4))                               # mảng 3 hàng 4 cột toàn số 0
o = np.ones(5, dtype=np.uint8)                     # 5 số 1 kiểu uint8
r = np.arange(0, 1, 0.25)                          # giống range nhưng cho số thực
l = np.linspace(0, 100, 5)                         # 5 điểm cách đều từ 0 đến 100

print(a)
print(z.shape, o, r, l)
[12.48 12.51 12.47 12.52]
(3, 4) [1 1 1 1 1] [0.   0.25 0.5  0.75] [  0.  25.  50.  75. 100.]

Quy ước chung là import numpy as np, mọi tài liệu và ví dụ trên mạng đều viết như vậy.

2. shape, ndim, dtype: ba thuộc tính phải nhìn đầu tiên

m = np.array([[1, 2, 3],
              [4, 5, 6]])
print(m.shape)   # (2, 3): 2 hàng, 3 cột
print(m.ndim)    # 2 chiều
print(m.dtype)   # int64 (Windows có thể là int32)
print(m.size)    # 6 phần tử

Một ảnh xám là mảng 2 chiều (cao, rộng), ảnh màu là mảng 3 chiều (cao, rộng, 3). Khi gặp lỗi với ảnh, việc đầu tiên luôn là in shape và dtype.

dtypeKhoảng giá trịDùng cho
uint80 → 255Ảnh thông thường
uint160 → 65535Ảnh 12/16 bit từ camera công nghiệp, ảnh độ sâu
float32số thựcTính toán trung gian, đầu vào mô hình AI
boolTrue/FalseMặt nạ

3. Truy cập và cắt mảng

Với mảng 2 chiều, chỉ số viết [hàng, cột], slicing giống list và không gồm vị trí kết thúc:

m = np.arange(1, 21).reshape(4, 5)   # 20 số xếp thành 4 hàng 5 cột
print(m)
print(m[1, 2])        # hàng 1, cột 2 → 8
print(m[0])           # cả hàng 0
print(m[:, 1])        # cả cột 1
print(m[1:3, 2:5])    # hàng 1–2, cột 2–4: một "vùng con"
[[ 1  2  3  4  5]
 [ 6  7  8  9 10]
 [11 12 13 14 15]
 [16 17 18 19 20]]
8
[1 2 3 4 5]
[ 2  7 12 17]
[[ 8  9 10]
 [13 14 15]]

Dòng cuối chính là thao tác cắt ROI trên ảnh: anh[y1:y2, x1:x2]. Lưu ý thứ tự: hàng (y) trước, cột (x) sau, ngược với cách nói thông thường "x, y". Đây là nguồn lỗi số một của người mới.

View hay copy?

vung = m[1:3, 2:5]
vung[:] = 0          # sửa vùng con...
print(m[1])          # ...mảng gốc cũng bị đổi!

m = np.arange(1, 21).reshape(4, 5)
vung = m[1:3, 2:5].copy()
vung[:] = 0
print(m[1])          # copy() thì mảng gốc giữ nguyên
[ 6  7  0  0  0]
[ 6  7  8  9 10]

Slicing tạo ra view: một cửa sổ nhìn vào cùng vùng nhớ, không sao chép dữ liệu (nên rất nhanh). Muốn sửa vùng con mà không ảnh hưởng ảnh gốc, dùng .copy().

4. Vector hóa: bỏ vòng lặp

Phép toán trên mảng NumPy áp dụng cho mọi phần tử cùng lúc:

do_mm = np.array([12.48, 12.51, 12.47, 12.62, 12.50])
print(do_mm * 1000)              # đổi sang µm: [12480. 12510. ...]
print(do_mm - 12.50)             # độ lệch so với danh nghĩa
print(np.abs(do_mm - 12.50) > 0.10)   # mảng bool: phần tử nào ngoài dung sai
[12480. 12510. 12470. 12620. 12500.]
[-0.02  0.01 -0.03  0.12  0.  ]
[False False False  True False]

So sánh tốc độ với vòng lặp Python trên 1 triệu phần tử:

import time

x = np.random.default_rng(0).random(1_000_000)

t0 = time.perf_counter()
tong_lap = 0.0
for v in x:
    tong_lap += v * v
t1 = time.perf_counter()
tong_np = np.sum(x * x)
t2 = time.perf_counter()

print(f"Vòng lặp: {(t1 - t0) * 1000:.0f} ms, NumPy: {(t2 - t1) * 1000:.1f} ms")
print(np.isclose(tong_lap, tong_np))
Vòng lặp: 250 ms, NumPy: 4.0 ms
True

Con số cụ thể tùy máy, nhưng NumPy thường nhanh hơn vài chục lần, với phép chuyên dụng như np.dot(x, x) còn nhanh hơn nữa. Lần gọi NumPy đầu tiên trong chương trình có thể chậm hơn vì thư viện còn khởi động, nên khi đo tốc độ hãy chạy vài lần và lấy kết quả sau. Một ảnh 5 MP có 5 triệu pixel: duyệt bằng vòng lặp for là quá chậm cho dây chuyền. Quy tắc: khi thấy mình viết vòng lặp qua từng pixel, hãy dừng lại và tìm cách vector hóa.

5. Mặt nạ boolean: lọc theo điều kiện

do_mm = np.array([12.48, 12.51, 12.47, 12.62, 12.50, 12.36])
ngoai = np.abs(do_mm - 12.50) > 0.10      # mặt nạ

print(do_mm[ngoai])          # lấy các giá trị ngoài dung sai: [12.62 12.36]
print(ngoai.sum())           # đếm số True: 2
print(np.where(ngoai)[0])    # vị trí: [3 5]
print(np.where(ngoai, "NG", "OK"))   # chọn giá trị theo điều kiện
[12.62 12.36]
2
[3 5]
['OK' 'OK' 'OK' 'NG' 'OK' 'NG']

Kết hợp nhiều điều kiện dùng & (và), | (hoặc), ~ (phủ định), và bắt buộc đặt từng điều kiện trong ngoặc: (a > 10) & (a < 20). Dùng and/or với mảng sẽ báo lỗi.

6. Thống kê và trục (axis)

rng = np.random.default_rng(1)
# 3 ca sản xuất × 4 lần đo mỗi ca
do = np.round(12.5 + rng.normal(0, 0.03, size=(3, 4)), 3)
print(do)
print("TB toàn bộ:", do.mean().round(4))
print("TB theo ca (axis=1):", do.mean(axis=1).round(4))
print("Độ lệch chuẩn theo lần đo (axis=0):", do.std(axis=0).round(4))
print("Max:", do.max(), "tại", np.unravel_index(do.argmax(), do.shape))

axis=0 gộp theo cột (đi dọc xuống các hàng), axis=1 gộp theo hàng. Với ảnh màu (cao, rộng, 3), anh.mean(axis=(0, 1)) cho độ sáng trung bình của từng kênh màu, một phép bạn sẽ dùng nhiều.

7. Broadcasting: phép toán giữa mảng khác kích thước

anh_mau = np.full((2, 3, 3), 100, dtype=np.float32)   # ảnh 2x3 màu, mọi pixel = 100
he_so_kenh = np.array([1.2, 1.0, 0.8])                  # cân bằng trắng: nhân từng kênh
ket_qua = anh_mau * he_so_kenh                          # (2,3,3) × (3,) → (2,3,3)
print(ket_qua[0, 0])                                    # [120. 100.  80.]

NumPy tự "kéo giãn" mảng nhỏ cho khớp mảng lớn khi các chiều cuối tương thích. Đây là cách chuẩn hóa ảnh trước khi đưa vào mô hình AI: (anh - mean) / std với mean, std là mảng 3 phần tử.

8. reshape, nối mảng

v = np.arange(12)
print(v.reshape(3, 4).shape)        # (3, 4)
print(v.reshape(2, -1).shape)       # -1: NumPy tự tính → (2, 6)
a, b = np.zeros((2, 3)), np.ones((2, 3))
print(np.vstack([a, b]).shape)      # chồng theo chiều dọc → (4, 3)
print(np.hstack([a, b]).shape)      # ghép ngang → (2, 6)

Tóm tắt

  • Luôn kiểm tra shape và dtype. Ảnh: (cao, rộng) hoặc (cao, rộng, 3), thường uint8.
  • Chỉ số [hàng, cột] = [y, x]; slicing tạo view, cần .copy() nếu muốn độc lập.
  • Vector hóa thay cho vòng lặp; mặt nạ boolean để lọc; & | ~ với ngoặc.
  • axis chọn chiều để gộp; broadcasting cho phép toán giữa mảng khác kích thước.

Bài tập

Bài 1. Kiểm tra lô hàng

Tạo 1000 giá trị đo ngẫu nhiên phân phối chuẩn quanh 25,00 mm, độ lệch chuẩn 0,02 (rng.normal, seed 42). Không dùng vòng lặp, tính: trung bình, độ lệch chuẩn, số sản phẩm ngoài dung sai ±0,05 và vị trí của 3 sản phẩm đầu tiên bị NG.

Xem đáp án
import numpy as np

rng = np.random.default_rng(42)
do = rng.normal(25.00, 0.02, size=1000)
ng = np.abs(do - 25.00) > 0.05
print(f"TB {do.mean():.4f}, std {do.std():.4f}")
print("Số NG:", ng.sum())
print("3 vị trí NG đầu tiên:", np.where(ng)[0][:3])

Với độ lệch chuẩn 0,02 thì giới hạn ±0,05 là ±2,5σ, nên lý thuyết khoảng 1,2% sản phẩm nằm ngoài, tức khoảng 12 trên 1000. Kết quả của bạn sẽ quanh con số này.

Bài 2. "Ảnh" bàn cờ

Tạo mảng uint8 8×8, ô (hàng + cột) chẵn có giá trị 255, ô lẻ có giá trị 0, không dùng vòng lặp. Gợi ý: np.indices hoặc broadcasting giữa np.arange(8)[:, None] và np.arange(8).

Xem đáp án
import numpy as np

hang = np.arange(8)[:, None]      # shape (8, 1)
cot = np.arange(8)                # shape (8,)
ban_co = np.where((hang + cot) % 2 == 0, 255, 0).astype(np.uint8)
print(ban_co[:3])
[[255   0 255   0 255   0 255   0]
 [  0 255   0 255   0 255   0 255]
 [255   0 255   0 255   0 255   0]]

Đây chính là ảnh bàn cờ dùng để hiệu chuẩn camera ở chặng 8 (dĩ nhiên với ô lớn hơn).

Bài 3. Thống kê theo lưới ô

Tạo mảng 480×640 số ngẫu nhiên 0–255 (seed 0). Chia thành lưới 3×4 ô (mỗi ô 160×160) và tính giá trị trung bình của từng ô, kết quả là mảng 3×4. Thử làm bằng reshape thay vì vòng lặp.

Xem đáp án
import numpy as np

anh = np.random.default_rng(0).integers(0, 256, size=(480, 640)).astype(np.float32)
o = 160
tb_o = anh.reshape(480 // o, o, 640 // o, o).mean(axis=(1, 3))
print(tb_o.shape)        # (3, 4)
print(tb_o.round(1))

reshape(3, 160, 4, 160) tách mỗi chiều thành (số ô, kích thước ô); lấy trung bình theo trục 1 và 3 (bên trong ô). Kỹ thuật này dùng để phát hiện vùng ảnh bị tối cục bộ, ví dụ khi một đèn trong dãy đèn bị hỏng.

Bài tiếp theo: áp dụng mọi thứ vừa học lên ảnh thật, khi ảnh chỉ là một mảng số.

Bài tiếp theo
Ban biên tập HọcFree

Chúng tôi viết những bài hướng dẫn dễ hiểu, thực tế để ai cũng có thể tự học lập trình và ngoại ngữ, hoàn toàn miễn phí.