Подготовка к собеседованию по компьютерному зрению
Вопросы для собеседования инженера по компьютерному зрению
15 отобранных вопросов для собеседования по компьютерному зрению, сгруппированных по уровням подготовки. Используйте их для повторения основ, понимания практических компромиссов и рассуждений на уровне Senior-специалиста в производственной среде.
1Что такое дискретная двумерная свёртка изображения и как она связана с ядрами, фильтрами и кросс-корреляцией?
Ядро задаёт веса локального фильтра. В каждой позиции умножают значения изображения на соответствующие веса и суммируют. При свёртке $(I*K)_{ij}=\sum_{mn}K_{mn}I_{i-m,j-n}$ ядро развёрнуто относительно скользящего скалярного произведения. При кросс-корреляции индексы идут как $i+m,j+n$. Симметричные ядра дают одинаковый результат. Типичные слои Conv2d выполняют кросс-корреляцию. Обучаемое ядро может принять нужную ориентацию.
2В каких случаях в конвейере компьютерного зрения следует использовать представления RGB, HSV, Lab, YCbCr или градации серого (grayscale)?
Используйте представление, которое сохраняет необходимые для задачи признаки и соответствует предварительной обработке данных при обучении модели. RGB (красный, зелёный и синий каналы) общепринят для обучаемых моделей; многие API в OpenCV используют BGR (синий, зелёный и красный каналы), поэтому проверяйте порядок каналов, а не предполагайте, что фреймворк обеспечивает его автоматически. HSV (тон, насыщенность, значение) и HSL (тон, насыщенность, светлота) могут быть удобны для цветовой пороговой фильтрации, но цветовой тон (hue) нестабилен вблизи нулевой насыщенности и, как правило, не инвариантен к изменениям освещения или баланса белого. CIE Lab разделяет светлоту и координаты оппонентных цветов и приблизительно перцептивно равномерен при заданных предположениях об условиях просмотра и опорном белом; евклидово расстояние Delta E 1976 является лишь приближённой мерой визуального различия. YCbCr отделяет яркость Y' от каналов цветоразности и поддерживает субдискретизацию цветности (chroma subsampling) во многих форматах изображений и видео; коэффициенты, диапазон и функции передачи зависят от стандарта. Градации серого полезны, когда цвет не имеет значения: они сокращают объем памяти для хранения несжатых каналов с трех до одного при той же точности, но не гарантируют сокращение общих вычислений модели на две трети. Сохраняйте цвет, если он несёт информацию о метках, и проверяйте соглашения о масштабировании и преобразовании.
import cv2
import numpy as np
# Sample image: saturated red under normal and shaded lighting
img_bgr = np.zeros((100, 100, 3), dtype=np.uint8)
img_bgr[:, :50] = [0, 0, 255] # Bright Red in BGR
img_bgr[:, 50:] = [0, 0, 120] # Shaded/Dark Red in BGR
# Convert to HSV (OpenCV H: 0-179, S: 0-255, V: 0-255)
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
# Red Hue wraps around 0/180; track red with single Hue range [0, 10] & high saturation
lower_red = np.array([0, 100, 50])
upper_red = np.array([10, 255, 255])
mask_hsv = cv2.inRange(img_hsv, lower_red, upper_red)
print("HSV segmented pixels:", np.count_nonzero(mask_hsv), "out of", mask_hsv.size)
3Почему в системах компьютерного зрения важны поканальная нормализация изображений, нормализация каждого отдельного изображения и согласованное масштабирование пикселей?
Используйте диапазон пикселей, порядок каналов и нормализацию, ожидаемые обученной моделью, согласованно как при обучении, так и на этапе вывода (inference). Для 8-битных изображений деление на 255 отображает значения в диапазон [0,1], что масштабирует их, но не центрирует относительно нуля; отображение в [-1,1] центрирует диапазон, но не обязательно среднее значение данных. Изображения также могут использовать другую целочисленную разрядность или физические единицы с плавающей точкой. Большие немасштабированные значения могут ухудшить обусловленность оптимизации, но не приводят неизбежно к взрыву градиентов. Поканальная стандартизация датасета использует формулу (x-mu_c)/sigma_c со статистиками обучающей выборки или заданными константами предобученной модели; она обеспечивает нулевое среднее и единичную дисперсию на том распределении, по которому они оценивались, а не на каждом новом изображении или датасете. Стандартизация по z-оценке отдельного изображения использует среднее и стандартное отклонение этого конкретного изображения с защитой от околонулевой дисперсии; масштабирование min-max — это другая операция. InstanceNorm — это слой нейронной сети, который обычно нормализует каждый пример и канал по пространственным осям, а не синоним любой предобработки отдельных изображений. Нормализация каждого изображения по его собственным статистикам может уменьшить вариации яркости и контраста, но способна удалить полезную информацию об абсолютной интенсивности, поэтому проверяйте её целесообразность для конкретной задачи.
import torch
import torchvision.transforms as T
from PIL import Image
import numpy as np
# Create a mock uint8 RGB image
raw_img = Image.fromarray(np.random.randint(0, 256, (224, 224, 3), dtype=np.uint8))
# Standard transform pipeline:
# 1. ToTensor scales uint8 [0, 255] -> float32 [0.0, 1.0] and permutes HWC -> CHW
# 2. Normalize standardizes per-channel using dataset mean and std
preprocess = T.Compose([
T.ToTensor(),
T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])
tensor_img = preprocess(raw_img)
print("Tensor shape:", tensor_img.shape)
print("Tensor dtype:", tensor_img.dtype)
print("Channel 0 min/max:", round(tensor_img[0].min().item(), 2), round(tensor_img[0].max().item(), 2))
4Как геометрические и фотометрические аугментации помогают модели и как проверить, сохраняет ли преобразование метку?
Поворот, сдвиг и масштаб меняют координаты, а яркость, цвет и размытие меняют значения пикселей. При классификации модель учится выдавать ту же метку при допустимых изменениях. При детекции и сегментации вместе с изображением преобразуйте рамки или маски. Проверьте смысл метки и правдоподобие для рабочего домена. Отражение может сохранять класс машины, но портить букву. Поворот допустим для некоторых снимков сверху, но часто нереалистичен для дорожной камеры.
5Как работают ближайший сосед, билинейная, бикубическая и площадная интерполяция? Откуда берутся алиасинг и размытие?
Ближайший сосед выбирает один пиксель и сохраняет дискретные значения, например метки маски. Билинейная смешивает четыре, бикубическая использует окрестность 4 × 4 и может давать ореолы. Площадной метод усредняет вклад исходных пикселей по перекрытию и полезен при уменьшении. Без достаточного подавления частот выше нового предела дискретизации появляются муар и ложные узоры. Сглаживающий фильтр уменьшает этот риск ценой потери резких деталей.
6На каких реальных ухудшениях изображения проверять систему машинного зрения и как они меняют входные данные?
Проверяйте слабый свет, блики, осадки, размытие, шум сенсора, rolling shutter, компрессию и низкое разрешение. Отдельно измеряйте частичные перекрытия, обрезку кадром и необычные ракурсы. Эти условия меняют яркость, контраст и детализацию или закрывают признаки цели. Соберите реальные примеры и контролируемые уровни искажений. Сравнивайте пропуски, ложные срабатывания и калибровку по условиям и их сочетаниям.
7Как HOG представляет структуру изображения и чем уступает признакам, обученным свёрточной сетью?
HOG вычисляет градиенты яркости, собирает по ячейкам гистограммы направлений с весом по величине градиента и нормализует соседние ячейки блоками. Вектор отражает локальные края и форму и может служить входом линейному SVM. Нормализация уменьшает влияние локального контраста. Дескриптор фиксирован и чувствителен к масштабу, позе и фону. CNN может обучить многоуровневые признаки под задачу, но качество зависит от данных и обучения.
8Как шаг, отступы, дилатация и размер ядра определяют размер выхода свёртки и рецептивное поле?
Вдоль оси эффективный размер ядра $k'=d(k-1)+1$. Для входа $I$, шага $s$ и суммарного отступа $p_l+p_r$ размер выхода равен $\lfloor(I+p_l+p_r-k')/s\rfloor+1$. При симметричном отступе сумма равна $2p$. Для цепочки слоёв обновляйте накопленный шаг $j_l=j_{l-1}s_l$ и рецептивное поле $r_l=r_{l-1}+(k'_l-1)j_{l-1}$, начиная с $j_0=r_0=1$. Дилатация увеличивает охват без добавления весов.
9Как ядро свёртки связано с классическим линейным фильтром и когда двумерный фильтр разделим?
Ядро — импульсная характеристика линейного фильтра, неизменного при сдвиге. Его спектр определяет, какие пространственные частоты он пропускает. Ненулевое ядро разделимо, когда $K=uv^\top$, то есть имеет ранг 1. Тогда выполните два одномерных прохода. Для квадратного ядра на пиксель получается порядка $2k$ операций вместо $k^2$, хотя ускорение зависит от реализации. Через SVD можно также приблизить неразделимое ядро суммой нескольких разделимых.
10Как масштабирование, центральная или случайная обрезка, растяжение и letterbox меняют входные данные?
Растяжение до заданных сторон искажает пропорции. Центральная обрезка оставляет центр и может удалить периферийный объект. Случайная обрезка с последующим изменением размера расширяет диапазон масштабов и видимых частей, но может потерять цель. Letterbox сохраняет пропорции, вписывая изображение в целевой прямоугольник и дополняя поля. Поля сами становятся признаком. При детекции преобразуйте разметку теми же коэффициентами и смещениями и проверьте соответствие обучения и эксплуатации.
11Какие ошибки разметки встречаются в компьютерном зрении и как их обнаружить и уменьшить?
Проверяйте неверные классы, пропущенные объекты, неточные рамки и точки, неоднозначные границы масок. Сравнивайте независимую разметку и проверяйте случаи, где прогнозы вне обучающего блока расходятся с ней. Высокая потеря помогает отобрать примеры, но может означать трудный корректный случай. Уточняйте инструкции, разрешайте пометки неоднозначных областей, повторно размечайте выборку и проверяйте согласие. Устойчивые потери могут уменьшить влияние остаточного шума.
import torch
import torch.nn.functional as F
def find_label_noise_candidates(model, dataloader, device, top_k=50):
model.eval()
sample_losses = []
with torch.no_grad():
for batch_idx, (images, targets, sample_ids) in enumerate(dataloader):
images, targets = images.to(device), targets.to(device)
logits = model(images)
loss_per_sample = F.cross_entropy(logits, targets, reduction='none')
for sid, l, target, pred in zip(sample_ids, loss_per_sample.cpu(), targets.cpu(), logits.argmax(dim=-1).cpu()):
sample_losses.append({'id': sid, 'loss': l.item(), 'target': target.item(), 'pred': pred.item()})
sample_losses.sort(key=lambda x: x['loss'], reverse=True)
return sample_losses[:top_k]
12Как дисбаланс классов влияет на обучение классификаторов изображений, детекторов и моделей плотного предсказания?
При классификации частые классы дают больше слагаемых потери, поэтому общая точность может скрывать пропуски редких классов. В детекции фон может подавлять сигнал от объектов, а редкие категории дают мало положительных примеров. В сегментации большие области дают больше пикселей, поэтому небольшие и тонкие объекты могут теряться. Проверяйте метрики по классам и размерам. Подбирайте взвешивание, отбор примеров или подходящую потерю по реальным ограничениям задачи.
13Как бы вы отлаживали появление значений NaN или нестабильность функции потерь при масштабном распределённом обучении моделей компьютерного зрения?
Отладка значений NaN или нестабильности функции потерь при масштабном обучении моделей компьютерного зрения с использованием распределённого параллелизма по данным (DDP) требует изоляции первопричины: вызвана ли она некорректными входными данными, численным переполнением или округлением слишком малых значений до нуля при автоматическом использовании смешанной точности (AMP) или нестабильностью оптимизации между рабочими процессами. Во-первых, зафиксируйте воспроизводимые входные данные, начальные состояния генераторов случайных чисел и конфигурацию и добавьте диагностические хуки (само по себе обнаружение аномалий не делает выполнение детерминированным): включите детектор аномалий (`torch.autograd.set_detect_anomaly(True)`), зарегистрируйте хуки на градиенты и активации, чтобы отследить точный слой, где появляются NaN, и внедрите строгую валидацию данных в DataLoader (проверка на конечность значений, выявление поврежденных файлов нулевого размера, пустых ограничивающих рамок или нулевой дисперсии в делителях при нормализации). Логирование на уровне мини-батчей должно отслеживать потери по каждому рангу, URI входных данных, норму градиентов до клиппинга и коэффициенты масштабирования GradScaler. Во-вторых, проверьте работу автоматического использования смешанной точности (AMP) и динамического масштабирования потерь: в FP16 большие градиенты легко приводят к переполнению (`> 65504`), в результате чего GradScaler пропускает некорректные шаги обновления и умножает масштаб на заданный коэффициент уменьшения. Устойчивое падение коэффициента масштабирования является симптомом проблемы, требующим исследования, а не нормальной сходимостью к нулю; перевод нестабильных операций (например, softmax, LayerNorm, экспонент в focal loss или знаменателей IoU для рамок) в FP32 или переход на BF16 (чей динамический диапазон совпадает с FP32) обычно стабилизирует обучение. Наконец, проверьте специфичные для DDP проблемы: операции all-reduce, распространяющие NaN с одного рабочего процесса на все ранги, масштабирование темпа обучения при прогреве (warmup, например, линейное правило масштабирования при больших глобальных размерах батча) и выполнение клиппинга градиентов строго после отмены масштабирования (unscaling) градиентов AMP. Пустые аннотации могут являться валидными отрицательными примерами, поэтому сверяйте их с контрактом задачи, а не отбрасывайте любое изображение без объектов.
import torch
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4)
for step, (images, targets, uris) in enumerate(dataloader):
# 1. Input sanitization
if not torch.isfinite(images).all():
print(f"Corrupt input detected from URIs: {uris}")
continue
optimizer.zero_grad(set_to_none=True)
with autocast(dtype=torch.float16):
outputs = model(images)
loss = criterion(outputs, targets)
if not torch.isfinite(loss):
print(f"NaN/Inf loss at step {step} on rank {torch.distributed.get_rank()}; skipping step.")
continue
scaler.scale(loss).backward()
# Unscale before clipping to inspect true gradient norms
scaler.unscale_(optimizer)
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
if torch.isnan(grad_norm) or torch.isinf(grad_norm):
print(f"Warning: Grad norm is {grad_norm}. Scaler will adjust.")
scaler.step(optimizer)
scaler.update()
14Как бы вы спроектировали компонент цифровой стабилизации видео в режиме онлайн для камеры с дрожанием кадров?
Оцените робастное межкадровое движение по отслеживаемым углам или сопоставленным дескрипторам, исключая движение объектов переднего плана и выбирая подходящую модель движения: сдвиг (translation), подобие (similarity), аффинную модель или гомографию. Явно определите направления преобразований: если используются векторы-столбцы и матрица H_t отображает координаты кадра t-1 в кадр t, траектория от опорного кадра к текущему выражается как P_t = H_t * P_{t-1}. Сглаживайте эту траекторию до S_t с помощью каузального фильтра, использующего только текущие и прошлые кадры, или ограниченного окна с будущими кадрами (lookahead), корректно обрабатывая вращения и преобразования, а не просто усредняя матричные элементы. Прямое корректирующее преобразование, переводящее координаты текущего кадра в стабилизированный, задается как C_t = S_t * inverse(P_t); передавайте само преобразование или обратное к нему в зависимости от соглашения о выборке координат в API трансформации (warp API). Ограничьте величину обрезки/зума (crop/zoom) и силу стабилизации для обработки недопустимых границ; сбрасывайте или восстанавливайте состояние при срыве трекинга и смене планов сцены. Сильный параллакс и эффект построчного считывания при скользящем затворе (rolling shutter) могут потребовать более сложных моделей движения или поддержки инерциальных датчиков. Строго каузальная реализация не использует будущие кадры; каждый кадр в окне lookahead увеличивает задержку захвата, поэтому выбирайте глубину буферизации и политику кадрирования на основе измеренного качества выходного видео и требований к задержке.
15Как бы вы оптимизировали конвейер компьютерного зрения реального времени, если детектор работает медленнее целевой частоты кадров?
Когда детектор объектов не может поддерживать целевую частоту кадров (например, выдаёт 15–20 кадров в секунду (FPS) на видеопотоке в 60 FPS), надёжная архитектура для рабочей среды отделяет детекцию от цикла отображения реального времени с помощью асинхронного многопоточного конвейера, сочетающего тяжёлую детекцию с лёгким трекингом. В такой гибридной архитектуре поток захвата непрерывно считывает видеокадры. Тяжёлый детектор выполняется асинхронно в качестве «детектора опорных кадров» в фоновом рабочем потоке. В то же время лёгкий трекер (например, на основе оптического потока Лукаса — Канаде или корреляционного фильтра, опционально совмещённый с предсказанием фильтра Калмана) выполняется синхронно на каждом кадре на полных 60 FPS, поддерживая предсказанное состояние объектов между обновлениями детектора. Системы ByteTrack/BoT-SORT работают по принципу отслеживания на основе детекции (tracking-by-detection): при отсутствии новых детекций им требуется явная стратегия предсказания или визуального трекинга, при этом необходимо отслеживать возраст треков, неопределённость и полноту обнаружения (recall) новых объектов. Для обработки отставания и ограничения поступления данных при перегрузке (backpressure) без создания неограниченной задержки или накопления устаревших кадров используются ограниченные кольцевые буферы и стратегии сброса кадров в пользу самого свежего. Когда детектор завершает обработку кадра T_0 в момент времени T_curr, его вывод оказывается устаревшим. Конвейер выполняет пересчёт координат / обратное проецирование: он сопоставляет задержанные результаты детекции с историческим состоянием треклета в момент T_0, обновляет идентификаторы и пропущенные треки, после чего переносит исправления вперёд к моменту T_curr с помощью векторов движения трекера или шагов предсказания фильтра Калмана.
import queue
import threading
frame_queue = queue.Queue(maxsize=1) # Drop stale frames, keep latest
det_result_queue = queue.Queue()
def detector_worker():
while True:
frame, frame_id, timestamp = frame_queue.get()
boxes, scores, classes = heavy_detector.infer(frame)
det_result_queue.put({'frame_id': frame_id, 'boxes': boxes, 'timestamp': timestamp})
def realtime_pipeline(video_stream):
tracker = FastTracker() # e.g. Optical flow or Kalman tracker
for frame, frame_id, timestamp in video_stream:
if frame_queue.empty():
frame_queue.put((frame, frame_id, timestamp))
if not det_result_queue.empty():
det_result = det_result_queue.get()
tracker.reconcile_and_correct(det_result, current_frame_id=frame_id)
active_tracks = tracker.update(frame)
display_or_downstream(frame, active_tracks)