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

Я надеюсь, что вы найдете это полезным!

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

Пойдем!

Самый простой способ визуализировать и понять, что делают побитовые операции и как они работают, — посмотреть на преобразования, которые они могут выполнять с бинарными изображениями.
В OpenCV существует четыре побитовых операции: и, или, исключительное или — часто отмечается исключающее или — и нет. And, or и xor требуют два бита и выводят один бит в соответствии с их таблицей истинности. Not принимает один единственный бит и возвращает противоположное: 0 становится 1 и наоборот.
Двоичные изображения представляют собой матрицы, состоящие только из 0 (обычно черные пиксели) и 255 (обычно белые пиксели), бинарные операции работают с ними очень просто: все, что они делают, — это применяют побитовые операции к каждому пикселю.

Я проиллюстрирую четыре побитовые операции двумя изображениями ниже:

Они были созданы с использованием следующего кода:

import numpy as np
import cv2 as cv
# we create a black image of 400*400 pixels and draw a 100*100 white square in the middle.
black_one = np.zeros((400, 400), dtype=np.uint8)
black_one[150:250, 150:250] = 255
# same process, except that the white square has been gently pushed towards the bottom-right corner.
black_two = np.zeros((400, 400), dtype=np.uint8)
black_two[200:300, 200:300] = 255

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

one_and_two = cv.bitwise_and(black_one, black_two)

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

Точно так же мы можем использовать оператор или, чтобы найти объединение между двумя белыми квадратами: когда пиксель белый хотя бы в одном из двух изображений — когда хотя бы один из двух операндов «истинен» ” — , на результирующем изображении он остается белым:

Точно так же мы использовали функцию bitwise_or для выполнения этой операции.

Или является инклюзивным, что означает, что пиксели, которые являются белыми на обоих изображениях, остаются белыми. Если вы хотите выбрать белые пиксели на одном из изображений, но не на обоих, вам нужно использовать оператор xor:

Функция, выполняющая это преобразование, называется cv.bitwise_xor и работает точно так же, как и две предыдущие.

Наконец, функция cv.bitwise_not принимает одно изображение и меняет местами черные и белые пиксели. Вот как выглядит black_one после не-ed.

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

green = np.zeros((400, 400, 3), dtype=np.uint8)
green[:, :, 1] = 255

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

Давайте рассмотрим пример с оператором not :

Не выключает зеленый канал, а включает красный и синий каналы.

Чтобы обобщить поведение оператора not, можно сказать, что пиксели заменяются их симметричными относительно 127. Например, 0 становится 255, а 200 становится 55.

Функции OpenCV bitwise_xx можно вызывать с дополнительным параметром, который мы еще не использовали: mask. Маски позволяют выбирать определенные области изображения. Пример стоит тысячи слов, поэтому вот пример, который применяет оператор и к изображению, используя black_one в качестве маски:

Вот код, который дал этот результат:

image: np.ndarray = cv.imread(image_path)
mask = np.zeros(image.shape[:2], dtype=np.uint8)
mask[150:250, 150:250] = 255
masked_image = cv.bitwise_and(image, image, mask=mask)

Есть две вещи, которые нужно знать при использовании масок:

  • они должны быть двухмерными;
  • к нему применяется бинарный порог, а это значит, что все пиксели, у которых коэффициент серого находится в пределах от 0 до 127, округляются до 0, а более темные — до 255.

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

из этих двух изображений:

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

Я позволю вам решить эту проблему самостоятельно, прежде чем читать решение ниже…

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

weather = cv.imread(weather_path)
weather_height, weather_width = weather.shape[:2]
region_of_interest = image[:weather_height, :weather_width]

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

greyscale_sun = cv.cvtColor(weather, cv.COLOR_BGR2GRAY)
_, threshold = cv.threshold(greyscale_sun, 254, 255, cv.THRESH_BINARY)

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

Используя это изображение в качестве маски, теперь мы можем вставить изображение озера, оставив пустое место, где будет размещена иконка:

background = cv.bitwise_and(region_of_interest, region_of_interest, mask=threshold)

Что дает нам следующее изображение:

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

threshold_inverse = cv.bitwise_not(threshold)
icon = cv.bitwise_and(weather, weather, mask=threshold_inverse)

Вот как выглядит значок:

Мы можем использовать функцию OpenCV add, чтобы соединить эти два изображения вместе, что даст следующее изображение:

Последний шаг — заменить левый верхний угол исходного изображения отредактированной областью интереса:

image[:weather_height, :weather_width] = cv.add(background, icon)

и Боб — твой дядя, у тебя получится этот прекрасный маленький коллаж!