Image

Применение классического компьютерного зрения для извлечения судоку

Почему не стоит усложнять решения простых проблем

Делиться

Анимированное перспективное преобразование, деформирующее сетку судоку

В нынешнюю эпоху ажиотажа вокруг искусственного интеллекта складывается впечатление, что все используют модели Vision-Language и крупные Vision Transformers для решения любых задач в области компьютерного зрения. Многие считают эти инструменты универсальными и сразу же используют самую современную, блестящую модель, вместо того чтобы понять, какой именно сигнал они хотят извлечь. Но зачастую красота кроется в простоте. Это один из важнейших уроков, которые я усвоил как инженер: не усложняйте решения простых задач.

Анимация конвейера переработки

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

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

Обзор

  • Обнаружение
    • Оттенки серого
    • Обнаружение краев
    • Расширение
    • Обнаружение контура
  • Трансформация перспективы
    • Вариант А: Простая сортировка на основе суммы/разности
    • Вариант B: Задача оптимизации назначения
    • Вариант C: Циклическая сортировка с якорем
    • Применение преобразования перспективы
  • Заключение

Обнаружение

Для обнаружения сеток судоку я рассмотрел множество различных подходов: от простого порогового значения, преобразований линий Хафа или какой-либо формы обнаружения границ до обучения модели глубокого обучения для сегментации или обнаружения ключевых точек.

Давайте определим некоторые предположения , чтобы определить масштаб проблемы:

  1. Сетка судоку отчетливо и полностью видна в кадре с четкой четырехугольной рамкой, с сильным контрастом к фону.
  2. Поверхность, на которой печатается сетка судоку, должна быть плоской, но при съемке под углом она может выглядеть перекошенной или повернутой.
Примеры для различного качества изображения

Я покажу вам простой конвейер с несколькими этапами фильтрации для определения границ сетки судоку. На высоком уровне конвейер обработки выглядит следующим образом:

Схема этапов технологического конвейера
Визуализация этапов технологического конвейера

Оттенки серого

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

def find_sudoku_grid( image: np.ndarray, ) -> np.ndarray | None: «»» Находит наибольший квадратный контур на изображении, вероятно, сетку судоку. Возвращает: контур найденной сетки в виде массива numpy или None, если не найдено. «»» gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

Обнаружение краев

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

Объяснение порогов Canny Edge

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

Перед передачей изображений в Canny часто используется фильтр размытия для уменьшения шума, но в этом случае края очень четкие, но узкие, поэтому размытие не применяется.

def find_sudoku_grid( image: np.ndarray, canny_threshold_1: int = 100, canny_threshold_2: int = 255, ) -> np.ndarray | None: «»» Находит наибольший квадратный контур на изображении, вероятно, это сетка судоку. Аргументы: image: Входное изображение. canny_threshold_1: Нижний порог для детектора рёбер Canny. canny_threshold_2: Верхний порог для детектора рёбер Canny. Возвращает: Контур найденной сетки в виде массива numpy или None, если не найдено. «»» … canny = cv2.Canny(gray, threshold1=canny_threshold_1, threshold2=canny_threshold_2)

f20c6d64264f79e85a8a0a4aafde1b36

Расширение

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

def find_sudoku_grid( изображение: np.ndarray, canny_threshold_1: int = 100, canny_threshold_2: int = 255, morph_kernel_size: int = 3, ) -> np.ndarray | None: «»» Находит наибольший квадратный контур на изображении, вероятно, сетку судоку. Args: image: Входное изображение. canny_threshold_1: Первый порог для детектора рёбер Кэнни. canny_threshold_2: Второй порог для детектора рёбер Кэнни. morph_kernel_size: Размер ядра морфологической операции. Возвращает: Контур найденной сетки в виде массива numpy или None, если не найдено. «»» … kernel = cv2.getStructuringElement( shape=cv2.MORPH_RECT, ksize=(morph_kernel_size, morph_kernel_size) ) mask = cv2.morphologyEx(canny, op=cv2.MORPH_DILATE, kernel=kernel, iterations=1)

183db1dd7f2a6fd04a2b11de35e4e0a7

Обнаружение контура

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

контуры, _ = cv2.findContours( маска, режим = cv2.RETR_EXTERNAL, метод = cv2.CHAIN_APPROX_SIMPLE )

7f1fa34d7ffea1ff7177923e3e30aa17

Это первоначальное обнаружение контура вернёт список контуров, содержащих каждый отдельный пиксель, являющийся его частью. Мы можем использовать алгоритм Дугласа–Пейкера для итеративного сокращения количества точек контура и аппроксимации контура простым многоугольником. Мы можем выбрать минимальное расстояние между точками для алгоритма.

