cs0815
cs0815

Reputation: 17388

distance/similarity between 2 bitmap images in C#

I came across the following code taken from here:

using System;
using System.Drawing;

class Program
{
    static void Main()
    {
        Bitmap img1 = new Bitmap("Lenna50.jpg");
        Bitmap img2 = new Bitmap("Lenna100.jpg");

        if (img1.Size != img2.Size)
        {
            Console.Error.WriteLine("Images are of different sizes");
            return;
        }

        float diff = 0;

        for (int y = 0; y < img1.Height; y++)
        {
            for (int x = 0; x < img1.Width; x++)
            {
                diff += (float)Math.Abs(img1.GetPixel(x, y).R - img2.GetPixel(x, y).R) / 255;
                diff += (float)Math.Abs(img1.GetPixel(x, y).G - img2.GetPixel(x, y).G) / 255;
                diff += (float)Math.Abs(img1.GetPixel(x, y).B - img2.GetPixel(x, y).B) / 255;
            }
        }

        Console.WriteLine("diff: {0} %", 100 * diff / (img1.Width * img1.Height * 3));
    }
}

Unfortunately, this is really slow. Is anyone aware of a faster way of calculating the distance between 2 images? Thanks!

To provide some more context as well. I am working on something like this:

https://rogerjohansson.blog/2008/12/07/genetic-programming-evolution-of-mona-lisa/

I evolve SVGs which are then translated into a Bitmap and compared to the target image.

Just came across the aforgenet library - see for example:

enter link description here

PS:

I start to rewrite the above using LockBits. The code below is my current attempt but I am a bit stuck. Please note, that bmp1 is the 'target picture' and does not really change - hence the copying can be factored out/only needs to be done once. The Bitmap bmp2 is passed in and to be compared with bmp1 (there are 100s of bmp2s). Ultimately, I would like to determine the similarity between bmp1 and bmp2 using some distance (e.g. Euclidean distance of bytes?). Any pointers regarding this and how to speed the code up would be very much appreciated. Thanks!

public double Compare(Bitmap bmp1, Bitmap bmp2)
{
    BitmapData bitmapData1 = bmp1.LockBits(new Rectangle(0, 0, bmp1.Width, bmp1.Height), ImageLockMode.ReadWrite, bmp1.PixelFormat);
    BitmapData bitmapData2 = bmp2.LockBits(new Rectangle(0, 0, bmp2.Width, bmp2.Height), ImageLockMode.ReadWrite, bmp2.PixelFormat);

    IntPtr ptr1 = bitmapData1.Scan0;
    int bytes1 = bitmapData1.Stride * bmp1.Height;
    byte[] rgbValues1 = new byte[bytes1];
    byte[] r1 = new byte[bytes1 / 3];
    byte[] g1 = new byte[bytes1 / 3];
    byte[] b1 = new byte[bytes1 / 3];
    Marshal.Copy(ptr1, rgbValues1, 0, bytes1);
    bmp1.UnlockBits(bitmapData1);

    IntPtr ptr2 = bitmapData2.Scan0;
    int bytes2 = bitmapData2.Stride * bmp2.Height;
    byte[] rgbValues2 = new byte[bytes2];
    byte[] r2 = new byte[bytes2 / 3];
    byte[] g2 = new byte[bytes2 / 3];
    byte[] b2 = new byte[bytes2 / 3];
    Marshal.Copy(ptr2, rgbValues2, 0, bytes2);
    bmp2.UnlockBits(bitmapData2);

    int stride = bitmapData1.Stride;
    for (int column = 0; column < bitmapData1.Height; column++)
    {
        for (int row = 0; row < bitmapData1.Width; row++)
        {
            //????      
        }
    }

    return 0;
}

PPS:

I (think I) made some more progress. The following code seems to work:

using System.Drawing;
using System.Drawing.Imaging;
using Color = System.Drawing.Color;

