⭐ Vision Engineer · Chặng 14Bài 5/5Bài học~45 phút

Xây portfolio Vision Engineer: 3 dự án, README thuyết phục, phỏng vấn

Chọn 3 capstone, README có escape/overkill/p99/%GRR, GIF demo, repo sạch, dòng CV có số liệu và câu hỏi phỏng vấn Vision Engineer kèm đáp án mẫu.

Xây portfolio Vision Engineer: 3 dự án, README thuyết phục, phỏng vấn

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

  • Chọn và trình bày 3 dự án
  • Viết README thuyết phục
  • Chuẩn bị phỏng vấn

Nên học trước: Viết tài liệu dự án Vision

Nội dung bài viết
  1. 1. Nhà tuyển dụng nhìn gì trong 2 phút đầu
  2. 2. Chọn 3 dự án: phủ đủ ba mảng kỹ năng
  3. 3. README thuyết phục: bài toán, con số, quyết định, giới hạn
  4. 4. Video và GIF demo
  5. 5. Repo GitHub sạch
  6. 6. Dòng CV: động từ + việc + công nghệ + kết quả đo được
  7. 7. Chuẩn bị phỏng vấn: câu hỏi thường gặp và câu trả lời mẫu
  8. 8. Script kiểm tra README và repo trước khi gửi link
  9. 9. Bài tập
  10. Tóm tắt

Bạn đã đi hết lộ trình: từ Python, OpenCV, quang học, chiếu sáng, hiệu chuẩn, đo lường, PLC, Deep Learning tới triển khai, và vừa học cách viết tài liệu trong bài "Viết tài liệu dự án Vision". Bước cuối là biến tất cả thành thứ nhà tuyển dụng nhìn thấy được trong vài phút. Với vị trí Vision Engineer ở nhà máy, người phỏng vấn ít quan tâm bạn học khóa nào; họ muốn biết bạn có chọn được đèn, tính được độ phân giải, chứng minh được trạm đạt yêu cầu bằng số liệu, và nói chuyện được với kỹ sư PLC hay không. Ba capstone của chặng này được thiết kế đúng cho mục đích đó. Bài này hướng dẫn chọn và trình bày 3 dự án, viết README thuyết phục bằng con số (escape, overkill, cycle time, %GRR), làm video/GIF demo, giữ repo GitHub sạch, viết dòng CV, và chuẩn bị các câu hỏi phỏng vấn kỹ thuật thường gặp kèm câu trả lời mẫu. Cuối bài có script kiểm tra README và repo trước khi bạn gửi link cho ai.

Mục tiêu cuối bàiChọn và trình bày 3 dự án phủ đủ kỹ năng của Vision Engineer; viết README mở đầu bằng bài toán và con số, có demo, bảng kết quả, quyết định kỹ thuật và giới hạn; giữ repo sạch (không dữ liệu mật, không file nặng, chạy được bằng một lệnh); viết dòng CV có số liệu; trả lời được các câu hỏi phỏng vấn về quang học, chiếu sáng, hiệu chuẩn, chỉ số OK/NG và triển khai.

1. Nhà tuyển dụng nhìn gì trong 2 phút đầu

Người sàng lọc hồ sơ (thường là trưởng nhóm kỹ thuật) mở link GitHub của bạn giữa hai cuộc họp. Trong 2 phút, họ đọc dòng đầu README, nhìn ảnh/GIF, liếc bảng kết quả, có thể mở một file code. Mỗi thứ họ thấy phải trả lời một câu hỏi:

Họ muốn biếtBằng chứng trong portfolio
Bạn hiểu bài toán công nghiệp, không chỉ chạy được thư viện?Dòng đầu README có sản phẩm, loại lỗi, tốc độ dây chuyền, dung sai
Bạn đo được và trung thực về kết quả?Bảng escape/overkill trên tập test tách biệt, cỡ mẫu, %GRR, p99
Bạn nắm phần cứng?Lý do chọn camera, ống kính, đèn; tính toán FOV và mm/pixel
Bạn tích hợp được vào dây chuyền?Bắt tay PLC, xử lý lỗi camera, lưu truy vết, chạy 24/7
Code của bạn người khác đọc và chạy được?Cấu trúc thư mục rõ, một lệnh chạy demo, có test
Bạn giao tiếp được?README mạch lạc, video giải thích ngắn, tài liệu bàn giao

2. Chọn 3 dự án: phủ đủ ba mảng kỹ năng

Ba dự án chất lượng, đầy đủ, có số liệu thuyết phục hơn mười notebook dang dở. Ba capstone của lộ trình phủ ba mảng mà một trạm vision thật cần:

