Удалите паразитные маленькие островки шума на изображении - Python OpenCV
Я пытаюсь избавиться от фонового шума от некоторых из моих изображений. Это нефильтрованное изображение.
Для фильтрации я использовал этот код для генерации маски того, что должно остаться на изображении:
element = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
mask = cv2.erode(mask, element, iterations = 1)
mask = cv2.dilate(mask, element, iterations = 1)
mask = cv2.erode(mask, element)
С помощью этого кода и когда я замаскирую нежелательные пиксели из исходного изображения, я получу следующее:
Как вы можете видеть, все крошечные точки в средней области исчезли, но многие из более плотных областей также исчезли. Чтобы уменьшить фильтрацию, я попытался изменить второй параметр getStructuringElement()
быть (1,1), но при этом я получаю первое изображение, как будто ничего не было отфильтровано.
Можно ли как-нибудь применить фильтр между этими двумя крайностями?
Кроме того, кто-нибудь может объяснить мне, что именно делает getStructuringElement()
делать? Что такое "структурирующий элемент"? Что он делает и как его размер (второй параметр) влияет на уровень фильтрации?
1 ответ
Многие ваши вопросы проистекают из того факта, что вы не знаете, как работает морфологическая обработка изображений, но мы можем развеять ваши сомнения. Вы можете интерпретировать структурирующий элемент как "базовую форму" для сравнения. 1 в структурирующем элементе соответствует пикселю, который вы хотите посмотреть в этой форме, а 0 - это тот, который вы хотите игнорировать. Существуют разные формы, например прямоугольные (как вы уже поняли с MORPH_RECT
), эллипс, круговой и т. д.
В качестве таких, cv2.getStructuringElement
возвращает структурирующий элемент для вас. Первый параметр указывает тип, который вы хотите, а второй параметр определяет размер, который вы хотите. В вашем случае вам нужен 2х2 "прямоугольник"... который на самом деле квадрат, но это нормально.
В более убогом смысле вы используете структурирующий элемент и сканируете изображение слева направо и сверху вниз и захватываете пиксельные окрестности. Каждая пиксельная окрестность имеет свой центр точно на интересующем пикселе, на который вы смотрите. Размер каждой пиксельной окрестности совпадает с размером структурирующего элемента.
размывание
Для размытия вы исследуете все пиксели в окрестности пикселя, которые касаются элемента структурирования. Если каждый ненулевой пиксель касается пикселя структурирующего элемента, который равен 1, то выходной пиксель в соответствующей центральной позиции по отношению к входу равен 1. Если существует хотя бы один ненулевой пиксель, который не касается пикселя структурирования то есть 1, то на выходе будет 0.
Что касается прямоугольного структурирующего элемента, вам нужно убедиться, что каждый пиксель в структурирующем элементе касается ненулевого пикселя в вашем изображении для соседства пикселей. Если это не так, то выходной сигнал равен 0, иначе 1. Это эффективно устраняет небольшие паразитные области шума, а также немного уменьшает площадь объектов.
Размер зависит от того, где больше прямоугольник, тем больше сокращение. Размер элемента структурирования - это базовая линия, где любые объекты, которые меньше этого прямоугольного элемента структурирования, можно рассматривать как фильтруемые и не отображаемые в выходных данных. По сути, выбор прямоугольного структурирующего элемента 1 x 1 совпадает с самим входным изображением, поскольку этот структурирующий элемент умещается во все пиксели внутри него, так как пиксель представляет собой наименьшее возможное представление информации в изображении.
расширение
Дилатация является противоположностью эрозии. Если есть хотя бы один ненулевой пиксель, который касается пикселя в структурирующем элементе, который равен 1, то выходной сигнал равен 1, в противном случае выходной сигнал равен 0. Вы можете думать об этом как о небольшом увеличении областей объекта и увеличении небольших островков.
В данном случае с размером подразумевается, что чем больше структурирующий элемент, тем больше будут площади объектов и тем больше станут изолированные острова.
То, что вы делаете, это сначала эрозия, а затем расширение. Это то, что известно как операция открытия. Цель этой операции состоит в том, чтобы удалить небольшие островки шума, одновременно пытаясь сохранить участки более крупных объектов на изображении. Эрозия удаляет эти острова, в то время как дилатация увеличивает крупные объекты до их первоначальных размеров.
Вы по какой-то причине снова следите за этим, что я не совсем понимаю, но это нормально.
То, что я лично сделал бы, это сначала выполнить операцию закрытия, которая представляет собой дилатацию с последующей эрозией. Закрытие помогает сгруппировать области, которые находятся близко друг к другу, в один объект. Таким образом, вы видите, что есть несколько более крупных областей, которые расположены близко друг к другу, и, вероятно, их следует объединить, прежде чем мы что-нибудь еще сделаем. Поэтому я сначала делаю закрытие, а затем открываю, чтобы мы могли удалить изолированные шумные области. Обратите внимание, что я собираюсь увеличить размер закрывающего элемента структурирования, поскольку я хочу убедиться, что я получаю соседние пиксели, а размер открывающего элемента структурирования меньше, чтобы я не хотел по ошибке удалить любую из более крупных областей.
Как только вы сделаете это, я замаскирую любую дополнительную информацию с исходным изображением, чтобы вы оставили большие области нетронутыми, а маленькие острова исчезли.
Вместо цепочки эрозии с последующей дилатацией или дилатации с последующей эрозией, используйте cv2.morphologyEx
где можно указать MORPH_OPEN
а также MORPH_CLOSE
как флаги.
Таким образом, я бы лично сделал это, предполагая, что ваш образ называется spots.png
:
import cv2
import numpy as np
img = cv2.imread('spots.png')
img_bw = 255*(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) > 5).astype('uint8')
se1 = cv2.getStructuringElement(cv2.MORPH_RECT, (5,5))
se2 = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
mask = cv2.morphologyEx(img_bw, cv2.MORPH_CLOSE, se1)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, se2)
mask = np.dstack([mask, mask, mask]) / 255
out = img * mask
cv2.imshow('Output', out)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite('output.png', out)
Приведенный выше код довольно понятен. Сначала я читаю изображение, а затем преобразовываю изображение в градации серого и пороговое значение с интенсивностью 5, чтобы создать маску того, что считается пикселями объекта. Это довольно чистое изображение, поэтому все, что больше 5, похоже, сработало. Для процедур морфологии мне нужно преобразовать изображение в uint8
и масштабируем маску до 255. Затем мы создаем два структурирующих элемента - один, который является прямоугольником 5 x 5 для операции закрытия, и другой, который является 2 x 2 для операции открытия. я бегу cv2.morphologyEx
дважды для операций открытия и закрытия соответственно для порогового изображения.
После этого я складываю маску так, чтобы она стала трехмерной матрицей, и делю ее на 255, чтобы она стала маской [0,1]
а затем мы умножаем эту маску на исходное изображение, чтобы мы могли получить исходные пиксели изображения и сохранить то, что считается истинным объектом, из выходных данных маски.
Остальное только для иллюстрации. Я показываю изображение в окне, и я также сохраняю изображение в файл с именем output.png
и его цель - показать вам, как выглядит изображение в этом посте.
Я получаю это:
Имейте в виду, что это не идеально, но гораздо лучше, чем раньше. Вам придется поэкспериментировать с размерами структурирующих элементов, чтобы получить что-то, что вы считаете хорошим выходом, но этого, безусловно, достаточно, чтобы начать работу. Удачи!
Версия C++
Были некоторые запросы на перевод кода, который я написал выше, в версию C++ с использованием OpenCV. Наконец-то я приступил к написанию C++-версии кода, и это было протестировано на OpenCV 3.1.0. Код для этого ниже. Как видите, код очень похож на тот, который вы видели в версии Python. Тем не менее, я использовал cv::Mat::setTo
на копии исходного изображения и установите все, что не было частью окончательной маски, равным 0. Это то же самое, что выполнить поэлементное умножение в Python.
#include <opencv2/opencv.hpp>
using namespace cv;
int main(int argc, char *argv[])
{
// Read in the image
Mat img = imread("spots.png", CV_LOAD_IMAGE_COLOR);
// Convert to black and white
Mat img_bw;
cvtColor(img, img_bw, COLOR_BGR2GRAY);
img_bw = img_bw > 5;
// Define the structuring elements
Mat se1 = getStructuringElement(MORPH_RECT, Size(5, 5));
Mat se2 = getStructuringElement(MORPH_RECT, Size(2, 2));
// Perform closing then opening
Mat mask;
morphologyEx(img_bw, mask, MORPH_CLOSE, se1);
morphologyEx(mask, mask, MORPH_OPEN, se2);
// Filter the output
Mat out = img.clone();
out.setTo(Scalar(0), mask == 0);
// Show image and save
namedWindow("Output", WINDOW_NORMAL);
imshow("Output", out);
waitKey(0);
destroyWindow("Output");
imwrite("output.png", out);
}
Результаты должны быть такими же, как и в версии Python.
Также можно удалить небольшие кластеры пикселей, используя remove_small_objects
функция в skimage:
import matplotlib.pyplot as plt
from skimage import morphology
import numpy as np
import skimage
# read the image, grayscale it, binarize it, then remove small pixel clusters
im = plt.imread('spots.png')
grayscale = skimage.color.rgb2gray(im)
binarized = np.where(grayscale>0.1, 1, 0)
processed = morphology.remove_small_objects(binarized.astype(bool), min_size=2, connectivity=2).astype(int)
# black out pixels
mask_x, mask_y = np.where(processed == 0)
im[mask_x, mask_y, :3] = 0
# plot the result
plt.figure(figsize=(10,10))
plt.imshow(im)
Это отображает:
Чтобы сохранить только большие кластеры, попробуйте увеличить min_size
(наименьший размер удерживаемых кластеров) и уменьшение connectivity
(размер окрестности пикселя при формировании кластеров). Используя только эти два параметра, можно сохранить только кластеры пикселей подходящего размера.