Перейти к содержанию

Блок 4. Олимпиадное применение cv2 и PIL

Для чего нужен этот блок

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

  • в PIL — с изображением как с набором пикселей;
  • в cv2 — с изображением как с массивом;
  • с grayscale, threshold, масками, размытием, морфологией и контурами.

Теперь важно сделать следующий шаг:

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

Этот блок посвящен не отдельным функциям, а именно типовым подходам:

  • как из изображения получить объект;
  • как найти координаты объекта;
  • как вычислить его площадь;
  • как сравнить два фрагмента;
  • как выделить главный объект;
  • как проверять простые гипотезы об изображении;
  • как мыслить при решении задач с картинками.

После изучения блока ты должен уметь:

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

1. Главная идея олимпиадной обработки изображений

Во многих олимпиадных задачах важно понять простую вещь:

тебе не нужно “обрабатывать красивую картинку”, тебе нужно превратить изображение в удобные численные признаки.

Обычно картинка — это просто вход, из которого надо получить:

  • координаты;
  • число объектов;
  • площадь;
  • форму;
  • цвет;
  • наличие или отсутствие нужного элемента;
  • отношение одного объекта к другому.

То есть мыслить нужно не как дизайнер, а как алгоритмист.


2. Базовая схема решения задач с изображением

Очень часто полезно думать по такой схеме:

  1. Что именно надо найти?
  2. По какому признаку это можно выделить?
  3. Нужен ли цвет или достаточно яркости?
  4. Нужно ли сначала очистить изображение?
  5. Что будет итогом: число, координаты, площадь, класс?

Эта схема помогает не тонуть в функциях библиотеки.


3. Изображение как источник признаков

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

Например:

  • число белых пикселей;
  • число объектов;
  • площадь самого большого объекта;
  • координаты центра объекта;
  • цвет объекта;
  • ширина и высота найденной области;
  • отношение ширины к высоте;
  • похожесть на шаблон.

Это и есть переход от “картинки” к “данным”.


4. Типовая задача 1: найти объект на изображении

Одна из самых частых задач:

на изображении есть объект, его нужно найти.

Обычно решение выглядит так:

  1. загрузить изображение;
  2. выбрать способ выделения:

  3. по цвету;

  4. по яркости;
  5. по форме;
  6. построить маску;
  7. очистить маску;
  8. найти контуры;
  9. выбрать нужный объект.

5. Если объект выделяется цветом

Например, надо найти красную метку на фоне.

Тогда удобный подход:

  1. перевести изображение в HSV;
  2. построить маску по диапазону цвета;
  3. убрать шум;
  4. найти контур;
  5. получить координаты.

Пример:

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. Если объект выделяется яркостью

Иногда цвет не важен, а объект просто светлый или темный.

Тогда удобный путь:

  1. grayscale;
  2. blur;
  3. threshold;
  4. morphology;
  5. 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: найти площадь объекта

Очень частая олимпиадная задача — измерить размер объекта.

Есть два основных способа:

  1. по площади контура;
  2. по количеству пикселей в маске.

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: выбрать лучший шаблон

Пусть есть объект и несколько шаблонов. Нужно понять, на какой шаблон объект похож больше всего.

Простейшая идея:

  1. привести все к одному размеру;
  2. перевести в grayscale или binary;
  3. посчитать число различающихся пикселей;
  4. выбрать шаблон с минимальной разницей.

Пример:

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: найти объект ближе всего к центру кадра

Иногда на изображении несколько объектов, и нужен тот, который ближе к центру.

Тогда можно:

  1. найти центр изображения;
  2. для каждого объекта найти центр;
  3. посчитать расстояние до центра кадра;
  4. выбрать минимальное.

Пример:

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. Как тренироваться

Лучший способ освоить олимпиадное применение — решать маленькие прикладные мини-задачи.

Например:

  1. найти самый большой белый объект;
  2. найти центр красного маркера;
  3. посчитать число белых фигур;
  4. выделить только объекты больше заданной площади;
  5. вырезать объект и сравнить его с шаблоном;
  6. выбрать объект, ближайший к центру кадра;
  7. определить, объект широкий или высокий.

Это уже очень близко к реальному стилю задач.


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)