Zero-Shot Pattern Detection
for Engineering BOM Drawings
Đặc tả kỹ thuật chi tiết hệ thống phát hiện ký hiệu linh kiện lặp lại trong bản vẽ kỹ thuật BOM, không cần dữ liệu huấn luyện, hoạt động hoàn toàn zero-shot ở thời điểm suy luận.
1. Giới thiệu
Bài toán đặt ra: cho một ảnh template (ký hiệu linh kiện cần tìm) và một ảnh drawing (bản vẽ mạch điện kỹ thuật), hệ thống phải xác định tọa độ của tất cả các vị trí mà ký hiệu đó xuất hiện trong bản vẽ — bất kể tỉ lệ, góc xoay nhỏ, hay biến thiên phong cách vẽ.
Điểm đặc biệt: hệ thống phải hoạt động zero-shot — không có dữ liệu huấn luyện, không fine-tuning, bất kỳ template nào cũng có thể nhận diện ngay tại runtime.
2. Mục tiêu & Ràng buộc
2.1 Mục tiêu chức năng
| # | Yêu cầu | Mức ưu tiên |
|---|---|---|
| F1 | Nhận template và drawing dưới dạng ảnh, trả về danh sách bounding box + score | Bắt buộc |
| F2 | Hoạt động zero-shot — không cần dữ liệu huấn luyện cho class mới | Bắt buộc |
| F3 | Phát hiện ký hiệu ở nhiều tỉ lệ (0.7×–1.8×) và góc xoay nhỏ (±10°), 90° | Bắt buộc |
| F4 | Web UI để upload, chạy detection, download kết quả | Quan trọng |
| F5 | Chạy được trên CPU (không bắt buộc GPU) | Quan trọng |
| F6 | Stage 3 VLM filter (optional) để loại FP phức tạp | Tùy chọn |
| F7 | Triển khai được trên HuggingFace Spaces (Docker) | Tùy chọn |
2.2 Ràng buộc phi chức năng
- CPU: < 120s / drawing A3 (3508×2480px)
- GPU (CUDA): < 30s / drawing
- Memory: < 4GB VRAM (DINOv2 ViT-S)
- VLM: ~4.5GB VRAM bfloat16 (lazy-load)
- Recall > 90% trên bản vẽ chuẩn
- FP rate thấp (chỉ giữ detection NCC & DINOv2 đều cao)
- Bounding box IoU ≥ 0.5 với ground truth
- Chấp nhận miss các ký hiệu ở biên ảnh
3. Tổng quan kiến trúc Pipeline
Hệ thống theo kiến trúc cascade 3 giai đoạn (+ 1 optional), mỗi giai đoạn loại bỏ các ứng viên sai dần dần. Thiết kế này đạt recall cao từ Stage 1, sau đó tăng precision dần qua Stage 2 và 2b.
3.1 Luồng xử lý theo loại template
Pipeline phân nhánh tự động dựa trên đặc tính của template:
| Loại template | Tiêu chí phân loại | Pipeline được chọn | Ví dụ |
|---|---|---|---|
| Simple (outline) | edge_density < 0.05 AND interior_fill < 0.02 |
Pass A (NCC scales 0.30–1.0) + Pass B (rotate drawing 90°) + Pass C (DINODense nếu lớn hơn template) | Resistor IEC (hình chữ nhật) |
| Complex (filled) | Không thỏa mãn điều kiện Simple | Scale probe → Pass 3a (góc ≈0°) + Pass 3b (góc ≈90°) với adaptive scales | Resistor ANSI (zigzag), Bridge Rectifier, cổng logic |
3.1 Stage 0 — Tiền xử lý (Preprocessor)
Module: src/preprocessor.py
Chuẩn hóa cả template và drawing về cùng định dạng binary grayscale trước khi đưa vào pipeline.
Các bước xử lý
dilate_pattern > 0: làm dày nét template để khớp với drawing có stroke khác (Pass 2 — relaxed NCC pass).Output format
{
"processed": np.ndarray, # binary uint8, shape (H, W), values {0, 255}
"original": np.ndarray, # ảnh gốc RGB, để visualization
"scale_factor": 1.0 # nếu resize (hiện tại luôn = 1.0)
}
3.2 Stage 1 — NCC Multi-scale Matching
Module: src/ncc_matcher.py
Sử dụng Normalized Cross-Correlation (NCC) để nhanh chóng đề xuất các vùng ứng viên (candidate regions) trong drawing mà template có thể khớp. NCC là convolution tuyến tính, chạy rất nhanh trên CPU với cv2.matchTemplate(TM_CCOEFF_NORMED).
Thuật toán chi tiết
for scale in scales:
# Resize template về scale đang xét
scaled_tmpl = cv2.resize(template, (int(tw*scale), int(th*scale)))
for angle in angles:
# Rotate template quanh tâm
rotated = rotate_template(scaled_tmpl, angle)
# Chạy NCC sliding window trên toàn bộ drawing
ncc_map = cv2.matchTemplate(drawing, rotated, cv2.TM_CCOEFF_NORMED)
# Thu thập tất cả vị trí có NCC >= threshold
locs = np.where(ncc_map >= ncc_threshold)
for (y, x) in zip(*locs):
candidates.append({x, y, rw, rh, ncc_map[y,x], scale, angle})
# NMS (Non-Maximum Suppression) để loại bỏ duplicate
final_candidates = apply_nms(candidates, iou_threshold=0.30)
Xử lý góc xoay (Rotation Handling)
Hàm rotate_template(template, angle) dùng cv2.getRotationMatrix2D. Trường hợp đặc biệt cho góc ≈90°:
# Với 75° ≤ |angle| ≤ 105°: mở rộng canvas để template không bị cắt
if 75 <= abs(angle % 180) <= 105:
out_w, out_h = h, w # hoán đổi chiều rộng/cao
M[0, 2] += (out_w - w) / 2
M[1, 2] += (out_h - h) / 2
NMS với Cross-orientation Protection
NMS sử dụng containment-aware IoU: score = max(IoU, intersection / min_area). Quan trọng: các candidate theo hướng ngang (angle ≤ 70°) và dọc (70° ≤ |angle| ≤ 110°) KHÔNG triệt tiêu lẫn nhau, tránh mất ký hiệu ở hai hướng cùng vị trí.
Tham số NCC cho từng pass
| Pass | Template | NCC Threshold | Angles | Scales |
|---|---|---|---|---|
| Pass 1 (Strict) | Nguyên bản | 0.55 | [-10, -5, 0, 5, 10] | Mặc định NCCMatcher |
| Pass 2 (Relaxed) | Dilated (5×5) | 0.28 (complex) / 0.50 (simple) | [-10, -5, 0, 5, 10] | Mặc định NCCMatcher |
| Pass 3a (Micro 0°) | Nguyên bản | 0.28 | [-10, -5, 0, 5, 10] | Adaptive ± probe_scale |
| Pass 3b (Micro 90°) | Nguyên bản | 0.28 | [80, 85, 90, 95, 100] | Adaptive ± probe_scale |
| Pass A (Simple) | Nguyên bản | 0.42 | [-10,..,10, 80,..,100] | [0.30, 0.35, 0.40, 0.50, 0.60, 0.70, 1.0] |
Scale Probe (Complex Template)
Trước khi chạy Pass 3a/3b, pipeline thực hiện scale probe nhanh để xác định tỉ lệ tốt nhất:
probe_scales = [0.25, 0.30, 0.35, 0.40, 0.50, 0.65, 0.85, 1.0, 1.2, 1.5, 1.8, 2.0, 2.5]
for ps in probe_scales:
# Resize template và chạy NCC trên drawing, lấy max
best_probe_s = ps with highest max_NCC
# Nếu best_probe_s > 1.40: bỏ qua Pass 1+2, chuyển sang probe-focused mode
_use_probe_focused = (no_std_candidates) or (best_probe_s > 1.40)
if _use_probe_focused:
# Scales = best_probe_s × [0.80, 0.85, 0.90, 0.95, 1.0, 1.05, 1.10, 1.15, 1.20]
micro_scales = [round(best_probe_s * f, 2) for f in fracs]
3.3 Stage 2 — DINOv2 Verification
Module: src/dino_verifier.py · Model: dinov2_vits14 (ViT-S/14, 21M params)
Sử dụng embedding từ DINOv2 để kiểm tra xem vùng ảnh trong drawing có thực sự chứa ký hiệu giống template không, bằng cách so sánh cosine similarity giữa các vector feature.
Tại sao DINOv2?
Embedding pipeline
# 1. Encode template (cached sau lần đầu)
template_feat = model.forward_features(template_tensor)["x_norm_patchtokens"].mean(dim=1)
template_feat = F.normalize(template_feat, dim=1)
# 2. Với mỗi candidate:
crop = drawing[y:y+h, x:x+w] # cắt vùng ảnh
# Derotate nếu cần (Pass 3a/3b với derotate=True)
if derotate and abs(angle) > 1.0:
crop = rotate_crop(crop, -angle) # xoay ngược để align với template
# Encode full crop
crop_feat = model.forward_features(crop_tensor)["x_norm_patchtokens"].mean(dim=1)
crop_feat = F.normalize(crop_feat, dim=1)
# Encode center crop (70% bbox, loại bỏ wire leads)
center_crop = crop[h*15//100 : h*85//100, w*15//100 : w*85//100]
center_feat = ... (tương tự)
# 3. Cosine similarity — lấy max của full và center crop
sim = max(cosine(crop_feat, template_feat),
cosine(center_feat, template_feat))
# 4. Lọc theo threshold
if sim >= cosine_threshold:
candidate["dino_score"] = sim
candidate["confidence"] = (ncc_score + sim) / 2
verified.append(candidate)
DINOv2 Threshold theo pass
| Pass | Template loại | Threshold | Lý do |
|---|---|---|---|
| Standard (Pass 1+2) | All | 0.84 | Threshold mặc định, cân bằng precision/recall |
| Pass 3a (micro 0°) | Complex, probe-focused | 0.878 | Loại FP (diode/cap dino=0.843–0.877) trong khi giữ TP (dino≥0.884) |
| Pass 3b (micro 90°) | Complex, probe-focused | 0.78 (cố định) | Vertical crops mất ~0.04 score do warpAffine; TP dọc đo được 0.80–0.83 |
| Pass A (Simple, Chamfer) | Simple outline | N/A | Simple templates dùng Chamfer thay DINOv2 |
| Pass C (DINODense) | Simple, large instance | 0.78 | Dense sliding window, threshold thoáng hơn |
Pass 3b NCC Gate (bổ sung)
Sau DINOv2 verification cho Pass 3b, áp dụng thêm NCC gate:
# Genuine vertical resistors: NCC 0.73–0.78 với rotated template # Structural FP (capacitors nhìn giống vertical): NCC 0.35–0.45 # → Gap rõ ràng tại NCC = 0.55 verified_3b = [c for c in verified_3b if c["ncc_score"] >= 0.55]
3.4 Stage 2b — Structural Filters
Module: src/postprocessor.py
Bộ lọc hình học chạy sau DINOv2 để loại các FP mà DINOv2 không phân biệt được (vì có feature tương đồng mức cao). Các filter này không phụ thuộc model — chỉ dùng phân tích pixel.
Danh sách filter (Simple Template)
dino_score ≥ 0.88.Chamfer Distance Filter (Complex Template)
Đo sự khớp cạnh giữa vùng ảnh trong drawing và template ở cùng kích thước:
# 1. Resize (và rotate nếu vertical) template về kích thước bbox
tmpl_scaled = cv2.resize(rotate_if_vert(template), (bbox_w, bbox_h))
# 2. Canny edges cho cả hai
tmpl_edges = cv2.Canny(tmpl_scaled, 30, 100)
region_edges = cv2.Canny(drawing_region, 30, 100)
# 3. Distance transform từ region edges
dt = cv2.distanceTransform(255 - region_edges, cv2.DIST_L2, 5)
# 4. Chamfer = mean distance từ template edges đến region edges (bidirectional)
t2r = mean(dt[tmpl_edge_pixels]) # template → region
r2t = mean(dt_tmpl[region_edge_pixels]) # region → template (symmetric)
chamfer = (t2r + r2t) / 2
# Threshold: max_chamfer = 5.0 (probe-focused), 3.0 (simple template)
if chamfer > max_chamfer:
reject(candidate)
Confidence Gap Filter
# Tìm gap lớn nhất trong phân bố confidence (sorted descending)
confs = sorted([c["confidence"] for c in candidates], reverse=True)
best_gap = max(confs[i] - confs[i+1] for i in range(len(confs)-1))
# Nếu gap >= 0.075: loại cluster thấp hơn
if best_gap >= 0.075:
threshold = midpoint_of_gap
candidates = [c for c in candidates if c["confidence"] >= threshold]
Final NMS (Containment-aware)
def containment_iou(a, b):
inter = intersection_area(a, b)
min_area = min(a.area, b.area)
return max(standard_iou(a, b),
inter / min_area) # ← key: loại candidate bị "chứa" trong cái khác
# Sort by confidence desc, greedy NMS
final_nms(candidates, iou_threshold=0.40, use_union_bbox=False)
3.5 Stage 3 — VLM Semantic Filter (Optional)
Module: src/vlm_verifier.py · Model: Qwen/Qwen2-VL-2B-Instruct
Stage 3 chỉ kích hoạt khi use_vlm=True. Model Qwen2-VL-2B được lazy-load (~4.5GB bf16) và chỉ xử lý các candidate "borderline" (conf < vlm_keep_min_conf).
Open-classification approach
# Prompt template (open-classification)
prompt = """You are an electronics expert. Look at this crop from a schematic.
Choose exactly one label from:
resistor | inductor | capacitor | diode | crystal | transistor | op-amp | logic-gate | wire-junction | other
Reply with only the label, nothing else."""
# Classify template để biết target class
target_class = classify_template(template) # → "resistor"
# Với mỗi borderline candidate:
label = classify_crop(drawing_crop) # → "diode" | "resistor" | ...
# Reject-only mode (default):
reject_labels = ALL_CLASSES - {target_class} - UNTRUSTED_LABELS
# UNTRUSTED_LABELS = {"transistor", "wire-junction", "other"}
# → Model 2B hay gọi nhầm resistor là "transistor" → không dùng làm lý do reject
if label in reject_labels: # e.g., "diode", "capacitor"
reject(candidate)
else:
keep(candidate) # "resistor" hoặc nhãn không đáng tin
Recall-boost mode
Khi vlm_recall_boost=True (theo mặc định khi enable VLM), pipeline mở rộng scale sweep và nới lỏng Chamfer/DINOv2 threshold để tìm thêm candidate, rồi để VLM làm bộ lọc precision:
| Tham số | Normal | VLM Recall-boost |
|---|---|---|
| Scale fracs (probe-focused) | [0.80–1.20] | [0.55–1.55] |
| DINOv2 threshold (3a) | 0.878 | 0.80 |
| Chamfer max | 5.0 | 7.0 |
| Confidence gap filter | Bật | Tắt |
4. NCC Matcher — Chi tiết kỹ thuật
File: src/ncc_matcher.py · class NCCMatcher
API
class NCCMatcher:
def __init__(self,
scales: List[float] = None, # default: [0.85, 0.90, ..., 1.15]
angles: List[float] = None, # default: [-10, -5, 0, 5, 10]
ncc_threshold: float = 0.55,
nms_iou_threshold: float = 0.30):
def match(self, drawing: np.ndarray, template: np.ndarray) -> List[dict]:
"""Returns: [{x, y, w, h, ncc_score, scale, angle}, ...]"""
def rotate_template(self, template: np.ndarray, angle: float) -> np.ndarray:
Candidate format
{
"x": int, # tọa độ top-left trong drawing
"y": int,
"w": int, # kích thước bounding box (= rotated template size)
"h": int,
"ncc_score": float, # NCC score [0, 1]
"scale": float, # tỉ lệ scale so với template gốc
"angle": float # góc xoay tính bằng độ
}
NMS cross-orientation protection
def _apply_nms(candidates, iou_threshold):
for i, cand in enumerate(sorted_candidates):
for j, other in enumerate(remaining):
# KHÔNG suppress nếu 2 candidate khác orientation group
a_vert = 70 <= abs(cand["angle"]) <= 110
b_vert = 70 <= abs(other["angle"]) <= 110
if a_vert != b_vert:
continue # ← bỏ qua so sánh giữa H và V
if containment_iou(cand, other) > iou_threshold:
suppress(lower_ncc_candidate)
5. DINOv2 Verifier — Chi tiết kỹ thuật
File: src/dino_verifier.py · class DINOVerifier
Model architecture
| Thuộc tính | dinov2_vits14 |
|---|---|
| Architecture | Vision Transformer, patch size 14×14 |
| Parameters | 21M (Small variant) |
| Training | DINO self-supervised, 142M web images |
| Output dim | 384 (patch tokens pooled → mean) |
| Input size | Bất kỳ (resize về bội số 14 trước khi encode) |
| VRAM | ~0.5GB fp32 / ~0.25GB fp16 |
Feature extraction
features = model.forward_features(image_tensor) # x_norm_patchtokens: (B, N_patches, D) — patch-level features pooled = features["x_norm_patchtokens"].mean(dim=1) # (B, D) unit_norm = F.normalize(pooled, dim=1) # L2-normalized
Orientation normalization
def embed_crops_normalized(crops):
"""Normalize tất cả crops về landscape orientation trước khi encode."""
for crop in crops:
h, w = crop.shape[:2]
if h > w: # portrait → rotate CCW để thành landscape
crop = cv2.rotate(crop, cv2.ROTATE_90_COUNTERCLOCKWISE)
return embed_crops_batch(normalized_crops)
Confidence computation
confidence = (ncc_score + dino_score) / 2 # Cân bằng giữa NCC (recall signal) và DINOv2 (precision signal) # Candidates được sort by confidence giảm dần
6. Structural Filters — Chi tiết kỹ thuật
File: src/postprocessor.py · class Postprocessor
filter_wire_leads — thông số đo
def filter_wire_leads(candidates, drawing, pattern):
for cand in candidates:
if cand["dino_score"] >= 0.88:
keep(cand) # ← DINOv2 bypass: high-confidence không cần filter
continue
# Probe left và right (horizontal component)
probe = 24 # pixel
min_run = 2 # số pixel tối thiểu của dark run (mạnh)
min_run_weak = 0 # một phía OK nếu phía kia weak
has_left_lead = any_dark_run_left(bbox, probe, min_run)
has_right_lead = any_dark_run_right(bbox, probe, min_run)
if (has_left_lead + has_right_lead) == 0:
reject(cand) # cả hai phía đều không có lead
Title-block filter
# Phát hiện khung tên bản vẽ (title block) bằng vertical frame lines
# Chỉ chạy khi title_block_x < drawing_width (có vertical lines)
if title_block_x < W:
bom_start_x = title_block_x
candidates = [c for c in candidates if c["x"] + c["w"] <= bom_start_x]
# Không có frame lines → open circuit, bom_start_x = W (không filter)
7. VLM Verifier — Chi tiết kỹ thuật
File: src/vlm_verifier.py · class VLMVerifier
Model loading (lazy)
class VLMVerifier:
def __init__(self, model_name="Qwen/Qwen2-VL-2B-Instruct", ...):
self._model = None # lazy: chỉ load khi cần
def _ensure_loaded(self):
if self._model is None:
# Giải phóng VRAM cache trước khi load VLM
torch.cuda.empty_cache()
self._model = Qwen2VLForConditionalGeneration.from_pretrained(
model_name, torch_dtype=torch.bfloat16,
low_cpu_mem_usage=True
).to(device)
Classification vocabulary
CLASSES = [
"resistor", "inductor", "capacitor", "diode", "crystal",
"transistor", "op-amp", "logic-gate", "wire-junction", "other"
]
UNTRUSTED_LABELS = {"transistor", "wire-junction", "other"}
# Các nhãn 2B model hay gọi sai → không dùng làm lý do reject
# Reject labels khi target = "resistor":
reject_labels = CLASSES - {"resistor"} - UNTRUSTED_LABELS
# = {"inductor", "capacitor", "diode", "crystal", "op-amp", "logic-gate"}
OOM protection
# Trong server.py: VLM failure không crash request
try:
verified = vlm.filter_by_template_class(drawing, template, candidates, ...)
except Exception as vlm_err:
traceback.print_exc()
print(f"[VLM] Falling back to NCC+DINOv2 result: {vlm_err}")
# verified giữ nguyên từ Stage 2b
8. Web UI — FastAPI Backend
File: app/web/server.py
API Endpoints
| Method | Endpoint | Mô tả |
|---|---|---|
| GET | / | Trả về index.html (SPA) |
| POST | /api/detect | Nhận pattern + drawing, trả về detections + visualization |
| GET | /api/export/csv | Export kết quả cuối cùng dạng CSV |
| GET | /static/* | CSS, JS, favicon |
POST /api/detect — Request/Response
# Request (multipart/form-data)
pattern: UploadFile # ảnh template
drawing: UploadFile # ảnh bản vẽ
mode: str = "auto" # "auto" | "manual"
ncc_threshold: float = 0.55
cosine_threshold: float = 0.84
final_nms_iou: float = 0.40
use_vlm: str = "false"
# Response (JSON)
{
"success": true,
"total_detections": 22,
"detections": [
{
"bbox": {"x": 69, "y": 9, "w": 108, "h": 63},
"confidence": 0.880,
"ncc_score": 0.822,
"dino_score": 0.939,
"scale": 1.5,
"angle": 0.0
}, ...
],
"elapsed": 24.5,
"visualization": "base64_png_string",
"drawing_preview": "base64_png_string"
}
Pipeline singleton
# Pipeline được khởi tạo một lần, cache trong suốt server lifetime
_pipeline: Optional[PatternDetectionPipeline] = None
def get_pipeline() -> PatternDetectionPipeline:
global _pipeline
if _pipeline is None:
_pipeline = PatternDetectionPipeline()
return _pipeline
# VLM toggle tại runtime (không restart server)
pipeline.use_vlm = coerce_bool(use_vlm) # lazy-loads VLM on first enable
9. Cấu hình hệ thống
Tất cả tham số config
| Key | Default | Mô tả |
|---|---|---|
ncc_threshold | 0.55 | NCC threshold cho Pass 1 (strict) |
nms_iou_threshold | 0.30 | IoU threshold cho NMS nội trong NCC matcher |
dino_model | "dinov2_vits14" | Model DINOv2 (vits14 hoặc vitb14) |
cosine_threshold | 0.84 | DINOv2 cosine similarity threshold (Pass 1+2) |
device | auto | "cuda" hoặc "cpu" — auto detect nếu None |
final_nms_iou | 0.40 | IoU threshold cho Final NMS |
dilate_pattern | 0 | Kernel size để dilate template stroke (0 = off) |
use_dino_dense | True | Kích hoạt Pass C (DINODense) cho large-scale simple templates |
dense_sim_threshold | 0.78 | Similarity threshold cho DINODense |
use_vlm | False | Kích hoạt Stage 3 VLM filter |
vlm_model | "Qwen/Qwen2-VL-2B-Instruct" | HuggingFace model ID cho VLM |
vlm_keep_min_conf | 0.75 | Candidate trên threshold này bỏ qua VLM (trusted) |
vlm_reject_only | True | Blacklist mode: chỉ reject khi VLM gọi đúng tên FP |
vlm_recall_boost | follows use_vlm | Mở rộng scale sweep khi VLM on |
vlm_symbol_name | None | Gợi ý class name cho VLM prompt |
Ví dụ khởi tạo
from src.pipeline import PatternDetectionPipeline
# Cấu hình cơ bản
pipe = PatternDetectionPipeline()
# Cấu hình tùy chỉnh
pipe = PatternDetectionPipeline(config={
"cosine_threshold": 0.84,
"final_nms_iou": 0.4,
"use_vlm": True,
"vlm_symbol_name": "a zigzag resistor",
})
# Chạy detection
result = pipe.detect_auto("template.png", "drawing.png",
return_visualization=True)
print(result["total_detections"])
for d in result["detections"]:
print(d["bbox"], d["confidence"])
10. Docker & HuggingFace Spaces
Dockerfile strategy
FROM python:3.11-slim
# CPU-only PyTorch (HF free tier không có GPU)
RUN pip install "torch==2.1.0+cpu" "torchvision==0.16.0+cpu" \
--extra-index-url https://download.pytorch.org/whl/cpu
RUN pip install -r requirements.txt
COPY . .
# Pre-download DINOv2 weights tại build time (~86MB)
# → tránh cold-start delay khi request đầu tiên
RUN python -c "import torch; torch.hub.load('facebookresearch/dinov2', 'dinov2_vits14')" || true
EXPOSE 7860
CMD ["python", "-m", "uvicorn", "app.web.server:app", "--host", "0.0.0.0", "--port", "7860"]
HuggingFace Spaces YAML frontmatter
--- title: BOM Pattern Detection sdk: docker app_port: 7860 pinned: false ---
11. Kết quả thực nghiệm
Drawing 4 + test_2.png (zigzag resistor template, 1536×1024px)
Các ký hiệu được phát hiện thành công
| Loại | Số lượng | Đặc điểm khó | Kết quả |
|---|---|---|---|
| Horizontal resistor (0°) | 15 | Kích thước 91–129px, NCC 0.66–0.84 | Tất cả detected |
| Vertical resistor (90°) | 7 | DINOv2 score thấp hơn ~0.04 so với horizontal | Tất cả detected |
| Resistor trong hộp kép (GT22) | 1 | DINOv2=0.844, trùng range với diode FP | Missed (irreducible) |
| Resistor ở biên ảnh dưới (GT24) | 1 | NCC bị suppressed trong NCC NMS | Missed (boundary) |
So sánh trước/sau các cải tiến chính
| Version | Detections | GT Covered | FPs | Thay đổi chính |
|---|---|---|---|---|
| Baseline (Session 5) | 25 | 9/24 | 16 | NCC only, no DINOv2 derotate |
| + DINOv2 derotate + 3b threshold | 28 | 22/24 | 4 | 3b DINOv2=0.78, NCC gate 0.55 |
| + 3a threshold 0.878 (FP removal) | 22 | 22/24 | 0 | DINOv2=0.878 loại diode/cap FP |
Example pairs (synthetic test)
| Pair | Detections | Expected | Status |
|---|---|---|---|
| example1 (bridge rectifier) | 5 | 5 | Exact |
| example2 (simple component) | 4 | 4 | Exact |
Unit tests
$ python -m pytest tests/ -q .......... [100%] 10 passed in 0.56s
12. Quyết định thiết kế
Tại sao NCC + DINOv2 thay vì end-to-end detector?
NCC alone: Nhanh nhưng brittle — thất bại khi có noise, scale variation nhỏ, hay domain shift nhẹ giữa template và drawing.
DINOv2 dense sliding window: Chính xác nhưng cực kỳ chậm. Với drawing 1536×1024 và window 63×108: ~(1536-63)×(1024-108) ≈ 1.35M vị trí × encode cost → không thực tế.
Hybrid NCC → DINOv2: NCC propose 200–400 candidates với recall cao. DINOv2 verify với accuracy neural. Tổng cost = NCC (fast) + DINOv2 trên vài trăm crops (manageable). Đây là kiến trúc cascade hiệu quả nhất cho bài toán này.
Tại sao ViT-S thay vì ViT-B?
ViT-S/14 (21M params) được chọn vì: (1) đủ feature quality cho bản vẽ kỹ thuật binary, (2) 2× nhanh hơn ViT-B trên CPU, (3) 0.5GB VRAM thay vì 1.5GB. Swap sang dinov2_vitb14 nếu cần accuracy cao hơn.
Tại sao không dùng CLIP?
Thực nghiệm trong design_spec/model_survey.md: CLIP image-to-image similarity không cho separation FP tốt hơn DINOv2 trên bản vẽ. CLIP text-guided phá vỡ zero-shot vì cần biết tên class trước.
Tại sao VLM dùng open-classification thay vì yes/no?
Model 2B có "agreement bias" — trả lời "yes" cho 40/40 crops khi prompt dạng yes/no. Open-classification bắt model phải chọn từ vocabulary cụ thể, loại bỏ bias này. Chi tiết trong design_spec/model_survey.md §VLM.
Tại sao DINOv2 threshold 3a khác 3b?
Pass 3b (vertical) sử dụng warpAffine để de-rotate crop về horizontal trước khi so sánh với template. Phép biến đổi affine gây ra interpolation artifacts, làm cosine similarity giảm ~0.03–0.05. Đo thực nghiệm: genuine vertical resistors score 0.80–0.83, trong khi horizontal TPs score 0.85–0.94. Dùng chung threshold sẽ miss tất cả vertical instances.
13. Hạn chế & Hướng cải thiện
Hạn chế hiện tại
Hướng cải thiện
Các thành phần đã được loại bỏ
| Thành phần | Lý do loại | Commit |
|---|---|---|
| CLIP ViT-B/32 verifier | Benchmark cho thấy không cải thiện FP separation so với DINOv2. Text-guided phá vỡ zero-shot. | e5f1c38 |
| Borderline DINOv2 rescue (Chamfer+DINOv2) | Candidate được rescue tại (928,731) sau đó xác nhận là FP (wire corner). | d15fb7a |
| LightGlue keypoint verification | Phụ thuộc keypoint descriptor — không hoạt động tốt với binary line-art ít texture. | N/A (không implement) |