$ sudo teach|
    $ sudo teach|
    IT school
  • Telegram
  • Партнёрам
  • Все курсы
$ sudo teach IT
OOO "SALEPROFIT"Контакты и реквизитыIT-Park Logo

Школа

  • Блог
  • Проверить сертификат

Сотрудничество

  • Стать учителем
  • Партнёрская программа
  • О проекте

Право

  • Оферта
  • Политика конфиденциальности

© 2023–2026 $ sudo teach IT™. All Rights Reserved. Public user contributions licensed under CC BY-SA 4.0 license with attribution required
TelegramGitHubYouTube
ГлавнаяБлогКомпьютерное зрение на практике: детектируем объекты на видео с помощью OpenCV и Python

Компьютерное зрение на практике: детектируем объекты на видео с помощью OpenCV и Python

$ sudo teach IT
·18 июля 2026 г.·10 мин·67
Компьютерное зрение на практике: детектируем объекты на видео с помощью OpenCV и Python

Шаг за шагом строим пайплайн обработки видеопотока: захват кадров, фоновое вычитание, трекинг объектов. Весь код — рабочий, с объяснением каждого параметра.

Содержание
Что такое пайплайн обработки видео и зачем он нуженЗахват видеопотокаОсновной цикл чтения кадровПредобработка кадраФоновое вычитаниеПодводные камни фонового вычитанияМорфологические операции: очищаем маскуПоиск контуров и фильтрация объектовПростой трекинг объектовСобираем всё вместеТипичные проблемы и как их решатьСлишком много ложных срабатыванийОбъекты «мигают» — то обнаруживаются, то нетНесколько объектов сливаются в одинВысокая нагрузка на CPUКуда двигаться дальшеИтог

Компьютерное зрение — одна из тех областей, где разрыв между теорией и практикой ощущается особенно остро. Можно прочитать десяток статей о свёрточных сетях и алгоритмах детекции, но первый же реальный видеопоток с шумом, переменным освещением и перекрывающимися объектами ставит в тупик. Именно поэтому имеет смысл начинать не с нейросетей, а с классических инструментов — и прежде всего с OpenCV.

В этой статье мы построим полноценный пайплайн обработки видеопотока: захватим кадры с камеры или из файла, выделим движущиеся объекты через фоновое вычитание, применим морфологические операции для очистки маски, найдём контуры и реализуем простой трекинг. Весь код рабочий, каждый параметр объяснён — не для галочки, а чтобы вы понимали, что происходит и почему.


Что такое пайплайн обработки видео и зачем он нужен

Видео — это последовательность кадров. Звучит банально, но именно из этого факта вытекает вся архитектура работы с видеопотоком: мы обрабатываем кадры по одному, применяя к каждому набор операций. Этот набор и называют пайплайном.

Типичный пайплайн детекции объектов на видео выглядит так:

  1. Захват кадра — получаем изображение из источника.
  2. Предобработка — приводим кадр к нужному формату (оттенки серого, размытие).
  3. Детекция — выделяем интересующие области (объекты, движение).
  4. Постобработка — убираем шум, объединяем фрагменты.
  5. Трекинг — связываем объекты между кадрами.
  6. Визуализация — рисуем bounding box'ы, метки, траектории.

Каждый этап влияет на следующий. Плохое размытие на шаге 2 — и детектор на шаге 3 будет находить десятки ложных объектов. Пропустили постобработку — трекер будет путаться. Разберём каждый шаг подробно.


Захват видеопотока

OpenCV предоставляет класс VideoCapture, который умеет работать как с файлами, так и с камерами.

code
import cv2

# Захват с веб-камеры (индекс 0 — первая доступная камера)
cap = cv2.VideoCapture(0)

# Или захват из файла:
# cap = cv2.VideoCapture('traffic.mp4')

if not cap.isOpened():
    raise RuntimeError("Не удалось открыть источник видео")

# Получаем параметры потока
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

