OpenCVMiddleSystem design
Как оценивать качество computer vision pipeline: метрики, golden datasets и regression tests?
Качество CV-pipeline оценивают через IoU/mAP для детекции, mIoU для сегментации, фиксированный golden dataset с аннотациями и pytest-регрессионные тесты на precision, recall и latency.
Зачем нужна формальная оценка качества CV-pipeline
Computer vision pipeline часто деградирует незаметно: изменилось освещение на камере, появился новый тип объектов, обновилась модель DNN. Без метрик, тестовых наборов и регрессионных тестов деградация обнаруживается только в production. Правильная система оценки включает три слоя: метрики качества, golden datasets и автоматические regression tests.
1. Метрики для разных задач
Детекция объектов
- IoU (Intersection over Union) — перекрытие предсказанного и ground truth bounding box. Порог 0.5 — стандарт PASCAL VOC.
- Precision / Recall — на конкретном пороге confidence.
- mAP (mean Average Precision) — усреднение AP по классам и порогам IoU. Стандарт COCO: mAP@[0.5:0.95].
Сегментация
- Pixel Accuracy — доля правильно классифицированных пикселей.
- mIoU — среднее IoU по классам сегментации.
- Dice Coefficient — 2*|A∩B| / (|A|+|B|), особенно для медицинских изображений.
Трекинг
- MOTA (Multi-Object Tracking Accuracy) — учитывает FP, FN и ID switches.
- IDF1 — согласованность идентификаторов объектов.
import numpy as np
def compute_iou(box1, box2):
"""box = [x1, y1, x2, y2]"""
x1 = max(box1[0], box2[0])
y1 = max(box1[1], box2[1])
x2 = min(box1[2], box2[2])
y2 = min(box1[3], box2[3])
intersection = max(0, x2 - x1) * max(0, y2 - y1)
area1 = (box1[2] - box1[0]) * (box1[3] - box1[1])
area2 = (box2[2] - box2[0]) * (box2[3] - box2[1])
union = area1 + area2 - intersection
return intersection / union if union > 0 else 0.0
def evaluate_detections(predictions, ground_truths, iou_threshold=0.5):
"""
predictions: list of dicts {box, confidence, class_id}
ground_truths: list of dicts {box, class_id}
"""
tp, fp, fn = 0, 0, 0
matched = set()
for pred in sorted(predictions, key=lambda x: -x["confidence"]):
best_iou, best_idx = 0, -1
for i, gt in enumerate(ground_truths):
if i in matched or gt["class_id"] != pred["class_id"]:
continue
iou = compute_iou(pred["box"], gt["box"])
if iou > best_iou:
best_iou, best_idx = iou, i
if best_iou >= iou_threshold:
tp += 1
matched.add(best_idx)
else:
fp += 1
fn = len(ground_truths) - len(matched)
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
return {"precision": precision, "recall": recall, "tp": tp, "fp": fp, "fn": fn}
2. Golden datasets
Golden dataset — фиксированный набор изображений с размеченными ground truth аннотациями, представляющий реальные условия эксплуатации. Правила составления:
- Включите edge cases: плохое освещение, частичные перекрытия, мелкие объекты.
- Зафиксируйте версию датасета в git (хэш или тег).
- Разделите на подмножества: ночь/день, разные камеры, разные условия.
- Никогда не модифицируйте golden dataset — только добавляйте новые подмножества.
3. Regression tests с pytest
import pytest
import cv2
import json
import numpy as np
from pathlib import Path
GOLDEN_DIR = Path("tests/golden")
ANNOTATIONS = json.loads((GOLDEN_DIR / "annotations.json").read_text())
def load_detector():
net = cv2.dnn.readNetFromONNX("models/detector.onnx")
net.setPreferableBackend(cv2.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv2.dnn.DNN_TARGET_CPU)
return net
@pytest.fixture(scope="module")
def detector():
return load_detector()
@pytest.mark.parametrize("image_name", ANNOTATIONS.keys())
def test_detection_regression(detector, image_name):
image = cv2.imread(str(GOLDEN_DIR / "images" / image_name))
assert image is not None, f"Image not found: {image_name}"
# Run detector
predictions = run_detector(detector, image) # ваша функция
ground_truths = ANNOTATIONS[image_name]
metrics = evaluate_detections(predictions, ground_truths, iou_threshold=0.5)
# Пороги регрессии
assert metrics["precision"] >= 0.85, (
f"{image_name}: precision {metrics['precision']:.3f} < 0.85"
)
assert metrics["recall"] >= 0.80, (
f"{image_name}: recall {metrics['recall']:.3f} < 0.80"
)
def test_latency_regression(detector):
"""Проверка, что pipeline не стал медленнее."""
import time
image = cv2.imread(str(GOLDEN_DIR / "images" / "benchmark.jpg"))
times = []
for _ in range(20):
start = time.perf_counter()
run_detector(detector, image)
times.append(time.perf_counter() - start)
p95 = np.percentile(times, 95)
assert p95 < 0.1, f"P95 latency {p95*1000:.1f}ms exceeds 100ms threshold"
Подводные камни
- Optimistic golden dataset — если датасет состоит только из «хороших» снимков, метрики будут завышены и не отражают реальные условия.
- Нет разбивки по подгруппам — среднее mAP может быть 0.85, но ночные снимки дают 0.3. Всегда считайте метрики по срезам (slice-based evaluation).
- Confidence threshold влияет на precision/recall — тест должен явно фиксировать порог; разные пороги дают несопоставимые цифры.
- Аннотации дрейфуют — если ground truth разметчики меняются, аннотации могут стать несогласованными. Используйте inter-annotator agreement (Cohen's kappa).
- Тест на latency зависит от железа CI — запускайте latency-тесты на dedicated runner или с явной пометкой «flaky на виртуалках».
- Переобучение на golden dataset — если разработчики видят golden dataset и «подгоняют» модель под него, набор теряет ценность. Держите test set изолированным.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.