Блок 4. Олимпиадное применение cv2 и PIL¶
Для чего нужен этот блок¶
В предыдущих блоках ты познакомился с основами:
- в
PIL— с изображением как с набором пикселей; - в
cv2— с изображением как с массивом; - с grayscale, threshold, масками, размытием, морфологией и контурами.
Теперь важно сделать следующий шаг:
понять, как все это применять в олимпиадных и инженерных задачах, где картинка — это не цель, а входные данные для алгоритма.
Этот блок посвящен не отдельным функциям, а именно типовым подходам:
- как из изображения получить объект;
- как найти координаты объекта;
- как вычислить его площадь;
- как сравнить два фрагмента;
- как выделить главный объект;
- как проверять простые гипотезы об изображении;
- как мыслить при решении задач с картинками.
После изучения блока ты должен уметь:
- выстраивать решение от условия задачи к алгоритму;
- выбирать нужный конвейер обработки;
- выделять полезную область изображения;
- извлекать признаки объекта;
- сравнивать изображения и фрагменты;
- решать базовые задачи на поиск, измерение и классификацию объектов.
1. Главная идея олимпиадной обработки изображений¶
Во многих олимпиадных задачах важно понять простую вещь:
тебе не нужно “обрабатывать красивую картинку”, тебе нужно превратить изображение в удобные численные признаки.
Обычно картинка — это просто вход, из которого надо получить:
- координаты;
- число объектов;
- площадь;
- форму;
- цвет;
- наличие или отсутствие нужного элемента;
- отношение одного объекта к другому.
То есть мыслить нужно не как дизайнер, а как алгоритмист.
2. Базовая схема решения задач с изображением¶
Очень часто полезно думать по такой схеме:
- Что именно надо найти?
- По какому признаку это можно выделить?
- Нужен ли цвет или достаточно яркости?
- Нужно ли сначала очистить изображение?
- Что будет итогом: число, координаты, площадь, класс?
Эта схема помогает не тонуть в функциях библиотеки.
3. Изображение как источник признаков¶
В олимпиадной задаче изображение обычно нужно свести к признакам.
Например:
- число белых пикселей;
- число объектов;
- площадь самого большого объекта;
- координаты центра объекта;
- цвет объекта;
- ширина и высота найденной области;
- отношение ширины к высоте;
- похожесть на шаблон.
Это и есть переход от “картинки” к “данным”.
4. Типовая задача 1: найти объект на изображении¶
Одна из самых частых задач:
на изображении есть объект, его нужно найти.
Обычно решение выглядит так:
- загрузить изображение;
-
выбрать способ выделения:
-
по цвету;
- по яркости;
- по форме;
- построить маску;
- очистить маску;
- найти контуры;
- выбрать нужный объект.
5. Если объект выделяется цветом¶
Например, надо найти красную метку на фоне.
Тогда удобный подход:
- перевести изображение в
HSV; - построить маску по диапазону цвета;
- убрать шум;
- найти контур;
- получить координаты.
Пример:
import cv2
import numpy as np
img = cv2.imread("picture.png")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
lower = np.array([0, 100, 100])
upper = np.array([10, 255, 255])
mask = cv2.inRange(hsv, lower, upper)
kernel = np.ones((3, 3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
contours, hierarchy = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
print(x, y, w, h)
6. Если объект выделяется яркостью¶
Иногда цвет не важен, а объект просто светлый или темный.
Тогда удобный путь:
- grayscale;
- blur;
- threshold;
- morphology;
- contours.
Пример:
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, 180, 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)
print(len(contours))
7. Типовая задача 2: найти координаты объекта¶
Иногда требуется не просто найти объект, а получить его координаты.
Что может означать “координаты объекта”:
- левый верхний угол;
- центр объекта;
- центр прямоугольника вокруг объекта;
- центр массы контура.
8. Координаты через boundingRect¶
Самый простой способ — ограничивающий прямоугольник.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
print("Центр:", cx, cy)
Когда этого достаточно¶
Если объект примерно компактный и нужно просто оценить его положение, этого способа часто хватает.
9. Координаты через моменты¶
Если нужен более точный центр, можно использовать моменты.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
M = cv2.moments(cnt)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
print("Центр массы:", cx, cy)
Когда это полезно¶
Если фигура не прямоугольная или у нее сложная форма, центр массы обычно точнее.
10. Типовая задача 3: найти площадь объекта¶
Очень частая олимпиадная задача — измерить размер объекта.
Есть два основных способа:
- по площади контура;
- по количеству пикселей в маске.
11. Площадь через контур¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
area = cv2.contourArea(cnt)
print(area)
Плюс этого способа¶
Он очень удобен, когда объект уже выделен как отдельный контур.
12. Площадь через маску¶
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
area = np.sum(img == 255)
print(area)
Когда это полезно и почему¶
Если задача сводится к “сколько пикселей принадлежит объекту”, этот способ очень прямой и понятный.
13. Что выбрать: contourArea или число пикселей¶
cv2.contourArea(...)— хорошо, когда уже есть контур объекта;np.sum(mask == 255)— хорошо, когда есть маска и нужно просто посчитать пиксели.
Оба подхода полезны, и на практике стоит знать оба.
14. Типовая задача 4: найти самый большой объект¶
Часто известно, что главный объект на изображении — самый крупный.
Тогда решение очень удобное:
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
largest = max(contours, key=cv2.contourArea)
print(cv2.contourArea(largest))
Где это помогает¶
- поиск главной детали;
- выбор центрального объекта;
- игнорирование мелкого шума.
15. Типовая задача 5: посчитать объекты¶
Если на изображении несколько отдельных объектов, их число часто можно получить через контуры.
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)
Почему нужен фильтр по площади¶
Иначе шумовые точки тоже будут считаться объектами.
16. Типовая задача 6: вырезать найденный объект¶
После того как объект найден, его часто нужно извлечь для дальнейшего анализа.
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)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
fragment = img[y:y+h, x:x+w]
cv2.imwrite("object.png", fragment)
17. Типовая задача 7: сравнить два фрагмента¶
В олимпиадных задачах иногда нужно понять:
- одинаковые ли две области;
- сильно ли они отличаются;
- какой шаблон ближе к объекту.
Самый простой путь — сравнивать массивы пикселей.
18. Сравнение одинакового размера¶
Если два изображения одного размера, можно сравнить их поэлементно.
import cv2
import numpy as np
img1 = cv2.imread("a.png", 0)
img2 = cv2.imread("b.png", 0)
same = np.array_equal(img1, img2)
print(same)
Что делает np.array_equal¶
Проверяет, совпадают ли все элементы массива.
19. Сравнение по числу различающихся пикселей¶
Иногда изображения почти одинаковые, и нужно измерить разницу.
import cv2
import numpy as np
img1 = cv2.imread("a.png", 0)
img2 = cv2.imread("b.png", 0)
diff = np.sum(img1 != img2)
print(diff)
Как это понимать¶
- если
diff = 0, изображения совпадают; - чем больше
diff, тем сильнее различие.
20. Сравнение после приведения к одному размеру¶
Если фрагменты разных размеров, их можно привести к одному масштабу.
import cv2
import numpy as np
img1 = cv2.imread("a.png", 0)
img2 = cv2.imread("b.png", 0)
img2 = cv2.resize(img2, (img1.shape[1], img1.shape[0]))
diff = np.sum(img1 != img2)
print(diff)
Важно¶
Такой подход грубый, но для простых олимпиадных задач может быть полезен.
21. Типовая задача 8: выбрать лучший шаблон¶
Пусть есть объект и несколько шаблонов. Нужно понять, на какой шаблон объект похож больше всего.
Простейшая идея:
- привести все к одному размеру;
- перевести в grayscale или binary;
- посчитать число различающихся пикселей;
- выбрать шаблон с минимальной разницей.
Пример:
import cv2
import numpy as np
obj = cv2.imread("object.png", 0)
best_name = None
best_score = None
for name in ["template1.png", "template2.png", "template3.png"]:
temp = cv2.imread(name, 0)
temp = cv2.resize(temp, (obj.shape[1], obj.shape[0]))
score = np.sum(obj != temp)
if best_score is None or score < best_score:
best_score = score
best_name = name
print(best_name, best_score)
22. Типовая задача 9: распознать простую фигуру¶
Иногда задача не требует нейросетей. Нужно просто понять, что перед нами:
- круг;
- квадрат;
- прямоугольник;
- узкий объект;
- широкий объект.
Для этого часто достаточно простых признаков:
- площадь;
- ширина;
- высота;
- отношение
w / h; - площадь прямоугольника
w * h; - сравнение площади фигуры и площади рамки.
23. Пример: использовать отношение ширины к высоте¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
ratio = w / h
print(ratio)
Как это применять¶
- если
ratioблизко к1, объект почти квадратный; - если
ratioсильно больше1, объект широкий; - если сильно меньше
1, высокий и узкий.
24. Пример: заполненность прямоугольника¶
Полезный признак — насколько фигура заполняет свой bounding rectangle.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
area = cv2.contourArea(cnt)
x, y, w, h = cv2.boundingRect(cnt)
rect_area = w * h
fill = area / rect_area
print(fill)
Почему это полезно¶
- у прямоугольной плотной фигуры заполненность обычно выше;
- у круга внутри рамки она ниже;
- у сложной рваной формы — еще ниже.
Это простой, но очень полезный признак.
25. Типовая задача 10: проверить наличие объекта¶
Иногда задача формулируется очень просто:
есть ли на изображении нужный объект?
Тогда часто достаточно:
- построить маску;
- найти контуры;
- проверить, есть ли контур нужной площади.
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
found = False
for cnt in contours:
if cv2.contourArea(cnt) >= 200:
found = True
print(found)
26. Типовая задача 11: найти объект ближе всего к центру кадра¶
Иногда на изображении несколько объектов, и нужен тот, который ближе к центру.
Тогда можно:
- найти центр изображения;
- для каждого объекта найти центр;
- посчитать расстояние до центра кадра;
- выбрать минимальное.
Пример:
import cv2
img = cv2.imread("binary.png", 0)
height, width = img.shape
frame_cx = width // 2
frame_cy = height // 2
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
best_cnt = None
best_dist = None
for cnt in contours:
if cv2.contourArea(cnt) >= 100:
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
dist2 = (cx - frame_cx) ** 2 + (cy - frame_cy) ** 2
if best_dist is None or dist2 < best_dist:
best_dist = dist2
best_cnt = cnt
print(best_dist)
Почему используется dist2¶
Это квадрат расстояния. Корень брать не нужно, потому что для сравнения порядок сохраняется.
27. Как мыслить при решении олимпиадной задачи¶
Очень полезный вопрос к себе:
какой минимальный набор признаков уже позволяет решить задачу?
Например:
- не надо распознавать весь объект, если достаточно его площади;
- не надо анализировать весь кадр, если можно взять ROI;
- не надо использовать сложный метод, если хватает threshold + contourArea.
В олимпиадах часто выигрывает не “самая умная библиотека”, а самый прямой и надежный способ.
28. ROI — область интереса¶
Во многих задачах полезно не обрабатывать все изображение, а вырезать только нужную область — ROI.
Например:
- только верхнюю часть кадра;
- только область табло;
- только область с маркером;
- только центр изображения.
Пример:
import cv2
img = cv2.imread("picture.png")
height, width, channels = img.shape
roi = img[height//4:3*height//4, width//4:3*width//4]
cv2.imwrite("roi.png", roi)
Почему это полезно здеесь¶
- меньше шумных областей;
- быстрее работа;
- проще подобрать порог и маску.
29. Типичный конвейер для олимпиадной задачи¶
Один из самых универсальных шаблонов выглядит так:
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)
best_cnt = None
best_area = 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area >= 100 and area > best_area:
best_area = area
best_cnt = cnt
if best_cnt is not None:
x, y, w, h = cv2.boundingRect(best_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)
print("Площадь:", best_area)
print("Центр:", cx, cy)
cv2.imwrite("result.png", result)
Этот шаблон уже покрывает очень много базовых задач.
30. Когда использовать PIL¶
Хотя в олимпиадных задачах чаще удобнее cv2, PIL тоже полезен:
- быстро открыть и сохранить изображение;
- просто работать с пикселями;
- делать crop;
- строить маленькие учебные примеры;
- генерировать простые картинки вручную.
Если задача совсем простая и нужна только работа с пикселями, PIL может быть даже понятнее.
31. Когда использовать cv2¶
cv2 особенно полезен, когда нужны:
- grayscale;
- threshold;
- blur;
- morphology;
- contours;
- работа с цветом в
HSV; - практические конвейеры обработки.
То есть для олимпиадных задач cv2 обычно является основным инструментом.
32. Частые ошибки в олимпиадных задачах¶
1. Слишком сложное решение¶
Новичок иногда пытается сразу искать сложный метод, хотя хватает:
- порога;
- маски;
- contourArea;
- boundingRect.
2. Нет промежуточной визуализации¶
Если сохранять только финальный ответ, трудно понять, где ошибка.
Полезно сохранять:
gray;binary;mask;clean;result.
3. Неправильный выбор порога¶
Порог надо подбирать по смыслу задачи, а не случайно.
4. Нет фильтрации по площади¶
Из-за этого шум принимается за объект.
5. Игнорирование ROI¶
Иногда задача сильно упрощается, если анализировать только нужную часть изображения.
6. Путаница между координатами и индексами¶
Нужно помнить:
- в массиве доступ идет как
img[y, x]; - прямоугольник часто задается как
x, y, w, h.
33. Как тренироваться¶
Лучший способ освоить олимпиадное применение — решать маленькие прикладные мини-задачи.
Например:
- найти самый большой белый объект;
- найти центр красного маркера;
- посчитать число белых фигур;
- выделить только объекты больше заданной площади;
- вырезать объект и сравнить его с шаблоном;
- выбрать объект, ближайший к центру кадра;
- определить, объект широкий или высокий.
Это уже очень близко к реальному стилю задач.
34. Мини-задача 1. Найти координаты самого большого объекта¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
print(x, y, w, h)
35. Мини-задача 2. Посчитать площадь белой фигуры¶
import cv2
import numpy as np
img = cv2.imread("binary.png", 0)
print(np.sum(img == 255))
36. Мини-задача 3. Выбрать самый большой объект и найти его центр¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
if len(contours) > 0:
cnt = max(contours, key=cv2.contourArea)
x, y, w, h = cv2.boundingRect(cnt)
cx = x + w // 2
cy = y + h // 2
print(cx, cy)
37. Мини-задача 4. Сравнить объект с шаблоном¶
import cv2
import numpy as np
obj = cv2.imread("object.png", 0)
temp = cv2.imread("template.png", 0)
temp = cv2.resize(temp, (obj.shape[1], obj.shape[0]))
score = np.sum(obj != temp)
print(score)
38. Мини-задача 5. Проверить наличие объекта площадью не меньше 500¶
import cv2
img = cv2.imread("binary.png", 0)
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
found = False
for cnt in contours:
if cv2.contourArea(cnt) >= 500:
found = True
print(found)
39. Практические задания¶
Задание 1¶
На бинарном изображении найди самый большой объект и выведи:
- его площадь;
- координаты рамки
x, y, w, h; - координаты центра.
Задание 2¶
Построй программу, которая считает число объектов площадью не меньше 100.
Задание 3¶
На цветном изображении найди объект по цвету, построй маску и вырежи его.
Задание 4¶
Возьми два фрагмента изображения, приведи их к одному размеру и оцени различие по числу несовпадающих пикселей.
Задание 5¶
Для нескольких найденных объектов выбери тот, который находится ближе всего к центру кадра.
Задание 6¶
Для объекта вычисли отношение w / h и реши, является ли он скорее широким или скорее высоким.
Задание 7¶
Для найденной фигуры вычисли заполненность bounding rectangle:
fill = area / (w * h)
и попробуй сравнить этот признак для круга и прямоугольника.
40. Итог блока¶
После этого блока ты должен понимать главное:
в олимпиадной задаче изображение почти всегда нужно свести к простым признакам, а дальше решить задачу уже обычной алгоритмикой.
Самые важные идеи этого блока:
- искать не “картинку”, а признаки;
- выбирать самый простой надежный конвейер;
- использовать маски, контуры, площади и координаты;
- не усложнять решение без необходимости.
Если ты уверенно умеешь:
- выделить объект;
- найти его координаты;
- вычислить площадь;
- вырезать фрагмент;
- сравнить его с шаблоном;
- выбрать нужный объект по признакам,
то у тебя уже есть очень хорошая база для олимпиадных задач по обработке изображений.
41. Базовый шаблончик¶
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)
best_cnt = None
best_area = 0
for cnt in contours:
area = cv2.contourArea(cnt)
if area >= 100 and area > best_area:
best_area = area
best_cnt = cnt
if best_cnt is not None:
x, y, w, h = cv2.boundingRect(best_cnt)
cx = x + w // 2
cy = y + h // 2
fill = best_area / (w * h)
cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2)
cv2.circle(result, (cx, cy), 4, (255, 0, 0), -1)
print("Площадь:", best_area)
print("Центр:", cx, cy)
print("Заполненность:", fill)
cv2.imwrite("result.png", result)