Dos cubos

Compramos dos cubos de 3×3 para casa.

  • Cubo oficial: cada casilla tiene un borde negro y el color se concentra en el centro de la casilla. El centro blanco tiene el logotipo de la marca y, por el brillo, refleja la luz.
  • Cubo de edición coreana: es ligero y gira con suavidad. En cambio, es de plástico fluorescente sin pegatinas, así que los colores son intensos, y es tan fino que se transparentan los colores de detrás.

Primero con un VLM

Probamos a conectar Qwen3-VL-8B, el que usábamos para el go. Le mostramos las dos fotos a la vez y le pedimos que respondiera con letras los colores de las 9 casillas de cada cara.

El resultado fue peor que el del escaneo normal. Apenas distinguía los colores y a menudo respondía con una cara llena de un solo color, como RRRRRRRRR. No servía para una tarea que exige señalar con exactitud la posición de las 9 casillas. Hoy el escaneo con VLM solo queda como botón auxiliar que descarta las caras vacías o de un solo color y las sustituye por el resultado del escaneo normal.

Segmentación por color en C++

Así que leímos las fotos directamente en C++.

  1. Se busca la zona del cubo en la foto y las fotos grandes se reducen a 1280px.
  2. Se ajusta el balance de blancos, se crean varios candidatos de cuadrícula para las tres caras y se promedia el color del interior de cada casilla. Se excluyen los bordes para evitar el marco negro y los reflejos.
  3. Se clasifica cada una en uno de los seis colores con condiciones HSV y RGB.
  4. Se determina cada cara por su color central y se confirman las caras una a una en el orden amarillo → rojo → verde → naranja → azul → blanco.
  5. Qué foto tiene el blanco arriba se deduce del logotipo de la marca y de la proporción de amarillo y blanco. Al cambiar el orden de las fotos, el resultado fue el mismo.

Arreglas uno y se rompe el otro

Primero ajustamos el cubo de edición coreana. Corregimos uno a uno problemas como que el verde fluorescente se leyera como amarillo o que los colores transparentados se leyeran como el color de otra cara, hasta que las primeras fotos se leyeron correctamente del todo.

Después añadimos el cubo oficial, que parecía más fácil, pero al principio solo encontró tres de las seis caras.

Esta es la pantalla de la primera lectura del cubo oficial. No encontró las caras de arriba, izquierda y derecha.

Los bordes negros ayudan a separar las casillas, pero en cuanto se sale un poco del interior de la casilla se mezclan el negro y el color de la mesa, y confundía el blanco con el verde. En el cubo oficial, el naranja y el rojo también se parecen. Al añadir condiciones para el cubo oficial se volvían a romper las fotos del cubo de edición coreana, y no paramos de ir y venir ajustando los dos cubos.

CubeModel.cpp, que contiene la clasificación de colores, tiene 230 umbrales decimales, y CubePhotoReconstructor.cpp, el archivo de reconocimiento de fotos, tiene 379. Son valores elegidos a mano para encajar con unas pocas fotos cuya respuesta se conocía.

El código real de esta etapa

Esta es la condición que separa el amarillo del blanco. El amarillo debe tener un componente verde parecido al rojo, y eso es también lo que lo separa del naranja.

src/CubeModel.cppcpp · 12 líneas
// --- 강한/옅은 특징 ---
const bool strongWhite = (v > 0.62 && s < 0.22 && spread < 0.20)
    || (v > 0.72 && s < 0.28 && spread < 0.25);
const bool paleWhite = (v > 0.55 && s < 0.32 && spread < 0.28)
    || (v > 0.60 && s < 0.38 && deg >= 170.0 && deg < 255.0 && !blueChrome && spread < 0.32);
const bool strongYellow = (v > 0.40 && r > 0.42 && g > 0.38 && b < 0.50
                           && (r + g) > b * 2.4 && deg >= 40.0 && deg < 95.0
                           && g >= r * 0.72 // 노랑: G≈R, 주황(G≪R) 제외
                           && !(g > r + 0.14 && s > 0.45));
const bool paleYellow = (v > 0.38 && r > 0.35 && g > 0.32 && b < 0.58
                         && (r + g) > b * 1.9 && deg >= 38.0 && deg < 100.0
                         && g >= r * 0.70 && !greenChrome);

Esta es la función que busca el logotipo en el centro blanco del cubo oficial. Se fija en la proporción de puntos claros y oscuros.

src/CubePhotoReconstructor.cppcpp · 26 líneas
bool hasWhiteCenterLogo(const QImage &img, int cx, int cy, int radius)
{
    if (img.isNull() || radius < 2)
        return false;
    int bright = 0, dark = 0, n = 0;
    const int r = std::max(4, radius);
    for (int dy = -r; dy <= r; ++dy) {
        for (int dx = -r; dx <= r; ++dx) {
            if (dx * dx + dy * dy > r * r)
                continue;
            const int x = std::clamp(cx + dx, 0, img.width() - 1);
            const int y = std::clamp(cy + dy, 0, img.height() - 1);
            const QColor c = img.pixelColor(x, y);
            const int sum = c.red() + c.green() + c.blue();
            const int spr = std::max({c.red(), c.green(), c.blue()}) - std::min({c.red(), c.green(), c.blue()});
            ++n;
            if (sum > 400 && spr < 55)
                ++bright;
            else if (sum < 160)
                ++dark;
        }
    }
    if (n < 20)
        return false;
    return bright * 5 >= n * 2 && dark * 20 >= n && dark * 4 <= n;
}

La siguiente entrada es "3. Qué significaba de verdad 54/54, la tasa de reconocimiento real y la distribución".