OpenCV на С# (OpenCVSharp)

Вероятно после прочтения заголовка у многих возникнет вопрос как OpenCV связан с C#, почему не python? На самом деле ответ простой, потому что нет такого опыта программирования на python, а потребность в OpenCV возникла. OpenCV — прекрасная библиотека для обработки изображений. А OpenCVSharp — это оболочка .NET для библиотеки компьютерного зрения OpenCV, которая позволяет нам с комфортом использовать её из приложения .NET на Windows, Linux или других OS.
Для начала вспомним о том, что изображение в коде выглядит как набор двухмерных матриц яркостей палитры RGB или её разновидностей. Библиотека OpenCV предоставляет прекрасные возможности для обработки изображений, а также покадровой обработки видео.
Подключение библиотек
Для работы нам понадобится подключить NuGet библиотеки, OpenCvSharp4.runtime.win
для windows.
Разумеется для других операционных систем набор библиотек будет другим (имеется ввиду OpenCvSharp4.runtime.ubuntu), собственно как и запуск в докере.
Загрузка изображения
Для начала необходимо загрузить изображение в память:
// создание по пути к файлу
var imageRGB = new Mat(@"D:\blanks\test.png");
// создание по FileStream
var imageRGB = Mat.FromStream(imageStream, ImreadModes.Color);
где Mat — это класс n-мерного плотного массива OpenCVSharp. При создании можно указать какой тип изображения загружаем в память. Под типом подразумевается:
цветное, то есть трехканальное BGR (аналог RGB с другой компоновкой субпикселей)
изображение в оттенках серого, то есть одноканальное изображение, где всего одна матрица яркостей для всех каналов
подробнее можно прочитать здесь.
Оттенки серого
Часто после загрузки изображения в память нужно привести его в оттенки серого, то есть сделать из 3 матриц одну, примерно «усредняя» значения в зависимости от настроек. В OpenCVSharp это делается примерно так:
// перевод в оттенки серого поумолчанию
Cv2.CvtColor(imageRGB, imageGray, ColorConversionCodes.BGR2GRAY);
// а если необходимо самому определить пропорции цветов для оттенков серого
var channgels = Cv2.Split(imageRGB);
/*
Конвертируем в серое с изменеными коэффициентами каналов,
чтобы вытянуть синий (шариковая ручка).
*/
var grayTransform = channgels[0] * 0.2f + channgels[1] * 0.2f + channgels[2] * 0.6f;
var imageGray = grayTransform.ToMat();
здесь imageGray это изображение в оттенках серого, в первом случае перевод в оттенки идет по формуле OpenCVSharp (0.299*R+0.587*G+0.114*B), а во втором мы сами определяем коэффициенты цветов, что может быть полезно, если вы знаете какие каналы более активны. После перевода в оттенки серого тип изображения меняется, остается только один канал, содержащий яркости, при этом если такое изображение просмотреть оно будет серым.
Фильтры и сглаживание (размытие)
Когда изображение в оттенках серого, часто его нужно обработать фильтрами, чтобы размыть или наоборот придать больший контраст между яркими частями и фоном. Тема фильтров достаточна обширна и выходит за рамки статьи. Про то, как устроены фильтры можно прочитать здесь. Применять фильтры в OpenCVSharp достаточно просто:
// фильтр усреднения, размера 7 на 7
Cv2.Blur(img, dst, new Size(7, 7));
// фильтр гаусса, размера 7 на 7
Cv2.GaussianBlur(img, dst, new Size(7, 7), 0, 0, BorderTypes.Default);
// медианный фильтр
Cv2.MedianBlur(img, dst, 7);
// двусторонний фильтр
Cv2.BilateralFilter(img, dst, 2, 2.2, 2.5)
также стоит упомянуть, что похожим функционалом обладают морфологические преобразования. Помимо указанных методов есть и другие функции применения фильтров filter2D, erode, dilate и другие. Каждый фильтр выполняет свои функции и в сочетании с другими может дать необходимый результат.
Бинаризация
Когда изображение находится в оттенках серого часто требуется найти некоторые области, которые выделяются по яркости. Проще говоря нужно отделить объект или объекты и отбросить фон. Для этих целей может потребоваться:
/*
здесь прямая бинаризация, где черным закрашивается, всё что ниже 127,
и белым (256 четвертый аргумент) всё что выше
*/
Cv2.Threshold(imageGray, imageBin, 127, 255, ThresholdTypes.Binary);
// бинаризация наоборот, то есть всё что ниже 128 белый, выше черный
Cv2.Threshold(imageGray, imageBin, 127, 255, ThresholdTypes.BinaryInv);
/*
адаптивное пороговое значение, для изображений имеющих разные
условия освещения в разных областях, adaptiveMethod может быть MeanC
или GaussianC про различия можно прочитать
https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html
*/
Cv2.AdaptiveThreshold(imageGray, imageBin, 255,
AdaptiveThresholdTypes.GaussianC, ThresholdTypes.Binary, 11, 2)
такая бинаризация называется пороговой, она проводится по жесткому порогу или порогам. Есть и другие виды бинаризации, такие как Оцу и прочие.
Выделение контуров
После бинаризации мы разделили изображение на белые или черные пятна. Теперь необходимо найти и выделить эти пятна. Такой функционал определяется методом findContours.
Cv2.FindContours(imageBin, out contours, out hierarchy,
RetrievalModes.List, ContourApproximationModes.ApproxSimple);
здесь под контуром понимается кривая, соединяющая все непрерывные точки (вдоль границы), имеющие одинаковый цвет или интенсивность.
Пожалуй здесь стоит заметить, что контуры выделяются черным и белым цветом, это значит, черные и белые пятна после бинаризации будут в списке контуров. Метод может возвращать контура как в виде списка, так и в виде дерева, к тому же тип аппроксимации можно задавать (ссылка). Замечу, выделяя контура, можно отметить, что контура бывают вложенные в другие контура и с этим стоит считаться.
Для выделения определенных контуров часто используют оператор Кэнни, сочетание фильтров или морфологических преобразований.
Фильтрация контуров
Получив список контуров, необходимо найти те контуры, которые нам необходимы. Здесь стоит отталкиваться от геометрических характеристик контуров и важно понимать, что контура, как правило, представляют из себя геометрические фигуры вроде прямоугольника, окружности, квадрата или тд.
// обрамление контура, прямоугольник обрамляющий контур
var rect = Cv2.BoundingRect(contour);
// периметр контура, замкнутой области (второй параметр указывает замкнута ли область)
var p = Cv2.ArcLength(c, true);
// площадь контура (площадь пятна)
double area = Cv2.ContourArea(contour);
// Минимально возможное обрамление контура (поворотное обрамление), обрамление и угол
var rect = Cv2.MinAreaRect(contour);
// минимально охватывающий круг
Cv2.MinEnclosingCircle(c, out Point2f center, out float radius);
за счет этих характеристик можно фильтровать контура и находить те, которые нам нужны, чтобы обрабатывать их дальше. В частности, можно проверять насколько заполнен контур, путем вычисления коэффициента отношения заполненности площади к отношению площади ограничивающего прямоугольника.
var occupancyRate = area / (rect.Width * rect.Height)
здесь рассчитывается коэффициент заполненности области, то есть насколько пятно заполнено, нет ли выколотых областей и прочее. Также для подобных целей часто используют моменты контуров.
Заключение
Целью этой статьи является обозрение некоторого спектра возможностей как библиотеки OpenCV, так и её оболочки OpenCVSharp. Надеюсь, мне удалось заинтересовать Вас ее использовать. Стоит отметить, что библиотека достаточно непростая и если вы решили её изучать Вас ждет долгий и сложный путь, однако, если пройти его до конца обработка изображений станет несложным, а самое главное понятным процессом. Здесь можно как рисовать на изображении элементы, линии и прочее, так и находить нужные объекты, отслеживать их на видеокадрах изменять на под себя в промышленных масштабах. В настоящей статье представлен обзор минимальных возможностей этой библиотеки для знакомства с ней.