Компьютерное зрение на Go: Быстрый старт с OpenCV и GoCV

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

Настройка GoCV и базовые операции с изображениями

Настройка GoCV и базовые операции с изображениями

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

В индустрии де-факто стандартом для таких задач является библиотека OpenCV, написанная на C++. Она невероятно быстрая, но писать на C++ скрипты для интеграции с веб-сервисами или базами данных бывает избыточно сложно. Здесь на помощь приходит Go с его простотой и встроенной многопоточностью.

Связующим звеном выступает GoCV — популярная обертка, которая позволяет вызывать мощные алгоритмы OpenCV прямо из кода на Go.

Анатомия цифрового изображения

Прежде чем писать код, нужно понять, с чем именно мы работаем.

Любое изображение состоит из пикселей. Если мы максимально приблизим черно-белую картинку, то увидим сетку, где каждый квадрат имеет свою яркость: от абсолютно черного (значение 00) до абсолютно белого (значение 255255).

В OpenCV эта сетка чисел хранится в специальной структуре данных, которая называется Mat (от слова Matrix — матрица).

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

Важно сразу запомнить систему координат, с которой работает OpenCV. Она отличается от классической декартовой системы из школьной математики:

  • Точка отсчета (0,0)(0, 0) находится в левом верхнем углу изображения.
  • Ось XX (колонки матрицы) идет слева направо.
  • Ось YY (строки матрицы) идет сверху вниз.

Если размер нашего изображения 800×600800 \times 600 (ширина 800, высота 600), то правый нижний пиксель будет иметь координаты X=799X = 799, Y=599Y = 599.

Установка GoCV

Поскольку GoCV — это мост к C++ библиотеке, обычного go get недостаточно. На вашем компьютере должен быть установлен сам OpenCV и компилятор C++.

Процесс установки зависит от операционной системы, но разработчики GoCV подготовили автоматические скрипты.

  1. Сначала скачиваем сам пакет GoCV:
    go get -u gocv.io/x/gocv
    
  2. Переходим в директорию скачанного пакета (обычно в $GOPATH/pkg/mod/gocv.io/x/gocv@...).
  3. Запускаем сборку OpenCV:
    • Для Linux / macOS: выполните команду make install. Скрипт сам скачает исходники OpenCV и скомпилирует их.
    • Для Windows: выполните win_build_opencv.cmd. Потребуется установленный компилятор MinGW-w64 и CMake.

Примечание: Компиляция OpenCV может занять от 10 до 30 минут в зависимости от мощности процессора.

Чтение и показ изображения

Напишем первую программу. Наша задача — загрузить картинку с диска, показать её в окне и дождаться, пока пользователь нажмет любую клавишу для закрытия.

package main

import (
	"fmt"
	"gocv.io/x/gocv"
)

func main() {
	// 1. Читаем изображение из файла
	img := gocv.IMRead("part_123.jpg", gocv.IMReadColor)

	// Проверяем, удалось ли прочитать файл
	if img.IsEmpty() {
		fmt.Println("Ошибка: файл не найден или поврежден")
		return
	}
	// Обязательно освобождаем память C++ при выходе из функции
	defer img.Close()

	// 2. Создаем окно для показа
	window := gocv.NewWindow("Контроль детали")
	defer window.Close()

	// 3. Показываем матрицу (изображение) в окне
	window.IMShow(img)

	// 4. Ждем нажатия любой клавиши (0 означает бесконечное ожидание)
	window.WaitKey(0)
}

Разберем ключевые моменты:

  • gocv.IMRead загружает файл в объект Mat. Флаг gocv.IMReadColor заставляет OpenCV прочитать картинку в цвете, даже если исходник был черно-белым (об устройстве цвета мы поговорим в следующей главе).
  • Метод .IsEmpty() — стандартный способ проверить корректность загрузки. Если вы укажете неверный путь, программа не упадет с паникой, а просто вернет пустую матрицу.
  • Управление памятью: объект Mat хранится в памяти, выделенной C++. Сборщик мусора Go не умеет автоматически очищать эту память. Поэтому для каждого созданного Mat мы обязаны вызывать img.Close(), иначе в долгоживущем приложении (например, при обработке видеопотока) произойдет утечка памяти.

Обрезка: выделение зоны интереса (ROI)

В задачах автоматизации редко нужно анализировать весь кадр целиком. Если камера фиксирует проезд автомобиля, нас интересует только зона с номером. Если мы проверяем маркировку на микросхеме — нам нужна только поверхность чипа.

Выделение прямоугольного фрагмента матрицы называется получением ROI (Region of Interest — зона интереса).

В GoCV для этого используется стандартная структура image.Rectangle из встроенного пакета Go image. Мы указываем координаты левого верхнего и правого нижнего углов нужной нам зоны.

package main

import (
	"image"
	"gocv.io/x/gocv"
)

func main() {
	img := gocv.IMRead("part_123.jpg", gocv.IMReadColor)
	if img.IsEmpty() {
		return
	}
	defer img.Close()

	// Определяем прямоугольник: от X=100, Y=50 до X=400, Y=300
	cropArea := image.Rect(100, 50, 400, 300)

	// Вырезаем зону интереса
	croppedImg := img.Region(cropArea)
	// croppedImg указывает на ту же память пикселей, что и img!
	// Однако метод создает новый заголовок матрицы в C++, поэтому его тоже нужно закрывать.
	defer croppedImg.Close()

	// Сохраняем результат в новый файл
	gocv.IMWrite("defect_zone.jpg", croppedImg)
}

Важное свойство метода .Region(): он не копирует пиксели в новую область памяти. Он создает новый «взгляд» (заголовок матрицы) на ту же самую область памяти исходного изображения. Это делает операцию обрезки мгновенной, что критически важно при обработке видео 60 кадров в секунду. Однако, если вы измените пиксели внутри croppedImg, они изменятся и в оригинальном img.

Мы научились загружать изображение в память, представлять его как матрицу и вырезать нужные фрагменты для дальнейшего анализа. Но пока мы работали с картинкой «как есть». В реальных условиях освещение всегда несовершенно, а детали сливаются с фоном. Чтобы алгоритмы могли находить объекты, исходную матрицу нужно математически преобразовать: убрать лишние цвета, повысить контраст и отфильтровать шум. Этим мы займемся на следующем этапе.

Преобразование цветов, фильтрация и размытие

Преобразование цветов, фильтрация и размытие

Мы уже умеем загружать изображение в матрицу Mat и вырезать из него нужную прямоугольную зону (ROI). Но если мы сразу попытаемся написать алгоритм, который ищет дефекты на детали или считывает показания приборов, мы столкнемся с суровой реальностью: реальные фотографии «грязные».

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

Цветовые пространства: почему BGR недостаточно

По умолчанию OpenCV считывает цветные изображения в формате BGR (Blue, Green, Red). Это историческая особенность библиотеки — каналы идут не в привычном порядке RGB, а в обратном.

Каждый пиксель описывается тремя числами от 00 до 255255. Если наша рабочая задача — найти на конвейере красную деталь, логично было бы написать правило: «ищи пиксели, где красный канал больше 200200, а остальные меньше 5050».

На практике это не работает. Как только на деталь упадет тень, значение красного канала может упасть с 220220 до 9090. При ярком блике значения всех трех каналов BB, GG и RR взлетят под 255255, и красный цвет станет белым. В модели BGR цвет и освещенность намертво склеены.

Решение 1: Оттенки серого (Grayscale)

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

Перевод в градации серого превращает трехканальную матрицу в одноканальную. Это не только убирает проблемы с искажением оттенков, но и в три раза сокращает объем данных, ускоряя дальнейшую обработку.

В GoCV это делается одной функцией:

grayMat := gocv.NewMat()
defer grayMat.Close()

// Конвертация из BGR в градации серого
gocv.CvtColor(img, &grayMat, gocv.ColorBGRToGray)

Решение 2: Пространство HSV

Если цвет критически важен (например, сортировка помидоров по степени зрелости), нам нужно отделить сам оттенок от того, насколько ярко он освещен. Для этого используется цветовое пространство HSV:

  • Hue (Оттенок) — сам цвет (красный, зеленый, синий).
  • Saturation (Насыщенность) — насколько цвет сочный (от блекло-серого до чистого цвета).
  • Value (Яркость) — насколько цвет светлый или темный.

В модели HSV, если красная деталь уходит в тень, меняется только параметр Value. Параметр Hue (оттенок) остается неизменным. Это позволяет алгоритму уверенно находить нужный цвет при любом освещении.

Перевод в HSV в GoCV выглядит аналогично:

hsvMat := gocv.NewMat()
defer hsvMat.Close()

gocv.CvtColor(img, &hsvMat, gocv.ColorBGRToHSV)

Фильтрация шума: Размытие по Гауссу

Даже если мы перевели изображение в нужный цветовой формат, на нем остается цифровой шум. Для человеческого глаза он почти незаметен, но компьютер видит изображение как таблицу чисел. Из-за шума соседние пиксели однотонной поверхности могут иметь значения 140140, 145145, 138138, 147147.

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

Чтобы сгладить эти перепады, применяется размытие. Золотой стандарт в компьютерном зрении — размытие по Гауссу (Gaussian Blur).

Как работает ядро свертки

Размытие работает с помощью математической операции «свертка». Алгоритм берет квадратную матрицу небольшого размера, например 5×55 \times 5 пикселей, которая называется ядром (kernel).

Ядро скользит по всему изображению. Для каждого пикселя оно берет значения его соседей, умножает их на определенные веса и вычисляет среднее значение, которое и записывается в центр. В фильтре Гаусса веса распределены так, что центральный пиксель имеет наибольшее влияние, а чем дальше сосед — тем меньше его вес. Это позволяет размыть шум, но при этом сохранить реальные границы объектов (в отличие от простого усреднения, которое превращает картинку в мыло).

В GoCV размытие применяется так:

blurredMat := gocv.NewMat()
defer blurredMat.Close()

// Размытие по Гауссу с ядром 5x5
gocv.GaussianBlur(grayMat, &blurredMat, image.Pt(5, 5), 0, 0, gocv.BorderDefault)

Размер ядра (в примере image.Pt(5, 5)) определяет силу размытия. Важное математическое правило: размер ядра всегда должен быть нечетным числом (3×33 \times 3, 5×55 \times 5, 11×1111 \times 11). Это необходимо, чтобы у матрицы ядра был четко выраженный центральный пиксель, вокруг которого строится вычисление. Чем больше ядро, тем сильнее размытие. Для мелкого шума камеры обычно хватает 3×33 \times 3 или 5×55 \times 5.

Собираем конвейер предобработки

Теперь мы можем объединить полученные знания в единый пайплайн подготовки изображения. Это стандартный шаблон, с которого начинается большинство скриптов компьютерного зрения на Go:

package main

import (
	"image"
	"gocv.io/x/gocv"
)

func main() {
	// 1. Читаем исходное изображение
	img := gocv.IMRead("part_on_conveyor.jpg", gocv.IMReadColor)
	if img.Empty() {
		return
	}
	defer img.Close()

	// 2. Выделяем зону интереса (ROI), если нужно
	roiRect := image.Rect(100, 100, 500, 400)
	roiMat := img.Region(roiRect)
	defer roiMat.Close()

	// 3. Переводим в градации серого (избавляемся от цвета)
	grayMat := gocv.NewMat()
	defer grayMat.Close()
	gocv.CvtColor(roiMat, &grayMat, gocv.ColorBGRToGray)

	// 4. Применяем размытие по Гауссу (убираем шум)
	blurredMat := gocv.NewMat()
	defer blurredMat.Close()
	gocv.GaussianBlur(grayMat, &blurredMat, image.Pt(5, 5), 0, 0, gocv.BorderDefault)

	// Теперь blurredMat готова к поиску объектов!
}

Обратите внимание на управление памятью: каждая промежуточная матрица (grayMat, blurredMat) создается через gocv.NewMat() и обязательно сопровождается вызовом defer ...Close(). В GoCV мы работаем с C++ объектами под капотом, и сборщик мусора Go не сможет очистить их самостоятельно.

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

Бинаризация и поиск контуров объектов

Бинаризация и поиск контуров объектов

В прошлой главе мы остановились на очищенном от шума изображении в градациях серого. Для нас очевидно, где на нем находится нужная деталь, а где — фон конвейерной ленты. Но для компьютера это всё ещё матрица, заполненная числами от 0 до 255, где значения плавно перетекают друг в друга. Чтобы автоматика могла посчитать объекты или найти их координаты, ей нужен строгий ответ: «да» (это объект) или «нет» (это фон).

Нам предстоит превратить градиентную матрицу в жесткую черно-белую карту, а затем извлечь из нее математические границы объектов.

