OpenCVSeniorExperience

Представьте, pipeline на OpenCV даёт хорошие результаты в dev, но плохо работает на новых данных. Как вы будете разбираться?

Сначала сравните статистики dev и новых данных (яркость, blur score, разрешение), затем визуализируйте каждый шаг pipeline на failing-примерах, ищите захардкоженные пороги и масштабо-зависимые параметры.

Отладка OpenCV pipeline, деградирующего на новых данных

Хорошие метрики на dev-сете при плохих на новых данных — классический признак переобучения к условиям съёмки, а не к задаче. Разберём систематический подход к диагностике.

Шаг 1: Охарактеризовать «новые данные»

Прежде чем трогать код, нужно понять, чем новые данные отличаются от dev-сета.

import cv2
import numpy as np
from pathlib import Path

def image_stats(path: str) -> dict:
    img = cv2.imread(path)
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    return {
        "path": path,
        "shape": img.shape,
        "dtype": str(img.dtype),
        "mean_brightness": float(gray.mean()),
        "std_brightness": float(gray.std()),
        "blur_score": float(cv2.Laplacian(gray, cv2.CV_64F).var()),  # резкость
    }

# Сравнить dev и новые данные
dev_stats = [image_stats(str(p)) for p in Path("data/dev").glob("*.jpg")]
new_stats = [image_stats(str(p)) for p in Path("data/new").glob("*.jpg")]

print("Dev avg brightness:", np.mean([s["mean_brightness"] for s in dev_stats]))
print("New avg brightness:", np.mean([s["mean_brightness"] for s in new_stats]))

Типичные различия: яркость, контраст, размытость (blur score), разрешение, соотношение сторон, цветовая температура, наличие артефактов сжатия.

Шаг 2: Визуализировать промежуточные стадии на failing-примерах

def visualize_pipeline(img_path: str, out_dir: str = "/tmp/debug"):
    import os
    os.makedirs(out_dir, exist_ok=True)
    img = cv2.imread(img_path)
    assert img is not None, f"Не удалось загрузить {img_path}"

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    cv2.imwrite(f"{out_dir}/01_gray.png", gray)

    denoised = cv2.fastNlMeansDenoising(gray, h=10)
    cv2.imwrite(f"{out_dir}/02_denoised.png", denoised)

    _, thresh = cv2.threshold(denoised, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    cv2.imwrite(f"{out_dir}/03_thresh.png", thresh)

    contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    vis = img.copy()
    cv2.drawContours(vis, contours, -1, (0, 255, 0), 2)
    cv2.imwrite(f"{out_dir}/04_contours.png", vis)
    print(f"Found {len(contours)} contours")

Шаг 3: Проверить жёстко заданные пороги

Фиксированные числа — главная причина деградации при смене условий. Заменяйте на адаптивные методы.

# Плохо: захардкоженный порог
_, thresh = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# Хорошо: Otsu автоматически находит порог по гистограмме
_, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# Ещё лучше при неравномерном освещении: адаптивный порог
thresh = cv2.adaptiveThreshold(
    gray, 255,
    cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    cv2.THRESH_BINARY,
    blockSize=11,
    C=2
)

Шаг 4: Нормализация перед обработкой

# CLAHE: адаптивное выравнивание гистограммы
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)

# Нормализация диапазона
normalized = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX)

Шаг 5: Проверить предобработку разрешения

Если dev-сет был 1080p, а новые данные 480p — фильтры с абсолютными размерами ядра (5, 5) теперь размывают значительно сильнее относительно деталей объекта. Масштабируйте ядра пропорционально разрешению.

def adaptive_blur_kernel(img: np.ndarray, base_size: int = 5) -> tuple:
    h, w = img.shape[:2]
    scale = min(h, w) / 720  # относительно базового разрешения
    k = max(3, int(base_size * scale))
    k = k if k % 2 == 1 else k + 1  # ядро должно быть нечётным
    return (k, k)

kernel = adaptive_blur_kernel(gray)
blurred = cv2.GaussianBlur(gray, kernel, 0)

Шаг 6: Построить метрики по обоим сетам

Запустить pipeline на dev и на новых данных с одинаковым скриптом оценки, сравнить распределения метрик (precision, recall, IoU). Это покажет, насколько сильна деградация и в каком классе объектов.

Подводные камни

  • Захардкоженные пороги: Canny(50, 150) и threshold(127) — первые кандидаты на ревизию при смене условий.
  • Размер ядра без масштабирования: GaussianBlur(5,5) на 4K-изображении почти ничего не делает, на 240p — убивает детали.
  • Отсутствие проверки imread: cv2.imread возвращает None без исключения; весь pipeline молча ломается.
  • Разные цветовые пространства камер: HDR-камеры могут давать 16-bit изображения, которые imread обрезает до 8-bit.
  • EXIF-ориентация: cv2.imread игнорирует EXIF; на телефонных снимках изображение может быть повёрнуто на 90°.
  • Выводы только по «лёгким» примерам: dev-сет часто отбирается «удачными» снимками; новые данные содержат граничные случаи.
  • Нет логирования метрик по партиям: без агрегата по 100+ изображениям случайный шум маскирует системную деградацию.

What hurts your answer

  • Сразу обвинять OpenCV, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг OpenCV
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics