Представьте, 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
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения