2025, Nov 07 12:02

Надежное распознавание грани кубика без наклеек: YOLOv11 и простая постобработка

Надежное распознавание грани кубика без наклеек: откажитесь от контуров Canny. Используйте сегментацию YOLOv11 и простую проверку формы и цвета в OpenCV.

Надежно распознавать грани кубика Рубика без наклеек классическими методами обработки изображений сложнее, чем кажется. Границы между соседними кубиками одного цвета слабые или отсутствуют, а простые цветовые эвристики легко сбиваются из‑за фона. Если вы пробовали конвейеры контуров на базе Canny и уперлись в стену, этот материал предлагает практичный путь, который работает: переходите от краев к семантической сегментации и добавьте легкие геометрические и цветовые проверки.

Постановка задачи: почему классический подход дает сбои

Методы, основанные на выделении границ, хорошо работают с контрастными наклейками. На кубиках без наклеек соседние элементы могут иметь одинаковый оттенок, поэтому границы едва заметны или их вовсе нет. Фон нередко «вмешивается» с похожей насыщенностью и яркостью, что приводит либо к слипшимся контурам, либо к пропускам. Даже, казалось бы, очевидные трюки разваливаются: белые элементы не несут насыщенности, а насыщенность деревянного пола может соперничать с кубиком, поэтому простое маскирование по H/S не позволяет чисто изолировать грань.

Базовый подход, который буксует на практике

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

import cv2 as cv
import numpy as np
from google.colab.patches import cv2_imshow

img_src = cv.imread('cube.png')

gray_img = cv.cvtColor(img_src, cv.COLOR_BGR2GRAY)
denoised_img = cv.fastNlMeansDenoising(gray_img, None, 20, 7, 7)
smeared_img = cv.blur(denoised_img, (3, 3))
edges_img = cv.Canny(smeared_img, 30, 60, 3)
thick_edges = cv.dilate(edges_img, cv.getStructuringElement(cv.MORPH_RECT, (9, 9)))

cnts, _ = cv.findContours(thick_edges, cv.RETR_TREE, cv.CHAIN_APPROX_SIMPLE)

boxes = []

for c in cnts:
    approx_poly = cv.approxPolyDP(c, 0.1 * cv.arcLength(c, True), True)
    if len(approx_poly) == 4 or True:
        x, y, w, h = cv.boundingRect(approx_poly)
        aspect = float(w) / h
        area = cv.contourArea(approx_poly)
        if aspect >= 0.8 and aspect <= 1.2 and w >= 30 and w <= 80 and area >= 900:
            boxes.append({"x": x, "y": y, "w": w, "h": h})

vis = img_src.copy()
for b in boxes:
    x, y, w, h = b["x"], b["y"], b["w"], b["h"]
    cv.rectangle(vis, (x, y), (x + w, y + h), (0, 255, 0), 2)

cv2.imshow(vis)

Во многих реальных сценах это не разделяет элементы: соседние одинаково окрашенные кубики часто сливаются в один контур.

Коренные причины

Суть проблемы в том, что решающий признак — не в границах, а в семантике. Вы пытаетесь распознать конкретный объект и раскладку его грани при разном освещении и на разных фонах. Градиенты, пороги по уровню серого или эвристики по одному каналу — нестабильные сигналы для этой задачи. Как показывает практика, белые элементы не имеют насыщенности, а фон может давать сопоставимые насыщенность и яркость, поэтому чистая сегментация исключительно по цветовым каналам нереалистична. Подход должен интерпретировать формы и области целиком.

бросьте в это ИИ. он в этом хорош. не чат‑боты, а модели семантической сегментации.

Рабочее решение: сегментация YOLOv11 с легкой постобработкой

Обучите модель сегментации YOLOv11 на гранях кубика, затем выполните простые проверки на согласованность формы и цвета. Такой подход избегает хрупких краев и опирается на обученные маски. Подготовьте датасет в формате YOLOv11 Instance Segmentation и создайте data.yaml:

train: ../train/images
val: ../valid/images
test: ../test/images

nc: 6
names: ['Cube']

Установите ultralytics и запустите обучение:

!pip install ultralytics
from ultralytics import YOLO

seg_net = YOLO('best.pt')

seg_net.train(data='./data/data.yaml', epochs=100, batch=64, device='cuda')