namespace ClassLibrary1
{
  public unsafe class BitmapComparer : IBitmapComparer
  {
    private readonly Color[] _targetBitmapColors;
    private readonly int _width;
    private readonly int _height;
    private readonly int _maxPointerLength;
    private readonly PixelFormat _pixelFormat;

    public BitmapComparer(Bitmap targetBitmap)
    {
      _width = targetBitmap.Width;
      _height = targetBitmap.Height;
      _maxPointerLength = _width * _height;
      _pixelFormat = targetBitmap.PixelFormat;
      _targetBitmapColors = new Color[_maxPointerLength];

      var bData = targetBitmap.LockBits(new Rectangle(0, 0, _width, _height), ImageLockMode.ReadWrite, _pixelFormat);
      var scan0 = (byte*) bData.Scan0.ToPointer();

      for (var i = 0; i < _maxPointerLength; i += 4)
      {
        _targetBitmapColors[i] = Color.FromArgb(scan0[i + 2], scan0[i + 1], scan0[i + 0]);
      }

      targetBitmap.UnlockBits(bData);
    }

    // https://rogerjohansson.blog/2008/12/09/genetic-programming-mona-lisa-faq/
    public double Compare(Bitmap bitmapToCompareWith)
    {
      var bData = bitmapToCompareWith.LockBits(new Rectangle(0, 0, _width, _height), ImageLockMode.ReadWrite, _pixelFormat);
      var scan0 = (byte*) bData.Scan0.ToPointer();
      double distance = 0;

      for (var i = 0; i < _maxPointerLength; i += 4)
      {
        distance += 
                ( ((_targetBitmapColors[i].R - scan0[i + 2]) * (_targetBitmapColors[i].R - scan0[i + 2]))
                + ((_targetBitmapColors[i].G - scan0[i + 1]) * (_targetBitmapColors[i].G - scan0[i + 1]))
                + ((_targetBitmapColors[i].B - scan0[i + 0]) * (_targetBitmapColors[i].B - scan0[i + 0])));
      }

      bitmapToCompareWith.UnlockBits(bData);

      return distance;
    }
  }
}

Upvotes: 2

Views: 1306

Answers (2)

Todor Balabanov
Todor Balabanov

Reputation: 388

Using all pixels always will be time-consuming. What if you use a randomly selected pixels sample of the images. Also, you can apply hierarchical image granularity. In this way, you will get more information about the details presented in the images.

I am also working on a similar project. It is available in GitHub under the name Ellipses-Image-Approximator.

Something like this:

package eu.veldsoft.ellipses.image.approximator;

import java.awt.image.BufferedImage;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * Compare to raster images by using Euclidean distance between the pixels but
 * in probabilistic sampling on hierarchical image detailization.
 * 
 * @author Todor Balabanov
 */
class HierarchicalProbabilisticImageComparator implements ImageComparator {
    /** A pseudo-random number generator instance. */
    private static final Random PRNG = new Random();

    /**
     * Euclidean distance color comparator instance.
     */
    private static final ColorComparator EUCLIDEAN = new EuclideanColorComparator();

    /** Recursive descent depth level. */
    private int depthLevel = 1;

    /**
     * Size of the sample in percentages from the size of the population (from
     * 0.0 to 1.0).
     */
    private double samplePercent = 0.1;

    /** A supportive array for the first image pixels. */
    private int aPixels[] = null;

    /** A supportive array for the second image pixels. */
    private int bPixels[] = null;

    /**
     * Constructor without parameters for default members' values.
     */
    public HierarchicalProbabilisticImageComparator() {
        this(1, 0.1);
    }

    /**
     * Constructor with all parameters.
     * 
     * @param depthLevel
     *            Recursive descent depth level.
     * @param samplePercent
     *            Size of the sample in percentages from the size of the
     *            population (from 0.0 to 1.0).
     */
    public HierarchicalProbabilisticImageComparator(int depthLevel,
            double samplePercent) {
        super();

        this.depthLevel = depthLevel;
        this.samplePercent = samplePercent;
    }