3794052b3fc91c7d38a6224fc4812132
b65308015a62ddeea43f20d5a30cba23

Если предположить, что даже для некоторых наиболее асимметричных прямоугольников кратчайшая сторона составляет не менее 10% длины окружности фигуры, мы можем отфильтровать контуры до многоугольников, имеющих ровно четыре точки.

contour_candidates: list[np.ndarray] = [] for cnt in contours: # Аппроксимируем контур до многоугольника epsilon = 0.1 * cv2.arcLength(curve=cnt, closed=True) approx = cv2.approxPolyDP(curve=cnt, epsilon=epsilon, closed=True) # Оставляем только многоугольники с 4 вершинами, если len(approx) == 4: contour_candidates.append(approx)

Наконец, мы берём самый большой обнаруженный контур, предположительно, финальную сетку судоку. Мы сортируем контуры по площади в обратном порядке, а затем берём первый элемент, соответствующий наибольшей площади контура.

best_contour = sorted(contour_candidates, key=cv2.contourArea, reverse=True)[0]

e1280107b1236a59e121855c142f7c2a

Трансформация перспективы

Наконец, нам нужно преобразовать полученную сетку обратно в квадрат. Для этого можно использовать перспективное преобразование. Матрицу преобразования можно рассчитать, указав, где в итоге должны располагаться четыре точки контура сетки судоку: в четырёх углах изображения.

rect_dst = np.array( [[0, 0], [ширина — 1, 0], [ширина — 1, высота — 1], [0, высота — 1]], )

e5351a035a63d9089c20942f94496fbf

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

34cda4e39590a848c613b36ea2bcbe45

Вариант А: Простая сортировка на основе суммы/разности

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

p_сумма = p_x + p_y p_дифф = p_x — p_y

На основании этих значений теперь можно дифференцировать углы:

  • В левом верхнем углу есть как малые значения x, так и y, у него наименьшая сумма argmin(p_sum)
  • В правом нижнем углу находится самая большая сумма argmax(p_sum)
  • В правом верхнем углу находится самая большая разница argmax(p_diff)
  • В левом нижнем углу наименьшая разница argmin(p_diff)

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

Анимация вращающегося квадрата, каждый угол которого имеет свой цвет и линии, указывающие на назначение углов изображения

def order_points(pts: np.ndarray) -> np.ndarray: «»» Упорядочивает четыре угловые точки контура в последовательной последовательности: верхний левый, верхний правый, нижний правый, нижний левый. Аргументы: pts: Массив numpy формы (4, 2), представляющий четыре угла. Возвращает: Массив numpy формы (4, 2) с упорядоченными точками. «»» # При необходимости изменяет форму с (4, 1, 2) на (4, 2) pts = pts.reshape(4, 2) rect = np.zeros((4, 2), dtype=np.float32) # Верхняя левая точка будет иметь наименьшую сумму, тогда как # нижняя правая точка будет иметь наибольшую сумму s = pts.sum(axis=1) rect[0] = pts[np.argmin(s)] rect[2] = pts[np.argmax(s)] # Верхняя правая точка будет иметь наименьшую разницу, # тогда как нижняя левая точка будет иметь наибольшую разницу diff = np.diff(pts, axis=1) rect[1] = pts[np.argmin(diff)] rect[3] = pts[np.argmax(diff)] return rect

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

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

Вариант B: Задача оптимизации назначения

Другой подход заключается в минимизации расстояний между каждой точкой и назначенным ей углом. Это можно реализовать с помощью вычисления pairwise_distances между каждой точкой и углами и функции linear_sum_assignment из Scipy, которая решает задачу назначения, минимизируя при этом функцию стоимости.

def order_points_simplified(pts: np.ndarray) -> np.ndarray: «»» Упорядочивает набор точек для наилучшего соответствия целевому набору угловых точек. Аргументы: pts: Массив numpy формы (N, 2), представляющий упорядочиваемые точки. Возвращает: Массив numpy формы (N, 2) с упорядоченными точками. «»» # При необходимости изменяем форму с (N, 1, 2) на (N, 2) pts = pts.reshape(-1, 2) # Рассчитываем расстояние между каждой точкой и каждым целевым углом D = pairwise_distances(pts, pts_corner) # Находим оптимальное однозначное назначение # row_ind[i] должно сопоставляться с col_ind[i] row_ind, col_ind = linear_sum_assignment(D) # Создаём пустой массив для хранения отсортированных точек order_pts = np.zeros_like(pts) # Размещаем каждую точку в правильном слоте в зависимости от угла, с которым она была сопоставлена. # Например, точка, сопоставленная с target_corners[0], попадает в order_pts[0]. order_pts[col_ind] = pts[row_ind] return order_pts

