Neste exercício eu mostro algumas formas de criar variações de uma cor específica.

Durante a pesquisa, pude perceber que a teoria de cores é vasta. Não é minha intenção aprofundar nesse tema, mas fiquei surpreso com a quantidade de tópicos sobre o assunto, sistemas, métricas, modelos e matemática que está envolvida na percepção e quantificação das cores. Minha surpresa, em parte, é fruto do meu pouquíssimo conhecimento. As cores são estudadas nas mais diversas áreas como medicina, psicologia, design, propaganda, tecnologia, etc.

Existem modelos de representação que são mais apropriados para as telas de dispositivos eletrônicos, modelos para as tintas ciano, magenta, amarelo e preto das impressoras, modelos que mapeiam a percepção humana, e muitos outros.

A ideia para este exercício nasceu de um outro projeto que estou desenvolvendo. Trata-se do treinamento de uma rede neural artificial para reconhecimento de cores. Algo como entregar para a rede os valores de RGB de uma cor ela ser capaz de classificar como amarela, verde, rosa, vermelha, etc. Acontece que não encontrei na internet nenhuma base de treinamento que se adequasse ao que eu queria fazer e comecei a criar a minha própria. O trabalho foi bem mais difícil do que eu imaginava e decidi criar este artigo mostrando um pouco o que encontrei sobre cores.