print(f"Разрешение: {width}x{height}, FPS: {fps}")

Важный момент: cap.get(cv2.CAP_PROP_FPS) для веб-камеры часто возвращает 0 или неверное значение — это нормально, реальный FPS определяется скоростью захвата и обработки. Для файлов значение обычно корректное.

Основной цикл чтения кадров

code
while True:
    ret, frame = cap.read()
    
    # ret — булево значение: True, если кадр успешно считан
    if not ret:
        print("Поток завершён или кадр не получен")
        break
    
    # Здесь будет вся обработка
    
    cv2.imshow('Frame', frame)
    
    # Выход по нажатию 'q'
    if cv2.waitKey(1) & 0xFF == ord('q'):
        break

cap.release()
cv2.destroyAllWindows()

cv2.waitKey(1) — задержка в 1 миллисекунду. Это не просто пауза: без вызова waitKey окно OpenCV не будет обновляться. Значение & 0xFF нужно для корректной работы на 64-битных системах, где waitKey может возвращать значения больше 255.


Предобработка кадра

Сырой кадр с камеры содержит цветовые шумы, артефакты сжатия и другие помехи. Перед детекцией его нужно подготовить.

code
def preprocess_frame(frame):
    # Переводим в оттенки серого — большинство алгоритмов детекции
    # работают с одноканальным изображением
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    
    # Гауссово размытие убирает высокочастотный шум.
    # (21, 21) — размер ядра (должен быть нечётным),
    # 0 — стандартное отклонение (вычисляется автоматически из размера ядра)
    blurred = cv2.GaussianBlur(gray, (21, 21), 0)
    
    return blurred

Размер ядра размытия — один из ключевых параметров. Слишком маленькое ядро (например, 3×3) почти не убирает шум. Слишком большое (51×51) размоет реальные границы объектов, и детектор их пропустит. Для большинства задач хорошо работают значения от 5×5 до 21×21 — зависит от разрешения и характера шума.


Фоновое вычитание

Фоновое вычитание (background subtraction) — классический подход к детекции движения. Идея проста: если у нас есть модель «пустой» сцены, всё, что от неё отличается, — это объекты переднего плана.

OpenCV предоставляет несколько готовых алгоритмов. Два наиболее используемых:

  • MOG2 (Mixture of Gaussians v2) — моделирует каждый пиксель как смесь гауссовых распределений. Хорошо справляется с постепенными изменениями освещения.
  • KNN (K-Nearest Neighbours) — более точен при резких изменениях фона, но медленнее.
code
# Создаём вычитатель фона
# history=500 — количество кадров для построения модели фона
# varThreshold=16 — порог чувствительности (чем выше, тем меньше ложных срабатываний)
# detectShadows=True — алгоритм будет отмечать тени серым цветом (127)
bg_subtractor = cv2.createBackgroundSubtractorMOG2(
    history=500,
    varThreshold=16,
    detectShadows=True
)

def apply_background_subtraction(frame):
    # Применяем вычитание фона
    # learningRate=-1 означает автоматический выбор скорости обучения
    fg_mask = bg_subtractor.apply(frame, learningRate=-1)
    
    # Убираем тени: заменяем серые пиксели (127) чёрными (0)
    # Оставляем только белые пиксели (255) — реальные объекты переднего плана
    _, fg_mask = cv2.threshold(fg_mask, 200, 255, cv2.THRESH_BINARY)
    
    return fg_mask

Подводные камни фонового вычитания

Проблема 1: Медленная инициализация. Первые history кадров алгоритм строит модель фона — в этот период маска будет «шумной». Для файлов можно прогнать несколько секунд видео без обработки, чтобы модель стабилизировалась.

Проблема 2: Статичные объекты. Если объект остановился, через несколько секунд он «поглощается» фоном — алгоритм начнёт считать его частью сцены. Это фундаментальное ограничение метода, а не баг.

