There may be different reasons why we would like to compare two images and find out the differences. I would say that two good uses could be motion detection and to find whether a second copy of an image has been altered compared with the first one.
This is a very simple program where I feed two slightly different images with the same dimensions and cycling through all the pixels, I find the differences between them and show them on a PictureBox using a color that will show a contrast.
When an object enters the view window of a surveillance camera, part of the image won't change because the object won't occupy the full window, or even if it does, there would be a completely different picture compared with a former one. That may trigger another function of a program. Who knows? A border or a silhouette would be handy, but that is another program. Here we are only comparing, Ok?
A camera would get raw images. That would be the best scenario, but a saved-to-file image could have differences due to compression algorithms.
On certain formats images are stored in compressed form. Two images of the same dimensions may compress to different file sizes depending on the byte values contained on it. This compression process modifies the image, so when it is displayed again it could have some bytes that have been changed, so it is not the same as the original. To Our eyes it may look ok, but at the byte level it has been degraded, some of the byte colors are similar on shade, but not exactly the same.
On the image, the picture on the left shows a large amount of differences, but at the moment I created the second image, I only added a red trace to the original and saved to another file.
If you have an image, let's say on *.jpge format, save a copy, then do an small editing and then save to another file. When they are displayed again they will show more differences than the one you just created. Give it a try!
On my program I have a mechanism to compensate a bit for it. I don't just compare all the bytes; I compare the difference between them with a tolerance. This gives me a certain accuracy, because the bytes not edited, usually don't change. You would have a better grasp of it when you start playing with the program.
As I said, this is a very simple program. One function to modify the pixel format and one routine to compare bytes. The rest is just bells and whistles.
Here is a view of the program. It has a menu strip, a TrackBar, a ToolStrip with a button, a label and three PictureBoxes on split panels. The panels have the autoscroll property set to true, the PictureBoxes 1 and 2 have the height fixed and the width calculated to keep proportions of resized images, and the PictureBox3 has the autosize property set to true.
In this view you will see the former position of the clouds on one color, and the next on another color. This happened just by accident. There is no way to know in which directions the changes were made at least you use date creation or date modified. If the image 'A' is different than the image 'B', then the image 'B' is different than the image 'A'. What I am comparing here is changes in color, so one color shows lighter and the other shows darker. I chose to display on inverted colors to show where the differences are, instead of repeating one of the images or only displaying the changes, so they can be located, but you could do it in any way you would prefer.
Here is the program:
Imports System.Drawing.Drawing2D Imports System.Drawing.Imaging Imports System.IO Imports System.Runtime.InteropServices
Class Level variables
Public Class Form1 Dim ToolTip1 As New ToolTip Dim W As Integer = 459 'Size of my initial image Dim H As Integer = 700 ' " Dim Ratio As Single ' To keep proportions . . .
Load Form and create Tooltips
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load ToolTip1.SetToolTip(TrackBar1, "Compensate For Image Compression Or Decompression") ToolTip1.SetToolTip(Label1, "Show Tolerance Value") End Sub
We need to load the two images to compare. So we open a file from the menu. It is loaded to PictureBox1. Then we open another file, it will also go to PictureBox1, but the image already on PictureBox1 is transferred to PictureBox2. So image 2 is the one loaded first, but it doesn't matter for our processing. Here you may prefer Drag-And-Drop, it would be easier to load. Look into it.
Private Sub OpenToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles OpenToolStripMenuItem.Click Dim FDialog As New OpenFileDialog Try With FDialog 'Select file extension filter .Filter = "Image Files|*.bmp;*.gif;*.jpg;*.png;*.tif" If .ShowDialog = DialogResult.OK Then 'Move a copy of the image from PictureBox1 to PictureBox2 PictureBox2.Image = PictureBox1.Image.Clone 'Read File Info And Assign Image to PictureBox1 PictureBox1.Image = Image.FromFile(.FileName) 'Find And Save Image Dimensions W = System.Drawing.Image.FromFile(.FileName).Width H = System.Drawing.Image.FromFile(.FileName).Height ' Keep Proportion On Resizing Ratio = CSng(W / H) 'Resize PictureBoxes to New dimensions PictureBox2.Height = 320 PictureBox1.Height = 320 PictureBox1.Width = CInt(320 * Ratio) PictureBox2.Width = CInt(320 * Ratio) 'Form's Title to show last image loaded and it's dimensions Me.Text = "Differ ->" & Path.GetFileName(.FileName) & " " & W & "x" & H End If End With Catch ex As Exception MessageBox.Show(ex.ToString) Finally If Not FDialog Is Nothing Then FDialog.Dispose() FDialog = Nothing End If End Try End Sub
When we open a file from the menu, the image dimensions are stored on the variables 'W' and 'H', with them we calculate the ratio between height and width, and resize the PictureBoxes 1 and 2 accordingly. Then we modify hte text of the form's title bar. And do a cleanup.
When we press the "Find Differences" button we call two functions:
And feed the two images to the process.
Private Sub btnOk_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnOK.Click 'Display Results On PictureBox3 PictureBox3.Image = Diff1(ConvertToARGB(PictureBox1.Image), ConvertToARGB(PictureBox2.Image)) 'Show It PictureBox3.Refresh() End Sub
The two images we want to compare could be on of any of several file types. Some of them won't allow certain processes like generating graphic objects needed to our program, due to palette indexing or other reasons. So we modify them by changing the pixel format with the function ConvertToARGB(); this way our program won't complain.
Public Shared Function ConvertToARGB(ByVal original As Bitmap) As Bitmap Dim newImage As New Bitmap(original.Width, original.Height, System.Drawing.Imaging.PixelFormat.Format32bppArgb) newImage.SetResolution(original.HorizontalResolution, original.VerticalResolution) Dim g As Graphics = Graphics.FromImage(newImage) g.DrawImageUnscaled(original, 0, 0) g.Dispose() Return newImage End Function
This function returns an image on a 32 bits per pixel with alpha channel. We apply it to the two images. On it we create a bitmap with the desired format and paint on it a new image.
The function Diff1(), is the one that produces our seeked results. The first thing it does with the images is to find out their dimensions. If they don't match it returns image 2 and exits. You can't compare the pixels if they are not arranged on the same way and they are not the same amount. If the pictures are already different in size, we have our result before comparing bytes.
If images dimensions are the same, then we create two data objects and isolate them on memory using lockbits. The lockbits process uses a rectangular selection of the image. We will use the whole of the images. And compare the bits of image one with the corresponding bits on image 2.
'Function to cycle through the two images pixels Private Function Diff1(ByVal imgx As Bitmap, ByVal imgY As Bitmap) As Image 'If the two images are of different dimension, tell so and exit function returning something to PictureBox3 If imgx.Size <> imgY.Size Then MessageBox.Show("Images Are Of Different Sizes") Return imgY End If 'Use LockBits for Processing Dim x As Integer 'Counter For Loop Try 'Get Data For Both Images For Processing For Rectangular Area(Full Image) Dim bmpSr1 As BitmapData = imgx.LockBits(New Rectangle(0, 0, W, H), ImageLockMode.ReadWrite, imgx.PixelFormat) Dim bmpSr2 As BitmapData = imgY.LockBits(New Rectangle(0, 0, W, H), ImageLockMode.ReadWrite, imgx.PixelFormat) 'Find Initial Point For Checking Dim ptrSr1 As IntPtr = bmpSr1.Scan0 Dim ptrSr2 As IntPtr = bmpSr2.Scan0 'Position of color bits and values of intensity and color for pixels On Images 1 and 2 Dim A, R, G, B, A1, A2 As Integer B = 0 G = 1 R = 2 A = 3 'Lenght of Data Dim bytesSr1 As Integer = bmpSr1.Stride * H Dim bytesSr2 As Integer = bmpSr2.Stride * H 'Type Of Data Dim rgbvaluesSr1(bytesSr1) As Byte Dim rgbvaluesSr2(bytesSr1) As Byte 'Lock Rectangular Section While Processing System.Runtime.InteropServices.Marshal.Copy(ptrSr1, rgbvaluesSr1, 0, bytesSr1) System.Runtime.InteropServices.Marshal.Copy(ptrSr2, rgbvaluesSr2, 0, bytesSr2) 'Loop Thru And Compare For x = 0 To bytesSr1 - 4 Step 4 'What Value For First Image A1 = CInt(rgbvaluesSr1(x + R)) + CInt(rgbvaluesSr1(x + G)) + CInt(rgbvaluesSr1(x + B)/>/>) 'What Value For Second Image A2 = CInt(rgbvaluesSr2(x + R)) + CInt(rgbvaluesSr2(x + G)) + CInt(rgbvaluesSr2(x + B)/>/>) 'Set The Alpha Value rgbvaluesSr2(x + A) = 255 'Compare Difference With A Tolerance If A1 - A2 > TrackBar1.Value Then 'If There Is A Difference Mark It On Second Image With A Color That Shows rgbvaluesSr2(x + R) = 0 rgbvaluesSr2(x + G) = 185 rgbvaluesSr2(x + B)/>/> = 255 ElseIf A2 - A1 > TrackBar1.Value Then 'Or With Another rgbvaluesSr2(x + R) = 255 rgbvaluesSr2(x + G) = 0 rgbvaluesSr2(x + B)/>/> = 200 Else 'If There Is No Difference Invert Color Of Pixel On Second Image rgbvaluesSr2(x + R) = 255 - rgbvaluesSr2(x + R) rgbvaluesSr2(x + G) = 255 - rgbvaluesSr2(x + G) rgbvaluesSr2(x + B)/>/> = 255 - rgbvaluesSr2(x + B)/>/> End If Next 'Loop Ends 'Copy Locked Data To Byte Array System.Runtime.InteropServices.Marshal.Copy(rgbvaluesSr2, 0, ptrSr2, bytesSr2) 'Unlock Both Data Objects imgx.UnlockBits(bmpSr1) imgY.UnlockBits(bmpSr1) 'And Return Image Return imgY Catch ex As Exception 'If There Was A Problem, Say So And Return Something To Image3 MessageBox.Show("There Was An Error") Return PictureBox1.Image End Try MessageBox.Show("Can't use" & PictureBox1.Image.PixelFormat.ToString()) Return imgx End Function
We locate the beginning of the two images and using a counter we move along the data. This counter increases 4 bits on every loop. Every bit represents the value of the 3 color components and the transparency. B-G-R-A (ARGB). So if we are at the green bit, the next green will be 4 bits away.
On a rectangular image we have the width and height on pixels, but to use the lockbits, we need another value that is the "Stride". Stride is the width multiplied by the bytes of the pixel: 4. It is the same as the width measured on bytes instead of pixels.
After, we find the two colors for each corresponding pixel on the 2 images by using A1 and A2:
A1 = CInt(rgbvaluesSr1(x + R)) + CInt(rgbvaluesSr1(x + G)) + CInt(rgbvaluesSr1(x + B)/>/>) A2 = CInt(rgbvaluesSr2(x + R)) + CInt(rgbvaluesSr2(x + G)) + CInt(rgbvaluesSr2(x + B)/>/>)
Substract them and compare the difference with the value of the TrackBar. If we didn't have the alterations introduced by the compression we could just compare one with the other, but this is not a perfect world as you may already have found out. Also I used two comparisons, for brighter or darker, to avoid the use of Math.Abs that slows down the process.
After finding our values I modify image2 with a color for a difference on the brighter direction and another on the darker. The rest is only inverted on color. When the loop has finished, we unlock our memory blocks and return a new image, to PictureBox3.
The tolerance set by the TrackBar value, produces a cleaner image. For a good quality image, it could be small, for a bad one it must be high. For my initial images it is set to 50, and still shows too many errors. For a good quality one, probably 10 will be good enough.
Private Sub TrackBar1_Scroll(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles TrackBar1.Scroll 'Show TrackBar's Value Label1.Text = TrackBar1.Value.ToString End Sub
End of program
Private Sub ExitToolStripMenuItem_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ExitToolStripMenuItem.Click Application.Exit() End Sub End Class
Thank you for reading and, please check the attached project.
Number of downloads: 930