Блок 3. Практика обработки изображений в cv2¶
Для чего нужен этот блок¶
В предыдущем блоке ты познакомился с базовой работой в cv2:
- загрузкой изображения;
- массивом пикселей;
- grayscale;
- threshold;
- масками.
Теперь пора перейти к более практическим инструментам, которые часто используются в реальных задачах:
- размытие;
- борьба с шумом;
- морфологические операции;
- поиск контуров;
- выделение объектов прямоугольниками;
- подсчет объектов на изображении.
Главная идея этого блока такая:
в реальной задаче изображение редко бывает идеально чистым, поэтому перед поиском объектов его обычно нужно сначала подготовить.
После изучения блока ты должен уметь:
- понимать, что такое шум на изображении;
- использовать размытие для сглаживания;
- применять морфологические операции для очистки маски;
- находить контуры объектов;
- строить рамки вокруг найденных объектов;
- считать число объектов;
- отбирать объекты по площади;
- получать простые признаки объектов.
1. Общая схема практической обработки¶
Во многих задачах цепочка выглядит так:
- загрузили изображение;
- перевели в удобный вид;
- выделили нужную область;
- очистили изображение от шума;
- нашли объекты;
- измерили их свойства.
То есть недостаточно просто сделать threshold.
Обычно после этого еще нужно улучшить результат, чтобы объекты стали более чистыми и удобными для анализа.
2. Что такое шум¶
Шум — это случайные маленькие помехи на изображении.
Например:
- одиночные белые точки на черном фоне;
- маленькие черные дыры внутри белого объекта;
- рваные края у фигуры;
- мелкие лишние пятна.
Если не убирать шум, то:
- один объект может развалиться на несколько частей;
- маленькие мусорные точки будут считаться отдельными объектами;
- контуры будут неточными.
3. Размытие изображения¶
Один из самых простых способов сделать изображение более гладким — размытие.
Размытие помогает:
- уменьшить мелкий шум;
- сгладить резкие скачки яркости;
- подготовить изображение к threshold или поиску контуров.
Gaussian Blur¶
Самый популярный вариант — GaussianBlur.
import cv2
img = cv2.imread("picture.png")
blurred = cv2.GaussianBlur(img, (5, 5), 0)
cv2.imwrite("blurred.png", blurred)
Что означают параметры¶
cv2.GaussianBlur(img, (5, 5), 0)
img— исходное изображение;(5, 5)— размер окна размытия;0— параметр sigma, обычно можно оставить0.
Что важно понять¶
Чем больше окно, тем сильнее размытие.
Например:
(3, 3)— слабое;(5, 5)— среднее;(9, 9)— сильнее.
4. Размытие перед бинаризацией¶
Очень частый прием: сначала слегка размыть, а потом делать threshold.
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
cv2.imwrite("binary_after_blur.png", binary)
Почему это полезно¶
Если сразу делать threshold по шумному изображению, получится много мусора.
Если сначала сгладить изображение, бинаризация часто дает более чистый результат.
5. Median Blur¶
Еще один полезный вид размытия — medianBlur.
Он часто хорошо работает против одиночного импульсного шума.
import cv2
img = cv2.imread("picture.png")
filtered = cv2.medianBlur(img, 5)
cv2.imwrite("median.png", filtered)
Когда он полезен¶
Если на изображении есть отдельные случайные точки, medianBlur может сработать лучше обычного размытия.
6. Морфологические операции¶
После threshold часто получается бинарная маска, но она бывает неидеальной:
- есть лишние белые точки;
- есть дырки внутри объекта;
- есть тонкие разрывы;
- края слишком рваные.
Для исправления таких проблем используют морфологические операции.
Основные из них:
- erosion;
- dilation;
- opening;
- closing.
7. Что такое ядро¶
Морфологические операции используют специальную маленькую матрицу — ядро.
Обычно его создают так:
import numpy as np
kernel = np.ones((3, 3), np.uint8)
Это означает квадрат 3 x 3, заполненный единицами.
Можно делать и ядро 5 x 5:
kernel = np.ones((5, 5), np.uint8)
Размер ядра влияет на силу операции.
8. Erosion¶
Erosion уменьшает белые области.
Это помогает:
- убрать маленькие белые точки;
- отрезать тонкие шумовые хвосты;
- сделать объекты чуть компактнее.
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
kernel = np.ones((3, 3), np.uint8)
eroded = cv2.erode(img, kernel, iterations=1)
cv2.imwrite("eroded.png", eroded)
Что полезно помнить¶
Если сделать erosion слишком сильным, маленькие объекты могут исчезнуть полностью.
9. Dilation¶
Dilation расширяет белые области.
Это помогает:
- закрыть маленькие разрывы;
- утолщить объект;
- соединить близкие части фигуры.
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
kernel = np.ones((3, 3), np.uint8)
dilated = cv2.dilate(img, kernel, iterations=1)
cv2.imwrite("dilated.png", dilated)
10. Opening¶
Opening = erosion, а потом dilation.
Обычно используется для того, чтобы:
- убрать маленький белый шум;
- сохранить крупные объекты.
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
kernel = np.ones((3, 3), np.uint8)
opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imwrite("opened.png", opened)
Когда полезно¶
Если на маске много мелких белых точек, opening часто хорошо очищает результат.
11. Closing¶
Closing = dilation, а потом erosion.
Обычно используется для того, чтобы:
- закрыть маленькие черные дырки внутри объекта;
- соединить близкие части белой области.
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
kernel = np.ones((3, 3), np.uint8)
closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
cv2.imwrite("closed.png", closed)
12. Практическая цепочка очистки маски¶
Очень часто полезно сделать так:
- grayscale;
- blur;
- threshold;
- opening или closing.
Пример:
import cv2
import numpy as np
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((3, 3), np.uint8)
clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
cv2.imwrite("clean.png", clean)
Это уже очень типичный и полезный шаблон.
13. Контуры¶
После того как маска стала достаточно чистой, можно искать контуры объектов.
Контур — это граница фигуры.
Если у тебя есть белые объекты на черном фоне, cv2.findContours(...) позволяет найти их формы.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
Что возвращает функция¶
contours— список найденных контуров;hierarchy— информация о вложенности контуров.
На первом этапе обычно достаточно работать только с contours.
14. Что означают параметры findContours¶
cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.RETR_EXTERNAL¶
Означает: брать только внешние контуры.
Это удобно, если не хочется отдельно обрабатывать внутренние дырки.
cv2.CHAIN_APPROX_SIMPLE¶
Означает: хранить контур в более компактном виде.
Для базовых задач почти всегда подходит именно этот режим.
15. Как посчитать объекты¶
Если на изображении каждый объект отделен от других, то количество внешних контуров часто и есть число объектов.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print("Количество объектов:", len(contours))
Но есть важное условие¶
Это работает хорошо, если:
- объекты не слиплись;
- маска чистая;
- шум уже убран.
16. Нарисовать найденные контуры¶
Чтобы проверить результат, полезно рисовать найденные контуры поверх изображения.
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)
cv2.imwrite("contours.png", img)
Что означает -1¶
Это значит: нарисовать все контуры.
17. Площадь контура¶
Для каждого найденного объекта можно посчитать площадь.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
area = cv2.contourArea(cnt)
print(area)
Зачем это нужно¶
Площадь помогает:
- отфильтровать шум;
- найти самый большой объект;
- игнорировать слишком маленькие контуры.
18. Фильтрация по площади¶
Например, можно оставить только объекты площадью не меньше 100.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
big_contours = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area >= 100:
big_contours.append(cnt)
print(len(big_contours))
Это очень частый прием.
19. Bounding Rectangle¶
После того как найден объект, очень удобно построить вокруг него прямоугольник.
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite("rectangles.png", img)
Что означают x, y, w, h¶
x, y— координаты левого верхнего угла;w— ширина прямоугольника;h— высота прямоугольника.
20. Зачем нужен boundingRect¶
Прямоугольник вокруг объекта полезен, когда нужно:
- найти положение объекта;
- вырезать найденную область;
- оценить размеры объекта;
- показать результат визуально.
21. Вырезать объект по прямоугольнику¶
После boundingRect можно сразу взять фрагмент изображения.
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for i, cnt in enumerate(contours):
x, y, w, h = cv2.boundingRect(cnt)
fragment = img[y:y+h, x:x+w]
cv2.imwrite(f"object_{i}.png", fragment)
22. Самый большой объект¶
Иногда на изображении нужно найти не все объекты, а самый крупный.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
largest = max(contours, key=cv2.contourArea)
print(cv2.contourArea(largest))
Когда это полезно¶
Если известно, что главный объект на изображении самый большой, такой прием очень удобен.
23. Прямоугольник только для самого большого объекта¶
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
largest = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(largest)
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv2.imwrite("largest_object.png", img)
24. Центр прямоугольника¶
Если нужно получить примерное положение объекта, можно найти центр его прямоугольника.
cx = x + w // 2
cy = y + h // 2
Полный пример:
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
cv2.circle(img, (cx, cy), 4, (255, 0, 0), -1)
cv2.imwrite("centers.png", img)
25. Моменты и более точный центр¶
Есть и более точный способ найти центр объекта — через моменты.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
M = cv2.moments(cnt)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
print(cx, cy)
Идея¶
Это дает центр массы контура.
Для первого знакомства достаточно знать, что такой способ существует и часто точнее, чем центр прямоугольника.
26. Подсчет объектов после фильтрации¶
Очень типичная задача: не просто посчитать все контуры, а посчитать только нормальные объекты.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
count = 0
for cnt in contours:
if cv2.contourArea(cnt) >= 100:
count += 1
print(count)
Так можно отсечь мелкий мусор.
27. Полный практический пример¶
Ниже — уже очень полезный шаблон обработки.
Задача: найти объекты, очистить шум, нарисовать рамки и посчитать число объектов.
import cv2
import numpy as np
img = cv2.imread("picture.png")
result = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((3, 3), np.uint8)
clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
contours, hierarchy = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
count = 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area >= 100:
count += 1
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2)
print("Количество объектов:", count)
cv2.imwrite("clean_mask.png", clean)
cv2.imwrite("objects_detected.png", result)
Почему этот шаблон хорош¶
Он показывает почти весь основной конвейер:
- подготовка;
- очистка;
- поиск;
- фильтрация;
- визуализация.
28. Когда нужен copy()¶
Обрати внимание на строку:
result = img.copy()
Это нужно, чтобы рисовать рамки не на исходном массиве, а на его копии.
Так удобнее:
- исходник остается нетронутым;
- можно отдельно сохранить результат обработки.
29. Частые ошибки новичка¶
1. Поиск контуров по слишком грязной маске¶
Если не убрать шум, будет много ложных объектов.
2. Слишком сильное размытие¶
Если размыть слишком сильно, маленькие детали исчезнут.
3. Неподходящий порог¶
Плохой threshold может слить объект с фоном или, наоборот, разорвать его.
4. Отсутствие фильтрации по площади¶
Из-за этого мусорные точки считаются объектами.
5. Работа без визуальной проверки¶
Очень важно сохранять промежуточные результаты:
gray;binary;clean;- итог с рамками.
Тогда проще понять, на каком этапе ошибка.
6. Использование max(contours, ...), когда контуров нет¶
Если список contours пустой, будет ошибка. Значит, иногда надо делать проверку:
if len(contours) > 0:
largest = max(contours, key=cv2.contourArea)
30. Минимум, который нужно запомнить¶
Размытие:
blurred = cv2.GaussianBlur(img, (5, 5), 0)
Median blur:
filtered = cv2.medianBlur(img, 5)
Ядро:
kernel = np.ones((3, 3), np.uint8)
Erosion:
eroded = cv2.erode(img, kernel, iterations=1)
Dilation:
dilated = cv2.dilate(img, kernel, iterations=1)
Opening:
opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
Closing:
closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
Контуры:
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
Нарисовать контуры:
cv2.drawContours(result, contours, -1, (0, 255, 0), 2)
Площадь:
area = cv2.contourArea(cnt)
Прямоугольник:
x, y, w, h = cv2.boundingRect(cnt)
Нарисовать рамку:
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2)
Центр прямоугольника:
cx = x + w // 2
cy = y + h // 2
31. Готовые примеры¶
Пример 1. Размытие изображения¶
import cv2
img = cv2.imread("picture.png")
blurred = cv2.GaussianBlur(img, (5, 5), 0)
cv2.imwrite("blurred.png", blurred)
Пример 2. Бинаризация после размытия¶
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
cv2.imwrite("binary.png", binary)
Пример 3. Очистка маски через opening¶
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
kernel = np.ones((3, 3), np.uint8)
opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imwrite("opened.png", opened)
Пример 4. Найти и посчитать контуры¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
print(len(contours))
Пример 5. Нарисовать контуры¶
import cv2
img = cv2.imread("picture.png")
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, contours, -1, (0, 255, 0), 2)
cv2.imwrite("contours.png", img)
Пример 6. Оставить только крупные объекты¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
count = 0
for cnt in contours:
if cv2.contourArea(cnt) >= 100:
count += 1
print(count)
Пример 7. Нарисовать рамки вокруг объектов¶
import cv2
img = cv2.imread("picture.png")
result = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
if cv2.contourArea(cnt) >= 100:
x, y, w, h = cv2.boundingRect(cnt)
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.imwrite("rectangles.png", result)
Пример 8. Найти центр объектов¶
import cv2
img = cv2.imread("picture.png")
result = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
_, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
contours, hierarchy = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
if cv2.contourArea(cnt) >= 100:
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
cv2.circle(result, (cx, cy), 4, (255, 0, 0), -1)
cv2.imwrite("centers.png", result)
32. Практические задания¶
Задание 1¶
Открой изображение, переведи его в grayscale, размыть через GaussianBlur и сохрани результат.
Задание 2¶
Сделай бинаризацию размытого изображения.
Задание 3¶
Попробуй применить opening и сравни результат с обычной бинаризацией.
Задание 4¶
Найди количество внешних контуров на маске.
Задание 5¶
Оставь только те контуры, у которых площадь не меньше 100.
Задание 6¶
Нарисуй прямоугольники вокруг всех крупных объектов.
Задание 7¶
Найди самый большой объект и выдели только его.
Задание 8¶
Для каждого крупного объекта найди центр и отметь его точкой.
33. Почему это важно для олимпиадных задач¶
Во многих прикладных задачах недостаточно просто “найти белые пиксели”.
Нужно:
- отделить объект от шума;
- понять, сколько объектов есть;
- определить их размеры;
- найти их координаты;
- выбрать главный объект.
Именно для этого и нужны:
- blur;
- morphology;
- contours;
- boundingRect;
- contourArea.
Это уже очень близко к реальным задачам компьютерного зрения на соревнованиях.
34. Итог блока¶
После этого блока ты должен понимать такую практическую схему:
- изображение нужно не только загрузить, но и подготовить;
- шум почти всегда надо убирать;
- бинарная маска должна быть чистой;
- объекты удобно искать через контуры;
- контуры можно измерять и визуализировать.
Это один из самых полезных базовых наборов для уверенной работы с изображениями в cv2.
35. Базовый шаблон¶
import cv2
import numpy as np
img = cv2.imread("picture.png")
result = img.copy()
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
_, binary = cv2.threshold(blurred, 127, 255, cv2.THRESH_BINARY)
kernel = np.ones((3, 3), np.uint8)
clean = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)
contours, hierarchy = cv2.findContours(clean, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for cnt in contours:
area = cv2.contourArea(cnt)
if area >= 100:
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.circle(result, (cx, cy), 4, (255, 0, 0), -1)
cv2.imwrite("result.png", result)