Проблема 3: Освещение. Резкое изменение освещения (облако закрыло солнце, включился свет) даёт всплеск ложных срабатываний. Параметр varThreshold помогает, но не решает проблему полностью.


Морфологические операции: очищаем маску

После фонового вычитания маска обычно содержит дыры внутри объектов и мелкий шум снаружи. Морфологические операции позволяют это исправить.

code
def clean_mask(fg_mask):
    # Ядро для морфологических операций
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    
    # Эрозия убирает мелкие белые точки (шум)
    # Но также уменьшает размер реальных объектов
    eroded = cv2.erode(fg_mask, kernel, iterations=1)
    
    # Дилатация восстанавливает размер объектов
    # и заполняет небольшие дыры
    dilated = cv2.dilate(eroded, kernel, iterations=3)
    
    # Закрытие (closing) = дилатация + эрозия
    # Хорошо заполняет дыры внутри объектов
    closed = cv2.morphologyEx(dilated, cv2.MORPH_CLOSE, kernel, iterations=2)
    
    return closed

Последовательность операций имеет значение. Сначала эрозия убирает шум, потом дилатация восстанавливает объекты — это называется «открытие» (opening). Закрытие делает обратное: сначала дилатация, потом эрозия, что заполняет дыры. Комбинируя их, можно добиться чистой маски даже при сложном фоне.


Поиск контуров и фильтрация объектов

Теперь из маски нужно извлечь отдельные объекты. OpenCV находит контуры — замкнутые кривые, описывающие границы белых областей.

code
def find_objects(fg_mask, min_area=500):
    # cv2.RETR_EXTERNAL — только внешние контуры (без вложенных)
    # cv2.CHAIN_APPROX_SIMPLE — сжатое представление (только угловые точки)
    contours, _ = cv2.findContours(
        fg_mask,
        cv2.RETR_EXTERNAL,
        cv2.CHAIN_APPROX_SIMPLE
    )
    
    objects = []
    for contour in contours:
        area = cv2.contourArea(contour)
        
        # Фильтруем слишком маленькие контуры — это шум
        if area < min_area:
            continue
        
        # Получаем ограничивающий прямоугольник
        x, y, w, h = cv2.boundingRect(contour)
        
        # Центр объекта
        cx = x + w // 2
        cy = y + h // 2
        
        objects.append({
            'bbox': (x, y, w, h),
            'center': (cx, cy),
            'area': area
        })
    
    return objects

Параметр min_area — критически важный порог. Слишком маленькое значение — и вы будете трекать каждую пылинку. Слишком большое — пропустите реальные объекты. Подбирается эмпирически под конкретную задачу: для пешеходов в кадре видеонаблюдения это обычно 1000–3000 пикселей, для транспорта — 5000 и выше.


Простой трекинг объектов

Детекция находит объекты в каждом кадре независимо. Трекинг — это связывание обнаруженных объектов между кадрами, чтобы понять: «этот объект в кадре N — тот же, что в кадре N-1».

Реализуем простой трекер на основе евклидова расстояния. Это не самый точный метод (существуют SORT, DeepSORT, ByteTrack), но он прозрачен и хорошо объясняет принцип.

code
import numpy as np
from collections import OrderedDict

