Độ sâu trường ảnh, khẩu độ và lấy nét bằng chỉ số độ nét
Số f, ánh sáng, DOF và nhiễu xạ bằng con số; lấy nét bằng phương sai Laplacian, Tenengrad; khóa và giám sát ống kính trong sản xuất.
Học xong bài này, bạn sẽ
- Hiểu đánh đổi khẩu độ, độ sáng, độ sâu trường ảnh
- Lấy nét bằng chỉ số độ nét
- Khóa ống kính ổn định trong sản xuất
Nên học trước: Chọn camera và ống kính: độ phân giải, FOV, tiêu cự
Nội dung bài viết
Ảnh mờ là một trong những lỗi phổ biến nhất của hệ thống Machine Vision mới lắp đặt, và là lỗi âm thầm nhất của hệ thống đã chạy vài tháng. Nguyên nhân thường nằm ở ba khái niệm gắn chặt với nhau: khẩu độ, độ sâu trường ảnh (DOF) và lấy nét. Bài trước, "Chọn camera và ống kính: độ phân giải, FOV, tiêu cự", đã chọn cảm biến và tiêu cự; bài này chọn nốt thông số cuối cùng của ống kính là khẩu độ, bằng con số: bao nhiêu ánh sáng mất đi, độ sâu trường ảnh được bao nhiêu, nhiễu xạ bắt đầu làm mờ ảnh từ đâu. Sau đó bạn sẽ lấy nét bằng chỉ số độ nét thay vì bằng mắt, và khóa ống kính để giữ nét qua nhiều năm sản xuất. Bài sau, "Ống kính telecentric: khi nào cần?", dùng lại công thức DOF của bài này.
1. Khẩu độ và số f: ánh sáng
Khẩu độ (aperture) là lỗ mở cho ánh sáng đi qua ống kính, điều chỉnh bằng vòng khẩu trên ống kính Machine Vision. Nó được ghi bằng số f: N = f / D (tiêu cự chia đường kính đồng tử vào). Số f càng lớn thì lỗ càng nhỏ. Lượng ánh sáng tới cảm biến tỉ lệ với diện tích lỗ, tức với 1/N²; các nấc chuẩn 1.4, 2, 2.8, 4, 5.6, 8, 11, 16 cách nhau một hệ số √2, nên mỗi nấc giảm một nửa ánh sáng.
CAC_NAC = [1.4, 2, 2.8, 4, 5.6, 8, 11, 16]
N_GOC, PHOI_SANG_GOC = 2.8, 100 # ở f/2.8, phơi sáng 100 µs cho ảnh đủ sáng
print(" N | ánh sáng so với f/2.8 | phơi sáng cần (µs) | nhòe ở 0,5 m/s, 0,05 mm/px (px)")
for N in CAC_NAC:
tuong_doi = (N_GOC / N) ** 2
t = PHOI_SANG_GOC / tuong_doi
print(f"f/{N:<4} | {tuong_doi:21.2f} | {t:18.0f} | {500 * t * 1e-6 / 0.05:10.1f}")
N | ánh sáng so với f/2.8 | phơi sáng cần (µs) | nhòe ở 0,5 m/s, 0,05 mm/px (px)
f/1.4 | 4.00 | 25 | 0.2
f/2 | 1.96 | 51 | 0.5
f/2.8 | 1.00 | 100 | 1.0
f/4 | 0.49 | 204 | 2.0
f/5.6 | 0.25 | 400 | 4.0
f/8 | 0.12 | 816 | 8.2
f/11 | 0.06 | 1543 | 15.4
f/16 | 0.03 | 3265 | 32.7
Từ f/2.8 đóng xuống f/8 mất khoảng 8 lần ánh sáng. Nếu vật đứng yên, bù bằng phơi sáng dài hơn là xong. Nếu vật chạy trên băng tải, phơi sáng khoảng 800 µs cho nhòe 8 pixel (bài "Đèn strobe và đồng bộ trigger"): phải bù bằng đèn mạnh hơn. Mỗi nấc khẩu độ đóng thêm là một khoản nợ ánh sáng phải trả bằng đèn, phơi sáng hoặc gain.
2. Độ sâu trường ảnh (DOF)
Khi lấy nét ở một mặt phẳng, điểm nằm trước hoặc sau mặt phẳng đó không hội tụ thành điểm trên cảm biến mà thành một đĩa mờ. DOF là khoảng dọc trục quang mà trong đó đĩa mờ còn nhỏ hơn một giới hạn chấp nhận được gọi là vòng mờ cho phép c (circle of confusion), đo trên cảm biến. Trong Machine Vision, c gắn với pixel: c = 1 pixel (khắt khe, cho đo lường) hoặc c = 2 pixel (thường dùng cho phát hiện lỗi). Ở khoảng cách làm việc gần, công thức gần đúng theo độ phóng đại là:
N_w = N · (1 + m) khẩu độ làm việc (working F-number)
DOF ≈ 2 · N_w · c / m² = 2 · N · c · (1 + m) / m²
m : độ phóng đại (kích thước cảm biến / FOV) c : vòng mờ cho phép trên cảm biến (mm)
Giải thích: phía cảm biến, độ sâu tiêu cự là ±N_w·c; độ phóng đại theo trục bằng m²,
nên phía vật dải này rộng gấp 1/m².
DOF tăng khi: đóng khẩu (N lớn), giảm độ phóng đại (FOV lớn hơn, pixel trên vật to hơn; DOF tăng theo 1/m², rất mạnh), và nới c (chấp nhận ảnh mềm hơn). Đóng khẩu không miễn phí: ngoài mất ánh sáng còn có nhiễu xạ. Ánh sáng qua lỗ tròn không hội tụ thành điểm mà thành đĩa Airy, đường kính tới vòng tối đầu tiên:
d_Airy = 2,44 · λ · N_w (λ: bước sóng; 550 nm = 0,55 µm cho ánh sáng xanh lá)
Ví dụ: λ = 0,55 µm, N = 8, m = 0,132 → N_w = 9,06 → d_Airy ≈ 12,2 µm ≈ 3,5 pixel 3,45 µm
Bảng dưới đây tính cho cấu hình của trạm kiểm tra bề mặt ở bài tập cuối chặng 6: cảm biến 5 MP pixel 3,45 µm, ống kính 35 mm ở khoảng 300 mm (m = 0,132, FOV khoảng 64 mm), và một cấu hình chụp cận cảnh m = 0,5 (FOV khoảng 17 mm) để so sánh:
P_UM, LAM_UM = 3.45, 0.55 # pixel (µm), bước sóng (µm)
def dof_mm(N, m, c_um):
return 2 * N * (1 + m) * c_um / 1000 / m ** 2
def airy_um(N, m, lam_um=LAM_UM):
return 2.44 * lam_um * N * (1 + m)
for m in (0.132, 0.5):
print(f"m = {m} (FOV ngang {2448 * P_UM / 1000 / m:.0f} mm, pixel trên vật {P_UM / m:.0f} µm)")
print(" N | DOF c=1px (mm) | DOF c=2px (mm) | Airy (µm) | Airy (px)")
for N in (2.8, 4, 5.6, 8, 11, 16):
a = airy_um(N, m)
canh_bao = " ← nhiễu xạ > 2 px" if a / P_UM > 2 else ""
print(f" f/{N:<4}| {dof_mm(N, m, P_UM):14.2f} | {dof_mm(N, m, 2 * P_UM):14.2f} |"
f" {a:9.1f} | {a / P_UM:9.1f}{canh_bao}")
N_toi_da = 2 * P_UM / (2.44 * LAM_UM * (1 + m))
print(f" Số f lớn nhất để Airy ≤ 2 px: f/{N_toi_da:.1f}")
m = 0.132 (FOV ngang 64 mm, pixel trên vật 26 µm)
N | DOF c=1px (mm) | DOF c=2px (mm) | Airy (µm) | Airy (px)
f/2.8 | 1.26 | 2.51 | 4.3 | 1.2
f/4 | 1.79 | 3.59 | 6.1 | 1.8
f/5.6 | 2.51 | 5.02 | 8.5 | 2.5 ← nhiễu xạ > 2 px
f/8 | 3.59 | 7.17 | 12.2 | 3.5 ← nhiễu xạ > 2 px
f/11 | 4.93 | 9.86 | 16.7 | 4.8 ← nhiễu xạ > 2 px
f/16 | 7.17 | 14.34 | 24.3 | 7.0 ← nhiễu xạ > 2 px
Số f lớn nhất để Airy ≤ 2 px: f/4.5
m = 0.5 (FOV ngang 17 mm, pixel trên vật 7 µm)
N | DOF c=1px (mm) | DOF c=2px (mm) | Airy (µm) | Airy (px)
f/2.8 | 0.12 | 0.23 | 5.6 | 1.6
f/4 | 0.17 | 0.33 | 8.1 | 2.3 ← nhiễu xạ > 2 px
f/5.6 | 0.23 | 0.46 | 11.3 | 3.3 ← nhiễu xạ > 2 px
f/8 | 0.33 | 0.66 | 16.1 | 4.7 ← nhiễu xạ > 2 px
f/11 | 0.46 | 0.91 | 22.1 | 6.4 ← nhiễu xạ > 2 px
f/16 | 0.66 | 1.32 | 32.2 | 9.3 ← nhiễu xạ > 2 px
Số f lớn nhất để Airy ≤ 2 px: f/3.4
- Với m = 0,132, DOF vài mm đến hơn chục mm, thoải mái cho phần lớn chi tiết phẳng. Ở f/4 và c = 2 pixel, DOF khoảng 3,6 mm, khớp con số đã dùng trong bài tập cuối chặng 6.
- Phóng đại lên m = 0,5 thì DOF giảm hơn 10 lần: chụp cận cảnh chi tiết nhỏ, DOF chỉ còn vài phần mười mm. Đây là lý do bài toán kiểm tra linh kiện nhỏ có chiều cao thay đổi (chân IC, mối hàn) rất khó.
- Nhiễu xạ đặt trần cho số f: với pixel 3,45 µm và ánh sáng xanh lá, đĩa Airy vượt 2 pixel từ khoảng f/5.6 (m nhỏ) hoặc f/4 (m = 0,5). Đóng khẩu quá mức này thì DOF "trên giấy" tăng nhưng ảnh mềm đi ở mọi nơi, kể cả ở mặt phẳng nét. Ngưỡng 2 pixel là quy tắc tham khảo: bài toán đo cạnh subpixel chịu được ít hơn, bài toán có/không chịu được nhiều hơn. Ánh sáng bước sóng ngắn (xanh dương) cho đĩa Airy nhỏ hơn ánh sáng đỏ hay hồng ngoại.
- Thực tế phổ biến: f/4 đến f/8 cho pixel cỡ 3–5 µm; pixel càng nhỏ, cửa sổ khẩu độ dùng được càng hẹp. Khi không có số f nào thỏa cả DOF lẫn nhiễu xạ, phải đổi cấu hình: giảm độ phóng đại (pixel trên vật to hơn), dùng pixel lớn hơn, hoặc dùng ống kính telecentric/ống kính lấy nét điện tử cho chiều cao thay đổi.
3. Kiểm chứng công thức gần đúng
Công thức DOF theo m là gần đúng. Công thức "chính xác" theo quang học hình học (thấu kính mỏng, dùng khoảng cách siêu tiêu cự H) cho giới hạn gần và xa của vùng nét. So sánh hai công thức để biết khi nào được dùng bản gần đúng:
def dof_chinh_xac(f, s, N, c):
"""Giới hạn gần/xa theo thấu kính mỏng. f, s (khoảng cách lấy nét), c tính bằng mm."""
H = f * f / (N * c) + f # khoảng cách siêu tiêu cự
gan = s * (H - f) / (H + s - 2 * f)
xa = s * (H - f) / (H - s) if s < H else float("inf")
return gan, xa
c = 2 * 0.00345 # 2 pixel 3,45 µm
for f, s in ((35, 300), (12, 1000)):
m = f / (s - f)
print(f"f = {f} mm, lấy nét ở {s} mm (m = {m:.4f}):")
for N in (4, 8):
gan, xa = dof_chinh_xac(f, s, N, c)
gan_dung = 2 * N * c * (1 + m) / m ** 2
print(f" f/{N}: vùng nét {gan:7.1f} – {xa:7.1f} mm → DOF {xa - gan:6.2f} mm;"
f" gần đúng 2N·c(1+m)/m² = {gan_dung:6.2f} mm")
f = 35 mm, lấy nét ở 300 mm (m = 0.1321):
f/4: vùng nét 298.2 – 301.8 mm → DOF 3.58 mm; gần đúng 2N·c(1+m)/m² = 3.58 mm
f/8: vùng nét 296.5 – 303.6 mm → DOF 7.17 mm; gần đúng 2N·c(1+m)/m² = 7.16 mm
f = 12 mm, lấy nét ở 1000 mm (m = 0.0121):
f/4: vùng nét 840.8 – 1233.6 mm → DOF 392.82 mm; gần đúng 2N·c(1+m)/m² = 378.73 mm
f/8: vùng nét 725.3 – 1609.6 mm → DOF 884.31 mm; gần đúng 2N·c(1+m)/m² = 757.47 mm
Ở khoảng cách làm việc gần (m = 0,13), hai công thức gần như trùng nhau và vùng nét gần đối xứng quanh mặt lấy nét. Với độ phóng đại rất nhỏ (camera nhìn xa, m ≈ 0,012), vùng nét lệch về phía sau và công thức gần đúng đánh giá thấp DOF, nhưng đây là trường hợp ít gặp trong kiểm tra chi tiết. Cả hai công thức chỉ là quang học hình học lý tưởng: khi chọn cấu hình thật, ưu tiên số liệu DOF và MTF trong datasheet hoặc công cụ tính của hãng ống kính, và luôn chụp thử ở các độ cao biên của vật.
4. Chỉ số độ nét: kiểm chứng trên ảnh mờ mô phỏng
Mắt người rất kém trong việc nhận ra độ nét cực đại, nhất là trên màn hình nhỏ hoặc ảnh thu nhỏ. Hãy dùng một chỉ số độ nét (focus measure) tính trên một vùng ROI có nhiều chi tiết (chữ, cạnh, vân bề mặt). Hai chỉ số phổ biến, đều dựa trên ý tưởng "ảnh nét có nhiều thành phần tần số cao":
- Phương sai Laplacian: lọc ảnh bằng toán tử Laplacian (đạo hàm bậc hai) rồi lấy phương sai. Rất nhanh, một dòng code.
- Tenengrad: trung bình bình phương độ lớn gradient Sobel (gx² + gy²). Thường ít nhạy với nhiễu hơn Laplacian một chút.
Để thấy chỉ số hoạt động thế nào, ta mô phỏng việc xoay vòng nét. Một vật lệch khỏi mặt nét một đoạn δ (mm) tạo đĩa mờ trên cảm biến có đường kính khoảng m² · |δ| / N_w, cùng lý luận như công thức DOF. Ta làm mờ một ảnh tổng hợp bằng đĩa mờ đúng kích thước đó, thêm nhiễu, rồi tính hai chỉ số:
import numpy as np
import cv2
rng = np.random.default_rng(0)
P_MM, M = 0.00345, 0.132 # pixel cảm biến (mm), độ phóng đại
S = 4 # mô phỏng ở độ phân giải gấp 4 để đĩa mờ nhỏ hơn 1 px vẫn có tác dụng
goc = np.full((240, 320), 120, np.uint8)
goc[::40, :] = 60
goc[:, ::40] = 60
cv2.putText(goc, "LOT 2417-B", (20, 130), cv2.FONT_HERSHEY_SIMPLEX, 1.4, 230, 3)
goc = cv2.add(goc, rng.integers(0, 25, goc.shape, dtype=np.uint8))
goc_hi = cv2.resize(goc, None, fx=S, fy=S, interpolation=cv2.INTER_NEAREST).astype(np.float32)
def dia_mo(d):
"""Nhân đĩa mờ (defocus) đường kính d (pixel của ảnh độ phân giải cao)."""
if d < 1:
return np.ones((1, 1), np.float32)
n = int(np.ceil(d / 2)) * 2 + 1
y, x = np.mgrid[0:n, 0:n] - n // 2
k = (np.hypot(x, y) <= d / 2).astype(np.float32)
return k / k.sum()
def chup(delta_mm, N, nhieu=2.0):
d_px = M * M * abs(delta_mm) / (N * (1 + M)) / P_MM # đĩa mờ trên cảm biến (pixel)
hi = cv2.filter2D(goc_hi, -1, dia_mo(d_px * S))
img = cv2.resize(hi, (320, 240), interpolation=cv2.INTER_AREA) # gộp về pixel cảm biến
img = cv2.GaussianBlur(img, (0, 0), 0.6) # ống kính + cảm biến vốn hơi mềm
img += np.random.default_rng(1).normal(0, nhieu, img.shape) # nhiễu cảm biến
return np.clip(img, 0, 255).astype(np.uint8), d_px
def var_laplacian(g):
return cv2.Laplacian(g, cv2.CV_64F).var()
def tenengrad(g):
gx = cv2.Sobel(g, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(g, cv2.CV_64F, 0, 1, ksize=3)
return np.mean(gx * gx + gy * gy)
for N in (2.8, 8):
print(f"f/{N}: δ (mm) | đĩa mờ (px) | var Laplacian | Tenengrad | (chuẩn hóa theo δ = 0)")
ref = None
for delta in (0, 0.5, 1, 2, 3, 5):
img, d = chup(delta, N)
vl, tg = var_laplacian(img), tenengrad(img)
ref = ref or (vl, tg)
print(f" {delta:6.1f} | {d:11.2f} | {vl:13.1f} | {tg:9.0f} | {vl / ref[0]:5.2f} {tg / ref[1]:5.2f}")
f/2.8: δ (mm) | đĩa mờ (px) | var Laplacian | Tenengrad | (chuẩn hóa theo δ = 0)
0.0 | 0.00 | 449.6 | 6005 | 1.00 1.00
0.5 | 0.80 | 296.2 | 5080 | 0.66 0.85
1.0 | 1.59 | 189.2 | 4247 | 0.42 0.71
2.0 | 3.19 | 117.4 | 3102 | 0.26 0.52
3.0 | 4.78 | 94.8 | 2148 | 0.21 0.36
5.0 | 7.97 | 86.1 | 1112 | 0.19 0.19
f/8: δ (mm) | đĩa mờ (px) | var Laplacian | Tenengrad | (chuẩn hóa theo δ = 0)
0.0 | 0.00 | 449.6 | 6005 | 1.00 1.00
0.5 | 0.28 | 449.6 | 6005 | 1.00 1.00
1.0 | 0.56 | 348.5 | 5425 | 0.78 0.90
2.0 | 1.12 | 275.5 | 4949 | 0.61 0.82
3.0 | 1.67 | 189.2 | 4247 | 0.42 0.71
5.0 | 2.79 | 128.8 | 3413 | 0.29 0.57
Hai điều rút ra:
- Chỉ số giảm đơn điệu khi ảnh mờ dần và phản ứng ngay cả với đĩa mờ dưới 1 pixel (phương sai Laplacian còn 66% ở đĩa mờ 0,8 pixel), tức là nhạy hơn mắt người rất nhiều. Phương sai Laplacian giảm nhanh hơn Tenengrad gần điểm nét vì nó nhấn mạnh tần số cao hơn, nhưng xa điểm nét nó chạm "sàn" khoảng 0,19 do nhiễu cảm biến (nhiễu cũng là tần số cao), trong khi Tenengrad vẫn tiếp tục giảm.
- Đường cong ở f/8 phẳng hơn nhiều so với f/2.8: cùng độ lệch 2 mm, ở f/2.8 phương sai Laplacian còn 26%, ở f/8 còn 61%. Đó chính là DOF lớn hơn, và cũng là lý do lấy nét ở khẩu mở lớn (mục 5): đường cong nhọn thì đỉnh dễ tìm chính xác.
Trên dây chuyền, vòng nét được xoay từng nấc và bạn chỉ có vài điểm đo. Nội suy parabol qua ba điểm quanh cực đại cho vị trí nét tốt nhất chính xác hơn bước xoay. Mô phỏng: vị trí nét thật nằm ở +0,37 mm, ta chỉ đo ở các nấc cách nhau 0,5 mm:
import numpy as np
import cv2
rng = np.random.default_rng(0)
goc = np.full((240, 320), 120, np.uint8)
cv2.putText(goc, "LOT 2417-B", (20, 130), cv2.FONT_HERSHEY_SIMPLEX, 1.4, 230, 3)
goc = cv2.add(goc, rng.integers(0, 25, goc.shape, dtype=np.uint8))
NET_THAT = 0.37 # mm: vị trí vòng nét cho ảnh nét nhất (chưa biết)
def anh_tai(x_mm):
"""Ảnh khi vòng nét ở vị trí x_mm: mờ Gauss tăng theo |x − NET_THAT| (f/2.8, m = 0,132)."""
d_px = 0.132 ** 2 * abs(x_mm - NET_THAT) / (2.8 * 1.132) / 0.00345
img = cv2.GaussianBlur(goc.astype(np.float32), (0, 0), 0.6 + 0.35 * d_px)
return np.clip(img + np.random.default_rng(1).normal(0, 2, img.shape), 0, 255).astype(np.uint8)
def tenengrad(g):
gx = cv2.Sobel(g, cv2.CV_64F, 1, 0, ksize=3)
gy = cv2.Sobel(g, cv2.CV_64F, 0, 1, ksize=3)
return np.mean(gx * gx + gy * gy)
def dinh_parabol(x, y, buoc):
"""Vị trí đỉnh của parabol đi qua điểm lớn nhất và hai điểm hai bên."""
i = int(np.argmax(y))
a, b, c = y[i - 1], y[i], y[i + 1]
return x[i] + 0.5 * (a - c) / (a - 2 * b + c) * buoc
vi_tri = np.arange(-2.0, 2.01, 0.5) # các nấc vòng nét đã thử (mm)
anh = [anh_tai(x) for x in vi_tri]
lap = np.array([cv2.Laplacian(g, cv2.CV_64F).var() for g in anh])
ten = np.array([tenengrad(g) for g in anh])
for x, s in zip(vi_tri, ten):
print(f"vòng nét {x:+.1f} mm: Tenengrad {s:7.1f} {'█' * int(40 * s / ten.max())}")
print(f"Nấc tốt nhất: {vi_tri[int(np.argmax(ten))]:+.2f} mm (thật: {NET_THAT:+.2f} mm)")
print(f"Nội suy parabol: var Laplacian → {dinh_parabol(vi_tri, lap, 0.5):+.2f} mm,"
f" Tenengrad → {dinh_parabol(vi_tri, ten, 0.5):+.2f} mm")
vòng nét -2.0 mm: Tenengrad 1157.6 ██████████████
vòng nét -1.5 mm: Tenengrad 1428.7 █████████████████
vòng nét -1.0 mm: Tenengrad 1772.0 █████████████████████
vòng nét -0.5 mm: Tenengrad 2222.1 ███████████████████████████
vòng nét +0.0 mm: Tenengrad 2857.6 ██████████████████████████████████
vòng nét +0.5 mm: Tenengrad 3278.5 ████████████████████████████████████████
vòng nét +1.0 mm: Tenengrad 2497.4 ██████████████████████████████
vòng nét +1.5 mm: Tenengrad 1971.4 ████████████████████████
vòng nét +2.0 mm: Tenengrad 1583.2 ███████████████████
Nấc tốt nhất: +0.50 mm (thật: +0.37 mm)
Nội suy parabol: var Laplacian → +0.46 mm, Tenengrad → +0.43 mm
Nấc tốt nhất lệch 0,13 mm so với vị trí thật; nội suy parabol qua ba điểm quanh đỉnh kéo ước lượng về 0,43 mm với Tenengrad (đường cong trơn, gần dạng parabol hơn) và 0,46 mm với phương sai Laplacian (đỉnh nhọn, không đối xứng). Muốn chính xác hơn nữa, quét lại với bước nhỏ hơn quanh đỉnh. Cùng ý tưởng này được dùng trong lấy nét tự động bằng ống kính lỏng (liquid lens) hoặc bàn trượt Z: quét thô, rồi nội suy quanh đỉnh.
5. Quy trình lấy nét tại hiện trường
- Đặt vật mẫu (hoặc mẫu chuẩn có chữ, vạch) đúng vị trí và độ cao làm việc thật; bật đúng đèn của trạm.
- Mở khẩu độ lớn nhất (số f nhỏ nhất), giảm phơi sáng để ảnh không cháy. DOF nông nhất nên đỉnh của đường cong độ nét nhọn nhất, dễ tìm chính xác.
- Chạy script hiển thị chỉ số độ nét theo thời gian thực (dưới đây), xoay chậm vòng nét qua lại để tìm cực đại. Với vật có chiều cao thay đổi, cân nhắc lấy nét ở giữa dải chiều cao chứ không phải ở mặt trên.
- Đóng khẩu về giá trị làm việc đã tính ở mục 2. Một số ống kính bị dịch tiêu điểm khi đóng khẩu (focus shift): kiểm tra lại chỉ số độ nét và chỉnh nhẹ nếu cần.
- Chỉnh phơi sáng/đèn bù lượng sáng đã mất.
- Khóa vít vòng nét và vòng khẩu (mục 6), kiểm tra lại chỉ số sau khi khóa (siết vít có thể làm vòng xoay nhẹ), ghi chỉ số chuẩn vào hồ sơ trạm.
Script lấy nét theo thời gian thực (bài thực hành của trang). Nó dùng webcam hoặc camera có driver UVC qua cv2.VideoCapture; với camera công nghiệp GigE Vision/USB3 Vision, thay phần đọc khung bằng SDK của hãng, phần còn lại giữ nguyên:
import time
import cv2
cap = cv2.VideoCapture(0) # camera công nghiệp: thay bằng SDK của hãng
if not cap.isOpened():
raise SystemExit("Không mở được camera")
ROI_W, ROI_H = 200, 150 # ROI ở giữa ảnh, đặt lên vùng có chữ/cạnh
diem_max, lich_su = 0.0, []
t0 = time.time()
while True:
ok, frame = cap.read()
if not ok:
break
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if frame.ndim == 3 else frame
h, w = gray.shape
x0, y0 = (w - ROI_W) // 2, (h - ROI_H) // 2
roi = gray[y0:y0 + ROI_H, x0:x0 + ROI_W]
diem = cv2.Laplacian(roi, cv2.CV_64F).var() # phương sai Laplacian
diem_max = max(diem_max, diem)
lich_su = (lich_su + [diem])[-5:] # trung bình 5 khung để bớt dao động
tb = sum(lich_su) / len(lich_su)
hien = frame.copy() if frame.ndim == 3 else cv2.cvtColor(frame, cv2.COLOR_GRAY2BGR)
mau = (0, 200, 0) if tb >= 0.95 * diem_max else (0, 165, 255) # xanh: gần đỉnh
cv2.rectangle(hien, (x0, y0), (x0 + ROI_W, y0 + ROI_H), mau, 2)
cv2.rectangle(hien, (10, h - 30), (10 + int((w - 20) * tb / max(diem_max, 1e-9)), h - 10), mau, -1)
cv2.putText(hien, f"Do net {tb:8.1f} max {diem_max:8.1f} [r] reset [q] thoat",
(10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.6, mau, 2)
cv2.imshow("Lay net", hien)
phim = cv2.waitKey(1) & 0xFF
if phim == ord("q"):
break
if phim == ord("r"): # đổi vật mẫu/ROI → đặt lại cực đại
diem_max = 0.0
cap.release()
cv2.destroyAllWindows()
print(f"Chỉ số độ nét cao nhất trong phiên: {diem_max:.1f} (thời gian chạy {time.time() - t0:.1f} s)")
Cách dùng: xoay vòng nét chậm theo một chiều cho tới khi thanh độ nét đạt cực đại rồi bắt đầu giảm, xoay ngược lại nhẹ cho tới khi khung chuyển màu xanh (trong khoảng 5% của cực đại). Giá trị tuyệt đối của chỉ số không có ý nghĩa giữa các cảnh khác nhau; chỉ có ý nghĩa khi so sánh trên cùng ROI, cùng ánh sáng. Bạn có thể thêm Tenengrad (hàm ở mục 4) để hiển thị song song hai chỉ số.
6. Khóa ống kính ổn định trong sản xuất
Rung động của máy dập, băng tải, quạt tủ điện làm vòng nét và vòng khẩu xoay dần trong vài tuần. Một trạm lấy nét hoàn hảo hôm nghiệm thu có thể mờ đi âm thầm mà không ai nhận ra cho tới khi tỉ lệ lọt lỗi tăng. Các biện pháp, từ bắt buộc tới nên có:
| Biện pháp | Cách làm |
|---|---|
| Ống kính có vít khóa | Chọn ống kính Machine Vision có vít khóa vòng nét và vòng khẩu (đa số ống kính công nghiệp có). Siết vừa tay bằng đúng dụng cụ, không siết quá làm hỏng ren |
| Đánh dấu vị trí | Vạch bút sơn (witness mark) ngang qua vòng nét, vòng khẩu và thân ống kính sau khi khóa: nhìn là biết vòng có bị xoay hay không |
| Ống kính tiêu cự cố định | Không dùng ống kính zoom trong sản xuất: nhiều bộ phận chuyển động, dễ lệch khi rung |
| Vỏ bảo vệ ống kính | Ống che (lens tube) hoặc hộp camera: chống va chạm, chống người tò mò xoay vòng, chống bụi dầu bám mặt trước |
| Đỡ ống kính nặng | Ống kính lớn (telecentric, ống kính cho cảm biến lớn) cần giá đỡ ôm thân, không để ngàm C gánh toàn bộ trọng lượng và rung |
| Giá camera cứng | Camera dịch chuyển vài phần mười mm cũng làm lệch nét ở độ phóng đại lớn và làm sai hiệu chuẩn |
| Ghi hồ sơ | Ghi số f, khoảng cách làm việc, chỉ số độ nét chuẩn trên mẫu chuẩn, ảnh chuẩn vào hồ sơ trạm; mọi thay đổi phải ghi lại |
| Giám sát tự động | Tính chỉ số độ nét trên một ROI cố định (vạch chuẩn trên đồ gá, hoặc mẫu chuẩn chạy đầu ca) và báo động khi giảm dưới ngưỡng |
Ví dụ giám sát: mỗi đầu ca chạy mẫu chuẩn, so chỉ số độ nét với giá trị lúc nghiệm thu. Dữ liệu dưới đây mô phỏng 30 ca: bụi bám dần làm chỉ số giảm chậm, rồi ca 24 vòng nét bị xoay:
import numpy as np
rng = np.random.default_rng(5)
CHUAN = 410.0 # chỉ số độ nét của mẫu chuẩn lúc nghiệm thu
diem = CHUAN * (1 - 0.009 * np.arange(30)) + rng.normal(0, 6, 30) # bụi bám dần
diem[23:] *= 0.55 # ca 24: vòng nét bị xoay
for ca, d in enumerate(diem, 1):
ti_le = d / CHUAN
if ti_le < 0.7:
muc = "DỪNG: kiểm tra nét/khóa ống kính ngay"
elif ti_le < 0.85:
muc = "CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu"
else:
continue
print(f"Ca {ca:2d}: độ nét {d:6.1f} = {ti_le:5.1%} chuẩn → {muc}")
if ti_le < 0.7:
break # trạm dừng chờ kỹ sư
Ca 16: độ nét 344.3 = 84.0% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 18: độ nét 340.3 = 83.0% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 19: độ nét 339.8 = 82.9% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 20: độ nét 337.0 = 82.2% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 21: độ nét 331.9 = 81.0% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 22: độ nét 335.8 = 81.9% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 23: độ nét 328.4 = 80.1% chuẩn → CẢNH BÁO: lau ống kính, kiểm tra vạch đánh dấu
Ca 24: độ nét 176.9 = 43.1% chuẩn → DỪNG: kiểm tra nét/khóa ống kính ngay
Cảnh báo bắt đầu từ ca 16 (bụi bám làm giảm độ tương phản, nên cũng làm giảm chỉ số: một cảnh báo sớm có ích), và trạm dừng ngay ca 24 khi vòng nét bị xoay. Ngưỡng 85% và 70% là ví dụ; chọn ngưỡng của bạn từ thử nghiệm: chỉ số độ nét giảm bao nhiêu thì phép đo hoặc tỉ lệ phát hiện lỗi bắt đầu bị ảnh hưởng. Dao động ngẫu nhiên làm ca 17 không bị cảnh báo dù ca 16 có: trong thực tế nên dùng trung bình vài lần đo hoặc yêu cầu vượt ngưỡng hai lần liên tiếp để tránh báo động chập chờn.
7. Sai lầm thường gặp
- Lấy nét ở khẩu đóng: DOF sâu nên "trông nét" ở cả một dải, vị trí nét thật nằm đâu đó không rõ; vật hơi cao/thấp là ra khỏi vùng nét.
- Đóng khẩu tối đa "cho chắc": nhiễu xạ làm mềm toàn bộ ảnh, mất ánh sáng phải bù bằng gain.
- Lấy nét bằng mắt trên ảnh thu nhỏ của màn hình.
- Lấy nét không có vật thật, không có đèn thật (tờ giấy đặt tạm, đèn trần): khác độ cao, khác bước sóng.
- Không khóa vít, không đánh dấu, không giám sát độ nét: trạm mờ dần mà không ai biết.
- So sánh chỉ số độ nét giữa hai cảnh khác nhau hoặc khác độ sáng.
8. Bài tập
Bài 1. Camera 12 MP pixel 3,45 µm, ống kính cho độ phóng đại m = 0,2, đèn xanh lá 550 nm. Chi tiết dao động chiều cao ±1 mm (cần DOF ≥ 2 mm, c = 2 pixel). Chọn số f nhỏ nhất đạt DOF và cho biết đĩa Airy bằng bao nhiêu pixel. Nếu cần DOF 4 mm thì sao?
Xem đáp án
p, lam, m = 3.45, 0.55, 0.2
c = 2 * p / 1000 # mm
for can in (2.0, 4.0):
N_can = can * m ** 2 / (2 * c * (1 + m))
N = next(n for n in (2.8, 4, 5.6, 8, 11, 16) if n >= N_can)
airy = 2.44 * lam * N * (1 + m)
print(f"DOF ≥ {can} mm: cần N ≥ {N_can:.2f} → f/{N}, DOF {2 * N * c * (1 + m) / m ** 2:.2f} mm,"
f" Airy {airy:.1f} µm = {airy / p:.1f} px")
DOF ≥ 2.0 mm: cần N ≥ 4.83 → f/5.6, DOF 2.32 mm, Airy 9.0 µm = 2.6 px
DOF ≥ 4.0 mm: cần N ≥ 9.66 → f/11, DOF 4.55 mm, Airy 17.7 µm = 5.1 px
DOF 2 mm: f/5.6, đĩa Airy khoảng 2,6 pixel, chấp nhận được cho phát hiện lỗi, hơi mềm cho đo cạnh. DOF 4 mm: cần f/11, đĩa Airy khoảng 5,1 pixel: ảnh mềm rõ. Nên đổi cấu hình: giảm độ phóng đại (ví dụ m = 0,14 thì DOF tăng khoảng 2 lần ở cùng số f, đổi lại pixel trên vật to hơn), hoặc giảm dao động chiều cao bằng đồ gá.
Bài 2 (thực hành của bài). Viết script hiển thị chỉ số độ nét (phương sai Laplacian) theo thời gian thực để lấy nét. Script cần có những thành phần gì để dùng được ở hiện trường?
Xem đáp án
Script đầy đủ ở mục 5. Những thành phần cần có: (1) tính chỉ số trên một ROI có chi tiết, không phải toàn ảnh (nền phẳng chỉ góp nhiễu); (2) làm mượt chỉ số qua vài khung để số không nhảy; (3) giữ cực đại trong phiên và hiển thị tỉ lệ hiện tại so với cực đại (thanh ngang, đổi màu khi trong khoảng 5% của đỉnh), có phím đặt lại khi đổi mẫu; (4) giữ nguyên đèn, phơi sáng, gain trong suốt lúc lấy nét; (5) lưu giá trị chuẩn sau khi khóa ống kính để giám sát về sau. Mở rộng: cho chọn ROI bằng chuột (cv2.selectROI), hiển thị thêm Tenengrad, vẽ đồ thị chỉ số theo thời gian.
Bài 3. Một trạm đã chạy 3 tháng, tỉ lệ lọt lỗi xước tăng dần, chỉ số độ nét của mẫu chuẩn còn 78% so với lúc nghiệm thu nhưng vạch đánh dấu trên vòng nét vẫn thẳng hàng. Nguyên nhân có thể là gì và bạn kiểm tra theo thứ tự nào?
Xem đáp án
Vạch đánh dấu còn thẳng nên vòng nét gần như không xoay. Các nguyên nhân còn lại, kiểm tra từ dễ tới khó: (1) mặt trước ống kính hoặc kính bảo vệ bám bụi, dầu: lau và đo lại chỉ số; (2) đèn yếu đi làm giảm độ tương phản (chỉ số tỉ lệ với bình phương tương phản): đo độ sáng trung bình ROI, so với hồ sơ; (3) camera hoặc giá đỡ bị xê dịch, thay đổi khoảng cách làm việc: đo lại khoảng cách và thang mm/pixel; (4) đồ gá hoặc băng tải mòn làm chi tiết nằm cao/thấp hơn: đo chiều cao mẫu chuẩn tại trạm; (5) vòng khẩu bị xoay (kiểm tra vạch của vòng khẩu). Sau khi sửa, chạy lại bộ mẫu kiểm tra phát hiện lỗi chứ không chỉ chỉ số độ nét.
Tóm tắt
- N = f/D; ánh sáng ∝ 1/N², mỗi nấc f giảm một nửa ánh sáng; vật chuyển động thì phải bù bằng đèn chứ không bằng phơi sáng.
- DOF ≈ 2·N·c·(1 + m)/m² với c = 1–2 pixel; DOF tăng mạnh khi giảm độ phóng đại (1/m²), tăng tuyến tính theo N.
- Đĩa Airy = 2,44·λ·N·(1 + m); giữ khoảng ≤ 2 pixel. Pixel nhỏ thu hẹp cửa sổ khẩu độ dùng được (thường f/4–f/8 với pixel 3–5 µm).
- Lấy nét ở khẩu mở lớn bằng chỉ số độ nét (phương sai Laplacian, Tenengrad) trên ROI có chi tiết, cùng điều kiện sáng; nội suy parabol quanh đỉnh; rồi đóng khẩu, kiểm tra lại.
- Khóa vít, đánh dấu, dùng ống kính cố định, đỡ ống kính nặng, ghi hồ sơ và giám sát chỉ số độ nét của mẫu chuẩn mỗi ca.
Bài tiếp theo: Ống kính telecentric: khi nào cần?, tính sai số phối cảnh của ống kính thường và quyết định bằng số có cần ống kính telecentric cho bài đo hay không.
Bài tập thực hành
- Viết script hiển thị chỉ số độ nét (phương sai Laplacian) theo thời gian thực để lấy nét.
Làm xong bài tập mới chuyển sang bài tiếp theo: tự viết code là cách duy nhất để nhớ lâu.