Dự ánChứng minh kỹ năngCon số "đinh" trong README
Capstone 1: trạm đo kích thước hoàn chỉnhHiệu chuẩn, đo subpixel, phân tích hệ thống đo (MSA), Modbus TCP, biên bản nghiệm thu tự sinh%GRR, bias so với CMM, escape/overkill trên mẫu vàng, p99
Capstone 2: trạm đọc DataMatrix và số seriĐọc mã 2D, OCR/OCV, đối chiếu lệnh sản xuất, chống trùng bằng CSDL, đạt tốc độ dây chuyềnTỉ lệ đọc đúng, escape trên các loại nhãn lỗi, p99 so với nhịp 150 hộp/phút
Capstone 3: kiểm tra lỗi bề mặt bằng AI, trả kết quả PLCDữ liệu chia theo lô, huấn luyện, chọn ngưỡng theo escape, ONNX, giám sát driftEscape trên lô chưa thấy, overkill, sai lệch ONNX/PyTorch, thời điểm phát hiện mất nét

Nếu ứng tuyển vị trí thiên về AI, đặt Capstone 3 lên đầu; vị trí đo lường, kiểm tra chất lượng thì Capstone 1. Có thể thêm một dự án thứ tư thể hiện điều riêng của bạn (ví dụ dự án dùng camera và đèn thật, dù đơn giản: ảnh thật luôn gây ấn tượng hơn ảnh tổng hợp).

Dự án ở công ty: không đưa lên mạngMã nguồn, ảnh sản phẩm, dữ liệu khách hàng của dự án làm cho công ty thuộc về công ty và thường có cam kết bảo mật. Không đưa lên GitHub, kể cả "chỉ vài ảnh". Trong CV và phỏng vấn, mô tả ở mức tổng quát (loại sản phẩm, loại lỗi, kết quả, vai trò của bạn) và tái hiện kỹ thuật bằng dữ liệu tự tạo, như cách các capstone dùng ảnh tổng hợp.

3. README thuyết phục: bài toán, con số, quyết định, giới hạn

README của portfolio khác README bàn giao ở bài trước: người đọc là nhà tuyển dụng, nên kết quả và cách bạn suy nghĩ lên đầu, hướng dẫn cài đặt xuống dưới. Cấu trúc gợi ý:

  1. Một câu bài toán có số: sản phẩm, loại lỗi, tốc độ, tiêu chí.
  2. GIF demo 10–15 giây ngay dưới tiêu đề.
  3. Bảng kết quả: yêu cầu và giá trị đạt, cỡ mẫu, điều kiện thử.
  4. Kiến trúc: sơ đồ camera → xử lý → PLC/CSDL.
  5. Quyết định kỹ thuật và lý do: vì sao đèn này, thuật toán này, ngưỡng này; điều đã thử mà không hiệu quả.
  6. Giới hạn và bước tiếp theo: trung thực về dữ liệu tổng hợp, phần chưa thử.
  7. Cách chạy: một lệnh tái tạo toàn bộ kết quả.

Hiểu đúng các con số trước khi đưa vào README (người phỏng vấn sẽ hỏi):

Chỉ sốĐịnh nghĩaCách trình bày đúng
Escape (lọt lỗi)Số/tỉ lệ sản phẩm NG bị trạm cho là OK"0 / 412 ô NG trên 2 lô chưa dùng khi huấn luyện" (luôn kèm cỡ mẫu)
Overkill (loại nhầm)Tỉ lệ sản phẩm OK bị trạm loại"1,9% trên 1000 nắp OK"; nói rõ đánh đổi với escape khi chọn ngưỡng
Cycle time / p99Thời gian xử lý; p99 là mức mà 99% chu kỳ nhanh hơn"p99 31 ms trên PC 8 lõi, ngân sách 50 ms" (kèm phần cứng)
%GRRBiến thiên của hệ thống đo (lặp lại và tái lập) so với dung sai"%GRR 4,2% (≤ 10%: chấp nhận được), 3 chi tiết × 30 lần"

Ví dụ phần đầu README cho Capstone 3 (số liệu minh họa: dùng số trong biên bản của bạn):

# Kiểm tra lỗi bề mặt nắp nhôm bằng AI, trả kết quả cho PLC

Phát hiện xước/đốm trên nắp nhôm xước sợi ở 30 nắp/phút; trả OK/NG qua Modbus TCP;
mục tiêu escape = 0 trên lô vật liệu chưa từng thấy.

![Demo](docs/demo.gif)

## Kết quả (tập test: 2 lô chưa dùng khi huấn luyện)
| Chỉ số | Yêu cầu | Đạt |
|---|---|---|
| Escape | 0 | 0 / 412 ô NG |
| Overkill | ≤ 2 % | 1,9 % (1000 nắp OK) |
| Sai lệch ONNX so với PyTorch | ≤ 1e-4 | 4e-7, không quyết định nào bị lật |
| Suy luận 1 nắp (64 ô), p99 | ≤ 50 ms | 31 ms (CPU 8 lõi) |
| Cảnh báo mất nét | trước escape đầu tiên | sớm hơn 140 sản phẩm |