class CentroidTracker:
    def __init__(self, max_disappeared=30):
        # Счётчик для присвоения уникальных ID
        self.next_object_id = 0
        # Словарь: ID -> центроид
        self.objects = OrderedDict()
        # Словарь: ID -> количество кадров без обнаружения
        self.disappeared = OrderedDict()
        # Максимум кадров без обнаружения до удаления объекта
        self.max_disappeared = max_disappeared
    
    def register(self, centroid):
        """Регистрируем новый объект с уникальным ID."""
        self.objects[self.next_object_id] = centroid
        self.disappeared[self.next_object_id] = 0
        self.next_object_id += 1
    
    def deregister(self, object_id):
        """Удаляем объект из трекера."""
        del self.objects[object_id]
        del self.disappeared[object_id]
    
    def update(self, input_centroids):
        """
        Обновляем трекер новыми центроидами.
        Возвращает словарь ID -> центроид.
        """
        # Если нет входных данных — увеличиваем счётчик исчезновения
        if len(input_centroids) == 0:
            for object_id in list(self.disappeared.keys()):
                self.disappeared[object_id] += 1
                if self.disappeared[object_id] > self.max_disappeared:
                    self.deregister(object_id)
            return self.objects
        
        # Если трекер пуст — регистрируем все объекты
        if len(self.objects) == 0:
            for centroid in input_centroids:
                self.register(centroid)
            return self.objects
        
        # Вычисляем матрицу расстояний между существующими
        # и новыми центроидами
        object_ids = list(self.objects.keys())
        object_centroids = list(self.objects.values())
        
        # Матрица расстояний: строки — существующие объекты,
        # столбцы — новые центроиды
        D = np.linalg.norm(
            np.array(object_centroids)[:, np.newaxis] - 
            np.array(input_centroids)[np.newaxis, :],
            axis=2
        )
        
        # Жадное сопоставление: для каждого существующего объекта
        # находим ближайший новый центроид
        rows = D.min(axis=1).argsort()
        cols = D.argmin(axis=1)[rows]
        
        used_rows = set()
        used_cols = set()
        
        for (row, col) in zip(rows, cols):
            if row in used_rows or col in used_cols:
                continue
            
            # Если расстояние слишком большое — не сопоставляем
            if D[row, col] > 100:
                continue
            
            object_id = object_ids[row]
            self.objects[object_id] = input_centroids[col]
            self.disappeared[object_id] = 0
            
            used_rows.add(row)
            used_cols.add(col)
        
        # Необработанные строки — объекты, которые не нашли пару
        unused_rows = set(range(D.shape[0])) - used_rows
        for row in unused_rows:
            object_id = object_ids[row]
            self.disappeared[object_id] += 1
            if self.disappeared[object_id] > self.max_disappeared:
                self.deregister(object_id)
        
        # Необработанные столбцы — новые объекты
        unused_cols = set(range(D.shape[1])) - used_cols
        for col in unused_cols:
            self.register(input_centroids[col])
        
        return self.objects

Порог расстояния 100 пикселей — ещё один эмпирический параметр. Он должен быть больше максимального смещения объекта между кадрами, но меньше расстояния между соседними объектами. Для видео с высоким FPS объекты смещаются мало — порог можно снизить. Для низкого FPS — увеличить.

Тем, кто хочет разобраться с OpenCV системно — от базовых операций с изображениями до сложных пайплайнов, — стоит посмотреть на специализированный курс по OpenCV: там эти концепции разбираются последовательно, с практическими задачами на реальных данных.


Собираем всё вместе

code
import cv2
import numpy as np

def draw_results(frame, tracked_objects, objects_data):
    """Визуализируем результаты на кадре."""
    # Рисуем bounding box'ы
    for obj in objects_data:
        x, y, w, h = obj['bbox']
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
    
    # Рисуем ID трекера
    for object_id, centroid in tracked_objects.items():
        cx, cy = centroid
        cv2.circle(frame, (cx, cy), 4, (0, 0, 255), -1)
        cv2.putText(
            frame,
            f"ID {object_id}",
            (cx - 10, cy - 10),
            cv2.FONT_HERSHEY_SIMPLEX,
            0.5,
            (0, 0, 255),
            2
        )
    
    return frame


def main():
    cap = cv2.VideoCapture(0)  # или путь к файлу
    
    bg_subtractor = cv2.createBackgroundSubtractorMOG2(
        history=500,
        varThreshold=16,
        detectShadows=True
    )
    
    tracker = CentroidTracker(max_disappeared=30)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        # Предобработка
        gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        blurred = cv2.GaussianBlur(gray, (21, 21), 0)
        
        # Фоновое вычитание
        fg_mask = bg_subtractor.apply(blurred, learningRate=-1)
        _, fg_mask = cv2.threshold(fg_mask, 200, 255, cv2.THRESH_BINARY)
        
        # Морфологическая очистка
        fg_mask = cv2.erode(fg_mask, kernel, iterations=1)
        fg_mask = cv2.dilate(fg_mask, kernel, iterations=3)
        fg_mask = cv2.morphologyEx(fg_mask, cv2.MORPH_CLOSE, kernel, iterations=2)
        
        # Поиск объектов
        objects = find_objects(fg_mask, min_area=800)
        
        # Обновление трекера
        centroids = [obj['center'] for obj in objects]
        tracked = tracker.update(centroids)
        
        # Визуализация
        result = draw_results(frame.copy(), tracked, objects)
        
        cv2.imshow('Detection', result)
        cv2.imshow('Mask', fg_mask)
        
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    cap.release()
    cv2.destroyAllWindows()


if __name__ == '__main__':
    main()

Типичные проблемы и как их решать

Слишком много ложных срабатываний

  • Увеличьте varThreshold в MOG2 (попробуйте 25–50).
  • Увеличьте min_area в find_objects.
  • Добавьте больше итераций эрозии в очистке маски.
  • Проверьте освещение: нестабильный свет — главный враг фонового вычитания.

Объекты «мигают» — то обнаруживаются, то нет

  • Уменьшите varThreshold — алгоритм стал слишком строгим.
  • Увеличьте max_disappeared в трекере — объект не будет удаляться так быстро.
  • Проверьте размытие: возможно, ядро слишком большое и размывает границы объектов.

Несколько объектов сливаются в один

  • Уменьшите ядро дилатации.
  • Рассмотрите применение алгоритма водораздела (cv2.watershed) для разделения слипшихся объектов.

Высокая нагрузка на CPU

  • Уменьшите разрешение: frame = cv2.resize(frame, (640, 480)) перед обработкой.
  • Обрабатывайте не каждый кадр: if frame_count % 2 == 0: process(frame).
  • Переведите горячие места на NumPy-операции вместо Python-циклов.

Куда двигаться дальше

Описанный пайплайн — хорошая отправная точка, но у него есть потолок. Фоновое вычитание плохо работает при движущейся камере, сложном фоне и перекрытиях. Для более сложных сценариев следующие шаги:

  • Оптический поток (Lucas-Kanade, Farneback) — для трекинга при движущейся камере.
  • Детекторы на основе нейросетей — YOLO, SSD, EfficientDet — для точной классификации объектов.
  • Современные трекеры — SORT, ByteTrack — для надёжного трекинга в толпе.
  • OpenCV DNN-модуль — позволяет запускать предобученные модели прямо через OpenCV без зависимости от тяжёлых фреймворков.

Итог

Мы построили полноценный пайплайн: захват видеопотока, предобработка кадра, фоновое вычитание с MOG2, морфологическая очистка маски, поиск контуров и трекинг на основе центроидов. Каждый компонент независим и заменяем — это важное свойство хорошей архитектуры. Можно поменять алгоритм фонового вычитания на KNN, трекер на SORT, или добавить нейросетевой детектор — остальные части останутся нетронутыми.

Классический OpenCV — не устаревший инструмент, а фундамент. Понимание того, как работают эти алгоритмы на уровне пикселей и матриц, делает работу с современными нейросетевыми подходами осознанной, а не магической.

Если хочется разобраться в OpenCV системно — от базовой работы с изображениями до сложных задач компьютерного зрения, — стоит обратить внимание на курс по OpenCV: там практика поставлена в центр, а теория объясняется ровно в том объёме, который нужен для реального применения.

Войдите, чтобы поставить лайк и оставить комментарий.

Автор

$ sudo teach IT

OpenCV

Курс по теме