    private double distance(int width, int level, int minX, int minY, int maxX,
            int maxY) {
        /*
         * At the bottom of the recursive descent, distance is zero, and
         * descending is canceled.
         */
        if (level > depthLevel) {
            return 0;
        }

        /* Rectangle's boundaries should be observed. */
        if (maxX <= minX || maxY <= minY) {
            return 0;
        }

        /*
         * Sample size calculated according formula.
         * 
         * https://www.surveymonkey.com/mp/sample-size-calculator/
         */
        int sampleSize = (int) ((maxX - minX) * (maxY - minY) * samplePercent);

        /* Generate unique indices of pixels with the size of the sample. */
        Set<Integer> indices = new HashSet<Integer>();
        while (indices.size() < sampleSize) {
            int x = minX + PRNG.nextInt(maxX - minX + 1);
            int y = minY + PRNG.nextInt(maxY - minY + 1);
            indices.add(y * width + x);
        }

        /* The Euclidean distance of the randomly selected pixels. */
        double sum = 0;
        for (int index : indices) {
            sum += EUCLIDEAN.distance(aPixels[index], bPixels[index]);
        }

        /* Do a recursive descent. */
        return (sum / sampleSize) * level
                + distance(width, level + 1, minX, minY,
                        maxX - (maxX - minX) / 2, maxY - (maxY - minY) / 2)
                + distance(width, level + 1, maxX - (maxX - minX) / 2, minY,
                        maxX, maxY - (maxY - minY) / 2)
                + distance(width, level + 1, minX, maxY - (maxY - minY) / 2,
                        maxX - (maxX - minX) / 2, maxY)
                + distance(width, level + 1, maxX - (maxX - minX) / 2,
                        maxY - (maxY - minY) / 2, maxX, maxY);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public double distance(BufferedImage a, BufferedImage b) {
        if (a.getWidth() != b.getWidth()) {
            throw new RuntimeException("Images width should be identical!");
        }

        if (a.getHeight() != b.getHeight()) {
            throw new RuntimeException("Images height should be identical!");
        }

        aPixels = a.getRGB(0, 0, a.getWidth(), a.getHeight(), null, 0,
                a.getWidth());

        bPixels = b.getRGB(0, 0, b.getWidth(), b.getHeight(), null, 0,
                b.getWidth());

        /* Do a recursive calculation. */
        return distance(Math.min(a.getWidth(), b.getWidth()), 1, 0, 0,
                Math.min(a.getWidth() - 1, b.getWidth() - 1),
                Math.min(a.getHeight() - 1, b.getHeight() - 1));
    }
}

Upvotes: 1

Kvam
Kvam

Reputation: 2218

As others have pointed out, you can use BitMap.LockBits and use pointers instead of GetPixel. The following runs about 200 times faster than the original approach:

static float CalculateDifference(Bitmap bitmap1, Bitmap bitmap2)
{
    if (bitmap1.Size != bitmap2.Size)
    {
        return -1;
    }

    var rectangle = new Rectangle(0, 0, bitmap1.Width, bitmap1.Height);

    BitmapData bitmapData1 = bitmap1.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap1.PixelFormat);
    BitmapData bitmapData2 = bitmap2.LockBits(rectangle, ImageLockMode.ReadOnly, bitmap2.PixelFormat);

    float diff = 0;
    var byteCount = rectangle.Width * rectangle.Height * 3;

    unsafe
    {
        // scan to first byte in bitmaps
        byte* pointer1 = (byte*)bitmapData1.Scan0.ToPointer();
        byte* pointer2 = (byte*)bitmapData2.Scan0.ToPointer();

        for (int x = 0; x < byteCount; x++)
        {
            diff += (float)Math.Abs(*pointer1 - *pointer2) / 255;
            pointer1++;
            pointer2++;
        }
    }

    bitmap1.UnlockBits(bitmapData1);
    bitmap2.UnlockBits(bitmapData2);

    return 100 * diff / byteCount;
}

Upvotes: 0

Related Questions