├── .gitignore ├── LICENSE ├── README.md ├── docs └── images │ ├── diffImage.png │ ├── firstImage.png │ └── secondImage.png └── src ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── ImageDiff.sln ├── ImageDiff ├── AnalyzerTypes.cs ├── Analyzers │ ├── BitmapAnalyzerFactory.cs │ ├── CIE76Analyzer.cs │ ├── CIELab.cs │ ├── CIExyz.cs │ ├── ExactMatchAnalyzer.cs │ └── IBitmapAnalyzer.cs ├── BitmapComparer.cs ├── BoundingBoxModes.cs ├── BoundingBoxes │ ├── BoundingBoxIdentifierFactory.cs │ ├── IBoundingBoxIdentifier.cs │ ├── MultipleBoundingBoxIdentifier.cs │ └── SingleBoundingBoxIdentifer.cs ├── CompareOptions.cs ├── IImageComparer.cs ├── ImageDiff.csproj ├── LabelerTypes.cs ├── Labelers │ ├── BasicLabeler.cs │ ├── ConnectedComponentLabeler.cs │ ├── IDifferenceLabeler.cs │ └── LabelerFactory.cs └── Properties │ └── AssemblyInfo.cs └── ImageDiffTests ├── AnalyerFactoryTests.cs ├── BitmapComparerTests.cs ├── BoundingBoxFactoryTests.cs ├── ImageDiffTests.csproj ├── LabelerFactoryTests.cs ├── Properties └── AssemblyInfo.cs ├── images ├── TestImage1.png └── TestImage2.png └── packages.config /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | *_i.c 38 | *_p.c 39 | *_i.h 40 | *.ilk 41 | *.meta 42 | *.obj 43 | *.pch 44 | *.pdb 45 | *.pgc 46 | *.pgd 47 | *.rsp 48 | *.sbr 49 | *.tlb 50 | *.tli 51 | *.tlh 52 | *.tmp 53 | *.tmp_proj 54 | *.log 55 | *.vspscc 56 | *.vssscc 57 | .builds 58 | *.pidb 59 | *.svclog 60 | *.scc 61 | 62 | # Chutzpah Test files 63 | _Chutzpah* 64 | 65 | # Visual C++ cache files 66 | ipch/ 67 | *.aps 68 | *.ncb 69 | *.opensdf 70 | *.sdf 71 | *.cachefile 72 | 73 | # Visual Studio profiler 74 | *.psess 75 | *.vsp 76 | *.vspx 77 | 78 | # TFS 2012 Local Workspace 79 | $tf/ 80 | 81 | # Guidance Automation Toolkit 82 | *.gpState 83 | 84 | # ReSharper is a .NET coding add-in 85 | _ReSharper*/ 86 | *.[Rr]e[Ss]harper 87 | *.DotSettings.user 88 | 89 | # JustCode is a .NET coding addin-in 90 | .JustCode 91 | 92 | # TeamCity is a build add-in 93 | _TeamCity* 94 | 95 | # DotCover is a Code Coverage Tool 96 | *.dotCover 97 | 98 | # NCrunch 99 | _NCrunch_* 100 | .*crunch*.local.xml 101 | 102 | # MightyMoose 103 | *.mm.* 104 | AutoTest.Net/ 105 | 106 | # Web workbench (sass) 107 | .sass-cache/ 108 | 109 | # Installshield output folder 110 | [Ee]xpress/ 111 | 112 | # DocProject is a documentation generator add-in 113 | DocProject/buildhelp/ 114 | DocProject/Help/*.HxT 115 | DocProject/Help/*.HxC 116 | DocProject/Help/*.hhc 117 | DocProject/Help/*.hhk 118 | DocProject/Help/*.hhp 119 | DocProject/Help/Html2 120 | DocProject/Help/html 121 | 122 | # Click-Once directory 123 | publish/ 124 | 125 | # Publish Web Output 126 | *.[Pp]ublish.xml 127 | *.azurePubxml 128 | # TODO: Comment the next line if you want to checkin your web deploy settings 129 | # but database connection strings (with potential passwords) will be unencrypted 130 | *.pubxml 131 | *.publishproj 132 | 133 | # NuGet Packages 134 | *.nupkg 135 | # The packages folder can be ignored because of Package Restore 136 | **/packages/* 137 | # except build/, which is used as an MSBuild target. 138 | !**/packages/build/ 139 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 140 | #!**/packages/repositories.config 141 | 142 | # Windows Azure Build Output 143 | csx/ 144 | *.build.csdef 145 | 146 | # Windows Store app package directory 147 | AppPackages/ 148 | 149 | # Others 150 | sql/ 151 | *.Cache 152 | ClientBin/ 153 | [Ss]tyle[Cc]op.* 154 | ~$* 155 | *~ 156 | *.dbmdl 157 | *.dbproj.schemaview 158 | *.pfx 159 | *.publishsettings 160 | node_modules/ 161 | 162 | # RIA/Silverlight projects 163 | Generated_Code/ 164 | 165 | # Backup & report files from converting an old project file 166 | # to a newer Visual Studio version. Backup files are not needed, 167 | # because we have git ;-) 168 | _UpgradeReport_Files/ 169 | Backup*/ 170 | UpgradeLog*.XML 171 | UpgradeLog*.htm 172 | 173 | # SQL Server files 174 | *.mdf 175 | *.ldf 176 | 177 | # Business Intelligence projects 178 | *.rdl.data 179 | *.bim.layout 180 | *.bim_*.settings 181 | 182 | # Microsoft Fakes 183 | FakesAssemblies/ 184 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Richard Clement 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/qg5rrd1rc1ioetum?svg=true)](https://ci.appveyor.com/project/RichClement/imagediff) 2 | 3 | ImageDiff 4 | ========== 5 | 6 | A .Net library for comparing images and highlighting the differences. 7 | 8 | ImageDiff has three stages to its processing. 9 | 10 | 1. Analyze the provided images. 11 | 2. Detect and label the differences between the images. 12 | 3. Build the bounding boxes for the identified blobs. 13 | 14 | After processing is finished, it will generate a new image which is derived from the second image provided to the `Compare` method. This `diff` image will contain highlighted bounding boxes around the differences between the two images. 15 | 16 | ![first image](/docs/images/firstImage.png) compared to ![second image](/docs/images/secondImage.png) produces 17 | ![image diff](/docs/images/diffImage.png) 18 | 19 | Usage 20 | ----- 21 | 22 | Default usage: 23 | 24 | var firstImage = new Bitmap("path/to/first/image"); 25 | var secondImage = new Bitmap("path/to/second/image); 26 | 27 | var comparer = new BitmapComparer(); 28 | var diff = comparer.Compare(firstImage, secondImage); 29 | 30 | When initialized without options, the following values are used: 31 | 32 | - AnalyzerType: ExactMatch 33 | - Labeler: Basic 34 | - JustNoticeableDifference: 2.3 35 | - DetectionPadding: 2 36 | - BoundingBoxPadding: 2 37 | - BoundingBoxColor: Red 38 | - BoundingBoxMode: Single 39 | 40 | 41 | The compare object can be configured to use different settings for the different stages of processing. 42 | 43 | var options = new CompareOptions 44 | { 45 | AnalyzerType = AnalyzerTypes.CIE76, 46 | JustNoticableDifference = 2.3, 47 | DetectionPadding = 2, 48 | Labeler = LabelerTypes.ConnectedComponentLabeling, 49 | BoundingBoxColor = Color.Red, 50 | BoundingBoxPadding = 2, 51 | BoundingBoxMode = BoundingBoxModes.Multiple 52 | }; 53 | var comparer = new BitmapComparer(options); 54 | 55 | #### Analyzer Type 56 | Two forms of image analysis are currently supported: 57 | 58 | - ExactMatch - requires that the RGB values of each pixel in the image be equal. 59 | - CIE76 - follows the [color difference formula](http://en.wikipedia.org/wiki/Color_difference "color difference formula") to generate a Euclidean distance between the colors in the pixels and flags a difference when the Just Noticeable Difference (JND) is greater than a value of 2.3. 60 | 61 | #### Just Noticeable Difference 62 | Specify this to control how distant two pixels can be in the color space before they are marked as different. 63 | 64 | #### Detection Padding 65 | How many pixels away from the current pixel to look, for neighbors that should be grouped together for labeling purposes. 66 | 67 | #### Labeler 68 | Two forms of blob labeling are currently supported: 69 | 70 | - Basic - basic labeling will group all differences together into a single group. This labeling format does not support `BoundingBoxMode.Multiple`. 71 | - [Connected Component Labeling](http://en.wikipedia.org/wiki/Connected-component_labeling "Connected Component Labeling") - Uses a two-pass algorithm to label the differences in an image and then aggregate the labels. The Detection Padding option is used to determine how far to travel when checking neighbor pixels. 72 | 73 | #### Bounding Box Color 74 | The color of the bounding box to be drawn when highlighting detected differences. 75 | 76 | #### Bounding Box Padding 77 | The number of pixels of padding to include around the detected difference when drawing a bounding box. 78 | 79 | #### Bounding Box Mode 80 | Specifies how to build the bounding boxes when highlighting the detected differences. 81 | 82 | - Single - Only generate one bounding box that encompasses all of the detected differences in the image. 83 | - Multiple - Generate a bounding box around each separate group of detected differences. This bounding box mode is not supported by `LabelerTypes.Basic`. 84 | -------------------------------------------------------------------------------- /docs/images/diffImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/docs/images/diffImage.png -------------------------------------------------------------------------------- /docs/images/firstImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/docs/images/firstImage.png -------------------------------------------------------------------------------- /docs/images/secondImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/docs/images/secondImage.png -------------------------------------------------------------------------------- /src/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/src/.nuget/NuGet.exe -------------------------------------------------------------------------------- /src/.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | 32 | 33 | 34 | 35 | $(SolutionDir).nuget 36 | 37 | 38 | 39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config 40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config 41 | 42 | 43 | 44 | $(MSBuildProjectDirectory)\packages.config 45 | $(PackagesProjectConfig) 46 | 47 | 48 | 49 | 50 | $(NuGetToolsPath)\NuGet.exe 51 | @(PackageSource) 52 | 53 | "$(NuGetExePath)" 54 | mono --runtime=v4.0.30319 "$(NuGetExePath)" 55 | 56 | $(TargetDir.Trim('\\')) 57 | 58 | -RequireConsent 59 | -NonInteractive 60 | 61 | "$(SolutionDir) " 62 | "$(SolutionDir)" 63 | 64 | 65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir) 66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols 67 | 68 | 69 | 70 | RestorePackages; 71 | $(BuildDependsOn); 72 | 73 | 74 | 75 | 76 | $(BuildDependsOn); 77 | BuildPackage; 78 | 79 | 80 | 81 | 82 | 83 | 84 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 141 | 142 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /src/ImageDiff.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageDiff", "ImageDiff\ImageDiff.csproj", "{8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ImageDiffTests", "ImageDiffTests\ImageDiffTests.csproj", "{0B0B0D1B-490B-4A48-9911-A02930F89FD7}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{5D95F6AB-B756-4131-9BAF-E04B01958268}" 11 | ProjectSection(SolutionItems) = preProject 12 | .nuget\NuGet.Config = .nuget\NuGet.Config 13 | .nuget\NuGet.exe = .nuget\NuGet.exe 14 | .nuget\NuGet.targets = .nuget\NuGet.targets 15 | EndProjectSection 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {0B0B0D1B-490B-4A48-9911-A02930F89FD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {0B0B0D1B-490B-4A48-9911-A02930F89FD7}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {0B0B0D1B-490B-4A48-9911-A02930F89FD7}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {0B0B0D1B-490B-4A48-9911-A02930F89FD7}.Release|Any CPU.Build.0 = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(SolutionProperties) = preSolution 33 | HideSolutionNode = FALSE 34 | EndGlobalSection 35 | EndGlobal 36 | -------------------------------------------------------------------------------- /src/ImageDiff/AnalyzerTypes.cs: -------------------------------------------------------------------------------- 1 | namespace ImageDiff 2 | { 3 | public enum AnalyzerTypes 4 | { 5 | ExactMatch = 0, 6 | CIE76 = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/BitmapAnalyzerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImageDiff.Analyzers 4 | { 5 | internal static class BitmapAnalyzerFactory 6 | { 7 | public static IBitmapAnalyzer Create(AnalyzerTypes type, double justNoticeableDifference) 8 | { 9 | switch (type) 10 | { 11 | case AnalyzerTypes.ExactMatch: 12 | return new ExactMatchAnalyzer(); 13 | case AnalyzerTypes.CIE76: 14 | return new CIE76Analyzer(justNoticeableDifference); 15 | default: 16 | throw new ArgumentException(string.Format("Unrecognized Difference Detection Mode: {0}", type)); 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/CIE76Analyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace ImageDiff.Analyzers 5 | { 6 | internal class CIE76Analyzer : IBitmapAnalyzer 7 | { 8 | private double JustNoticeableDifference { get; set; } 9 | 10 | public CIE76Analyzer(double justNoticeableDifference) 11 | { 12 | JustNoticeableDifference = justNoticeableDifference; 13 | } 14 | 15 | public bool[,] Analyze(Bitmap first, Bitmap second) 16 | { 17 | var diff = new bool[first.Width, first.Height]; 18 | for (var x = 0; x < first.Width; x++) 19 | { 20 | for (var y = 0; y < first.Height; y++) 21 | { 22 | var firstLab = CIELab.FromRGB(first.GetPixel(x, y)); 23 | var secondLab = CIELab.FromRGB(second.GetPixel(x, y)); 24 | 25 | var score = Math.Sqrt(Math.Pow(secondLab.L - firstLab.L, 2) + 26 | Math.Pow(secondLab.a - firstLab.a, 2) + 27 | Math.Pow(secondLab.b - firstLab.b, 2)); 28 | 29 | diff[x, y] = (score >= JustNoticeableDifference); 30 | } 31 | } 32 | return diff; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/CIELab.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace ImageDiff.Analyzers 5 | { 6 | internal struct CIELab 7 | { 8 | public double L { get; set; } 9 | public double a { get; set; } 10 | public double b { get; set; } 11 | 12 | public CIELab(double l, double a, double b) 13 | : this() 14 | { 15 | this.L = l; 16 | this.a = a; 17 | this.b = b; 18 | } 19 | 20 | public static CIELab FromRGB(Color color) 21 | { 22 | return FromCIExyz(CIExyz.FromRGB(color)); 23 | } 24 | 25 | public static CIELab FromCIExyz(CIExyz xyzColor) 26 | { 27 | var transformedX = Transformxyz(xyzColor.x/CIExyz.RefX); 28 | var transformedY = Transformxyz(xyzColor.y/CIExyz.RefY); 29 | var transformedZ = Transformxyz(xyzColor.z/CIExyz.RefZ); 30 | 31 | var L = 116.0 * transformedY - 16; 32 | var a = 500.0 * (transformedX - transformedY); 33 | var b = 200.0 * (transformedY - transformedZ); 34 | 35 | return new CIELab(L, a, b); 36 | } 37 | 38 | private static double Transformxyz(double t) 39 | { 40 | return ((t > 0.008856) ? Math.Pow(t, (1.0 / 3.0)) : ((7.787 * t) + (16.0 / 116.0))); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/CIExyz.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace ImageDiff.Analyzers 5 | { 6 | internal struct CIExyz 7 | { 8 | 9 | public const double RefX = 95.047; 10 | public const double RefY = 100.000; 11 | public const double RefZ = 108.883; 12 | 13 | public double x { get; set; } 14 | public double y { get; set; } 15 | public double z { get; set; } 16 | 17 | public CIExyz(double x, double y, double z) 18 | : this() 19 | { 20 | this.x = x; 21 | this.y = y; 22 | this.z = z; 23 | } 24 | 25 | public static CIExyz FromRGB(Color color) 26 | { 27 | // normalize red, green, blue values 28 | var rLinear = color.R / 255.0; 29 | var gLinear = color.G / 255.0; 30 | var bLinear = color.B / 255.0; 31 | 32 | // convert to a sRGB form 33 | var r = (rLinear > 0.04045) ? Math.Pow((rLinear + 0.055) / (1.055), 2.4) : (rLinear / 12.92); 34 | var g = (gLinear > 0.04045) ? Math.Pow((gLinear + 0.055) / (1.055), 2.4) : (gLinear / 12.92); 35 | var b = (bLinear > 0.04045) ? Math.Pow((bLinear + 0.055) / (1.055), 2.4) : (bLinear / 12.92); 36 | 37 | // converts 38 | return new CIExyz((r * 0.4124 + g * 0.3576 + b * 0.1805), 39 | (r * 0.2126 + g * 0.7152 + b * 0.0722), 40 | (r * 0.0193 + g * 0.1192 + b * 0.9505)); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/ExactMatchAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | 4 | namespace ImageDiff.Analyzers 5 | { 6 | internal class ExactMatchAnalyzer : IBitmapAnalyzer 7 | { 8 | public bool[,] Analyze(Bitmap first, Bitmap second) 9 | { 10 | var diff = new bool[first.Width, first.Height]; 11 | for (var x = 0; x < first.Width; x++) 12 | { 13 | for (var y = 0; y < first.Height; y++) 14 | { 15 | var firstPixel = first.GetPixel(x, y); 16 | var secondPixel = second.GetPixel(x, y); 17 | if (firstPixel != secondPixel) 18 | { 19 | diff[x, y] = true; 20 | } 21 | } 22 | } 23 | return diff; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/ImageDiff/Analyzers/IBitmapAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace ImageDiff.Analyzers 4 | { 5 | internal interface IBitmapAnalyzer 6 | { 7 | bool[,] Analyze(Bitmap first, Bitmap second); 8 | } 9 | } -------------------------------------------------------------------------------- /src/ImageDiff/BitmapComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | using ImageDiff.Analyzers; 6 | using ImageDiff.BoundingBoxes; 7 | using ImageDiff.Labelers; 8 | 9 | namespace ImageDiff 10 | { 11 | public class BitmapComparer : IImageComparer 12 | { 13 | private LabelerTypes LabelerType { get; set; } 14 | private double JustNoticeableDifference { get; set; } 15 | private int DetectionPadding { get; set; } 16 | private int BoundingBoxPadding { get; set; } 17 | private Color BoundingBoxColor { get; set; } 18 | private BoundingBoxModes BoundingBoxMode { get; set; } 19 | private AnalyzerTypes AnalyzerType { get; set; } 20 | 21 | private IDifferenceLabeler Labeler { get; set; } 22 | private IBoundingBoxIdentifier BoundingBoxIdentifier { get; set; } 23 | private IBitmapAnalyzer BitmapAnalyzer { get; set; } 24 | 25 | public BitmapComparer(CompareOptions options = null) 26 | { 27 | if (options == null) 28 | { 29 | options = new CompareOptions(); 30 | } 31 | Initialize(options); 32 | 33 | BitmapAnalyzer = BitmapAnalyzerFactory.Create(AnalyzerType, JustNoticeableDifference); 34 | Labeler = LabelerFactory.Create(LabelerType, DetectionPadding); 35 | BoundingBoxIdentifier = BoundingBoxIdentifierFactory.Create(BoundingBoxMode, BoundingBoxPadding); 36 | } 37 | 38 | private void Initialize(CompareOptions options) 39 | { 40 | if (options.BoundingBoxPadding < 0) throw new ArgumentException("bounding box padding must be non-negative"); 41 | if (options.DetectionPadding < 0) throw new ArgumentException("detection padding must be non-negative"); 42 | 43 | LabelerType = options.Labeler; 44 | JustNoticeableDifference = options.JustNoticeableDifference; 45 | BoundingBoxColor = options.BoundingBoxColor; 46 | DetectionPadding = options.DetectionPadding; 47 | BoundingBoxPadding = options.BoundingBoxPadding; 48 | BoundingBoxMode = options.BoundingBoxMode; 49 | AnalyzerType = options.AnalyzerType; 50 | } 51 | 52 | public Bitmap Compare(Bitmap firstImage, Bitmap secondImage) 53 | { 54 | if (firstImage == null) throw new ArgumentNullException("firstImage"); 55 | if (secondImage == null) throw new ArgumentNullException("secondImage"); 56 | if (firstImage.Width != secondImage.Width || firstImage.Height != secondImage.Height) throw new ArgumentException("Bitmaps must be the same size."); 57 | 58 | var differenceMap = BitmapAnalyzer.Analyze(firstImage, secondImage); 59 | var differenceLabels = Labeler.Label(differenceMap); 60 | var boundingBoxes = BoundingBoxIdentifier.CreateBoundingBoxes(differenceLabels); 61 | var differenceBitmap = CreateImageWithBoundingBoxes(secondImage, boundingBoxes); 62 | return differenceBitmap; 63 | } 64 | 65 | public bool Equals(Bitmap firstImage, Bitmap secondImage) 66 | { 67 | if (firstImage == null && secondImage == null) return true; 68 | if (firstImage == null) return false; 69 | if (secondImage == null) return false; 70 | if (firstImage.Width != secondImage.Width || firstImage.Height != secondImage.Height) return false; 71 | 72 | var differenceMap = BitmapAnalyzer.Analyze(firstImage, secondImage); 73 | 74 | // differenceMap is a 2d array of boolean values, true represents a difference between the images 75 | // iterate over the dimensions of the array and look for a true value (difference) and return false 76 | for (var i = 0; i < differenceMap.GetLength(0); i++) 77 | { 78 | for (var j = 0; j < differenceMap.GetLength(1); j++) 79 | { 80 | if (differenceMap[i, j]) 81 | return false; 82 | } 83 | } 84 | return true; 85 | } 86 | 87 | private Bitmap CreateImageWithBoundingBoxes(Bitmap secondImage, IEnumerable boundingBoxes) 88 | { 89 | var differenceBitmap = secondImage.Clone() as Bitmap; 90 | if (differenceBitmap == null) throw new Exception("Could not copy secondImage"); 91 | 92 | var boundingRectangles = boundingBoxes.ToArray(); 93 | if (boundingRectangles.Length == 0) 94 | return differenceBitmap; 95 | 96 | using (var g = Graphics.FromImage(differenceBitmap)) 97 | { 98 | var pen = new Pen(BoundingBoxColor); 99 | foreach (var boundingRectangle in boundingRectangles) 100 | { 101 | g.DrawRectangle(pen, boundingRectangle); 102 | } 103 | } 104 | return differenceBitmap; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ImageDiff/BoundingBoxModes.cs: -------------------------------------------------------------------------------- 1 | namespace ImageDiff 2 | { 3 | public enum BoundingBoxModes 4 | { 5 | Single = 0, 6 | Multiple = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/ImageDiff/BoundingBoxes/BoundingBoxIdentifierFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImageDiff.BoundingBoxes 4 | { 5 | internal static class BoundingBoxIdentifierFactory 6 | { 7 | public static IBoundingBoxIdentifier Create(BoundingBoxModes mode, int padding) 8 | { 9 | switch (mode) 10 | { 11 | case BoundingBoxModes.Single: 12 | return new SingleBoundingBoxIdentifer(padding); 13 | case BoundingBoxModes.Multiple: 14 | return new MultipleBoundingBoxIdentifier(padding); 15 | default: 16 | throw new ArgumentException(string.Format("Unrecognized Bounding Box Mode: {0}", mode)); 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ImageDiff/BoundingBoxes/IBoundingBoxIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Drawing; 3 | 4 | namespace ImageDiff.BoundingBoxes 5 | { 6 | internal interface IBoundingBoxIdentifier 7 | { 8 | IEnumerable CreateBoundingBoxes(int[,] labelMap); 9 | } 10 | } -------------------------------------------------------------------------------- /src/ImageDiff/BoundingBoxes/MultipleBoundingBoxIdentifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | 6 | namespace ImageDiff.BoundingBoxes 7 | { 8 | internal class MultipleBoundingBoxIdentifier : IBoundingBoxIdentifier 9 | { 10 | private int Padding { get; set; } 11 | 12 | public MultipleBoundingBoxIdentifier(int padding) 13 | { 14 | Padding = padding; 15 | } 16 | 17 | public IEnumerable CreateBoundingBoxes(int[,] labelMap) 18 | { 19 | var boundedPoints = FindLabeledPointGroups(labelMap); 20 | var boundingRectangles = CreateBoundingBoxes(boundedPoints); 21 | return boundingRectangles; 22 | } 23 | 24 | private IEnumerable CreateBoundingBoxes(Dictionary> boundedPoints) 25 | { 26 | if (boundedPoints == null || boundedPoints.Count == 0) 27 | yield break; 28 | 29 | foreach (var kvp in boundedPoints) 30 | { 31 | var points = kvp.Value; 32 | var minPoint = new Point(points.Min(x => x.X), points.Min(y => y.Y)); 33 | var maxPoint = new Point(points.Max(x => x.X), points.Max(y => y.Y)); 34 | var rectangle = new Rectangle(minPoint.X - Padding, 35 | minPoint.Y - Padding, 36 | (maxPoint.X - minPoint.X) + (Padding * 2), 37 | (maxPoint.Y - minPoint.Y) + (Padding * 2)); 38 | 39 | yield return rectangle; 40 | } 41 | } 42 | 43 | private static Dictionary> FindLabeledPointGroups(int[,] labelMap) 44 | { 45 | var width = labelMap.GetLength(0); 46 | var height = labelMap.GetLength(1); 47 | 48 | var boundedPoints = new Dictionary>(); 49 | for (var x = 0; x < width; x++) 50 | { 51 | for (var y = 0; y < height; y++) 52 | { 53 | if (labelMap[x, y] == 0) continue; 54 | 55 | var label = labelMap[x, y]; 56 | if (!boundedPoints.ContainsKey(label)) 57 | boundedPoints.Add(label, new List { new Point(x, y) }); 58 | else 59 | boundedPoints[label].Add(new Point(x, y)); 60 | } 61 | } 62 | return boundedPoints; 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/ImageDiff/BoundingBoxes/SingleBoundingBoxIdentifer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | 6 | namespace ImageDiff.BoundingBoxes 7 | { 8 | internal class SingleBoundingBoxIdentifer : IBoundingBoxIdentifier 9 | { 10 | private int Padding { get; set; } 11 | 12 | public SingleBoundingBoxIdentifer(int padding) 13 | { 14 | Padding = padding; 15 | } 16 | 17 | public IEnumerable CreateBoundingBoxes(int[,] labelMap) 18 | { 19 | var points = FindLabeledPoints(labelMap); 20 | 21 | if (!points.Any()) 22 | return new List(); 23 | 24 | var minPoint = new Point(points.Min(x => x.X), points.Min(y => y.Y)); 25 | var maxPoint = new Point(points.Max(x => x.X), points.Max(y => y.Y)); 26 | 27 | var rectangle = new Rectangle(minPoint.X - Padding, 28 | minPoint.Y - Padding, 29 | (maxPoint.X - minPoint.X) + (Padding * 2), 30 | (maxPoint.Y - minPoint.Y) + (Padding * 2)); 31 | 32 | return new List { rectangle }; 33 | } 34 | 35 | private static List FindLabeledPoints(int[,] labelMap) 36 | { 37 | var width = labelMap.GetLength(0); 38 | var height = labelMap.GetLength(1); 39 | var points = new List(); 40 | for (var x = 0; x < width; x++) 41 | { 42 | for (var y = 0; y < height; y++) 43 | { 44 | if (labelMap[x, y] > 0) 45 | points.Add(new Point(x, y)); 46 | } 47 | } 48 | return points; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/ImageDiff/CompareOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace ImageDiff 4 | { 5 | public class CompareOptions 6 | { 7 | public AnalyzerTypes AnalyzerType { get; set; } 8 | public LabelerTypes Labeler { get; set; } 9 | public double JustNoticeableDifference { get; set; } 10 | public int DetectionPadding { get; set; } 11 | public int BoundingBoxPadding { get; set; } 12 | public Color BoundingBoxColor { get; set; } 13 | public BoundingBoxModes BoundingBoxMode { get; set; } 14 | 15 | public CompareOptions() 16 | { 17 | Labeler = LabelerTypes.Basic; 18 | JustNoticeableDifference = 2.3; 19 | DetectionPadding = 2; 20 | BoundingBoxPadding = 2; 21 | BoundingBoxColor = Color.Red; 22 | BoundingBoxMode = BoundingBoxModes.Single; 23 | AnalyzerType = AnalyzerTypes.ExactMatch; 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/ImageDiff/IImageComparer.cs: -------------------------------------------------------------------------------- 1 | using System.Drawing; 2 | 3 | namespace ImageDiff 4 | { 5 | public interface IImageComparer where T : Image 6 | { 7 | T Compare(T firstImage, T secondImage); 8 | bool Equals(T firstImage, T secondImage); 9 | } 10 | } -------------------------------------------------------------------------------- /src/ImageDiff/ImageDiff.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8C9C4ABC-4D3A-4DCD-AEE1-95E07E4FAE9F} 8 | Library 9 | Properties 10 | ImageDiff 11 | ImageDiff 12 | v4.5 13 | 512 14 | 15 | 16 | true 17 | full 18 | false 19 | bin\Debug\ 20 | DEBUG;TRACE 21 | prompt 22 | 4 23 | 24 | 25 | pdbonly 26 | true 27 | bin\Release\ 28 | TRACE 29 | prompt 30 | 4 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 68 | -------------------------------------------------------------------------------- /src/ImageDiff/LabelerTypes.cs: -------------------------------------------------------------------------------- 1 | namespace ImageDiff 2 | { 3 | public enum LabelerTypes 4 | { 5 | Basic = 0, 6 | ConnectedComponentLabeling = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/ImageDiff/Labelers/BasicLabeler.cs: -------------------------------------------------------------------------------- 1 | namespace ImageDiff.Labelers 2 | { 3 | internal class BasicLabeler : IDifferenceLabeler 4 | { 5 | public int[,] Label(bool[,] differenceMap) 6 | { 7 | var width = differenceMap.GetLength(0); 8 | var height = differenceMap.GetLength(1); 9 | var analyzedMap = new int[width, height]; 10 | 11 | for (var x = 0; x < width; x++) 12 | { 13 | for (var y = 0; y < height; y++) 14 | { 15 | if (differenceMap[x, y]) 16 | analyzedMap[x, y] = 1; 17 | } 18 | } 19 | return analyzedMap; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/ImageDiff/Labelers/ConnectedComponentLabeler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Drawing; 4 | using System.Linq; 5 | 6 | namespace ImageDiff.Labelers 7 | { 8 | internal class ConnectedComponentLabeler : IDifferenceLabeler 9 | { 10 | private int Padding { get; set; } 11 | private int[,] Labels { get; set; } 12 | private Dictionary> Linked { get; set; } 13 | 14 | public ConnectedComponentLabeler(int padding) 15 | { 16 | Padding = padding; 17 | Linked = new Dictionary>(); 18 | } 19 | 20 | public int[,] Label(bool[,] differenceMap) 21 | { 22 | var width = differenceMap.GetLength(0); 23 | var height = differenceMap.GetLength(1); 24 | 25 | Labels = new int[width, height]; 26 | 27 | var nextLabel = 1; 28 | for (var y = 0; y < height; y++) 29 | { 30 | for (var x = 0; x < width; x++) 31 | { 32 | if (differenceMap[x, y] == false) continue; 33 | 34 | var neighbors = Neighbors(differenceMap, x, y); 35 | if (neighbors == null || neighbors.Count == 0) 36 | { 37 | Linked.Add(nextLabel, new List { nextLabel }); 38 | Labels[x, y] = nextLabel; 39 | nextLabel += 1; 40 | } 41 | else 42 | { 43 | var neighborsLabels = NeighborsLabels(neighbors); 44 | Labels[x, y] = neighborsLabels.Min(); 45 | foreach (var label in neighborsLabels) 46 | { 47 | Linked[label] = Linked[label].Union(neighborsLabels).ToList(); 48 | } 49 | } 50 | } 51 | } 52 | 53 | // second pass 54 | for (var y = 0; y < height; y++) 55 | { 56 | for (var x = 0; x < width; x++) 57 | { 58 | var currentLabel = Labels[x, y]; 59 | if (currentLabel == 0) continue; 60 | 61 | Labels[x, y] = FindLowestEquivalentLabel(currentLabel); 62 | } 63 | } 64 | 65 | return Labels; 66 | } 67 | 68 | private int FindLowestEquivalentLabel(int currentLabel) 69 | { 70 | var equivalentLabels = Linked[currentLabel]; 71 | return equivalentLabels.Min(); 72 | } 73 | 74 | private List NeighborsLabels(IEnumerable neighbors) 75 | { 76 | return neighbors.Select(n => Labels[n.X, n.Y]).ToList(); 77 | } 78 | 79 | private List Neighbors(bool[,] bitmap, int x, int y) 80 | { 81 | var points = GenerateNeighbors(bitmap.GetLength(0), x, y); 82 | return points.Where(p => bitmap[p.X, p.Y]).ToList(); 83 | } 84 | 85 | private IEnumerable GenerateNeighbors(int width, int x, int y) 86 | { 87 | var points = new List(); 88 | var counter = 0; 89 | while (counter <= Padding) 90 | { 91 | var offset = counter + 1; 92 | 93 | if (x > counter) 94 | { 95 | points.Add(new Point(x - offset, y)); 96 | if (y > counter) points.Add(new Point(x - offset, y - offset)); 97 | } 98 | if (y > counter) 99 | { 100 | points.Add(new Point(x, y - offset)); 101 | if (x < (width - offset)) points.Add(new Point(x + offset, y - offset)); 102 | } 103 | counter += 1; 104 | } 105 | return points; 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /src/ImageDiff/Labelers/IDifferenceLabeler.cs: -------------------------------------------------------------------------------- 1 | namespace ImageDiff.Labelers 2 | { 3 | internal interface IDifferenceLabeler 4 | { 5 | int[,] Label(bool[,] differenceMap); 6 | } 7 | } -------------------------------------------------------------------------------- /src/ImageDiff/Labelers/LabelerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace ImageDiff.Labelers 4 | { 5 | internal static class LabelerFactory 6 | { 7 | public static IDifferenceLabeler Create(LabelerTypes types, int padding) 8 | { 9 | switch (types) 10 | { 11 | case LabelerTypes.Basic: 12 | return new BasicLabeler(); 13 | case LabelerTypes.ConnectedComponentLabeling: 14 | return new ConnectedComponentLabeler(padding); 15 | default: 16 | throw new ArgumentException(string.Format("Unrecognized Analyzer Type: {0}", types)); 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/ImageDiff/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ImageDiff")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ImageDiff")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | [assembly: InternalsVisibleTo("ImageDiffTests")] 18 | 19 | // Setting ComVisible to false makes the types in this assembly not visible 20 | // to COM components. If you need to access a type in this assembly from 21 | // COM, set the ComVisible attribute to true on that type. 22 | [assembly: ComVisible(false)] 23 | 24 | // The following GUID is for the ID of the typelib if this project is exposed to COM 25 | [assembly: Guid("4e462185-6889-4657-a2a9-b3540463fa51")] 26 | 27 | // Version information for an assembly consists of the following four values: 28 | // 29 | // Major Version 30 | // Minor Version 31 | // Build Number 32 | // Revision 33 | // 34 | // You can specify all the values or you can default the Build and Revision Numbers 35 | // by using the '*' as shown below: 36 | // [assembly: AssemblyVersion("1.0.*")] 37 | [assembly: AssemblyVersion("1.0.0.0")] 38 | [assembly: AssemblyFileVersion("1.0.0.0")] 39 | -------------------------------------------------------------------------------- /src/ImageDiffTests/AnalyerFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImageDiff; 3 | using ImageDiff.Analyzers; 4 | using NUnit.Framework; 5 | 6 | namespace ImageDiffTests 7 | { 8 | [TestFixture] 9 | public class AnalyerFactoryTests 10 | { 11 | [Test, ExpectedException(typeof(ArgumentException))] 12 | public void FactoryThrowsWithInvalidType() 13 | { 14 | BitmapAnalyzerFactory.Create((AnalyzerTypes)100, 2.3); 15 | } 16 | 17 | [Test] 18 | public void FactoryCreatesExactMatchAnalyzer() 19 | { 20 | var target = BitmapAnalyzerFactory.Create(AnalyzerTypes.ExactMatch, 2.3); 21 | Assert.IsInstanceOf(typeof(ExactMatchAnalyzer), target); 22 | } 23 | 24 | [Test] 25 | public void FactoryCreatesCIE76Analyzer() 26 | { 27 | var target = BitmapAnalyzerFactory.Create(AnalyzerTypes.CIE76, 2.3); 28 | Assert.IsInstanceOf(typeof(CIE76Analyzer), target); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ImageDiffTests/BitmapComparerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Drawing; 3 | using ImageDiff; 4 | using NUnit.Framework; 5 | 6 | namespace ImageDiffTests 7 | { 8 | [TestFixture] 9 | public class BitmapComparerTests 10 | { 11 | protected const string TestImage1 = "./images/TestImage1.png"; 12 | protected const string TestImage2 = "./images/TestImage2.png"; 13 | protected const string OutputFormat = "output_{0}.png"; 14 | protected Bitmap FirstImage { get; set; } 15 | protected Bitmap SecondImage { get; set; } 16 | 17 | [SetUp] 18 | public void Setup() 19 | { 20 | FirstImage = new Bitmap(TestImage1); 21 | SecondImage = new Bitmap(TestImage2); 22 | } 23 | 24 | [Test, ExpectedException(typeof(ArgumentNullException))] 25 | public void CompareThrowsWhenFirstImageIsNull() 26 | { 27 | var target = new BitmapComparer(null); 28 | target.Compare(null, SecondImage); 29 | } 30 | 31 | [Test, ExpectedException(typeof(ArgumentNullException))] 32 | public void CompareThrowsWhenSecondImageIsNull() 33 | { 34 | var target = new BitmapComparer(null); 35 | target.Compare(FirstImage, null); 36 | } 37 | 38 | [Test, ExpectedException(typeof(ArgumentException))] 39 | public void CompareThrowsWhenImagesAreNotSameWidth() 40 | { 41 | var firstBitmap = new Bitmap(10, 10); 42 | var secondBitmap = new Bitmap(20, 10); 43 | 44 | var target = new BitmapComparer(null); 45 | target.Compare(firstBitmap, secondBitmap); 46 | } 47 | 48 | [Test, ExpectedException(typeof(ArgumentException))] 49 | public void CompareThrowsWhenImagesAreNotSameHeight() 50 | { 51 | var firstBitmap = new Bitmap(10, 10); 52 | var secondBitmap = new Bitmap(10, 20); 53 | 54 | var target = new BitmapComparer(null); 55 | target.Compare(firstBitmap, secondBitmap); 56 | } 57 | 58 | [Test, ExpectedException(typeof(ArgumentException))] 59 | public void ImageDiffThrowsWhenBoundingBoxPaddingIsLessThanZero() 60 | { 61 | var target = new BitmapComparer(new CompareOptions 62 | { 63 | BoundingBoxColor = Color.Red, 64 | BoundingBoxMode = BoundingBoxModes.Single, 65 | AnalyzerType = AnalyzerTypes.ExactMatch, 66 | DetectionPadding = 2, 67 | BoundingBoxPadding = -2, 68 | Labeler = LabelerTypes.Basic 69 | }); 70 | } 71 | 72 | [Test, ExpectedException(typeof(ArgumentException))] 73 | public void ImageDiffThrowsWhenDetectionPaddingIsLessThanZero() 74 | { 75 | var target = new BitmapComparer(new CompareOptions 76 | { 77 | BoundingBoxColor = Color.Red, 78 | BoundingBoxMode = BoundingBoxModes.Single, 79 | AnalyzerType = AnalyzerTypes.ExactMatch, 80 | DetectionPadding = -2, 81 | BoundingBoxPadding = 2, 82 | Labeler = LabelerTypes.Basic 83 | }); 84 | } 85 | 86 | [Test] 87 | public void CompareWorksWithNoOptions() 88 | { 89 | var target = new BitmapComparer(); 90 | var result = target.Compare(FirstImage, SecondImage); 91 | result.Save(string.Format(OutputFormat, "CompareWorksWithNullOptions"), SecondImage.RawFormat); 92 | } 93 | 94 | [Test] 95 | public void CompareWorksWithNullOptions() 96 | { 97 | var target = new BitmapComparer(null); 98 | var result = target.Compare(FirstImage, SecondImage); 99 | result.Save(string.Format(OutputFormat, "CompareWorksWithNullOptions"), SecondImage.RawFormat); 100 | } 101 | 102 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 103 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 104 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 105 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 106 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 107 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 108 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 109 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 110 | public void CompareWorksWithIdenticalImages(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 111 | { 112 | var target = new BitmapComparer(new CompareOptions 113 | { 114 | AnalyzerType = aType, 115 | BoundingBoxMode = bMode, 116 | Labeler = lType 117 | }); 118 | var result = target.Compare(FirstImage, FirstImage); 119 | result.Save(string.Format(OutputFormat, string.Format("CompareWorksWithIdenticalImages_{0}_{1}_{2}", aType, bMode, lType)), 120 | SecondImage.RawFormat); 121 | } 122 | 123 | 124 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 125 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 126 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 127 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 128 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 129 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 130 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 131 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 132 | public void CompareWorksWithDifferentImages(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 133 | { 134 | var target = new BitmapComparer(new CompareOptions 135 | { 136 | BoundingBoxColor = Color.Red, 137 | BoundingBoxMode = bMode, 138 | AnalyzerType = aType, 139 | DetectionPadding = 2, 140 | BoundingBoxPadding = 2, 141 | Labeler = lType 142 | }); 143 | var result = target.Compare(FirstImage, SecondImage); 144 | result.Save(string.Format(OutputFormat, string.Format("CompareWorksWithDifferentImages_{0}_{1}_{2}", aType, bMode, lType)), SecondImage.RawFormat); 145 | } 146 | 147 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 148 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 149 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 150 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 151 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 152 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 153 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 154 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 155 | public void EqualsReturnsTrueWithSameImage(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 156 | { 157 | var target = new BitmapComparer(new CompareOptions 158 | { 159 | BoundingBoxColor = Color.Red, 160 | BoundingBoxMode = bMode, 161 | AnalyzerType = aType, 162 | DetectionPadding = 2, 163 | BoundingBoxPadding = 2, 164 | Labeler = lType 165 | }); 166 | var newInstanceOfFirstImage = new Bitmap(TestImage1); 167 | var result = target.Equals(FirstImage, newInstanceOfFirstImage); 168 | Assert.IsTrue(result); 169 | } 170 | 171 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 172 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 173 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 174 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 175 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 176 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 177 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 178 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 179 | public void EqualsReturnsFalseWithDifferentImage(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 180 | { 181 | var target = new BitmapComparer(new CompareOptions 182 | { 183 | BoundingBoxColor = Color.Red, 184 | BoundingBoxMode = bMode, 185 | AnalyzerType = aType, 186 | DetectionPadding = 2, 187 | BoundingBoxPadding = 2, 188 | Labeler = lType 189 | }); 190 | var result = target.Equals(FirstImage, SecondImage); 191 | Assert.IsFalse(result); 192 | } 193 | 194 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 195 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 196 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 197 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 198 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 199 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 200 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 201 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 202 | public void EqualsReturnsTrueWithNullImages(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 203 | { 204 | var target = new BitmapComparer(new CompareOptions 205 | { 206 | BoundingBoxColor = Color.Red, 207 | BoundingBoxMode = bMode, 208 | AnalyzerType = aType, 209 | DetectionPadding = 2, 210 | BoundingBoxPadding = 2, 211 | Labeler = lType 212 | }); 213 | var result = target.Equals(null, null); 214 | Assert.IsTrue(result); 215 | } 216 | 217 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 218 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 219 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 220 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 221 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 222 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 223 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 224 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 225 | public void EqualsReturnsFalseWithNullFirstImage(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 226 | { 227 | var target = new BitmapComparer(new CompareOptions 228 | { 229 | BoundingBoxColor = Color.Red, 230 | BoundingBoxMode = bMode, 231 | AnalyzerType = aType, 232 | DetectionPadding = 2, 233 | BoundingBoxPadding = 2, 234 | Labeler = lType 235 | }); 236 | var result = target.Equals(null, SecondImage); 237 | Assert.IsFalse(result); 238 | } 239 | 240 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.Basic)] 241 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 242 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 243 | [TestCase(AnalyzerTypes.CIE76, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 244 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.Basic)] 245 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Single, LabelerTypes.ConnectedComponentLabeling)] 246 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.Basic)] 247 | [TestCase(AnalyzerTypes.ExactMatch, BoundingBoxModes.Multiple, LabelerTypes.ConnectedComponentLabeling)] 248 | public void EqualsReturnsFalseWithNullSecondImage(AnalyzerTypes aType, BoundingBoxModes bMode, LabelerTypes lType) 249 | { 250 | var target = new BitmapComparer(new CompareOptions 251 | { 252 | BoundingBoxColor = Color.Red, 253 | BoundingBoxMode = bMode, 254 | AnalyzerType = aType, 255 | DetectionPadding = 2, 256 | BoundingBoxPadding = 2, 257 | Labeler = lType 258 | }); 259 | var result = target.Equals(FirstImage, null); 260 | Assert.IsFalse(result); 261 | } 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/ImageDiffTests/BoundingBoxFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImageDiff; 3 | using ImageDiff.BoundingBoxes; 4 | using NUnit.Framework; 5 | 6 | namespace ImageDiffTests 7 | { 8 | [TestFixture] 9 | public class BoundingBoxFactoryTests 10 | { 11 | [Test, ExpectedException(typeof(ArgumentException))] 12 | public void FactoryThrowsWithInvalidType() 13 | { 14 | BoundingBoxIdentifierFactory.Create((BoundingBoxModes)100, 0); 15 | } 16 | 17 | [Test] 18 | public void FactoryCreatesSingleBoundingBoxIdentifier() 19 | { 20 | var target = BoundingBoxIdentifierFactory.Create(BoundingBoxModes.Single, 0); 21 | Assert.IsInstanceOf(typeof(SingleBoundingBoxIdentifer), target); 22 | } 23 | 24 | [Test] 25 | public void FactoryCreatesMultipleBoundingBoxIdentifier() 26 | { 27 | var target = BoundingBoxIdentifierFactory.Create(BoundingBoxModes.Multiple, 0); 28 | Assert.IsInstanceOf(typeof(MultipleBoundingBoxIdentifier), target); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ImageDiffTests/ImageDiffTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {0B0B0D1B-490B-4A48-9911-A02930F89FD7} 8 | Library 9 | Properties 10 | ImageDiffTests 11 | ImageDiffTests 12 | v4.5 13 | 512 14 | ..\ 15 | true 16 | 17 | 18 | true 19 | full 20 | false 21 | bin\Debug\ 22 | DEBUG;TRACE 23 | prompt 24 | 4 25 | 26 | 27 | pdbonly 28 | true 29 | bin\Release\ 30 | TRACE 31 | prompt 32 | 4 33 | 34 | 35 | 36 | False 37 | ..\packages\NUnit.2.6.4\lib\nunit.framework.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {8c9c4abc-4d3a-4dcd-aee1-95e07e4fae9f} 59 | ImageDiff 60 | 61 | 62 | 63 | 64 | Always 65 | 66 | 67 | Always 68 | 69 | 70 | 71 | 72 | 73 | 74 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 75 | 76 | 77 | 78 | 85 | -------------------------------------------------------------------------------- /src/ImageDiffTests/LabelerFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using ImageDiff; 3 | using ImageDiff.Labelers; 4 | using NUnit.Framework; 5 | 6 | namespace ImageDiffTests 7 | { 8 | [TestFixture] 9 | public class LabelerFactoryTests 10 | { 11 | [Test, ExpectedException(typeof(ArgumentException))] 12 | public void FactoryThrowsWithInvalidType() 13 | { 14 | LabelerFactory.Create((LabelerTypes)100, 0); 15 | } 16 | 17 | [Test] 18 | public void FactoryCreatesBasicLabeler() 19 | { 20 | var target = LabelerFactory.Create(LabelerTypes.Basic, 0); 21 | Assert.IsInstanceOf(typeof(BasicLabeler), target); 22 | } 23 | 24 | [Test] 25 | public void FactoryCreatesConnectedComponentLabeler() 26 | { 27 | var target = LabelerFactory.Create(LabelerTypes.ConnectedComponentLabeling, 0); 28 | Assert.IsInstanceOf(typeof(ConnectedComponentLabeler), target); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/ImageDiffTests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ImageDiffTests")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ImageDiffTests")] 13 | [assembly: AssemblyCopyright("")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("157d2932-0b07-4b90-adaf-f7421c728ae0")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/ImageDiffTests/images/TestImage1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/src/ImageDiffTests/images/TestImage1.png -------------------------------------------------------------------------------- /src/ImageDiffTests/images/TestImage2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/richclement/ImageDiff/953d4a8bf389475f103003a7e455f80885cab8af/src/ImageDiffTests/images/TestImage2.png -------------------------------------------------------------------------------- /src/ImageDiffTests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | --------------------------------------------------------------------------------