OpenCV

Превратите изображения в данные. OpenCV — ключ к компьютерному зрению. От первых пикселей до собственных проектов.

Открыть курс

Продолжите обучение

Все курсы
Python – для начинающих!

Python – для начинающих!

С нуля до профессионального уровня. Подходит для всех. Учитесь каждый день и овладейте самым популярным языком программирования.

Перейти к курсу
Приложения для iPhone и Apple Watch на SwiftUI

Приложения для iPhone и Apple Watch на SwiftUI

Разработка приложений для iPhone и Apple Watch на SwiftUI: навигация, SwiftData, виджеты, часы, выпуск. Нужен Mac с Xcode 27, сами устройства не нужны.

Перейти к курсу
Ботостроение Telegram

Ботостроение Telegram

Лёгкий, быстрый и доступный способ познакомиться с миром ботостроения в Telegram. Видео, конспекты, практика и помощь – всё у нас на курсе.

Перейти к курсу
Приложения для macOS на SwiftUI

Приложения для macOS на SwiftUI

Разработка приложений для Mac на SwiftUI: окна и меню, Liquid Glass, SwiftData, сеть, выпуск. Нужен Mac с macOS 27 и Xcode 27.

Перейти к курсу

Другие статьи

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость
python

Python для аналитики на практике: как строить признаки и проверять их воспроизводимость

Поймём, где чаще всего ломается воспроизводимость (версии данных, фильтры, дрейф) и как оформить пайплайн признаков так, чтобы результаты совпадали между запусками и окружениями.

17 сентября 2026 г.
1040
Как работает компьютерное зрение: от пикселя до распознавания объектов
работает

Как работает компьютерное зрение: от пикселя до распознавания объектов

Обзорная статья о ключевых концепциях CV — фильтрации, детекции краёв, contours и базовом распознавании — с примерами на OpenCV и Python.

16 июля 2026 г.
650
Создание Telegram бота в 2026 легко и просто! Полный курсы!
создание

Создание Telegram бота в 2026 легко и просто! Полный курсы!

19 июня 2026 г.
1421
Как выбрать, куда идти в IT: веб, боты, данные — без хаоса
выбрать

Как выбрать, куда идти в IT: веб, боты, данные — без хаоса

Разберём самые понятные направления для новичка (веб, боты, аналитика/данные) и чем они отличаются по шагам обучения. В конце вы получите простой план, как проверить, что вам подходит, и какой стартовый путь выбрать.

5 октября 2026 г.
580
Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть
путь

Путь от README до демо: как собрать портфолио разработчика, чтобы его хотели смотреть

Разберём структуру репозитория, как упаковать проект в понятную историю (проблема → решение → запуск → метрики качества) и какие артефакты повышают доверие: скринкасты, архитектурная схема, чек-лист продакшн-готовности.

17 сентября 2026 г.
1070
Как понять, какой язык программирования учить первым: простой тест и выбор
понять

Как понять, какой язык программирования учить первым: простой тест и выбор

Помогу новичку выбрать первый язык без стресса: объясню варианты по целям (веб, боты, анализ данных), что учить в первую очередь и как не потратить месяцы впустую. В конце — краткий план старта на ближайшие недели.

2 октября 2026 г.
620

Комментарии

Станьте первым в обсуждении

Поделитесь мнением о статье или задайте вопрос автору

Содержание

Что такое пайплайн обработки видео и зачем он нуженЗахват видеопотокаОсновной цикл чтения кадровПредобработка кадраФоновое вычитаниеПодводные камни фонового вычитанияМорфологические операции: очищаем маскуПоиск контуров и фильтрация объектовПростой трекинг объектовСобираем всё вместеТипичные проблемы и как их решатьСлишком много ложных срабатыванийОбъекты «мигают» — то обнаруживаются, то нетНесколько объектов сливаются в одинВысокая нагрузка на CPUКуда двигаться дальшеИтог