## Quyết định kỹ thuật
- Chia dữ liệu theo lô vật liệu, không chia ngẫu nhiên: chia ngẫu nhiên cho escape 0 "ảo",
  chia theo lô lộ ra 2,2 % escape với ngưỡng mặc định 0,5.
- Chọn ngưỡng theo escape trên tập val (0,31), chấp nhận overkill 1,9 %.
- Giám sát độ nét bằng EWMA: phát hiện camera mất nét trước khi tỉ lệ NG thay đổi.

## Giới hạn
Dữ liệu tổng hợp; chưa thử nắp màu đen; vết xước song song vân dưới 0,5 mm khó thấy.

## Chạy lại toàn bộ
`python run_all.py` – tạo dữ liệu, huấn luyện, xuất ONNX, chạy trạm với PLC giả, sinh biên bản.

4. Video và GIF demo

  • GIF 10–15 giây đầu README: ảnh đầu vào → khung/đường đo vẽ lên ảnh → kết quả OK/NG; nên dưới khoảng 10 MB để trang tải nhanh, rộng khoảng 800 pixel, chữ đọc được.
  • Video 2–3 phút (YouTube không công khai, hoặc file trong mục Releases): 15 giây bối cảnh, chạy demo thật, chỉ vào con số nghiệm thu, một sự cố được xử lý (camera mất kết nối, PLC timeout). Quay màn hình độ phân giải đủ lớn, giọng nói rõ, không nhạc nền ồn.
  • Ảnh thật (dù chụp bằng webcam với đèn bàn) tạo khác biệt lớn. Ghi chú rõ ảnh nào là tổng hợp.

Chuyển video quay màn hình thành GIF gọn bằng ffmpeg (tạo bảng màu riêng cho GIF nên ảnh nét hơn):

ffmpeg -ss 5 -t 12 -i demo.mp4 \
  -vf "fps=10,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
  docs/demo.gif

Hoặc tạo GIF trực tiếp từ các ảnh kết quả mà chương trình của bạn đã vẽ, bằng Pillow. Ví dụ dưới đây tự tạo 8 khung hình băng tải có khung OK/NG và thanh trạng thái:

from pathlib import Path

import cv2
import numpy as np
from PIL import Image

rng = np.random.default_rng(0)
khung_hinh = []
for i in range(8):
    anh = np.full((240, 400, 3), 60, np.uint8)
    cv2.rectangle(anh, (0, 90), (400, 190), (90, 90, 90), -1)                 # băng tải
    x = 40 + i * 40
    cv2.circle(anh, (x + 30, 140), 32, (190, 190, 190), -1)                   # nắp
    ng = i in (3, 6)
    if ng:
        cv2.line(anh, (x + 18, 128), (x + 40, 150), (40, 40, 40), 2)          # vết xước
    mau = (0, 0, 255) if ng else (0, 200, 0)
    cv2.rectangle(anh, (x - 6, 104), (x + 66, 176), mau, 2)
    cv2.rectangle(anh, (0, 0), (400, 32), (30, 30, 30), -1)
    cv2.putText(anh, f"#{1000 + i}  {'NG' if ng else 'OK'}  {int(rng.integers(24, 33))} ms",
                (10, 22), cv2.FONT_HERSHEY_SIMPLEX, 0.6, mau, 2)
    khung_hinh.append(Image.fromarray(cv2.cvtColor(anh, cv2.COLOR_BGR2RGB)))

Path("docs").mkdir(exist_ok=True)
khung_hinh[0].save("docs/demo.gif", save_all=True, append_images=khung_hinh[1:],
                   duration=400, loop=0, optimize=True)                     # 400 ms/khung, lặp vô hạn