После инференса проверьте кандидатную область. Ниже приведены проверки: область должна быть примерно квадратной, в основном заполненной, а каждый элемент — относительно однородным по целевому цветовому диапазону. Тест однородности выполняется в HSV с использованием заранее заданных color_ranges.

import cv2
import numpy as np
from ultralytics import YOLO


def looks_square(tile, tol=0.2):
    h, w = tile.shape[:2]
    r1, r2 = h / w, w / h
    if r1 < 1 - tol or r1 > 1 + tol:
        return False
    if r2 < 1 - tol or r2 > 1 + tol:
        return False
    return True


def mostly_filled(tile, fill_thresh=0.85):
    h, w, c = tile.shape
    total = h * w * c
    filled = np.sum(tile > 0)
    return filled / total > fill_thresh


def passes_color_uniformity(tile, color, min_ratio):
    if color not in color_ranges:
        return False
    hh, ww = tile.shape[:2]
    hsv = cv2.cvtColor(tile, cv2.COLOR_BGR2HSV)
    lower, upper = color_ranges[color]
    mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
    return (np.count_nonzero(mask) / (hh * ww)) > min_ratio


def run_seg_inference(net: YOLO, frame):
    return net(frame, verbose=False)


def extract_face_grid(outputs, n, homogenity_thres=0.6):
    for _, pred in enumerate(outputs):
        orig = pred.orig_img
        H, W, _ = orig.shape
        if pred.masks is not None:
            for idx, mk in enumerate(pred.masks.data):
                mask_np = (mk.cpu().numpy() * 255).astype(np.uint8)
                if mask_np.shape[0] != orig.shape[0] or mask_np.shape[1] != orig.shape[1]:
                    mask_np = cv2.resize(mask_np, (W, H), interpolation=cv2.INTER_NEAREST)
                mask_np, rect = simplify_mask(mask_np, eps=0.005)
                masked = cv2.bitwise_and(orig, orig, mask=mask_np)
                x1, y1, ww, hh = rect
                x2, y2 = x1 + ww, y1 + hh
                x1 = max(0, x1)
                y1 = max(0, y1)
                x2 = min(orig.shape[1], x2)
                y2 = min(orig.shape[0], y2)
                crop = masked[y1:y2, x1:x2]
                if not looks_square(crop):
                    continue
                if not mostly_filled(crop):
                    continue
                tags, uniform = infer_colors_grid(crop, n, color_detection_model)
                if sum([sum(row) for row in uniform]) < homogenity_thres * len(uniform) * len(uniform[0]):
                    continue
                return tags, crop, mask_np, rect
    return None, None, None, None


def infer_colors_grid(patch, n, color_detection_model):
    h, w, _ = patch.shape
    hh, ww = h // n, w // n
    tag_grid = [['' for _ in range(n)] for __ in range(n)]
    uniform_grid = [[False for _ in range(n)] for __ in range(n)]
    for i in range(n):
        for j in range(n):
            cell = patch[i * hh:(i + 1) * hh, j * ww:(j + 1) * ww]
            tag_grid[i][j] = find_best_matching_color_legacy(
                get_median_color(cell), tpe='bgr')
            uniform_grid[i][j] = passes_color_uniformity(cell, tag_grid[i][j], 0.5)
    return tag_grid, uniform_grid

Запустите это на кадре и получите сетку грани, соответствующую вырезку и маску:

results_out = run_seg_inference(seg_net, current_frame)

face_grid, face_crop, face_mask, face_rect = extract_face_grid(results_out, n=grid_n, homogenity_thres=0.6)

Здесь модель сегментации формирует чистую маску объекта, а затем выполняются строгие проверки, соответствующие геометрии и цветовому устройству грани кубика. Там, где выделение границ сливает элементы или вовсе пропускает грань, обученная сегментация берет на себя основную работу. Благодаря этому вся последующая логика сводится к прямолинейной валидации и выборке по сетке.

Почему это важно

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

Выводы

Если контуры по краям не справляются с гранями без наклеек, перестаньте «бороться» с картинкой. Используйте модель семантической сегментации, чтобы надежно изолировать кубик, затем подтвердите детекцию простыми геометрическими ограничениями и проверками однородности по каждой ячейке. Снимайте цвет по сетке медианой и сопоставляйте его вашим текущим классификатором цветов. Эта комбинация устойчива при слабых границах, похожих фонах и разных условиях съемки, а конечный код остается ясным и поддерживаемым.

Статья основана на вопросе на StackOverflow от Tripaloski и ответе от Tripaloski.