Imagens podem ser entendidas como sinais bidimensionais (ou tridimensionais no caso de volumes 3D).
Pensando no caso de imagens digitais, vamos representá-las como um matriz de pixels representando o brilho de uma determinada região.
Para incluir informação de cor, cada pixel passa a ser representada por uma tupla de 3 valores RGB que representam a componente vermelha, verde e azul, respectivamente.
Ate aí, nada fora do usual. Contudo, isso torna a vida de implementar bibliotecas em processamento de imagens com suporte a cor um inferno na terra um pouco inconveniente, nos obrigando a lidar com um mar de cópias e conversões desnecessárias de imagens, sempre tendo que tratar casos com cor e sem cor.
Mas tem um truque interessante aplicado nos primordios da transmissao de sinais de tv a cores que pode nos ser útil. Na era mezozoica em que as pessoas dividiam espaço com os terriveis dinossauros, elas sofriam com polio e TVs sem cores. Obviamente, o problema das TVs a cores foi atacado primeiro, e com seu desenvolvimento surgiu um problema: o sistema de transmissao de TV analógico (não temos pixels aqui, mas a ideia segue) é contruído em torno de sistemas de vídeo sem cores, que contém apenas a informação de brilho. Como podemos adaptar os sistemas de transmissão, para enviar informações de cor, enquanto mantemos compatibilidade com sistemas sem cor?
A resposta encontrada foi o uso do espaço de cores YCbCr, definido por nossos amigos da NTSC, um sistema de cores onde separamos as informações de luminância (Y) e crominância (Cb e Cr). De forma que aparelhos antigos pudessem interpretar a informação de luminância e ignorar a informação de croma, mantendo-se acessivel a TVs preto e branco.
Embora possamos usar YCbCr nas implementações futuras, pretendo usar Lab, que tambem possui a característica interessante de separar luminância e crominância, esse formato ainda se propõe a ser um formato onde variações numericas correspondam a percepção de mudanca de cor percebida por meros humanos.
O espaço Lab é composto por luminância (L) que representa a informação de brilho que é representada num dominio de [0,100], e as componentes de cor (ab) que representam a mudanca entre verde a vermelho (a) r azul a amarelo (b) ambas representadas num domínio de [-128, 128].
Mão na massa
Para podermos converter nossas imagens, normalmente representadas num espaço RGB (ou sRGB pra ser mais preciso), vamos precisar introduzir o espaço de cores XYZ (sim, temos mais espaços de cores do que pode imaginar). A ideia é que XYZ é um espaço de cores extremamente abrangente e que consegue abrigar todas as cores visíveis, o que acaba fazendo desse formato uma excelente opção de formato intermediário de conversão entre todos os demais formatos. Com isso ele será o nosso comutador de cores, e pra incluir conversão de cores para qualquer novo formato, nos basta implementar uma opção de conversão de/para o formato XYZ.
Então, nosso primeiro passo é converter nossa entrada RGB para o espaço XYZ, que consiste de três operações básicas:
- Escalarmos nosso valor RGB de tal forma que cada componente fique dentro do dominio [0,1], usualmente a representação de RGB vai depender do numero de bits útilizado na representação de cores da imagem ([0,256) ou [0,65536) para imagens de 8 ou 16 bits).
- Uma correção de gamma que é responsável por resolver alguns problemas de não linearidade na representação usual de cores.
- A conversão de fato, que consiste de uma transformação linear para o espaço XYZ.
proc rgb2xyz(red, green, blue, max: real = 255.0): (real, real, real) {
var r: real = red / max;
var g: real = green / max;
var b: real = blue / max;
// gamma correction
r = if r < 0.04045 then r / 12.92 else ((r + 0.055) / 1.055)**2.4;
g = if g < 0.04045 then g / 12.92 else ((g + 0.055) / 1.055)**2.4;
b = if b < 0.04045 then b / 12.92 else ((b + 0.055) / 1.055)**2.4;
// convert xyz
const x = 0.4124564 * r + 0.3575761 * g + 0.1804375 * b;
const y = 0.2126729 * r + 0.7151522 * g + 0.072175 * b;
const z = 0.0193339 * r + 0.119192 * g + 0.9503041 * b;
return (x, y, z);
}
A conversão entre XYZ e Lab é bastante similar e consiste de 3 passos:
- Escalarmos nosso valor, considerando um determinado ponto de referencia para o branco (usualmente D65 que representa o branco percebido em ambientes de luz natural).
- Aplicarmos uma função \(f(x)=\cases{\sqrt[3]{x} & if x>(216/24389) \\ { { (24389/27) x + 16} \over {116}} & \text{otherwise}}\) que aplica a transformação responsável por aproximar o espaço de representação da percepção humana.
- A conversão que consiste de uma transformação para o espaço Lab.
proc xyz2lab(x, y, z): (real, real, real) {
// reference white point (D65)
const wx = 0.95047;
const wy = 1.00000;
const wz = 1.08883;
var fx = x / wx;
var fy = y / wy;
var fz = z / wz;
const epsilon = 216.0/24389.0;
const kappa = 24389.0/27.0;
fx = if fx > epsilon then cbrt(fx) else (kappa * fx + 16.0) / 116.0;
fy = if fy > epsilon then cbrt(fy) else (kappa * fy + 16.0) / 116.0;
fz = if fz > epsilon then cbrt(fz) else (kappa * fz + 16.0) / 116.0;
const l = (116.0 * fy) - 16.0; // [0,100]
const a = 500.0 * (fx - fy); // [-128, 128]
const b = 200.0 * (fy - fz); // [-128, 128]
return (l, a, b);
}
A operação inversa, a qual vamos precisar para salvar nossas imagens de volta no espaço RGB, consiste simplesmente da inversa das mesmas funções.
proc lab2xyz(l: real, a: real, b: real): (real, real, real) {
//white reference
const wx = 0.95047;
const wy = 1.00000;
const wz = 1.08883;
var fy = (16.0 + l) / 116.0;
var fx = fy + (a / 500.0);
var fz = fy - (b / 200.0);
const kappa = 24389.0/27.0;
const epsilon = 216.0/24389.0;
const delta = cbrt(epsilon);
fx = if fx > delta then fx ** 3.0 else (116.0 * fx - 16.0) / kappa;
fy = if fy > delta then fy ** 3.0 else (116.0 * fy - 16.0) / kappa;
fz = if fz > delta then fz ** 3.0 else (116.0 * fz - 16.0) / kappa;
return (fx * wx, fy * wy, fz * wz);
}
proc xyz2rgb(x: real, y: real, z: real, maxval: real = 255.0): (real, real, real) {
var r: real = 3.2406 * x - 1.5372 * y - 0.4986 * z;
var g: real = -0.9689 * x + 1.8758 * y + 0.0415 * z;
var b: real = 0.0557 * x - 0.2040 * y + 1.0570 * z;
// gamma correction
r = if r < 0.0031308 then r * 12.92 else 1.055 * (r ** (1/2.4) - 0.055);
g = if g < 0.0031308 then g * 12.92 else 1.055 * (g ** (1/2.4) - 0.055);
b = if b < 0.0031308 then b * 12.92 else 1.055 * (b ** (1/2.4) - 0.055);
return (r*maxval, g*maxval, b*maxval);
}
E é isso aí. Caso queira implementar por conta, recomendo o colorizer como uma referencia muito útil para os testes.
Comment