Бинаризация: разделение мира на черное и белое

Процесс превращения изображения в строго двухцветное называется бинаризацией (или пороговым преобразованием — thresholding).

Логика предельно проста: мы задаем числовой порог TT. Алгоритм проходит по каждому пикселю матрицы и задает вопрос: «Ты ярче этого порога?».

Математически это выглядит так: PT255P \geq T \rightarrow 255 P<T0P < T \rightarrow 0

Где PP — яркость текущего пикселя, TT — заданный нами порог. Если яркость пикселя больше или равна порогу, он становится абсолютно белым (255). Если меньше — абсолютно черным (0). В результате исчезают все полутона, тени и блики.

В GoCV за эту операцию отвечает функция Threshold.

// Применяем порог 127. Все, что ярче 127, станет белым (255)
gocv.Threshold(grayImg, &binaryImg, 127, 255, gocv.ThresholdBinary)

Автоматический порог: метод Оцу

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

Чтобы не подбирать порог вручную, используют метод Оцу (Otsu's method). Этот алгоритм сам анализирует гистограмму изображения (распределение светлых и темных пикселей) и находит идеальный порог TT, который лучше всего разделяет картинку на два выраженных класса: фон и объекты.

Чтобы включить его в GoCV, мы добавляем флаг ThresholdOtsu к базовому флагу бинаризации:

// Значение 0 на месте порога игнорируется, алгоритм вычислит его сам
gocv.Threshold(grayImg, &binaryImg, 0, 255, gocv.ThresholdBinary|gocv.ThresholdOtsu)

От пикселей к векторам: поиск контуров

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

Контур в OpenCV — это не просто линия на картинке. Это массив координат (X,Y)(X, Y), соединенных в замкнутую фигуру (векторный полигон), который очерчивает границу белого пятна.

Функция FindContours сканирует бинарное изображение и возвращает структуру gocv.PointsVector — список всех найденных контуров.

contours := gocv.FindContours(binaryImg, gocv.RetrievalExternal, gocv.ChainApproxSimple)
defer contours.Close()

Здесь мы передаем два важных флага:

  1. Режим поиска (gocv.RetrievalExternal): говорит алгоритму искать только самые внешние границы. Если внутри нашей детали есть отверстие (которое тоже образует границу перепада черного и белого), алгоритм его проигнорирует. Это экономит ресурсы, когда нам нужны только габариты объекта.
  2. Метод аппроксимации (gocv.ChainApproxSimple): сжимает контуры. Если граница объекта — это прямая линия из 100 пикселей, алгоритм не будет сохранять координаты всех 100 точек. Он сохранит только 2 точки (начало и конец отрезка), кардинально экономя память.

Собираем пайплайн воедино

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

package main

import (
	"image/color"
	"gocv.io/x/gocv"
)

func main() {
	// 1. Читаем оригинальное изображение
	img := gocv.IMRead("conveyor_parts.jpg", gocv.IMReadColor)
	defer img.Close()

	// 2. Подготавливаем матрицы для промежуточных шагов
	gray := gocv.NewMat()
	defer gray.Close()
	blurred := gocv.NewMat()
	defer blurred.Close()
	binary := gocv.NewMat()
	defer binary.Close()

	// 3. Предобработка (из прошлой главы)
	gocv.CvtColor(img, &gray, gocv.ColorBGRToGray)
	gocv.GaussianBlur(gray, &blurred, image.Pt(5, 5), 0, 0, gocv.BorderDefault)

	// 4. Бинаризация с автоматическим порогом Оцу
	gocv.Threshold(blurred, &binary, 0, 255, gocv.ThresholdBinary|gocv.ThresholdOtsu)

	// 5. Поиск внешних контуров
	contours := gocv.FindContours(binary, gocv.RetrievalExternal, gocv.ChainApproxSimple)
	defer contours.Close()

	// 6. Отрисовка контуров на ОРИГИНАЛЬНОМ изображении
	// Цвет: зеленый (R:0, G:255, B:0), толщина линии: 2 пикселя
	green := color.RGBA{0, 255, 0, 0}
	for i := 0; i < contours.Size(); i++ {
		gocv.DrawContours(&img, contours, i, green, 2)
	}

	// 7. Показываем результат
	window := gocv.NewWindow("Found Contours")
	defer window.Close()
	window.IMShow(img)
	window.WaitKey(0)
}

В функции DrawContours мы передаем индекс i, чтобы отрисовать конкретный контур из списка. Если вместо индекса передать -1, OpenCV отрисует сразу все найденные контуры за один вызов.

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

Анализ геометрии и фильтрация контуров по площади

Анализ геометрии и фильтрация контуров по площади

Мы успешно перевели изображение в черно-белый формат и нашли контуры объектов. Но если вы запустите этот код на реальной фотографии с веб-камеры или камеры на производстве, вас ждет сюрприз. Вместо двух-трех ожидаемых деталей алгоритм найдет сотни контуров.

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

Площадь контура — главный фильтр от шума

Самый надежный и вычислительно дешевый способ отсеять мусор — измерить площадь каждого найденного контура. Пылинка занимает 5–10 пикселей, а реальная деталь — тысячи.

В GoCV для этого используется функция gocv.ContourArea(). Она принимает вектор контура и возвращает значение типа float64 — количество пикселей, заключенных внутри этого многоугольника.

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

Ограничивающий прямоугольник (Bounding Box)

Когда мы отсеяли мусор, у нас остались только нужные контуры. Но сам по себе контур — это сложный массив из сотен координат XX и YY, описывающих изгибы объекта. Работать с такой фигурой неудобно.

В задачах автоматизации нам чаще всего нужно знать лишь габариты объекта и его положение. Для этого сложный контур упаковывают в ограничивающий прямоугольник (Bounding Box). Это минимальный прямоугольник со сторонами, параллельными осям координат, в который полностью помещается наш объект.

В GoCV за это отвечает функция gocv.BoundingRect(). Она принимает контур и возвращает стандартную структуру image.Rectangle из встроенного пакета Go. Эта структура сразу дает нам координаты левого верхнего угла (Min.X, Min.Y), а также ширину и высоту объекта (Dx(), Dy()).

Поиск центра объекта (Пространственные моменты)

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

В компьютерном зрении для вычисления центра масс используются пространственные моменты (Image Moments). Момент — это взвешенное среднее значение интенсивностей пикселей изображения.

В GoCV моменты вычисляются функцией gocv.Moments(). Она возвращает набор математических характеристик фигуры. Нас интересуют три из них:

  1. M00M_{00} — нулевой момент. По сути, это точная площадь фигуры.
  2. M10M_{10} — сумма всех координат XX пикселей фигуры.
  3. M01M_{01} — сумма всех координат YY пикселей фигуры.

Чтобы найти координаты центра масс (CxC_x и CyC_y), нужно разделить сумму координат на площадь. Формулы выглядят так:

Cx=M10M00C_x = \frac{M_{10}}{M_{00}}

Cy=M01M00C_y = \frac{M_{01}}{M_{00}}

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

Важное уточнение: если контур представляет собой просто точку (площадь равна нулю), то M00M_{00} будет равен 00. Чтобы избежать ошибки деления на ноль в Go, всегда проверяйте, что M000M_{00} \neq 0.

Собираем конвейер фильтрации на Go

Теперь объединим все эти концепции в единый рабочий цикл. Предположим, мы уже получили массив контуров contours из функции FindContours (как мы делали в прошлой главе).

// Перебираем все найденные контуры
for i := 0; i < contours.Size(); i++ {
    contour := contours.At(i)

    // 1. Фильтрация по площади
    area := gocv.ContourArea(contour)
    if area < 500 { // Игнорируем всё, что меньше 500 пикселей
        continue
    }

    // 2. Получаем ограничивающий прямоугольник (Bounding Box)
    rect := gocv.BoundingRect(contour)

    // Отрисовываем прямоугольник синим цветом (толщина линии 2)
    gocv.Rectangle(&img, rect, color.RGBA{0, 0, 255, 0}, 2)

    // 3. Вычисляем моменты для поиска центра
    moments := gocv.Moments(contour)

    // Защита от деления на ноль
    if moments["m00"] != 0 {
        // Вычисляем координаты центра по формулам
        cx := int(moments["m10"] / moments["m00"])
        cy := int(moments["m01"] / moments["m00"])

        center := image.Pt(cx, cy)

        // Рисуем красную точку в центре объекта (радиус 5, сплошная заливка -1)
        gocv.Circle(&img, center, 5, color.RGBA{255, 0, 0, 0}, -1)
    }
}

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

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

Обнаружение объектов с помощью каскадов Хаара

Обнаружение объектов с помощью каскадов Хаара

До сих пор мы находили объекты, опираясь на их цвет и площадь: переводили изображение в HSV, отсекали лишнее бинаризацией и искали контуры. Но что делать, если нужный объект сливается с фоном по цвету? Или его цвет постоянно меняется, как у человеческого лица или автомобиля на парковке?

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

Что такое признаки Хаара

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

Математически это выглядит так:

Δ=PblackPwhite\Delta = \sum P_{black} - \sum P_{white}

Где Δ\Delta — итоговое значение признака, Pblack\sum P_{black} — сумма яркостей всех пикселей под черной частью шаблона, а Pwhite\sum P_{white} — сумма яркостей под белой частью.

Если мы ищем лицо человека, алгоритм будет опираться на анатомические закономерности освещения. Например, область глаз всегда темнее, чем переносица и щеки (из-за глазных впадин). Если наложить горизонтальный черно-белый шаблон на глаза и щеки, разница Δ\Delta будет высокой. Если наложить его на ровную стену — разница будет близка к нулю.

Принцип каскада: почему это работает быстро

Чтобы найти объект, алгоритм использует метод «скользящего окна»: он берет квадратную рамку и проходит ей по каждому пикселю изображения. Для окна размером 24x24 пикселя существует более 160 000 вариантов признаков Хаара. Проверять их все для каждого пикселя на HD-фотографии — это миллиарды вычислений, которые заняли бы часы.

Чтобы алгоритм мог работать в реальном времени, был придуман каскадный классификатор.

Вместо того чтобы проверять все 160 000 признаков, алгоритм разбивает их на этапы (каскады).

  1. На первом этапе проверяются 2–3 самых базовых признака (например, темная линия глаз).
  2. Если окно не проходит эту проверку, алгоритм мгновенно отбрасывает его и сдвигается дальше.
  3. Если проходит — окно передается на второй, более сложный этап.

Большинство фоновых участков изображения (небо, стены, асфальт) отбрасываются уже на первом-втором этапе. Долгие и сложные вычисления применяются только к тем редким участкам, которые действительно похожи на искомый объект.

Каскады Хаара в GoCV

Вам не нужно самостоятельно вычислять признаки или обучать каскады с нуля. OpenCV поставляется с набором готовых, уже обученных XML-файлов для поиска лиц, глаз, улыбок, номеров машин и даже кошек.

Для загрузки такой модели в GoCV используется структура CascadeClassifier.

// Создаем новый классификатор
classifier := gocv.NewCascadeClassifier()
defer classifier.Close()

// Загружаем готовую XML-модель для поиска лиц спереди
classifier.Load("haarcascade_frontalface_default.xml")

Подготовка изображения

Каскады Хаара анализируют перепады яркости, а не оттенки. Цветная информация (каналы BGR) не просто бесполезна для этого алгоритма, но и требует в три раза больше памяти и времени на обработку. Поэтому перед поиском изображение обязательно переводится в градации серого.

img := gocv.IMRead("team.jpg", gocv.IMReadColor)
defer img.Close()

gray := gocv.NewMat()
defer gray.Close()

// Перевод в ЧБ — обязательный шаг для каскадов Хаара
gocv.CvtColor(img, &gray, gocv.ColorBGRToGray)

Поиск объектов: DetectMultiScale

Главный метод для поиска — DetectMultiScale. Название "MultiScale" означает, что алгоритм умеет находить объекты разного размера (ведь лицо может быть как крупным планом, так и далеко на заднем фоне). Для этого он постепенно увеличивает размер скользящего окна и проходит по изображению несколько раз.

Метод возвращает срез []image.Rectangle — массив Bounding Box'ов (ограничивающих прямоугольников), внутри которых найден объект.

// Поиск объектов на черно-белом изображении
rects := classifier.DetectMultiScale(gray)

// Отрисовка результатов на исходном цветном изображении
for _, r := range rects {
    gocv.Rectangle(&img, r, color.RGBA{0, 255, 0, 0}, 2)
}

Тонкая настройка параметров

Метод DetectMultiScale можно вызвать с дополнительными параметрами с помощью DetectMultiScaleWithParams. Это необходимо для настройки баланса между точностью и количеством ложных срабатываний.

Два главных параметра:

  1. scaleFactor (по умолчанию 1.1) — коэффициент увеличения окна на каждом проходе. Значение 1.1 означает, что окно увеличивается на 10%. Если поставить 1.05 (5%), алгоритм будет искать тщательнее, но работать медленнее.
  2. minNeighbors (по умолчанию 3) — минимальное количество успешных срабатываний соседних окон, чтобы подтвердить находку. Так как скользящее окно сдвигается попиксельно, настоящее лицо будет обнаружено несколько раз подряд с небольшим смещением. Если алгоритм нашел лицо только в одном положении рамки (соседей нет) — скорее всего, это ложное срабатывание на текстуру фона.
// Настройка: окно растет на 10%, нужно минимум 5 подтверждений
rects := classifier.DetectMultiScaleWithParams(gray, 1.1, 5, 0, image.Pt(30, 30), image.Pt(500, 500))

Последние два параметра (image.Pt) — это минимальный и максимальный размер искомого объекта. Отсечение слишком мелких или слишком крупных рамок сильно ускоряет работу.

Каскады Хаара — это мощный и быстрый инструмент, который отлично подходит для автоматизации на слабом железе (например, на Raspberry Pi). Научившись находить сложные объекты в статичных кадрах, мы готовы перейти к финальному этапу курса: захвату видеопотока с веб-камеры и реакции на события в реальном времени.

Обработка видеопотока в реальном времени и триггеры действий

Обработка видеопотока в реальном времени и триггеры действий

Мы научились находить объекты, анализировать их геометрию и распознавать сложные паттерны с помощью каскадов Хаара на статичных фотографиях. Но в реальных задачах автоматизации конвейер не останавливается ради кадра, а люди не замирают перед камерой.

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

От статики к потоку: цикл захвата

Вместо чтения файла с диска через метод IMRead, для работы с видео в GoCV используется структура VideoCapture. Она умеет подключаться к веб-камерам, IP-камерам (по RTSP-ссылкам) или читать видеофайлы.

Чтобы видео "ожило", нам нужен бесконечный цикл for, который будет непрерывно запрашивать у камеры новый кадр.

Посмотрим на базовый каркас захвата видеопотока:

package main

import (
	"fmt"
	"gocv.io/x/gocv"
)

func main() {
	// Открываем веб-камеру по умолчанию (индекс 0)
	webcam, err := gocv.VideoCaptureDevice(0)
	if err != nil {
		fmt.Println("Ошибка открытия камеры")
		return
	}
	defer webcam.Close()

	window := gocv.NewWindow("Live Feed")
	defer window.Close()

	// ВАЖНО: Создаем матрицу ОДИН раз до цикла
	img := gocv.NewMat()
	defer img.Close()

	for {
		// Читаем кадр с камеры в существующую матрицу img
		if ok := webcam.Read(&img); !ok || img.Empty() {
			fmt.Println("Не удалось прочитать кадр")
			continue
		}

		// Здесь будет наша логика обработки (Хаар, контуры и т.д.)

		window.IMShow(img)

		// Пауза 1 миллисекунда для обновления окна и перехвата клавиш
		if window.WaitKey(1) == 27 { // 27 — это код клавиши ESC
			break
		}
	}
}

Обратите внимание на критически важную деталь управления памятью: мы вызываем gocv.NewMat() до начала цикла. Внутри цикла метод Read(&img) просто перезаписывает данные в уже выделенной области памяти. Если бы мы создавали новый объект Mat на каждой итерации, оперативная память закончилась бы за несколько секунд.

Проблема 30 FPS: почему нам нужны умные триггеры

Допустим, мы поместили внутрь нашего цикла каскад Хаара из прошлой главы. Камера работает со скоростью 30 кадров в секунду (FPS=30FPS = 30). Человек подходит к камере и стоит перед ней 2 секунды.

Если наша логика звучит как «если N>0N > 0, отправить сигнал тревоги» (где NN — количество найденных лиц), то за 2 секунды система сгенерирует 60 одинаковых сигналов. Это переполнит логи, положит базу данных или заспамит оператора в мессенджере.

Нам нужен механизм Debounce (троттлинг) — искусственное ограничение частоты срабатывания триггера.

Реализация состояния и задержки

Чтобы триггер срабатывал умно, программа должна обладать «памятью» о прошлом. Нам достаточно хранить метку времени последнего успешного срабатывания.

Введем условие: мы реагируем на объект только в том случае, если с момента прошлой реакции прошло время Δt5\Delta t \geq 5 секунд.

// Добавляем импорт пакета time
import "time"

// ... инициализация камеры ...

// Переменная для хранения времени последнего срабатывания
lastTriggerTime := time.Time{}
cooldown := 5 * time.Second

for {
    webcam.Read(&img)

    // Допустим, функция detectFaces возвращает срез найденных прямоугольников
    rects := detectFaces(img)

    if len(rects) > 0 {
        // Проверяем, прошел ли кулдаун
        if time.Since(lastTriggerTime) >= cooldown {

            // ТРИГГЕР ДЕЙСТВИЯ:
            fmt.Println("АЛАРМ! Обнаружен объект. Сохраняю кадр...")
            gocv.IMWrite("alert_frame.jpg", img)

            // Обновляем время последнего срабатывания
            lastTriggerTime = time.Now()
        }
    }

    // ... отрисовка и WaitKey ...
}

Теперь система работает стабильно: увидев человека, она мгновенно реагирует, сохраняет кадр-доказательство на диск, а затем «засыпает» для новых триггеров на 5 секунд. При этом сам цикл видео не останавливается — отрисовка интерфейса и анализ продолжаются в реальном времени, блокируется только отправка повторяющихся действий.

Полный пайплайн автоматизации

Теперь мы можем собрать все знания курса в единый рабочий инструмент. Следующий код — это готовое решение для автоматизированного поста наблюдения. Он захватывает видео, переводит его в градации серого, ищет объекты (например, лица), отрисовывает вокруг них Bounding Box и раз в 5 секунд сохраняет кадр при обнаружении активности.

package main

import (
	"fmt"
	"image/color"
	"time"

	"gocv.io/x/gocv"
)

func main() {
	webcam, _ := gocv.VideoCaptureDevice(0)
	defer webcam.Close()

	window := gocv.NewWindow("Security Feed")
	defer window.Close()

	img := gocv.NewMat()
	defer img.Close()

	gray := gocv.NewMat()
	defer gray.Close()

	// Загружаем классификатор (убедитесь, что XML файл лежит рядом)
	classifier := gocv.NewCascadeClassifier()
	defer classifier.Close()
	classifier.Load("haarcascade_frontalface_default.xml")

	blue := color.RGBA{0, 0, 255, 0}
	lastTriggerTime := time.Time{}
	cooldown := 5 * time.Second

	for {
		if ok := webcam.Read(&img); !ok || img.Empty() {
			continue
		}

		// 1. Предобработка: перевод в Grayscale для каскада Хаара
		gocv.CvtColor(img, &gray, gocv.ColorBGRToGray)

		// 2. Поиск объектов
		rects := classifier.DetectMultiScale(gray)

		// 3. Отрисовка и Триггеры
		for _, r := range rects {
			// Рисуем рамку вокруг объекта
			gocv.Rectangle(&img, r, blue, 3)
		}

		if len(rects) > 0 {
			if time.Since(lastTriggerTime) >= cooldown {
				filename := fmt.Sprintf("alert_%d.jpg", time.Now().Unix())
				gocv.IMWrite(filename, img)
				fmt.Printf("Объект зафиксирован! Сохранен %s\n", filename)

				lastTriggerTime = time.Now()
			}
		}

		window.IMShow(img)
		if window.WaitKey(1) == 27 {
			break
		}
	}
}

Итоги курса

За время этого короткого курса вы прошли путь от понимания того, как пиксели хранятся в памяти, до создания полноценной системы компьютерного зрения реального времени на Go.

Вы научились:

  1. Управлять памятью C++ матриц в среде Go.
  2. Изолировать объекты от фона с помощью цветовых пространств и бинаризации.
  3. Анализировать геометрию через контуры и моменты.
  4. Находить сложные объекты с помощью машинного обучения (каскады Хаара).
  5. Связывать анализ видеопотока с реальными бизнес-действиями через триггеры.

Этого фундамента достаточно, чтобы начать решать большинство классических задач автоматизации на производстве, в системах безопасности или в пет-проектах.