Neste trabalho, disponibilizo apenas trechos do código fonte que gera as variações de cores. Como o código faz parte parte de um trabalho maior, ainda está bastante desorganizado e preferi não incluir o projeto no GitHub. Para fazer a conversão entre sistemas de cores, utilizei a biblioteca para Commons Imaging (https://commons.apache.org/proper/commons-imaging/), para a linguagem Java.

Definição do problema

A ideia é gerar variações de uma cor de modo que as cores resultantes ainda possam ser caracterizadas como a cor inicial. Por exemplo, pegar um tom que possamos chamar de vermelho e gerar variações e ainda chamá-las de vermelho.

Não é uma tarefa tão trivial. A percepção humana de cor, quando é muito sutil, entra em um nível de subjetividade que pode ser difícil de traduzir em termos computacionais.

Na imagem abaixo, podemos chamar todas essas cores de amarelo, ou a mais clara só pode ser chamada de bege? O tom mais escuro ainda é amarelo ou já entrou no tom de cor marrom?

Tons de cor amarela

São essas pecepções que tornam esse problema muito interessante de ser resolvido computacionalmente.

Existe uma condição da visão humana chamada daltonismo. Pessoas que possuem essa condição tem dificuldade em diferenciar cores, principalmente tons de vermelho ou verde. Esta condição tem origem em fatores genéticos, hereditários, mas podendo aconecer também por lesões na região dos olhos, por exemplo. No daltonismo, as células da retina responsáveis por perceber a tonalidade da cor fazem o trabalho de forma diferente da maioria das pessoas. O daltonismo não está associado com outros problemas além da capacidade de distinguir cores.

Abaixo, as cores de referência para as quais vou gerar variações.

Bege, preto, azul, marrom, cinza, verde, laranja, rosa, roxo, vermelho, azul e amarelo

Variando o RGB

O RGB é um sistema de cores em que as definições para formar uma cor são originadas na percepção humana da luz. Exergamos as cores após a luz estimular células sensíveis localizadas no fundo dos olhos. Dependendo da cor (em termos físicos, a cor é radiação eletromagnética em uma faixa de comprimento de onda específico), as céclulas são estimuladas mais ou menos, e há diferentes células para diferentes cores. Essas células conseguem gerar diferentes níveis de sinais elétricos para o cérebro, que interpreta como as cores que vemos.

Assim, quando o olho recebe luz na faixa de comprimento de onda que consideramos o verde e também a luz que consideramos vermelha, o cérebro interpreta isso como a cor amarela. Por isso o RGB é considerado um sistema aditivo, pois as cores se sobrepõem para formar outras. As letras R, G e B vem do inglês e significam red, green e blue (vermelho, verde, azul) e são precisamente as três intensidades de cor que mais estimulam nossos olhos. Telas eletrônicas funcionam baseando-se nesse princípio. Cada ponto de luz da tela é composto por 3 minúsculos LEDs, vermelho, verde e azul. Para gerar a cor, cada um desses LEDs acende em um nível específico.

A imagem abaixo ilustra a lógica do RGB. Nela, os canais de cor vermelha e verde estão no máximo, enquanto o canal azul está no mínimo. Isso faz gerar a cor amarela. A imagem ainda mostra que a variação do canal vermelho varia a cor entre verde e amarelo. Variando o canal verde, a cor muda de vermelho para amarelo. E, por fim, na variação do canal azul a cor vai do amarelo para o branco.

Para quem trabalha com programação de computadores, especialmente front-end, e para pessoas que trabalham em alguma área relacionada a criação artística assistida por computador, o sistema de cores RGB deve ser bastante comum. Boa parte da definição de cores em um aplicativo web ou aplicativo de celular é realizada com códigos RGB.

Algumas variações do RGB, especialmente as utilizados para mapear cores para dispositivos eletrônicos, como TVs, tablets, celulares, etc, usa um mecanismo não linear. Isto é, uma variação de 10 unidades, digamos, de 40 para 50, e de 50 para 60 na cor vermelha, verde, ou azul, não é percebida na mesma intensidade nos dois intervalos, mesmo as variações deles sendo iguais em valor.

fun generateRgbVariations(color: Color, maxVariants: Int): List<Color> {
    val range = ceil(cbrt(maxVariants.toDouble())).toInt()
    val rangeHalfLeft = if (range % 2 == 1) (range / 2) + 1 else range / 2
    val rangeHalfRight = range / 2
    val startRed = (color.red - rangeHalfLeft)
        .let { max(0, it) }
        .let { it - if (color.red + rangeHalfRight > 255) color.red + rangeHalfRight - 255 else 0 }
    val endRed = (color.red + rangeHalfRight)
        .let { min(255, it) }
        .let { it - if (color.red - rangeHalfLeft < 0) color.red - rangeHalfLeft else 0 }
    val startGreen = (color.green - rangeHalfLeft)
        .let { max(0, it) }
        .let { it - if (color.green + rangeHalfRight > 255) color.green + rangeHalfRight - 255 else 0 }
    val endGreen = (color.green + rangeHalfRight)
        .let { min(255, it) }
        .let { it - if (color.green - rangeHalfLeft < 0) color.green - rangeHalfLeft else 0 }
    val startBlue = (color.blue - rangeHalfLeft)
        .let { max(0, it) }
        .let { it - if (color.blue + rangeHalfRight > 255) color.blue + rangeHalfRight - 255 else 0 }
    val endBlue = (color.blue + rangeHalfRight)
        .let { min(255, it) }
        .let { it - if (color.blue - rangeHalfLeft < 0) color.blue - rangeHalfLeft else 0 }
    val variants = mutableListOf<Color>()
    for (red in startRed..endRed) {
        for (green in startGreen..endGreen) {
            for (blue in startBlue..endBlue) {
                variants += Color(red, green, blue)
            }
        }
    }
    return variants
}

Nas imagens a seguir, todas os quadradinhos tem uma cor diferente. Talvez as diferenças não sejas perceptíveis dependendo da iluminação da tela, da qualidade e da percepção de quem está lendo.

Para variar as cores nesse sistema, é possível variar cada um dos canais que formam a cor, para baixo e para cima, não se afastando muito da cor original. Variando cerca de 6 unidades para baixo e 6 para cima em cada um dos canais RGB, o resultado é apresentado a seguir.

Variando os canais RGB cerca de 30 unidades para mais e para menos, os resultados são mostrados a seguir. As cores variam bem mais. Alguns tons da cor branca com certeza não podem mais ser chamados de branco. Na verdade, as variações de branco e bege são bem parecidas.

Variando o HSV

O HSV é uma representação alternativa para o RGB. Nesse sistema, cada valor de H corresponde uma gradação de uma cor em seu estado mais puro. Como uma variação do RGB, suas cores primárias ainda são o vermelho, verde e azul. No entanto, o modelo HSV simula a percepção humana natural para diferenciação de tonalidade. Ele também é um modelo não linear.

Imagine um pouco de tinta branca sendo adicionada em uma tinta vermelha. Dependendo das quantidades, o resultado estará se aproximando de uma cor que podemos chamar de rosado. Um pensamento semelhante pode ser adotado para adição de tinta preta. O sistema de cores HSV é uma representação mais intuitiva para as cores, baseado um pouco mais na percepção que temos ao misturar tintas, por exemplo.

Para cada cor H, o HSV mapeia a saturação S e o brilho V, variando ambos em termos percentuais, entre 0% e 100%. Uma cor com 100% de saturação está em seu estado mais puro, enquanto que a saturação em 0% deixa a cor acinzentada. E sobre a cor também aplica-se o brilho V. Uma com zero brilho vai estar preta, enquanto com o brilho máximo vai estar com os valores de H e S totalmente visíveis.

No HSV, as mesmas cores do RGB são dispostas em um círculo. A seleção de uma combinação específica é feita ao escolher um angulo no círculo da imagem abaixo. Esse ângulo é o valor de H. Sobre esse valor, são aplicados a saturação e o brilho.

Na imagem de cima, a cor vermelha com saturação de 100%, 75%, 50%, 25% e 0%. Na imagem de baixo, com luminosidade de 100%, 75%, 50%, 25% e 0%

Logo, não é muito difícil perceber que para gerar variações a partir de um sistema HSV, podemos também variar os valores de H, S e V. Mantendo o valor de H e variando S e V, manteremos a característica fundamental da cor e estaremos variando a saturação e o brilho dela. Não podemos adicionar ou subtrair muito brilho, sob risco de descaracterizar a cor à nossa percepção, deixando-a extremamente clara ou escura. Também possível variar um pouco o ângulo de H e repetir as variações de S e V.

fun generateHsvVariations(color: Color, maxVariants: Int): List<Color> {
    val baseHsv = ColorConversions.convertRGBtoHSV(color.rgb)
    val variants = TreeSet<Color> { o1, o2 -> o1.rgb.compareTo(o2.rgb) }
    val hMaxDiff = 3.00
    val sMaxDiff = 0.15
    val vMaxDiff = 0.1
    var hIncrement = 0.0
    do {
        for (hSign in listOf(-1, 1)) {
            val h = (baseHsv.H + hIncrement * hSign)
                .let { if (it > 360.0) it - 360 else it }
                .let { if (it < 0) 360.0 + it else it }
            var sIncrement = 0.000
            do {
                for (sSign in listOf(-1, 1)) {
                    val s = baseHsv.S + sIncrement * sSign
                    if (s < 0.0 || s > 1.0) continue
                    if (s < baseHsv.S - vMaxDiff || s > baseHsv.S + vMaxDiff) continue
                    var vIncrement = 0.000
                    do {
                        for (vSign in listOf(-1, 1)) {
                            val v = baseHsv.V + vIncrement * vSign
                            if (v < 0.0 || v > 1.0) continue
                            if (v < baseHsv.V - vMaxDiff || v > baseHsv.V + vMaxDiff) continue
                            val variant = Color(ColorConversions.convertHSVtoRGB(h, s, v))
                            if (variants.contains(variant)) continue
                            variants += variant
                        }
                        vIncrement += 0.001
                    } while (
                        variants.size < maxVariants
                        && (baseHsv.V - vIncrement >= 0.0 || baseHsv.V + vIncrement <= 1.0)
                        && vIncrement < vMaxDiff
                    )
                }
                sIncrement += 0.001
            } while (
                variants.size < maxVariants
                && (baseHsv.S - sIncrement >= 0.0 || baseHsv.S + sIncrement <= 1.0)
                && sIncrement < sMaxDiff
            )
        }
        hIncrement += 0.0125
    } while (variants.size < maxVariants && hIncrement < hMaxDiff)
    return variants.toList()
}

As imagens abaixo mostram as primeiras 1225 variações usando a estratégia descrita.

Já as imagens abaixo apresentam uma amostra aleatória de 1225 cores selecionadas dentre 60000 cores.

Variando o CIELab

O sistema de cores Lab foi criado pela Comissão Internacional de Iluminação – CIE – e faz um mapeamento matemático da percepção de cores que observamos. Ele é linear, fazendo que variações nos valores do modelo sejam percebidas de maneira igual pelos olhos.

O modelo CIELab é uma variação do modelo CIEXYZ, com objetivo de deixar os valores mais adequados à visão humana das cores. A base para o modelo CIE XYZ veio de experimentos de correspondência de cores realizados no final do século 19 e início do século 20. Nesses experimentos, os observadores ajustavam a intensidade de três cores primárias diferentes para fazer com que a cor resultante correspondesse, aos seus olhos, a uma cor de referência.

Para gerar as variações, a diferença entre uma cor e outra foi calculada usando a distância euclidiana, onde os valores L, a e b são as dimensões. Como o CIELab é um modelo linear, o cálculo de distância entre cores com diferentes níveis de níveis de cores primárias pode ser realizado.

fun generateCieLabVariations(color: Color, maxVariants: Int): List<Color> {
    val baseLabColor = ColorConversions.convertXYZtoCIELab(ColorConversions.convertRGBtoXYZ(color.rgb))
    fun calculateDistance(l: Double, a: Double, b: Double): Int {
        return sqrt(
            (baseLabColor.L - l).pow(2) +
                    (baseLabColor.a - a).pow(2) +
                    (baseLabColor.b - b).pow(2)
        ).times(1000000).toInt()
    }

    data class Variant(val variantColor: Color, val distance: Int) : Comparable<Variant> {
        override fun compareTo(other: Variant): Int {
            if(this.variantColor.rgb == other.variantColor.rgb) return 0
            return distance.compareTo(other.distance)
        }
    }

    val variants = TreeSet<Variant>()
    variants += Variant(color, 0)
    for (r in 0..255) {
        for (g in 0..255) {
            for (b in 0..255) {
                val variantColor = Color(r, g, b)
                val variantLab = ColorConversions.convertXYZtoCIELab(ColorConversions.convertRGBtoXYZ(variantColor.rgb))
                val distance = calculateDistance(variantLab.L, variantLab.a, variantLab.b)
                val variant = Variant(variantColor, distance)
                if (variants.size < maxVariants) {
                    variants += variant
                } else if (variants.higher(variant) != null) {
                    variants.pollLast()
                    variants += variant
                }
            }
        }
    }
    return variants.map { it.variantColor }
}

Sem dúvida, os melhores resultados foram com o modelo de cores CIELab. Cada uma das imagens a seguir é formada com 1225 variações de cores mais próximas da cor original.

As imagens a seguir mostram uma seleção aleatória de 1225 cores entre as 60000 cores mais próximas da cor de referência. Com exceção das cores preta, branco e cinza, todas as outras mantiveram tonalidades que poderiam, sem muito esforço, serem chamadas com o mesmo nome da cor original. Branco e preto estão nos limites, onde qualquer adição nelas pode provocar um diferença perceptível, além do fato delas só conseguirem se diferenciar indo par um um único lado. No caso da cor cinza, imagino que os motivos sejam semelhantes. O cinza é uma tonalidade intermediária entre preto e branco e alterar qualquer um dos valores adiciona algum nível de cor vermelha, verde, amarela, etc, fazendo com que o cinza deixe de se parecer com cinza.

Conclusão

Sem dúvidas o modelo CIELab foi o mais interessante do ponto de vista da percepção humana. Isso faz bastante sentido, pois o o modelo foi criado baseado nessa necessidade. O simples cálculo da disância euclidiana permite a criação de vários milhares de cores variantes sem que elas percam a característica original.

Teoria de cores e ciência de cores é uma área de estudo bem vasta. Com certeza realizar essa atividade foi de grande valor como introdução nesses conceitos.

Com este artigo quase concluído, senti falta de fazer os experimentos com o modelo de cores CMYK, que usa 4 cores e é bastante usada por impressoras. Buscar maneiras de gerar variações de cores seria um trabalho interessante e, talvez, resultasse no entendimento de como impressoras geram cores a partir dessas 4 tintas. Isso vai ficar para futuros trabalhos.

O próximo passo é criar uma conjunto de dados de cores e usar para treinar a rede neural. Agora com todos esse conhecimento, uma atividade interessante para se fazer seja treinar a rede com os dados gerados por cada um desses modelos, ou uma mistura deles, e compara os resultados.

Referências

A tonalidade das cores de referência foram obtidas no site da Adobe.

Categorized in:

Ideias de Programação,

Last Update: 19/01/2024