Dois cubos

Compramos dois cubos 3×3 para casa.

  • Cubo oficial: cada casa tem uma borda preta, e a cor fica concentrada no meio da casa. O centro branco tem o logotipo da marca, e o brilho dele reflete a luz.
  • Cubo lançado na Coreia: é leve e gira com suavidade. Por outro lado, é de plástico fluorescente sem adesivos, então as cores são intensas, e ele é fino o bastante para as cores de trás aparecerem por transparência.

Primeiro com um VLM

Conectamos o Qwen3-VL-8B, que usávamos no go. Mostramos as duas fotos de uma vez e pedimos que ele respondesse em letras as cores das 9 casas de cada face.

O resultado foi pior que o da leitura comum. Ele quase não distinguia as cores, e muitas vezes respondia preenchendo uma face com uma única cor, como RRRRRRRRR. Não servia para uma tarefa que exige apontar com precisão a posição das 9 casas. Hoje a leitura por VLM ficou só como um botão auxiliar, que descarta faces vazias ou preenchidas com uma cor só e as substitui pelo resultado da leitura comum.

Segmentação de cores em C++

Então passamos a ler diretamente em C++.

  1. Encontra a área do cubo na foto e reduz fotos grandes para 1280 px.
  2. Ajusta o balanço de branco, monta vários candidatos de grade para as três faces e tira a média da cor dentro de cada casa. As bordas ficam de fora para evitar os contornos e os reflexos.
  3. Classifica em uma das seis cores com condições de HSV e RGB.
  4. Define cada face pela cor do centro e confirma uma face por vez na ordem amarelo → vermelho → verde → laranja → azul → branco.
  5. Qual foto tem o branco em cima é descoberto pelo logotipo da marca e pela proporção entre amarelo e branco. Trocando a ordem das fotos, o resultado foi o mesmo.

Acertar um quebra o outro

Ajustamos primeiro o cubo lançado na Coreia. Corrigimos um por um problemas como o verde fluorescente sendo lido como amarelo e as cores transparentes sendo lidas como cores de outra face, até as primeiras fotos serem lidas totalmente certas.

Depois colocamos o cubo oficial, que parecia mais fácil, mas no começo só três das seis faces foram encontradas.

Esta é a tela da primeira leitura do cubo oficial. As faces de cima, da esquerda e da direita não foram encontradas.

A borda preta ajuda a separar as casas, mas basta sair um pouco de dentro da casa para o preto e a cor da mesa se misturarem, confundindo o branco com o verde. No cubo oficial, o laranja e o vermelho também são parecidos. Ao colocar condições para o cubo oficial, as fotos do cubo lançado na Coreia quebravam de novo, e ficamos indo e voltando, ajustando os dois cubos.

O CubeModel.cpp, que contém a classificação de cores, tem 230 limiares decimais, e o CubePhotoReconstructor.cpp, o arquivo de reconhecimento de fotos, tem 379. São valores escolhidos à mão para se ajustarem a algumas fotos cuja resposta era conhecida.

O código real desta etapa

Esta é a condição que separa o amarelo do branco. O amarelo precisa ter o componente verde parecido com o vermelho, e é isso que também o separa do laranja.

src/CubeModel.cppcpp · 12 linhas
// --- 강한/옅은 특징 ---
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 é a função que encontra o logotipo no centro branco do cubo oficial. Ela olha a proporção entre pontos claros e escuros.

src/CubePhotoReconstructor.cppcpp · 26 linhas
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;
}

O próximo post é "3. O que 54/54 realmente significava, a taxa real de reconhecimento e a distribuição".