André Alves de Lima

Talking about Software Development and more…

Como podemos comparar imagens no Windows Forms?

Um dia desses eu estava respondendo algumas questões nos fóruns da MSDN e me deparei com uma dúvida muito interessante. Basicamente o autor da pergunta estava querendo comparar as imagens de dois PictureBoxes, só que ele não estava conseguindo. O problema é que, mesmo as imagens sendo idênticas, a sua metodologia de comparação não estava funcionando. Neste artigo, vamos descobrir o porquê disso e veremos como comparar imagens no Windows Forms da maneira correta.

Entendendo o problema

Imagine a situação em que você tem dois PictureBoxes em um Form e você quer saber se a imagem dos dois é igual ou não. Para entendermos melhor, vamos construir um formulário de exemplo:

Como você pode ver, o formulário é extremamente simples. Ele contém dois PictureBoxes (pictureBox1 e pictureBox2) e um botão (compararImagensButton). Ao clicarmos no botão, analisaremos se as imagens são iguais ou diferentes.

Porém, antes de fazermos a comparação das imagens, vamos fazer o carregamento delas. Para testar a comparação, baixei dois arquivos PNG de exemplo deste site. Se você preferir, você pode baixar um arquivo zip com as duas imagens que eu utilizei aqui. Antes de prosseguir você deve colocar as duas imagens disponíveis no arquivo zip dentro do diretório “bin/debug” do projeto de exemplo:

Após ter extraído as imagens, vá até o construtor do formulário para fazermos o carregamento da mesma imagem (imagem1.png) nos dois PictureBoxes:

        public FormComparacaoImagens()
        {
            InitializeComponent();
            var imagem = Image.FromFile("imagem1.png");
            pictureBox1.Image = imagem;
            pictureBox2.Image = imagem;
        }

Feito isso, dê um duplo clique no botão “Comparar Imagens” para implementarmos o código de comparação das imagens. A primeira ideia que vem na cabeça é compará-las utilizando o operador “==” ou o método “Equals“:

        private void compararImagensButton_Click(object sender, EventArgs e)
        {
            if (ComparaImagem(pictureBox1.Image, pictureBox2.Image))
            {
                MessageBox.Show("As imagens são iguais.");
            }
            else
            {
                MessageBox.Show("As imagens são diferentes.");
            }
        }

        private bool ComparaImagem(Image imagem1, Image imagem2)
        {
            return imagem1 == imagem2 || imagem1.Equals(imagem2);
        }

Execute a aplicação de exemplo e clique no botão “Comparar Imagens“:

E o nosso método retorna que as imagens são iguais. Pronto, está tudo funcionando. Fim do artigo, certo? Errado! Esse método de comparação só funciona porque nós carregamos exatamente a mesma instância de “Image” nos dois PictureBoxes. Se alterarmos só um pouquinho a forma de carregamento, esse método deixará de funcionar:

        public FormComparacaoImagens()
        {
            InitializeComponent();
            pictureBox1.Image = Image.FromFile("imagem1.png");
            pictureBox2.Image = Image.FromFile("imagem1.png");
        }

Conseguiu notar a diferença no carregamento? Ao invés de utilizarmos a mesma instância como imagem dos dois PictureBoxes, nós carregamos duas vezes a imagem do arquivo. E, com isso, mesmo que o conteúdo das imagens dos PictureBoxes seja idêntico, o operador “==” e o método “Equals” entenderá que a imagem é diferente (pois eles comparam se as instâncias de “Image” são diferentes, e não o seu conteúdo).

Para resolver esse impasse, temos duas opções. Ou comparamos as imagens byte a byte ou comparamos as imagens pixel a pixel. Vamos ver a diferença entre essas duas metodologias?

Comparando byte a byte

O primeiro tipo de comparação é a comparação byte a byte. Não sei se você sabe, mas, uma imagem é representada no final das contas por uma cadeia de bytes (aliás, todas as variáveis no final das contas são cadeias de bytes). Dessa forma, se pegarmos cada um dos bytes da imagem e compararmos com os bytes da segunda imagem, conseguimos identificar se as imagens são iguais ou não.