Анимированный вращающийся перекошенный четырехугольник с углами, правильно назначенными углам изображения

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

Вариант C: Циклическая сортировка с якорем

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

Эскиз углов, назначенных каждому углу

Поскольку углы циклические, нам необходимо выбрать якорь, гарантирующий абсолютный порядок точек. Мы просто выбираем точку с наименьшей суммой x и y.

def order_points(self, pts: np.ndarray) -> np.ndarray: «»» Упорядочивает точки по углу вокруг центроида, затем поворачивает их, начиная с верхнего левого угла. Аргументы: pts: Массив numpy формы (4, 2). Возвращает: Массив numpy формы (4, 2) с упорядоченными точками.»»» pts = pts.reshape(4, 2) center = pts.mean(axis=0) angles = np.arctan2(pts[:, 1] — center[1], pts[:, 0] — center[0]) pts_cyclic = pts[np.argsort(angles)] sum_of_coords = pts_cyclic.sum(axis=1) top_left_idx = np.argmin(sum_of_coords) return np.roll(pts_cyclic, -top_left_idx, ось=0)

Анимированный вращающийся перекошенный четырехугольник с правильно назначенными углами с помощью метода назначения углов

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

rect_src = order_points(grid_contour)

Применение преобразования перспективы

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

Анимация применения перспективного преобразования

Поскольку у нас уже есть список точек для обнаруженного четырехугольника, отсортированный в rect_src, и у нас есть целевые угловые точки в rect_dst, мы можем использовать метод OpenCV для вычисления матрицы преобразования:

warp_mat = cv2.getPerspectiveTransform(rect_src, rect_dst)

Результатом является матрица деформации размером 3×3, определяющая преобразование из наклонного трёхмерного перспективного вида в плоский вид сверху вниз. Чтобы получить этот плоский вид сверху вниз для нашей сетки судоку, мы можем применить к исходному изображению следующее перспективное преобразование:

warped = cv2.warpPerspective(img, warp_mat, (side_len, side_len))

И вуаля, у нас есть идеально квадратная сетка судоку!

59bd5f56013bccf6201b91c569027a5e

Заключение

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

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

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

Источник: towardsdatascience.com

✅ Найденные теги: новости, Применение
Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Фото сгенерированных лиц: исследование показывает, что люди не могут отличить настоящие лица от сгенерированных
Нейросети построили капитализм за трое суток: 100 агентов Claude заперли…
Скетч: цифровой осьминог и виртуальный мир внутри компьютера с человечком.
Сцена с жестами пальцами, где один жест символизирует "VPN", а другой "KHP".
‼️Paramount купила Warner Bros. Discovery — сумма сделки составила безумные…
Скриншот репозитория GitHub "Claude Scientific Skills" AI для научных исследований.
Структура эффективного запроса Claude с элементами задачи, контекста и референса.
Эскиз и готовая веб-страница платформы для AI-дизайна в современном темном режиме.
ideipro logotyp
Image Not Found
Звёздное небо с галактиками и туманностями, космос, Вселенная, астрофотография.

Система оповещения обсерватории Рубина отправила 800 000 сигналов в первую ночь наблюдений.

Астрономы будут получать оповещения о небесных явлениях в течение нескольких минут после их обнаружения. Теренс О'Брайен, редактор раздела «Выходные». Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной…

Мар 2, 2026
Женщина с длинными тёмными волосами в синем свете, нейтральный фон.

Расследование в отношении 61-фунтовой машины, которая «пожирает» пластик и выплевывает кирпичи.

Обзор компактного пресса для мягкого пластика Clear Drop — и что будет дальше. Шон Холлистер, старший редактор Публикации этого автора будут добавляться в вашу ежедневную рассылку по электронной почте и в ленту новостей на главной странице вашего…

Мар 2, 2026
Черный углеродное волокно с текстурой плетения, отражающий свет.

Материал будущего: как работает «бессмертный» композит

Учёные из Университета штата Северная Каролина представили композит нового поколения, способный самостоятельно восстанавливаться после серьёзных повреждений.  Речь идёт о модифицированном армированном волокном полимере (FRP), который не просто сохраняет прочность при малом весе, но и способен «залечивать» внутренние…

Мар 2, 2026
Круглый экран с изображением замка и горы, рядом электронная плата.

Круглый дисплей Waveshare для креативных проектов

Круглый 7-дюймовый сенсорный дисплей от Waveshare создан для разработчиков и дизайнеров, которым нужен нестандартный экран.  Это IPS-панель с разрешением 1 080×1 080 пикселей, поддержкой 10-точечного ёмкостного сенсора, оптической склейкой и защитным закалённым стеклом, выполненная в круглом форм-факторе.…

Мар 2, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых