8 pontos por GN⁺ 12 일 전 | 2 comentários | Compartilhar no WhatsApp
  • Ao converter cores inteiras de 8 bits para ponto flutuante, há diferença entre o método padrão de dividir por 255 e o método alternativo de dividir por 256 após um viés de 0,5
  • O método com 255 mapeia o inteiro 0 para 0.0 e 255 para 1.0, facilitando lidar diretamente com preto e branco, além de coincidir com a conversão UNORM-para-float da GPU
  • O método com 256 usa (img + 0.5) / 256.0 para colocar cada valor no centro do intervalo, o que pode simplificar o tratamento de bordas em tarefas como dithering, mas 0 deixa de ser 0.0, então a lógica de processamento fica presa à entrada de 8 bits
  • No método com 255, os intervalos das extremidades têm metade da largura, então ao arredondar de volta para 8 bits números aleatórios uniformes em [0, 1], os valores 0 e 255 aparecem com metade da frequência dos demais, mas a conversão de ida e volta de imagens reais continua funcionando sem perdas
  • Se você estiver processando imagens de terceiros, a resposta correta é normalizar por 255; o método com 256 só vale a pena se você controlar tanto o salvamento quanto o carregamento

Definição do problema

  • Em um programa que recebe uma imagem, converte para ponto flutuante, processa e depois salva novamente como cor de 8 bits, a questão é como fazer a conversão entre inteiro e ponto flutuante
  • Existem duas abordagens
    • Método padrão (divide por 255): pixels = img / 255.0 → processamento → output = np.trunc(result * 255 + 0.5)
    • Método alternativo (divide por 256): pixels = (img + 0.5) / 256.0 → processamento → output = np.trunc(result * 256)
    • Em ambos os casos, o valor é limitado a 0~255 antes da conversão final: output.clip(0, 255).astype(np.uint8)
  • O método padrão mapeia o inteiro 0 para 0.0 e 255 para 1.0, igual à conversão UNORM-para-float da GPU
  • O método alternativo adiciona um viés de 0,5, fazendo com que o inteiro 0 seja mapeado para 0.5/256 = 0.001953125
    • Por causa disso, sem conhecer essa constante, não é possível detectar pixels pretos
    • Mesmo usando cálculo em ponto flutuante, a lógica continua presa à entrada de 8 bits
    • No método padrão, sempre é possível assumir preto como 0.0

Objeções ao 255.0

  • Quando o método padrão é desenhado numa reta numérica, ele parece um pouco estranho
  • Existem bins menores nas duas extremidades

    • Os bins das extremidades da fórmula padrão se projetam para fora do intervalo [0,1], em uma forma "esticada"
    • Ao converter de volta de ponto flutuante para inteiro, a largura dos bins das extremidades é apenas metade da dos demais
      • Isso torna "mais difícil" para o algoritmo produzir valores extremos
      • Se você gerar ruído uniforme em [0,1] e arredondar com a fórmula padrão, os valores 0 e 255 ocorrem com metade da frequência dos outros inteiros
    • Um histograma de 1 milhão de números aleatórios uniformes confirma que os bins de 0 e 255 têm metade da altura dos demais
    • Ainda assim, é difícil imaginar uma situação prática em que esse viés de evitar extremos realmente seja um problema
      • A imagem original continua fazendo ida e volta sem perda (uint8 → float → uint8)
      • Resultados um pouco abaixo de 0.0 ou acima de 1.0 ainda são arredondados para o bin correto, igualando a distribuição de saída
      • Exemplo: se o processamento subtrai 0.005 da cor, no método padrão o preto cai abaixo de 0, enquanto no método alternativo permanece positivo, mas ambos acabam produzindo o inteiro 0 no final
  • Imprecisão

    • Os valores em ponto flutuante do método padrão não são exatos; por exemplo, 128/255.0 ≈ 0.501961, enquanto 128/256.0 = 0.5
    • O erro de arredondamento faz a distância entre valores em ponto flutuante variar ligeiramente, mas o erro é tão pequeno que não causa problema prático
      • Ponto flutuante de 32 bits tem mantissa de 23 bits, e o erro fica no nível do bit menos significativo, abaixo de 2⁻²³
      • Um erro relativo de 0,00001% é irrelevante até em processamento de imagem sofisticado; a imprecisão é um problema estético, não técnico
  • Valores que não pertencem ao intervalo inteiro

    • O método alternativo coloca cada valor em ponto flutuante exatamente no meio entre dois inteiros
      • Como o valor quantizado original não pode ser conhecido, o ponto médio entre dois inteiros consecutivos é um bom compromisso como estimativa
    • Há quem diga que isso facilita o dithering (post de blog de Andrew Kesler, de 2015, "Converting Color Depth")
      • Dá para adicionar ruído sem se preocupar com casos de borda
      • Já os valores extremos estranhos da fórmula padrão exigem cuidado extra para manter a consistência da distribuição do ruído

Dois tipos de quantizador

  • As duas abordagens podem ser vistas como dois tipos de quantizador escalar uniforme (uniform scalar quantizer)
  • Segundo o artigo da Wikipedia sobre quantização, quantizadores uniformes para dados de entrada com sinal são classificados em dois tipos
    • mid-tread: mapeia 0 para o nível de reconstrução de valor 0 (equivalente ao patamar da escada)
    • mid-riser: mapeia 0 para o limiar de classificação do valor 0 (equivalente ao espelho do degrau)
    • A Wikipedia cita como fonte o artigo de 1977 de Allen Gresho, "Quantization"
  • Fórmulas do quantizador (L é o número de níveis de saída, por exemplo 256)
    • Quantizador em escada mid-tread: codificação k = trunc(xL + 0.5), decodificação yₖ = k/L
    • Quantizador em escada mid-riser: codificação k = trunc(xL), decodificação yₖ = (k+0.5)/L
  • Aplicando aos dois métodos
    • Fórmula padrão = mid-tread (L=255)
    • Fórmula alternativa = mid-riser (L=256)
  • O método padrão usa mid-tread para entrada sem sinal com código L=255, uma combinação que não é ideal para entradas de 8 bits
    • Essa escolha foi feita pela conveniência de programação de mapear as extremidades para 0.0 e 1.0
  • Erro de quantização maior, mas na prática não

    • Se o sistema fosse codificar um número real x∈\[0,1\] distribuído uniformemente em um inteiro de 8 bits e depois reconstruí-lo como real, a fórmula padrão desperdiçaria faixa dinâmica
      • O intervalo representável no método padrão é [-0.5/255, 255.5/255], mais amplo do que o necessário para entradas em [0,1], aumentando o erro de reconstrução
      • Segundo o cálculo do usuário Peter Mudrievskij no StackOverflow, o erro absoluto médio é 1/1020 com divisor 255 e 1/1024 com divisor 256, portanto dividir por 256 é teoricamente um pouco mais preciso
    • Mas, na prática, não é isso que está sendo feito
      • A premissa é carregar uma imagem RGB de 8 bits, processá-la e salvá-la de novo; ao salvar, você não controla necessariamente o método de quantização, e a informação perdida desaparece para sempre
      • Se a imagem foi salva multiplicando e arredondando com a fórmula padrão, carregá-la dividindo por 256 não recupera precisão nenhuma
      • A alegação de menor erro de reconstrução só faz sentido quando você controla tanto o salvamento quanto o carregamento
    • Carregar a imagem de outra pessoa com a fórmula alternativa pode, na verdade, introduzir mais erro
      • É muito provável que ela tenha sido quantizada com a fórmula padrão, então decodificar em outra escala é teoricamente incorreto
      • Na prática, como cor não é uma medição absoluta, isso só significa processar em uma faixa um pouco menor com um pequeno deslocamento
    • Não se deve misturar etapas de codificação e decodificação dos dois quantizadores; isso gera código quebrado com facilidade

Conclusão

  • Se você estiver processando imagens fornecidas por outras pessoas, deve normalizar os valores RGB por 255
    • Valores em ponto flutuante imprecisos ou preocupações abstratas com erro de reconstrução não são bons motivos para escolher a alternativa
  • Se você controla totalmente o salvamento e o carregamento da imagem, não precisa mapear 0 para 0 e não se importa em vincular o código de processamento à faixa dinâmica de 8 bits, então dividir por 256 pode render um pequeno ganho de precisão
    • Só tenha em mente que um colega pode carregar a imagem com a fórmula padrão e arruinar o plano

Outras visões

2 comentários

 
GN⁺ 12 일 전
Comentários do Hacker News
  • O que exatamente os valores de cor significam em geral não muda muita coisa com 8 bits por componente. O erro gerado pela diferença entre usar 255 ou 256 no denominador é muito pequeno, e para perceber a diferença seria preciso ter boa sensibilidade a cores e olhar a tela bem de perto, além de que monitores e telas de celular normalmente nem estão calibrados
    Mas se você estiver gerando sinal VGA com um microcontrolador e só tiver 8 pinos de saída de cor (3 para vermelho, 3 para verde, 2 para azul), isso vira um problema bem chato. Nesse caso, o valor da cor é exatamente o nível de tensão de 0V~0.7V que precisa ser enviado ao monitor VGA
    O canal azul é mapeado como 0→0V, 1→0.23V, 2→0.47V, 3→0.7V, enquanto vermelho/verde são mapeados como 0→0V, 1→0.1V, …, 7→0.7V. Tirando as extremidades, as tensões do azul não coincidem em nada com as de vermelho/verde, então não dá para ver um cinza puro, e até a cor mais próxima acaba com um leve tom azulado ou amarelado, dependendo da direção da diferença
    Além disso, quase todos os gradientes que misturam azul com outros canais também parecem desalinhados. Por exemplo, as cores mais próximas na linha que vai do vermelho puro ao branco puro parecem um pouco alaranjadas ou arroxeadas
    O código para saída VGA colorida de 8 bits no Raspberry Pi Pico 2 com framebuffer duplo de 320x240 está aqui: https://github.com/moefh/pico-vga-8bit-demo

    • Lembro de quando era criança olhando para uma tela CRT com ruído e vendo linhas azuladas e amareladas bem fracas nas bordas. Sempre me perguntei por que justamente essas duas cores; se for a mesma causa, só agora entendi
    • Faltou considerar a correção de gama. Antes de converter um valor do intervalo 0~255 em tensão, o PC normalmente eleva esse valor à potência de 2.2
      Isso faz a diferença entre valores pequenos e grandes ficar muito mais acentuada: 2^2.2 = 4.595, 255^2.2 = 196,964.699
    • Para esse problema, dithering temporal parece ser a melhor opção. Modulação delta-sigma por pixel é algo relativamente fácil de fazer
      Se mudar a 30Hz, parece difícil para uma pessoa distinguir entre um leve tom azulado e um leve tom amarelado
    • Então talvez seja por isso que as cores RGBI eram tão comuns nos anos 80
  • Um argumento a favor de 255 é olhar para o caso extremo de uma imagem em preto e branco. Com um único bit, 0 é preto e 1 é branco
    Parece bastante claro que 0 deve mapear para 0.0 e 1 deve mapear para 1.0. Afinal, é preto e branco, não cinza claro (0.25) e cinza escuro (0.75). Ou seja, uma imagem em preto e branco é normalizada por 1, não por 2
    Com 2 bits, normalmente temos 0=preto, 1=cinza claro, 2=cinza escuro, 3=branco, então é natural mapear para 0.0, 0.33, 0.66, 1.0. Preto deve ser preto, branco deve ser branco, e os intervalos devem ser iguais, então a normalização é por 3
    Levando essa lógica até 8 bits, chegamos à normalização por 255. Mesmo que a diferença fique muito pequena com 8 bits, preto ainda deve ser 0.0 e branco deve ser 1.0
    A outra abordagem, usar normalização por 256 com 8 bits, faz a faixa de saída variar conforme o número de bits. Com 1 bit seria [0.25, 0.75], com 2 bits seria [0.125, 0.875], e assim por diante. Em geral, o que se quer ao aumentar o número de bits é mais nuance, não uma mudança no contraste

  • Foi um texto que realmente deu o que pensar e me fez reexaminar algumas premissas pessoais
    Vendo pela perspectiva da engenharia elétrica, é difícil concordar com a apresentação de “dois tipos de quantizadores” no texto. Matematicamente ela é rigorosa, mas não é uma explicação baseada em sistemas reais
    Em ADCs sempre existe, de forma inerente, uma incerteza de quantização de ±1/2 LSB. A característica de transferência é sempre uma amostragem mid-tread, ou pelo menos nunca vi um contraexemplo. Isso vale tanto para ADCs bipolares quanto unipolares
    O menor código corresponde à tensão negativa de referência e o maior código à tensão positiva de referência. O gráfico da característica de transferência mostra que, como no texto, as faixas máxima e mínima têm efetivamente largura de 1/2 LSB
    Em sistemas unipolares, não é possível representar exatamente as tensões intermediárias; em outras palavras, surge o problema do cinza. Em sistemas bipolares, 0V é o valor N/2 do mid-tread, mas isso não significa que existam “256 intervalos”
    Por isso vou continuar usando (VREF+ - VREF-) * k / (2^N - 1). Ou seja, concordo com a normalização por 255. No fim, isso é igual ao erro de contagem de postes de cerca: há N valores, mas N-1 intervalos. Se há menos intervalos que valores, então um intervalo precisa ser dividido entre dois valores, e é por isso que surgem intervalos de 1/2 LSB nas extremidades

    • Toda documentação de ADC que já vi diz que não é possível representar a escala cheia positiva. Por exemplo, em um ADC de 8 bits ±1V, -128 significa -1V, e +127 significa 127/128=0.99219V
      A transição de 126 para 127 acontece a um ponto que está a 1.5 LSB da escala positiva total. Uma diferença de 1 LSB significa 1/128=0.00781V, não 2/255=0.00784V
      Mas, na prática, se tensão e incerteza realmente importam, essa diferença quase nunca tem relevância. Há viés na tensão de referência e também erros de linearidade. 1 LSB não corresponde exatamente nem a 1/128 nem a 2/255, e acabam sendo necessários parâmetros de calibração
  • Isso se parece com a diferença, em computação científica, entre amostras centradas no nó e amostras centradas na célula, vista em uma dimensão. É preciso decidir se o valor está no meio do intervalo (ou do triângulo/tetraedro) ou na fronteira do intervalo (ou nos vértices do triângulo/tetraedro)
    Em computação científica, não faz sentido começar a processar dados sem saber como os valores devem ser interpretados. Em processamento de áudio também: se você só recebeu um fluxo de inteiros, precisa saber qual era a intenção de representação desses inteiros, por exemplo se era codificação mu-law ou linear, para poder calcular algo sobre o sinal original. Espera-se que os metadados associados aos valores tragam essa resposta
    Mas, com valores de pixel de 8 bits, se não houver metadados adequados no formato de arquivo para transmitir essa intenção de representação, fica-se à deriva e não existe resposta certa. Como diz o autor, não dá para criticar alguém por escolher o método que dá melhor resultado para seu próprio uso, mas vale apontar que bits sem contexto têm seu significado degradado

    • Isso me lembrou o valor de normalização usado na quantização das imagens de satélite Sentinel-2 level-2 da ESA
      É mais ou menos assim: o Número Digital DN=0 fica reservado como valor “NO_DATA”, e quando o DN está no intervalo [1; 1;215-1], o valor de refletância L2A SR é dado por L2A_SRi = (L2A_DNi + BOA_ADD_OFFSETi) / QUANTIFICATION_VALUE
      https://sentiwiki.copernicus.eu/web/s2-products
  • Há um erro em supor que existem 256 níveis de 0 a 255. Na verdade, há 256 valores que podem ser representados em 8 bits, e existem 255 intervalos entre 0 (preto) e 255 (branco puro)
    Portanto, dividir por 255 não é um problema. Claro, 128 não é exatamente um cinza de meio tom, e os valores quantizados de 8 bits de 0~255 quase sempre estão em sRGB, não em um espaço perceptual linear
    Em APIs modernas, uma confusão parecida também acontece ao lidar com posições de amostragem. Isso ocorre porque a posição é especificada como coordenada, não como centro do pixel

    • A API do BeOS usa o centro do pixel como referência. Hoje em dia ninguém mais vai se importar com isso, mas enfim
  • Algebricamente, a resposta é clara: f(x) -> [0, 255]
    Se f(n * 0) == n * f(0) não vale, coisas estranhas acontecem. Por exemplo, se f(x) -> [0, 255], então f(0) + f(0) + f(0) = 0 + 0 + 0 = 0 = f(0)
    Por outro lado, se f(x) -> [0.5/8, 7.5/8], então f(0) + f(0) + f(0) = 0.5/8 + 0.5/8 + 0.5/8 = 1.5/8 != f(0)
    Se você escolhe a segunda opção, não dá para esperar que um cálculo feito no lado de x e um cálculo feito no lado de f(x) coincidam entre si. Ou seja, a correspondência algébrica se rompe

  • Quero defender a solução do +0.5. Primeiro, não gosto dos intervalos de meia largura nas bordas; segundo, a representação baseada em 255 normalmente é de imagens SDR, não HDR
    Os valores RGB representam luminância para algum estado de adaptação, e o “0” de uma cena diurna não é “luminância 0”. É apenas cerca de 0,001 da região mais brilhante, e ainda são milhões de fótons, muito mais do que 0
    Em certo sentido, o olho percebe contraste em uma escala deslizante, e não existe um 0 absoluto no sistema. Por exemplo, historicamente os sistemas de transmissão usaram 16~235 como faixa de luminância SDR. Considero que a lógica de “precisa existir um 0” introduz viés, e acho que na maioria dos casos o 0 não é necessário

    • Do ponto de vista de quem já fez muito processamento e renderização de imagens para VFX, parece que você esqueceu que depois disso acontece uma conversão de espaço de cor. Em SDR antigo, isso seria para Rec.709 linearizado a partir de sRGB; em formatos mais novos, seria para uma gama mais ampla. Portanto, a compressão da faixa dinâmica acontece depois do carregamento
      Além disso, boa parte dos fluxos de trabalho de processamento e composição de imagens, certos ou errados, assume que 0 significa 0. Então, em 8 bits, muita gente considera que 0u mapeia para 0.0f, e 255 para 1.0f. Se um valor 0 em máscara ou alfa ficar um pouco acima de 0.0, algum código em algum lugar vai mascarar outras operações com um limiar rígido de 0.0, gerando artefatos. Por outro lado, se 255 em alfa deixa de ser 1.0f, então depois da pré-multiplicação o objeto fica ligeiramente transparente
      A mesma coisa pode acontecer quando, por causa do +0.5, 254 passa a ser 1.0f em mascaramento
    • O texto foca em RGB, mas o mesmo problema de quantização existe em todo tipo de sinal que é mapeado entre uma representação discreta e uma contínua
      O ponto principal não é representar 0 fótons, e sim maximizar a informação armazenada em 1 byte. Idealmente, você não deve usar menos o valor 0 do byte, nem adicionar viés aos dados que deveriam cair no bucket 0. Mesmo em um espaço de cor que vá de claro a muito claro, todos os bytes devem representar fatias de mesmo tamanho da faixa de brilho
    • O fato de os sistemas de transmissão terem usado historicamente 16~235 como faixa de luminância SDR é justamente o problema. Infelizmente, até o HDMI “moderno” ainda sofre com essa convenção esquisita, então, se display e fonte não entrarem em acordo, a imagem fica lavada ou os pretos ficam esmagados
    • As duas soluções somam 0.5. A diferença é em que parte do processo isso acontece
    • É uma ideia interessante, mas dá uma sensação de que o mundo saiu do eixo. Do ponto de vista do programa de processamento, o preto antigo (0.0) e o branco antigo (1.0) viram um cinza muito escuro e um cinza muito claro
  • Se a régua vai até 12 polegadas, então você deve normalizar pelo comprimento L, não pelo número de pontos na régua, que seria 13

    • Essa analogia me confunde. Não sei se a “régua” é uma régua de 255 polegadas com 256 pontos marcados de 0 a 255, ou uma régua de 256 polegadas com 256 intervalos de 1 polegada, de modo que L = 256×1
    • Se o que você realmente quer contar são postes de cerca, então o erro do poste de cerca não é erro nenhum
    • Certo, mas >> 8 é muito mais rápido
    • Quem decidiu que o número representa um ponto? Pode muito bem representar o intervalo entre pontos
    • Sou eu que estou sendo burro? O 0 não começa no ponto inicial?
  • Foi um texto prazeroso de ler por tratar de um tema em que eu não pensava havia um bom tempo. Fez lembrar momentos no desenvolvimento de jogos em que a lógica do jogo usava matemática de ponto flutuante, mas a pixel art precisava ser desenhada em coordenadas inteiras
    Em alguns lugares, tentamos algo parecido com +0.5 para fazer as coisas parecerem menos estranhas. Isso acontecia especialmente quando havia uma câmera em movimento, e a câmera também precisava ser truncada
    O texto de 2002 do Jonathan Blow linkado abaixo [1] também foi interessante. A visualização do primeiro texto ajuda bastante quando você aprofunda mais no assunto
    [1] https://web.archive.org/web/20240706043551/https://number-no...

 
GN⁺ 12 일 전
Opiniões no Lobste.rs
  • Parece bagunçado, mas está certo: o valor correto é 255
    Se isso não parecer intuitivo, dá para ver pelo caso degenerado de 2 bits. Quando os únicos valores inteiros possíveis são 0, 1, 2 e 3, se você calcular toda a conversão de inteiro→ponto flutuante, o resultado será 0.0, 0.33..., 0.66..., 1.0 para evitar o comportamento estranho em que preto/branco não são preto/branco ou em que os intervalos ficam claramente desiguais
    Portanto, a conversão inversa acaba sendo multiplicar por 3, não por 4 (2^2)
    • A primeira parte está certa, mas daí não se segue que “a conversão inversa deve multiplicar por 3, não por 4”
      A conversão inversa exige quantização (arredondamento), e esse é justamente o ponto central em que a simetria se quebra
      Se você criar um gradiente uniforme de números reais no intervalo 0..=1 e quantizá-lo para 0, 1, 2, 3, verá que multiplicar por 3 produz um resultado desigual. round() após ×3 super-representa 1 e 2, e floor ou ceil após ×3 tratam 0 ou 3 como singularidades, fazendo o gradiente parecer usar apenas 3 das 4 cores
      A lógica de /3 e ×3 parece aceitável ao converter números exatos em ida e volta, mas os valores intermediários são muito afetados pela escolha do arredondamento, e isso passa a importar assim que você começa a processar os dados
      As proporções inteiras só ficam uniformes ao multiplicar por (4-ε) e aplicar floor, o que equivale a ×4, floor() e clamp(). Parece um erro estranho de diferença de 1 ou de ε, mas intuitivamente é a solução com melhor aparência
  • O título me confundiu bastante. Não sei se foi de propósito, mas no fim das contas parece ser algo como “0..1 corresponde a [0..255.0] ou a [0.5..255.5]?”
    Para mim a resposta sempre foi “obviamente” [0.0..255.0], mas talvez isso não seja óbvio para todo mundo
    O texto diz que os intervalos “extremos” têm só metade da capacidade dos outros, mas também não acho esse enquadramento correto
    Se não existem valores fora de [0..1], o fato de parecer um intervalo mais estreito é um artefato de renderização. Ele só foi renderizado mais estreito porque você cortou os buckets sabendo que não existem valores fora do intervalo
    Por outro lado, se existem valores fora de [0..1], esse intervalo é infinito. O texto reconhece o segundo caso, mas não o primeiro
    No momento em que se aceita o primeiro, o comportamento correto parece claro, mas o simples fato de este texto existir também mostra objetivamente que não é uma questão “clara” :D
    • Se 0…255.0 é mesmo tão óbvio, então qual faixa de valores de ponto flutuante deveria voltar para o inteiro 0, e qual deveria voltar para o inteiro 255?
      Se 0..<1 vai para o inteiro 0, e 254>..255.0 vai para o inteiro 255, então o 128 some do mapa. Você provavelmente quer que 127.5..128.5 vá para 128, mas então para onde essas metades deveriam ir?
      Se você deslocar tudo um pouco para acertar o 128, então 0..0.99609375 será mapeado para o inteiro 0
  • A abordagem padrão também parece ter surgido porque as pessoas naturalmente acabam chamando round()
    Como essa forma parece bastante natural para as pessoas, provavelmente virou padrão por simplicidade
  • Fico me perguntando se a abordagem oposta ao que se queria obter com 256 também teria utilidade. Ou seja, mandar 0.0 para 0, 1.0 para 255 e mapear os outros valores de ponto flutuante para 1 até 254
    uint8_t output = 0.0f >= result  
                     ? 0  
                     : 1.0f <= result  
                     ? 255  
                     : 1 + 253*result;  
    
    Seria bom se, mesmo durante o processamento, preto continuasse preto e branco continuasse branco
    • Assim, 0 e 255 acabam ficando com uma fatia maior do intervalo unitário do que os outros números. Aproximadamente 0,8%, ou seja, 255/253
  • A primeira imagem aparece quebrada no meu ambiente
    • Autor do texto aqui. Você quer dizer que o arquivo de imagem está corrompido? Eu o comprimi com pngcrush. Ou quer dizer que há algo errado com o conteúdo da imagem?