print(len(khung_hinh), "khung,", Path("docs/demo.gif").stat().st_size // 1024, "KB")
8 khung, 22 KB

5. Repo GitHub sạch

NênKhông nên
Cấu trúc src/, tests/, docs/, config/; một lệnh chạy demoMột thư mục 40 notebook tên test2_final_v3.ipynb
requirements.txt ghim phiên bản, ghi phiên bản Python"Cài các thư viện cần thiết"
.gitignore loại dataset, trọng số huấn luyện, .venv, __pycache__, .envĐẩy dataset 2 GB, file .pt 200 MB (GitHub chặn file trên 100 MB)
Mô hình nhỏ để demo: Git LFS hoặc đính kèm trong Releases, ghi mã bămMô hình chỉ có trên máy bạn, README nói "liên hệ để lấy"
LICENSE (ví dụ MIT) để người khác biết được phép dùng thế nàoKhông có license
Commit có ý nghĩa ("Thêm bù biên theo loại kích thước, bias 12 µm → 1,5 µm")Một commit "upload" duy nhất chứa mọi thứ
Ghim 3 repo tốt nhất lên trang cá nhân GitHub, README hồ sơ ngắn gọnTrang cá nhân đầy repo fork không sửa gì
Đường dẫn tương đối, cấu hình qua fileC:\Users\Lan\Desktop\... trong code; mật khẩu, IP của công ty
git init
git lfs install && git lfs track "*.onnx"          # mô hình demo nhỏ qua Git LFS
git add .gitattributes .gitignore README.md LICENSE requirements.txt src tests docs config
git commit -m "Trạm đo kích thước: hiệu chuẩn, đo subpixel, Modbus TCP, biên bản tự sinh"
gh repo create tram-do-kich-thuoc --public --source=. --push   # hoặc tạo repo trên web rồi git push

6. Dòng CV: động từ + việc + công nghệ + kết quả đo được

YếuMạnh
Làm dự án phát hiện lỗi bằng AIXây trạm kiểm tra lỗi bề mặt bằng CNN + ONNX Runtime, chọn ngưỡng theo escape: 0 lọt lỗi trên 2 lô vật liệu mới, overkill 1,9%, p99 31 ms; tích hợp PLC qua Modbus TCP
Biết OpenCV, HalconĐo 4 kích thước chi tiết dập bằng caliper subpixel, %GRR 4,2% với dung sai ±0,05 mm; tìm và sửa sai số hệ thống 12 µm do bù biên
Đọc mã vạchTrạm xác minh nhãn 150 hộp/phút: DataMatrix + OCR số seri, đối chiếu lệnh sản xuất, chống trùng bằng SQLite; 0 lọt trên 44 nhãn lỗi thuộc 8 loại

Bản tiếng Anh cho hồ sơ công ty nước ngoài, cùng công thức: "Built an AI surface-inspection station (CNN, ONNX Runtime, Modbus TCP): 0 escapes on 2 unseen material lots, 1.9% overkill, p99 latency 31 ms." Mỗi dòng CV phải là thứ bạn giải thích được chi tiết trong 5 phút phỏng vấn.

7. Chuẩn bị phỏng vấn: câu hỏi thường gặp và câu trả lời mẫu

Vòng kỹ thuật cho Vision Engineer thường xoay quanh năm mảng. Câu trả lời tốt luôn có con số, đánh đổi, và một ví dụ từ dự án của bạn.

Quang học và camera

Hỏi: Cần phát hiện vết lỗi 0,2 mm trên chi tiết 100 × 75 mm. Chọn camera thế nào?
Trả lời mẫu: Lấy FOV có biên, ví dụ 110 × 82 mm. Muốn vết lỗi phủ ít nhất 3–4 pixel thì kích thước pixel trên vật ≤ 0,2/4 = 0,05 mm, nên cần ≥ 110/0,05 = 2200 pixel theo chiều ngang và ≥ 82/0,05 = 1640 pixel theo chiều dọc: camera 2 MP (1600 × 1200) không đủ, 5 MP (2448 × 2048) cho khoảng 0,045 mm/pixel, đạt. Sau đó chọn ống kính theo khoảng cách làm việc và kích thước cảm biến, kiểm tra độ sâu trường ảnh và độ phân giải quang học của ống kính có tương xứng pixel không.

Hỏi: Khi nào dùng ống kính telecentric?
Trả lời mẫu: Khi đo kích thước mà chi tiết có thể lệch độ cao hoặc có chiều sâu: ống kính thường làm vật gần trông to hơn (sai số phối cảnh), telecentric giữ độ phóng đại gần như không đổi trong dải làm việc. Đánh đổi: đắt, to, FOV bị giới hạn bởi đường kính thấu kính trước.

Chiếu sáng

Hỏi: Vết xước trên bề mặt kim loại bóng khó thấy. Bạn thử đèn gì?
Trả lời mẫu: Đèn góc thấp (dark field): bề mặt phẳng phản xạ ánh sáng đi ra ngoài nên tối, mép xước tán xạ ánh sáng vào camera nên sáng lên, tương phản cao. Nếu bề mặt cong và bóng gây chói, dùng đèn vòm (dome) để chiếu khuếch tán đều. Để đo biên dạng thì dùng đèn nền (backlight). Luôn thêm strobe và kính lọc/bước sóng phù hợp để loại ánh sáng môi trường, và kiểm chứng bằng bộ mẫu có lỗi nhỏ nhất.

Hiệu chuẩn và đo lường

Hỏi: Làm sao biết phép đo của trạm đủ tin cậy?
Trả lời mẫu: Hiệu chuẩn mm/pixel bằng tấm chuẩn (lưới chấm hoặc bàn cờ) có chứng nhận, kiểm tra phần dư; rồi nghiên cứu hệ thống đo: đo lặp lại nhiều chi tiết nhiều lần để tính %GRR so với dung sai (≤ 10% tốt, 10–30% cân nhắc, > 30% không chấp nhận), so với CMM để biết bias, và thử tuyến tính trên dải kích thước. Ở Capstone 1, %GRR tốt nhưng thử tuyến tính lộ ra sai số hệ thống 12 µm, sửa bằng bù biên theo loại kích thước.

Chỉ số OK/NG

Hỏi: Mô hình của bạn accuracy 99%. Như vậy tốt chưa?
Trả lời mẫu: Chưa đủ thông tin. Nếu 1% sản phẩm là NG, mô hình luôn trả OK cũng đạt 99% accuracy mà lọt toàn bộ lỗi. Tôi báo escape (NG bị cho qua) và overkill (OK bị loại) riêng, kèm cỡ mẫu và tập test tách theo lô. Ngưỡng chọn theo chi phí: escape đến tay khách hàng thường đắt hơn overkill nhiều lần, nên chọn ngưỡng sao cho escape = 0 trên tập val, rồi báo overkill phải trả.

Hỏi: Test 300 mẫu NG, không lọt mẫu nào. Có nói tỉ lệ lọt bằng 0 được không?
Trả lời mẫu: Không. 0/300 chỉ cho biết tỉ lệ lọt nhỏ, với độ tin cậy 95% thì cận trên khoảng 3/300 = 1% (quy tắc "rule of three"). Muốn khẳng định tỉ lệ lọt dưới 0,1% cần khoảng 3000 mẫu NG không lọt. (Xem Bài 3.)

Triển khai

Hỏi: Mô hình đúng trong Python nhưng sai trên máy trạm C#. Bạn làm gì?
Trả lời mẫu: 9/10 trường hợp là tiền xử lý khác: thứ tự kênh BGR/RGB, chia 255, mean/std, kiểu nội suy khi resize, layout HWC/NCHW. Tôi xuất một bộ ảnh vàng kèm logit mong đợi và tổng từng kênh tensor; phía C# chạy parity test, so tổng kênh trước để khoanh vùng lỗi tiền xử lý hay lỗi gọi mô hình. Sau đó mới xét sai số số học (FP16/INT8).

Hỏi: Trạm chạy tốt 3 tuần rồi tỉ lệ NG tăng gấp đôi. Bạn điều tra thế nào?
Trả lời mẫu: Xem log có cấu trúc: tỉ lệ NG theo giờ, theo ca, theo lô; phân bố điểm của mô hình và các chỉ số ảnh (độ sáng, độ nét) so với đường cơ sở lúc nghiệm thu. Nếu độ sáng/độ nét trôi thì nghi đèn lão hóa, kính bẩn, camera lệch nét; chạy bộ mẫu chuẩn để xác nhận. Nếu ảnh ổn mà điểm trôi, có thể vật liệu đổi lô: lấy mẫu NG cho QA xác nhận bằng mắt. Không đổi ngưỡng cho "đẹp số" trước khi biết nguyên nhân.

Hỏi: Bắt tay với PLC thế nào để không mất kết quả?
Trả lời mẫu: PLC đặt TRIGGER, trạm chụp và xử lý, ghi kết quả và mã lỗi vào thanh ghi trước, rồi mới bật DONE; PLC đọc kết quả khi thấy DONE và hạ TRIGGER; trạm hạ DONE. Có timeout ở cả hai phía, có giá trị LỖI riêng (không bao giờ mặc định OK khi xử lý thất bại), và bộ đếm chu kỳ để phát hiện mất đồng bộ.

Câu hỏi hành vi

Chuẩn bị 3 câu chuyện theo khung STAR (Situation, Task, Action, Result: bối cảnh, nhiệm vụ, hành động, kết quả), mỗi câu chuyện 2 phút: một lỗi khó bạn tìm ra (ví dụ pixel-locking ở Capstone 1), một lần chọn đánh đổi (escape/overkill ở Capstone 3), một lần làm việc với người khác (kỹ sư PLC, QA, công nhân vận hành). Kết thúc mỗi câu chuyện bằng một con số.

Bài kiểm tra thực hànhNhiều công ty cho bài làm tại chỗ hoặc mang về: đếm vật trên ảnh, đo đường kính lỗ, phát hiện vật thiếu, đọc mã. Họ chấm cách bạn kiểm chứng (ảnh khó, trường hợp không có vật, báo lỗi thay vì báo OK) nhiều hơn chấm thuật toán. Luyện lại các bài của chặng OpenCV và Measurement với yêu cầu: có hàm kiểm thử, có xử lý ảnh xấu.

Script sau chấm một repo theo các tiêu chí của bài: dòng mở đầu có con số, demo ở gần đầu, bảng kết quả với chỉ số công nghiệp, mục giới hạn, lệnh chạy, không có đường dẫn cá nhân hay bí mật, không có file nặng hoặc file trọng số lẫn trong repo, có LICENSE và .gitignore:

import re
from pathlib import Path

CHI_SO = r"escape|overkill|p99|cycle time|%GRR|GRR|lọt lỗi|loại nhầm"


def cham_readme(van_ban):
    dong = van_ban.splitlines()
    dau = "\n".join(dong[:15])
    tieu_chi = {
        "Tiêu đề + câu bài toán có con số": bool(re.search(r"^# .+", van_ban, re.M))
                                           and bool(re.search(r"\d", "\n".join(dong[1:6]))),
        "Demo (gif/mp4/ảnh) trong 15 dòng đầu": bool(re.search(r"\.(gif|mp4|png|jpg)\b", dau, re.I)),
        "Bảng kết quả có chỉ số công nghiệp": bool(re.search(r"^\|.*(" + CHI_SO + r")", van_ban, re.M | re.I)),
        "Kết quả có cỡ mẫu (x / N)": bool(re.search(r"\d+\s*/\s*\d+", van_ban)),
        "Có mục quyết định kỹ thuật": bool(re.search(r"^##.*(quyết định|decision|thiết kế)", van_ban, re.M | re.I)),
        "Có mục giới hạn": bool(re.search(r"^##.*(giới hạn|limitation)", van_ban, re.M | re.I)),
        "Có lệnh chạy": bool(re.search(r"`(python|docker|bash|make)[^`]*`", van_ban)),
        "Không có đường dẫn cá nhân": not re.search(r"[A-Z]:\\Users\\|/home/\w+/", van_ban),
        "Không có mật khẩu/khóa": not re.search(r"(password|api[_-]?key|token)\s*[:=]\s*\S+", van_ban, re.I),
    }
    return tieu_chi


def cham_repo(goc, gioi_han_mb=50):
    goc = Path(goc)
    van_de = []
    for f in ("README.md", "LICENSE", ".gitignore", "requirements.txt"):
        if not (goc / f).exists():
            van_de.append(f"thiếu {f}")
    for p in sorted(goc.rglob("*")):
        if not p.is_file() or ".git" in p.parts:
            continue
        mb = p.stat().st_size / 1e6
        if mb > gioi_han_mb:
            van_de.append(f"{p.relative_to(goc)}: {mb:.0f} MB (GitHub chặn file > 100 MB)")
        if p.suffix in (".pt", ".pth", ".ckpt") or p.name == ".env":
            van_de.append(f"{p.relative_to(goc)}: không nên đưa lên repo")
    return van_de


def bao_cao(goc):
    tc = cham_readme((Path(goc) / "README.md").read_text(encoding="utf-8"))
    print(f"== {goc}: README {sum(tc.values())}/{len(tc)}")
    for ten, dat in tc.items():
        if not dat:
            print("   [ ]", ten)
    for v in cham_repo(goc):
        print("   [!]", v)


# Hai repo thử: một repo "người mới", một repo theo mẫu mục 3
yeu = Path("repo_yeu")
yeu.mkdir(exist_ok=True)
(yeu / "README.md").write_text("# Defect detection\n\nDự án phát hiện lỗi bằng YOLO.\n\n"
                               "Chạy file C:\\Users\\Lan\\Desktop\\main.py\n", encoding="utf-8")
(yeu / "best.pt").write_bytes(b"\0" * 1000)
(yeu / ".env").write_text("PLC_PASSWORD=123\n")

tot = Path("repo_tot")
(tot / "docs").mkdir(parents=True, exist_ok=True)
for f in ("LICENSE", ".gitignore", "requirements.txt"):
    (tot / f).write_text("...\n")
(tot / "README.md").write_text("""# Kiểm tra lỗi bề mặt nắp nhôm bằng AI, trả kết quả cho PLC

Phát hiện xước/đốm trên nắp nhôm ở 30 nắp/phút; trả OK/NG qua Modbus TCP.

![Demo](docs/demo.gif)

## Kết quả (2 lô chưa dùng khi huấn luyện)
| Chỉ số | Yêu cầu | Đạt |
|---|---|---|
| Escape | 0 | 0 / 412 ô NG |
| Overkill | ≤ 2 % | 1,9 % (1000 nắp OK) |
| p99 | ≤ 50 ms | 31 ms |

## Quyết định kỹ thuật
Chia dữ liệu theo lô; chọn ngưỡng theo escape trên val.

## Giới hạn
Dữ liệu tổng hợp; chưa thử nắp màu đen.

## Chạy lại toàn bộ
`python run_all.py`
""", encoding="utf-8")

bao_cao("repo_yeu")
bao_cao("repo_tot")
== repo_yeu: README 1/9
   [ ] Tiêu đề + câu bài toán có con số
   [ ] Demo (gif/mp4/ảnh) trong 15 dòng đầu
   [ ] Bảng kết quả có chỉ số công nghiệp
   [ ] Kết quả có cỡ mẫu (x / N)
   [ ] Có mục quyết định kỹ thuật
   [ ] Có mục giới hạn
   [ ] Có lệnh chạy
   [ ] Không có đường dẫn cá nhân
   [!] thiếu LICENSE
   [!] thiếu .gitignore
   [!] thiếu requirements.txt
   [!] .env: không nên đưa lên repo
   [!] best.pt: không nên đưa lên repo
== repo_tot: README 9/9

Repo "người mới" trượt gần hết: không có con số, không demo, không kết quả, lộ đường dẫn cá nhân, đẩy cả file trọng số và file .env chứa mật khẩu. Đặc biệt nguy hiểm là .env: một khi đã đẩy lên GitHub, xóa ở commit sau vẫn còn trong lịch sử; phải đổi mật khẩu ngay và làm sạch lịch sử. Script chỉ kiểm tra hình thức; nội dung có thuyết phục hay không thì nhờ một kỹ sư khác đọc và kể lại dự án của bạn trong một câu.

9. Bài tập

Bài 1 (thực hành chính). Đưa 3 capstone lên GitHub, mỗi repo có README theo mẫu mục 3, GIF hoặc video demo, và bảng kết quả đo được lấy từ biên bản tự sinh. Dùng script mục 8 chấm cả ba repo cho đến khi đạt mọi tiêu chí, và chuẩn bị một trang hồ sơ GitHub ghim ba repo.

Xem đáp án

Trình tự cho mỗi capstone:

  1. Chạy lại toàn bộ dự án từ đầu bằng một lệnh trên máy sạch (môi trường ảo mới); sửa đến khi chạy được. Lệnh đó ghi vào mục "Chạy lại toàn bộ".
  2. Lấy bảng kết quả từ biên bản tự sinh (không gõ tay số liệu), ghi rõ cỡ mẫu và điều kiện thử.
  3. Tạo GIF 10–15 giây (Pillow như mục 4 hoặc ffmpeg từ video quay màn hình), đặt ở docs/demo.gif, chèn ngay dưới câu bài toán.
  4. Viết mục quyết định kỹ thuật: mỗi capstone có một "câu chuyện" đáng kể (Capstone 1: sai số hệ thống 12 µm và bù biên; Capstone 2: các lỗi bộ đánh giá tìm ra; Capstone 3: chia theo lô và chọn ngưỡng theo escape).
  5. Thêm .gitignore (dữ liệu sinh ra, trọng số, .venv), LICENSE, requirements.txt ghim phiên bản; mô hình demo qua Git LFS hoặc Releases.
  6. Chạy script chấm, sửa, rồi git push; ghim ba repo trên trang cá nhân theo thứ tự phù hợp vị trí ứng tuyển.

Chấm cả ba repo một lượt (ở đây dùng lại repo mẫu của mục 8 làm ví dụ cho ba thư mục; trên máy bạn, thay bằng đường dẫn tới ba repo đã clone):

import shutil

for ten in ["capstone-1-tram-do", "capstone-2-doc-ma", "capstone-3-loi-be-mat"]:
    shutil.copytree("repo_tot", ten, dirs_exist_ok=True)       # thay bằng repo thật của bạn
    bao_cao(ten)
== capstone-1-tram-do: README 9/9
== capstone-2-doc-ma: README 9/9
== capstone-3-loi-be-mat: README 9/9

Cả ba đạt 9/9 tiêu chí hình thức. Kiểm tra cuối cùng không tự động được: clone repo về một máy khác, làm theo README; mở link GIF trên điện thoại; nhờ một người không biết dự án đọc README 2 phút rồi nói lại bài toán và kết quả.

Bài 2. Viết lại các dòng CV sau theo công thức "động từ + việc + công nghệ + kết quả đo được": (a) "Tham gia dự án vision cho dây chuyền đóng gói." (b) "Sử dụng PyTorch, ONNX." (c) "Có kinh nghiệm làm việc với PLC."

Xem đáp án
  • (a) "Triển khai trạm kiểm tra nhãn trên dây chuyền đóng gói 150 hộp/phút: đọc DataMatrix + OCR số seri, đối chiếu lệnh sản xuất; 0 nhãn lỗi lọt trong bộ thử 300 nhãn, p99 < 150 ms."
  • (b) "Xuất mô hình PyTorch sang ONNX có kiểm chứng (sai lệch logit < 1e-6 trên 200 ảnh, bộ ảnh vàng cho phía C#), chạy ONNX Runtime trong ứng dụng .NET." Kỹ năng công cụ để ở mục "Kỹ năng"; dòng kinh nghiệm phải nói làm gì với công cụ đó.
  • (c) "Thiết kế giao tiếp trạm vision với PLC qua Modbus TCP: bắt tay TRIGGER/DONE có timeout, mã lỗi riêng, kiểm thử 60/60 chu kỳ với PLC giả trước khi lắp đặt."

Nếu dự án làm cho công ty, giữ con số ở mức được phép công bố (tỉ lệ, tốc độ) và không nêu tên khách hàng, sản phẩm cụ thể khi chưa được đồng ý.

Bài 3. Câu hỏi phỏng vấn: "Trạm của bạn không lọt mẫu nào trong n mẫu NG. Với độ tin cậy 95%, tỉ lệ lọt lỗi thật có thể cao tới bao nhiêu?" Tính chính xác cận trên với n = 30, 300, 3000 và so với quy tắc 3/n. Cần bao nhiêu mẫu NG để khẳng định tỉ lệ lọt dưới 0,1%?

Xem đáp án

Nếu tỉ lệ lọt thật là p, xác suất không lọt mẫu nào trong n mẫu là (1 − p)n. Cận trên 95% là giá trị p mà xác suất đó bằng 5%: p = 1 − 0,051/n ≈ 3/n (vì ln 0,05 ≈ −3).

import math

for n in (30, 300, 3000):
    p = 1 - 0.05 ** (1 / n)
    print(f"0 lọt / {n:4d} mẫu NG: cận trên 95% = {p * 100:.3f} %  (quy tắc 3/n: {300 / n:.3f} %)")
n_can = math.ceil(math.log(0.05) / math.log(1 - 0.001))
print("Để khẳng định tỉ lệ lọt < 0,1 % cần", n_can, "mẫu NG không lọt mẫu nào")
0 lọt /   30 mẫu NG: cận trên 95% = 9.503 %  (quy tắc 3/n: 10.000 %)
0 lọt /  300 mẫu NG: cận trên 95% = 0.994 %  (quy tắc 3/n: 1.000 %)
0 lọt / 3000 mẫu NG: cận trên 95% = 0.100 %  (quy tắc 3/n: 0.100 %)
Để khẳng định tỉ lệ lọt < 0,1 % cần 2995 mẫu NG không lọt mẫu nào

Vì vậy trong README và phỏng vấn, viết "0 / 412 ô NG" thay vì "escape 0%", và hiểu rằng nghiệm thu với vài chục mẫu NG chỉ chứng minh tỉ lệ lọt dưới khoảng 10%. Đây cũng là lý do giám sát sau triển khai (kiểm tra mẫu chuẩn mỗi ca, theo dõi drift) quan trọng: tập thử không bao giờ đủ lớn để chứng minh mọi thứ.

Tóm tắt

  • Ba dự án đầy đủ, có số liệu, phủ ba mảng: đo lường (Capstone 1), đọc mã/OCR và truy vết (Capstone 2), AI và triển khai (Capstone 3). Dự án công ty: không đưa mã và dữ liệu lên mạng.
  • README portfolio: câu bài toán có số, GIF demo, bảng kết quả (escape, overkill kèm cỡ mẫu, p99, %GRR), quyết định kỹ thuật, giới hạn, một lệnh chạy lại.
  • Repo sạch: cấu trúc rõ, phiên bản ghim, .gitignore, LICENSE, không file nặng, không trọng số lẫn vào git, không bí mật, không đường dẫn cá nhân.
  • Dòng CV: động từ + việc + công nghệ + kết quả đo được; mỗi dòng giải thích được trong 5 phút.
  • Phỏng vấn: tính độ phân giải từ kích thước lỗi, chọn đèn theo bề mặt, %GRR và bias, escape/overkill thay vì accuracy, quy tắc 3/n, parity tiền xử lý, điều tra drift, bắt tay PLC; ba câu chuyện STAR kết thúc bằng con số.

Đây là bài cuối của lộ trình Vision Engineer. Hãy đưa ba capstone lên GitHub, gửi hồ sơ, và tiếp tục học từ những trạm thật mà bạn sắp xây.

Bài tập thực hành

  1. Đưa 3 capstone lên GitHub với README, ảnh/video demo và kết quả đo được.

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.

Xem đích đến 🎯
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í.