Время прочтения: 5 мин.

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

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

Алгоритм выявления атмосферных осадков

Для начала проведем подготовительную работу: импортируем библиотеки opencv и numpy, инициализируем захват кадров из видеофайла с именем, записанным в переменной fname, и создадим экземпляр класса backSub для исключения фона.

import cv2 as cv
import numpy as np
cap = cv2.VideoCapture(fname)
backSub = cv2.createBackgroundSubtractorMOG2(25, 16, False)

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

while True:
    ret, im_src = cap.read()
    if not ret:
        break

Считанный кадр im_src представляет собой RGB-изображение разрешением 1920×1080 пикселей.

Далее выполним несколько операций обработки исходного видеокадра.

Первая операция — вычитание фона.

fg_mask = backSub.apply(frame)

Результатом этой операции является grayscale-изображение fg_mask, содержащее маску движущихся объектов.

Теперь выполним фильтрацию на основе морфологической операции открытия с ядром 3×3.

kernel = np.ones((3,3),np.uint8)
filtered = cv.morphologyEx(fg_mask, cv.MORPH_OPEN, kernel)

В результате с изображения исчезнут отдельные мелкие артефакты.

Далее с помощью функции findContours выделим контуры.

cnt_all, hierarchy = cv.findContours(filtered,\
                                                         cv.RETR_EXTERNAL,\
                                                         cv.CHAIN_APPROX_SIMPLE)

Для иллюстрации выполним обводку найденных контуров.

Исключим самые маленькие по площади контуры. В качестве минимальной площади контура выберем значение 16.

min_area = 16
cnt_sel_by_area = []
for cnt in cnt_all:
    if cv.contourArea(cnt, False) >= min_area:
        cnt_sel_by_area.append(cnt)

Результат:

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

shape_coef_min = 4
cnt_sel_by_shape = []
for cnt in cnt_sel_by_area:
    peri = cv.arcLength(cnt, True)
    area = cv.contourArea(cnt)
    shape_coef = (peri ** 2) / area / (4 * np.pi)
    if shape_coef >= shape_coef_min:
        cnt_sel_by_shape.append(cnt)

Визуально оценим результат этого действия:

Переходим к отбору контуров по направлению. Для того, чтобы определить ориентацию контуров относительно вертикали, используем аппроксимацию контуров прямыми линиями с помощью функции fitLine. Среди возвращаемых этой функцией значений есть координаты направляющего вектора прямой vx и vy. Оценить угол между прямой и вертикалью можно через скалярное произведение направляющего вектора прямой и направляющего вектора оси ординат. Значение этого угла может лежать в диапазоне от 0 до 180 градусов. Для удобства приведем это значение в диапазон 0..90 угловых градусов. Установим в качестве порога максимального отклонения значение 30 градусов и выберем контуры, удовлетворяющие этому условию.

max_angle = 30
cnt_sel = []
for cnt in cnt_sel_by_shape:
    vx, vy, x, y = cv.fitLine(cnt, cv.DIST_L2, 0, 0.01, 0.01).flatten()
    vect_line = np.array([vx, vy])
    vect_y_axis = np.array([0, 1])
    dot_product = np.dot(vect_line, vect_y_axis)
    angle = np.rad2deg(np.arccos(dot_product))
    angle = min(angle, 180 - angle)
    if angle <= max_angle:
        cnt_sel.append(cnt)

Результат отбора контуров проиллюстрирован на следующем изображении. Здесь зеленым цветом отмечены выбранные контуры, а красным — исключенные.

Выполним последнее действие: рассчитаем количество выбранных контуров

n_cnt_curr = len(cnt_sel)

Для иллюстрации нанесём выбранные контуры на исходный видеокадр.

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

decision_thr = 3

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

alpha = 0.1
n_cnt_avg = n_cnt_curr * alpha + n_cnt_avg * (1 - alpha)
n_cnt_avg_int = int(round(n_cnt_avg))

Далее сравним усредненное значение с пороговым и сделаем вывод о наличии осадков.

is_precipitation = n_cnt_avg_int >= decision_thr
concl_dict_ru = {True: 'есть осадки', False: 'нет осадков'}
conclusion = concl_dict_ru[is_precipitation]

Проиллюстрируем промежуточные расчеты, динамику количества контуров и гипотезу о наличии осадков.

Видео с демонстрацией работы описанного алгоритма можно здесь или здесь. Кадр из демонстрации приведен ниже.

Код размещен в репозитории автора.

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