Primeiramente, precisaremos de um método para pegar a representação das imagens em array de bytes. Esse método receberá uma instância de Image, criará um MemoryStream, carregará a imagem nesse MemoryStream e copiará os bytes da imagem em um array através do método Read do MemoryStream:

        private byte[] GetImageByteArray(Image image)
        {
            using (var stream = new System.IO.MemoryStream())
            {
                image.Save(stream, System.Drawing.Imaging.ImageFormat.Bmp);
                stream.Seek(0, System.IO.SeekOrigin.Begin);
                var byteArray = new byte[stream.Length];
                stream.Read(byteArray, 0, Convert.ToInt32(stream.Length));
                return byteArray;
            }
        }

Feito isso, para compararmos as duas imagens, basta pegarmos o byte array de cada uma delas e compará-los utilizando o método SequenceEqual da classe Enumerable:

        private bool ComparaImagem(Image imagem1, Image imagem2)
        {
            return Enumerable.SequenceEqual(GetImageByteArray(imagem1), GetImageByteArray(imagem2));
        }

E com isso, a comparação funcionará corretamente, retornando que as imagens são iguais quando o conteúdo delas é igual, independentemente se as instâncias de Image apontam para o mesmo lugar ou não.

Edit: o Ari C. Raimundo fez um comentário muito importante sobre essa metodologia de comparação. Ela não funciona caso as imagens tenham “o mesmo conteúdo” só que com formatos diferentes (por exemplo, a mesma imagem salva em PNG e BMP). Ele sugeriu uma metodologia de comparação utilizando BitmapData.Scan0, porém, eu não consegui fazer funcionar com o exemplo que eu encontrei. De qualquer forma, fica a observação que esse método só funciona caso as imagens realmente estejam vindo do mesmo arquivo. Caso essa não seja a sua realidade, a única alternativa que eu encontrei até agora foi fazer a comparação pixel a pixel (que você pode conferir abaixo).

Comparando pixel a pixel

Outra forma de detectarmos se o conteúdo de duas imagens é idêntico seria compararmos cada um dos pixels da imagem. Essa metodologia é bem parecida com a comparação byte a byte, uma vez que os pixels são representados por bytes no final das contas.

E como é que ficaria para compararmos as imagens dessa forma? Bom, primeiro vamos criar um método que compara os pixels de dois Bitmaps:

        private bool ComparaPixels(Bitmap imagem1, Bitmap imagem2)
        {
            if (imagem1.Width != imagem2.Width || imagem1.Height != imagem2.Height)
                return false;

            for (int x = 0; x < imagem1.Width; x++) 
            { 
                for (int y = 0; y < imagem1.Height; y++) 
                { 
                    if (imagem1.GetPixel(x, y) != imagem2.GetPixel(x, y)) 
                    { 
                        return false; 
                    } 
                } 
            }

            return true;
        }

Precisamos trabalhar com Bitmaps nesse caso porque somente essa classe possui o método GetPixel. Como você pode perceber, o método simplesmente detecta a altura e largura da imagem e percorre cada um dos eixos (x e y) comparando cada pixel de uma imagem com o mesmo pixel da outra. Ao encontrar um pixel diferente, retornamos “false“.

Finalmente, uma vez que temos o método “ComparaPixels“, podemos utilizá-lo no nosso método “ComparaImagem“:

        private bool ComparaImagem(Image imagem1, Image imagem2)
        {
            using (var bitmap1 = new Bitmap(imagem1))
            using (var bitmap2 = new Bitmap(imagem2))
            {
                return ComparaPixels(bitmap1, bitmap2);
            }
        }

Repare que precisamos converter as instâncias de Image em Bitmap. É importante que utilizemos a cláusula “using” ao fazermos isso, pois dessa forma a memória será automaticamente liberada logo após a chamada do método “ComparaPixels“.

Qual opção é melhor?

OK, agora você já sabe como comparar o conteúdo de duas imagens, seja comparando byte a byte ou comparando pixel a pixel. Mas, você deve estar se perguntando: qual é a melhor opção nesse caso?

Eu diria que, em questão de estética do código, tanto faz. Ambas as opções são fáceis de entender e o código está bem limpo e confiável. E em questão de performance?

Bom, nos testes que eu realizei comparando as duas metodologias, a comparação byte a byte foi muito mais rápida que a comparação pixel a pixel. Com as imagens de teste que eu disponibilizei para você, a comparação byte a byte demorou em média 31 milisegundos para executar, enquanto que a comparação pixel a pixel demorou em média 650 milisegundos para executar.

Dessa forma, a não ser que você tenha uma necessidade muito específica e realmente tenha que comparar a imagem pixel a pixel (se você tiver que comparar duas imagens em formatos diferentes – PNG e BMP, por exemplo), sugiro que você utilize a comparação byte a byte.

Concluindo

A comparação do conteúdo de imagens no Windows Forms não é uma tarefa tão trivial. Isso porque não podemos simplesmente utilizar os operadores “==” ou “Equals“, uma vez que eles comparam a instância das imagens e não o seu conteúdo.

Nesse artigo você aprendeu a comparar imagens no Windows Forms através de duas metodologias, bem como um pequeno estudo de qual das duas opções é a melhor.

E você, já precisou comparar duas imagens? Qual das duas metodologias você utilizou? Ou você utilizou uma outra metodologia completamente diferente? Conte para nós nos comentários.

Por fim, convido você a inscrever-se na minha newsletter. Ao fazer isso, você receberá um e-mail toda semana sobre o artigo publicado e ficará sabendo também em primeira mão sobre o artigo da próxima semana, além de receber dicas “bônus” que eu só compartilho por e-mail. Inscreva-se utilizando o formulário logo abaixo.

Até a próxima!

André Lima

Photo by John Lester used under Creative Commons
https://www.flickr.com/photos/pathfinderlinden/7155072088/

Newsletter do André Lima

* indicates required



Powered by MailChimp

11 thoughts on “Como podemos comparar imagens no Windows Forms?

  • Ari C. Raimundo disse:

    André,

    Não sei se estou enganado, mas a sua comparação byte a byte me parece que contém problemas.

    O que foi realizado no método GetImageByteArray na verdade foi transformar todo o arquivo binário que representa a imagem em um array de bytes. Isso não me parece correto, pois podemos ter imagens em formatos diferentes (TIF e BMP por exemplo) mas com dados semelhantes. Pior ainda, podemos ter duas imagens com o mesmo formato e dados semelhantes, e que devido a tags ou outras informações nos cabeçalhos, podem possuir diferentes arrays de bytes.

    Uma abordagem mais correta, na minha opinião, seria transformar os dados da imagem (via BitmapData.Scan0) em um array de bytes e então fazer a comparação.

    Eu também testaria a altura e a largura das imagens antes de converter as mesmas em array de bytes.

    Parabéns pelos seus posts, sempre bem interessantes.

    • andrealveslima disse:

      Olá Ari, obrigado pelo comentário, que foi muito pertinente, a propósito..

      Realmente você tem razão.. Se decidirmos comparar duas imagens com o mesmo conteúdo so que com formatos diferentes (PNG e BMP, por exemplo), o método GetImageByteArray vai, obviamente, retornar arrays diferentes e o resultado será que as imagens são diferentes.. Se nesse caso utilizarmos a comparação pixel a pixel, o resultado será que as imagens são iguais..

      Dei uma pesquisada nessa sua sugestão de utilizar o BitmapData.Scan0 e não consegui obter o resultado esperado (se as imagens têm o mesmo formato – PNG e PNG, por exemplo – o método retorna que as imagens são iguais, porém, se salvo a mesma imagem em BMP, o método retorna que as imagens são diferentes).. Me baseei neste artigo onde o autor usa o Scan0 para fazer a comparação:

      Fast Bitmap comparison – C#

      Você teria um exemplo de comparação de imagens (sem ser pixel a pixel) que funcione mesmo que as imagens tenham formatos diferentes?

      Vou editar o post colocando as suas observações que foram muito importantes.. Muito obrigado novamente!

      Grande abraço!
      André Lima

      • Ari C. Raimundo disse:

        André,

        Alguns problemas:

        1 – Você tem que comparar o array de bytes desconsiderando o padding da imagem (quando existir).

        2 – Temos que verificar a quantidade de canais (bytes per pixel).

        3 – Não podemos comparar formatos que possuem compactação com perda de qualidade (lossy) com formatos sem perda de qualidade (lossless). Por exemplo: JPEG e TIF, transformando o TIF em JPEG.

        Fiz algumas mudanças no código existente no link que você passou. Acredito que está OK, não cheguei a fazer muitos testes. Acho que é possível fazer a comparação em paralelo também para melhorar o desempenho.

        private byte[] GetImageByteArray(Image image)
        {
            Bitmap bmp = image as Bitmap;
            if (bmp == null)
                throw new ArgumentException("Invalid image", "image");
        
            Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
            BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, image.PixelFormat);
        
            int bytesPerPixel = Image.GetPixelFormatSize(image.PixelFormat) / 8;
        
            byte[] bytes = new byte[bmpData.Stride * bmp.Height];
            byte[] outputBytes = new byte[image.Width * image.Height * bytesPerPixel];
        
            Marshal.Copy(bmpData.Scan0, bytes, 0, bytes.Length);
        
            int padding = bmpData.Stride - image.Width * bytesPerPixel;
            int widthBytes = bmpData.Width * bytesPerPixel;
        
            int counter = 0;
            for (int i = 0; i &lt; bmpData.Height; ++i)
            {
                for (int j = 0; j &lt; widthBytes; ++j)
                    outputBytes[counter++] = bytes[i * widthBytes + j];
                counter += padding;
            }
        
            bmp.UnlockBits(bmpData);
            return outputBytes;
        }
        
        private static bool CompareBitmaps(Image left, Image right)
        {
            if (object.Equals(left, right))
                return true;
            if (left == null || right == null)
                return false;
            if (!left.Size.Equals(right.Size) || !left.PixelFormat.Equals(right.PixelFormat))
                return false;
        
            Bitmap leftBitmap = left as Bitmap;
            Bitmap rightBitmap = right as Bitmap;
            if (leftBitmap == null || rightBitmap == null)
                return false;
        
            byte[] leftBytes = GetImageByteArray(left);
            byte[] rightBytes = GetImageByteArray(right);
        
            // shouldn&#039;t happen
            if (leftBytes.Length != rightBytes.Length)
                return false;
        
            for (int i = 0; i &lt; leftBytes.Length; ++i)
            {
                if (leftBytes[i] != rightBytes[i])
                    return false;
            }
        
            return true;
        }
        

        Abraços.

        • andrealveslima disse:

          Olá Ari!

          Valeu pelo retorno.. Assim que for possível eu vou testar esse código que você enviou e te aviso se deu certo..

          Abraço!
          André Lima

        • andrealveslima disse:

          Olá Ari! Obrigado novamente!

          Testei o método que você enviou no seu último comentário utilizando algumas imagens de teste.. Realmente ele funciona bem melhor do que o método que eu sugeri originalmente no artigo.. Utilizando imagens png salvas por diferentes aplicativos (Paintbrush, Photoshop e PNG Gauntlet), o seu método retornou que as imagens são iguais, enquanto que o método que só compara o byte array retornou que as imagens são diferentes..

          E, como você disse, o método não consegue comparar imagens de formatos diferentes (png e jpg, por exemplo).. Acho que nesse caso nem é possível comparar mesmo, uma vez que as imagens realmente serão diferentes devido à perda de informações ocasionada pelo algoritmo de compressão..

          Grande abraço!
          André Lima

        • carolina disse:

          E se eu quero saber se uma imagem está contida em outra?
          Por exemplo, tenho uma logo e quero descobrir se ela foi usada em outras fotos!

  • Marcelo disse:

    Boa Tarde!

    Poderia me passar o código fonte completo?

    Obrigado.

    • andrealveslima disse:

      Olá Marcelo!

      Adicionei esse projeto na minha coleção de projetos para download, que está acessível para assinantes da minha newsletter.. Para receber o acesso a esse projeto (e a diversos outros relacionados aos artigos mais recentes), assine a minha newsletter..

      Abraço!
      André Lima

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *